shinestacker 1.3.0__tar.gz → 1.4.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.0 → shinestacker-1.4.0}/CHANGELOG.md +39 -1
  2. {shinestacker-1.3.0/src/shinestacker.egg-info → shinestacker-1.4.0}/PKG-INFO +1 -1
  3. {shinestacker-1.3.0 → shinestacker-1.4.0}/THIRD_PARTY_LICENSES.txt +6 -13
  4. {shinestacker-1.3.0 → shinestacker-1.4.0}/docs/alignment.md +18 -2
  5. shinestacker-1.4.0/index.html +47 -0
  6. shinestacker-1.4.0/src/shinestacker/_version.py +1 -0
  7. {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/algorithms/align.py +229 -41
  8. {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/algorithms/align_auto.py +15 -3
  9. {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/algorithms/align_parallel.py +81 -25
  10. {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/algorithms/balance.py +23 -13
  11. {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/algorithms/base_stack_algo.py +14 -20
  12. {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/algorithms/depth_map.py +9 -14
  13. {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/algorithms/noise_detection.py +3 -1
  14. {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/algorithms/pyramid.py +8 -22
  15. {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/algorithms/pyramid_auto.py +5 -14
  16. {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/algorithms/pyramid_tiles.py +18 -20
  17. {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/algorithms/stack_framework.py +1 -1
  18. {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/algorithms/utils.py +37 -10
  19. {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/algorithms/vignetting.py +2 -0
  20. {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/app/gui_utils.py +10 -0
  21. {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/app/main.py +3 -1
  22. {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/app/project.py +3 -1
  23. {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/app/retouch.py +3 -1
  24. {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/config/gui_constants.py +2 -2
  25. {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/core/core_utils.py +10 -1
  26. {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/gui/action_config.py +172 -7
  27. shinestacker-1.4.0/src/shinestacker/gui/action_config_dialog.py +864 -0
  28. {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/gui/colors.py +1 -0
  29. {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/gui/folder_file_selection.py +5 -0
  30. {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/gui/gui_run.py +2 -2
  31. {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/gui/main_window.py +18 -9
  32. {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/gui/menu_manager.py +26 -2
  33. {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/gui/new_project.py +5 -5
  34. {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/gui/project_controller.py +4 -0
  35. {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/gui/project_editor.py +6 -4
  36. shinestacker-1.4.0/src/shinestacker/gui/recent_file_manager.py +93 -0
  37. {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/gui/sys_mon.py +24 -23
  38. {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/retouch/base_filter.py +5 -5
  39. {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/retouch/brush_preview.py +3 -0
  40. {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/retouch/brush_tool.py +11 -11
  41. {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/retouch/display_manager.py +21 -37
  42. {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/retouch/image_editor_ui.py +129 -71
  43. shinestacker-1.4.0/src/shinestacker/retouch/image_view_status.py +61 -0
  44. shinestacker-1.4.0/src/shinestacker/retouch/image_viewer.py +123 -0
  45. {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/retouch/io_gui_handler.py +12 -2
  46. shinestacker-1.4.0/src/shinestacker/retouch/overlaid_view.py +212 -0
  47. {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/retouch/shortcuts_help.py +13 -3
  48. shinestacker-1.4.0/src/shinestacker/retouch/sidebyside_view.py +479 -0
  49. shinestacker-1.4.0/src/shinestacker/retouch/view_strategy.py +466 -0
  50. {shinestacker-1.3.0 → shinestacker-1.4.0/src/shinestacker.egg-info}/PKG-INFO +1 -1
  51. {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker.egg-info/SOURCES.txt +6 -0
  52. shinestacker-1.3.0/src/shinestacker/_version.py +0 -1
  53. shinestacker-1.3.0/src/shinestacker/gui/action_config_dialog.py +0 -873
  54. shinestacker-1.3.0/src/shinestacker/retouch/image_viewer.py +0 -465
  55. {shinestacker-1.3.0 → shinestacker-1.4.0}/.coveragerc +0 -0
  56. {shinestacker-1.3.0 → shinestacker-1.4.0}/.flake8 +0 -0
  57. {shinestacker-1.3.0 → shinestacker-1.4.0}/.github/workflows/ci-multiplatform.yml +0 -0
  58. {shinestacker-1.3.0 → shinestacker-1.4.0}/.github/workflows/pylint.yml +0 -0
  59. {shinestacker-1.3.0 → shinestacker-1.4.0}/.github/workflows/pypi-publish.yml +0 -0
  60. {shinestacker-1.3.0 → shinestacker-1.4.0}/.github/workflows/release.yml +0 -0
  61. {shinestacker-1.3.0 → shinestacker-1.4.0}/.gitignore +0 -0
  62. {shinestacker-1.3.0 → shinestacker-1.4.0}/.pylintrc +0 -0
  63. {shinestacker-1.3.0 → shinestacker-1.4.0}/.readthedocs.yaml +0 -0
  64. {shinestacker-1.3.0 → shinestacker-1.4.0}/LICENSE +0 -0
  65. {shinestacker-1.3.0 → shinestacker-1.4.0}/MANIFEST.in +0 -0
  66. {shinestacker-1.3.0 → shinestacker-1.4.0}/README.md +0 -0
  67. {shinestacker-1.3.0 → shinestacker-1.4.0}/docs/api.md +0 -0
  68. {shinestacker-1.3.0 → shinestacker-1.4.0}/docs/balancing.md +0 -0
  69. {shinestacker-1.3.0 → shinestacker-1.4.0}/docs/conf.py +0 -0
  70. {shinestacker-1.3.0 → shinestacker-1.4.0}/docs/focus_stacking.md +0 -0
  71. {shinestacker-1.3.0 → shinestacker-1.4.0}/docs/gui.md +0 -0
  72. {shinestacker-1.3.0 → shinestacker-1.4.0}/docs/index.md +0 -0
  73. {shinestacker-1.3.0 → shinestacker-1.4.0}/docs/job.md +0 -0
  74. {shinestacker-1.3.0 → shinestacker-1.4.0}/docs/main.md +0 -0
  75. {shinestacker-1.3.0 → shinestacker-1.4.0}/docs/multilayer.md +0 -0
  76. {shinestacker-1.3.0 → shinestacker-1.4.0}/docs/noise.md +0 -0
  77. {shinestacker-1.3.0 → shinestacker-1.4.0}/docs/requirements.txt +0 -0
  78. {shinestacker-1.3.0 → shinestacker-1.4.0}/docs/vignetting.md +0 -0
  79. {shinestacker-1.3.0 → shinestacker-1.4.0}/img/coffee.gif +0 -0
  80. {shinestacker-1.3.0 → shinestacker-1.4.0}/img/coffee_stack.jpg +0 -0
  81. {shinestacker-1.3.0 → shinestacker-1.4.0}/img/extreme-vignetting.jpg +0 -0
  82. {shinestacker-1.3.0 → shinestacker-1.4.0}/img/flies.gif +0 -0
  83. {shinestacker-1.3.0 → shinestacker-1.4.0}/img/flies_stack.jpg +0 -0
  84. {shinestacker-1.3.0 → shinestacker-1.4.0}/img/flow-diagram.png +0 -0
  85. {shinestacker-1.3.0 → shinestacker-1.4.0}/img/gui-finder.png +0 -0
  86. {shinestacker-1.3.0 → shinestacker-1.4.0}/img/gui-project-new.png +0 -0
  87. {shinestacker-1.3.0 → shinestacker-1.4.0}/img/gui-project-run.png +0 -0
  88. {shinestacker-1.3.0 → shinestacker-1.4.0}/img/gui-retouch.png +0 -0
  89. {shinestacker-1.3.0 → shinestacker-1.4.0}/pyproject.toml +0 -0
  90. {shinestacker-1.3.0 → shinestacker-1.4.0}/requirements.txt +0 -0
  91. {shinestacker-1.3.0 → shinestacker-1.4.0}/scripts/build_release.py +0 -0
  92. {shinestacker-1.3.0 → shinestacker-1.4.0}/scripts/git-rev-list.sh +0 -0
  93. {shinestacker-1.3.0 → shinestacker-1.4.0}/scripts/validate-tomli.py +0 -0
  94. {shinestacker-1.3.0 → shinestacker-1.4.0}/setup.cfg +0 -0
  95. {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/__init__.py +0 -0
  96. {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/algorithms/__init__.py +0 -0
  97. {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/algorithms/denoise.py +0 -0
  98. {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/algorithms/exif.py +0 -0
  99. {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/algorithms/multilayer.py +0 -0
  100. {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/algorithms/sharpen.py +0 -0
  101. {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/algorithms/stack.py +0 -0
  102. {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/algorithms/white_balance.py +0 -0
  103. {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/app/__init__.py +0 -0
  104. {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/app/about_dialog.py +0 -0
  105. {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/app/help_menu.py +0 -0
  106. {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/app/open_frames.py +0 -0
  107. {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/config/__init__.py +0 -0
  108. {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/config/config.py +0 -0
  109. {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/config/constants.py +0 -0
  110. {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/core/__init__.py +0 -0
  111. {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/core/colors.py +0 -0
  112. {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/core/exceptions.py +0 -0
  113. {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/core/framework.py +0 -0
  114. {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/core/logging.py +0 -0
  115. {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/gui/__init__.py +0 -0
  116. {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/gui/base_form_dialog.py +0 -0
  117. {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/gui/flow_layout.py +0 -0
  118. {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/gui/gui_images.py +0 -0
  119. {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/gui/gui_logging.py +0 -0
  120. {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/gui/ico/focus_stack_bkg.png +0 -0
  121. {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/gui/ico/shinestacker.icns +0 -0
  122. {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/gui/ico/shinestacker.ico +0 -0
  123. {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/gui/ico/shinestacker.png +0 -0
  124. {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/gui/ico/shinestacker.svg +0 -0
  125. {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/gui/img/close-round-line-icon.png +0 -0
  126. {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/gui/img/forward-button-icon.png +0 -0
  127. {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/gui/img/play-button-round-icon.png +0 -0
  128. {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/gui/img/plus-round-line-icon.png +0 -0
  129. {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/gui/project_converter.py +0 -0
  130. {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/gui/project_model.py +0 -0
  131. {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/gui/select_path_widget.py +0 -0
  132. {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/gui/tab_widget.py +0 -0
  133. {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/gui/time_progress_bar.py +0 -0
  134. {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/retouch/__init__.py +0 -0
  135. {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/retouch/brush.py +0 -0
  136. {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/retouch/brush_gradient.py +0 -0
  137. {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/retouch/denoise_filter.py +0 -0
  138. {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/retouch/exif_data.py +0 -0
  139. {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/retouch/file_loader.py +0 -0
  140. {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/retouch/filter_manager.py +0 -0
  141. {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/retouch/icon_container.py +0 -0
  142. {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/retouch/io_manager.py +0 -0
  143. {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/retouch/layer_collection.py +0 -0
  144. {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/retouch/undo_manager.py +0 -0
  145. {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/retouch/unsharp_mask_filter.py +0 -0
  146. {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/retouch/vignetting_filter.py +0 -0
  147. {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker/retouch/white_balance_filter.py +0 -0
  148. {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker.egg-info/dependency_links.txt +0 -0
  149. {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker.egg-info/entry_points.txt +0 -0
  150. {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker.egg-info/requires.txt +0 -0
  151. {shinestacker-1.3.0 → shinestacker-1.4.0}/src/shinestacker.egg-info/top_level.txt +0 -0
@@ -2,8 +2,46 @@
2
2
 
3
3
  This page reports the main releases only and the main changes therein.
4
4
 
5
+ ## [v1.4.0] - 2025-09-14
6
+ **GUI improvements**
5
7
 
6
- ## [v1.3.0] - 2025-09-05
8
+ ### Added
9
+ - added retouch view mode with master and frame side by side and top-bottom
10
+ - implemented "Open Recent" menu entry for both projects and retouch images
11
+ - expert options can be shown with a checkbox in each dialog
12
+ - optional summary plots for alignment transformation parameters
13
+
14
+ ## Fixed
15
+ - fixed bug in plot generation
16
+ - fixes warning due to missing glyph in PDF generation on macOS
17
+ - safer parallel plot generation using a thread locks
18
+
19
+ ### Changed
20
+ - code refactoring in various areas
21
+
22
+ ---
23
+
24
+ ### Changed
25
+ - code cleanup
26
+
27
+ ## [v1.3.1] - 2025-09-08
28
+ **Fixes and optimizations**
29
+
30
+ ## Fixed
31
+ - fixed input folder widget in job configuration
32
+ - better management of patological alignments
33
+ - restored alignment match plots
34
+
35
+ ### Changed
36
+ - improved automatic parameters for parallel alignment
37
+ - improved pyramid performances by combining two input steps
38
+ - improved performances of ORB and SURF feature extraction with key points caching
39
+ - improved configuration GUI using tabs and other minor GUI improvements
40
+ - code clean up and some fixes
41
+
42
+ ---
43
+
44
+ ## [v1.3.0] - 2025-09-06
7
45
  **Parallel processing and input flexibility**
8
46
 
9
47
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: shinestacker
3
- Version: 1.3.0
3
+ Version: 1.4.0
4
4
  Summary: ShineStacker
5
5
  Author-email: Luca Lista <luka.lista@gmail.com>
6
6
  License-Expression: LGPL-3.0
@@ -40,18 +40,17 @@ psdtags
40
40
  License: MIT
41
41
  https://pypi.org/project/psdtags/
42
42
 
43
+ -------------------------------------------------------------------------------
44
+ psutil
45
+ License: BSD-3-Clause
46
+ https://pypi.org/project/psutil/
47
+
43
48
  -------------------------------------------------------------------------------
44
49
  PySide6
45
50
  License: GNU Lesser General Public License v3.0 (LGPL-3.0)
46
51
  or commercial license from The Qt Company
47
52
  https://doc.qt.io/qtforpython/
48
53
 
49
- Full license text follows:
50
-
51
- --- BEGIN LGPL-3.0 LICENSE ---
52
- <qui incolla il testo completo della LGPL-3.0>
53
- --- END LGPL-3.0 LICENSE ---
54
-
55
54
  -------------------------------------------------------------------------------
56
55
  scipy
57
56
  License: BSD-3-Clause
@@ -67,14 +66,8 @@ tqdm
67
66
  License: Mozilla Public License 2.0 (MPL-2.0)
68
67
  https://github.com/tqdm/tqdm
69
68
 
70
- Full license text follows:
71
-
72
- --- BEGIN MPL-2.0 LICENSE ---
73
- <qui incolla il testo completo della MPL-2.0>
74
- --- END MPL-2.0 LICENSE ---
75
-
76
69
  -------------------------------------------------------------------------------
77
70
 
78
71
  NOTE:
79
72
  This file is provided for license compliance and attribution purposes.
80
- Your application code is licensed separately under the GNU Lesser General Public License v3.0.
73
+ Shine Stacker code is licensed separately under the GNU Lesser General Public License v3.0.
@@ -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
 
@@ -127,3 +136,10 @@ give raise to an exception.
127
136
  | 0.2887 | AKAZE | BRISK | NORM_HAMMING |
128
137
  | 0.4075 | AKAZE | SIFT | KNN |
129
138
  | 0.4397 | SIFT | SIFT | KNN |
139
+
140
+ ## References
141
+
142
+ For a detailed review of the various image registration methods, see the publication below:
143
+ * [A Review of Keypoints’ Detection and Feature Description in Image Registration](https://onlinelibrary.wiley.com/doi/10.1155/2021/8509164), Scientific Programming 2021, 8509164, doi:10.1155/2021/8509164
144
+
145
+
@@ -0,0 +1,47 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Shine Stacker</title>
7
+ <meta name="description" content="Shine Stacker – Open source focus stacking framework with GUI for macro and micro photography.">
8
+
9
+ <!-- Open Graph / Facebook -->
10
+ <meta property="og:type" content="website">
11
+ <meta property="og:title" content="Shine Stacker">
12
+ <meta property="og:description" content="Open source focus stacking framework with GUI for macro and micro photography.">
13
+ <meta property="og:image" content="https://raw.githubusercontent.com/lucalista/shinestacker/main/src/shinestacker/gui/ico/shinestacker.png">
14
+ <meta property="og:url" content="https://lucalista.github.io/shinestacker/">
15
+
16
+ <!-- Twitter Card -->
17
+ <meta name="twitter:card" content="summary_large_image">
18
+ <meta name="twitter:title" content="Shine Stacker">
19
+ <meta name="twitter:description" content="Open source focus stacking framework with GUI for macro and micro photography.">
20
+ <meta name="twitter:image" content="https://raw.githubusercontent.com/lucalista/shinestacker/main/src/shinestacker/gui/ico/shinestacker.png">
21
+ <style>
22
+ body { font-family: sans-serif; margin: 40px; line-height: 1.6; background: #f7f7f7; color: #333; }
23
+ h1 { color: #2c3e50; font-size: 48px}
24
+ a { color: #2980b9; text-decoration: none; }
25
+ a:hover { text-decoration: underline; }
26
+ .container { max-width: 800px; margin: auto; background: #fff; padding: 30px; border-radius: 10px; box-shadow: 0 4px 10px rgba(0,0,0,0.1);}
27
+ .logo { width: 150px; }
28
+ ul { text-align: left; }
29
+ .footer { color: #A0A0A0; }
30
+ </style>
31
+ </head>
32
+ <body>
33
+ <div class="container" style="text-align:center;">
34
+ <h1>Shine Stacker</h1>
35
+ <img src="https://raw.githubusercontent.com/lucalista/shinestacker/main/src/shinestacker/gui/ico/shinestacker.png" alt="Shine Stacker Logo" class="logo"> <p>Open source framework and GUI for focus stacking macro and micro photography.</p>
36
+ <h2><a href="https://lucalista.github.io/shinestacker/docs/main.html">Go to GitHub website</h2>
37
+ <ul>
38
+ <li><a href="https://shinestacker.wordpress.com/">Shine Stacker on Wordpress</a></li>
39
+ <li><a href="https://shinestacker.readthedocs.io/">Complete documentation</a></li>
40
+ <li><a href="https://github.com/lucalista/shinestacker">Source code and releases on GitHub</a></li>
41
+ </ul>
42
+ <hr style="color: #f0f0ff;">
43
+ <p class="footer">Shine Stacker, &copy; 2025, <a href="https://github.com/lucalista" alt="Luca Lista">Luca Lista</a> (LGPL-3.0).<br>
44
+ Logo © <a href="https://linktr.ee/alelista" alt="Alessandro Lista">Alessandro Lista</a> — All rights reserved.
45
+ </div>
46
+ </body>
47
+ </html>
@@ -0,0 +1 @@
1
+ __version__ = '1.4.0'
@@ -3,13 +3,15 @@ import os
3
3
  import math
4
4
  import logging
5
5
  import numpy as np
6
- import matplotlib.pyplot as plt
7
6
  import cv2
7
+ import matplotlib.pyplot as plt
8
8
  from .. config.constants import constants
9
9
  from .. core.exceptions import InvalidOptionError
10
10
  from .. core.colors import color_str
11
+ from .. core.core_utils import setup_matplotlib_mode
11
12
  from .utils import img_8bit, img_bw_8bit, save_plot, img_subsample
12
13
  from .stack_framework import SubAction
14
+ setup_matplotlib_mode()
13
15
 
14
16
  _DEFAULT_FEATURE_CONFIG = {
15
17
  'detector': constants.DEFAULT_DETECTOR,
@@ -75,7 +77,7 @@ def decompose_affine_matrix(m):
75
77
 
76
78
  def check_affine_matrix(m, img_shape, affine_thresholds=_AFFINE_THRESHOLDS):
77
79
  if affine_thresholds is None:
78
- return True, "No thresholds provided"
80
+ return True, "No thresholds provided", None
79
81
  (scale_x, scale_y), rotation, shear, (tx, ty) = decompose_affine_matrix(m)
80
82
  h, w = img_shape[:2]
81
83
  reasons = []
@@ -94,13 +96,14 @@ def check_affine_matrix(m, img_shape, affine_thresholds=_AFFINE_THRESHOLDS):
94
96
  if abs(ty) > max_ty:
95
97
  reasons.append(f"y-translation too large (|{ty:.1f}| > {max_ty:.1f})")
96
98
  if reasons:
97
- return False, "; ".join(reasons)
98
- return True, "Transformation within acceptable limits"
99
+ return False, "; ".join(reasons), None
100
+ return True, "Transformation within acceptable limits", \
101
+ (scale_x, scale_y, tx, ty, rotation, shear)
99
102
 
100
103
 
101
104
  def check_homography_distortion(m, img_shape, homography_thresholds=_HOMOGRAPHY_THRESHOLDS):
102
105
  if homography_thresholds is None:
103
- return True, "No thresholds provided"
106
+ return True, "No thresholds provided", None
104
107
  h, w = img_shape[:2]
105
108
  corners = np.array([[0, 0], [w, 0], [w, h], [0, h]], dtype=np.float32)
106
109
  transformed = cv2.perspectiveTransform(corners.reshape(1, -1, 2), m).reshape(-1, 2)
@@ -127,19 +130,20 @@ def check_homography_distortion(m, img_shape, homography_thresholds=_HOMOGRAPHY_
127
130
  if max_angle_dev > homography_thresholds['max_skew']:
128
131
  reasons.append(f"angle distortion too large ({max_angle_dev:.1f}°)")
129
132
  if reasons:
130
- return False, "; ".join(reasons)
131
- return True, "Transformation within acceptable limits"
133
+ return False, "; ".join(reasons), None
134
+ return True, "Transformation within acceptable limits", \
135
+ (area_ratio, aspect_ratio, max_angle_dev)
132
136
 
133
137
 
134
- def check_transform(m, img_0, transform_type,
138
+ def check_transform(m, img_shape, transform_type,
135
139
  affine_thresholds, homography_thresholds):
136
140
  if transform_type == constants.ALIGN_RIGID:
137
141
  return check_affine_matrix(
138
- m, img_0.shape, affine_thresholds)
142
+ m, img_shape, affine_thresholds)
139
143
  if transform_type == constants.ALIGN_HOMOGRAPHY:
140
144
  return check_homography_distortion(
141
- m, img_0.shape, homography_thresholds)
142
- return False, f'invalid transfrom option {transform_type}'
145
+ m, img_shape, homography_thresholds)
146
+ return False, f'invalid transfrom option {transform_type}', None
143
147
 
144
148
 
145
149
  def get_good_matches(des_0, des_ref, matching_config=None):
@@ -247,7 +251,10 @@ def find_transform(src_pts, dst_pts, transform=constants.DEFAULT_TRANSFORM,
247
251
  confidence=align_confidence / 100.0,
248
252
  refineIters=refine_iters)
249
253
  else:
250
- raise InvalidOptionError("transform", transform)
254
+ raise InvalidOptionError(
255
+ 'transform', method,
256
+ f". Valid options are: {constants.ALIGN_HOMOGRAPHY}, {constants.ALIGN_RIGID}"
257
+ )
251
258
  return result
252
259
 
253
260
 
@@ -270,6 +277,18 @@ def rescale_trasnsform(m, w0, h0, w_sub, h_sub, subsample, transform):
270
277
  return m
271
278
 
272
279
 
280
+ def plot_matches(msk, img_ref_sub, img_0_sub, kp_ref, kp_0, good_matches, plot_path):
281
+ matches_mask = msk.ravel().tolist()
282
+ img_match = cv2.cvtColor(cv2.drawMatches(
283
+ img_8bit(img_0_sub), kp_0, img_8bit(img_ref_sub),
284
+ kp_ref, good_matches, None, matchColor=(0, 255, 0),
285
+ singlePointColor=None, matchesMask=matches_mask,
286
+ flags=2), cv2.COLOR_BGR2RGB)
287
+ plt.figure(figsize=constants.PLT_FIG_SIZE)
288
+ plt.imshow(img_match, 'gray')
289
+ save_plot(plot_path)
290
+
291
+
273
292
  def align_images(img_ref, img_0, feature_config=None, matching_config=None, alignment_config=None,
274
293
  plot_path=None, callbacks=None,
275
294
  affine_thresholds=_AFFINE_THRESHOLDS,
@@ -315,22 +334,16 @@ def align_images(img_ref, img_0, feature_config=None, matching_config=None, alig
315
334
  m = None
316
335
  if n_good_matches >= min_matches:
317
336
  transform = alignment_config['transform']
318
- src_pts = np.float32([kp_0[m.queryIdx].pt for m in good_matches]).reshape(-1, 1, 2)
319
- dst_pts = np.float32([kp_ref[m.trainIdx].pt for m in good_matches]).reshape(-1, 1, 2)
337
+ src_pts = np.float32(
338
+ [kp_0[match.queryIdx].pt for match in good_matches]).reshape(-1, 1, 2)
339
+ dst_pts = np.float32(
340
+ [kp_ref[match.trainIdx].pt for match in good_matches]).reshape(-1, 1, 2)
320
341
  m, msk = find_transform(src_pts, dst_pts, transform, alignment_config['align_method'],
321
342
  *(alignment_config[k]
322
343
  for k in ['rans_threshold', 'max_iters',
323
344
  'align_confidence', 'refine_iters']))
324
345
  if plot_path is not None:
325
- matches_mask = msk.ravel().tolist()
326
- img_match = cv2.cvtColor(cv2.drawMatches(
327
- img_8bit(img_0_sub), kp_0, img_8bit(img_ref_sub),
328
- kp_ref, good_matches, None, matchColor=(0, 255, 0),
329
- singlePointColor=None, matchesMask=matches_mask,
330
- flags=2), cv2.COLOR_BGR2RGB)
331
- plt.figure(figsize=constants.PLT_FIG_SIZE)
332
- plt.imshow(img_match, 'gray')
333
- save_plot(plot_path)
346
+ plot_matches(msk, img_ref_sub, img_0_sub, kp_ref, kp_0, good_matches, plot_path)
334
347
  if callbacks and 'save_plot' in callbacks:
335
348
  callbacks['save_plot'](plot_path)
336
349
  h_sub, w_sub = img_0_sub.shape[:2]
@@ -339,9 +352,11 @@ def align_images(img_ref, img_0, feature_config=None, matching_config=None, alig
339
352
  if m is None:
340
353
  raise InvalidOptionError("transform", transform)
341
354
  transform_type = alignment_config['transform']
342
- is_valid, reason = check_transform(
343
- m, img_0, transform_type,
355
+ is_valid, reason, result = check_transform(
356
+ m, img_0.shape, transform_type,
344
357
  affine_thresholds, homography_thresholds)
358
+ if callbacks and 'save_transform_result' in callbacks:
359
+ callbacks['save_transform_result'](result)
345
360
  if not is_valid:
346
361
  if callbacks and 'warning' in callbacks:
347
362
  callbacks['warning'](f"invalid transformation: {reason}")
@@ -397,6 +412,18 @@ class AlignFramesBase(SubAction):
397
412
  for k in self.alignment_config:
398
413
  if k in kwargs:
399
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
400
427
 
401
428
  def align_images(self, idx, img_ref, img_0):
402
429
  pass
@@ -407,6 +434,15 @@ class AlignFramesBase(SubAction):
407
434
  def begin(self, process):
408
435
  self.process = process
409
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)
410
446
 
411
447
  def run_frame(self, idx, ref_idx, img_0):
412
448
  if idx == self.process.ref_idx:
@@ -422,24 +458,29 @@ class AlignFramesBase(SubAction):
422
458
  f"{os.path.basename(self.process.input_filepath(idx))}"
423
459
 
424
460
  def end(self):
425
- if self.plot_summary:
426
- plt.figure(figsize=constants.PLT_FIG_SIZE)
427
- 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)
428
464
  no_ref = x != self.process.ref_idx + 1
429
465
  x = x[no_ref]
430
- y = self._n_good_matches[no_ref]
466
+ y = np.array(items)[no_ref]
431
467
  if self.process.ref_idx == 0:
432
- y_max = y[1]
468
+ y_ref = y[1]
433
469
  elif self.process.ref_idx >= len(y):
434
- y_max = y[-1]
470
+ y_ref = y[-1]
435
471
  else:
436
- 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
437
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)
438
478
  plt.plot([self.process.ref_idx + 1, self.process.ref_idx + 1],
439
- [0, y_max], color='cornflowerblue', linestyle='--', label='reference frame')
479
+ [0, y_ref], color='cornflowerblue', linestyle='--', label='reference frame')
440
480
  plt.plot([x[0], x[-1]], [self.min_matches, self.min_matches], color='lightgray',
441
481
  linestyle='--', label='min. matches')
442
482
  plt.plot(x, y, color='navy', label='matches')
483
+ plt.title("Number of matches")
443
484
  plt.xlabel('frame')
444
485
  plt.ylabel('# of matches')
445
486
  plt.legend()
@@ -448,19 +489,160 @@ class AlignFramesBase(SubAction):
448
489
  plot_path = f"{self.process.working_path}/{self.process.plot_path}/" \
449
490
  f"{self.process.name}-matches.pdf"
450
491
  save_plot(plot_path)
451
- plt.close('all')
452
492
  self.process.callback(constants.CALLBACK_SAVE_PLOT, self.process.id,
453
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
+ )
454
639
 
455
640
 
456
641
  class AlignFrames(AlignFramesBase):
457
- def __init__(self, enabled=True, feature_config=None, matching_config=None,
458
- alignment_config=None, **kwargs):
459
- super().__init__(enabled)
460
-
461
642
  def align_images(self, idx, img_ref, img_0):
462
643
  idx_str = f"{idx:04d}"
463
644
  idx_tot_str = self.process.idx_tot_str(idx)
645
+
464
646
  callbacks = {
465
647
  'message': lambda: self.print_message(f'{idx_tot_str}: find matches'),
466
648
  'matches_message': lambda n: self.print_message(f'{idx_tot_str}: good matches: {n}'),
@@ -470,11 +652,14 @@ class AlignFrames(AlignFramesBase):
470
652
  f': {msg}', constants.LOG_COLOR_WARNING),
471
653
  'save_plot': lambda plot_path: self.process.callback(
472
654
  constants.CALLBACK_SAVE_PLOT, self.process.id,
473
- 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)
474
657
  }
475
658
  if self.plot_matches:
476
- plot_path = f"{self.process.working_path}/{self.process.plot_path}/" \
477
- f"{self.process.name}-matches-{idx_str}.pdf"
659
+ plot_path = os.path.join(
660
+ self.process.working_path,
661
+ self.process.plot_path,
662
+ f"{self.process.name}-matches-{idx_str}.pdf")
478
663
  else:
479
664
  plot_path = None
480
665
  affine_thresholds, homography_thresholds = self.get_transform_thresholds()
@@ -496,5 +681,8 @@ class AlignFrames(AlignFramesBase):
496
681
  return None
497
682
  return img
498
683
 
684
+ def relative_transformation(self):
685
+ return False
686
+
499
687
  def sequential_processing(self):
500
688
  return True
@@ -1,23 +1,27 @@
1
1
  # pylint: disable=C0114, C0115, C0116, W0718, R0912, R0915, E1101, R0914, R0911, E0606, R0801, R0902
2
2
  import os
3
+ import numpy as np
3
4
  from ..config.constants import constants
4
5
  from .align import AlignFramesBase, AlignFrames
5
6
  from .align_parallel import AlignFramesParallel
7
+ from .utils import get_first_image_file, get_img_metadata, read_img
6
8
 
7
9
 
8
10
  class AlignFramesAuto(AlignFramesBase):
9
11
  def __init__(self, enabled=True, feature_config=None, matching_config=None,
10
12
  alignment_config=None, **kwargs):
11
- super().__init__(enabled=True, feature_config=None, matching_config=None,
12
- alignment_config=None, **kwargs)
13
13
  self.mode = kwargs.pop('mode', constants.DEFAULT_ALIGN_MODE)
14
+ self.memory_limit = kwargs.pop('memory_limit', constants.DEFAULT_ALIGN_MEMORY_LIMIT_GB)
14
15
  self.max_threads = kwargs.pop('max_threads', constants.DEFAULT_ALIGN_MAX_THREADS)
15
16
  self.chunk_submit = kwargs.pop('chunk_submit', constants.DEFAULT_ALIGN_CHUNK_SUBMIT)
16
17
  self.bw_matching = kwargs.pop('bw_matching', constants.DEFAULT_ALIGN_BW_MATCHING)
17
18
  self.kwargs = kwargs
19
+ super().__init__(enabled=True, feature_config=None, matching_config=None,
20
+ alignment_config=None, **kwargs)
18
21
  available_cores = os.cpu_count() or 1
19
22
  self.num_threads = min(self.max_threads, available_cores)
20
23
  self._implementation = None
24
+ self.overhead = 30.0
21
25
 
22
26
  def begin(self, process):
23
27
  if self.mode == 'sequential' or self.num_threads == 1:
@@ -39,7 +43,15 @@ class AlignFramesAuto(AlignFramesBase):
39
43
  descriptor = constants.DEFAULT_DESCRIPTOR
40
44
  if detector in (constants.DETECTOR_SIFT, constants.DETECTOR_AKAZE) or \
41
45
  descriptor in (constants.DESCRIPTOR_SIFT, constants.DESCRIPTOR_AKAZE):
42
- num_threads = min(3, self.num_threads)
46
+ shape, dtype = get_img_metadata(
47
+ read_img(get_first_image_file(process.input_filepaths())))
48
+ bytes_per_pixel = 3 * np.dtype(dtype).itemsize
49
+ img_memory = bytes_per_pixel * float(shape[0]) * float(shape[1]) * \
50
+ self.overhead / constants.ONE_GIGA
51
+ num_threads = max(
52
+ 1,
53
+ int(round(self.memory_limit) / img_memory))
54
+ num_threads = min(num_threads, self.num_threads)
43
55
  chunk_submit = True
44
56
  else:
45
57
  num_threads = self.num_threads