shinestacker 1.5.0__tar.gz → 1.5.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of shinestacker might be problematic. Click here for more details.

Files changed (152) hide show
  1. {shinestacker-1.5.0 → shinestacker-1.5.2}/CHANGELOG.md +31 -4
  2. {shinestacker-1.5.0/src/shinestacker.egg-info → shinestacker-1.5.2}/PKG-INFO +1 -1
  3. shinestacker-1.5.2/src/shinestacker/_version.py +1 -0
  4. shinestacker-1.5.2/src/shinestacker/app/args.py +23 -0
  5. {shinestacker-1.5.0 → shinestacker-1.5.2}/src/shinestacker/app/main.py +17 -8
  6. {shinestacker-1.5.0 → shinestacker-1.5.2}/src/shinestacker/app/project.py +2 -3
  7. {shinestacker-1.5.0 → shinestacker-1.5.2}/src/shinestacker/app/retouch.py +8 -4
  8. {shinestacker-1.5.0 → shinestacker-1.5.2}/src/shinestacker/config/gui_constants.py +2 -2
  9. {shinestacker-1.5.0 → shinestacker-1.5.2}/src/shinestacker/gui/new_project.py +17 -14
  10. {shinestacker-1.5.0 → shinestacker-1.5.2}/src/shinestacker/retouch/base_filter.py +59 -35
  11. {shinestacker-1.5.0 → shinestacker-1.5.2}/src/shinestacker/retouch/brush_preview.py +29 -15
  12. {shinestacker-1.5.0 → shinestacker-1.5.2}/src/shinestacker/retouch/denoise_filter.py +4 -3
  13. {shinestacker-1.5.0 → shinestacker-1.5.2}/src/shinestacker/retouch/display_manager.py +14 -19
  14. shinestacker-1.5.2/src/shinestacker/retouch/filter_manager.py +20 -0
  15. {shinestacker-1.5.0 → shinestacker-1.5.2}/src/shinestacker/retouch/image_editor_ui.py +48 -96
  16. {shinestacker-1.5.0 → shinestacker-1.5.2}/src/shinestacker/retouch/image_viewer.py +31 -27
  17. {shinestacker-1.5.0 → shinestacker-1.5.2}/src/shinestacker/retouch/io_gui_handler.py +0 -3
  18. {shinestacker-1.5.0 → shinestacker-1.5.2}/src/shinestacker/retouch/overlaid_view.py +38 -17
  19. {shinestacker-1.5.0 → shinestacker-1.5.2}/src/shinestacker/retouch/shortcuts_help.py +35 -31
  20. {shinestacker-1.5.0 → shinestacker-1.5.2}/src/shinestacker/retouch/sidebyside_view.py +57 -76
  21. {shinestacker-1.5.0 → shinestacker-1.5.2}/src/shinestacker/retouch/unsharp_mask_filter.py +5 -4
  22. {shinestacker-1.5.0 → shinestacker-1.5.2}/src/shinestacker/retouch/view_strategy.py +176 -89
  23. {shinestacker-1.5.0 → shinestacker-1.5.2}/src/shinestacker/retouch/vignetting_filter.py +4 -3
  24. {shinestacker-1.5.0 → shinestacker-1.5.2}/src/shinestacker/retouch/white_balance_filter.py +62 -17
  25. {shinestacker-1.5.0 → shinestacker-1.5.2/src/shinestacker.egg-info}/PKG-INFO +1 -1
  26. {shinestacker-1.5.0 → shinestacker-1.5.2}/src/shinestacker.egg-info/SOURCES.txt +1 -0
  27. shinestacker-1.5.0/src/shinestacker/_version.py +0 -1
  28. shinestacker-1.5.0/src/shinestacker/retouch/filter_manager.py +0 -12
  29. {shinestacker-1.5.0 → shinestacker-1.5.2}/.coveragerc +0 -0
  30. {shinestacker-1.5.0 → shinestacker-1.5.2}/.flake8 +0 -0
  31. {shinestacker-1.5.0 → shinestacker-1.5.2}/.github/workflows/ci-multiplatform.yml +0 -0
  32. {shinestacker-1.5.0 → shinestacker-1.5.2}/.github/workflows/pylint.yml +0 -0
  33. {shinestacker-1.5.0 → shinestacker-1.5.2}/.github/workflows/pypi-publish.yml +0 -0
  34. {shinestacker-1.5.0 → shinestacker-1.5.2}/.github/workflows/release.yml +0 -0
  35. {shinestacker-1.5.0 → shinestacker-1.5.2}/.gitignore +0 -0
  36. {shinestacker-1.5.0 → shinestacker-1.5.2}/.pylintrc +0 -0
  37. {shinestacker-1.5.0 → shinestacker-1.5.2}/.readthedocs.yaml +0 -0
  38. {shinestacker-1.5.0 → shinestacker-1.5.2}/LICENSE +0 -0
  39. {shinestacker-1.5.0 → shinestacker-1.5.2}/MANIFEST.in +0 -0
  40. {shinestacker-1.5.0 → shinestacker-1.5.2}/README.md +0 -0
  41. {shinestacker-1.5.0 → shinestacker-1.5.2}/THIRD_PARTY_LICENSES.txt +0 -0
  42. {shinestacker-1.5.0 → shinestacker-1.5.2}/docs/alignment.md +0 -0
  43. {shinestacker-1.5.0 → shinestacker-1.5.2}/docs/api.md +0 -0
  44. {shinestacker-1.5.0 → shinestacker-1.5.2}/docs/balancing.md +0 -0
  45. {shinestacker-1.5.0 → shinestacker-1.5.2}/docs/conf.py +0 -0
  46. {shinestacker-1.5.0 → shinestacker-1.5.2}/docs/focus_stacking.md +0 -0
  47. {shinestacker-1.5.0 → shinestacker-1.5.2}/docs/gui.md +0 -0
  48. {shinestacker-1.5.0 → shinestacker-1.5.2}/docs/index.md +0 -0
  49. {shinestacker-1.5.0 → shinestacker-1.5.2}/docs/job.md +0 -0
  50. {shinestacker-1.5.0 → shinestacker-1.5.2}/docs/main.md +0 -0
  51. {shinestacker-1.5.0 → shinestacker-1.5.2}/docs/multilayer.md +0 -0
  52. {shinestacker-1.5.0 → shinestacker-1.5.2}/docs/noise.md +0 -0
  53. {shinestacker-1.5.0 → shinestacker-1.5.2}/docs/requirements.txt +0 -0
  54. {shinestacker-1.5.0 → shinestacker-1.5.2}/docs/vignetting.md +0 -0
  55. {shinestacker-1.5.0 → shinestacker-1.5.2}/img/coffee.gif +0 -0
  56. {shinestacker-1.5.0 → shinestacker-1.5.2}/img/coffee_stack.jpg +0 -0
  57. {shinestacker-1.5.0 → shinestacker-1.5.2}/img/extreme-vignetting.jpg +0 -0
  58. {shinestacker-1.5.0 → shinestacker-1.5.2}/img/flies.gif +0 -0
  59. {shinestacker-1.5.0 → shinestacker-1.5.2}/img/flies_stack.jpg +0 -0
  60. {shinestacker-1.5.0 → shinestacker-1.5.2}/img/flow-diagram.png +0 -0
  61. {shinestacker-1.5.0 → shinestacker-1.5.2}/img/gui-finder.png +0 -0
  62. {shinestacker-1.5.0 → shinestacker-1.5.2}/img/gui-project-new.png +0 -0
  63. {shinestacker-1.5.0 → shinestacker-1.5.2}/img/gui-project-run.png +0 -0
  64. {shinestacker-1.5.0 → shinestacker-1.5.2}/img/gui-retouch.png +0 -0
  65. {shinestacker-1.5.0 → shinestacker-1.5.2}/index.html +0 -0
  66. {shinestacker-1.5.0 → shinestacker-1.5.2}/pyproject.toml +0 -0
  67. {shinestacker-1.5.0 → shinestacker-1.5.2}/requirements.txt +0 -0
  68. {shinestacker-1.5.0 → shinestacker-1.5.2}/scripts/build_release.py +0 -0
  69. {shinestacker-1.5.0 → shinestacker-1.5.2}/scripts/git-rev-list.sh +0 -0
  70. {shinestacker-1.5.0 → shinestacker-1.5.2}/scripts/validate-tomli.py +0 -0
  71. {shinestacker-1.5.0 → shinestacker-1.5.2}/setup.cfg +0 -0
  72. {shinestacker-1.5.0 → shinestacker-1.5.2}/src/shinestacker/__init__.py +0 -0
  73. {shinestacker-1.5.0 → shinestacker-1.5.2}/src/shinestacker/algorithms/__init__.py +0 -0
  74. {shinestacker-1.5.0 → shinestacker-1.5.2}/src/shinestacker/algorithms/align.py +0 -0
  75. {shinestacker-1.5.0 → shinestacker-1.5.2}/src/shinestacker/algorithms/align_auto.py +0 -0
  76. {shinestacker-1.5.0 → shinestacker-1.5.2}/src/shinestacker/algorithms/align_parallel.py +0 -0
  77. {shinestacker-1.5.0 → shinestacker-1.5.2}/src/shinestacker/algorithms/balance.py +0 -0
  78. {shinestacker-1.5.0 → shinestacker-1.5.2}/src/shinestacker/algorithms/base_stack_algo.py +0 -0
  79. {shinestacker-1.5.0 → shinestacker-1.5.2}/src/shinestacker/algorithms/denoise.py +0 -0
  80. {shinestacker-1.5.0 → shinestacker-1.5.2}/src/shinestacker/algorithms/depth_map.py +0 -0
  81. {shinestacker-1.5.0 → shinestacker-1.5.2}/src/shinestacker/algorithms/exif.py +0 -0
  82. {shinestacker-1.5.0 → shinestacker-1.5.2}/src/shinestacker/algorithms/multilayer.py +0 -0
  83. {shinestacker-1.5.0 → shinestacker-1.5.2}/src/shinestacker/algorithms/noise_detection.py +0 -0
  84. {shinestacker-1.5.0 → shinestacker-1.5.2}/src/shinestacker/algorithms/pyramid.py +0 -0
  85. {shinestacker-1.5.0 → shinestacker-1.5.2}/src/shinestacker/algorithms/pyramid_auto.py +0 -0
  86. {shinestacker-1.5.0 → shinestacker-1.5.2}/src/shinestacker/algorithms/pyramid_tiles.py +0 -0
  87. {shinestacker-1.5.0 → shinestacker-1.5.2}/src/shinestacker/algorithms/sharpen.py +0 -0
  88. {shinestacker-1.5.0 → shinestacker-1.5.2}/src/shinestacker/algorithms/stack.py +0 -0
  89. {shinestacker-1.5.0 → shinestacker-1.5.2}/src/shinestacker/algorithms/stack_framework.py +0 -0
  90. {shinestacker-1.5.0 → shinestacker-1.5.2}/src/shinestacker/algorithms/utils.py +0 -0
  91. {shinestacker-1.5.0 → shinestacker-1.5.2}/src/shinestacker/algorithms/vignetting.py +0 -0
  92. {shinestacker-1.5.0 → shinestacker-1.5.2}/src/shinestacker/algorithms/white_balance.py +0 -0
  93. {shinestacker-1.5.0 → shinestacker-1.5.2}/src/shinestacker/app/__init__.py +0 -0
  94. {shinestacker-1.5.0 → shinestacker-1.5.2}/src/shinestacker/app/about_dialog.py +0 -0
  95. {shinestacker-1.5.0 → shinestacker-1.5.2}/src/shinestacker/app/gui_utils.py +0 -0
  96. {shinestacker-1.5.0 → shinestacker-1.5.2}/src/shinestacker/app/help_menu.py +0 -0
  97. {shinestacker-1.5.0 → shinestacker-1.5.2}/src/shinestacker/app/open_frames.py +0 -0
  98. {shinestacker-1.5.0 → shinestacker-1.5.2}/src/shinestacker/config/__init__.py +0 -0
  99. {shinestacker-1.5.0 → shinestacker-1.5.2}/src/shinestacker/config/config.py +0 -0
  100. {shinestacker-1.5.0 → shinestacker-1.5.2}/src/shinestacker/config/constants.py +0 -0
  101. {shinestacker-1.5.0 → shinestacker-1.5.2}/src/shinestacker/core/__init__.py +0 -0
  102. {shinestacker-1.5.0 → shinestacker-1.5.2}/src/shinestacker/core/colors.py +0 -0
  103. {shinestacker-1.5.0 → shinestacker-1.5.2}/src/shinestacker/core/core_utils.py +0 -0
  104. {shinestacker-1.5.0 → shinestacker-1.5.2}/src/shinestacker/core/exceptions.py +0 -0
  105. {shinestacker-1.5.0 → shinestacker-1.5.2}/src/shinestacker/core/framework.py +0 -0
  106. {shinestacker-1.5.0 → shinestacker-1.5.2}/src/shinestacker/core/logging.py +0 -0
  107. {shinestacker-1.5.0 → shinestacker-1.5.2}/src/shinestacker/gui/__init__.py +0 -0
  108. {shinestacker-1.5.0 → shinestacker-1.5.2}/src/shinestacker/gui/action_config.py +0 -0
  109. {shinestacker-1.5.0 → shinestacker-1.5.2}/src/shinestacker/gui/action_config_dialog.py +0 -0
  110. {shinestacker-1.5.0 → shinestacker-1.5.2}/src/shinestacker/gui/base_form_dialog.py +0 -0
  111. {shinestacker-1.5.0 → shinestacker-1.5.2}/src/shinestacker/gui/colors.py +0 -0
  112. {shinestacker-1.5.0 → shinestacker-1.5.2}/src/shinestacker/gui/flow_layout.py +0 -0
  113. {shinestacker-1.5.0 → shinestacker-1.5.2}/src/shinestacker/gui/folder_file_selection.py +0 -0
  114. {shinestacker-1.5.0 → shinestacker-1.5.2}/src/shinestacker/gui/gui_images.py +0 -0
  115. {shinestacker-1.5.0 → shinestacker-1.5.2}/src/shinestacker/gui/gui_logging.py +0 -0
  116. {shinestacker-1.5.0 → shinestacker-1.5.2}/src/shinestacker/gui/gui_run.py +0 -0
  117. {shinestacker-1.5.0 → shinestacker-1.5.2}/src/shinestacker/gui/ico/focus_stack_bkg.png +0 -0
  118. {shinestacker-1.5.0 → shinestacker-1.5.2}/src/shinestacker/gui/ico/shinestacker.icns +0 -0
  119. {shinestacker-1.5.0 → shinestacker-1.5.2}/src/shinestacker/gui/ico/shinestacker.ico +0 -0
  120. {shinestacker-1.5.0 → shinestacker-1.5.2}/src/shinestacker/gui/ico/shinestacker.png +0 -0
  121. {shinestacker-1.5.0 → shinestacker-1.5.2}/src/shinestacker/gui/ico/shinestacker.svg +0 -0
  122. {shinestacker-1.5.0 → shinestacker-1.5.2}/src/shinestacker/gui/img/close-round-line-icon.png +0 -0
  123. {shinestacker-1.5.0 → shinestacker-1.5.2}/src/shinestacker/gui/img/forward-button-icon.png +0 -0
  124. {shinestacker-1.5.0 → shinestacker-1.5.2}/src/shinestacker/gui/img/play-button-round-icon.png +0 -0
  125. {shinestacker-1.5.0 → shinestacker-1.5.2}/src/shinestacker/gui/img/plus-round-line-icon.png +0 -0
  126. {shinestacker-1.5.0 → shinestacker-1.5.2}/src/shinestacker/gui/main_window.py +0 -0
  127. {shinestacker-1.5.0 → shinestacker-1.5.2}/src/shinestacker/gui/menu_manager.py +0 -0
  128. {shinestacker-1.5.0 → shinestacker-1.5.2}/src/shinestacker/gui/project_controller.py +0 -0
  129. {shinestacker-1.5.0 → shinestacker-1.5.2}/src/shinestacker/gui/project_converter.py +0 -0
  130. {shinestacker-1.5.0 → shinestacker-1.5.2}/src/shinestacker/gui/project_editor.py +0 -0
  131. {shinestacker-1.5.0 → shinestacker-1.5.2}/src/shinestacker/gui/project_model.py +0 -0
  132. {shinestacker-1.5.0 → shinestacker-1.5.2}/src/shinestacker/gui/recent_file_manager.py +0 -0
  133. {shinestacker-1.5.0 → shinestacker-1.5.2}/src/shinestacker/gui/select_path_widget.py +0 -0
  134. {shinestacker-1.5.0 → shinestacker-1.5.2}/src/shinestacker/gui/sys_mon.py +0 -0
  135. {shinestacker-1.5.0 → shinestacker-1.5.2}/src/shinestacker/gui/tab_widget.py +0 -0
  136. {shinestacker-1.5.0 → shinestacker-1.5.2}/src/shinestacker/gui/time_progress_bar.py +0 -0
  137. {shinestacker-1.5.0 → shinestacker-1.5.2}/src/shinestacker/retouch/__init__.py +0 -0
  138. {shinestacker-1.5.0 → shinestacker-1.5.2}/src/shinestacker/retouch/brush.py +0 -0
  139. {shinestacker-1.5.0 → shinestacker-1.5.2}/src/shinestacker/retouch/brush_gradient.py +0 -0
  140. {shinestacker-1.5.0 → shinestacker-1.5.2}/src/shinestacker/retouch/brush_tool.py +0 -0
  141. {shinestacker-1.5.0 → shinestacker-1.5.2}/src/shinestacker/retouch/exif_data.py +0 -0
  142. {shinestacker-1.5.0 → shinestacker-1.5.2}/src/shinestacker/retouch/file_loader.py +0 -0
  143. {shinestacker-1.5.0 → shinestacker-1.5.2}/src/shinestacker/retouch/icon_container.py +0 -0
  144. {shinestacker-1.5.0 → shinestacker-1.5.2}/src/shinestacker/retouch/image_view_status.py +0 -0
  145. {shinestacker-1.5.0 → shinestacker-1.5.2}/src/shinestacker/retouch/io_manager.py +0 -0
  146. {shinestacker-1.5.0 → shinestacker-1.5.2}/src/shinestacker/retouch/layer_collection.py +0 -0
  147. {shinestacker-1.5.0 → shinestacker-1.5.2}/src/shinestacker/retouch/transformation_manager.py +0 -0
  148. {shinestacker-1.5.0 → shinestacker-1.5.2}/src/shinestacker/retouch/undo_manager.py +0 -0
  149. {shinestacker-1.5.0 → shinestacker-1.5.2}/src/shinestacker.egg-info/dependency_links.txt +0 -0
  150. {shinestacker-1.5.0 → shinestacker-1.5.2}/src/shinestacker.egg-info/entry_points.txt +0 -0
  151. {shinestacker-1.5.0 → shinestacker-1.5.2}/src/shinestacker.egg-info/requires.txt +0 -0
  152. {shinestacker-1.5.0 → shinestacker-1.5.2}/src/shinestacker.egg-info/top_level.txt +0 -0
@@ -2,20 +2,48 @@
2
2
 
3
3
  This page reports the main releases only and the main changes therein.
4
4
 
5
+ ## [v1.5.2] - 2025-09-21
6
+ **Bug fixes**
7
+
8
+ ### Fixed
9
+ - fixed white balance filter functionality
10
+ - fixed brush preview visiblity in view mode transitions
11
+
12
+ ### Changed
13
+ - code refactoring and cleanup
14
+
15
+ ---
16
+
17
+ ## [v1.5.1] - 2025-09-20
18
+ **Several bug fixes**
19
+
20
+ ### Added
21
+ - new command-line arguments -v1, -v2, -v3, allow different view modes at startup
22
+
23
+ ### Fixed
24
+ - consistent and restyled cursor for current layer view
25
+ - fixed ghost cursors in side-by-side views
26
+ - fixed cursor shift at startup
27
+ - fixed brush preview at image borders
28
+ - fixed lower/upper case GUI labels
29
+ - improved help and description text
30
+
31
+ ---
32
+
5
33
  ## [v1.5.0] - 2025-09-16
6
- **GUI updates and fixes**
34
+ **GUI improvements and fixes**
7
35
 
8
36
  ### Added
9
37
  - implemented image rotation
10
- - dotted cursor in secondary two-image view
11
38
 
12
39
  ### Fixed
13
- - fixed zoom in wheel events for side-by-side view
40
+ - fixed zoom in wheel events for side-by-side views
14
41
  - restored standard cursor in empty retouch views
15
42
  - lower/upper case GUI labels
16
43
 
17
44
  ### Changed
18
45
  - code refactoring and cleanup
46
+ - dotted cursor in secondary two-image view
19
47
 
20
48
  ---
21
49
 
@@ -369,5 +397,4 @@ This release is equivalent to v0.3.2, but resolves a problem for PyPI distributi
369
397
  - several stability improvements
370
398
  - several bug fixes
371
399
 
372
-
373
400
  ---
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: shinestacker
3
- Version: 1.5.0
3
+ Version: 1.5.2
4
4
  Summary: ShineStacker
5
5
  Author-email: Luca Lista <luka.lista@gmail.com>
6
6
  License-Expression: LGPL-3.0
@@ -0,0 +1 @@
1
+ __version__ = '1.5.2'
@@ -0,0 +1,23 @@
1
+ # pylint: disable=C0114, C0116
2
+
3
+ def add_project_arguments(parser):
4
+ parser.add_argument('-x', '--expert', action='store_true', help='''
5
+ expert options are visible by default.
6
+ ''')
7
+
8
+
9
+ def add_retouch_arguments(parser):
10
+ parser.add_argument('-p', '--path', nargs='?', help='''
11
+ import frames from one or more directories.
12
+ Multiple directories can be specified separated by ';'.
13
+ ''')
14
+ view_group = parser.add_mutually_exclusive_group()
15
+ view_group.add_argument('-v1', '--view-overlaid', action='store_true', help='''
16
+ set overlaid view.
17
+ ''')
18
+ view_group.add_argument('-v2', '--view-side-by-side', action='store_true', help='''
19
+ set side-by-side view.
20
+ ''')
21
+ view_group.add_argument('-v3', '--view-top-bottom', action='store_true', help='''
22
+ set top-bottom view.
23
+ ''')
@@ -1,4 +1,4 @@
1
- # pylint: disable=C0114, C0115, C0116, C0413, E0611, R0903, E1121, W0201
1
+ # pylint: disable=C0114, C0115, C0116, C0413, E0611, R0903, E1121, W0201, R0915, R0912
2
2
  import sys
3
3
  import os
4
4
  import logging
@@ -20,6 +20,7 @@ from shinestacker.app.gui_utils import (
20
20
  disable_macos_special_menu_items, fill_app_menu, set_css_style)
21
21
  from shinestacker.app.help_menu import add_help_action
22
22
  from shinestacker.app.open_frames import open_frames
23
+ from .args import add_project_arguments, add_retouch_arguments
23
24
 
24
25
 
25
26
  class SelectionDialog(QDialog):
@@ -211,19 +212,21 @@ if a single file is specified, it can be either a project or an image.
211
212
  Multiple frames can be specified as a list of files.
212
213
  Multiple files can be specified separated by ';'.
213
214
  ''')
214
- parser.add_argument('-p', '--path', nargs='?', help='''
215
- import frames from one or more directories.
216
- Multiple directories can be specified separated by ';'.
215
+ app_group = parser.add_mutually_exclusive_group()
216
+ app_group.add_argument('-j', '--project', action='store_true', help='''
217
+ open project window at startup instead of project windows (default).
217
218
  ''')
218
- parser.add_argument('-r', '--retouch', action='store_true', help='''
219
+ app_group.add_argument('-r', '--retouch', action='store_true', help='''
219
220
  open retouch window at startup instead of project windows.
220
221
  ''')
221
- parser.add_argument('-x', '--expert', action='store_true', help='''
222
- expert options are visible by default.
223
- ''')
222
+ add_project_arguments(parser)
223
+ add_retouch_arguments(parser)
224
224
  args = vars(parser.parse_args(sys.argv[1:]))
225
225
  filename = args['filename']
226
226
  path = args['path']
227
+ if filename and path:
228
+ print("can't specify both arguments --filename and --path", file=sys.stderr)
229
+ sys.exit(1)
227
230
  setup_logging(console_level=logging.DEBUG, file_level=logging.DEBUG, disable_console=True)
228
231
  app = Application(sys.argv)
229
232
  if config.DONT_USE_NATIVE_MENU:
@@ -239,6 +242,12 @@ expert options are visible by default.
239
242
  main_app.activateWindow()
240
243
  if args['expert']:
241
244
  main_app.project_window.set_expert_options()
245
+ if args['view_overlaid']:
246
+ main_app.retouch_window.set_strategy('overlaid')
247
+ elif args['view_side_by_side']:
248
+ main_app.retouch_window.set_strategy('sidebyside')
249
+ elif args['view_top_bottom']:
250
+ main_app.retouch_window.set_strategy('topbottom')
242
251
  if filename:
243
252
  filenames = filename.split(';')
244
253
  filename = filenames[0]
@@ -17,6 +17,7 @@ from shinestacker.gui.main_window import MainWindow
17
17
  from shinestacker.app.gui_utils import (
18
18
  disable_macos_special_menu_items, fill_app_menu, set_css_style)
19
19
  from shinestacker.app.help_menu import add_help_action
20
+ from .args import add_project_arguments
20
21
 
21
22
 
22
23
  class ProjectApp(MainWindow):
@@ -52,9 +53,7 @@ def main():
52
53
  parser.add_argument('-f', '--filename', nargs='?', help='''
53
54
  project filename.
54
55
  ''')
55
- parser.add_argument('-x', '--expert', action='store_true', help='''
56
- expert options are visible by default.
57
- ''')
56
+ add_project_arguments(parser)
58
57
  args = vars(parser.parse_args(sys.argv[1:]))
59
58
  setup_logging(console_level=logging.DEBUG, file_level=logging.DEBUG, disable_console=True)
60
59
  app = Application(sys.argv)
@@ -13,6 +13,7 @@ from shinestacker.app.gui_utils import (
13
13
  disable_macos_special_menu_items, fill_app_menu, set_css_style)
14
14
  from shinestacker.app.help_menu import add_help_action
15
15
  from shinestacker.app.open_frames import open_frames
16
+ from .args import add_retouch_arguments
16
17
 
17
18
 
18
19
  class RetouchApp(ImageEditorUI):
@@ -44,10 +45,7 @@ def main():
44
45
  import frames from files.
45
46
  Multiple files can be specified separated by ';'.
46
47
  ''')
47
- parser.add_argument('-p', '--path', nargs='?', help='''
48
- import frames from one or more directories.
49
- Multiple directories can be specified separated by ';'.
50
- ''')
48
+ add_retouch_arguments(parser)
51
49
  args = vars(parser.parse_args(sys.argv[1:]))
52
50
  filename = args['filename']
53
51
  path = args['path']
@@ -65,6 +63,12 @@ Multiple directories can be specified separated by ';'.
65
63
  editor = RetouchApp()
66
64
  app.editor = editor
67
65
  editor.show()
66
+ if args['view_overlaid']:
67
+ editor.set_strategy('overlaid')
68
+ elif args['view_side_by_side']:
69
+ editor.set_strategy('sidebyside')
70
+ elif args['view_top_bottom']:
71
+ editor.set_strategy('topbottom')
68
72
  open_frames(editor, filename, path)
69
73
  sys.exit(app.exec())
70
74
 
@@ -26,7 +26,7 @@ class _GuiConstants:
26
26
  'outer': (255, 0, 0, 200),
27
27
  'inner': (255, 0, 0, 150),
28
28
  'gradient_end': (255, 0, 0, 0),
29
- 'pen': (255, 0, 0, 150),
29
+ 'pen': (255, 0, 0, 200),
30
30
  'preview': (255, 180, 180),
31
31
  'cursor_inner': (255, 0, 0, 120),
32
32
  'preview_inner': (255, 255, 255, 150)
@@ -55,7 +55,7 @@ class _GuiConstants:
55
55
  DEFAULT_BRUSH_OPACITY = 100
56
56
  DEFAULT_BRUSH_FLOW = 100
57
57
  BRUSH_SIZES = {
58
- 'default': 50,
58
+ 'default': 100,
59
59
  'min': 5,
60
60
  'mid': 50,
61
61
  'max': 1000
@@ -108,17 +108,19 @@ class NewProjectDialog(BaseFormDialog):
108
108
  step2_layout.addRow("Vignetting correction:", self.vignetting_correction)
109
109
  step2_layout.addRow(
110
110
  # f" {constants.ACTION_ICONS[constants.ACTION_ALIGNFRAMES]} "
111
- "Align layers:", self.align_frames)
111
+ "Align frames:", self.align_frames)
112
112
  step2_layout.addRow(
113
113
  # f" {constants.ACTION_ICONS[constants.ACTION_BALANCEFRAMES]} "
114
- "Balance layers:", self.balance_frames)
114
+ "Balance frames:", self.balance_frames)
115
115
  step2_layout.addRow(
116
116
  # f" {constants.ACTION_ICONS[constants.ACTION_FOCUSSTACKBUNCH]} "
117
- "Bunch stack:", self.bunch_stack)
118
- step2_layout.addRow("Bunch frames:", self.bunch_frames)
119
- step2_layout.addRow("Bunch overlap:", self.bunch_overlap)
117
+ "Create bunches:", self.bunch_stack)
118
+ self.bunch_stack.setToolTip("Combine multiple frames into fewer, high-quality "
119
+ "composite frames for easier retouching")
120
+ step2_layout.addRow("Frames per bunch:", self.bunch_frames)
121
+ step2_layout.addRow("Overlap between bunches:", self.bunch_overlap)
120
122
  self.bunches_label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)
121
- step2_layout.addRow("Number of bunches: ", self.bunches_label)
123
+ step2_layout.addRow("Number of resulting bunches: ", self.bunches_label)
122
124
  if self.expert():
123
125
  step2_layout.addRow(
124
126
  f" {constants.ACTION_ICONS[constants.ACTION_FOCUSSTACK]} "
@@ -133,14 +135,14 @@ class NewProjectDialog(BaseFormDialog):
133
135
  if self.expert():
134
136
  step2_layout.addRow(
135
137
  f" {constants.ACTION_ICONS[constants.ACTION_MULTILAYER]} "
136
- "Save multi layer TIFF:", self.multi_layer)
138
+ "Export as multilayer TIFF:", self.multi_layer)
137
139
  step2_group.setLayout(step2_layout)
138
140
  self.form_layout.addRow(step2_group)
139
141
  step3_group = QGroupBox("3) Confirm")
140
142
  step3_layout = QVBoxLayout()
141
143
  step3_layout.setContentsMargins(15, 0, 15, 15)
142
144
  step3_layout.addWidget(
143
- QLabel("Click 🆗 to confirm and prepare the job."))
145
+ QLabel("Click 🆗 to create project with these settings."))
144
146
  step3_layout.addWidget(
145
147
  QLabel("Select: <b>View</b> > <b>Expert options</b> for advanced configuration."))
146
148
  step3_group.setLayout(step3_layout)
@@ -149,6 +151,7 @@ class NewProjectDialog(BaseFormDialog):
149
151
  step4_layout = QHBoxLayout()
150
152
  step4_layout.setContentsMargins(15, 0, 15, 15)
151
153
  step4_layout.addWidget(QLabel("Press ▶️ to run your job."))
154
+ step4_layout.addStretch()
152
155
  icon_path = f"{os.path.dirname(__file__)}/ico/shinestacker.png"
153
156
  app_icon = QIcon(icon_path)
154
157
  icon_pixmap = app_icon.pixmap(80, 80)
@@ -293,12 +296,12 @@ class NewProjectDialog(BaseFormDialog):
293
296
  "Processing may require a significant amount "
294
297
  "of memory or I/O buffering.\n\n"
295
298
  "Continue anyway?")
296
- msg.setInformativeText("You may consider to split the processing "
297
- " using a bunch stack to reduce memory usage.\n\n"
298
- '✅ Check the option "Bunch stack".\n\n'
299
- "➡️ Check expert options for the stacking algorithm."
300
- 'Go to "View" > "Expert Options".'
301
- )
299
+ msg.setInformativeText('You may consider creating "bunches" to reduce '
300
+ "the number of frames for retouching.\n\n"
301
+ '✅ Check "Create bunches" to combine frames '
302
+ "into manageable composites.\n\n"
303
+ "➡️ Check expert options for the stacking algorithm.\n\n"
304
+ 'Go to "View" > "Expert Options".')
302
305
  msg.setStandardButtons(QMessageBox.Ok | QMessageBox.Cancel)
303
306
  msg.setDefaultButton(QMessageBox.Cancel)
304
307
  if msg.exec_() != QMessageBox.Ok:
@@ -1,16 +1,25 @@
1
1
  # pylint: disable=C0114, C0115, C0116, E0611, W0718, R0915, R0903, R0913, R0917, R0902, R0914
2
2
  import traceback
3
- from abc import ABC, abstractmethod
3
+ from abc import abstractmethod
4
4
  import numpy as np
5
+ from PySide6.QtCore import Qt, QThread, QTimer, QObject, Signal
5
6
  from PySide6.QtWidgets import (
6
7
  QHBoxLayout, QLabel, QSlider, QDialog, QVBoxLayout, QCheckBox, QDialogButtonBox)
7
- from PySide6.QtCore import Qt, Signal, QThread, QTimer
8
+ from .layer_collection import LayerCollectionHandler
8
9
 
9
10
 
10
- class BaseFilter(ABC):
11
- def __init__(self, name, editor, allow_partial_preview=True,
11
+ class BaseFilter(QObject, LayerCollectionHandler):
12
+ update_master_thumbnail_requested = Signal()
13
+ mark_as_modified_requested = Signal()
14
+ filter_gui_set_enabled_requested = Signal(bool)
15
+
16
+ def __init__(self, name, parent, image_viewer, layer_collection, undo_manager,
17
+ allow_partial_preview=True,
12
18
  partial_preview_threshold=0.75, preview_at_startup=False):
13
- self.editor = editor
19
+ QObject.__init__(self, parent)
20
+ LayerCollectionHandler.__init__(self, layer_collection)
21
+ self.image_viewer = image_viewer
22
+ self.undo_manager = undo_manager
14
23
  self.name = name
15
24
  self.allow_partial_preview = allow_partial_preview
16
25
  self.partial_preview_threshold = partial_preview_threshold
@@ -31,11 +40,16 @@ class BaseFilter(ABC):
31
40
  def apply(self, image, *params):
32
41
  pass
33
42
 
43
+ def connect_signals(self, update_master_thumbnail, mark_as_modified, filter_gui_set_enabled):
44
+ self.update_master_thumbnail_requested.connect(update_master_thumbnail)
45
+ self.mark_as_modified_requested.connect(mark_as_modified)
46
+ self.filter_gui_set_enabled_requested.connect(filter_gui_set_enabled)
47
+
34
48
  def run_with_preview(self, **kwargs):
35
- if self.editor.has_no_master_layer():
49
+ if self.has_no_master_layer():
36
50
  return
37
- self.editor.copy_master_layer()
38
- dlg = QDialog(self.editor)
51
+ self.copy_master_layer()
52
+ dlg = QDialog(self.parent())
39
53
  layout = QVBoxLayout(dlg)
40
54
  active_worker = None
41
55
  last_request_id = 0
@@ -46,8 +60,8 @@ class BaseFilter(ABC):
46
60
  def cleanup():
47
61
  nonlocal active_worker, dialog_closed # noqa
48
62
  dialog_closed = True
49
- self.editor.restore_master_layer()
50
- self.editor.image_viewer.update_master_display()
63
+ self.restore_master_layer()
64
+ self.image_viewer.update_master_display()
51
65
  if active_worker and active_worker.isRunning():
52
66
  active_worker.wait()
53
67
  initial_timer.stop()
@@ -58,13 +72,13 @@ class BaseFilter(ABC):
58
72
  if dialog_closed or request_id != expected_id:
59
73
  return
60
74
  if region:
61
- current_region = self.editor.image_viewer.get_visible_image_portion()[1]
75
+ current_region = self.image_viewer.get_visible_image_portion()[1]
62
76
  if current_region == region:
63
- self.editor.set_master_layer(img)
64
- self.editor.image_viewer.update_master_display()
77
+ self.set_master_layer(img)
78
+ self.image_viewer.update_master_display()
65
79
  else:
66
- self.editor.set_master_layer(img)
67
- self.editor.image_viewer.update_master_display()
80
+ self.set_master_layer(img)
81
+ self.image_viewer.update_master_display()
68
82
  try:
69
83
  dlg.activateWindow()
70
84
  except Exception:
@@ -84,10 +98,10 @@ class BaseFilter(ABC):
84
98
  current_id = last_request_id
85
99
  visible_region = None
86
100
  if kwargs.get('partial_preview', self.allow_partial_preview):
87
- visible_data = self.editor.image_viewer.get_visible_image_portion()
101
+ visible_data = self.image_viewer.get_visible_image_portion()
88
102
  if visible_data:
89
103
  visible_img, visible_region = visible_data
90
- master_img = self.editor.master_layer_copy()
104
+ master_img = self.master_layer_copy()
91
105
  if visible_img.size < master_img.size * self.partial_preview_threshold:
92
106
  params = tuple(self.get_params() or ())
93
107
  worker = self.PreviewWorker(
@@ -107,14 +121,14 @@ class BaseFilter(ABC):
107
121
  params = tuple(self.get_params() or ())
108
122
  worker = self.PreviewWorker(
109
123
  self.apply,
110
- args=(self.editor.master_layer_copy(), *params),
124
+ args=(self.master_layer_copy(), *params),
111
125
  request_id=current_id
112
126
  )
113
127
  else:
114
128
  params = tuple(self.get_params() or ())
115
129
  worker = self.PreviewWorker(
116
130
  self.apply,
117
- args=(self.editor.master_layer_copy(), *params),
131
+ args=(self.master_layer_copy(), *params),
118
132
  request_id=current_id
119
133
  )
120
134
  active_worker = worker
@@ -123,8 +137,8 @@ class BaseFilter(ABC):
123
137
  active_worker.start()
124
138
 
125
139
  def restore_original():
126
- self.editor.restore_master_layer()
127
- self.editor.image_viewer.update_master_display()
140
+ self.restore_master_layer()
141
+ self.image_viewer.update_master_display()
128
142
  try:
129
143
  dlg.activateWindow()
130
144
  except Exception:
@@ -139,26 +153,34 @@ class BaseFilter(ABC):
139
153
  if accepted:
140
154
  params = tuple(self.get_params() or ())
141
155
  try:
142
- h, w = self.editor.master_layer().shape[:2]
156
+ h, w = self.master_layer().shape[:2]
143
157
  except Exception:
144
- h, w = self.editor.master_layer_copy().shape[:2]
158
+ h, w = self.master_layer_copy().shape[:2]
145
159
  try:
146
- self.editor.undo_manager.extend_undo_area(0, 0, w, h)
147
- self.editor.undo_manager.save_undo_state(
148
- self.editor.master_layer_copy(),
160
+ self.undo_manager.extend_undo_area(0, 0, w, h)
161
+ self.undo_manager.save_undo_state(
162
+ self.master_layer_copy(),
149
163
  self.name
150
164
  )
151
165
  except Exception:
152
166
  pass
153
- final_img = self.apply(self.editor.master_layer_copy(), *params)
154
- self.editor.set_master_layer(final_img)
155
- self.editor.copy_master_layer()
156
- self.editor.image_viewer.update_master_display()
157
- self.editor.display_manager.update_master_thumbnail()
158
- self.editor.mark_as_modified()
167
+ final_img = self.apply(self.master_layer_copy(), *params)
168
+ self.set_master_layer(final_img)
169
+ self.copy_master_layer()
170
+ self.image_viewer.update_master_display()
171
+ self.update_master_thumbnail_requested.emit()
172
+ self.mark_as_modified_requested.emit()
159
173
  else:
160
174
  restore_original()
161
175
 
176
+ def connect_preview_toggle(self, preview_check, do_preview, restore_original):
177
+ def on_toggled(checked):
178
+ if checked:
179
+ do_preview()
180
+ else:
181
+ restore_original()
182
+ preview_check.toggled.connect(on_toggled)
183
+
162
184
  def create_base_widgets(self, layout, buttons, preview_latency, parent):
163
185
  self.preview_check = QCheckBox("Preview")
164
186
  self.preview_check.setChecked(self.preview_at_startup)
@@ -199,10 +221,12 @@ class BaseFilter(ABC):
199
221
 
200
222
 
201
223
  class OneSliderBaseFilter(BaseFilter):
202
- def __init__(self, name, editor, max_value, initial_value, title,
224
+ def __init__(self, name, parent, image_viewer, layer_collection, undo_manager,
225
+ max_value, initial_value, title,
203
226
  allow_partial_preview=True, partial_preview_threshold=0.5,
204
227
  preview_at_startup=True):
205
- super().__init__(name, editor, allow_partial_preview,
228
+ super().__init__(name, parent, image_viewer, layer_collection, undo_manager,
229
+ allow_partial_preview,
206
230
  partial_preview_threshold, preview_at_startup)
207
231
  self.max_range = 500
208
232
  self.max_value = max_value
@@ -234,7 +258,7 @@ class OneSliderBaseFilter(BaseFilter):
234
258
  self.preview_timer.timeout.connect(do_preview)
235
259
 
236
260
  slider_local.valueChanged.connect(self.config_changed)
237
- self.editor.connect_preview_toggle(
261
+ self.connect_preview_toggle(
238
262
  self.preview_check, self.do_preview_delayed, restore_original)
239
263
  self.button_box.accepted.connect(dlg.accept)
240
264
  self.button_box.rejected.connect(dlg.reject)
@@ -1,4 +1,4 @@
1
- # pylint: disable=C0114, C0115, C0116, E0611, R0913, R0917, R0914, W0718
1
+ # pylint: disable=C0114, C0115, C0116, E0611, R0913, R0917, R0914, W0718, R0915
2
2
  import traceback
3
3
  import numpy as np
4
4
  from PySide6.QtWidgets import QGraphicsPixmapItem
@@ -72,38 +72,52 @@ class BrushPreviewItem(QGraphicsPixmapItem, LayerCollectionHandler):
72
72
  self.hide()
73
73
  return
74
74
  radius = size // 2
75
- x = int(scene_pos.x() - radius + 0.5)
76
- y = int(scene_pos.y() - radius)
75
+ x_center = int(scene_pos.x() + 0.5)
76
+ y_center = int(scene_pos.y() + 0.5)
77
+ x = x_center - radius
78
+ y = y_center - radius
77
79
  w = h = size
78
80
  if not self.valid_current_layer_idx():
79
81
  self.hide()
80
82
  return
81
- layer_area = self.get_layer_area(self.current_layer(), x, y, w, h)
82
- master_area = self.get_layer_area(self.master_layer(), x, y, w, h)
83
+ height, width = self.current_layer().shape[:2]
84
+ visible_x = max(0, x)
85
+ visible_y = max(0, y)
86
+ visible_w = min(width, x + w) - visible_x
87
+ visible_h = min(height, y + h) - visible_y
88
+ if visible_w <= 0 or visible_h <= 0:
89
+ self.hide()
90
+ return
91
+ layer_area = self.get_layer_area(
92
+ self.current_layer(), visible_x, visible_y, visible_w, visible_h)
93
+ master_area = self.get_layer_area(
94
+ self.master_layer(), visible_x, visible_y, visible_w, visible_h)
83
95
  if layer_area is None or master_area is None:
84
96
  self.hide()
85
97
  return
86
- height, width = self.current_layer().shape[:2]
87
98
  full_mask = create_brush_mask(size=size, hardness_percent=self.brush.hardness,
88
99
  opacity_percent=self.brush.opacity)[:, :, np.newaxis]
89
- mask_x_start = max(0, -x) if x < 0 else 0
90
- mask_y_start = max(0, -y) if y < 0 else 0
91
- mask_x_end = size - (max(0, (x + w) - width)) if (x + w) > width else size
92
- mask_y_end = size - (max(0, (y + h) - height)) if (y + h) > height else size
100
+ mask_x_start = max(0, -x)
101
+ mask_y_start = max(0, -y)
102
+ mask_x_end = mask_x_start + visible_w
103
+ mask_y_end = mask_y_start + visible_h
93
104
  mask_area = full_mask[mask_y_start:mask_y_end, mask_x_start:mask_x_end]
94
105
  area = (layer_area * mask_area + master_area * (1 - mask_area)) * 255.0
95
106
  area = area.astype(np.uint8)
96
107
  qimage = QImage(area.data, area.shape[1], area.shape[0],
97
108
  area.strides[0], QImage.Format_RGB888)
98
- mask = QPixmap(w, h)
109
+ mask = QPixmap(visible_w, visible_h)
99
110
  mask.fill(Qt.transparent)
100
111
  painter = QPainter(mask)
101
112
  painter.setPen(Qt.NoPen)
102
113
  painter.setBrush(Qt.black)
103
- painter.drawEllipse(0, 0, w, h)
114
+ center_x_in_visible = x_center - visible_x
115
+ center_y_in_visible = y_center - visible_y
116
+ painter.drawEllipse(
117
+ center_x_in_visible - radius, center_y_in_visible - radius, size, size)
104
118
  painter.end()
105
119
  pixmap = QPixmap.fromImage(qimage)
106
- final_pixmap = QPixmap(w, h)
120
+ final_pixmap = QPixmap(visible_w, visible_h)
107
121
  final_pixmap.fill(Qt.transparent)
108
122
  painter = QPainter(final_pixmap)
109
123
  painter.drawPixmap(0, 0, pixmap)
@@ -111,8 +125,8 @@ class BrushPreviewItem(QGraphicsPixmapItem, LayerCollectionHandler):
111
125
  painter.drawPixmap(0, 0, mask)
112
126
  painter.end()
113
127
  self.setPixmap(final_pixmap)
114
- x_start, y_start = max(0, x), max(0, y)
115
- self.setPos(x_start, y_start)
128
+ self.setPos(visible_x, visible_y)
129
+ self.show()
116
130
  except Exception:
117
131
  traceback.print_exc()
118
132
  self.hide()
@@ -1,11 +1,12 @@
1
- # pylint: disable=C0114, C0115, C0116, E0611, W0221
1
+ # pylint: disable=C0114, C0115, C0116, E0611, W0221, R0913, R0917
2
2
  from .base_filter import OneSliderBaseFilter
3
3
  from .. algorithms.denoise import denoise
4
4
 
5
5
 
6
6
  class DenoiseFilter(OneSliderBaseFilter):
7
- def __init__(self, name, editor):
8
- super().__init__(name, editor, 10.0, 2.5, "Denoise",
7
+ def __init__(self, name, parent, image_viewer, layer_collection, undo_manager):
8
+ super().__init__(name, parent, image_viewer, layer_collection, undo_manager,
9
+ 10.0, 2.5, "Denoise",
9
10
  allow_partial_preview=True, preview_at_startup=False)
10
11
 
11
12
  def apply(self, image, strength):