shinestacker 1.4.0__tar.gz → 1.5.0__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 (150) hide show
  1. {shinestacker-1.4.0 → shinestacker-1.5.0}/CHANGELOG.md +19 -6
  2. {shinestacker-1.4.0/src/shinestacker.egg-info → shinestacker-1.5.0}/PKG-INFO +7 -7
  3. {shinestacker-1.4.0 → shinestacker-1.5.0}/README.md +6 -6
  4. shinestacker-1.5.0/src/shinestacker/_version.py +1 -0
  5. {shinestacker-1.4.0 → shinestacker-1.5.0}/src/shinestacker/app/main.py +1 -1
  6. {shinestacker-1.4.0 → shinestacker-1.5.0}/src/shinestacker/config/gui_constants.py +5 -0
  7. {shinestacker-1.4.0 → shinestacker-1.5.0}/src/shinestacker/retouch/base_filter.py +8 -10
  8. {shinestacker-1.4.0 → shinestacker-1.5.0}/src/shinestacker/retouch/brush_preview.py +0 -1
  9. {shinestacker-1.4.0 → shinestacker-1.5.0}/src/shinestacker/retouch/display_manager.py +42 -42
  10. {shinestacker-1.4.0 → shinestacker-1.5.0}/src/shinestacker/retouch/image_editor_ui.py +52 -31
  11. {shinestacker-1.4.0 → shinestacker-1.5.0}/src/shinestacker/retouch/image_view_status.py +4 -0
  12. {shinestacker-1.4.0 → shinestacker-1.5.0}/src/shinestacker/retouch/image_viewer.py +6 -0
  13. {shinestacker-1.4.0 → shinestacker-1.5.0}/src/shinestacker/retouch/layer_collection.py +3 -0
  14. {shinestacker-1.4.0 → shinestacker-1.5.0}/src/shinestacker/retouch/overlaid_view.py +87 -84
  15. {shinestacker-1.4.0 → shinestacker-1.5.0}/src/shinestacker/retouch/sidebyside_view.py +113 -115
  16. shinestacker-1.5.0/src/shinestacker/retouch/transformation_manager.py +43 -0
  17. {shinestacker-1.4.0 → shinestacker-1.5.0}/src/shinestacker/retouch/undo_manager.py +22 -3
  18. {shinestacker-1.4.0 → shinestacker-1.5.0}/src/shinestacker/retouch/view_strategy.py +135 -44
  19. {shinestacker-1.4.0 → shinestacker-1.5.0/src/shinestacker.egg-info}/PKG-INFO +7 -7
  20. {shinestacker-1.4.0 → shinestacker-1.5.0}/src/shinestacker.egg-info/SOURCES.txt +1 -0
  21. shinestacker-1.4.0/src/shinestacker/_version.py +0 -1
  22. {shinestacker-1.4.0 → shinestacker-1.5.0}/.coveragerc +0 -0
  23. {shinestacker-1.4.0 → shinestacker-1.5.0}/.flake8 +0 -0
  24. {shinestacker-1.4.0 → shinestacker-1.5.0}/.github/workflows/ci-multiplatform.yml +0 -0
  25. {shinestacker-1.4.0 → shinestacker-1.5.0}/.github/workflows/pylint.yml +0 -0
  26. {shinestacker-1.4.0 → shinestacker-1.5.0}/.github/workflows/pypi-publish.yml +0 -0
  27. {shinestacker-1.4.0 → shinestacker-1.5.0}/.github/workflows/release.yml +0 -0
  28. {shinestacker-1.4.0 → shinestacker-1.5.0}/.gitignore +0 -0
  29. {shinestacker-1.4.0 → shinestacker-1.5.0}/.pylintrc +0 -0
  30. {shinestacker-1.4.0 → shinestacker-1.5.0}/.readthedocs.yaml +0 -0
  31. {shinestacker-1.4.0 → shinestacker-1.5.0}/LICENSE +0 -0
  32. {shinestacker-1.4.0 → shinestacker-1.5.0}/MANIFEST.in +0 -0
  33. {shinestacker-1.4.0 → shinestacker-1.5.0}/THIRD_PARTY_LICENSES.txt +0 -0
  34. {shinestacker-1.4.0 → shinestacker-1.5.0}/docs/alignment.md +0 -0
  35. {shinestacker-1.4.0 → shinestacker-1.5.0}/docs/api.md +0 -0
  36. {shinestacker-1.4.0 → shinestacker-1.5.0}/docs/balancing.md +0 -0
  37. {shinestacker-1.4.0 → shinestacker-1.5.0}/docs/conf.py +0 -0
  38. {shinestacker-1.4.0 → shinestacker-1.5.0}/docs/focus_stacking.md +0 -0
  39. {shinestacker-1.4.0 → shinestacker-1.5.0}/docs/gui.md +0 -0
  40. {shinestacker-1.4.0 → shinestacker-1.5.0}/docs/index.md +0 -0
  41. {shinestacker-1.4.0 → shinestacker-1.5.0}/docs/job.md +0 -0
  42. {shinestacker-1.4.0 → shinestacker-1.5.0}/docs/main.md +0 -0
  43. {shinestacker-1.4.0 → shinestacker-1.5.0}/docs/multilayer.md +0 -0
  44. {shinestacker-1.4.0 → shinestacker-1.5.0}/docs/noise.md +0 -0
  45. {shinestacker-1.4.0 → shinestacker-1.5.0}/docs/requirements.txt +0 -0
  46. {shinestacker-1.4.0 → shinestacker-1.5.0}/docs/vignetting.md +0 -0
  47. {shinestacker-1.4.0 → shinestacker-1.5.0}/img/coffee.gif +0 -0
  48. {shinestacker-1.4.0 → shinestacker-1.5.0}/img/coffee_stack.jpg +0 -0
  49. {shinestacker-1.4.0 → shinestacker-1.5.0}/img/extreme-vignetting.jpg +0 -0
  50. {shinestacker-1.4.0 → shinestacker-1.5.0}/img/flies.gif +0 -0
  51. {shinestacker-1.4.0 → shinestacker-1.5.0}/img/flies_stack.jpg +0 -0
  52. {shinestacker-1.4.0 → shinestacker-1.5.0}/img/flow-diagram.png +0 -0
  53. {shinestacker-1.4.0 → shinestacker-1.5.0}/img/gui-finder.png +0 -0
  54. {shinestacker-1.4.0 → shinestacker-1.5.0}/img/gui-project-new.png +0 -0
  55. {shinestacker-1.4.0 → shinestacker-1.5.0}/img/gui-project-run.png +0 -0
  56. {shinestacker-1.4.0 → shinestacker-1.5.0}/img/gui-retouch.png +0 -0
  57. {shinestacker-1.4.0 → shinestacker-1.5.0}/index.html +0 -0
  58. {shinestacker-1.4.0 → shinestacker-1.5.0}/pyproject.toml +0 -0
  59. {shinestacker-1.4.0 → shinestacker-1.5.0}/requirements.txt +0 -0
  60. {shinestacker-1.4.0 → shinestacker-1.5.0}/scripts/build_release.py +0 -0
  61. {shinestacker-1.4.0 → shinestacker-1.5.0}/scripts/git-rev-list.sh +0 -0
  62. {shinestacker-1.4.0 → shinestacker-1.5.0}/scripts/validate-tomli.py +0 -0
  63. {shinestacker-1.4.0 → shinestacker-1.5.0}/setup.cfg +0 -0
  64. {shinestacker-1.4.0 → shinestacker-1.5.0}/src/shinestacker/__init__.py +0 -0
  65. {shinestacker-1.4.0 → shinestacker-1.5.0}/src/shinestacker/algorithms/__init__.py +0 -0
  66. {shinestacker-1.4.0 → shinestacker-1.5.0}/src/shinestacker/algorithms/align.py +0 -0
  67. {shinestacker-1.4.0 → shinestacker-1.5.0}/src/shinestacker/algorithms/align_auto.py +0 -0
  68. {shinestacker-1.4.0 → shinestacker-1.5.0}/src/shinestacker/algorithms/align_parallel.py +0 -0
  69. {shinestacker-1.4.0 → shinestacker-1.5.0}/src/shinestacker/algorithms/balance.py +0 -0
  70. {shinestacker-1.4.0 → shinestacker-1.5.0}/src/shinestacker/algorithms/base_stack_algo.py +0 -0
  71. {shinestacker-1.4.0 → shinestacker-1.5.0}/src/shinestacker/algorithms/denoise.py +0 -0
  72. {shinestacker-1.4.0 → shinestacker-1.5.0}/src/shinestacker/algorithms/depth_map.py +0 -0
  73. {shinestacker-1.4.0 → shinestacker-1.5.0}/src/shinestacker/algorithms/exif.py +0 -0
  74. {shinestacker-1.4.0 → shinestacker-1.5.0}/src/shinestacker/algorithms/multilayer.py +0 -0
  75. {shinestacker-1.4.0 → shinestacker-1.5.0}/src/shinestacker/algorithms/noise_detection.py +0 -0
  76. {shinestacker-1.4.0 → shinestacker-1.5.0}/src/shinestacker/algorithms/pyramid.py +0 -0
  77. {shinestacker-1.4.0 → shinestacker-1.5.0}/src/shinestacker/algorithms/pyramid_auto.py +0 -0
  78. {shinestacker-1.4.0 → shinestacker-1.5.0}/src/shinestacker/algorithms/pyramid_tiles.py +0 -0
  79. {shinestacker-1.4.0 → shinestacker-1.5.0}/src/shinestacker/algorithms/sharpen.py +0 -0
  80. {shinestacker-1.4.0 → shinestacker-1.5.0}/src/shinestacker/algorithms/stack.py +0 -0
  81. {shinestacker-1.4.0 → shinestacker-1.5.0}/src/shinestacker/algorithms/stack_framework.py +0 -0
  82. {shinestacker-1.4.0 → shinestacker-1.5.0}/src/shinestacker/algorithms/utils.py +0 -0
  83. {shinestacker-1.4.0 → shinestacker-1.5.0}/src/shinestacker/algorithms/vignetting.py +0 -0
  84. {shinestacker-1.4.0 → shinestacker-1.5.0}/src/shinestacker/algorithms/white_balance.py +0 -0
  85. {shinestacker-1.4.0 → shinestacker-1.5.0}/src/shinestacker/app/__init__.py +0 -0
  86. {shinestacker-1.4.0 → shinestacker-1.5.0}/src/shinestacker/app/about_dialog.py +0 -0
  87. {shinestacker-1.4.0 → shinestacker-1.5.0}/src/shinestacker/app/gui_utils.py +0 -0
  88. {shinestacker-1.4.0 → shinestacker-1.5.0}/src/shinestacker/app/help_menu.py +0 -0
  89. {shinestacker-1.4.0 → shinestacker-1.5.0}/src/shinestacker/app/open_frames.py +0 -0
  90. {shinestacker-1.4.0 → shinestacker-1.5.0}/src/shinestacker/app/project.py +0 -0
  91. {shinestacker-1.4.0 → shinestacker-1.5.0}/src/shinestacker/app/retouch.py +0 -0
  92. {shinestacker-1.4.0 → shinestacker-1.5.0}/src/shinestacker/config/__init__.py +0 -0
  93. {shinestacker-1.4.0 → shinestacker-1.5.0}/src/shinestacker/config/config.py +0 -0
  94. {shinestacker-1.4.0 → shinestacker-1.5.0}/src/shinestacker/config/constants.py +0 -0
  95. {shinestacker-1.4.0 → shinestacker-1.5.0}/src/shinestacker/core/__init__.py +0 -0
  96. {shinestacker-1.4.0 → shinestacker-1.5.0}/src/shinestacker/core/colors.py +0 -0
  97. {shinestacker-1.4.0 → shinestacker-1.5.0}/src/shinestacker/core/core_utils.py +0 -0
  98. {shinestacker-1.4.0 → shinestacker-1.5.0}/src/shinestacker/core/exceptions.py +0 -0
  99. {shinestacker-1.4.0 → shinestacker-1.5.0}/src/shinestacker/core/framework.py +0 -0
  100. {shinestacker-1.4.0 → shinestacker-1.5.0}/src/shinestacker/core/logging.py +0 -0
  101. {shinestacker-1.4.0 → shinestacker-1.5.0}/src/shinestacker/gui/__init__.py +0 -0
  102. {shinestacker-1.4.0 → shinestacker-1.5.0}/src/shinestacker/gui/action_config.py +0 -0
  103. {shinestacker-1.4.0 → shinestacker-1.5.0}/src/shinestacker/gui/action_config_dialog.py +0 -0
  104. {shinestacker-1.4.0 → shinestacker-1.5.0}/src/shinestacker/gui/base_form_dialog.py +0 -0
  105. {shinestacker-1.4.0 → shinestacker-1.5.0}/src/shinestacker/gui/colors.py +0 -0
  106. {shinestacker-1.4.0 → shinestacker-1.5.0}/src/shinestacker/gui/flow_layout.py +0 -0
  107. {shinestacker-1.4.0 → shinestacker-1.5.0}/src/shinestacker/gui/folder_file_selection.py +0 -0
  108. {shinestacker-1.4.0 → shinestacker-1.5.0}/src/shinestacker/gui/gui_images.py +0 -0
  109. {shinestacker-1.4.0 → shinestacker-1.5.0}/src/shinestacker/gui/gui_logging.py +0 -0
  110. {shinestacker-1.4.0 → shinestacker-1.5.0}/src/shinestacker/gui/gui_run.py +0 -0
  111. {shinestacker-1.4.0 → shinestacker-1.5.0}/src/shinestacker/gui/ico/focus_stack_bkg.png +0 -0
  112. {shinestacker-1.4.0 → shinestacker-1.5.0}/src/shinestacker/gui/ico/shinestacker.icns +0 -0
  113. {shinestacker-1.4.0 → shinestacker-1.5.0}/src/shinestacker/gui/ico/shinestacker.ico +0 -0
  114. {shinestacker-1.4.0 → shinestacker-1.5.0}/src/shinestacker/gui/ico/shinestacker.png +0 -0
  115. {shinestacker-1.4.0 → shinestacker-1.5.0}/src/shinestacker/gui/ico/shinestacker.svg +0 -0
  116. {shinestacker-1.4.0 → shinestacker-1.5.0}/src/shinestacker/gui/img/close-round-line-icon.png +0 -0
  117. {shinestacker-1.4.0 → shinestacker-1.5.0}/src/shinestacker/gui/img/forward-button-icon.png +0 -0
  118. {shinestacker-1.4.0 → shinestacker-1.5.0}/src/shinestacker/gui/img/play-button-round-icon.png +0 -0
  119. {shinestacker-1.4.0 → shinestacker-1.5.0}/src/shinestacker/gui/img/plus-round-line-icon.png +0 -0
  120. {shinestacker-1.4.0 → shinestacker-1.5.0}/src/shinestacker/gui/main_window.py +0 -0
  121. {shinestacker-1.4.0 → shinestacker-1.5.0}/src/shinestacker/gui/menu_manager.py +0 -0
  122. {shinestacker-1.4.0 → shinestacker-1.5.0}/src/shinestacker/gui/new_project.py +0 -0
  123. {shinestacker-1.4.0 → shinestacker-1.5.0}/src/shinestacker/gui/project_controller.py +0 -0
  124. {shinestacker-1.4.0 → shinestacker-1.5.0}/src/shinestacker/gui/project_converter.py +0 -0
  125. {shinestacker-1.4.0 → shinestacker-1.5.0}/src/shinestacker/gui/project_editor.py +0 -0
  126. {shinestacker-1.4.0 → shinestacker-1.5.0}/src/shinestacker/gui/project_model.py +0 -0
  127. {shinestacker-1.4.0 → shinestacker-1.5.0}/src/shinestacker/gui/recent_file_manager.py +0 -0
  128. {shinestacker-1.4.0 → shinestacker-1.5.0}/src/shinestacker/gui/select_path_widget.py +0 -0
  129. {shinestacker-1.4.0 → shinestacker-1.5.0}/src/shinestacker/gui/sys_mon.py +0 -0
  130. {shinestacker-1.4.0 → shinestacker-1.5.0}/src/shinestacker/gui/tab_widget.py +0 -0
  131. {shinestacker-1.4.0 → shinestacker-1.5.0}/src/shinestacker/gui/time_progress_bar.py +0 -0
  132. {shinestacker-1.4.0 → shinestacker-1.5.0}/src/shinestacker/retouch/__init__.py +0 -0
  133. {shinestacker-1.4.0 → shinestacker-1.5.0}/src/shinestacker/retouch/brush.py +0 -0
  134. {shinestacker-1.4.0 → shinestacker-1.5.0}/src/shinestacker/retouch/brush_gradient.py +0 -0
  135. {shinestacker-1.4.0 → shinestacker-1.5.0}/src/shinestacker/retouch/brush_tool.py +0 -0
  136. {shinestacker-1.4.0 → shinestacker-1.5.0}/src/shinestacker/retouch/denoise_filter.py +0 -0
  137. {shinestacker-1.4.0 → shinestacker-1.5.0}/src/shinestacker/retouch/exif_data.py +0 -0
  138. {shinestacker-1.4.0 → shinestacker-1.5.0}/src/shinestacker/retouch/file_loader.py +0 -0
  139. {shinestacker-1.4.0 → shinestacker-1.5.0}/src/shinestacker/retouch/filter_manager.py +0 -0
  140. {shinestacker-1.4.0 → shinestacker-1.5.0}/src/shinestacker/retouch/icon_container.py +0 -0
  141. {shinestacker-1.4.0 → shinestacker-1.5.0}/src/shinestacker/retouch/io_gui_handler.py +0 -0
  142. {shinestacker-1.4.0 → shinestacker-1.5.0}/src/shinestacker/retouch/io_manager.py +0 -0
  143. {shinestacker-1.4.0 → shinestacker-1.5.0}/src/shinestacker/retouch/shortcuts_help.py +0 -0
  144. {shinestacker-1.4.0 → shinestacker-1.5.0}/src/shinestacker/retouch/unsharp_mask_filter.py +0 -0
  145. {shinestacker-1.4.0 → shinestacker-1.5.0}/src/shinestacker/retouch/vignetting_filter.py +0 -0
  146. {shinestacker-1.4.0 → shinestacker-1.5.0}/src/shinestacker/retouch/white_balance_filter.py +0 -0
  147. {shinestacker-1.4.0 → shinestacker-1.5.0}/src/shinestacker.egg-info/dependency_links.txt +0 -0
  148. {shinestacker-1.4.0 → shinestacker-1.5.0}/src/shinestacker.egg-info/entry_points.txt +0 -0
  149. {shinestacker-1.4.0 → shinestacker-1.5.0}/src/shinestacker.egg-info/requires.txt +0 -0
  150. {shinestacker-1.4.0 → shinestacker-1.5.0}/src/shinestacker.egg-info/top_level.txt +0 -0
@@ -2,6 +2,23 @@
2
2
 
3
3
  This page reports the main releases only and the main changes therein.
4
4
 
5
+ ## [v1.5.0] - 2025-09-16
6
+ **GUI updates and fixes**
7
+
8
+ ### Added
9
+ - implemented image rotation
10
+ - dotted cursor in secondary two-image view
11
+
12
+ ### Fixed
13
+ - fixed zoom in wheel events for side-by-side view
14
+ - restored standard cursor in empty retouch views
15
+ - lower/upper case GUI labels
16
+
17
+ ### Changed
18
+ - code refactoring and cleanup
19
+
20
+ ---
21
+
5
22
  ## [v1.4.0] - 2025-09-14
6
23
  **GUI improvements**
7
24
 
@@ -11,7 +28,7 @@ This page reports the main releases only and the main changes therein.
11
28
  - expert options can be shown with a checkbox in each dialog
12
29
  - optional summary plots for alignment transformation parameters
13
30
 
14
- ## Fixed
31
+ ### Fixed
15
32
  - fixed bug in plot generation
16
33
  - fixes warning due to missing glyph in PDF generation on macOS
17
34
  - safer parallel plot generation using a thread locks
@@ -19,15 +36,11 @@ This page reports the main releases only and the main changes therein.
19
36
  ### Changed
20
37
  - code refactoring in various areas
21
38
 
22
- ---
23
-
24
- ### Changed
25
- - code cleanup
26
39
 
27
40
  ## [v1.3.1] - 2025-09-08
28
41
  **Fixes and optimizations**
29
42
 
30
- ## Fixed
43
+ ### Fixed
31
44
  - fixed input folder widget in job configuration
32
45
  - better management of patological alignments
33
46
  - restored alignment match plots
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: shinestacker
3
- Version: 1.4.0
3
+ Version: 1.5.0
4
4
  Summary: ShineStacker
5
5
  Author-email: Luca Lista <luka.lista@gmail.com>
6
6
  License-Expression: LGPL-3.0
@@ -70,11 +70,11 @@ The GUI has two main working areas:
70
70
 
71
71
  <img src='https://raw.githubusercontent.com/lucalista/shinestacker/main/img/gui-retouch.png' width="600" referrerpolicy="no-referrer">
72
72
 
73
- # Resources
73
+ ## Resources
74
74
 
75
75
  🌍 [Website on WordPress](https://shinestacker.wordpress.com) • 📖 [Main documentation](https://shinestacker.readthedocs.io) • 📝 [Changelog](https://github.com/lucalista/shinestacker/blob/main/CHANGELOG.md)
76
76
 
77
- # Note for macOS users
77
+ ## Note for macOS users
78
78
 
79
79
  **The following note is only relevant if you download the application as compressed archive from the [release page](https://github.com/lucalista/shinestacker/releases).**
80
80
 
@@ -93,17 +93,17 @@ xattr -cr ~/Downloads/shinestacker/shinestacker.app
93
93
 
94
94
  macOS adds a quarantine flag to all files downloaded from the internet. The above command removes that flag while preserving all other application functionality.
95
95
 
96
- # Credits
96
+ ## Credits
97
97
 
98
98
  The first version of the core focus stack algorithm was initially inspired by the [Laplacian pyramids method](https://github.com/sjawhar/focus-stacking) implementation by Sami Jawhar that was used under permission of the author. The implementation in the latest releases was rewritten from the original code.
99
99
 
100
- # Resources
100
+ ## Resources
101
101
 
102
102
  * [Pyramid Methods in Image Processing](https://www.researchgate.net/publication/246727904_Pyramid_Methods_in_Image_Processing), E. H. Adelson, C. H. Anderson, J. R. Bergen, P. J. Burt, J. M. Ogden, RCA Engineer, 29-6, Nov/Dec 1984
103
103
  Pyramid methods in image processing
104
104
  * [A Multi-focus Image Fusion Method Based on Laplacian Pyramid](http://www.jcomputers.us/vol6/jcp0612-07.pdf), Wencheng Wang, Faliang Chang, Journal of Computers 6 (12), 2559, December 2011
105
105
 
106
- # License
106
+ ## License
107
107
 
108
108
  <img src="https://www.gnu.org/graphics/lgplv3-147x51.png" alt="LGPL 3 logo">
109
109
 
@@ -112,7 +112,7 @@ Pyramid methods in image processing
112
112
 
113
113
  - **Logo**: The Shine Stacker logo was designed by [Alessandro Lista](https://linktr.ee/alelista). Copyright © Alessandro Lista. All rights reserved. The logo is not covered by the LGPL-3.0 license of this project.
114
114
 
115
- # Attribution request
115
+ ## Attribution request
116
116
  📸 If you publish images created with Shine Stacker, please consider adding a note such as:
117
117
 
118
118
  *Created with Shine Stacker – https://github.com/lucalista/shinestacker*
@@ -38,11 +38,11 @@ The GUI has two main working areas:
38
38
 
39
39
  <img src='https://raw.githubusercontent.com/lucalista/shinestacker/main/img/gui-retouch.png' width="600" referrerpolicy="no-referrer">
40
40
 
41
- # Resources
41
+ ## Resources
42
42
 
43
43
  🌍 [Website on WordPress](https://shinestacker.wordpress.com) • 📖 [Main documentation](https://shinestacker.readthedocs.io) • 📝 [Changelog](https://github.com/lucalista/shinestacker/blob/main/CHANGELOG.md)
44
44
 
45
- # Note for macOS users
45
+ ## Note for macOS users
46
46
 
47
47
  **The following note is only relevant if you download the application as compressed archive from the [release page](https://github.com/lucalista/shinestacker/releases).**
48
48
 
@@ -61,17 +61,17 @@ xattr -cr ~/Downloads/shinestacker/shinestacker.app
61
61
 
62
62
  macOS adds a quarantine flag to all files downloaded from the internet. The above command removes that flag while preserving all other application functionality.
63
63
 
64
- # Credits
64
+ ## Credits
65
65
 
66
66
  The first version of the core focus stack algorithm was initially inspired by the [Laplacian pyramids method](https://github.com/sjawhar/focus-stacking) implementation by Sami Jawhar that was used under permission of the author. The implementation in the latest releases was rewritten from the original code.
67
67
 
68
- # Resources
68
+ ## Resources
69
69
 
70
70
  * [Pyramid Methods in Image Processing](https://www.researchgate.net/publication/246727904_Pyramid_Methods_in_Image_Processing), E. H. Adelson, C. H. Anderson, J. R. Bergen, P. J. Burt, J. M. Ogden, RCA Engineer, 29-6, Nov/Dec 1984
71
71
  Pyramid methods in image processing
72
72
  * [A Multi-focus Image Fusion Method Based on Laplacian Pyramid](http://www.jcomputers.us/vol6/jcp0612-07.pdf), Wencheng Wang, Faliang Chang, Journal of Computers 6 (12), 2559, December 2011
73
73
 
74
- # License
74
+ ## License
75
75
 
76
76
  <img src="https://www.gnu.org/graphics/lgplv3-147x51.png" alt="LGPL 3 logo">
77
77
 
@@ -80,7 +80,7 @@ Pyramid methods in image processing
80
80
 
81
81
  - **Logo**: The Shine Stacker logo was designed by [Alessandro Lista](https://linktr.ee/alelista). Copyright © Alessandro Lista. All rights reserved. The logo is not covered by the LGPL-3.0 license of this project.
82
82
 
83
- # Attribution request
83
+ ## Attribution request
84
84
  📸 If you publish images created with Shine Stacker, please consider adding a note such as:
85
85
 
86
86
  *Created with Shine Stacker – https://github.com/lucalista/shinestacker*
@@ -0,0 +1 @@
1
+ __version__ = '1.5.0'
@@ -102,7 +102,7 @@ class MainApp(QMainWindow):
102
102
  file_menu = action.menu()
103
103
  break
104
104
  if file_menu is not None:
105
- import_action = QAction("Import From Current Project", self)
105
+ import_action = QAction("Import from Current Project", self)
106
106
  import_action.triggered.connect(self.import_from_project)
107
107
  file_menu.addAction(import_action)
108
108
  else:
@@ -66,6 +66,11 @@ class _GuiConstants:
66
66
  ZOOM_IN_FACTOR = 1.10
67
67
  ZOOM_OUT_FACTOR = 1 / ZOOM_IN_FACTOR
68
68
 
69
+ ROTATE_LABEL = "Rotate"
70
+ ROTATE_90_CW_LABEL = f"{ROTATE_LABEL} 90° Clockwise"
71
+ ROTATE_90_CCW_LABEL = f"{ROTATE_LABEL} 90° Anticlockwise"
72
+ ROTATE_180_LABEL = f"{ROTATE_LABEL} 180°"
73
+
69
74
  def calculate_gamma(self):
70
75
  if self.BRUSH_SIZES['mid'] <= self.BRUSH_SIZES['min'] or self.BRUSH_SIZES['max'] <= 0:
71
76
  return 1.0
@@ -34,7 +34,6 @@ class BaseFilter(ABC):
34
34
  def run_with_preview(self, **kwargs):
35
35
  if self.editor.has_no_master_layer():
36
36
  return
37
-
38
37
  self.editor.copy_master_layer()
39
38
  dlg = QDialog(self.editor)
40
39
  layout = QVBoxLayout(dlg)
@@ -143,15 +142,14 @@ class BaseFilter(ABC):
143
142
  h, w = self.editor.master_layer().shape[:2]
144
143
  except Exception:
145
144
  h, w = self.editor.master_layer_copy().shape[:2]
146
- if hasattr(self.editor, "undo_manager"):
147
- try:
148
- self.editor.undo_manager.extend_undo_area(0, 0, w, h)
149
- self.editor.undo_manager.save_undo_state(
150
- self.editor.master_layer_copy(),
151
- self.name
152
- )
153
- except Exception:
154
- pass
145
+ 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(),
149
+ self.name
150
+ )
151
+ except Exception:
152
+ pass
155
153
  final_img = self.apply(self.editor.master_layer_copy(), *params)
156
154
  self.editor.set_master_layer(final_img)
157
155
  self.editor.copy_master_layer()
@@ -113,7 +113,6 @@ class BrushPreviewItem(QGraphicsPixmapItem, LayerCollectionHandler):
113
113
  self.setPixmap(final_pixmap)
114
114
  x_start, y_start = max(0, x), max(0, y)
115
115
  self.setPos(x_start, y_start)
116
- self.show()
117
116
  except Exception:
118
117
  traceback.print_exc()
119
118
  self.hide()
@@ -25,7 +25,6 @@ class ClickableLabel(QLabel):
25
25
 
26
26
  class DisplayManager(QObject, LayerCollectionHandler):
27
27
  status_message_requested = Signal(str)
28
- cursor_preview_state_changed = Signal(bool)
29
28
 
30
29
  def __init__(self, layer_collection, image_viewer, master_thumbnail_label,
31
30
  thumbnail_list, parent=None):
@@ -35,7 +34,6 @@ class DisplayManager(QObject, LayerCollectionHandler):
35
34
  self.master_thumbnail_label = master_thumbnail_label
36
35
  self.thumbnail_list = thumbnail_list
37
36
  self.view_mode = 'master'
38
- self.temp_view_individual = False
39
37
  self.needs_update = False
40
38
  self.update_timer = QTimer()
41
39
  self.update_timer.setInterval(gui_constants.PAINT_REFRESH_TIMER)
@@ -47,21 +45,10 @@ class DisplayManager(QObject, LayerCollectionHandler):
47
45
  self.refresh_master_view()
48
46
  self.needs_update = False
49
47
 
50
- def refresh_master_view(self):
51
- if self.has_no_master_layer():
52
- return
53
- self.image_viewer.update_master_display()
54
- self.update_master_thumbnail()
55
- self.image_viewer.refresh_display()
56
-
57
- def refresh_current_view(self):
58
- if self.number_of_layers() == 0:
59
- return
60
- self.image_viewer.update_current_display()
61
- self.image_viewer.refresh_display()
62
-
63
48
  def create_thumbnail(self, layer):
64
49
  source_layer = (layer // 256).astype(np.uint8) if layer.dtype == np.uint16 else layer
50
+ if not source_layer.flags.c_contiguous:
51
+ source_layer = np.ascontiguousarray(source_layer)
65
52
  height, width = source_layer.shape[:2]
66
53
  if layer.ndim == 3 and source_layer.shape[-1] == 3:
67
54
  qimg = QImage(source_layer.data, width, height, 3 * width, QImage.Format_RGB888)
@@ -157,48 +144,61 @@ class DisplayManager(QObject, LayerCollectionHandler):
157
144
  self.thumbnail_list.scrollToItem(
158
145
  self.thumbnail_list.item(index), QAbstractItemView.PositionAtCenter)
159
146
 
147
+ def _master_refresh_and_thumb(self):
148
+ self.image_viewer.show_master()
149
+ self.refresh_master_view()
150
+ self.thumbnail_highlight = gui_constants.THUMB_LO_COLOR
151
+ self.highlight_thumbnail(self.current_layer_idx())
152
+
153
+ def _current_refresh_and_thumb(self):
154
+ self.image_viewer.show_current()
155
+ self.refresh_current_view()
156
+ self.thumbnail_highlight = gui_constants.THUMB_HI_COLOR
157
+ self.highlight_thumbnail(self.current_layer_idx())
158
+
160
159
  def set_view_master(self):
161
160
  if self.has_no_master_layer():
162
161
  return
163
162
  self.view_mode = 'master'
164
- self.temp_view_individual = False
165
- self.refresh_master_view()
166
- self.thumbnail_highlight = gui_constants.THUMB_LO_COLOR
167
- self.highlight_thumbnail(self.current_layer_idx())
163
+ self._master_refresh_and_thumb()
168
164
  self.status_message_requested.emit("View mode: Master")
169
- self.cursor_preview_state_changed.emit(True)
170
165
 
171
166
  def set_view_individual(self):
172
167
  if self.has_no_master_layer():
173
168
  return
174
169
  self.view_mode = 'individual'
175
- self.temp_view_individual = False
176
- self.refresh_current_view()
177
- self.thumbnail_highlight = gui_constants.THUMB_HI_COLOR
178
- self.highlight_thumbnail(self.current_layer_idx())
170
+ self._current_refresh_and_thumb()
179
171
  self.status_message_requested.emit("View mode: Individual layers")
180
- self.cursor_preview_state_changed.emit(False)
172
+
173
+ def refresh_master_view(self):
174
+ if self.has_no_master_layer():
175
+ return
176
+ self.image_viewer.update_master_display()
177
+ self.image_viewer.refresh_display()
178
+ self.update_master_thumbnail()
179
+
180
+ def refresh_current_view(self):
181
+ if self.number_of_layers() == 0:
182
+ return
183
+ self.image_viewer.update_current_display()
184
+ self.image_viewer.refresh_display()
181
185
 
182
186
  def start_temp_view(self):
183
- if not self.temp_view_individual and self.view_mode == 'master':
184
- self.temp_view_individual = True
185
- self.image_viewer.update_brush_cursor()
186
- self.thumbnail_highlight = gui_constants.THUMB_HI_COLOR
187
- self.highlight_thumbnail(self.current_layer_idx())
188
- self.image_viewer.show_current()
189
- self.refresh_current_view()
190
- self.status_message_requested.emit("Temporary view: Individual layer (hold X)")
187
+ if self.view_mode == 'master':
188
+ self._current_refresh_and_thumb()
189
+ self.status_message_requested.emit("Temporary view: Individual layer")
190
+ else:
191
+ self._master_refresh_and_thumb()
192
+ self.image_viewer.strategy.brush_preview.hide()
193
+ self.status_message_requested.emit("Temporary view: Master")
191
194
 
192
195
  def end_temp_view(self):
193
- if self.temp_view_individual:
194
- self.temp_view_individual = False
195
- self.image_viewer.update_brush_cursor()
196
- self.thumbnail_highlight = gui_constants.THUMB_LO_COLOR
197
- self.highlight_thumbnail(self.current_layer_idx())
198
- self.image_viewer.show_master()
199
- self.refresh_master_view()
196
+ if self.view_mode == 'master':
197
+ self._master_refresh_and_thumb()
200
198
  self.status_message_requested.emit("View mode: Master")
201
- self.cursor_preview_state_changed.emit(True)
199
+ else:
200
+ self._current_refresh_and_thumb()
201
+ self.status_message_requested.emit("View: Individual layer")
202
202
 
203
203
  def allow_cursor_preview(self):
204
- return self.view_mode == 'master' and not self.temp_view_individual
204
+ return self.view_mode == 'master'
@@ -1,4 +1,4 @@
1
- # pylint: disable=C0114, C0115, C0116, E0611, R0902, R0914, R0915, R0904
1
+ # pylint: disable=C0114, C0115, C0116, E0611, R0902, R0914, R0915, R0904, W0108
2
2
  from functools import partial
3
3
  import numpy as np
4
4
  from PySide6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QFrame, QLabel, QMenu,
@@ -8,6 +8,7 @@ from PySide6.QtCore import Qt
8
8
  from PySide6.QtGui import QGuiApplication
9
9
  from .. config.constants import constants
10
10
  from .. config.gui_constants import gui_constants
11
+ from .. gui.recent_file_manager import RecentFileManager
11
12
  from .image_viewer import ImageViewer
12
13
  from .shortcuts_help import ShortcutsHelp
13
14
  from .brush import Brush
@@ -22,7 +23,7 @@ from .denoise_filter import DenoiseFilter
22
23
  from .unsharp_mask_filter import UnsharpMaskFilter
23
24
  from .white_balance_filter import WhiteBalanceFilter
24
25
  from .vignetting_filter import VignettingFilter
25
- from .. gui.recent_file_manager import RecentFileManager
26
+ from .transformation_manager import TransfromationManager
26
27
 
27
28
 
28
29
  class ImageEditorUI(QMainWindow, LayerCollectionHandler):
@@ -31,16 +32,17 @@ class ImageEditorUI(QMainWindow, LayerCollectionHandler):
31
32
  LayerCollectionHandler.__init__(self, LayerCollection())
32
33
  self._recent_file_manager = RecentFileManager("shinestacker-recent-images-files.txt")
33
34
  self.thumbnail_highlight = gui_constants.THUMB_MASTER_HI_COLOR
34
- self.undo_manager = UndoManager()
35
- self.undo_action = None
36
- self.redo_action = None
37
- self.undo_manager.stack_changed.connect(self.update_undo_redo_actions)
38
35
  self.io_gui_handler = None
39
36
  self.display_manager = None
40
37
  self.brush = Brush()
41
38
  self.brush_tool = BrushTool()
42
39
  self.modified = False
43
40
  self.mask_layer = None
41
+ self.transformation_manager = TransfromationManager(self)
42
+ self.undo_manager = UndoManager(self.transformation_manager)
43
+ self.undo_action = None
44
+ self.redo_action = None
45
+ self.undo_manager.stack_changed.connect(self.update_undo_redo_actions)
44
46
  self.filter_manager = FilterManager(self)
45
47
  self.filter_manager.register_filter("Denoise", DenoiseFilter)
46
48
  self.filter_manager.register_filter("Unsharp Mask", UnsharpMaskFilter)
@@ -238,8 +240,6 @@ class ImageEditorUI(QMainWindow, LayerCollectionHandler):
238
240
  self.master_thumbnail_label, self.thumbnail_list, parent=self)
239
241
  self.io_gui_handler = IOGuiHandler(self.layer_collection, self.undo_manager, parent=self)
240
242
  self.display_manager.status_message_requested.connect(self.show_status_message)
241
- self.display_manager.cursor_preview_state_changed.connect(
242
- self.image_viewer.set_allow_cursor_preview)
243
243
  self.io_gui_handler.status_message_requested.connect(self.show_status_message)
244
244
  self.io_gui_handler.update_title_requested.connect(self.update_title)
245
245
  self.io_gui_handler.mark_as_modified_requested.connect(self.mark_as_modified)
@@ -277,8 +277,8 @@ class ImageEditorUI(QMainWindow, LayerCollectionHandler):
277
277
 
278
278
  file_menu.addAction("&Close", self.close_file, "Ctrl+W")
279
279
  file_menu.addSeparator()
280
- file_menu.addAction("&Import frames", self.io_gui_handler.import_frames)
281
- file_menu.addAction("Import &EXIF data", self.io_gui_handler.select_exif_path)
280
+ file_menu.addAction("&Import Frames", self.io_gui_handler.import_frames)
281
+ file_menu.addAction("Import &EXIF Data", self.io_gui_handler.select_exif_path)
282
282
 
283
283
  edit_menu = menubar.addMenu("&Edit")
284
284
  self.undo_action = QAction("Undo", self)
@@ -293,7 +293,21 @@ class ImageEditorUI(QMainWindow, LayerCollectionHandler):
293
293
  edit_menu.addAction(self.redo_action)
294
294
  edit_menu.addSeparator()
295
295
 
296
- copy_action = QAction("Copy Layer to Master", self)
296
+ transf_menu = QMenu("&Transform")
297
+ rotate_90_cw_action = QAction(gui_constants.ROTATE_90_CW_LABEL, self)
298
+ transf_menu.addAction(rotate_90_cw_action)
299
+ rotate_90_cw_action.triggered.connect(lambda: self.transformation_manager.rotate_90_cw())
300
+ rotate_90_ccw_action = QAction(gui_constants.ROTATE_90_CCW_LABEL, self)
301
+ transf_menu.addAction(rotate_90_ccw_action)
302
+ rotate_90_ccw_action.triggered.connect(lambda: self.transformation_manager.rotate_90_ccw())
303
+ rotate_180_action = QAction(gui_constants.ROTATE_180_LABEL, self)
304
+ rotate_180_action.triggered.connect(lambda: self.transformation_manager.rotate_180())
305
+ transf_menu.addAction(rotate_180_action)
306
+ edit_menu.addMenu(transf_menu)
307
+
308
+ edit_menu.addSeparator()
309
+
310
+ copy_action = QAction("Copy Current Layer to Master", self)
297
311
  copy_action.setShortcut("Ctrl+M")
298
312
  copy_action.triggered.connect(self.copy_layer_to_master)
299
313
  edit_menu.addAction(copy_action)
@@ -310,22 +324,22 @@ class ImageEditorUI(QMainWindow, LayerCollectionHandler):
310
324
 
311
325
  view_strategy_menu = QMenu("View &Mode", view_menu)
312
326
 
313
- self.view_action_modes = {
327
+ self.view_mode_actions = {
314
328
  'overlaid': QAction("Overlaid", self),
315
- 'sidebyside': QAction("Side By Side", self),
329
+ 'sidebyside': QAction("Side by Side", self),
316
330
  'topbottom': QAction("Top-Bottom", self)
317
331
  }
318
- overlaid_mode = self.view_action_modes['overlaid']
332
+ overlaid_mode = self.view_mode_actions['overlaid']
319
333
  overlaid_mode.setShortcut("Ctrl+1")
320
334
  overlaid_mode.setCheckable(True)
321
335
  overlaid_mode.triggered.connect(lambda: set_strategy('overlaid'))
322
336
  view_strategy_menu.addAction(overlaid_mode)
323
- side_by_side_mode = self.view_action_modes['sidebyside']
337
+ side_by_side_mode = self.view_mode_actions['sidebyside']
324
338
  side_by_side_mode.setShortcut("Ctrl+2")
325
339
  side_by_side_mode.setCheckable(True)
326
340
  side_by_side_mode.triggered.connect(lambda: set_strategy('sidebyside'))
327
341
  view_strategy_menu.addAction(side_by_side_mode)
328
- side_by_side_mode = self.view_action_modes['topbottom']
342
+ side_by_side_mode = self.view_mode_actions['topbottom']
329
343
  side_by_side_mode.setShortcut("Ctrl+3")
330
344
  side_by_side_mode.setCheckable(True)
331
345
  side_by_side_mode.triggered.connect(lambda: set_strategy('topbottom'))
@@ -338,31 +352,40 @@ class ImageEditorUI(QMainWindow, LayerCollectionHandler):
338
352
  self.view_master_action.setEnabled(enable_shortcuts)
339
353
  self.view_individual_action.setEnabled(enable_shortcuts)
340
354
  self.toggle_view_master_individual_action.setEnabled(enable_shortcuts)
341
- for label, mode in self.view_action_modes.items():
355
+ for label, mode in self.view_mode_actions.items():
342
356
  mode.setEnabled(label != strategy)
343
357
  mode.setChecked(label == strategy)
344
358
 
345
359
  cursor_menu = view_menu.addMenu("Cursor Style")
346
360
 
347
- cursor_stype = self.image_viewer.get_cursor_style()
348
- brush_action = QAction("Simple Brush", self)
361
+ self.cursor_style_actions = {
362
+ 'brush': QAction("Simple Brush", self),
363
+ 'preview': QAction("Brush Preview", self),
364
+ 'outline': QAction("Outline Only", self)
365
+ }
366
+ brush_action = self.cursor_style_actions['brush']
349
367
  brush_action.setCheckable(True)
350
- brush_action.setChecked(cursor_stype == 'brush')
351
- brush_action.triggered.connect(lambda: self.image_viewer.set_cursor_style('brush'))
368
+ brush_action.triggered.connect(lambda: set_cursor_style('brush'))
352
369
  cursor_menu.addAction(brush_action)
353
370
 
354
- preview_action = QAction("Brush Preview", self)
371
+ preview_action = self.cursor_style_actions['preview']
355
372
  preview_action.setCheckable(True)
356
- preview_action.setChecked(cursor_stype == 'preview')
357
- preview_action.triggered.connect(lambda: self.image_viewer.set_cursor_style('preview'))
373
+ preview_action.triggered.connect(lambda: set_cursor_style('preview'))
358
374
  cursor_menu.addAction(preview_action)
359
375
 
360
- outline_action = QAction("Outline Only", self)
376
+ outline_action = self.cursor_style_actions['outline']
361
377
  outline_action.setCheckable(True)
362
- outline_action.setChecked(cursor_stype == 'outline')
363
- outline_action.triggered.connect(lambda: self.image_viewer.set_cursor_style('outline'))
378
+ outline_action.triggered.connect(lambda: set_cursor_style('outline'))
364
379
  cursor_menu.addAction(outline_action)
365
380
 
381
+ def set_cursor_style(cursor_style):
382
+ self.image_viewer.set_cursor_style(cursor_style)
383
+ for label, style in self.cursor_style_actions.items():
384
+ style.setEnabled(label != cursor_style)
385
+ style.setChecked(label == cursor_style)
386
+
387
+ set_cursor_style(self.image_viewer.get_cursor_style())
388
+
366
389
  cursor_group = QActionGroup(self)
367
390
  cursor_group.addAction(preview_action)
368
391
  cursor_group.addAction(outline_action)
@@ -432,13 +455,13 @@ class ImageEditorUI(QMainWindow, LayerCollectionHandler):
432
455
  white_balance_action = QAction("White Balance", self)
433
456
  white_balance_action.triggered.connect(self.white_balance)
434
457
  filter_menu.addAction(white_balance_action)
435
- vignetting_action = QAction("Vignetting correction", self)
458
+ vignetting_action = QAction("Vignetting Correction", self)
436
459
  vignetting_action.triggered.connect(self.vignetting_correction)
437
460
  filter_menu.addAction(vignetting_action)
438
461
 
439
462
  help_menu = menubar.addMenu("&Help")
440
463
  help_menu.setObjectName("Help")
441
- shortcuts_help_action = QAction("Shortcuts and mouse", self)
464
+ shortcuts_help_action = QAction("Shortcuts and Mouse", self)
442
465
 
443
466
  def shortcuts_help():
444
467
  self.shortcuts_help_dialog = ShortcutsHelp(self)
@@ -693,13 +716,11 @@ class ImageEditorUI(QMainWindow, LayerCollectionHandler):
693
716
 
694
717
  def set_view_master(self):
695
718
  self.display_manager.set_view_master()
696
- self.display_manager.refresh_master_view()
697
719
  self.thumbnail_highlight = gui_constants.THUMB_MASTER_HI_COLOR
698
720
  self.highlight_master_thumbnail()
699
721
 
700
722
  def set_view_individual(self):
701
723
  self.display_manager.set_view_individual()
702
- self.display_manager.refresh_current_view()
703
724
  self.thumbnail_highlight = gui_constants.THUMB_MASTER_LO_COLOR
704
725
  self.highlight_master_thumbnail()
705
726
 
@@ -59,3 +59,7 @@ class ImageViewStatus(QObject):
59
59
 
60
60
  def set_max_scale(self, min_scale):
61
61
  self.max_scale = min_scale
62
+
63
+ def set_scroll(self, h_scroll, v_scroll):
64
+ self.h_scroll = h_scroll
65
+ self.v_scroll = v_scroll
@@ -35,8 +35,14 @@ class ImageViewer(QWidget):
35
35
  self.strategy.show()
36
36
  self.strategy.resize(self.size())
37
37
  if not self.strategy.empty():
38
+ self.strategy.cleanup_brush_preview()
38
39
  self.strategy.update_master_display()
39
40
  self.strategy.update_current_display()
41
+ self.strategy.setup_brush_cursor()
42
+ self.strategy.update_brush_cursor()
43
+ self.strategy.show_master()
44
+ self.strategy.setFocus()
45
+ self.strategy.activateWindow()
40
46
 
41
47
  def empty(self):
42
48
  return self.strategy.empty()
@@ -143,6 +143,9 @@ class LayerCollectionHandler:
143
143
  def has_master_layer(self):
144
144
  return self.layer_collection.has_master_layer()
145
145
 
146
+ def set_layer(self, idx, img):
147
+ self.layer_collection.layer_stack[idx] = img
148
+
146
149
  def set_layer_stack(self, stk):
147
150
  self.layer_collection.set_layer_stack(stk)
148
151