shinestacker 1.3.1__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 (151) hide show
  1. {shinestacker-1.3.1 → shinestacker-1.5.0}/CHANGELOG.md +38 -1
  2. {shinestacker-1.3.1/src/shinestacker.egg-info → shinestacker-1.5.0}/PKG-INFO +7 -7
  3. {shinestacker-1.3.1 → shinestacker-1.5.0}/README.md +6 -6
  4. {shinestacker-1.3.1 → shinestacker-1.5.0}/docs/alignment.md +11 -2
  5. shinestacker-1.5.0/src/shinestacker/_version.py +1 -0
  6. {shinestacker-1.3.1 → shinestacker-1.5.0}/src/shinestacker/algorithms/align.py +198 -18
  7. {shinestacker-1.3.1 → shinestacker-1.5.0}/src/shinestacker/algorithms/align_parallel.py +17 -1
  8. {shinestacker-1.3.1 → shinestacker-1.5.0}/src/shinestacker/algorithms/balance.py +23 -13
  9. {shinestacker-1.3.1 → shinestacker-1.5.0}/src/shinestacker/algorithms/noise_detection.py +3 -1
  10. {shinestacker-1.3.1 → shinestacker-1.5.0}/src/shinestacker/algorithms/utils.py +21 -10
  11. {shinestacker-1.3.1 → shinestacker-1.5.0}/src/shinestacker/algorithms/vignetting.py +2 -0
  12. {shinestacker-1.3.1 → shinestacker-1.5.0}/src/shinestacker/app/main.py +1 -1
  13. {shinestacker-1.3.1 → shinestacker-1.5.0}/src/shinestacker/config/gui_constants.py +7 -2
  14. {shinestacker-1.3.1 → shinestacker-1.5.0}/src/shinestacker/core/core_utils.py +10 -1
  15. {shinestacker-1.3.1 → shinestacker-1.5.0}/src/shinestacker/gui/action_config.py +172 -7
  16. {shinestacker-1.3.1 → shinestacker-1.5.0}/src/shinestacker/gui/action_config_dialog.py +246 -285
  17. {shinestacker-1.3.1 → shinestacker-1.5.0}/src/shinestacker/gui/gui_run.py +2 -2
  18. {shinestacker-1.3.1 → shinestacker-1.5.0}/src/shinestacker/gui/main_window.py +14 -5
  19. {shinestacker-1.3.1 → shinestacker-1.5.0}/src/shinestacker/gui/menu_manager.py +26 -2
  20. {shinestacker-1.3.1 → shinestacker-1.5.0}/src/shinestacker/gui/project_controller.py +4 -0
  21. shinestacker-1.5.0/src/shinestacker/gui/recent_file_manager.py +93 -0
  22. {shinestacker-1.3.1 → shinestacker-1.5.0}/src/shinestacker/retouch/base_filter.py +13 -15
  23. {shinestacker-1.3.1 → shinestacker-1.5.0}/src/shinestacker/retouch/brush_preview.py +3 -1
  24. {shinestacker-1.3.1 → shinestacker-1.5.0}/src/shinestacker/retouch/brush_tool.py +11 -11
  25. {shinestacker-1.3.1 → shinestacker-1.5.0}/src/shinestacker/retouch/display_manager.py +43 -59
  26. {shinestacker-1.3.1 → shinestacker-1.5.0}/src/shinestacker/retouch/image_editor_ui.py +161 -82
  27. shinestacker-1.5.0/src/shinestacker/retouch/image_view_status.py +65 -0
  28. shinestacker-1.5.0/src/shinestacker/retouch/image_viewer.py +129 -0
  29. {shinestacker-1.3.1 → shinestacker-1.5.0}/src/shinestacker/retouch/io_gui_handler.py +12 -2
  30. {shinestacker-1.3.1 → shinestacker-1.5.0}/src/shinestacker/retouch/layer_collection.py +3 -0
  31. shinestacker-1.5.0/src/shinestacker/retouch/overlaid_view.py +215 -0
  32. {shinestacker-1.3.1 → shinestacker-1.5.0}/src/shinestacker/retouch/shortcuts_help.py +13 -3
  33. shinestacker-1.5.0/src/shinestacker/retouch/sidebyside_view.py +477 -0
  34. shinestacker-1.5.0/src/shinestacker/retouch/transformation_manager.py +43 -0
  35. {shinestacker-1.3.1 → shinestacker-1.5.0}/src/shinestacker/retouch/undo_manager.py +22 -3
  36. shinestacker-1.5.0/src/shinestacker/retouch/view_strategy.py +557 -0
  37. {shinestacker-1.3.1 → shinestacker-1.5.0/src/shinestacker.egg-info}/PKG-INFO +7 -7
  38. {shinestacker-1.3.1 → shinestacker-1.5.0}/src/shinestacker.egg-info/SOURCES.txt +6 -0
  39. shinestacker-1.3.1/src/shinestacker/_version.py +0 -1
  40. shinestacker-1.3.1/src/shinestacker/retouch/image_viewer.py +0 -465
  41. {shinestacker-1.3.1 → shinestacker-1.5.0}/.coveragerc +0 -0
  42. {shinestacker-1.3.1 → shinestacker-1.5.0}/.flake8 +0 -0
  43. {shinestacker-1.3.1 → shinestacker-1.5.0}/.github/workflows/ci-multiplatform.yml +0 -0
  44. {shinestacker-1.3.1 → shinestacker-1.5.0}/.github/workflows/pylint.yml +0 -0
  45. {shinestacker-1.3.1 → shinestacker-1.5.0}/.github/workflows/pypi-publish.yml +0 -0
  46. {shinestacker-1.3.1 → shinestacker-1.5.0}/.github/workflows/release.yml +0 -0
  47. {shinestacker-1.3.1 → shinestacker-1.5.0}/.gitignore +0 -0
  48. {shinestacker-1.3.1 → shinestacker-1.5.0}/.pylintrc +0 -0
  49. {shinestacker-1.3.1 → shinestacker-1.5.0}/.readthedocs.yaml +0 -0
  50. {shinestacker-1.3.1 → shinestacker-1.5.0}/LICENSE +0 -0
  51. {shinestacker-1.3.1 → shinestacker-1.5.0}/MANIFEST.in +0 -0
  52. {shinestacker-1.3.1 → shinestacker-1.5.0}/THIRD_PARTY_LICENSES.txt +0 -0
  53. {shinestacker-1.3.1 → shinestacker-1.5.0}/docs/api.md +0 -0
  54. {shinestacker-1.3.1 → shinestacker-1.5.0}/docs/balancing.md +0 -0
  55. {shinestacker-1.3.1 → shinestacker-1.5.0}/docs/conf.py +0 -0
  56. {shinestacker-1.3.1 → shinestacker-1.5.0}/docs/focus_stacking.md +0 -0
  57. {shinestacker-1.3.1 → shinestacker-1.5.0}/docs/gui.md +0 -0
  58. {shinestacker-1.3.1 → shinestacker-1.5.0}/docs/index.md +0 -0
  59. {shinestacker-1.3.1 → shinestacker-1.5.0}/docs/job.md +0 -0
  60. {shinestacker-1.3.1 → shinestacker-1.5.0}/docs/main.md +0 -0
  61. {shinestacker-1.3.1 → shinestacker-1.5.0}/docs/multilayer.md +0 -0
  62. {shinestacker-1.3.1 → shinestacker-1.5.0}/docs/noise.md +0 -0
  63. {shinestacker-1.3.1 → shinestacker-1.5.0}/docs/requirements.txt +0 -0
  64. {shinestacker-1.3.1 → shinestacker-1.5.0}/docs/vignetting.md +0 -0
  65. {shinestacker-1.3.1 → shinestacker-1.5.0}/img/coffee.gif +0 -0
  66. {shinestacker-1.3.1 → shinestacker-1.5.0}/img/coffee_stack.jpg +0 -0
  67. {shinestacker-1.3.1 → shinestacker-1.5.0}/img/extreme-vignetting.jpg +0 -0
  68. {shinestacker-1.3.1 → shinestacker-1.5.0}/img/flies.gif +0 -0
  69. {shinestacker-1.3.1 → shinestacker-1.5.0}/img/flies_stack.jpg +0 -0
  70. {shinestacker-1.3.1 → shinestacker-1.5.0}/img/flow-diagram.png +0 -0
  71. {shinestacker-1.3.1 → shinestacker-1.5.0}/img/gui-finder.png +0 -0
  72. {shinestacker-1.3.1 → shinestacker-1.5.0}/img/gui-project-new.png +0 -0
  73. {shinestacker-1.3.1 → shinestacker-1.5.0}/img/gui-project-run.png +0 -0
  74. {shinestacker-1.3.1 → shinestacker-1.5.0}/img/gui-retouch.png +0 -0
  75. {shinestacker-1.3.1 → shinestacker-1.5.0}/index.html +0 -0
  76. {shinestacker-1.3.1 → shinestacker-1.5.0}/pyproject.toml +0 -0
  77. {shinestacker-1.3.1 → shinestacker-1.5.0}/requirements.txt +0 -0
  78. {shinestacker-1.3.1 → shinestacker-1.5.0}/scripts/build_release.py +0 -0
  79. {shinestacker-1.3.1 → shinestacker-1.5.0}/scripts/git-rev-list.sh +0 -0
  80. {shinestacker-1.3.1 → shinestacker-1.5.0}/scripts/validate-tomli.py +0 -0
  81. {shinestacker-1.3.1 → shinestacker-1.5.0}/setup.cfg +0 -0
  82. {shinestacker-1.3.1 → shinestacker-1.5.0}/src/shinestacker/__init__.py +0 -0
  83. {shinestacker-1.3.1 → shinestacker-1.5.0}/src/shinestacker/algorithms/__init__.py +0 -0
  84. {shinestacker-1.3.1 → shinestacker-1.5.0}/src/shinestacker/algorithms/align_auto.py +0 -0
  85. {shinestacker-1.3.1 → shinestacker-1.5.0}/src/shinestacker/algorithms/base_stack_algo.py +0 -0
  86. {shinestacker-1.3.1 → shinestacker-1.5.0}/src/shinestacker/algorithms/denoise.py +0 -0
  87. {shinestacker-1.3.1 → shinestacker-1.5.0}/src/shinestacker/algorithms/depth_map.py +0 -0
  88. {shinestacker-1.3.1 → shinestacker-1.5.0}/src/shinestacker/algorithms/exif.py +0 -0
  89. {shinestacker-1.3.1 → shinestacker-1.5.0}/src/shinestacker/algorithms/multilayer.py +0 -0
  90. {shinestacker-1.3.1 → shinestacker-1.5.0}/src/shinestacker/algorithms/pyramid.py +0 -0
  91. {shinestacker-1.3.1 → shinestacker-1.5.0}/src/shinestacker/algorithms/pyramid_auto.py +0 -0
  92. {shinestacker-1.3.1 → shinestacker-1.5.0}/src/shinestacker/algorithms/pyramid_tiles.py +0 -0
  93. {shinestacker-1.3.1 → shinestacker-1.5.0}/src/shinestacker/algorithms/sharpen.py +0 -0
  94. {shinestacker-1.3.1 → shinestacker-1.5.0}/src/shinestacker/algorithms/stack.py +0 -0
  95. {shinestacker-1.3.1 → shinestacker-1.5.0}/src/shinestacker/algorithms/stack_framework.py +0 -0
  96. {shinestacker-1.3.1 → shinestacker-1.5.0}/src/shinestacker/algorithms/white_balance.py +0 -0
  97. {shinestacker-1.3.1 → shinestacker-1.5.0}/src/shinestacker/app/__init__.py +0 -0
  98. {shinestacker-1.3.1 → shinestacker-1.5.0}/src/shinestacker/app/about_dialog.py +0 -0
  99. {shinestacker-1.3.1 → shinestacker-1.5.0}/src/shinestacker/app/gui_utils.py +0 -0
  100. {shinestacker-1.3.1 → shinestacker-1.5.0}/src/shinestacker/app/help_menu.py +0 -0
  101. {shinestacker-1.3.1 → shinestacker-1.5.0}/src/shinestacker/app/open_frames.py +0 -0
  102. {shinestacker-1.3.1 → shinestacker-1.5.0}/src/shinestacker/app/project.py +0 -0
  103. {shinestacker-1.3.1 → shinestacker-1.5.0}/src/shinestacker/app/retouch.py +0 -0
  104. {shinestacker-1.3.1 → shinestacker-1.5.0}/src/shinestacker/config/__init__.py +0 -0
  105. {shinestacker-1.3.1 → shinestacker-1.5.0}/src/shinestacker/config/config.py +0 -0
  106. {shinestacker-1.3.1 → shinestacker-1.5.0}/src/shinestacker/config/constants.py +0 -0
  107. {shinestacker-1.3.1 → shinestacker-1.5.0}/src/shinestacker/core/__init__.py +0 -0
  108. {shinestacker-1.3.1 → shinestacker-1.5.0}/src/shinestacker/core/colors.py +0 -0
  109. {shinestacker-1.3.1 → shinestacker-1.5.0}/src/shinestacker/core/exceptions.py +0 -0
  110. {shinestacker-1.3.1 → shinestacker-1.5.0}/src/shinestacker/core/framework.py +0 -0
  111. {shinestacker-1.3.1 → shinestacker-1.5.0}/src/shinestacker/core/logging.py +0 -0
  112. {shinestacker-1.3.1 → shinestacker-1.5.0}/src/shinestacker/gui/__init__.py +0 -0
  113. {shinestacker-1.3.1 → shinestacker-1.5.0}/src/shinestacker/gui/base_form_dialog.py +0 -0
  114. {shinestacker-1.3.1 → shinestacker-1.5.0}/src/shinestacker/gui/colors.py +0 -0
  115. {shinestacker-1.3.1 → shinestacker-1.5.0}/src/shinestacker/gui/flow_layout.py +0 -0
  116. {shinestacker-1.3.1 → shinestacker-1.5.0}/src/shinestacker/gui/folder_file_selection.py +0 -0
  117. {shinestacker-1.3.1 → shinestacker-1.5.0}/src/shinestacker/gui/gui_images.py +0 -0
  118. {shinestacker-1.3.1 → shinestacker-1.5.0}/src/shinestacker/gui/gui_logging.py +0 -0
  119. {shinestacker-1.3.1 → shinestacker-1.5.0}/src/shinestacker/gui/ico/focus_stack_bkg.png +0 -0
  120. {shinestacker-1.3.1 → shinestacker-1.5.0}/src/shinestacker/gui/ico/shinestacker.icns +0 -0
  121. {shinestacker-1.3.1 → shinestacker-1.5.0}/src/shinestacker/gui/ico/shinestacker.ico +0 -0
  122. {shinestacker-1.3.1 → shinestacker-1.5.0}/src/shinestacker/gui/ico/shinestacker.png +0 -0
  123. {shinestacker-1.3.1 → shinestacker-1.5.0}/src/shinestacker/gui/ico/shinestacker.svg +0 -0
  124. {shinestacker-1.3.1 → shinestacker-1.5.0}/src/shinestacker/gui/img/close-round-line-icon.png +0 -0
  125. {shinestacker-1.3.1 → shinestacker-1.5.0}/src/shinestacker/gui/img/forward-button-icon.png +0 -0
  126. {shinestacker-1.3.1 → shinestacker-1.5.0}/src/shinestacker/gui/img/play-button-round-icon.png +0 -0
  127. {shinestacker-1.3.1 → shinestacker-1.5.0}/src/shinestacker/gui/img/plus-round-line-icon.png +0 -0
  128. {shinestacker-1.3.1 → shinestacker-1.5.0}/src/shinestacker/gui/new_project.py +0 -0
  129. {shinestacker-1.3.1 → shinestacker-1.5.0}/src/shinestacker/gui/project_converter.py +0 -0
  130. {shinestacker-1.3.1 → shinestacker-1.5.0}/src/shinestacker/gui/project_editor.py +0 -0
  131. {shinestacker-1.3.1 → shinestacker-1.5.0}/src/shinestacker/gui/project_model.py +0 -0
  132. {shinestacker-1.3.1 → shinestacker-1.5.0}/src/shinestacker/gui/select_path_widget.py +0 -0
  133. {shinestacker-1.3.1 → shinestacker-1.5.0}/src/shinestacker/gui/sys_mon.py +0 -0
  134. {shinestacker-1.3.1 → shinestacker-1.5.0}/src/shinestacker/gui/tab_widget.py +0 -0
  135. {shinestacker-1.3.1 → shinestacker-1.5.0}/src/shinestacker/gui/time_progress_bar.py +0 -0
  136. {shinestacker-1.3.1 → shinestacker-1.5.0}/src/shinestacker/retouch/__init__.py +0 -0
  137. {shinestacker-1.3.1 → shinestacker-1.5.0}/src/shinestacker/retouch/brush.py +0 -0
  138. {shinestacker-1.3.1 → shinestacker-1.5.0}/src/shinestacker/retouch/brush_gradient.py +0 -0
  139. {shinestacker-1.3.1 → shinestacker-1.5.0}/src/shinestacker/retouch/denoise_filter.py +0 -0
  140. {shinestacker-1.3.1 → shinestacker-1.5.0}/src/shinestacker/retouch/exif_data.py +0 -0
  141. {shinestacker-1.3.1 → shinestacker-1.5.0}/src/shinestacker/retouch/file_loader.py +0 -0
  142. {shinestacker-1.3.1 → shinestacker-1.5.0}/src/shinestacker/retouch/filter_manager.py +0 -0
  143. {shinestacker-1.3.1 → shinestacker-1.5.0}/src/shinestacker/retouch/icon_container.py +0 -0
  144. {shinestacker-1.3.1 → shinestacker-1.5.0}/src/shinestacker/retouch/io_manager.py +0 -0
  145. {shinestacker-1.3.1 → shinestacker-1.5.0}/src/shinestacker/retouch/unsharp_mask_filter.py +0 -0
  146. {shinestacker-1.3.1 → shinestacker-1.5.0}/src/shinestacker/retouch/vignetting_filter.py +0 -0
  147. {shinestacker-1.3.1 → shinestacker-1.5.0}/src/shinestacker/retouch/white_balance_filter.py +0 -0
  148. {shinestacker-1.3.1 → shinestacker-1.5.0}/src/shinestacker.egg-info/dependency_links.txt +0 -0
  149. {shinestacker-1.3.1 → shinestacker-1.5.0}/src/shinestacker.egg-info/entry_points.txt +0 -0
  150. {shinestacker-1.3.1 → shinestacker-1.5.0}/src/shinestacker.egg-info/requires.txt +0 -0
  151. {shinestacker-1.3.1 → shinestacker-1.5.0}/src/shinestacker.egg-info/top_level.txt +0 -0
@@ -2,11 +2,48 @@
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
+
22
+ ## [v1.4.0] - 2025-09-14
23
+ **GUI improvements**
24
+
25
+ ### Added
26
+ - added retouch view mode with master and frame side by side and top-bottom
27
+ - implemented "Open Recent" menu entry for both projects and retouch images
28
+ - expert options can be shown with a checkbox in each dialog
29
+ - optional summary plots for alignment transformation parameters
30
+
31
+ ### Fixed
32
+ - fixed bug in plot generation
33
+ - fixes warning due to missing glyph in PDF generation on macOS
34
+ - safer parallel plot generation using a thread locks
35
+
36
+ ### Changed
37
+ - code refactoring in various areas
38
+
39
+
5
40
  ## [v1.3.1] - 2025-09-08
41
+ **Fixes and optimizations**
6
42
 
7
- ## Fixed
43
+ ### Fixed
8
44
  - fixed input folder widget in job configuration
9
45
  - better management of patological alignments
46
+ - restored alignment match plots
10
47
 
11
48
  ### Changed
12
49
  - improved automatic parameters for parallel alignment
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: shinestacker
3
- Version: 1.3.1
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*
@@ -100,12 +100,21 @@ This class has extra parameters, in addition to the above ones:
100
100
 
101
101
  * ```max_threads``` (optional, default: ```2```): number of parallel processes allowed. The number of actual threads will not be greater than the number of available CPU cores.
102
102
  * ```chunk_submit``` (optional, default: ```True```): submit at most ```max_threads``` parallel processes. If ```chunk_submit``` is greater than ```max_threads``` a moderate performance gain is achieved at the cost of a possibly large memory occupancy.
103
- * ```bw_matching``` (optional, default: ```False```): perform matches on black and white version of the images in order to save memory. Preliminary tests indicate that the gain with this option is marginal, and this option may be dropped in the future.
103
+ * ```bw_matching``` (optional, default: ```False```): perform matches on black and white version of the images in order to save memory. Preliminary tests indicate that the gain with this option is marginal, and this option may be dropped in the future.
104
+
105
+ ## Automatic selection of processing strategy
106
+
107
+ A class ```AlignFramesAuto``` implements alignment with either sequential or parallel processing, and automatically tunes parallel processing parameters.
108
+ This class has extra parameters, in addition to the above ones:
109
+
110
+ * ```mode``` (optional, default: ```auto```): can be ```auto```, ```sequential``` or ```parallel```.
111
+ * ```memory_limit``` (optional, default: 8×1024<sup>3</sup>sup>): memory limit to determine optimal running parameters
112
+
104
113
 
105
114
  ## Allowed configurations
106
115
 
107
116
  ⚠️ Not all combinations of detector, descriptor and match methods are allowed. Combinations that are not allowed
108
- give raise to an exception.
117
+ give raise to an exception. This is automatically prevented if one works with the GUI, but may occur when using python scripting. Below the table of the allowed combination with a comparison of CPU performances.
109
118
 
110
119
  ## CPU performances
111
120
 
@@ -0,0 +1 @@
1
+ __version__ = '1.5.0'
@@ -5,13 +5,13 @@ import logging
5
5
  import numpy as np
6
6
  import cv2
7
7
  import matplotlib.pyplot as plt
8
- import matplotlib
9
8
  from .. config.constants import constants
10
9
  from .. core.exceptions import InvalidOptionError
11
10
  from .. core.colors import color_str
11
+ from .. core.core_utils import setup_matplotlib_mode
12
12
  from .utils import img_8bit, img_bw_8bit, save_plot, img_subsample
13
13
  from .stack_framework import SubAction
14
- matplotlib.use('Agg')
14
+ setup_matplotlib_mode()
15
15
 
16
16
  _DEFAULT_FEATURE_CONFIG = {
17
17
  'detector': constants.DEFAULT_DETECTOR,
@@ -135,14 +135,14 @@ def check_homography_distortion(m, img_shape, homography_thresholds=_HOMOGRAPHY_
135
135
  (area_ratio, aspect_ratio, max_angle_dev)
136
136
 
137
137
 
138
- def check_transform(m, img_0, transform_type,
138
+ def check_transform(m, img_shape, transform_type,
139
139
  affine_thresholds, homography_thresholds):
140
140
  if transform_type == constants.ALIGN_RIGID:
141
141
  return check_affine_matrix(
142
- m, img_0.shape, affine_thresholds)
142
+ m, img_shape, affine_thresholds)
143
143
  if transform_type == constants.ALIGN_HOMOGRAPHY:
144
144
  return check_homography_distortion(
145
- m, img_0.shape, homography_thresholds)
145
+ m, img_shape, homography_thresholds)
146
146
  return False, f'invalid transfrom option {transform_type}', None
147
147
 
148
148
 
@@ -251,7 +251,10 @@ def find_transform(src_pts, dst_pts, transform=constants.DEFAULT_TRANSFORM,
251
251
  confidence=align_confidence / 100.0,
252
252
  refineIters=refine_iters)
253
253
  else:
254
- raise InvalidOptionError("transform", transform)
254
+ raise InvalidOptionError(
255
+ 'transform', method,
256
+ f". Valid options are: {constants.ALIGN_HOMOGRAPHY}, {constants.ALIGN_RIGID}"
257
+ )
255
258
  return result
256
259
 
257
260
 
@@ -349,9 +352,11 @@ def align_images(img_ref, img_0, feature_config=None, matching_config=None, alig
349
352
  if m is None:
350
353
  raise InvalidOptionError("transform", transform)
351
354
  transform_type = alignment_config['transform']
352
- is_valid, reason, _result = check_transform(
353
- m, img_0, transform_type,
355
+ is_valid, reason, result = check_transform(
356
+ m, img_0.shape, transform_type,
354
357
  affine_thresholds, homography_thresholds)
358
+ if callbacks and 'save_transform_result' in callbacks:
359
+ callbacks['save_transform_result'](result)
355
360
  if not is_valid:
356
361
  if callbacks and 'warning' in callbacks:
357
362
  callbacks['warning'](f"invalid transformation: {reason}")
@@ -407,6 +412,18 @@ class AlignFramesBase(SubAction):
407
412
  for k in self.alignment_config:
408
413
  if k in kwargs:
409
414
  self.alignment_config[k] = kwargs[k]
415
+ self._area_ratio = None
416
+ self._aspect_ratio = None
417
+ self._max_angle_dev = None
418
+ self._scale_x = None
419
+ self._scale_y = None
420
+ self._translation_x = None
421
+ self._translation_y = None
422
+ self._rotation = None
423
+ self._shear = None
424
+
425
+ def relative_transformation(self):
426
+ return None
410
427
 
411
428
  def align_images(self, idx, img_ref, img_0):
412
429
  pass
@@ -417,6 +434,15 @@ class AlignFramesBase(SubAction):
417
434
  def begin(self, process):
418
435
  self.process = process
419
436
  self._n_good_matches = np.zeros(process.total_action_counts)
437
+ self._area_ratio = np.ones(process.total_action_counts)
438
+ self._aspect_ratio = np.ones(process.total_action_counts)
439
+ self._max_angle_dev = np.zeros(process.total_action_counts)
440
+ self._scale_x = np.ones(process.total_action_counts)
441
+ self._scale_y = np.ones(process.total_action_counts)
442
+ self._translation_x = np.zeros(process.total_action_counts)
443
+ self._translation_y = np.zeros(process.total_action_counts)
444
+ self._rotation = np.zeros(process.total_action_counts)
445
+ self._shear = np.zeros(process.total_action_counts)
420
446
 
421
447
  def run_frame(self, idx, ref_idx, img_0):
422
448
  if idx == self.process.ref_idx:
@@ -432,24 +458,29 @@ class AlignFramesBase(SubAction):
432
458
  f"{os.path.basename(self.process.input_filepath(idx))}"
433
459
 
434
460
  def end(self):
435
- if self.plot_summary:
436
- plt.figure(figsize=constants.PLT_FIG_SIZE)
437
- x = np.arange(1, len(self._n_good_matches) + 1, dtype=int)
461
+
462
+ def get_coordinates(items):
463
+ x = np.arange(1, len(items) + 1, dtype=int)
438
464
  no_ref = x != self.process.ref_idx + 1
439
465
  x = x[no_ref]
440
- y = np.array(self._n_good_matches)[no_ref]
466
+ y = np.array(items)[no_ref]
441
467
  if self.process.ref_idx == 0:
442
- y_max = y[1]
468
+ y_ref = y[1]
443
469
  elif self.process.ref_idx >= len(y):
444
- y_max = y[-1]
470
+ y_ref = y[-1]
445
471
  else:
446
- y_max = (y[self.process.ref_idx - 1] + y[self.process.ref_idx]) / 2
472
+ y_ref = (y[self.process.ref_idx - 1] + y[self.process.ref_idx]) / 2
473
+ return x, y, y_ref
447
474
 
475
+ if self.plot_summary:
476
+ plt.figure(figsize=constants.PLT_FIG_SIZE)
477
+ x, y, y_ref = get_coordinates(self._n_good_matches)
448
478
  plt.plot([self.process.ref_idx + 1, self.process.ref_idx + 1],
449
- [0, y_max], color='cornflowerblue', linestyle='--', label='reference frame')
479
+ [0, y_ref], color='cornflowerblue', linestyle='--', label='reference frame')
450
480
  plt.plot([x[0], x[-1]], [self.min_matches, self.min_matches], color='lightgray',
451
481
  linestyle='--', label='min. matches')
452
482
  plt.plot(x, y, color='navy', label='matches')
483
+ plt.title("Number of matches")
453
484
  plt.xlabel('frame')
454
485
  plt.ylabel('# of matches')
455
486
  plt.legend()
@@ -458,15 +489,160 @@ class AlignFramesBase(SubAction):
458
489
  plot_path = f"{self.process.working_path}/{self.process.plot_path}/" \
459
490
  f"{self.process.name}-matches.pdf"
460
491
  save_plot(plot_path)
461
- plt.close('all')
462
492
  self.process.callback(constants.CALLBACK_SAVE_PLOT, self.process.id,
463
493
  f"{self.process.name}: matches", plot_path)
494
+ transform = self.alignment_config['transform']
495
+ title = "Transformation parameters rel. to reference frame"
496
+ if transform == constants.ALIGN_RIGID:
497
+ plt.figure(figsize=constants.PLT_FIG_SIZE)
498
+ x, y, y_ref = get_coordinates(self._rotation)
499
+ plt.plot([self.process.ref_idx + 1, self.process.ref_idx + 1],
500
+ [0, y_ref], color='cornflowerblue',
501
+ linestyle='--', label='reference frame')
502
+ plt.plot([x[0], x[-1]], [0, 0], color='cornflowerblue', linestyle='--')
503
+ plt.plot(x, y, color='navy', label='rotation (°)')
504
+ y_lim = max(abs(y.min()), abs(y.max())) * 1.1
505
+ plt.ylim(-y_lim, y_lim)
506
+ plt.title(title)
507
+ plt.xlabel('frame')
508
+ plt.ylabel('rotation angle (degrees)')
509
+ plt.legend()
510
+ plt.xlim(x[0], x[-1])
511
+ plot_path = f"{self.process.working_path}/{self.process.plot_path}/" \
512
+ f"{self.process.name}-rotation.pdf"
513
+ save_plot(plot_path)
514
+ self.process.callback(constants.CALLBACK_SAVE_PLOT, self.process.id,
515
+ f"{self.process.name}: rotation", plot_path)
516
+ plt.figure(figsize=constants.PLT_FIG_SIZE)
517
+ x, y_x, y_x_ref = get_coordinates(self._translation_x)
518
+ x, y_y, y_y_ref = get_coordinates(self._translation_y)
519
+ plt.plot([self.process.ref_idx + 1, self.process.ref_idx + 1],
520
+ [y_x_ref, y_y_ref], color='cornflowerblue',
521
+ linestyle='--', label='reference frame')
522
+ plt.plot([x[0], x[-1]], [0, 0], color='cornflowerblue', linestyle='--')
523
+ plt.plot(x, y_x, color='blue', label='translation, x (px)')
524
+ plt.plot(x, y_y, color='red', label='translation, y (px)')
525
+ y_lim = max(abs(y_x.min()), abs(y_x.max()), abs(y_y.min()), abs(y_y.max())) * 1.1
526
+ plt.ylim(-y_lim, y_lim)
527
+ plt.title(title)
528
+ plt.xlabel('frame')
529
+ plt.ylabel('translation (pixels)')
530
+ plt.legend()
531
+ plt.xlim(x[0], x[-1])
532
+ plot_path = f"{self.process.working_path}/{self.process.plot_path}/" \
533
+ f"{self.process.name}-translation.pdf"
534
+ save_plot(plot_path)
535
+ self.process.callback(constants.CALLBACK_SAVE_PLOT, self.process.id,
536
+ f"{self.process.name}: translation", plot_path)
537
+
538
+ plt.figure(figsize=constants.PLT_FIG_SIZE)
539
+ x, y, y_ref = get_coordinates(self._scale_x)
540
+ plt.plot([self.process.ref_idx + 1, self.process.ref_idx + 1],
541
+ [1, y_ref], color='cornflowerblue',
542
+ linestyle='--', label='reference frame')
543
+ plt.plot([x[0], x[-1]], [1, 1], color='cornflowerblue', linestyle='--')
544
+ plt.plot(x, y, color='blue', label='scale factor')
545
+ d_max = max(abs(y.min() - 1), abs(y.max() - 1)) * 1.1
546
+ plt.ylim(1.0 - d_max, 1.0 + d_max)
547
+ plt.title(title)
548
+ plt.xlabel('frame')
549
+ plt.ylabel('scale factor')
550
+ plt.legend()
551
+ plt.xlim(x[0], x[-1])
552
+ plot_path = f"{self.process.working_path}/{self.process.plot_path}/" \
553
+ f"{self.process.name}-scale.pdf"
554
+ save_plot(plot_path)
555
+ self.process.callback(constants.CALLBACK_SAVE_PLOT, self.process.id,
556
+ f"{self.process.name}: scale", plot_path)
557
+ elif transform == constants.ALIGN_HOMOGRAPHY:
558
+ plt.figure(figsize=constants.PLT_FIG_SIZE)
559
+ x, y, y_ref = get_coordinates(self._area_ratio)
560
+ plt.plot([self.process.ref_idx + 1, self.process.ref_idx + 1],
561
+ [0, y_ref], color='cornflowerblue',
562
+ linestyle='--', label='reference frame')
563
+ plt.plot([x[0], x[-1]], [0, 0], color='cornflowerblue', linestyle='--')
564
+ plt.plot(x, y, color='navy', label='area ratio')
565
+ d_max = max(abs(y.min() - 1), abs(y.max() - 1)) * 1.1
566
+ plt.ylim(1.0 - d_max, 1.0 + d_max)
567
+ plt.title(title)
568
+ plt.xlabel('frame')
569
+ plt.ylabel('warped area ratio')
570
+ plt.legend()
571
+ plt.xlim(x[0], x[-1])
572
+ plot_path = f"{self.process.working_path}/{self.process.plot_path}/" \
573
+ f"{self.process.name}-area-ratio.pdf"
574
+ save_plot(plot_path)
575
+ self.process.callback(constants.CALLBACK_SAVE_PLOT, self.process.id,
576
+ f"{self.process.name}: area ratio", plot_path)
577
+ plt.figure(figsize=constants.PLT_FIG_SIZE)
578
+ x, y, y_ref = get_coordinates(self._aspect_ratio)
579
+ plt.plot([self.process.ref_idx + 1, self.process.ref_idx + 1],
580
+ [0, y_ref], color='cornflowerblue',
581
+ linestyle='--', label='reference frame')
582
+ plt.plot([x[0], x[-1]], [0, 0], color='cornflowerblue', linestyle='--')
583
+ plt.plot(x, y, color='navy', label='aspect ratio')
584
+ y_min, y_max = y.min(), y.max()
585
+ delta = y_max - y_min
586
+ plt.ylim(y_min - 0.05 * delta, y_max + 0.05 * delta)
587
+ plt.title(title)
588
+ plt.xlabel('frame')
589
+ plt.ylabel('aspect ratio')
590
+ plt.legend()
591
+ plt.xlim(x[0], x[-1])
592
+ plot_path = f"{self.process.working_path}/{self.process.plot_path}/" \
593
+ f"{self.process.name}-aspect-ratio.pdf"
594
+ save_plot(plot_path)
595
+ self.process.callback(constants.CALLBACK_SAVE_PLOT, self.process.id,
596
+ f"{self.process.name}: aspect ratio", plot_path)
597
+ plt.figure(figsize=constants.PLT_FIG_SIZE)
598
+ x, y, y_ref = get_coordinates(self._max_angle_dev)
599
+ plt.plot([self.process.ref_idx + 1, self.process.ref_idx + 1],
600
+ [0, y_ref], color='cornflowerblue',
601
+ linestyle='--', label='reference frame')
602
+ plt.plot([x[0], x[-1]], [0, 0], color='cornflowerblue', linestyle='--')
603
+ plt.plot(x, y, color='navy', label='max. dev. ang. (°)')
604
+ y_lim = max(abs(y.min()), abs(y.max())) * 1.1
605
+ plt.ylim(-y_lim, y_lim)
606
+ plt.title(title)
607
+ plt.xlabel('frame')
608
+ plt.ylabel('max deviation angle (degrees)')
609
+ plt.legend()
610
+ plt.xlim(x[0], x[-1])
611
+ plot_path = f"{self.process.working_path}/{self.process.plot_path}/" \
612
+ f"{self.process.name}-rotation.pdf"
613
+ save_plot(plot_path)
614
+ self.process.callback(constants.CALLBACK_SAVE_PLOT, self.process.id,
615
+ f"{self.process.name}: rotation", plot_path)
616
+
617
+ def save_transform_result(self, idx, result):
618
+ if result is None:
619
+ return
620
+ transform = self.alignment_config['transform']
621
+ if transform == constants.ALIGN_HOMOGRAPHY:
622
+ area_ratio, aspect_ratio, max_angle_dev = result
623
+ self._area_ratio[idx] = area_ratio
624
+ self._aspect_ratio[idx] = aspect_ratio
625
+ self._max_angle_dev[idx] = max_angle_dev
626
+ elif transform == constants.ALIGN_RIGID:
627
+ scale_x, scale_y, translation_x, translation_y, rotation, shear = result
628
+ self._scale_x[idx] = scale_x
629
+ self._scale_y[idx] = scale_y
630
+ self._translation_x[idx] = translation_x
631
+ self._translation_y[idx] = translation_y
632
+ self._rotation[idx] = rotation
633
+ self._shear[idx] = shear
634
+ else:
635
+ raise InvalidOptionError(
636
+ 'transform', transform,
637
+ f". Valid options are: {constants.ALIGN_HOMOGRAPHY}, {constants.ALIGN_RIGID}"
638
+ )
464
639
 
465
640
 
466
641
  class AlignFrames(AlignFramesBase):
467
642
  def align_images(self, idx, img_ref, img_0):
468
643
  idx_str = f"{idx:04d}"
469
644
  idx_tot_str = self.process.idx_tot_str(idx)
645
+
470
646
  callbacks = {
471
647
  'message': lambda: self.print_message(f'{idx_tot_str}: find matches'),
472
648
  'matches_message': lambda n: self.print_message(f'{idx_tot_str}: good matches: {n}'),
@@ -476,7 +652,8 @@ class AlignFrames(AlignFramesBase):
476
652
  f': {msg}', constants.LOG_COLOR_WARNING),
477
653
  'save_plot': lambda plot_path: self.process.callback(
478
654
  constants.CALLBACK_SAVE_PLOT, self.process.id,
479
- f"{self.process.name}: matches\nframe {idx_str}", plot_path)
655
+ f"{self.process.name}: matches\nframe {idx_str}", plot_path),
656
+ 'save_transform_result': lambda result: self.save_transform_result(idx, result)
480
657
  }
481
658
  if self.plot_matches:
482
659
  plot_path = os.path.join(
@@ -504,5 +681,8 @@ class AlignFrames(AlignFramesBase):
504
681
  return None
505
682
  return img
506
683
 
684
+ def relative_transformation(self):
685
+ return False
686
+
507
687
  def sequential_processing(self):
508
688
  return True
@@ -39,6 +39,7 @@ class AlignFramesParallel(AlignFramesBase):
39
39
  self.chunk_submit = kwargs.get('chunk_submit', constants.DEFAULT_ALIGN_CHUNK_SUBMIT)
40
40
  self.bw_matching = kwargs.get('bw_matching', constants.DEFAULT_ALIGN_BW_MATCHING)
41
41
  self._img_cache = None
42
+ self._img_shapes = None
42
43
  self._img_locks = None
43
44
  self._cache_locks = None
44
45
  self._target_indices = None
@@ -48,6 +49,9 @@ class AlignFramesParallel(AlignFramesBase):
48
49
  self._kp = None
49
50
  self._des = None
50
51
 
52
+ def relative_transformation(self):
53
+ return True
54
+
51
55
  def cache_img(self, idx):
52
56
  with self._cache_locks[idx]:
53
57
  self._img_locks[idx] += 1
@@ -56,6 +60,8 @@ class AlignFramesParallel(AlignFramesBase):
56
60
  if self.bw_matching:
57
61
  img = img_bw(img)
58
62
  self._img_cache[idx] = img
63
+ if img is not None:
64
+ self._img_shapes[idx] = img.shape
59
65
  return self._img_cache[idx]
60
66
 
61
67
  def submit_threads(self, idxs, imgs):
@@ -112,6 +118,7 @@ class AlignFramesParallel(AlignFramesBase):
112
118
  self.process.id, self.process.name, 2 * n_frames)
113
119
  input_filepaths = self.process.input_filepaths()
114
120
  self._img_cache = [None] * n_frames
121
+ self._img_shapes = [None] * n_frames
115
122
  self._img_locks = [0] * n_frames
116
123
  self._cache_locks = [threading.Lock() for _ in range(n_frames)]
117
124
  self._target_indices = [None] * n_frames
@@ -168,9 +175,17 @@ class AlignFramesParallel(AlignFramesBase):
168
175
  self._transforms[idx] = None
169
176
  gc.collect()
170
177
  missing_transforms = 0
178
+ thresholds = self.get_transform_thresholds()
171
179
  for i in range(n_frames):
172
180
  if self._cumulative_transforms[i] is not None:
173
181
  self._cumulative_transforms[i] = self._cumulative_transforms[i].astype(np.float32)
182
+ is_valid, _reason, result = check_transform(
183
+ self._cumulative_transforms[i], self._img_shapes[i],
184
+ transform_type, *thresholds)
185
+ if is_valid:
186
+ self.save_transform_result(i, result)
187
+ else:
188
+ self._cumulative_transforms[i] = None
174
189
  else:
175
190
  missing_transforms += 1
176
191
  msg = "feature extaction completed"
@@ -277,7 +292,8 @@ class AlignFramesParallel(AlignFramesBase):
277
292
  return self.extract_features(idx, delta + 1)
278
293
  transform_type = self.alignment_config['transform']
279
294
  thresholds = self.get_transform_thresholds()
280
- is_valid, _reason, _result = check_transform(m, img_0, transform_type, *thresholds)
295
+ is_valid, _reason, _result = check_transform(m, img_0.shape, transform_type, *thresholds)
296
+ # self.save_transform_result(idx, result)
281
297
  if not is_valid:
282
298
  msg = f"invalid transformation for {self.image_str(idx)}"
283
299
  do_abort = self.alignment_config['abort_abnormal']