shinestacker 1.2.0__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 (145) hide show
  1. {shinestacker-1.2.0 → shinestacker-1.3.0}/CHANGELOG.md +45 -8
  2. {shinestacker-1.2.0/src/shinestacker.egg-info → shinestacker-1.3.0}/PKG-INFO +6 -6
  3. {shinestacker-1.2.0 → shinestacker-1.3.0}/README.md +4 -5
  4. {shinestacker-1.2.0 → shinestacker-1.3.0}/docs/alignment.md +8 -0
  5. {shinestacker-1.2.0 → shinestacker-1.3.0}/docs/focus_stacking.md +1 -0
  6. {shinestacker-1.2.0 → shinestacker-1.3.0}/docs/job.md +3 -1
  7. {shinestacker-1.2.0 → shinestacker-1.3.0}/docs/main.md +1 -1
  8. {shinestacker-1.2.0 → shinestacker-1.3.0}/pyproject.toml +1 -0
  9. {shinestacker-1.2.0 → shinestacker-1.3.0}/requirements.txt +1 -0
  10. shinestacker-1.3.0/src/shinestacker/_version.py +1 -0
  11. {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/algorithms/align.py +148 -115
  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.0 → shinestacker-1.3.0}/src/shinestacker/algorithms/balance.py +14 -13
  15. {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/algorithms/base_stack_algo.py +11 -2
  16. {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/algorithms/multilayer.py +14 -15
  17. {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/algorithms/noise_detection.py +13 -14
  18. {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/algorithms/pyramid.py +4 -4
  19. {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/algorithms/pyramid_auto.py +16 -10
  20. {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/algorithms/pyramid_tiles.py +19 -11
  21. {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/algorithms/stack.py +30 -26
  22. shinestacker-1.3.0/src/shinestacker/algorithms/stack_framework.py +330 -0
  23. {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/algorithms/vignetting.py +16 -13
  24. {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/app/main.py +7 -3
  25. {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/config/constants.py +63 -26
  26. {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/config/gui_constants.py +1 -1
  27. {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/core/core_utils.py +4 -0
  28. {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/core/framework.py +114 -33
  29. {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/gui/action_config.py +57 -5
  30. {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/gui/action_config_dialog.py +156 -17
  31. {shinestacker-1.2.0 → 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.0 → shinestacker-1.3.0}/src/shinestacker/gui/gui_images.py +10 -10
  34. {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/gui/gui_run.py +13 -11
  35. {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/gui/main_window.py +10 -5
  36. {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/gui/menu_manager.py +4 -0
  37. shinestacker-1.3.0/src/shinestacker/gui/new_project.py +348 -0
  38. {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/gui/project_controller.py +13 -9
  39. {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/gui/project_converter.py +4 -2
  40. {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/gui/project_editor.py +72 -53
  41. {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/gui/select_path_widget.py +1 -1
  42. shinestacker-1.3.0/src/shinestacker/gui/sys_mon.py +96 -0
  43. {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/gui/tab_widget.py +3 -3
  44. {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/gui/time_progress_bar.py +4 -3
  45. {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/retouch/exif_data.py +1 -1
  46. {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/retouch/image_editor_ui.py +2 -0
  47. {shinestacker-1.2.0 → shinestacker-1.3.0/src/shinestacker.egg-info}/PKG-INFO +6 -6
  48. {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker.egg-info/SOURCES.txt +4 -0
  49. {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker.egg-info/requires.txt +1 -0
  50. shinestacker-1.2.0/src/shinestacker/_version.py +0 -1
  51. shinestacker-1.2.0/src/shinestacker/algorithms/stack_framework.py +0 -308
  52. shinestacker-1.2.0/src/shinestacker/gui/new_project.py +0 -251
  53. {shinestacker-1.2.0 → shinestacker-1.3.0}/.coveragerc +0 -0
  54. {shinestacker-1.2.0 → shinestacker-1.3.0}/.flake8 +0 -0
  55. {shinestacker-1.2.0 → shinestacker-1.3.0}/.github/workflows/ci-multiplatform.yml +0 -0
  56. {shinestacker-1.2.0 → shinestacker-1.3.0}/.github/workflows/pylint.yml +0 -0
  57. {shinestacker-1.2.0 → shinestacker-1.3.0}/.github/workflows/pypi-publish.yml +0 -0
  58. {shinestacker-1.2.0 → shinestacker-1.3.0}/.github/workflows/release.yml +0 -0
  59. {shinestacker-1.2.0 → shinestacker-1.3.0}/.gitignore +0 -0
  60. {shinestacker-1.2.0 → shinestacker-1.3.0}/.pylintrc +0 -0
  61. {shinestacker-1.2.0 → shinestacker-1.3.0}/.readthedocs.yaml +0 -0
  62. {shinestacker-1.2.0 → shinestacker-1.3.0}/LICENSE +0 -0
  63. {shinestacker-1.2.0 → shinestacker-1.3.0}/MANIFEST.in +0 -0
  64. {shinestacker-1.2.0 → shinestacker-1.3.0}/THIRD_PARTY_LICENSES.txt +0 -0
  65. {shinestacker-1.2.0 → shinestacker-1.3.0}/docs/api.md +0 -0
  66. {shinestacker-1.2.0 → shinestacker-1.3.0}/docs/balancing.md +0 -0
  67. {shinestacker-1.2.0 → shinestacker-1.3.0}/docs/conf.py +0 -0
  68. {shinestacker-1.2.0 → shinestacker-1.3.0}/docs/gui.md +0 -0
  69. {shinestacker-1.2.0 → shinestacker-1.3.0}/docs/index.md +0 -0
  70. {shinestacker-1.2.0 → shinestacker-1.3.0}/docs/multilayer.md +0 -0
  71. {shinestacker-1.2.0 → shinestacker-1.3.0}/docs/noise.md +0 -0
  72. {shinestacker-1.2.0 → shinestacker-1.3.0}/docs/requirements.txt +0 -0
  73. {shinestacker-1.2.0 → shinestacker-1.3.0}/docs/vignetting.md +0 -0
  74. {shinestacker-1.2.0 → shinestacker-1.3.0}/img/coffee.gif +0 -0
  75. {shinestacker-1.2.0 → shinestacker-1.3.0}/img/coffee_stack.jpg +0 -0
  76. {shinestacker-1.2.0 → shinestacker-1.3.0}/img/extreme-vignetting.jpg +0 -0
  77. {shinestacker-1.2.0 → shinestacker-1.3.0}/img/flies.gif +0 -0
  78. {shinestacker-1.2.0 → shinestacker-1.3.0}/img/flies_stack.jpg +0 -0
  79. {shinestacker-1.2.0 → shinestacker-1.3.0}/img/flow-diagram.png +0 -0
  80. {shinestacker-1.2.0 → shinestacker-1.3.0}/img/gui-finder.png +0 -0
  81. {shinestacker-1.2.0 → shinestacker-1.3.0}/img/gui-project-new.png +0 -0
  82. {shinestacker-1.2.0 → shinestacker-1.3.0}/img/gui-project-run.png +0 -0
  83. {shinestacker-1.2.0 → shinestacker-1.3.0}/img/gui-retouch.png +0 -0
  84. {shinestacker-1.2.0 → shinestacker-1.3.0}/scripts/build_release.py +0 -0
  85. {shinestacker-1.2.0 → shinestacker-1.3.0}/scripts/git-rev-list.sh +0 -0
  86. {shinestacker-1.2.0 → shinestacker-1.3.0}/scripts/validate-tomli.py +0 -0
  87. {shinestacker-1.2.0 → shinestacker-1.3.0}/setup.cfg +0 -0
  88. {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/__init__.py +0 -0
  89. {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/algorithms/__init__.py +0 -0
  90. {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/algorithms/denoise.py +0 -0
  91. {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/algorithms/depth_map.py +0 -0
  92. {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/algorithms/exif.py +0 -0
  93. {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/algorithms/sharpen.py +0 -0
  94. {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/algorithms/utils.py +0 -0
  95. {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/algorithms/white_balance.py +0 -0
  96. {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/app/__init__.py +0 -0
  97. {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/app/about_dialog.py +0 -0
  98. {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/app/gui_utils.py +0 -0
  99. {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/app/help_menu.py +0 -0
  100. {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/app/open_frames.py +0 -0
  101. {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/app/project.py +0 -0
  102. {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/app/retouch.py +0 -0
  103. {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/config/__init__.py +0 -0
  104. {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/config/config.py +0 -0
  105. {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/core/__init__.py +0 -0
  106. {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/core/colors.py +0 -0
  107. {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/core/exceptions.py +0 -0
  108. {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/core/logging.py +0 -0
  109. {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/gui/__init__.py +0 -0
  110. {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/gui/colors.py +0 -0
  111. {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/gui/flow_layout.py +0 -0
  112. {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/gui/gui_logging.py +0 -0
  113. {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/gui/ico/focus_stack_bkg.png +0 -0
  114. {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/gui/ico/shinestacker.icns +0 -0
  115. {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/gui/ico/shinestacker.ico +0 -0
  116. {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/gui/ico/shinestacker.png +0 -0
  117. {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/gui/ico/shinestacker.svg +0 -0
  118. {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/gui/img/close-round-line-icon.png +0 -0
  119. {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/gui/img/forward-button-icon.png +0 -0
  120. {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/gui/img/play-button-round-icon.png +0 -0
  121. {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/gui/img/plus-round-line-icon.png +0 -0
  122. {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/gui/project_model.py +0 -0
  123. {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/retouch/__init__.py +0 -0
  124. {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/retouch/base_filter.py +0 -0
  125. {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/retouch/brush.py +0 -0
  126. {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/retouch/brush_gradient.py +0 -0
  127. {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/retouch/brush_preview.py +0 -0
  128. {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/retouch/brush_tool.py +0 -0
  129. {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/retouch/denoise_filter.py +0 -0
  130. {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/retouch/display_manager.py +0 -0
  131. {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/retouch/file_loader.py +0 -0
  132. {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/retouch/filter_manager.py +0 -0
  133. {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/retouch/icon_container.py +0 -0
  134. {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/retouch/image_viewer.py +0 -0
  135. {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/retouch/io_gui_handler.py +0 -0
  136. {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/retouch/io_manager.py +0 -0
  137. {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/retouch/layer_collection.py +0 -0
  138. {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/retouch/shortcuts_help.py +0 -0
  139. {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/retouch/undo_manager.py +0 -0
  140. {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/retouch/unsharp_mask_filter.py +0 -0
  141. {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/retouch/vignetting_filter.py +0 -0
  142. {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker/retouch/white_balance_filter.py +0 -0
  143. {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker.egg-info/dependency_links.txt +0 -0
  144. {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker.egg-info/entry_points.txt +0 -0
  145. {shinestacker-1.2.0 → shinestacker-1.3.0}/src/shinestacker.egg-info/top_level.txt +0 -0
@@ -2,20 +2,57 @@
2
2
 
3
3
  This page reports the main releases only and the main changes therein.
4
4
 
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
27
+ **Bug fixes and minor improvements**
28
+
29
+ ### Changes
30
+
31
+ * alignment is more tolerant in case of failures: frames are skipped and the running job is not stopped
32
+ * fixed the -x (--expert) option
33
+ * more safety checks prevent crashes for abnormal conditions
34
+ * reference frame index improved with a more consistent treatment, a better numbering scheme and GUI widget
35
+ * improved project undo action description text
36
+ * some bug fixes and code cleanup
37
+
38
+ ---
39
+
5
40
  ## [v1.2.0] - 2025-08-31
6
- **Improved pyramid algorithm**
41
+ **Parallel processing and more improvements**
42
+
43
+ ### Changes
7
44
 
8
45
  * Implemented parallel processing for pyramid stacking algorithm
9
- * optimized Pyramid algorithm: now a single algorithm selects automatically the best strategy ensuring that all the processing stays approximately within a given memory budget; This avoids memory issues in case many pictures are selected. Explicit configuration is also possible for specific needs.
10
- * Implemented automatic subsample option for alignment, balancing and vignetting, now default.
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.
47
+ * Implemented automatic subsample option for alignment, balancing and vignetting, now default
11
48
  * HLS and HSV corrections now supported for 16 bit images
12
49
  * Added luminosity correction in the LAB color space
13
50
  * Alignment module skips frames if transformation parameters are out of a reasonable ranges
14
- * Multilayer modules sends a warning if the estimated file size is > 1GB
15
- * "Run all jobs" action is enabled only if more than one job are present.
16
- * Updated default module names for "new project" dialog.
17
- * Code refactoring.
18
- * Some GUI fixes.
51
+ * Multilayer modules sends a warning if the estimated output file size is > 1GB
52
+ * "Run all jobs" action is enabled only if more than one job are present
53
+ * Updated default module names in project genereated by "new project" dialog
54
+ * Code refactoring
55
+ * Some GUI fixes
19
56
 
20
57
  ---
21
58
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: shinestacker
3
- Version: 1.2.0
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.
@@ -41,6 +41,8 @@ Arguments for the constructor of ```CombinedActions``` are for the :
41
41
  * ```working_path``` (optional): the directory that contains input and output image subdirectories. If not specified, it is the same as ```job.working_path```.
42
42
  * ```plot_path``` (optional, default: ```plots```): the directory within ```working_path``` that contains plots produced by the different actions.
43
43
  * ```resample``` (optional, default: 1): take every *n*<sup>th</sup> frame in the selected directory. Default: take all frames.
44
- * ```ref_idx``` (optional): the index of the image used as reference. Images are numbered starting from zero. If not specified, it is the index of the middle image.
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,11 +1,12 @@
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
7
8
  from .. config.constants import constants
8
- from .. core.exceptions import AlignmentError, InvalidOptionError
9
+ from .. core.exceptions import InvalidOptionError
9
10
  from .. core.colors import color_str
10
11
  from .utils import img_8bit, img_bw_8bit, save_plot, img_subsample
11
12
  from .stack_framework import SubAction
@@ -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,7 +131,18 @@ def check_homography_distortion(m, img_shape, homography_thresholds=_HOMOGRAPHY_
130
131
  return True, "Transformation within acceptable limits"
131
132
 
132
133
 
133
- def get_good_matches(des_0, des_1, matching_config=None):
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
+
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']
136
148
  good_matches = []
@@ -139,12 +151,12 @@ def get_good_matches(des_0, des_1, matching_config=None):
139
151
  {'algorithm': matching_config['flann_idx_kdtree'],
140
152
  'trees': matching_config['flann_trees']},
141
153
  {'checks': matching_config['flann_checks']})
142
- matches = flann.knnMatch(des_0, des_1, k=2)
154
+ matches = flann.knnMatch(des_0, des_ref, k=2)
143
155
  good_matches = [m for m, n in matches
144
156
  if m.distance < matching_config['threshold'] * n.distance]
145
157
  elif match_method == constants.MATCHING_NORM_HAMMING:
146
158
  bf = cv2.BFMatcher(cv2.NORM_HAMMING, crossCheck=True)
147
- good_matches = sorted(bf.match(des_0, des_1), key=lambda x: x.distance)
159
+ good_matches = sorted(bf.match(des_0, des_ref), key=lambda x: x.distance)
148
160
  else:
149
161
  raise InvalidOptionError(
150
162
  'match_method', match_method,
@@ -172,39 +184,42 @@ 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_1, 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']
179
207
  feature_config_descriptor = feature_config['descriptor']
180
208
  match_method = matching_config['match_method']
181
209
  validate_align_config(feature_config_detector, feature_config_descriptor, match_method)
182
- img_bw_0, img_bw_1 = img_bw_8bit(img_0), img_bw_8bit(img_1)
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
- }
210
+ img_bw_0, img_bw_ref = img_bw_8bit(img_0), img_bw_8bit(img_ref)
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,
199
214
  constants.DETECTOR_AKAZE,
200
215
  constants.DETECTOR_BRISK):
201
216
  kp_0, des_0 = detector.detectAndCompute(img_bw_0, None)
202
- kp_1, des_1 = detector.detectAndCompute(img_bw_1, None)
217
+ kp_ref, des_ref = detector.detectAndCompute(img_bw_ref, None)
203
218
  else:
204
219
  descriptor = descriptor_map[feature_config_descriptor]()
205
220
  kp_0, des_0 = descriptor.compute(img_bw_0, detector.detect(img_bw_0, None))
206
- kp_1, des_1 = descriptor.compute(img_bw_1, detector.detect(img_bw_1, None))
207
- return kp_0, kp_1, get_good_matches(des_0, des_1, matching_config)
221
+ kp_ref, des_ref = descriptor.compute(img_bw_ref, detector.detect(img_bw_ref, None))
222
+ return kp_0, kp_ref, get_good_matches(des_0, des_ref, matching_config)
208
223
 
209
224
 
210
225
  def find_transform(src_pts, dst_pts, transform=constants.DEFAULT_TRANSFORM,
@@ -236,7 +251,26 @@ def find_transform(src_pts, dst_pts, transform=constants.DEFAULT_TRANSFORM,
236
251
  return result
237
252
 
238
253
 
239
- def align_images(img_1, img_0, feature_config=None, matching_config=None, alignment_config=None,
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
+
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,
242
276
  homography_thresholds=_HOMOGRAPHY_THRESHOLDS):
@@ -250,11 +284,11 @@ def align_images(img_1, img_0, feature_config=None, matching_config=None, alignm
250
284
  min_matches = 4 if alignment_config['transform'] == constants.ALIGN_HOMOGRAPHY else 3
251
285
  if callbacks and 'message' in callbacks:
252
286
  callbacks['message']()
253
- h_ref, w_ref = img_1.shape[:2]
287
+ h_ref, w_ref = img_ref.shape[:2]
254
288
  h0, w0 = img_0.shape[:2]
255
289
  subsample = alignment_config['subsample']
256
290
  if subsample == 0:
257
- img_res = (float(h0) / 1000) * (float(w0) / 1000)
291
+ img_res = (float(h0) / constants.ONE_KILO) * (float(w0) / constants.ONE_KILO)
258
292
  target_res = constants.DEFAULT_ALIGN_RES_TARGET_MPX
259
293
  subsample = int(1 + math.floor(img_res / target_res))
260
294
  fast_subsampling = alignment_config['fast_subsampling']
@@ -262,11 +296,11 @@ def align_images(img_1, img_0, feature_config=None, matching_config=None, alignm
262
296
  while True:
263
297
  if subsample > 1:
264
298
  img_0_sub = img_subsample(img_0, subsample, fast_subsampling)
265
- img_1_sub = img_subsample(img_1, subsample, fast_subsampling)
299
+ img_ref_sub = img_subsample(img_ref, subsample, fast_subsampling)
266
300
  else:
267
- img_0_sub, img_1_sub = img_0, img_1
268
- kp_0, kp_1, good_matches = detect_and_compute(img_0_sub, img_1_sub,
269
- feature_config, matching_config)
301
+ img_0_sub, img_ref_sub = img_0, img_ref
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
@@ -282,7 +316,7 @@ def align_images(img_1, img_0, feature_config=None, matching_config=None, alignm
282
316
  if n_good_matches >= min_matches:
283
317
  transform = alignment_config['transform']
284
318
  src_pts = np.float32([kp_0[m.queryIdx].pt for m in good_matches]).reshape(-1, 1, 2)
285
- dst_pts = np.float32([kp_1[m.trainIdx].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)
286
320
  m, msk = find_transform(src_pts, dst_pts, transform, alignment_config['align_method'],
287
321
  *(alignment_config[k]
288
322
  for k in ['rans_threshold', 'max_iters',
@@ -290,47 +324,30 @@ def align_images(img_1, img_0, feature_config=None, matching_config=None, alignm
290
324
  if plot_path is not None:
291
325
  matches_mask = msk.ravel().tolist()
292
326
  img_match = cv2.cvtColor(cv2.drawMatches(
293
- img_8bit(img_0_sub), kp_0, img_8bit(img_1_sub),
294
- kp_1, good_matches, None, matchColor=(0, 255, 0),
327
+ img_8bit(img_0_sub), kp_0, img_8bit(img_ref_sub),
328
+ kp_ref, good_matches, None, matchColor=(0, 255, 0),
295
329
  singlePointColor=None, matchesMask=matches_mask,
296
330
  flags=2), cv2.COLOR_BGR2RGB)
297
- plt.figure(figsize=(10, 5))
331
+ plt.figure(figsize=constants.PLT_FIG_SIZE)
298
332
  plt.imshow(img_match, 'gray')
299
333
  save_plot(plot_path)
300
334
  if callbacks and 'save_plot' in callbacks:
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
-
334
351
  if callbacks and 'align_message' in callbacks:
335
352
  callbacks['align_message']()
336
353
  img_mask = np.ones_like(img_0, dtype=np.uint8)
@@ -358,12 +375,12 @@ def align_images(img_1, img_0, feature_config=None, matching_config=None, alignm
358
375
  return n_good_matches, m, img_warp
359
376
 
360
377
 
361
- class AlignFrames(SubAction):
378
+ class AlignFramesBase(SubAction):
362
379
  def __init__(self, enabled=True, feature_config=None, matching_config=None,
363
380
  alignment_config=None, **kwargs):
364
381
  super().__init__(enabled)
365
382
  self.process = None
366
- self.n_matches = None
383
+ self._n_good_matches = None
367
384
  self.feature_config = {**_DEFAULT_FEATURE_CONFIG, **(feature_config or {})}
368
385
  self.matching_config = {**_DEFAULT_MATCHING_CONFIG, **(matching_config or {})}
369
386
  self.alignment_config = {**_DEFAULT_ALIGNMENT_CONFIG, **(alignment_config or {})}
@@ -381,27 +398,78 @@ class AlignFrames(SubAction):
381
398
  if k in kwargs:
382
399
  self.alignment_config[k] = kwargs[k]
383
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
+
384
411
  def run_frame(self, idx, ref_idx, img_0):
385
412
  if idx == self.process.ref_idx:
386
413
  return img_0
387
414
  img_ref = self.process.img_ref(ref_idx)
388
415
  return self.align_images(idx, img_ref, img_0)
389
416
 
390
- def sub_msg(self, msg, color=constants.LOG_COLOR_LEVEL_3):
391
- self.process.sub_message_r(color_str(msg, color))
417
+ def get_transform_thresholds(self):
418
+ return _AFFINE_THRESHOLDS, _HOMOGRAPHY_THRESHOLDS
419
+
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))}"
423
+
424
+ 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)
428
+ no_ref = x != self.process.ref_idx + 1
429
+ x = x[no_ref]
430
+ y = self._n_good_matches[no_ref]
431
+ if self.process.ref_idx == 0:
432
+ y_max = y[1]
433
+ elif self.process.ref_idx >= len(y):
434
+ y_max = y[-1]
435
+ else:
436
+ y_max = (y[self.process.ref_idx - 1] + y[self.process.ref_idx]) / 2
437
+
438
+ plt.plot([self.process.ref_idx + 1, self.process.ref_idx + 1],
439
+ [0, y_max], color='cornflowerblue', linestyle='--', label='reference frame')
440
+ plt.plot([x[0], x[-1]], [self.min_matches, self.min_matches], color='lightgray',
441
+ linestyle='--', label='min. matches')
442
+ plt.plot(x, y, color='navy', label='matches')
443
+ plt.xlabel('frame')
444
+ plt.ylabel('# of matches')
445
+ plt.legend()
446
+ plt.ylim(0)
447
+ plt.xlim(x[0], x[-1])
448
+ plot_path = f"{self.process.working_path}/{self.process.plot_path}/" \
449
+ f"{self.process.name}-matches.pdf"
450
+ save_plot(plot_path)
451
+ plt.close('all')
452
+ self.process.callback(constants.CALLBACK_SAVE_PLOT, self.process.id,
453
+ f"{self.process.name}: matches", plot_path)
454
+
392
455
 
393
- def align_images(self, idx, img_1, img_0):
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):
394
462
  idx_str = f"{idx:04d}"
463
+ idx_tot_str = self.process.idx_tot_str(idx)
395
464
  callbacks = {
396
- 'message': lambda: self.sub_msg(': find matches'),
397
- 'matches_message': lambda n: self.sub_msg(f": good matches: {n}"),
398
- 'align_message': lambda: self.sub_msg(': align images'),
399
- 'ecc_message': lambda: self.sub_msg(": ecc refinement"),
400
- 'blur_message': lambda: self.sub_msg(': blur borders'),
401
- 'warning': lambda msg: self.sub_msg(
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(
402
470
  f': {msg}', constants.LOG_COLOR_WARNING),
403
471
  'save_plot': lambda plot_path: self.process.callback(
404
- 'save_plot', self.process.id,
472
+ constants.CALLBACK_SAVE_PLOT, self.process.id,
405
473
  f"{self.process.name}: matches\nframe {idx_str}", plot_path)
406
474
  }
407
475
  if self.plot_matches:
@@ -409,14 +477,9 @@ class AlignFrames(SubAction):
409
477
  f"{self.process.name}-matches-{idx_str}.pdf"
410
478
  else:
411
479
  plot_path = None
412
- if self.alignment_config['abort_abnormal']:
413
- affine_thresholds = _AFFINE_THRESHOLDS
414
- homography_thresholds = _HOMOGRAPHY_THRESHOLDS
415
- else:
416
- affine_thresholds = None
417
- homography_thresholds = None
480
+ affine_thresholds, homography_thresholds = self.get_transform_thresholds()
418
481
  n_good_matches, _m, img = align_images(
419
- img_1, img_0,
482
+ img_ref, img_0,
420
483
  feature_config=self.feature_config,
421
484
  matching_config=self.matching_config,
422
485
  alignment_config=self.alignment_config,
@@ -425,43 +488,13 @@ class AlignFrames(SubAction):
425
488
  affine_thresholds=affine_thresholds,
426
489
  homography_thresholds=homography_thresholds
427
490
  )
428
- self.n_matches[idx] = n_good_matches
491
+ self._n_good_matches[idx] = n_good_matches
429
492
  if n_good_matches < self.min_matches:
430
- self.process.sub_message(f": image not aligned, too few matches found: "
431
- f"{n_good_matches}", level=logging.CRITICAL)
432
- raise AlignmentError(idx, f"Image not aligned, too few matches found: "
433
- f"{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
434
497
  return img
435
498
 
436
- def begin(self, process):
437
- self.process = process
438
- self.n_matches = np.zeros(process.counts)
439
-
440
- def end(self):
441
- if self.plot_summary:
442
- plt.figure(figsize=(10, 5))
443
- x = np.arange(1, len(self.n_matches) + 1, dtype=int)
444
- no_ref = x != self.process.ref_idx + 1
445
- x = x[no_ref]
446
- y = self.n_matches[no_ref]
447
- y_max = y[1] \
448
- if self.process.ref_idx == 0 \
449
- else y[-1] if self.process.ref_idx == len(y) - 1 \
450
- else (y[self.process.ref_idx - 1] + y[self.process.ref_idx]) / 2
451
-
452
- plt.plot([self.process.ref_idx + 1, self.process.ref_idx + 1],
453
- [0, y_max], color='cornflowerblue', linestyle='--', label='reference frame')
454
- plt.plot([x[0], x[-1]], [self.min_matches, self.min_matches], color='lightgray',
455
- linestyle='--', label='min. matches')
456
- plt.plot(x, y, color='navy', label='matches')
457
- plt.xlabel('frame')
458
- plt.ylabel('# of matches')
459
- plt.legend()
460
- plt.ylim(0)
461
- plt.xlim(x[0], x[-1])
462
- plot_path = f"{self.process.working_path}/{self.process.plot_path}/" \
463
- f"{self.process.name}-matches.pdf"
464
- save_plot(plot_path)
465
- plt.close('all')
466
- self.process.callback('save_plot', self.process.id,
467
- f"{self.process.name}: matches", plot_path)
499
+ def sequential_processing(self):
500
+ return True