shinestacker 1.0.4.post2__tar.gz → 1.1.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 (138) hide show
  1. {shinestacker-1.0.4.post2 → shinestacker-1.1.0}/CHANGELOG.md +14 -1
  2. {shinestacker-1.0.4.post2/src/shinestacker.egg-info → shinestacker-1.1.0}/PKG-INFO +20 -1
  3. {shinestacker-1.0.4.post2 → shinestacker-1.1.0}/README.md +19 -0
  4. {shinestacker-1.0.4.post2 → shinestacker-1.1.0}/docs/focus_stacking.md +5 -0
  5. shinestacker-1.1.0/src/shinestacker/_version.py +1 -0
  6. {shinestacker-1.0.4.post2 → shinestacker-1.1.0}/src/shinestacker/algorithms/align.py +11 -11
  7. {shinestacker-1.0.4.post2 → shinestacker-1.1.0}/src/shinestacker/algorithms/base_stack_algo.py +27 -4
  8. {shinestacker-1.0.4.post2 → shinestacker-1.1.0}/src/shinestacker/algorithms/depth_map.py +8 -11
  9. {shinestacker-1.0.4.post2 → shinestacker-1.1.0}/src/shinestacker/algorithms/noise_detection.py +7 -2
  10. {shinestacker-1.0.4.post2 → shinestacker-1.1.0}/src/shinestacker/algorithms/pyramid.py +55 -41
  11. shinestacker-1.1.0/src/shinestacker/algorithms/pyramid_tiles.py +109 -0
  12. {shinestacker-1.0.4.post2 → shinestacker-1.1.0}/src/shinestacker/algorithms/stack.py +11 -8
  13. {shinestacker-1.0.4.post2 → shinestacker-1.1.0}/src/shinestacker/algorithms/stack_framework.py +5 -8
  14. {shinestacker-1.0.4.post2 → shinestacker-1.1.0}/src/shinestacker/algorithms/utils.py +5 -0
  15. {shinestacker-1.0.4.post2 → shinestacker-1.1.0}/src/shinestacker/config/constants.py +5 -2
  16. {shinestacker-1.0.4.post2 → shinestacker-1.1.0}/src/shinestacker/gui/action_config_dialog.py +26 -4
  17. shinestacker-1.1.0/src/shinestacker/gui/flow_layout.py +105 -0
  18. {shinestacker-1.0.4.post2 → shinestacker-1.1.0}/src/shinestacker/gui/gui_run.py +24 -19
  19. {shinestacker-1.0.4.post2 → shinestacker-1.1.0}/src/shinestacker/gui/main_window.py +2 -2
  20. {shinestacker-1.0.4.post2 → shinestacker-1.1.0}/src/shinestacker/gui/new_project.py +1 -0
  21. {shinestacker-1.0.4.post2 → shinestacker-1.1.0}/src/shinestacker/gui/project_controller.py +1 -0
  22. {shinestacker-1.0.4.post2 → shinestacker-1.1.0}/src/shinestacker/gui/project_converter.py +10 -4
  23. {shinestacker-1.0.4.post2 → shinestacker-1.1.0}/src/shinestacker/gui/time_progress_bar.py +2 -2
  24. {shinestacker-1.0.4.post2 → shinestacker-1.1.0/src/shinestacker.egg-info}/PKG-INFO +20 -1
  25. {shinestacker-1.0.4.post2 → shinestacker-1.1.0}/src/shinestacker.egg-info/SOURCES.txt +2 -0
  26. shinestacker-1.0.4.post2/src/shinestacker/_version.py +0 -1
  27. {shinestacker-1.0.4.post2 → shinestacker-1.1.0}/.coveragerc +0 -0
  28. {shinestacker-1.0.4.post2 → shinestacker-1.1.0}/.flake8 +0 -0
  29. {shinestacker-1.0.4.post2 → shinestacker-1.1.0}/.github/workflows/ci-multiplatform.yml +0 -0
  30. {shinestacker-1.0.4.post2 → shinestacker-1.1.0}/.github/workflows/pylint.yml +0 -0
  31. {shinestacker-1.0.4.post2 → shinestacker-1.1.0}/.github/workflows/pypi-publish.yml +0 -0
  32. {shinestacker-1.0.4.post2 → shinestacker-1.1.0}/.github/workflows/release.yml +0 -0
  33. {shinestacker-1.0.4.post2 → shinestacker-1.1.0}/.gitignore +0 -0
  34. {shinestacker-1.0.4.post2 → shinestacker-1.1.0}/.pylintrc +0 -0
  35. {shinestacker-1.0.4.post2 → shinestacker-1.1.0}/.readthedocs.yaml +0 -0
  36. {shinestacker-1.0.4.post2 → shinestacker-1.1.0}/LICENSE +0 -0
  37. {shinestacker-1.0.4.post2 → shinestacker-1.1.0}/MANIFEST.in +0 -0
  38. {shinestacker-1.0.4.post2 → shinestacker-1.1.0}/THIRD_PARTY_LICENSES.txt +0 -0
  39. {shinestacker-1.0.4.post2 → shinestacker-1.1.0}/docs/alignment.md +0 -0
  40. {shinestacker-1.0.4.post2 → shinestacker-1.1.0}/docs/api.md +0 -0
  41. {shinestacker-1.0.4.post2 → shinestacker-1.1.0}/docs/balancing.md +0 -0
  42. {shinestacker-1.0.4.post2 → shinestacker-1.1.0}/docs/conf.py +0 -0
  43. {shinestacker-1.0.4.post2 → shinestacker-1.1.0}/docs/gui.md +0 -0
  44. {shinestacker-1.0.4.post2 → shinestacker-1.1.0}/docs/index.md +0 -0
  45. {shinestacker-1.0.4.post2 → shinestacker-1.1.0}/docs/job.md +0 -0
  46. {shinestacker-1.0.4.post2 → shinestacker-1.1.0}/docs/main.md +0 -0
  47. {shinestacker-1.0.4.post2 → shinestacker-1.1.0}/docs/multilayer.md +0 -0
  48. {shinestacker-1.0.4.post2 → shinestacker-1.1.0}/docs/noise.md +0 -0
  49. {shinestacker-1.0.4.post2 → shinestacker-1.1.0}/docs/requirements.txt +0 -0
  50. {shinestacker-1.0.4.post2 → shinestacker-1.1.0}/docs/vignetting.md +0 -0
  51. {shinestacker-1.0.4.post2 → shinestacker-1.1.0}/img/coffee.gif +0 -0
  52. {shinestacker-1.0.4.post2 → shinestacker-1.1.0}/img/coffee_stack.jpg +0 -0
  53. {shinestacker-1.0.4.post2 → shinestacker-1.1.0}/img/extreme-vignetting.jpg +0 -0
  54. {shinestacker-1.0.4.post2 → shinestacker-1.1.0}/img/flies.gif +0 -0
  55. {shinestacker-1.0.4.post2 → shinestacker-1.1.0}/img/flies_stack.jpg +0 -0
  56. {shinestacker-1.0.4.post2 → shinestacker-1.1.0}/img/flow-diagram.png +0 -0
  57. {shinestacker-1.0.4.post2 → shinestacker-1.1.0}/img/gui-finder.png +0 -0
  58. {shinestacker-1.0.4.post2 → shinestacker-1.1.0}/img/gui-project-new.png +0 -0
  59. {shinestacker-1.0.4.post2 → shinestacker-1.1.0}/img/gui-project-run.png +0 -0
  60. {shinestacker-1.0.4.post2 → shinestacker-1.1.0}/img/gui-retouch.png +0 -0
  61. {shinestacker-1.0.4.post2 → shinestacker-1.1.0}/pyproject.toml +0 -0
  62. {shinestacker-1.0.4.post2 → shinestacker-1.1.0}/requirements.txt +0 -0
  63. {shinestacker-1.0.4.post2 → shinestacker-1.1.0}/scripts/build_release.py +0 -0
  64. {shinestacker-1.0.4.post2 → shinestacker-1.1.0}/scripts/git-rev-list.sh +0 -0
  65. {shinestacker-1.0.4.post2 → shinestacker-1.1.0}/scripts/validate-tomli.py +0 -0
  66. {shinestacker-1.0.4.post2 → shinestacker-1.1.0}/setup.cfg +0 -0
  67. {shinestacker-1.0.4.post2 → shinestacker-1.1.0}/src/shinestacker/__init__.py +0 -0
  68. {shinestacker-1.0.4.post2 → shinestacker-1.1.0}/src/shinestacker/algorithms/__init__.py +0 -0
  69. {shinestacker-1.0.4.post2 → shinestacker-1.1.0}/src/shinestacker/algorithms/balance.py +0 -0
  70. {shinestacker-1.0.4.post2 → shinestacker-1.1.0}/src/shinestacker/algorithms/denoise.py +0 -0
  71. {shinestacker-1.0.4.post2 → shinestacker-1.1.0}/src/shinestacker/algorithms/exif.py +0 -0
  72. {shinestacker-1.0.4.post2 → shinestacker-1.1.0}/src/shinestacker/algorithms/multilayer.py +0 -0
  73. {shinestacker-1.0.4.post2 → shinestacker-1.1.0}/src/shinestacker/algorithms/sharpen.py +0 -0
  74. {shinestacker-1.0.4.post2 → shinestacker-1.1.0}/src/shinestacker/algorithms/vignetting.py +0 -0
  75. {shinestacker-1.0.4.post2 → shinestacker-1.1.0}/src/shinestacker/algorithms/white_balance.py +0 -0
  76. {shinestacker-1.0.4.post2 → shinestacker-1.1.0}/src/shinestacker/app/__init__.py +0 -0
  77. {shinestacker-1.0.4.post2 → shinestacker-1.1.0}/src/shinestacker/app/about_dialog.py +0 -0
  78. {shinestacker-1.0.4.post2 → shinestacker-1.1.0}/src/shinestacker/app/gui_utils.py +0 -0
  79. {shinestacker-1.0.4.post2 → shinestacker-1.1.0}/src/shinestacker/app/help_menu.py +0 -0
  80. {shinestacker-1.0.4.post2 → shinestacker-1.1.0}/src/shinestacker/app/main.py +0 -0
  81. {shinestacker-1.0.4.post2 → shinestacker-1.1.0}/src/shinestacker/app/open_frames.py +0 -0
  82. {shinestacker-1.0.4.post2 → shinestacker-1.1.0}/src/shinestacker/app/project.py +0 -0
  83. {shinestacker-1.0.4.post2 → shinestacker-1.1.0}/src/shinestacker/app/retouch.py +0 -0
  84. {shinestacker-1.0.4.post2 → shinestacker-1.1.0}/src/shinestacker/config/__init__.py +0 -0
  85. {shinestacker-1.0.4.post2 → shinestacker-1.1.0}/src/shinestacker/config/config.py +0 -0
  86. {shinestacker-1.0.4.post2 → shinestacker-1.1.0}/src/shinestacker/config/gui_constants.py +0 -0
  87. {shinestacker-1.0.4.post2 → shinestacker-1.1.0}/src/shinestacker/core/__init__.py +0 -0
  88. {shinestacker-1.0.4.post2 → shinestacker-1.1.0}/src/shinestacker/core/colors.py +0 -0
  89. {shinestacker-1.0.4.post2 → shinestacker-1.1.0}/src/shinestacker/core/core_utils.py +0 -0
  90. {shinestacker-1.0.4.post2 → shinestacker-1.1.0}/src/shinestacker/core/exceptions.py +0 -0
  91. {shinestacker-1.0.4.post2 → shinestacker-1.1.0}/src/shinestacker/core/framework.py +0 -0
  92. {shinestacker-1.0.4.post2 → shinestacker-1.1.0}/src/shinestacker/core/logging.py +0 -0
  93. {shinestacker-1.0.4.post2 → shinestacker-1.1.0}/src/shinestacker/gui/__init__.py +0 -0
  94. {shinestacker-1.0.4.post2 → shinestacker-1.1.0}/src/shinestacker/gui/action_config.py +0 -0
  95. {shinestacker-1.0.4.post2 → shinestacker-1.1.0}/src/shinestacker/gui/base_form_dialog.py +0 -0
  96. {shinestacker-1.0.4.post2 → shinestacker-1.1.0}/src/shinestacker/gui/colors.py +0 -0
  97. {shinestacker-1.0.4.post2 → shinestacker-1.1.0}/src/shinestacker/gui/gui_images.py +0 -0
  98. {shinestacker-1.0.4.post2 → shinestacker-1.1.0}/src/shinestacker/gui/gui_logging.py +0 -0
  99. {shinestacker-1.0.4.post2 → shinestacker-1.1.0}/src/shinestacker/gui/ico/focus_stack_bkg.png +0 -0
  100. {shinestacker-1.0.4.post2 → shinestacker-1.1.0}/src/shinestacker/gui/ico/shinestacker.icns +0 -0
  101. {shinestacker-1.0.4.post2 → shinestacker-1.1.0}/src/shinestacker/gui/ico/shinestacker.ico +0 -0
  102. {shinestacker-1.0.4.post2 → shinestacker-1.1.0}/src/shinestacker/gui/ico/shinestacker.png +0 -0
  103. {shinestacker-1.0.4.post2 → shinestacker-1.1.0}/src/shinestacker/gui/ico/shinestacker.svg +0 -0
  104. {shinestacker-1.0.4.post2 → shinestacker-1.1.0}/src/shinestacker/gui/img/close-round-line-icon.png +0 -0
  105. {shinestacker-1.0.4.post2 → shinestacker-1.1.0}/src/shinestacker/gui/img/forward-button-icon.png +0 -0
  106. {shinestacker-1.0.4.post2 → shinestacker-1.1.0}/src/shinestacker/gui/img/play-button-round-icon.png +0 -0
  107. {shinestacker-1.0.4.post2 → shinestacker-1.1.0}/src/shinestacker/gui/img/plus-round-line-icon.png +0 -0
  108. {shinestacker-1.0.4.post2 → shinestacker-1.1.0}/src/shinestacker/gui/menu_manager.py +0 -0
  109. {shinestacker-1.0.4.post2 → shinestacker-1.1.0}/src/shinestacker/gui/project_editor.py +0 -0
  110. {shinestacker-1.0.4.post2 → shinestacker-1.1.0}/src/shinestacker/gui/project_model.py +0 -0
  111. {shinestacker-1.0.4.post2 → shinestacker-1.1.0}/src/shinestacker/gui/select_path_widget.py +0 -0
  112. {shinestacker-1.0.4.post2 → shinestacker-1.1.0}/src/shinestacker/gui/tab_widget.py +0 -0
  113. {shinestacker-1.0.4.post2 → shinestacker-1.1.0}/src/shinestacker/retouch/__init__.py +0 -0
  114. {shinestacker-1.0.4.post2 → shinestacker-1.1.0}/src/shinestacker/retouch/base_filter.py +0 -0
  115. {shinestacker-1.0.4.post2 → shinestacker-1.1.0}/src/shinestacker/retouch/brush.py +0 -0
  116. {shinestacker-1.0.4.post2 → shinestacker-1.1.0}/src/shinestacker/retouch/brush_gradient.py +0 -0
  117. {shinestacker-1.0.4.post2 → shinestacker-1.1.0}/src/shinestacker/retouch/brush_preview.py +0 -0
  118. {shinestacker-1.0.4.post2 → shinestacker-1.1.0}/src/shinestacker/retouch/brush_tool.py +0 -0
  119. {shinestacker-1.0.4.post2 → shinestacker-1.1.0}/src/shinestacker/retouch/denoise_filter.py +0 -0
  120. {shinestacker-1.0.4.post2 → shinestacker-1.1.0}/src/shinestacker/retouch/display_manager.py +0 -0
  121. {shinestacker-1.0.4.post2 → shinestacker-1.1.0}/src/shinestacker/retouch/exif_data.py +0 -0
  122. {shinestacker-1.0.4.post2 → shinestacker-1.1.0}/src/shinestacker/retouch/file_loader.py +0 -0
  123. {shinestacker-1.0.4.post2 → shinestacker-1.1.0}/src/shinestacker/retouch/filter_manager.py +0 -0
  124. {shinestacker-1.0.4.post2 → shinestacker-1.1.0}/src/shinestacker/retouch/icon_container.py +0 -0
  125. {shinestacker-1.0.4.post2 → shinestacker-1.1.0}/src/shinestacker/retouch/image_editor_ui.py +0 -0
  126. {shinestacker-1.0.4.post2 → shinestacker-1.1.0}/src/shinestacker/retouch/image_viewer.py +0 -0
  127. {shinestacker-1.0.4.post2 → shinestacker-1.1.0}/src/shinestacker/retouch/io_gui_handler.py +0 -0
  128. {shinestacker-1.0.4.post2 → shinestacker-1.1.0}/src/shinestacker/retouch/io_manager.py +0 -0
  129. {shinestacker-1.0.4.post2 → shinestacker-1.1.0}/src/shinestacker/retouch/layer_collection.py +0 -0
  130. {shinestacker-1.0.4.post2 → shinestacker-1.1.0}/src/shinestacker/retouch/shortcuts_help.py +0 -0
  131. {shinestacker-1.0.4.post2 → shinestacker-1.1.0}/src/shinestacker/retouch/undo_manager.py +0 -0
  132. {shinestacker-1.0.4.post2 → shinestacker-1.1.0}/src/shinestacker/retouch/unsharp_mask_filter.py +0 -0
  133. {shinestacker-1.0.4.post2 → shinestacker-1.1.0}/src/shinestacker/retouch/vignetting_filter.py +0 -0
  134. {shinestacker-1.0.4.post2 → shinestacker-1.1.0}/src/shinestacker/retouch/white_balance_filter.py +0 -0
  135. {shinestacker-1.0.4.post2 → shinestacker-1.1.0}/src/shinestacker.egg-info/dependency_links.txt +0 -0
  136. {shinestacker-1.0.4.post2 → shinestacker-1.1.0}/src/shinestacker.egg-info/entry_points.txt +0 -0
  137. {shinestacker-1.0.4.post2 → shinestacker-1.1.0}/src/shinestacker.egg-info/requires.txt +0 -0
  138. {shinestacker-1.0.4.post2 → shinestacker-1.1.0}/src/shinestacker.egg-info/top_level.txt +0 -0
@@ -2,7 +2,20 @@
2
2
 
3
3
  This page reports the main releases only and the main changes therein.
4
4
 
5
- ## [v-.-.-] - 2025-08-26
5
+ ## [v1.1.0] - 2025-08-28
6
+ **New Pyramids algorith, some improvements and more fixes**
7
+
8
+ ### Changes
9
+
10
+ * added Pyramids Tiles, that requires less RAM by fusing the image in tiles
11
+ * the alignment module now tolerates images of different shapes
12
+ * noisy pixel mask verifies that the mask has the same shape as input images
13
+ * minor changes to default alignment parameters
14
+ * some improvements to the GUI
15
+ * some bug fixes
16
+ ---
17
+
18
+ ## [v1.0.4.pre2] - 2025-08-26
6
19
  **Bug fixes**
7
20
 
8
21
  ### Changes
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: shinestacker
3
- Version: 1.0.4.post2
3
+ Version: 1.1.0
4
4
  Summary: ShineStacker
5
5
  Author-email: Luca Lista <luka.lista@gmail.com>
6
6
  License-Expression: LGPL-3.0
@@ -69,6 +69,25 @@ The GUI has two main working areas:
69
69
 
70
70
  <img src='https://raw.githubusercontent.com/lucalista/shinestacker/main/img/gui-retouch.png' width="600" referrerpolicy="no-referrer">
71
71
 
72
+ # Note for macOS users
73
+
74
+ **The following note is only relevant if you download the application as compressed archive from the [release page](https://github.com/lucalista/shinestacker/releases).**
75
+
76
+ The macOS system security protection prevent to run applications downloaded from the web that come from developers that don't hold an Apple Developer Certificate.
77
+
78
+ In order to prevent this, follow the instructions below:
79
+
80
+ 1. Download the compressed archive ```shinestacker-macos.tar.gz``` in your ```Download``` folder.
81
+ 2. Double-click the archive to uncompress it. You will find a new folder ```shinestacker```.
82
+ 3. Open a terminal (*Applications > Utilities > Terminal*)
83
+ 4. Type the folliwng command on the terminal:
84
+ ```bash
85
+ xattr -cr ~/Downloads/shinestacker/shinestacker.app
86
+ ```
87
+ 5. Now you can double-click the Sine Stacker icon app in the ```shiestacker``` folder and it should run.
88
+
89
+ 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
+
72
91
  # Resources
73
92
 
74
93
  🌍 [Website on WordPress](https://shinestacker.wordpress.com) • 📖 [Main documentation](https://shinestacker.readthedocs.io) • 📝 [Changelog](https://github.com/lucalista/shinestacker/blob/main/CHANGELOG.md)
@@ -38,6 +38,25 @@ 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
+ # Note for macOS users
42
+
43
+ **The following note is only relevant if you download the application as compressed archive from the [release page](https://github.com/lucalista/shinestacker/releases).**
44
+
45
+ The macOS system security protection prevent to run applications downloaded from the web that come from developers that don't hold an Apple Developer Certificate.
46
+
47
+ In order to prevent this, follow the instructions below:
48
+
49
+ 1. Download the compressed archive ```shinestacker-macos.tar.gz``` in your ```Download``` folder.
50
+ 2. Double-click the archive to uncompress it. You will find a new folder ```shinestacker```.
51
+ 3. Open a terminal (*Applications > Utilities > Terminal*)
52
+ 4. Type the folliwng command on the terminal:
53
+ ```bash
54
+ xattr -cr ~/Downloads/shinestacker/shinestacker.app
55
+ ```
56
+ 5. Now you can double-click the Sine Stacker icon app in the ```shiestacker``` folder and it should run.
57
+
58
+ 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
+
41
60
  # Resources
42
61
 
43
62
  🌍 [Website on WordPress](https://shinestacker.wordpress.com) • 📖 [Main documentation](https://shinestacker.readthedocs.io) • 📝 [Changelog](https://github.com/lucalista/shinestacker/blob/main/CHANGELOG.md)
@@ -42,6 +42,11 @@ Arguments for the constructor are:
42
42
  * ```gen_kernel``` (optional, default: 0.4)
43
43
  * ```float_type``` (optional, default: ```FLOAT_32```, possible values: ```FLOAT_32```, ```FLOAT_64```): precision for internal image representation
44
44
 
45
+ ```PyramidTilesStack```, Laplacian pyramid with tile pyramid merging to optimize memory usage for large files
46
+
47
+ Arguments for the constructor are the same ad for ```PyramidStack``` plus:
48
+ * ```tile_size``` (optional, default: 512): size of the tile used for partial image merging
49
+
45
50
  ```DepthMapStack```, Depth map focus stacking algorithm
46
51
 
47
52
  Arguments for the constructor are:
@@ -0,0 +1 @@
1
+ __version__ = '1.1.0'
@@ -6,7 +6,7 @@ import cv2
6
6
  from .. config.constants import constants
7
7
  from .. core.exceptions import AlignmentError, InvalidOptionError
8
8
  from .. core.colors import color_str
9
- from .utils import img_8bit, img_bw_8bit, save_plot, get_img_metadata, validate_image, img_subsample
9
+ from .utils import img_8bit, img_bw_8bit, save_plot, img_subsample
10
10
  from .stack_framework import SubAction
11
11
 
12
12
  _DEFAULT_FEATURE_CONFIG = {
@@ -161,9 +161,10 @@ def align_images(img_1, img_0, feature_config=None, matching_config=None, alignm
161
161
  except KeyError as e:
162
162
  raise InvalidOptionError("border_mode", alignment_config['border_mode']) from e
163
163
  min_matches = 4 if alignment_config['transform'] == constants.ALIGN_HOMOGRAPHY else 3
164
- validate_image(img_0, *get_img_metadata(img_1))
165
164
  if callbacks and 'message' in callbacks:
166
165
  callbacks['message']()
166
+ h_ref, w_ref = img_1.shape[:2]
167
+ h0, w0 = img_0.shape[:2]
167
168
  subsample = alignment_config['subsample']
168
169
  fast_subsampling = alignment_config['fast_subsampling']
169
170
  min_good_matches = alignment_config['min_good_matches']
@@ -204,15 +205,14 @@ def align_images(img_1, img_0, feature_config=None, matching_config=None, alignm
204
205
  flags=2), cv2.COLOR_BGR2RGB)
205
206
  plt.figure(figsize=(10, 5))
206
207
  plt.imshow(img_match, 'gray')
207
- plt.savefig(plot_path)
208
+ save_plot(plot_path)
208
209
  if callbacks and 'save_plot' in callbacks:
209
210
  callbacks['save_plot'](plot_path)
210
- h, w = img_0.shape[:2]
211
211
  h_sub, w_sub = img_0_sub.shape[:2]
212
212
  if subsample > 1:
213
213
  if transform == constants.ALIGN_HOMOGRAPHY:
214
214
  low_size = np.float32([[0, 0], [0, h_sub], [w_sub, h_sub], [w_sub, 0]])
215
- high_size = np.float32([[0, 0], [0, h], [w, h], [w, 0]])
215
+ high_size = np.float32([[0, 0], [0, h0], [w0, h0], [w0, 0]])
216
216
  scale_up = cv2.getPerspectiveTransform(low_size, high_size)
217
217
  scale_down = cv2.getPerspectiveTransform(high_size, low_size)
218
218
  m = scale_up @ m @ scale_down
@@ -230,17 +230,17 @@ def align_images(img_1, img_0, feature_config=None, matching_config=None, alignm
230
230
  img_mask = np.ones_like(img_0, dtype=np.uint8)
231
231
  if alignment_config['transform'] == constants.ALIGN_HOMOGRAPHY:
232
232
  img_warp = cv2.warpPerspective(
233
- img_0, m, (w, h),
233
+ img_0, m, (w_ref, h_ref),
234
234
  borderMode=cv2_border_mode, borderValue=alignment_config['border_value'])
235
235
  if alignment_config['border_mode'] == constants.BORDER_REPLICATE_BLUR:
236
- mask = cv2.warpPerspective(img_mask, m, (w, h),
236
+ mask = cv2.warpPerspective(img_mask, m, (w_ref, h_ref),
237
237
  borderMode=cv2.BORDER_CONSTANT, borderValue=0)
238
238
  elif alignment_config['transform'] == constants.ALIGN_RIGID:
239
239
  img_warp = cv2.warpAffine(
240
- img_0, m, (w, h),
240
+ img_0, m, (w_ref, h_ref),
241
241
  borderMode=cv2_border_mode, borderValue=alignment_config['border_value'])
242
242
  if alignment_config['border_mode'] == constants.BORDER_REPLICATE_BLUR:
243
- mask = cv2.warpAffine(img_mask, m, (w, h),
243
+ mask = cv2.warpAffine(img_mask, m, (w_ref, h_ref),
244
244
  borderMode=cv2.BORDER_CONSTANT, borderValue=0)
245
245
  if alignment_config['border_mode'] == constants.BORDER_REPLICATE_BLUR:
246
246
  if callbacks and 'blur_message' in callbacks:
@@ -293,7 +293,7 @@ class AlignFrames(SubAction):
293
293
  'ecc_message': lambda: self.sub_msg(": ecc refinement"),
294
294
  'blur_message': lambda: self.sub_msg(': blur borders'),
295
295
  'warning': lambda msg: self.sub_msg(
296
- f': {msg}', constants.LOG_COLOR_ALERT),
296
+ f': {msg}', constants.LOG_COLOR_WARNING),
297
297
  'save_plot': lambda plot_path: self.process.callback(
298
298
  'save_plot', self.process.id,
299
299
  f"{self.process.name}: matches\nframe {idx_str}", plot_path)
@@ -315,7 +315,7 @@ class AlignFrames(SubAction):
315
315
  if n_good_matches < self.min_matches:
316
316
  self.process.sub_message(f": image not aligned, too few matches found: "
317
317
  f"{n_good_matches}", level=logging.CRITICAL)
318
- raise AlignmentError(idx, f"too few matches found: "
318
+ raise AlignmentError(idx, f"Image not aligned, too few matches found: "
319
319
  f"{n_good_matches} < {self.min_matches}")
320
320
  return img
321
321
 
@@ -1,9 +1,10 @@
1
1
  # pylint: disable=C0114, C0115, C0116, E0602, R0903
2
+ import os
2
3
  import numpy as np
3
- from .. core.exceptions import InvalidOptionError, ImageLoadError
4
+ from .. core.exceptions import InvalidOptionError, ImageLoadError, RunStopException
4
5
  from .. config.constants import constants
5
6
  from .. core.colors import color_str
6
- from .utils import read_img, get_img_metadata, validate_image
7
+ from .utils import read_img, get_img_metadata, validate_image, get_img_file_shape, extension_tif_jpg
7
8
 
8
9
 
9
10
  class BaseStackAlgo:
@@ -11,6 +12,9 @@ class BaseStackAlgo:
11
12
  self._name = name
12
13
  self._steps_per_frame = steps_per_frame
13
14
  self.process = None
15
+ self.filenames = None
16
+ self.shape = None
17
+ self.do_step_callback = False
14
18
  if float_type == constants.FLOAT_32:
15
19
  self.float_type = np.float32
16
20
  elif float_type == constants.FLOAT_64:
@@ -24,8 +28,17 @@ class BaseStackAlgo:
24
28
  def name(self):
25
29
  return self._name
26
30
 
27
- def steps_per_frame(self):
28
- return self._steps_per_frame
31
+ def init(self, filenames):
32
+ self.filenames = filenames
33
+ first_img_file = ''
34
+ for filename in filenames:
35
+ if os.path.isfile(filename) and extension_tif_jpg(filename):
36
+ first_img_file = filename
37
+ break
38
+ self.shape = get_img_file_shape(first_img_file)
39
+
40
+ def total_steps(self, n_frames):
41
+ return self._steps_per_frame * n_frames
29
42
 
30
43
  def print_message(self, msg):
31
44
  self.process.sub_message_r(color_str(msg, constants.LOG_COLOR_LEVEL_3))
@@ -40,3 +53,13 @@ class BaseStackAlgo:
40
53
  else:
41
54
  validate_image(img, *metadata)
42
55
  return img, metadata, updated
56
+
57
+ def check_running(self, cleanup_callback=None):
58
+ if self.process.callback('check_running', self.process.id, self.process.name) is False:
59
+ if cleanup_callback is not None:
60
+ cleanup_callback()
61
+ raise RunStopException(self.name)
62
+
63
+ def after_step(self, step):
64
+ if self.do_step_callback:
65
+ self.process.callback('after_step', self.process.id, self.process.name, step)
@@ -2,7 +2,7 @@
2
2
  import numpy as np
3
3
  import cv2
4
4
  from .. config.constants import constants
5
- from .. core.exceptions import InvalidOptionError, RunStopException
5
+ from .. core.exceptions import InvalidOptionError
6
6
  from .utils import read_img, img_bw
7
7
  from .base_stack_algo import BaseStackAlgo
8
8
 
@@ -61,19 +61,18 @@ class DepthMapStack(BaseStackAlgo):
61
61
  raise InvalidOptionError("map_type", self.map_type, details=f" valid values are "
62
62
  f"{constants.DM_MAP_AVERAGE} and {constants.DM_MAP_MAX}.")
63
63
 
64
- def focus_stack(self, filenames):
64
+ def focus_stack(self):
65
65
  gray_images = []
66
66
  metadata = None
67
- for i, img_path in enumerate(filenames):
67
+ for i, img_path in enumerate(self.filenames):
68
68
  self.print_message(f": reading file (1/2) {img_path.split('/')[-1]}")
69
69
 
70
70
  img, metadata, _updated = self.read_image_and_update_metadata(img_path, metadata)
71
71
 
72
72
  gray = img_bw(img)
73
73
  gray_images.append(gray)
74
- self.process.callback('after_step', self.process.id, self.process.name, i)
75
- if self.process.callback('check_running', self.process.id, self.process.name) is False:
76
- raise RunStopException(self.name)
74
+ self.after_step(i)
75
+ self.check_running()
77
76
  dtype = metadata[1]
78
77
  gray_images = np.array(gray_images, dtype=self.float_type)
79
78
  if self.energy == constants.DM_ENERGY_SOBEL:
@@ -92,7 +91,7 @@ class DepthMapStack(BaseStackAlgo):
92
91
  energies = self.smooth_energy(energies)
93
92
  weights = self.get_focus_map(energies)
94
93
  blended_pyramid = None
95
- for i, img_path in enumerate(filenames):
94
+ for i, img_path in enumerate(self.filenames):
96
95
  self.print_message(f": reading file (2/2) {img_path.split('/')[-1]}")
97
96
  img = read_img(img_path).astype(self.float_type)
98
97
  weight = weights[i]
@@ -110,10 +109,8 @@ class DepthMapStack(BaseStackAlgo):
110
109
  for j in range(self.levels)]
111
110
  blended_pyramid = current_blend if blended_pyramid is None \
112
111
  else [np.add(bp, cb) for bp, cb in zip(blended_pyramid, current_blend)]
113
- self.process.callback('after_step', self.process.id,
114
- self.process.name, i + len(filenames))
115
- if self.process.callback('check_running', self.process.id, self.process.name) is False:
116
- raise RunStopException(self.name)
112
+ self.after_step(i + len(self.filenames))
113
+ self.check_running()
117
114
  result = blended_pyramid[0]
118
115
  self.print_message(': blend levels')
119
116
  for j in range(1, self.levels):
@@ -1,4 +1,4 @@
1
- # pylint: disable=C0114, C0115, C0116, E1101, W0718, R0914, R0915
1
+ # pylint: disable=C0114, C0115, C0116, E1101, W0718, R0914, R0915, R0902
2
2
  import os
3
3
  import errno
4
4
  import logging
@@ -11,7 +11,7 @@ from .. core.colors import color_str
11
11
  from .. core.exceptions import ImageLoadError
12
12
  from .. core.framework import JobBase
13
13
  from .. core.core_utils import make_tqdm_bar
14
- from .. core.exceptions import RunStopException
14
+ from .. core.exceptions import RunStopException, ShapeError
15
15
  from .stack_framework import FrameMultiDirectory, SubAction
16
16
  from .utils import read_img, save_plot, get_img_metadata, validate_image
17
17
 
@@ -154,6 +154,7 @@ class MaskNoise(SubAction):
154
154
  self.method = method
155
155
  self.process = None
156
156
  self.noise_mask_img = None
157
+ self.expected_shape = None
157
158
 
158
159
  def begin(self, process):
159
160
  self.process = process
@@ -163,6 +164,7 @@ class MaskNoise(SubAction):
163
164
  f': reading noisy pixel mask file: {self.noise_mask}',
164
165
  constants.LOG_COLOR_LEVEL_3))
165
166
  self.noise_mask_img = cv2.imread(path, cv2.IMREAD_GRAYSCALE)
167
+ self.expected_shape = self.noise_mask_img.shape[:2]
166
168
  if self.noise_mask_img is None:
167
169
  raise ImageLoadError(path, f"failed to load image file {self.noise_mask}.")
168
170
  else:
@@ -170,6 +172,9 @@ class MaskNoise(SubAction):
170
172
 
171
173
  def run_frame(self, _idx, _ref_idx, image):
172
174
  self.process.sub_message_r(color_str(': mask noisy pixels', constants.LOG_COLOR_LEVEL_3))
175
+ shape = image.shape[:2]
176
+ if shape != self.expected_shape:
177
+ raise ShapeError(self.expected_shape, shape)
173
178
  if len(image.shape) == 3:
174
179
  corrected = image.copy()
175
180
  for c in range(3):
@@ -1,25 +1,36 @@
1
- # pylint: disable=C0114, C0115, C0116, E1101
1
+ # pylint: disable=C0114, C0115, C0116, E1101, R0913, R0917, R0902
2
2
  import numpy as np
3
3
  import cv2
4
4
  from .. config.constants import constants
5
- from .. core.exceptions import RunStopException
6
5
  from .utils import read_img
7
6
  from .base_stack_algo import BaseStackAlgo
8
7
 
9
8
 
10
9
  class PyramidBase(BaseStackAlgo):
11
- def __init__(self, min_size=constants.DEFAULT_PY_MIN_SIZE,
10
+ def __init__(self, name, min_size=constants.DEFAULT_PY_MIN_SIZE,
12
11
  kernel_size=constants.DEFAULT_PY_KERNEL_SIZE,
13
12
  gen_kernel=constants.DEFAULT_PY_GEN_KERNEL,
14
13
  float_type=constants.DEFAULT_PY_FLOAT):
15
- super().__init__("pyramid", 2, float_type)
14
+ super().__init__(name, 2, float_type)
16
15
  self.min_size = min_size
17
16
  self.kernel_size = kernel_size
18
17
  self.pad_amount = (kernel_size - 1) // 2
19
- self.do_step_callback = False
20
18
  kernel = np.array([0.25 - gen_kernel / 2.0, 0.25,
21
19
  gen_kernel, 0.25, 0.25 - gen_kernel / 2.0])
22
20
  self.gen_kernel = np.outer(kernel, kernel)
21
+ self.dtype = None
22
+ self.num_pixel_values = None
23
+ self.max_pixel_value = None
24
+ self.n_levels = 0
25
+ self.n_frames = 0
26
+
27
+ def init(self, filenames):
28
+ super().init(filenames)
29
+ self.n_levels = int(np.log2(min(self.shape) / self.min_size))
30
+
31
+ def total_steps(self, n_frames):
32
+ self.n_frames = n_frames
33
+ return self._steps_per_frame * n_frames + self.n_levels
23
34
 
24
35
  def convolve(self, image):
25
36
  return cv2.filter2D(image, -1, self.gen_kernel, borderType=cv2.BORDER_REFLECT101)
@@ -55,6 +66,7 @@ class PyramidBase(BaseStackAlgo):
55
66
  return fused
56
67
 
57
68
  def collapse(self, pyramid):
69
+ self.print_message(': collapsing pyramid')
58
70
  img = pyramid[-1]
59
71
  for layer in pyramid[-2::-1]:
60
72
  expanded = self.expand_layer(img)
@@ -110,19 +122,22 @@ class PyramidBase(BaseStackAlgo):
110
122
  fused += np.where(best_d[:, :, np.newaxis] == layer, img, 0)
111
123
  return (fused / 2).astype(images.dtype)
112
124
 
125
+ def focus_stack_validate(self, cleanup_callback=None):
126
+ metadata = None
127
+ for i, img_path in enumerate(self.filenames):
128
+ self.print_message(f": validating file {img_path.split('/')[-1]}")
113
129
 
114
- class PyramidStack(PyramidBase):
115
- def __init__(self, min_size=constants.DEFAULT_PY_MIN_SIZE,
116
- kernel_size=constants.DEFAULT_PY_KERNEL_SIZE,
117
- gen_kernel=constants.DEFAULT_PY_GEN_KERNEL,
118
- float_type=constants.DEFAULT_PY_FLOAT):
119
- super().__init__(min_size, kernel_size, gen_kernel, float_type)
120
- self.offset = np.arange(-self.pad_amount, self.pad_amount + 1)
121
- self.dtype = None
122
- self.num_pixel_values = None
123
- self.max_pixel_value = None
130
+ _img, metadata, updated = self.read_image_and_update_metadata(img_path, metadata)
131
+ if updated:
132
+ self.dtype = metadata[1]
133
+ self.num_pixel_values = constants.NUM_UINT8 \
134
+ if self.dtype == np.uint8 else constants.NUM_UINT16
135
+ self.max_pixel_value = constants.MAX_UINT8 \
136
+ if self.dtype == np.uint8 else constants.MAX_UINT16
137
+ self.after_step(i + 1)
138
+ self.check_running(cleanup_callback)
124
139
 
125
- def process_single_image(self, img, levels):
140
+ def single_image_laplacian(self, img, levels):
126
141
  pyramid = [img.astype(self.float_type)]
127
142
  for _ in range(levels):
128
143
  next_layer = self.reduce_layer(pyramid[-1])
@@ -136,44 +151,43 @@ class PyramidStack(PyramidBase):
136
151
  h, w = pyr.shape[:2]
137
152
  expanded = expanded[:h, :w]
138
153
  laplacian.append(pyr - expanded)
154
+ return laplacian
155
+
156
+
157
+ class PyramidStack(PyramidBase):
158
+ def __init__(self, min_size=constants.DEFAULT_PY_MIN_SIZE,
159
+ kernel_size=constants.DEFAULT_PY_KERNEL_SIZE,
160
+ gen_kernel=constants.DEFAULT_PY_GEN_KERNEL,
161
+ float_type=constants.DEFAULT_PY_FLOAT):
162
+ super().__init__("pyramid", min_size, kernel_size, gen_kernel, float_type)
163
+ self.offset = np.arange(-self.pad_amount, self.pad_amount + 1)
164
+
165
+ def process_single_image(self, img, levels):
166
+ laplacian = self.single_image_laplacian(img, levels)
139
167
  return laplacian[::-1]
140
168
 
141
169
  def fuse_pyramids(self, all_laplacians):
142
170
  fused = [self.get_fused_base(np.stack([p[-1] for p in all_laplacians], axis=0))]
171
+ count = 0
143
172
  for layer in range(len(all_laplacians[0]) - 2, -1, -1):
144
173
  self.print_message(f': fusing pyramids, layer: {layer + 1}')
145
174
  laplacians = np.stack([p[layer] for p in all_laplacians], axis=0)
146
175
  fused.append(self.fuse_laplacian(laplacians))
176
+ count += 1
177
+ self.after_step(self._steps_per_frame * self.n_frames + count)
178
+ self.check_running()
147
179
  self.print_message(': pyramids fusion completed')
148
180
  return fused[::-1]
149
181
 
150
- def focus_stack(self, filenames):
151
- metadata = None
182
+ def focus_stack(self):
183
+ n = len(self.filenames)
184
+ self.focus_stack_validate()
152
185
  all_laplacians = []
153
- levels = None
154
- n = len(filenames)
155
- for i, img_path in enumerate(filenames):
156
- self.print_message(f": validating file {img_path.split('/')[-1]}")
157
-
158
- img, metadata, updated = self.read_image_and_update_metadata(img_path, metadata)
159
- if updated:
160
- self.dtype = metadata[1]
161
- self.num_pixel_values = constants.NUM_UINT8 \
162
- if self.dtype == np.uint8 else constants.NUM_UINT16
163
- self.max_pixel_value = constants.MAX_UINT8 \
164
- if self.dtype == np.uint8 else constants.MAX_UINT16
165
- levels = int(np.log2(min(img.shape[:2]) / self.min_size))
166
- if self.do_step_callback:
167
- self.process.callback('after_step', self.process.id, self.process.name, i)
168
- if self.process.callback('check_running', self.process.id, self.process.name) is False:
169
- raise RunStopException(self.name)
170
- for i, img_path in enumerate(filenames):
186
+ for i, img_path in enumerate(self.filenames):
171
187
  self.print_message(f": processing file {img_path.split('/')[-1]}")
172
188
  img = read_img(img_path)
173
- all_laplacians.append(self.process_single_image(img, levels))
174
- if self.do_step_callback:
175
- self.process.callback('after_step', self.process.id, self.process.name, i + n)
176
- if self.process.callback('check_running', self.process.id, self.process.name) is False:
177
- raise RunStopException(self.name)
189
+ all_laplacians.append(self.process_single_image(img, self.n_levels))
190
+ self.after_step(i + n + 1)
191
+ self.check_running()
178
192
  stacked_image = self.collapse(self.fuse_pyramids(all_laplacians))
179
193
  return stacked_image.astype(self.dtype)
@@ -0,0 +1,109 @@
1
+ # pylint: disable=C0114, C0115, C0116, E1101, R0914, R1702, R1732, R0913, R0917, R0912, R0915
2
+ import os
3
+ import tempfile
4
+ import numpy as np
5
+ from .. config.constants import constants
6
+ from .utils import read_img
7
+ from .pyramid import PyramidBase
8
+
9
+
10
+ class PyramidTilesStack(PyramidBase):
11
+ def __init__(self, min_size=constants.DEFAULT_PY_MIN_SIZE,
12
+ kernel_size=constants.DEFAULT_PY_KERNEL_SIZE,
13
+ gen_kernel=constants.DEFAULT_PY_GEN_KERNEL,
14
+ float_type=constants.DEFAULT_PY_FLOAT,
15
+ tile_size=constants.DEFAULT_PY_TILE_SIZE):
16
+ super().__init__("fast_pyramid", min_size, kernel_size, gen_kernel, float_type)
17
+ self.offset = np.arange(-self.pad_amount, self.pad_amount + 1)
18
+ self.dtype = None
19
+ self.num_pixel_values = None
20
+ self.max_pixel_value = None
21
+ self.tile_size = tile_size
22
+ self.temp_dir = tempfile.TemporaryDirectory()
23
+ self.n_tiles = 0
24
+
25
+ def init(self, filenames):
26
+ super().init(filenames)
27
+ self.n_tiles = (self.shape[0] // self.tile_size + 1) * (self.shape[1] // self.tile_size + 1)
28
+
29
+ def total_steps(self, n_frames):
30
+ n_steps = super().total_steps(n_frames)
31
+ return n_steps + self.n_tiles
32
+
33
+ def process_single_image(self, img, levels, img_index):
34
+ laplacian = self.single_image_laplacian(img, levels)
35
+ for i, level_data in enumerate(laplacian[::-1]):
36
+ np.save(os.path.join(self.temp_dir.name, f'img_{img_index}_level_{i}.npy'), level_data)
37
+ return len(laplacian)
38
+
39
+ def load_level(self, img_index, level):
40
+ return np.load(os.path.join(self.temp_dir.name, f'img_{img_index}_level_{level}.npy'))
41
+
42
+ def cleanup_temp_files(self):
43
+ self.temp_dir.cleanup()
44
+
45
+ def fuse_pyramids(self, all_level_counts, num_images):
46
+ max_levels = max(all_level_counts)
47
+ fused = []
48
+ count = self._steps_per_frame * self.n_frames
49
+ for level in range(max_levels - 1, -1, -1):
50
+ self.print_message(f': fusing pyramids, layer: {level + 1}')
51
+ if level == 0:
52
+ sample_level = self.load_level(0, 0)
53
+ h, w = sample_level.shape[:2]
54
+ del sample_level
55
+ fused_level = np.zeros((h, w, 3), dtype=self.float_type)
56
+ for y in range(0, h, self.tile_size):
57
+ for x in range(0, w, self.tile_size):
58
+ y_end = min(y + self.tile_size, h)
59
+ x_end = min(x + self.tile_size, w)
60
+ self.print_message(f': fusing tile [{x}, {x_end - 1}]×[{y}, {y_end - 1}]')
61
+ laplacians = []
62
+ for img_index in range(num_images):
63
+ if level < all_level_counts[img_index]:
64
+ full_laplacian = self.load_level(img_index, level)
65
+ tile = full_laplacian[y:y_end, x:x_end]
66
+ laplacians.append(tile)
67
+ del full_laplacian
68
+ stacked = np.stack(laplacians, axis=0)
69
+ fused_tile = self.fuse_laplacian(stacked)
70
+ fused_level[y:y_end, x:x_end] = fused_tile
71
+ del laplacians, stacked, fused_tile
72
+ self.after_step(count)
73
+ self.check_running(self.cleanup_temp_files)
74
+ count += 1
75
+ else:
76
+ laplacians = []
77
+ for img_index in range(num_images):
78
+ if level < all_level_counts[img_index]:
79
+ laplacian = self.load_level(img_index, level)
80
+ laplacians.append(laplacian)
81
+ if level == max_levels - 1:
82
+ stacked = np.stack(laplacians, axis=0)
83
+ fused_level = self.get_fused_base(stacked)
84
+ else:
85
+ stacked = np.stack(laplacians, axis=0)
86
+ fused_level = self.fuse_laplacian(stacked)
87
+ self.check_running(self.cleanup_temp_files)
88
+ fused.append(fused_level)
89
+ count += 1
90
+ self.after_step(count)
91
+ self.check_running(self.cleanup_temp_files)
92
+ self.print_message(': pyramids fusion completed')
93
+ return fused[::-1]
94
+
95
+ def focus_stack(self):
96
+ n = len(self.filenames)
97
+ self.focus_stack_validate(self.cleanup_temp_files)
98
+ all_level_counts = []
99
+ for i, img_path in enumerate(self.filenames):
100
+ self.print_message(f": processing file {img_path.split('/')[-1]}")
101
+ img = read_img(img_path)
102
+ level_count = self.process_single_image(img, self.n_levels, i)
103
+ all_level_counts.append(level_count)
104
+ self.after_step(i + n + 1)
105
+ self.check_running(self.cleanup_temp_files)
106
+ fused_pyramid = self.fuse_pyramids(all_level_counts, n)
107
+ stacked_image = self.collapse(fused_pyramid)
108
+ self.cleanup_temp_files()
109
+ return stacked_image.astype(self.dtype)
@@ -5,7 +5,7 @@ from .. config.constants import constants
5
5
  from .. core.framework import JobBase
6
6
  from .. core.colors import color_str
7
7
  from .. core.exceptions import InvalidOptionError
8
- from .utils import write_img
8
+ from .utils import write_img, extension_tif_jpg
9
9
  from .stack_framework import FrameDirectory, ActionList
10
10
  from .exif import copy_exif_from_file_to_file
11
11
  from .denoise import denoise
@@ -25,8 +25,7 @@ class FocusStackBase(JobBase, FrameDirectory):
25
25
 
26
26
  def focus_stack(self, filenames):
27
27
  self.sub_message_r(color_str(': reading input files', constants.LOG_COLOR_LEVEL_3))
28
- img_files = sorted([os.path.join(self.input_full_path, name) for name in filenames])
29
- stacked_img = self.stack_algo.focus_stack(img_files)
28
+ stacked_img = self.stack_algo.focus_stack()
30
29
  in_filename = filenames[0].split(".")
31
30
  out_filename = f"{self.output_dir}/{self.prefix}{in_filename[0]}." + \
32
31
  '.'.join(in_filename[1:])
@@ -37,8 +36,7 @@ class FocusStackBase(JobBase, FrameDirectory):
37
36
  if self.exif_path != '' and stacked_img.dtype == np.uint8:
38
37
  self.sub_message_r(': copy exif data')
39
38
  _dirpath, _, fnames = next(os.walk(self.exif_path))
40
- fnames = [name for name in fnames
41
- if os.path.splitext(name)[-1][1:].lower() in constants.EXTENSIONS]
39
+ fnames = [name for name in fnames if extension_tif_jpg(name)]
42
40
  exif_filename = f"{self.exif_path}/{fnames[0]}"
43
41
  copy_exif_from_file_to_file(exif_filename, out_filename)
44
42
  self.sub_message_r(' ' * 60)
@@ -52,6 +50,7 @@ class FocusStackBase(JobBase, FrameDirectory):
52
50
  self.frame_count += 1
53
51
 
54
52
  def init(self, job, working_path=''):
53
+ FrameDirectory.init(self, job)
55
54
  if self.exif_path is None:
56
55
  self.exif_path = job.paths[0]
57
56
  if self.exif_path != '':
@@ -79,7 +78,6 @@ class FocusStackBunch(ActionList, FocusStackBase):
79
78
  "overlap must be smaller than batch size")
80
79
 
81
80
  def init(self, job, _working_path=''):
82
- FrameDirectory.init(self, job)
83
81
  FocusStackBase.init(self, job, self.working_path)
84
82
 
85
83
  def begin(self):
@@ -94,6 +92,9 @@ class FocusStackBunch(ActionList, FocusStackBase):
94
92
  def run_step(self):
95
93
  self.print_message_r(color_str(f"fusing bunch: {self.count + 1}/{self.counts}",
96
94
  constants.LOG_COLOR_LEVEL_2))
95
+ img_files = [os.path.join(self.input_full_path, name)
96
+ for name in self._chunks[self.count - 1]]
97
+ self.stack_algo.init(img_files)
97
98
  self.focus_stack(self._chunks[self.count - 1])
98
99
 
99
100
 
@@ -101,13 +102,15 @@ class FocusStack(FocusStackBase):
101
102
  def __init__(self, name, stack_algo, enabled=True, **kwargs):
102
103
  super().__init__(name, stack_algo, enabled, **kwargs)
103
104
  self.stack_algo.do_step_callback = True
105
+ self.shape = None
104
106
 
105
107
  def run_core(self):
106
108
  self.set_filelist()
109
+ img_files = sorted([os.path.join(self.input_full_path, name) for name in self.filenames])
110
+ self.stack_algo.init(img_files)
107
111
  self.callback('step_counts', self.id, self.name,
108
- self.stack_algo.steps_per_frame() * len(self.filenames))
112
+ self.stack_algo.total_steps(len(self.filenames)))
109
113
  self.focus_stack(self.filenames)
110
114
 
111
115
  def init(self, job, _working_path=''):
112
- FrameDirectory.init(self, job)
113
116
  FocusStackBase.init(self, job, self.working_path)