shinestacker 1.2.1__tar.gz → 1.3.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 (144) hide show
  1. {shinestacker-1.2.1 → shinestacker-1.3.0}/CHANGELOG.md +27 -1
  2. {shinestacker-1.2.1/src/shinestacker.egg-info → shinestacker-1.3.0}/PKG-INFO +6 -6
  3. {shinestacker-1.2.1 → shinestacker-1.3.0}/README.md +4 -5
  4. {shinestacker-1.2.1 → shinestacker-1.3.0}/docs/alignment.md +8 -0
  5. {shinestacker-1.2.1 → shinestacker-1.3.0}/docs/focus_stacking.md +1 -0
  6. {shinestacker-1.2.1 → shinestacker-1.3.0}/docs/job.md +2 -0
  7. {shinestacker-1.2.1 → shinestacker-1.3.0}/docs/main.md +1 -1
  8. {shinestacker-1.2.1 → shinestacker-1.3.0}/pyproject.toml +1 -0
  9. {shinestacker-1.2.1 → shinestacker-1.3.0}/requirements.txt +1 -0
  10. shinestacker-1.3.0/src/shinestacker/_version.py +1 -0
  11. {shinestacker-1.2.1 → shinestacker-1.3.0}/src/shinestacker/algorithms/align.py +126 -94
  12. shinestacker-1.3.0/src/shinestacker/algorithms/align_auto.py +64 -0
  13. shinestacker-1.3.0/src/shinestacker/algorithms/align_parallel.py +296 -0
  14. {shinestacker-1.2.1 → shinestacker-1.3.0}/src/shinestacker/algorithms/balance.py +3 -1
  15. {shinestacker-1.2.1 → shinestacker-1.3.0}/src/shinestacker/algorithms/base_stack_algo.py +11 -2
  16. {shinestacker-1.2.1 → shinestacker-1.3.0}/src/shinestacker/algorithms/multilayer.py +8 -8
  17. {shinestacker-1.2.1 → shinestacker-1.3.0}/src/shinestacker/algorithms/noise_detection.py +10 -10
  18. {shinestacker-1.2.1 → shinestacker-1.3.0}/src/shinestacker/algorithms/pyramid.py +4 -4
  19. {shinestacker-1.2.1 → shinestacker-1.3.0}/src/shinestacker/algorithms/pyramid_auto.py +16 -10
  20. {shinestacker-1.2.1 → shinestacker-1.3.0}/src/shinestacker/algorithms/pyramid_tiles.py +19 -11
  21. {shinestacker-1.2.1 → shinestacker-1.3.0}/src/shinestacker/algorithms/stack.py +21 -17
  22. {shinestacker-1.2.1 → shinestacker-1.3.0}/src/shinestacker/algorithms/stack_framework.py +97 -46
  23. {shinestacker-1.2.1 → shinestacker-1.3.0}/src/shinestacker/algorithms/vignetting.py +13 -10
  24. {shinestacker-1.2.1 → shinestacker-1.3.0}/src/shinestacker/app/main.py +7 -3
  25. {shinestacker-1.2.1 → shinestacker-1.3.0}/src/shinestacker/config/constants.py +60 -25
  26. {shinestacker-1.2.1 → shinestacker-1.3.0}/src/shinestacker/config/gui_constants.py +1 -1
  27. {shinestacker-1.2.1 → shinestacker-1.3.0}/src/shinestacker/core/core_utils.py +4 -0
  28. {shinestacker-1.2.1 → shinestacker-1.3.0}/src/shinestacker/core/framework.py +104 -23
  29. {shinestacker-1.2.1 → shinestacker-1.3.0}/src/shinestacker/gui/action_config.py +4 -5
  30. {shinestacker-1.2.1 → shinestacker-1.3.0}/src/shinestacker/gui/action_config_dialog.py +152 -12
  31. {shinestacker-1.2.1 → shinestacker-1.3.0}/src/shinestacker/gui/base_form_dialog.py +2 -2
  32. shinestacker-1.3.0/src/shinestacker/gui/folder_file_selection.py +101 -0
  33. {shinestacker-1.2.1 → shinestacker-1.3.0}/src/shinestacker/gui/gui_run.py +12 -10
  34. {shinestacker-1.2.1 → shinestacker-1.3.0}/src/shinestacker/gui/main_window.py +6 -1
  35. shinestacker-1.3.0/src/shinestacker/gui/new_project.py +348 -0
  36. {shinestacker-1.2.1 → shinestacker-1.3.0}/src/shinestacker/gui/project_controller.py +10 -6
  37. {shinestacker-1.2.1 → shinestacker-1.3.0}/src/shinestacker/gui/project_converter.py +4 -2
  38. {shinestacker-1.2.1 → shinestacker-1.3.0}/src/shinestacker/gui/project_editor.py +37 -27
  39. {shinestacker-1.2.1 → shinestacker-1.3.0}/src/shinestacker/gui/select_path_widget.py +1 -1
  40. shinestacker-1.3.0/src/shinestacker/gui/sys_mon.py +96 -0
  41. {shinestacker-1.2.1 → shinestacker-1.3.0}/src/shinestacker/gui/time_progress_bar.py +4 -3
  42. {shinestacker-1.2.1 → shinestacker-1.3.0}/src/shinestacker/retouch/exif_data.py +1 -1
  43. {shinestacker-1.2.1 → shinestacker-1.3.0}/src/shinestacker/retouch/image_editor_ui.py +2 -0
  44. {shinestacker-1.2.1 → shinestacker-1.3.0/src/shinestacker.egg-info}/PKG-INFO +6 -6
  45. {shinestacker-1.2.1 → shinestacker-1.3.0}/src/shinestacker.egg-info/SOURCES.txt +4 -0
  46. {shinestacker-1.2.1 → shinestacker-1.3.0}/src/shinestacker.egg-info/requires.txt +1 -0
  47. shinestacker-1.2.1/src/shinestacker/_version.py +0 -1
  48. shinestacker-1.2.1/src/shinestacker/gui/new_project.py +0 -250
  49. {shinestacker-1.2.1 → shinestacker-1.3.0}/.coveragerc +0 -0
  50. {shinestacker-1.2.1 → shinestacker-1.3.0}/.flake8 +0 -0
  51. {shinestacker-1.2.1 → shinestacker-1.3.0}/.github/workflows/ci-multiplatform.yml +0 -0
  52. {shinestacker-1.2.1 → shinestacker-1.3.0}/.github/workflows/pylint.yml +0 -0
  53. {shinestacker-1.2.1 → shinestacker-1.3.0}/.github/workflows/pypi-publish.yml +0 -0
  54. {shinestacker-1.2.1 → shinestacker-1.3.0}/.github/workflows/release.yml +0 -0
  55. {shinestacker-1.2.1 → shinestacker-1.3.0}/.gitignore +0 -0
  56. {shinestacker-1.2.1 → shinestacker-1.3.0}/.pylintrc +0 -0
  57. {shinestacker-1.2.1 → shinestacker-1.3.0}/.readthedocs.yaml +0 -0
  58. {shinestacker-1.2.1 → shinestacker-1.3.0}/LICENSE +0 -0
  59. {shinestacker-1.2.1 → shinestacker-1.3.0}/MANIFEST.in +0 -0
  60. {shinestacker-1.2.1 → shinestacker-1.3.0}/THIRD_PARTY_LICENSES.txt +0 -0
  61. {shinestacker-1.2.1 → shinestacker-1.3.0}/docs/api.md +0 -0
  62. {shinestacker-1.2.1 → shinestacker-1.3.0}/docs/balancing.md +0 -0
  63. {shinestacker-1.2.1 → shinestacker-1.3.0}/docs/conf.py +0 -0
  64. {shinestacker-1.2.1 → shinestacker-1.3.0}/docs/gui.md +0 -0
  65. {shinestacker-1.2.1 → shinestacker-1.3.0}/docs/index.md +0 -0
  66. {shinestacker-1.2.1 → shinestacker-1.3.0}/docs/multilayer.md +0 -0
  67. {shinestacker-1.2.1 → shinestacker-1.3.0}/docs/noise.md +0 -0
  68. {shinestacker-1.2.1 → shinestacker-1.3.0}/docs/requirements.txt +0 -0
  69. {shinestacker-1.2.1 → shinestacker-1.3.0}/docs/vignetting.md +0 -0
  70. {shinestacker-1.2.1 → shinestacker-1.3.0}/img/coffee.gif +0 -0
  71. {shinestacker-1.2.1 → shinestacker-1.3.0}/img/coffee_stack.jpg +0 -0
  72. {shinestacker-1.2.1 → shinestacker-1.3.0}/img/extreme-vignetting.jpg +0 -0
  73. {shinestacker-1.2.1 → shinestacker-1.3.0}/img/flies.gif +0 -0
  74. {shinestacker-1.2.1 → shinestacker-1.3.0}/img/flies_stack.jpg +0 -0
  75. {shinestacker-1.2.1 → shinestacker-1.3.0}/img/flow-diagram.png +0 -0
  76. {shinestacker-1.2.1 → shinestacker-1.3.0}/img/gui-finder.png +0 -0
  77. {shinestacker-1.2.1 → shinestacker-1.3.0}/img/gui-project-new.png +0 -0
  78. {shinestacker-1.2.1 → shinestacker-1.3.0}/img/gui-project-run.png +0 -0
  79. {shinestacker-1.2.1 → shinestacker-1.3.0}/img/gui-retouch.png +0 -0
  80. {shinestacker-1.2.1 → shinestacker-1.3.0}/scripts/build_release.py +0 -0
  81. {shinestacker-1.2.1 → shinestacker-1.3.0}/scripts/git-rev-list.sh +0 -0
  82. {shinestacker-1.2.1 → shinestacker-1.3.0}/scripts/validate-tomli.py +0 -0
  83. {shinestacker-1.2.1 → shinestacker-1.3.0}/setup.cfg +0 -0
  84. {shinestacker-1.2.1 → shinestacker-1.3.0}/src/shinestacker/__init__.py +0 -0
  85. {shinestacker-1.2.1 → shinestacker-1.3.0}/src/shinestacker/algorithms/__init__.py +0 -0
  86. {shinestacker-1.2.1 → shinestacker-1.3.0}/src/shinestacker/algorithms/denoise.py +0 -0
  87. {shinestacker-1.2.1 → shinestacker-1.3.0}/src/shinestacker/algorithms/depth_map.py +0 -0
  88. {shinestacker-1.2.1 → shinestacker-1.3.0}/src/shinestacker/algorithms/exif.py +0 -0
  89. {shinestacker-1.2.1 → shinestacker-1.3.0}/src/shinestacker/algorithms/sharpen.py +0 -0
  90. {shinestacker-1.2.1 → shinestacker-1.3.0}/src/shinestacker/algorithms/utils.py +0 -0
  91. {shinestacker-1.2.1 → shinestacker-1.3.0}/src/shinestacker/algorithms/white_balance.py +0 -0
  92. {shinestacker-1.2.1 → shinestacker-1.3.0}/src/shinestacker/app/__init__.py +0 -0
  93. {shinestacker-1.2.1 → shinestacker-1.3.0}/src/shinestacker/app/about_dialog.py +0 -0
  94. {shinestacker-1.2.1 → shinestacker-1.3.0}/src/shinestacker/app/gui_utils.py +0 -0
  95. {shinestacker-1.2.1 → shinestacker-1.3.0}/src/shinestacker/app/help_menu.py +0 -0
  96. {shinestacker-1.2.1 → shinestacker-1.3.0}/src/shinestacker/app/open_frames.py +0 -0
  97. {shinestacker-1.2.1 → shinestacker-1.3.0}/src/shinestacker/app/project.py +0 -0
  98. {shinestacker-1.2.1 → shinestacker-1.3.0}/src/shinestacker/app/retouch.py +0 -0
  99. {shinestacker-1.2.1 → shinestacker-1.3.0}/src/shinestacker/config/__init__.py +0 -0
  100. {shinestacker-1.2.1 → shinestacker-1.3.0}/src/shinestacker/config/config.py +0 -0
  101. {shinestacker-1.2.1 → shinestacker-1.3.0}/src/shinestacker/core/__init__.py +0 -0
  102. {shinestacker-1.2.1 → shinestacker-1.3.0}/src/shinestacker/core/colors.py +0 -0
  103. {shinestacker-1.2.1 → shinestacker-1.3.0}/src/shinestacker/core/exceptions.py +0 -0
  104. {shinestacker-1.2.1 → shinestacker-1.3.0}/src/shinestacker/core/logging.py +0 -0
  105. {shinestacker-1.2.1 → shinestacker-1.3.0}/src/shinestacker/gui/__init__.py +0 -0
  106. {shinestacker-1.2.1 → shinestacker-1.3.0}/src/shinestacker/gui/colors.py +0 -0
  107. {shinestacker-1.2.1 → shinestacker-1.3.0}/src/shinestacker/gui/flow_layout.py +0 -0
  108. {shinestacker-1.2.1 → shinestacker-1.3.0}/src/shinestacker/gui/gui_images.py +0 -0
  109. {shinestacker-1.2.1 → shinestacker-1.3.0}/src/shinestacker/gui/gui_logging.py +0 -0
  110. {shinestacker-1.2.1 → shinestacker-1.3.0}/src/shinestacker/gui/ico/focus_stack_bkg.png +0 -0
  111. {shinestacker-1.2.1 → shinestacker-1.3.0}/src/shinestacker/gui/ico/shinestacker.icns +0 -0
  112. {shinestacker-1.2.1 → shinestacker-1.3.0}/src/shinestacker/gui/ico/shinestacker.ico +0 -0
  113. {shinestacker-1.2.1 → shinestacker-1.3.0}/src/shinestacker/gui/ico/shinestacker.png +0 -0
  114. {shinestacker-1.2.1 → shinestacker-1.3.0}/src/shinestacker/gui/ico/shinestacker.svg +0 -0
  115. {shinestacker-1.2.1 → shinestacker-1.3.0}/src/shinestacker/gui/img/close-round-line-icon.png +0 -0
  116. {shinestacker-1.2.1 → shinestacker-1.3.0}/src/shinestacker/gui/img/forward-button-icon.png +0 -0
  117. {shinestacker-1.2.1 → shinestacker-1.3.0}/src/shinestacker/gui/img/play-button-round-icon.png +0 -0
  118. {shinestacker-1.2.1 → shinestacker-1.3.0}/src/shinestacker/gui/img/plus-round-line-icon.png +0 -0
  119. {shinestacker-1.2.1 → shinestacker-1.3.0}/src/shinestacker/gui/menu_manager.py +0 -0
  120. {shinestacker-1.2.1 → shinestacker-1.3.0}/src/shinestacker/gui/project_model.py +0 -0
  121. {shinestacker-1.2.1 → shinestacker-1.3.0}/src/shinestacker/gui/tab_widget.py +0 -0
  122. {shinestacker-1.2.1 → shinestacker-1.3.0}/src/shinestacker/retouch/__init__.py +0 -0
  123. {shinestacker-1.2.1 → shinestacker-1.3.0}/src/shinestacker/retouch/base_filter.py +0 -0
  124. {shinestacker-1.2.1 → shinestacker-1.3.0}/src/shinestacker/retouch/brush.py +0 -0
  125. {shinestacker-1.2.1 → shinestacker-1.3.0}/src/shinestacker/retouch/brush_gradient.py +0 -0
  126. {shinestacker-1.2.1 → shinestacker-1.3.0}/src/shinestacker/retouch/brush_preview.py +0 -0
  127. {shinestacker-1.2.1 → shinestacker-1.3.0}/src/shinestacker/retouch/brush_tool.py +0 -0
  128. {shinestacker-1.2.1 → shinestacker-1.3.0}/src/shinestacker/retouch/denoise_filter.py +0 -0
  129. {shinestacker-1.2.1 → shinestacker-1.3.0}/src/shinestacker/retouch/display_manager.py +0 -0
  130. {shinestacker-1.2.1 → shinestacker-1.3.0}/src/shinestacker/retouch/file_loader.py +0 -0
  131. {shinestacker-1.2.1 → shinestacker-1.3.0}/src/shinestacker/retouch/filter_manager.py +0 -0
  132. {shinestacker-1.2.1 → shinestacker-1.3.0}/src/shinestacker/retouch/icon_container.py +0 -0
  133. {shinestacker-1.2.1 → shinestacker-1.3.0}/src/shinestacker/retouch/image_viewer.py +0 -0
  134. {shinestacker-1.2.1 → shinestacker-1.3.0}/src/shinestacker/retouch/io_gui_handler.py +0 -0
  135. {shinestacker-1.2.1 → shinestacker-1.3.0}/src/shinestacker/retouch/io_manager.py +0 -0
  136. {shinestacker-1.2.1 → shinestacker-1.3.0}/src/shinestacker/retouch/layer_collection.py +0 -0
  137. {shinestacker-1.2.1 → shinestacker-1.3.0}/src/shinestacker/retouch/shortcuts_help.py +0 -0
  138. {shinestacker-1.2.1 → shinestacker-1.3.0}/src/shinestacker/retouch/undo_manager.py +0 -0
  139. {shinestacker-1.2.1 → shinestacker-1.3.0}/src/shinestacker/retouch/unsharp_mask_filter.py +0 -0
  140. {shinestacker-1.2.1 → shinestacker-1.3.0}/src/shinestacker/retouch/vignetting_filter.py +0 -0
  141. {shinestacker-1.2.1 → shinestacker-1.3.0}/src/shinestacker/retouch/white_balance_filter.py +0 -0
  142. {shinestacker-1.2.1 → shinestacker-1.3.0}/src/shinestacker.egg-info/dependency_links.txt +0 -0
  143. {shinestacker-1.2.1 → shinestacker-1.3.0}/src/shinestacker.egg-info/entry_points.txt +0 -0
  144. {shinestacker-1.2.1 → shinestacker-1.3.0}/src/shinestacker.egg-info/top_level.txt +0 -0
@@ -2,8 +2,32 @@
2
2
 
3
3
  This page reports the main releases only and the main changes therein.
4
4
 
5
- ## [v1.2.1] - 2025-09-01
5
+
6
+ ## [v1.3.0] - 2025-09-05
7
+ **Parallel processing and input flexibility**
8
+
9
+ ### Added
10
+ - Parallel processing in alignment feature extraction
11
+ - Parallel processing of combined actions
12
+ - Job input can now specify a list of files (not only a folder)
13
+ - CPU and memory usage monitor widget for running jobs
14
+
15
+ ### Fixed
16
+ - Path in example project
17
+ - Bug fix in config dialog
18
+
19
+ ### Changed
20
+ - Changes some default parameters for better performances
21
+ - Some GUI restyling
22
+ - Code cleanup
23
+
24
+ ---
25
+
26
+ ## [v1.2.1] - 2025-09-02
6
27
  **Bug fixes and minor improvements**
28
+
29
+ ### Changes
30
+
7
31
  * alignment is more tolerant in case of failures: frames are skipped and the running job is not stopped
8
32
  * fixed the -x (--expert) option
9
33
  * more safety checks prevent crashes for abnormal conditions
@@ -16,6 +40,8 @@ This page reports the main releases only and the main changes therein.
16
40
  ## [v1.2.0] - 2025-08-31
17
41
  **Parallel processing and more improvements**
18
42
 
43
+ ### Changes
44
+
19
45
  * Implemented parallel processing for pyramid stacking algorithm
20
46
  * optimized pyramid algorithm: selects automatically the best within the given memory budget to avoid memory issues in case many pictures are selected. Explicit configuration is also possible for specific needs.
21
47
  * Implemented automatic subsample option for alignment, balancing and vignetting, now default
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: shinestacker
3
- Version: 1.2.1
3
+ Version: 1.3.0
4
4
  Summary: ShineStacker
5
5
  Author-email: Luca Lista <luka.lista@gmail.com>
6
6
  License-Expression: LGPL-3.0
@@ -20,6 +20,7 @@ Requires-Dist: numpy
20
20
  Requires-Dist: opencv_python
21
21
  Requires-Dist: pillow
22
22
  Requires-Dist: psdtags
23
+ Requires-Dist: psutil
23
24
  Requires-Dist: PySide6
24
25
  Requires-Dist: scipy
25
26
  Requires-Dist: tifffile
@@ -69,6 +70,10 @@ The GUI has two main working areas:
69
70
 
70
71
  <img src='https://raw.githubusercontent.com/lucalista/shinestacker/main/img/gui-retouch.png' width="600" referrerpolicy="no-referrer">
71
72
 
73
+ # Resources
74
+
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
+
72
77
  # Note for macOS users
73
78
 
74
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).**
@@ -88,11 +93,6 @@ xattr -cr ~/Downloads/shinestacker/shinestacker.app
88
93
 
89
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.
90
95
 
91
- # Resources
92
-
93
- 🌍 [Website on WordPress](https://shinestacker.wordpress.com) • 📖 [Main documentation](https://shinestacker.readthedocs.io) • 📝 [Changelog](https://github.com/lucalista/shinestacker/blob/main/CHANGELOG.md)
94
-
95
-
96
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.
@@ -38,6 +38,10 @@ 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
42
+
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
+
41
45
  # Note for macOS users
42
46
 
43
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).**
@@ -57,11 +61,6 @@ xattr -cr ~/Downloads/shinestacker/shinestacker.app
57
61
 
58
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.
59
63
 
60
- # Resources
61
-
62
- 🌍 [Website on WordPress](https://shinestacker.wordpress.com) • 📖 [Main documentation](https://shinestacker.readthedocs.io) • 📝 [Changelog](https://github.com/lucalista/shinestacker/blob/main/CHANGELOG.md)
63
-
64
-
65
64
  # Credits
66
65
 
67
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.
@@ -93,6 +93,14 @@ alignment_config = {
93
93
  * ```plot_matches``` (optional, default: ```False```): if ```True```, for each image matches with reference frame are drawn. May be useful for inspection and debugging.
94
94
  * ```enabled``` (optional, default: ```True```): allows to switch on and off this module.
95
95
 
96
+ ## Parallel processing
97
+
98
+ A class ```AlignFramesParallel``` implements alignment using parallel processing.
99
+ This class has extra parameters, in addition to the above ones:
100
+
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
+ * ```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.
96
104
 
97
105
  ## Allowed configurations
98
106
 
@@ -48,6 +48,7 @@ Arguments for the constructor are, in addition to the ones for ```PyramidStack``
48
48
  * ```tile_size``` (optional, default: 512): size of a time
49
49
  * ```n_tiled_layers``` (optional, default: 2): number of layers that are tiled. Usually the last one or two are the ones that take more memory.
50
50
  * ```max_threads``` (optional, default: number of cores, up to a maximum of 8): maximum number of thread used for parallel processing. The actual number of threads does not exceed the number of available cores.
51
+ * ```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.
51
52
 
52
53
 
53
54
  ```PyramidAutoStack```, pyramid algorithn with capability to automatically switch from all-in-memory to I/O buffered tiled.
@@ -44,3 +44,5 @@ Arguments for the constructor of ```CombinedActions``` are for the :
44
44
  * ```reference_index``` (optional, default: 0): the index of the image used as reference. Images are numbered starting from one to N. -1 is interpreted as the last image, 0 as the median index.
45
45
  * ```step_process``` (optional): if equal to ```True``` (default), each image is processed with respect to the previous or next image, depending if its file is placed in alphabetic order after or befor the reference image.
46
46
  * ```enabled``` (optional, default: ```True```): allows to switch on and off this module.
47
+ * ```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.
48
+ * ```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.
@@ -87,7 +87,7 @@ job.run()
87
87
 
88
88
  ### Core processing
89
89
  ```bash
90
- pip install imagecodecs matplotlib numpy opencv-python pillow psdtags scipy setuptools-scm tifffile tqdm
90
+ pip install imagecodecs matplotlib numpy opencv-python pillow psdtags psutil scipy setuptools-scm tifffile tqdm
91
91
  ```
92
92
  ## GUI support
93
93
  ```bash
@@ -26,6 +26,7 @@ dependencies = [
26
26
  "opencv_python",
27
27
  "pillow",
28
28
  "psdtags",
29
+ "psutil",
29
30
  "PySide6",
30
31
  "scipy",
31
32
  "tifffile",
@@ -6,6 +6,7 @@ numpy
6
6
  opencv_python
7
7
  pillow
8
8
  psdtags
9
+ psutil
9
10
  PySide6
10
11
  scipy
11
12
  tifffile
@@ -0,0 +1 @@
1
+ __version__ = '1.3.0'
@@ -1,6 +1,7 @@
1
1
  # pylint: disable=C0114, C0115, C0116, E1101, R0914, R0913, R0917, R0912, R0915, R0902, E1121, W0102
2
- import logging
2
+ import os
3
3
  import math
4
+ import logging
4
5
  import numpy as np
5
6
  import matplotlib.pyplot as plt
6
7
  import cv2
@@ -20,7 +21,7 @@ _DEFAULT_MATCHING_CONFIG = {
20
21
  'flann_idx_kdtree': constants.DEFAULT_FLANN_IDX_KDTREE,
21
22
  'flann_trees': constants.DEFAULT_FLANN_TREES,
22
23
  'flann_checks': constants.DEFAULT_FLANN_CHECKS,
23
- 'threshold': constants.DEFAULT_ALIGN_THRESHOLD
24
+ 'threshold': constants.DEFAULT_ALIGN_THRESHOLD,
24
25
  }
25
26
 
26
27
  _DEFAULT_ALIGNMENT_CONFIG = {
@@ -130,6 +131,17 @@ def check_homography_distortion(m, img_shape, homography_thresholds=_HOMOGRAPHY_
130
131
  return True, "Transformation within acceptable limits"
131
132
 
132
133
 
134
+ def check_transform(m, img_0, transform_type,
135
+ affine_thresholds, homography_thresholds):
136
+ if transform_type == constants.ALIGN_RIGID:
137
+ return check_affine_matrix(
138
+ m, img_0.shape, affine_thresholds)
139
+ if transform_type == constants.ALIGN_HOMOGRAPHY:
140
+ return check_homography_distortion(
141
+ m, img_0.shape, homography_thresholds)
142
+ return False, f'invalid transfrom option {transform_type}'
143
+
144
+
133
145
  def get_good_matches(des_0, des_ref, matching_config=None):
134
146
  matching_config = {**_DEFAULT_MATCHING_CONFIG, **(matching_config or {})}
135
147
  match_method = matching_config['match_method']
@@ -172,7 +184,23 @@ def validate_align_config(detector, descriptor, match_method):
172
184
  " require matching method Hamming distance")
173
185
 
174
186
 
175
- def detect_and_compute(img_0, img_ref, feature_config=None, matching_config=None):
187
+ detector_map = {
188
+ constants.DETECTOR_SIFT: cv2.SIFT_create,
189
+ constants.DETECTOR_ORB: cv2.ORB_create,
190
+ constants.DETECTOR_SURF: cv2.FastFeatureDetector_create,
191
+ constants.DETECTOR_AKAZE: cv2.AKAZE_create,
192
+ constants.DETECTOR_BRISK: cv2.BRISK_create
193
+ }
194
+
195
+ descriptor_map = {
196
+ constants.DESCRIPTOR_SIFT: cv2.SIFT_create,
197
+ constants.DESCRIPTOR_ORB: cv2.ORB_create,
198
+ constants.DESCRIPTOR_AKAZE: cv2.AKAZE_create,
199
+ constants.DETECTOR_BRISK: cv2.BRISK_create
200
+ }
201
+
202
+
203
+ def detect_and_compute_matches(img_ref, img_0, feature_config=None, matching_config=None):
176
204
  feature_config = {**_DEFAULT_FEATURE_CONFIG, **(feature_config or {})}
177
205
  matching_config = {**_DEFAULT_MATCHING_CONFIG, **(matching_config or {})}
178
206
  feature_config_detector = feature_config['detector']
@@ -180,19 +208,6 @@ def detect_and_compute(img_0, img_ref, feature_config=None, matching_config=None
180
208
  match_method = matching_config['match_method']
181
209
  validate_align_config(feature_config_detector, feature_config_descriptor, match_method)
182
210
  img_bw_0, img_bw_ref = img_bw_8bit(img_0), img_bw_8bit(img_ref)
183
- detector_map = {
184
- constants.DETECTOR_SIFT: cv2.SIFT_create,
185
- constants.DETECTOR_ORB: cv2.ORB_create,
186
- constants.DETECTOR_SURF: cv2.FastFeatureDetector_create,
187
- constants.DETECTOR_AKAZE: cv2.AKAZE_create,
188
- constants.DETECTOR_BRISK: cv2.BRISK_create
189
- }
190
- descriptor_map = {
191
- constants.DESCRIPTOR_SIFT: cv2.SIFT_create,
192
- constants.DESCRIPTOR_ORB: cv2.ORB_create,
193
- constants.DESCRIPTOR_AKAZE: cv2.AKAZE_create,
194
- constants.DETECTOR_BRISK: cv2.BRISK_create
195
- }
196
211
  detector = detector_map[feature_config_detector]()
197
212
  if feature_config_detector == feature_config_descriptor and \
198
213
  feature_config_detector in (constants.DETECTOR_SIFT,
@@ -236,6 +251,25 @@ def find_transform(src_pts, dst_pts, transform=constants.DEFAULT_TRANSFORM,
236
251
  return result
237
252
 
238
253
 
254
+ def rescale_trasnsform(m, w0, h0, w_sub, h_sub, subsample, transform):
255
+ if transform == constants.ALIGN_HOMOGRAPHY:
256
+ low_size = np.float32([[0, 0], [0, h_sub], [w_sub, h_sub], [w_sub, 0]])
257
+ high_size = np.float32([[0, 0], [0, h0], [w0, h0], [w0, 0]])
258
+ scale_up = cv2.getPerspectiveTransform(low_size, high_size)
259
+ scale_down = cv2.getPerspectiveTransform(high_size, low_size)
260
+ m = scale_up @ m @ scale_down
261
+ elif transform == constants.ALIGN_RIGID:
262
+ rotation = m[:2, :2]
263
+ translation = m[:, 2]
264
+ translation_fullres = translation * subsample
265
+ m = np.empty((2, 3), dtype=np.float32)
266
+ m[:2, :2] = rotation
267
+ m[:, 2] = translation_fullres
268
+ else:
269
+ return 0
270
+ return m
271
+
272
+
239
273
  def align_images(img_ref, img_0, feature_config=None, matching_config=None, alignment_config=None,
240
274
  plot_path=None, callbacks=None,
241
275
  affine_thresholds=_AFFINE_THRESHOLDS,
@@ -265,8 +299,8 @@ def align_images(img_ref, img_0, feature_config=None, matching_config=None, alig
265
299
  img_ref_sub = img_subsample(img_ref, subsample, fast_subsampling)
266
300
  else:
267
301
  img_0_sub, img_ref_sub = img_0, img_ref
268
- kp_0, kp_ref, good_matches = detect_and_compute(img_0_sub, img_ref_sub,
269
- feature_config, matching_config)
302
+ kp_0, kp_ref, good_matches = detect_and_compute_matches(
303
+ img_ref_sub, img_0_sub, feature_config, matching_config)
270
304
  n_good_matches = len(good_matches)
271
305
  if n_good_matches > min_good_matches or subsample == 1:
272
306
  break
@@ -301,34 +335,18 @@ def align_images(img_ref, img_0, feature_config=None, matching_config=None, alig
301
335
  callbacks['save_plot'](plot_path)
302
336
  h_sub, w_sub = img_0_sub.shape[:2]
303
337
  if subsample > 1:
304
- if transform == constants.ALIGN_HOMOGRAPHY:
305
- low_size = np.float32([[0, 0], [0, h_sub], [w_sub, h_sub], [w_sub, 0]])
306
- high_size = np.float32([[0, 0], [0, h0], [w0, h0], [w0, 0]])
307
- scale_up = cv2.getPerspectiveTransform(low_size, high_size)
308
- scale_down = cv2.getPerspectiveTransform(high_size, low_size)
309
- m = scale_up @ m @ scale_down
310
- elif transform == constants.ALIGN_RIGID:
311
- rotation = m[:2, :2]
312
- translation = m[:, 2]
313
- translation_fullres = translation * subsample
314
- m = np.empty((2, 3), dtype=np.float32)
315
- m[:2, :2] = rotation
316
- m[:, 2] = translation_fullres
317
- else:
338
+ m = rescale_trasnsform(m, w0, h0, w_sub, h_sub, subsample, transform)
339
+ if m is None:
318
340
  raise InvalidOptionError("transform", transform)
319
-
320
341
  transform_type = alignment_config['transform']
321
- is_valid = True
322
- reason = ""
323
- if transform_type == constants.ALIGN_RIGID:
324
- is_valid, reason = check_affine_matrix(
325
- m, img_0.shape, affine_thresholds)
326
- elif transform_type == constants.ALIGN_HOMOGRAPHY:
327
- is_valid, reason = check_homography_distortion(
328
- m, img_0.shape, homography_thresholds)
342
+ is_valid, reason = check_transform(
343
+ m, img_0, transform_type,
344
+ affine_thresholds, homography_thresholds)
329
345
  if not is_valid:
330
346
  if callbacks and 'warning' in callbacks:
331
347
  callbacks['warning'](f"invalid transformation: {reason}")
348
+ if alignment_config['abort_abnormal']:
349
+ raise RuntimeError("invalid transformation: {reason}")
332
350
  return n_good_matches, None, None
333
351
  if callbacks and 'align_message' in callbacks:
334
352
  callbacks['align_message']()
@@ -357,12 +375,12 @@ def align_images(img_ref, img_0, feature_config=None, matching_config=None, alig
357
375
  return n_good_matches, m, img_warp
358
376
 
359
377
 
360
- class AlignFrames(SubAction):
378
+ class AlignFramesBase(SubAction):
361
379
  def __init__(self, enabled=True, feature_config=None, matching_config=None,
362
380
  alignment_config=None, **kwargs):
363
381
  super().__init__(enabled)
364
382
  self.process = None
365
- self.n_matches = None
383
+ self._n_good_matches = None
366
384
  self.feature_config = {**_DEFAULT_FEATURE_CONFIG, **(feature_config or {})}
367
385
  self.matching_config = {**_DEFAULT_MATCHING_CONFIG, **(matching_config or {})}
368
386
  self.alignment_config = {**_DEFAULT_ALIGNMENT_CONFIG, **(alignment_config or {})}
@@ -380,69 +398,36 @@ class AlignFrames(SubAction):
380
398
  if k in kwargs:
381
399
  self.alignment_config[k] = kwargs[k]
382
400
 
401
+ def align_images(self, idx, img_ref, img_0):
402
+ pass
403
+
404
+ def print_message(self, msg, color=constants.LOG_COLOR_LEVEL_3, level=logging.INFO):
405
+ self.process.print_message(color_str(msg, color), level=level)
406
+
407
+ def begin(self, process):
408
+ self.process = process
409
+ self._n_good_matches = np.zeros(process.total_action_counts)
410
+
383
411
  def run_frame(self, idx, ref_idx, img_0):
384
412
  if idx == self.process.ref_idx:
385
413
  return img_0
386
414
  img_ref = self.process.img_ref(ref_idx)
387
415
  return self.align_images(idx, img_ref, img_0)
388
416
 
389
- def sub_msg(self, msg, color=constants.LOG_COLOR_LEVEL_3):
390
- self.process.sub_message_r(color_str(msg, color))
417
+ def get_transform_thresholds(self):
418
+ return _AFFINE_THRESHOLDS, _HOMOGRAPHY_THRESHOLDS
391
419
 
392
- def align_images(self, idx, img_ref, img_0):
393
- idx_str = f"{idx:04d}"
394
- callbacks = {
395
- 'message': lambda: self.sub_msg(': find matches'),
396
- 'matches_message': lambda n: self.sub_msg(f": good matches: {n}"),
397
- 'align_message': lambda: self.sub_msg(': align images'),
398
- 'ecc_message': lambda: self.sub_msg(": ecc refinement"),
399
- 'blur_message': lambda: self.sub_msg(': blur borders'),
400
- 'warning': lambda msg: self.sub_msg(
401
- f': {msg}', constants.LOG_COLOR_WARNING),
402
- 'save_plot': lambda plot_path: self.process.callback(
403
- 'save_plot', self.process.id,
404
- f"{self.process.name}: matches\nframe {idx_str}", plot_path)
405
- }
406
- if self.plot_matches:
407
- plot_path = f"{self.process.working_path}/{self.process.plot_path}/" \
408
- f"{self.process.name}-matches-{idx_str}.pdf"
409
- else:
410
- plot_path = None
411
- if self.alignment_config['abort_abnormal']:
412
- affine_thresholds = _AFFINE_THRESHOLDS
413
- homography_thresholds = _HOMOGRAPHY_THRESHOLDS
414
- else:
415
- affine_thresholds = None
416
- homography_thresholds = None
417
- n_good_matches, _m, img = align_images(
418
- img_ref, img_0,
419
- feature_config=self.feature_config,
420
- matching_config=self.matching_config,
421
- alignment_config=self.alignment_config,
422
- plot_path=plot_path,
423
- callbacks=callbacks,
424
- affine_thresholds=affine_thresholds,
425
- homography_thresholds=homography_thresholds
426
- )
427
- self.n_matches[idx] = n_good_matches
428
- if n_good_matches < self.min_matches:
429
- self.process.sub_message(color_str(f": image not aligned, too few matches found: "
430
- f"{n_good_matches}", constants.LOG_COLOR_WARNING),
431
- level=logging.WARNING)
432
- return None
433
- return img
434
-
435
- def begin(self, process):
436
- self.process = process
437
- self.n_matches = np.zeros(process.total_action_counts)
420
+ def image_str(self, idx):
421
+ return f"image: {self.process.idx_tot_str(idx)}, " \
422
+ f"{os.path.basename(self.process.input_filepath(idx))}"
438
423
 
439
424
  def end(self):
440
425
  if self.plot_summary:
441
426
  plt.figure(figsize=constants.PLT_FIG_SIZE)
442
- x = np.arange(1, len(self.n_matches) + 1, dtype=int)
427
+ x = np.arange(1, len(self._n_good_matches) + 1, dtype=int)
443
428
  no_ref = x != self.process.ref_idx + 1
444
429
  x = x[no_ref]
445
- y = self.n_matches[no_ref]
430
+ y = self._n_good_matches[no_ref]
446
431
  if self.process.ref_idx == 0:
447
432
  y_max = y[1]
448
433
  elif self.process.ref_idx >= len(y):
@@ -464,5 +449,52 @@ class AlignFrames(SubAction):
464
449
  f"{self.process.name}-matches.pdf"
465
450
  save_plot(plot_path)
466
451
  plt.close('all')
467
- self.process.callback('save_plot', self.process.id,
452
+ self.process.callback(constants.CALLBACK_SAVE_PLOT, self.process.id,
468
453
  f"{self.process.name}: matches", plot_path)
454
+
455
+
456
+ 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
+ def align_images(self, idx, img_ref, img_0):
462
+ idx_str = f"{idx:04d}"
463
+ idx_tot_str = self.process.idx_tot_str(idx)
464
+ callbacks = {
465
+ 'message': lambda: self.print_message(f'{idx_tot_str}: find matches'),
466
+ 'matches_message': lambda n: self.print_message(f'{idx_tot_str}: good matches: {n}'),
467
+ 'align_message': lambda: self.print_message(f'{idx_tot_str}: align images'),
468
+ 'blur_message': lambda: self.print_message(f'{idx_tot_str}: blur borders'),
469
+ 'warning': lambda msg: self.print_message(
470
+ f': {msg}', constants.LOG_COLOR_WARNING),
471
+ 'save_plot': lambda plot_path: self.process.callback(
472
+ constants.CALLBACK_SAVE_PLOT, self.process.id,
473
+ f"{self.process.name}: matches\nframe {idx_str}", plot_path)
474
+ }
475
+ 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"
478
+ else:
479
+ plot_path = None
480
+ affine_thresholds, homography_thresholds = self.get_transform_thresholds()
481
+ n_good_matches, _m, img = align_images(
482
+ img_ref, img_0,
483
+ feature_config=self.feature_config,
484
+ matching_config=self.matching_config,
485
+ alignment_config=self.alignment_config,
486
+ plot_path=plot_path,
487
+ callbacks=callbacks,
488
+ affine_thresholds=affine_thresholds,
489
+ homography_thresholds=homography_thresholds
490
+ )
491
+ self._n_good_matches[idx] = n_good_matches
492
+ if n_good_matches < self.min_matches:
493
+ self.process.print_message(
494
+ f"{self.image_str(idx)} not aligned, too few matches found: "
495
+ f"{n_good_matches}")
496
+ return None
497
+ return img
498
+
499
+ def sequential_processing(self):
500
+ return True
@@ -0,0 +1,64 @@
1
+ # pylint: disable=C0114, C0115, C0116, W0718, R0912, R0915, E1101, R0914, R0911, E0606, R0801, R0902
2
+ import os
3
+ from ..config.constants import constants
4
+ from .align import AlignFramesBase, AlignFrames
5
+ from .align_parallel import AlignFramesParallel
6
+
7
+
8
+ class AlignFramesAuto(AlignFramesBase):
9
+ def __init__(self, enabled=True, feature_config=None, matching_config=None,
10
+ alignment_config=None, **kwargs):
11
+ super().__init__(enabled=True, feature_config=None, matching_config=None,
12
+ alignment_config=None, **kwargs)
13
+ self.mode = kwargs.pop('mode', constants.DEFAULT_ALIGN_MODE)
14
+ self.max_threads = kwargs.pop('max_threads', constants.DEFAULT_ALIGN_MAX_THREADS)
15
+ self.chunk_submit = kwargs.pop('chunk_submit', constants.DEFAULT_ALIGN_CHUNK_SUBMIT)
16
+ self.bw_matching = kwargs.pop('bw_matching', constants.DEFAULT_ALIGN_BW_MATCHING)
17
+ self.kwargs = kwargs
18
+ available_cores = os.cpu_count() or 1
19
+ self.num_threads = min(self.max_threads, available_cores)
20
+ self._implementation = None
21
+
22
+ def begin(self, process):
23
+ if self.mode == 'sequential' or self.num_threads == 1:
24
+ self._implementation = AlignFrames(
25
+ self.enabled, self.feature_config, self.matching_config, self.alignment_config,
26
+ **self.kwargs)
27
+ else:
28
+ if self.mode == 'parallel':
29
+ num_threads = self.num_threads
30
+ chunk_submit = self.chunk_submit
31
+ else:
32
+ if self.feature_config is not None:
33
+ detector = self.feature_config.get(
34
+ 'detector', constants.DEFAULT_DETECTOR)
35
+ descriptor = self.feature_config.get(
36
+ 'descriptor', constants.DEFAULT_DESCRIPTOR)
37
+ else:
38
+ detector = constants.DEFAULT_DETECTOR
39
+ descriptor = constants.DEFAULT_DESCRIPTOR
40
+ if detector in (constants.DETECTOR_SIFT, constants.DETECTOR_AKAZE) or \
41
+ descriptor in (constants.DESCRIPTOR_SIFT, constants.DESCRIPTOR_AKAZE):
42
+ num_threads = min(3, self.num_threads)
43
+ chunk_submit = True
44
+ else:
45
+ num_threads = self.num_threads
46
+ chunk_submit = self.chunk_submit
47
+ self._implementation = AlignFramesParallel(
48
+ self.enabled, self.feature_config, self.matching_config, self.alignment_config,
49
+ max_threads=num_threads, chunk_submit=chunk_submit,
50
+ bw_matching=self.bw_matching,
51
+ **self.kwargs)
52
+ self._implementation.begin(process)
53
+
54
+ def align_images(self, idx, img_ref, img_0):
55
+ return self._implementation.align_images(idx, img_ref, img_0)
56
+
57
+ def run_frame(self, idx, ref_idx, img_0):
58
+ return self._implementation.run_frame(idx, ref_idx, img_0)
59
+
60
+ def sequential_processing(self):
61
+ return self._implementation.sequential_processing()
62
+
63
+ def end(self):
64
+ self._implementation.end()