shinestacker 1.1.0__tar.gz → 1.2.1__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.1.0 → shinestacker-1.2.1}/CHANGELOG.md +30 -2
  2. {shinestacker-1.1.0/src/shinestacker.egg-info → shinestacker-1.2.1}/PKG-INFO +1 -1
  3. {shinestacker-1.1.0 → shinestacker-1.2.1}/docs/alignment.md +2 -2
  4. {shinestacker-1.1.0 → shinestacker-1.2.1}/docs/balancing.md +7 -2
  5. {shinestacker-1.1.0 → shinestacker-1.2.1}/docs/focus_stacking.md +15 -2
  6. {shinestacker-1.1.0 → shinestacker-1.2.1}/docs/job.md +1 -1
  7. {shinestacker-1.1.0 → shinestacker-1.2.1}/docs/main.md +2 -3
  8. {shinestacker-1.1.0 → shinestacker-1.2.1}/docs/vignetting.md +1 -0
  9. shinestacker-1.2.1/src/shinestacker/_version.py +1 -0
  10. {shinestacker-1.1.0 → shinestacker-1.2.1}/src/shinestacker/algorithms/__init__.py +4 -1
  11. {shinestacker-1.1.0 → shinestacker-1.2.1}/src/shinestacker/algorithms/align.py +149 -34
  12. shinestacker-1.2.1/src/shinestacker/algorithms/balance.py +614 -0
  13. {shinestacker-1.1.0 → shinestacker-1.2.1}/src/shinestacker/algorithms/base_stack_algo.py +6 -0
  14. {shinestacker-1.1.0 → shinestacker-1.2.1}/src/shinestacker/algorithms/depth_map.py +1 -1
  15. {shinestacker-1.1.0 → shinestacker-1.2.1}/src/shinestacker/algorithms/multilayer.py +22 -13
  16. {shinestacker-1.1.0 → shinestacker-1.2.1}/src/shinestacker/algorithms/noise_detection.py +7 -8
  17. {shinestacker-1.1.0 → shinestacker-1.2.1}/src/shinestacker/algorithms/pyramid.py +3 -2
  18. shinestacker-1.2.1/src/shinestacker/algorithms/pyramid_auto.py +141 -0
  19. shinestacker-1.2.1/src/shinestacker/algorithms/pyramid_tiles.py +264 -0
  20. {shinestacker-1.1.0 → shinestacker-1.2.1}/src/shinestacker/algorithms/stack.py +20 -20
  21. shinestacker-1.2.1/src/shinestacker/algorithms/stack_framework.py +279 -0
  22. shinestacker-1.2.1/src/shinestacker/algorithms/utils.py +305 -0
  23. {shinestacker-1.1.0 → shinestacker-1.2.1}/src/shinestacker/algorithms/vignetting.py +26 -8
  24. {shinestacker-1.1.0 → shinestacker-1.2.1}/src/shinestacker/config/constants.py +31 -6
  25. {shinestacker-1.1.0 → shinestacker-1.2.1}/src/shinestacker/core/framework.py +12 -12
  26. {shinestacker-1.1.0 → shinestacker-1.2.1}/src/shinestacker/gui/action_config.py +59 -7
  27. shinestacker-1.2.1/src/shinestacker/gui/action_config_dialog.py +733 -0
  28. shinestacker-1.2.1/src/shinestacker/gui/base_form_dialog.py +23 -0
  29. {shinestacker-1.1.0 → shinestacker-1.2.1}/src/shinestacker/gui/gui_images.py +10 -10
  30. {shinestacker-1.1.0 → shinestacker-1.2.1}/src/shinestacker/gui/gui_run.py +1 -1
  31. {shinestacker-1.1.0 → shinestacker-1.2.1}/src/shinestacker/gui/main_window.py +6 -5
  32. {shinestacker-1.1.0 → shinestacker-1.2.1}/src/shinestacker/gui/menu_manager.py +16 -2
  33. {shinestacker-1.1.0 → shinestacker-1.2.1}/src/shinestacker/gui/new_project.py +26 -22
  34. {shinestacker-1.1.0 → shinestacker-1.2.1}/src/shinestacker/gui/project_controller.py +43 -27
  35. {shinestacker-1.1.0 → shinestacker-1.2.1}/src/shinestacker/gui/project_converter.py +2 -8
  36. {shinestacker-1.1.0 → shinestacker-1.2.1}/src/shinestacker/gui/project_editor.py +50 -27
  37. {shinestacker-1.1.0 → shinestacker-1.2.1}/src/shinestacker/gui/tab_widget.py +3 -3
  38. {shinestacker-1.1.0 → shinestacker-1.2.1}/src/shinestacker/retouch/exif_data.py +5 -5
  39. {shinestacker-1.1.0 → shinestacker-1.2.1}/src/shinestacker/retouch/shortcuts_help.py +4 -4
  40. {shinestacker-1.1.0 → shinestacker-1.2.1}/src/shinestacker/retouch/vignetting_filter.py +12 -8
  41. {shinestacker-1.1.0 → shinestacker-1.2.1/src/shinestacker.egg-info}/PKG-INFO +1 -1
  42. {shinestacker-1.1.0 → shinestacker-1.2.1}/src/shinestacker.egg-info/SOURCES.txt +1 -0
  43. shinestacker-1.1.0/src/shinestacker/_version.py +0 -1
  44. shinestacker-1.1.0/src/shinestacker/algorithms/balance.py +0 -416
  45. shinestacker-1.1.0/src/shinestacker/algorithms/pyramid_tiles.py +0 -109
  46. shinestacker-1.1.0/src/shinestacker/algorithms/stack_framework.py +0 -299
  47. shinestacker-1.1.0/src/shinestacker/algorithms/utils.py +0 -131
  48. shinestacker-1.1.0/src/shinestacker/gui/action_config_dialog.py +0 -589
  49. shinestacker-1.1.0/src/shinestacker/gui/base_form_dialog.py +0 -18
  50. {shinestacker-1.1.0 → shinestacker-1.2.1}/.coveragerc +0 -0
  51. {shinestacker-1.1.0 → shinestacker-1.2.1}/.flake8 +0 -0
  52. {shinestacker-1.1.0 → shinestacker-1.2.1}/.github/workflows/ci-multiplatform.yml +0 -0
  53. {shinestacker-1.1.0 → shinestacker-1.2.1}/.github/workflows/pylint.yml +0 -0
  54. {shinestacker-1.1.0 → shinestacker-1.2.1}/.github/workflows/pypi-publish.yml +0 -0
  55. {shinestacker-1.1.0 → shinestacker-1.2.1}/.github/workflows/release.yml +0 -0
  56. {shinestacker-1.1.0 → shinestacker-1.2.1}/.gitignore +0 -0
  57. {shinestacker-1.1.0 → shinestacker-1.2.1}/.pylintrc +0 -0
  58. {shinestacker-1.1.0 → shinestacker-1.2.1}/.readthedocs.yaml +0 -0
  59. {shinestacker-1.1.0 → shinestacker-1.2.1}/LICENSE +0 -0
  60. {shinestacker-1.1.0 → shinestacker-1.2.1}/MANIFEST.in +0 -0
  61. {shinestacker-1.1.0 → shinestacker-1.2.1}/README.md +0 -0
  62. {shinestacker-1.1.0 → shinestacker-1.2.1}/THIRD_PARTY_LICENSES.txt +0 -0
  63. {shinestacker-1.1.0 → shinestacker-1.2.1}/docs/api.md +0 -0
  64. {shinestacker-1.1.0 → shinestacker-1.2.1}/docs/conf.py +0 -0
  65. {shinestacker-1.1.0 → shinestacker-1.2.1}/docs/gui.md +0 -0
  66. {shinestacker-1.1.0 → shinestacker-1.2.1}/docs/index.md +0 -0
  67. {shinestacker-1.1.0 → shinestacker-1.2.1}/docs/multilayer.md +0 -0
  68. {shinestacker-1.1.0 → shinestacker-1.2.1}/docs/noise.md +0 -0
  69. {shinestacker-1.1.0 → shinestacker-1.2.1}/docs/requirements.txt +0 -0
  70. {shinestacker-1.1.0 → shinestacker-1.2.1}/img/coffee.gif +0 -0
  71. {shinestacker-1.1.0 → shinestacker-1.2.1}/img/coffee_stack.jpg +0 -0
  72. {shinestacker-1.1.0 → shinestacker-1.2.1}/img/extreme-vignetting.jpg +0 -0
  73. {shinestacker-1.1.0 → shinestacker-1.2.1}/img/flies.gif +0 -0
  74. {shinestacker-1.1.0 → shinestacker-1.2.1}/img/flies_stack.jpg +0 -0
  75. {shinestacker-1.1.0 → shinestacker-1.2.1}/img/flow-diagram.png +0 -0
  76. {shinestacker-1.1.0 → shinestacker-1.2.1}/img/gui-finder.png +0 -0
  77. {shinestacker-1.1.0 → shinestacker-1.2.1}/img/gui-project-new.png +0 -0
  78. {shinestacker-1.1.0 → shinestacker-1.2.1}/img/gui-project-run.png +0 -0
  79. {shinestacker-1.1.0 → shinestacker-1.2.1}/img/gui-retouch.png +0 -0
  80. {shinestacker-1.1.0 → shinestacker-1.2.1}/pyproject.toml +0 -0
  81. {shinestacker-1.1.0 → shinestacker-1.2.1}/requirements.txt +0 -0
  82. {shinestacker-1.1.0 → shinestacker-1.2.1}/scripts/build_release.py +0 -0
  83. {shinestacker-1.1.0 → shinestacker-1.2.1}/scripts/git-rev-list.sh +0 -0
  84. {shinestacker-1.1.0 → shinestacker-1.2.1}/scripts/validate-tomli.py +0 -0
  85. {shinestacker-1.1.0 → shinestacker-1.2.1}/setup.cfg +0 -0
  86. {shinestacker-1.1.0 → shinestacker-1.2.1}/src/shinestacker/__init__.py +0 -0
  87. {shinestacker-1.1.0 → shinestacker-1.2.1}/src/shinestacker/algorithms/denoise.py +0 -0
  88. {shinestacker-1.1.0 → shinestacker-1.2.1}/src/shinestacker/algorithms/exif.py +0 -0
  89. {shinestacker-1.1.0 → shinestacker-1.2.1}/src/shinestacker/algorithms/sharpen.py +0 -0
  90. {shinestacker-1.1.0 → shinestacker-1.2.1}/src/shinestacker/algorithms/white_balance.py +0 -0
  91. {shinestacker-1.1.0 → shinestacker-1.2.1}/src/shinestacker/app/__init__.py +0 -0
  92. {shinestacker-1.1.0 → shinestacker-1.2.1}/src/shinestacker/app/about_dialog.py +0 -0
  93. {shinestacker-1.1.0 → shinestacker-1.2.1}/src/shinestacker/app/gui_utils.py +0 -0
  94. {shinestacker-1.1.0 → shinestacker-1.2.1}/src/shinestacker/app/help_menu.py +0 -0
  95. {shinestacker-1.1.0 → shinestacker-1.2.1}/src/shinestacker/app/main.py +0 -0
  96. {shinestacker-1.1.0 → shinestacker-1.2.1}/src/shinestacker/app/open_frames.py +0 -0
  97. {shinestacker-1.1.0 → shinestacker-1.2.1}/src/shinestacker/app/project.py +0 -0
  98. {shinestacker-1.1.0 → shinestacker-1.2.1}/src/shinestacker/app/retouch.py +0 -0
  99. {shinestacker-1.1.0 → shinestacker-1.2.1}/src/shinestacker/config/__init__.py +0 -0
  100. {shinestacker-1.1.0 → shinestacker-1.2.1}/src/shinestacker/config/config.py +0 -0
  101. {shinestacker-1.1.0 → shinestacker-1.2.1}/src/shinestacker/config/gui_constants.py +0 -0
  102. {shinestacker-1.1.0 → shinestacker-1.2.1}/src/shinestacker/core/__init__.py +0 -0
  103. {shinestacker-1.1.0 → shinestacker-1.2.1}/src/shinestacker/core/colors.py +0 -0
  104. {shinestacker-1.1.0 → shinestacker-1.2.1}/src/shinestacker/core/core_utils.py +0 -0
  105. {shinestacker-1.1.0 → shinestacker-1.2.1}/src/shinestacker/core/exceptions.py +0 -0
  106. {shinestacker-1.1.0 → shinestacker-1.2.1}/src/shinestacker/core/logging.py +0 -0
  107. {shinestacker-1.1.0 → shinestacker-1.2.1}/src/shinestacker/gui/__init__.py +0 -0
  108. {shinestacker-1.1.0 → shinestacker-1.2.1}/src/shinestacker/gui/colors.py +0 -0
  109. {shinestacker-1.1.0 → shinestacker-1.2.1}/src/shinestacker/gui/flow_layout.py +0 -0
  110. {shinestacker-1.1.0 → shinestacker-1.2.1}/src/shinestacker/gui/gui_logging.py +0 -0
  111. {shinestacker-1.1.0 → shinestacker-1.2.1}/src/shinestacker/gui/ico/focus_stack_bkg.png +0 -0
  112. {shinestacker-1.1.0 → shinestacker-1.2.1}/src/shinestacker/gui/ico/shinestacker.icns +0 -0
  113. {shinestacker-1.1.0 → shinestacker-1.2.1}/src/shinestacker/gui/ico/shinestacker.ico +0 -0
  114. {shinestacker-1.1.0 → shinestacker-1.2.1}/src/shinestacker/gui/ico/shinestacker.png +0 -0
  115. {shinestacker-1.1.0 → shinestacker-1.2.1}/src/shinestacker/gui/ico/shinestacker.svg +0 -0
  116. {shinestacker-1.1.0 → shinestacker-1.2.1}/src/shinestacker/gui/img/close-round-line-icon.png +0 -0
  117. {shinestacker-1.1.0 → shinestacker-1.2.1}/src/shinestacker/gui/img/forward-button-icon.png +0 -0
  118. {shinestacker-1.1.0 → shinestacker-1.2.1}/src/shinestacker/gui/img/play-button-round-icon.png +0 -0
  119. {shinestacker-1.1.0 → shinestacker-1.2.1}/src/shinestacker/gui/img/plus-round-line-icon.png +0 -0
  120. {shinestacker-1.1.0 → shinestacker-1.2.1}/src/shinestacker/gui/project_model.py +0 -0
  121. {shinestacker-1.1.0 → shinestacker-1.2.1}/src/shinestacker/gui/select_path_widget.py +0 -0
  122. {shinestacker-1.1.0 → shinestacker-1.2.1}/src/shinestacker/gui/time_progress_bar.py +0 -0
  123. {shinestacker-1.1.0 → shinestacker-1.2.1}/src/shinestacker/retouch/__init__.py +0 -0
  124. {shinestacker-1.1.0 → shinestacker-1.2.1}/src/shinestacker/retouch/base_filter.py +0 -0
  125. {shinestacker-1.1.0 → shinestacker-1.2.1}/src/shinestacker/retouch/brush.py +0 -0
  126. {shinestacker-1.1.0 → shinestacker-1.2.1}/src/shinestacker/retouch/brush_gradient.py +0 -0
  127. {shinestacker-1.1.0 → shinestacker-1.2.1}/src/shinestacker/retouch/brush_preview.py +0 -0
  128. {shinestacker-1.1.0 → shinestacker-1.2.1}/src/shinestacker/retouch/brush_tool.py +0 -0
  129. {shinestacker-1.1.0 → shinestacker-1.2.1}/src/shinestacker/retouch/denoise_filter.py +0 -0
  130. {shinestacker-1.1.0 → shinestacker-1.2.1}/src/shinestacker/retouch/display_manager.py +0 -0
  131. {shinestacker-1.1.0 → shinestacker-1.2.1}/src/shinestacker/retouch/file_loader.py +0 -0
  132. {shinestacker-1.1.0 → shinestacker-1.2.1}/src/shinestacker/retouch/filter_manager.py +0 -0
  133. {shinestacker-1.1.0 → shinestacker-1.2.1}/src/shinestacker/retouch/icon_container.py +0 -0
  134. {shinestacker-1.1.0 → shinestacker-1.2.1}/src/shinestacker/retouch/image_editor_ui.py +0 -0
  135. {shinestacker-1.1.0 → shinestacker-1.2.1}/src/shinestacker/retouch/image_viewer.py +0 -0
  136. {shinestacker-1.1.0 → shinestacker-1.2.1}/src/shinestacker/retouch/io_gui_handler.py +0 -0
  137. {shinestacker-1.1.0 → shinestacker-1.2.1}/src/shinestacker/retouch/io_manager.py +0 -0
  138. {shinestacker-1.1.0 → shinestacker-1.2.1}/src/shinestacker/retouch/layer_collection.py +0 -0
  139. {shinestacker-1.1.0 → shinestacker-1.2.1}/src/shinestacker/retouch/undo_manager.py +0 -0
  140. {shinestacker-1.1.0 → shinestacker-1.2.1}/src/shinestacker/retouch/unsharp_mask_filter.py +0 -0
  141. {shinestacker-1.1.0 → shinestacker-1.2.1}/src/shinestacker/retouch/white_balance_filter.py +0 -0
  142. {shinestacker-1.1.0 → shinestacker-1.2.1}/src/shinestacker.egg-info/dependency_links.txt +0 -0
  143. {shinestacker-1.1.0 → shinestacker-1.2.1}/src/shinestacker.egg-info/entry_points.txt +0 -0
  144. {shinestacker-1.1.0 → shinestacker-1.2.1}/src/shinestacker.egg-info/requires.txt +0 -0
  145. {shinestacker-1.1.0 → shinestacker-1.2.1}/src/shinestacker.egg-info/top_level.txt +0 -0
@@ -2,12 +2,40 @@
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
6
+ **Bug fixes and minor improvements**
7
+ * alignment is more tolerant in case of failures: frames are skipped and the running job is not stopped
8
+ * fixed the -x (--expert) option
9
+ * more safety checks prevent crashes for abnormal conditions
10
+ * reference frame index improved with a more consistent treatment, a better numbering scheme and GUI widget
11
+ * improved project undo action description text
12
+ * some bug fixes and code cleanup
13
+
14
+ ---
15
+
16
+ ## [v1.2.0] - 2025-08-31
17
+ **Parallel processing and more improvements**
18
+
19
+ * Implemented parallel processing for pyramid stacking algorithm
20
+ * 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
+ * Implemented automatic subsample option for alignment, balancing and vignetting, now default
22
+ * HLS and HSV corrections now supported for 16 bit images
23
+ * Added luminosity correction in the LAB color space
24
+ * Alignment module skips frames if transformation parameters are out of a reasonable ranges
25
+ * Multilayer modules sends a warning if the estimated output file size is > 1GB
26
+ * "Run all jobs" action is enabled only if more than one job are present
27
+ * Updated default module names in project genereated by "new project" dialog
28
+ * Code refactoring
29
+ * Some GUI fixes
30
+
31
+ ---
32
+
5
33
  ## [v1.1.0] - 2025-08-28
6
- **New Pyramids algorith, some improvements and more fixes**
34
+ **New Pyramids algorithm, some improvements and more fixes**
7
35
 
8
36
  ### Changes
9
37
 
10
- * added Pyramids Tiles, that requires less RAM by fusing the image in tiles
38
+ * added Pyramids Tiles, that requires less RAM by fusing images in tiles
11
39
  * the alignment module now tolerates images of different shapes
12
40
  * noisy pixel mask verifies that the mask has the same shape as input images
13
41
  * minor changes to default alignment parameters
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: shinestacker
3
- Version: 1.1.0
3
+ Version: 1.2.1
4
4
  Summary: ShineStacker
5
5
  Author-email: Luca Lista <luka.lista@gmail.com>
6
6
  License-Expression: LGPL-3.0
@@ -65,7 +65,7 @@ alignment_config = {
65
65
  'border_mode': constants.BORDER_REPLICATE_BLUR,
66
66
  'border_value': (0, 0, 0, 0),
67
67
  'border_blur': 50,
68
- 'subsample': 1,
68
+ 'subsample': 0,
69
69
  'fast_subsampling': False,
70
70
  'min_good_matches': 100
71
71
  }
@@ -80,7 +80,7 @@ alignment_config = {
80
80
  * ```refine_iters``` (optional, default: 100): refinement iterations. Used only if ```transform=ALIGN_RIGID```.
81
81
  * ```align_confidence``` (optional, default: 99.9): alignment algorithm confidence (%). Used only if ```transform=ALIGN_RIGID```.
82
82
  * ```max_iters``` (optional, default: 2000): maximum number of iterations. Used only if ```transform=ALIGN_HOMOGRAPHY```.
83
- * ```subsample``` (optional, default: 1): subsample image for faster alignment. Faster, but alignment could be less accurate. It can save time, in particular for large images.
83
+ * ```subsample``` (optional, default: 0=automatic): subsample image for faster alignment. Faster, but alignment could be less accurate if only a small portion of the image is focused. It can save time, in particular for large images.
84
84
  * ```fast_subsampling``` (optional, default: ```False```): perform fast image subsampling without interpolation. Used if ```subsample``` is set to ```True```.
85
85
  * ```min_good_matches``` (optional, default: 100): if ```subsample```>1 and the number of good matches is below ```min_good_matches```, the alignment is retried without subsampling. This improbes robustness in case a too large subsampling factor is specified.
86
86
  * ```border_mode``` (optional, default: ```BORDER_REPLICATE_BLUR```): border mode. See [Adding borders to your images](https://docs.opencv.org/3.4/dc/da3/tutorial_copyMakeBorder.html) for more details. Possible values are:
@@ -5,7 +5,12 @@ job.add_action(Actions("balance", [BalanceFrames(*options)])
5
5
  ```
6
6
 
7
7
  Arguments for the constructor of ```BalanceFrames``` are:
8
- *```channel``` (optional, default: BALANCE_LUMI): channels to be balanced. Possible values are: ```BALANCE_LUMI``` (default): balance equally for R, G and B channels, should be reasonably fine for most of the cases; ```BALANCE_RGB```: balance luminosity separately for R, G and B channels, it may be needed if some but not all of the images have a undesired color dominance; ```BALANCE_HSV```: balance saturation a luminosity value in the HSV (Hue, Saturation, brightness Value) representation, it may be needed in cases of extreme luminosity variation that affects saturation; ```BALANCE_HLS```: balance saturation a luminosity value in the HLS (Hue, Lightness, Saturation) representation, it may be needed in cases of extreme luminosity variation that affects saturation. Note that ```BALANCE_HSV``` and ```BALANCE_HLS``` are only supported for 8-bit images.
8
+ * ```channel``` (optional, default: BALANCE_LUMI): channels to be balanced. Possible values are:
9
+ - ```BALANCE_LUMI``` (default): balance equally for R, G and B channels, should be reasonably fine for most of the cases;
10
+ - ```BALANCE_RGB```: balance luminosity separately for R, G and B channels, it may be needed if some but not all of the images have a undesired color dominance;
11
+ - ```BALANCE_HSV```: balance saturation and luminosity value in the HSV (Hue, Saturation, brightness Value) color space, it may be needed in cases of extreme luminosity variation that affects saturation;
12
+ - ```BALANCE_HLS```: balance saturation and luminosity value in the HLS (Hue, Lightness, Saturation) color space, it may be needed in cases of extreme luminosity variation that affects saturation;
13
+ - ```BALANCE_LAB```: balance luminosity in the LAB color space.
9
14
  * ```mask_size``` (optional): if specified, luminosity and color balance is only applied to pixels within a circle of radius equal to the minimum between the image width and height times ```mask_size```, i.e: 0.8 means 80% of a portrait image width or landscape image height. It may beuseful for images with vignetting, in order to avoid including in the balance processing the outer darker pixels.
10
15
  * ```intensity_interval``` (optional): if specifies, only pixels with intensity within the specified range are used. It may be useful to remove very dark areas or very light areas. Not used if ```MATCH_HIST``` is specified as value for ```corr_map```. The argument has to be a dictionary where one or both values corresponding to the keys ```min``` and ```max``` can be specified. The default values are:
11
16
  ```python
@@ -15,7 +20,7 @@ intensity_interval = {
15
20
  }
16
21
  ```
17
22
  Note that for 8-bit images the maximum intensity is 255, while for 16-bit images the maximum intensity is 65536.
18
- * ```subsample``` (optional, default: 8): extracts intensity histogram using every n-th pixel in each dimension in order to reduce processing time. By default, it takes one every 8 pixels in horizontal and vertical directions, i.e.: one every 100 pixels in total. This option is not ised if ```corr_map``` is equal to ```BALANCE_MATCH_HIST```.
23
+ * ```subsample``` (optional, default: 0=automatic): extracts intensity histogram using every n-th pixel in each dimension in order to reduce processing time. This option is not ised if ```corr_map``` is equal to ```BALANCE_MATCH_HIST```.
19
24
  * ```fast_subsampling``` (optional, default: ```False```): perform fast image subsampling without interpolation. Used if ```subsample``` is set to ```True```.
20
25
  * ```corr_map``` (optional, default: ```BALANCE_LINEAR```, possible values: ```BALANCE_LINEAR```, ```BALANCE_GAMMA``` and ```MATCH_HIST```): specifies the type of intensity correction.
21
26
  * ```BALANCE_LINEAR```: a linear correction is applied in order to balance the average intensity of the corrected images to the reference image in the specified channels.
@@ -34,7 +34,7 @@ Arguments for the constructor of ```FocusStackBunch``` are:
34
34
 
35
35
  ## Stack algorithms
36
36
 
37
- ```PyramidStack```, Laplacian pyramid focus stacking algorithm, optimized implementation
37
+ ```PyramidStack```, Laplacian pyramid focus stacking algorithm
38
38
 
39
39
  Arguments for the constructor are:
40
40
  * ```pyramid_min_size``` (optional, default: 32)
@@ -42,7 +42,20 @@ 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
45
+ ```PyramidTilesStack```, pyramid algorithn with I/O buffered tile pyramid merging to optimize memory usage for large files
46
+
47
+ Arguments for the constructor are, in addition to the ones for ```PyramidStack```:
48
+ * ```tile_size``` (optional, default: 512): size of a time
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
+ * ```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
+
52
+
53
+ ```PyramidAutoStack```, pyramid algorithn with capability to automatically switch from all-in-memory to I/O buffered tiled.
54
+
55
+ Arguments for the constructor are, in addition to the ones for ```PyramidTilesStack```:
56
+ * ```mode``` (optional, default: ```auto```): can be ```auto```, ```memory``` or ```tiled```.
57
+ * ```memory_limit``` (optional, default: 8×1024<sup>3</sup>sup>): memory limit to determine optimal running parameters
58
+
46
59
 
47
60
  Arguments for the constructor are the same ad for ```PyramidStack``` plus:
48
61
  * ```tile_size``` (optional, default: 512): size of the tile used for partial image merging
@@ -41,6 +41,6 @@ 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.
@@ -103,6 +103,5 @@ pip install ipywidgets
103
103
 
104
104
  | Issue | Workaround |
105
105
  |----------|----------------|
106
- | Balance modes ```HSV```/```HLS``` don't support 16-bit images | convert to 8-bit or use ```RGB``` or luminosity |
107
- | PNG support untested | Convert to TIFF/JPEG first |
108
- | GUI tests limited | Report bugs as GitHub issuse |
106
+ | PNG and RAW unsupported | Convert to TIFF/JPEG first |
107
+ | GUI tests limited | Report any bugs as GitHub issuse |
@@ -14,6 +14,7 @@ Arguments for the constructor of ```Vignetting``` are:
14
14
  * ```r_steps``` (optional, default: 100): number of radial steps to determine mean pixel luminosity.
15
15
  * ```black_threshold``` (optional, default: 1): apply correction only on pixels with luminosity greater than.
16
16
  * ```max_correction``` (optional, default: 1): if less than one, the correction is rescaled in order to be at most the specified valye.
17
+ * ```subsample``` (optional, default: 0=automatic): extracts intensity histogram using every n-th pixel in each dimension in order to reduce processing time.
17
18
  * ```plot_correction``` (optional, default: ```False```): if ```True```, plot vignetting correction curve for each frame.
18
19
  * ```plot_summary``` (optional, default: ```False```): if ```True```, plot a summary histogram with the vignetting correction levels.
19
20
  * ```enabled``` (optional, default: ```True```): allows to switch on and off this module.
@@ -0,0 +1 @@
1
+ __version__ = '1.2.1'
@@ -8,6 +8,8 @@ from .balance import BalanceFrames
8
8
  from .stack import FocusStackBunch, FocusStack
9
9
  from .depth_map import DepthMapStack
10
10
  from .pyramid import PyramidStack
11
+ from .pyramid_tiles import PyramidTilesStack
12
+ from .pyramid_auto import PyramidAutoStack
11
13
  from .multilayer import MultiLayer
12
14
  from .noise_detection import NoiseDetection, MaskNoise
13
15
  from .vignetting import Vignetting
@@ -16,5 +18,6 @@ logger.addHandler(logging.NullHandler())
16
18
 
17
19
  __all__ = [
18
20
  'StackJob', 'CombinedActions', 'AlignFrames', 'BalanceFrames', 'FocusStackBunch', 'FocusStack',
19
- 'DepthMapStack', 'PyramidStack', 'MultiLayer', 'NoiseDetection', 'MaskNoise', 'Vignetting'
21
+ 'DepthMapStack', 'PyramidStack', 'PyramidTilesStack', 'PyramidAutoStack', 'MultiLayer',
22
+ 'NoiseDetection', 'MaskNoise', 'Vignetting'
20
23
  ]
@@ -1,10 +1,11 @@
1
- # pylint: disable=C0114, C0115, C0116, E1101, R0914, R0913, R0917, R0912, R0915, R0902
1
+ # pylint: disable=C0114, C0115, C0116, E1101, R0914, R0913, R0917, R0912, R0915, R0902, E1121, W0102
2
2
  import logging
3
+ import math
3
4
  import numpy as np
4
5
  import matplotlib.pyplot as plt
5
6
  import cv2
6
7
  from .. config.constants import constants
7
- from .. core.exceptions import AlignmentError, InvalidOptionError
8
+ from .. core.exceptions import InvalidOptionError
8
9
  from .. core.colors import color_str
9
10
  from .utils import img_8bit, img_bw_8bit, save_plot, img_subsample
10
11
  from .stack_framework import SubAction
@@ -29,6 +30,7 @@ _DEFAULT_ALIGNMENT_CONFIG = {
29
30
  'refine_iters': constants.DEFAULT_REFINE_ITERS,
30
31
  'align_confidence': constants.DEFAULT_ALIGN_CONFIDENCE,
31
32
  'max_iters': constants.DEFAULT_ALIGN_MAX_ITERS,
33
+ 'abort_abnormal': constants.DEFAULT_ALIGN_ABORT_ABNORMAL,
32
34
  'border_mode': constants.DEFAULT_BORDER_MODE,
33
35
  'border_value': constants.DEFAULT_BORDER_VALUE,
34
36
  'border_blur': constants.DEFAULT_BORDER_BLUR,
@@ -44,8 +46,91 @@ _cv2_border_mode_map = {
44
46
  constants.BORDER_REPLICATE_BLUR: cv2.BORDER_REPLICATE
45
47
  }
46
48
 
49
+ _AFFINE_THRESHOLDS = {
50
+ 'max_rotation': 10.0, # degrees
51
+ 'min_scale': 0.9,
52
+ 'max_scale': 1.1,
53
+ 'max_shear': 5.0, # degrees
54
+ 'max_translation_ratio': 0.1, # 10% of image dimension
55
+ }
56
+
57
+ _HOMOGRAPHY_THRESHOLDS = {
58
+ 'max_skew': 10.0, # degrees
59
+ 'max_scale_change': 1.5, # max area change ratio
60
+ 'max_aspect_ratio': 2.0, # max aspect ratio change
61
+ }
62
+
63
+
64
+ def decompose_affine_matrix(m):
65
+ a, b, tx = m[0, 0], m[0, 1], m[0, 2]
66
+ c, d, ty = m[1, 0], m[1, 1], m[1, 2]
67
+ scale_x = math.sqrt(a**2 + b**2)
68
+ scale_y = math.sqrt(c**2 + d**2)
69
+ rotation = math.degrees(math.atan2(b, a))
70
+ shear = math.degrees(math.atan2(-c, d)) - rotation
71
+ shear = (shear + 180) % 360 - 180
72
+ return (scale_x, scale_y), rotation, shear, (tx, ty)
73
+
74
+
75
+ def check_affine_matrix(m, img_shape, affine_thresholds=_AFFINE_THRESHOLDS):
76
+ if affine_thresholds is None:
77
+ return True, "No thresholds provided"
78
+ (scale_x, scale_y), rotation, shear, (tx, ty) = decompose_affine_matrix(m)
79
+ h, w = img_shape[:2]
80
+ reasons = []
81
+ if abs(rotation) > affine_thresholds['max_rotation']:
82
+ reasons.append(f"rotation too large ({rotation:.1f}°)")
83
+ if scale_x < affine_thresholds['min_scale'] or scale_x > affine_thresholds['max_scale']:
84
+ reasons.append(f"x-scale out of range ({scale_x:.2f})")
85
+ if scale_y < affine_thresholds['min_scale'] or scale_y > affine_thresholds['max_scale']:
86
+ reasons.append(f"y-scale out of range ({scale_y:.2f})")
87
+ if abs(shear) > affine_thresholds['max_shear']:
88
+ reasons.append(f"shear too large ({shear:.1f}°)")
89
+ max_tx = w * affine_thresholds['max_translation_ratio']
90
+ max_ty = h * affine_thresholds['max_translation_ratio']
91
+ if abs(tx) > max_tx:
92
+ reasons.append(f"x-translation too large (|{tx:.1f}| > {max_tx:.1f})")
93
+ if abs(ty) > max_ty:
94
+ reasons.append(f"y-translation too large (|{ty:.1f}| > {max_ty:.1f})")
95
+ if reasons:
96
+ return False, "; ".join(reasons)
97
+ return True, "Transformation within acceptable limits"
47
98
 
48
- def get_good_matches(des_0, des_1, matching_config=None):
99
+
100
+ def check_homography_distortion(m, img_shape, homography_thresholds=_HOMOGRAPHY_THRESHOLDS):
101
+ if homography_thresholds is None:
102
+ return True, "No thresholds provided"
103
+ h, w = img_shape[:2]
104
+ corners = np.array([[0, 0], [w, 0], [w, h], [0, h]], dtype=np.float32)
105
+ transformed = cv2.perspectiveTransform(corners.reshape(1, -1, 2), m).reshape(-1, 2)
106
+ reasons = []
107
+ area_orig = w * h
108
+ area_new = cv2.contourArea(transformed)
109
+ area_ratio = area_new / area_orig
110
+ if area_ratio > homography_thresholds['max_scale_change'] or \
111
+ area_ratio < 1.0 / homography_thresholds['max_scale_change']:
112
+ reasons.append(f"area change too large ({area_ratio:.2f})")
113
+ rect = cv2.minAreaRect(transformed.astype(np.float32))
114
+ (w_rect, h_rect) = rect[1]
115
+ aspect_ratio = max(w_rect, h_rect) / min(w_rect, h_rect)
116
+ if aspect_ratio > homography_thresholds['max_aspect_ratio']:
117
+ reasons.append(f"aspect ratio change too large ({aspect_ratio:.2f})")
118
+ angles = []
119
+ for i in range(4):
120
+ vec1 = transformed[(i + 1) % 4] - transformed[i]
121
+ vec2 = transformed[(i - 1) % 4] - transformed[i]
122
+ angle = np.degrees(np.arccos(np.dot(vec1, vec2) /
123
+ (np.linalg.norm(vec1) * np.linalg.norm(vec2))))
124
+ angles.append(angle)
125
+ max_angle_dev = max(abs(angle - 90) for angle in angles)
126
+ if max_angle_dev > homography_thresholds['max_skew']:
127
+ reasons.append(f"angle distortion too large ({max_angle_dev:.1f}°)")
128
+ if reasons:
129
+ return False, "; ".join(reasons)
130
+ return True, "Transformation within acceptable limits"
131
+
132
+
133
+ def get_good_matches(des_0, des_ref, matching_config=None):
49
134
  matching_config = {**_DEFAULT_MATCHING_CONFIG, **(matching_config or {})}
50
135
  match_method = matching_config['match_method']
51
136
  good_matches = []
@@ -54,12 +139,12 @@ def get_good_matches(des_0, des_1, matching_config=None):
54
139
  {'algorithm': matching_config['flann_idx_kdtree'],
55
140
  'trees': matching_config['flann_trees']},
56
141
  {'checks': matching_config['flann_checks']})
57
- matches = flann.knnMatch(des_0, des_1, k=2)
142
+ matches = flann.knnMatch(des_0, des_ref, k=2)
58
143
  good_matches = [m for m, n in matches
59
144
  if m.distance < matching_config['threshold'] * n.distance]
60
145
  elif match_method == constants.MATCHING_NORM_HAMMING:
61
146
  bf = cv2.BFMatcher(cv2.NORM_HAMMING, crossCheck=True)
62
- good_matches = sorted(bf.match(des_0, des_1), key=lambda x: x.distance)
147
+ good_matches = sorted(bf.match(des_0, des_ref), key=lambda x: x.distance)
63
148
  else:
64
149
  raise InvalidOptionError(
65
150
  'match_method', match_method,
@@ -87,14 +172,14 @@ def validate_align_config(detector, descriptor, match_method):
87
172
  " require matching method Hamming distance")
88
173
 
89
174
 
90
- def detect_and_compute(img_0, img_1, feature_config=None, matching_config=None):
175
+ def detect_and_compute(img_0, img_ref, feature_config=None, matching_config=None):
91
176
  feature_config = {**_DEFAULT_FEATURE_CONFIG, **(feature_config or {})}
92
177
  matching_config = {**_DEFAULT_MATCHING_CONFIG, **(matching_config or {})}
93
178
  feature_config_detector = feature_config['detector']
94
179
  feature_config_descriptor = feature_config['descriptor']
95
180
  match_method = matching_config['match_method']
96
181
  validate_align_config(feature_config_detector, feature_config_descriptor, match_method)
97
- img_bw_0, img_bw_1 = img_bw_8bit(img_0), img_bw_8bit(img_1)
182
+ img_bw_0, img_bw_ref = img_bw_8bit(img_0), img_bw_8bit(img_ref)
98
183
  detector_map = {
99
184
  constants.DETECTOR_SIFT: cv2.SIFT_create,
100
185
  constants.DETECTOR_ORB: cv2.ORB_create,
@@ -114,12 +199,12 @@ def detect_and_compute(img_0, img_1, feature_config=None, matching_config=None):
114
199
  constants.DETECTOR_AKAZE,
115
200
  constants.DETECTOR_BRISK):
116
201
  kp_0, des_0 = detector.detectAndCompute(img_bw_0, None)
117
- kp_1, des_1 = detector.detectAndCompute(img_bw_1, None)
202
+ kp_ref, des_ref = detector.detectAndCompute(img_bw_ref, None)
118
203
  else:
119
204
  descriptor = descriptor_map[feature_config_descriptor]()
120
205
  kp_0, des_0 = descriptor.compute(img_bw_0, detector.detect(img_bw_0, None))
121
- kp_1, des_1 = descriptor.compute(img_bw_1, detector.detect(img_bw_1, None))
122
- return kp_0, kp_1, get_good_matches(des_0, des_1, matching_config)
206
+ kp_ref, des_ref = descriptor.compute(img_bw_ref, detector.detect(img_bw_ref, None))
207
+ return kp_0, kp_ref, get_good_matches(des_0, des_ref, matching_config)
123
208
 
124
209
 
125
210
  def find_transform(src_pts, dst_pts, transform=constants.DEFAULT_TRANSFORM,
@@ -151,8 +236,10 @@ def find_transform(src_pts, dst_pts, transform=constants.DEFAULT_TRANSFORM,
151
236
  return result
152
237
 
153
238
 
154
- def align_images(img_1, img_0, feature_config=None, matching_config=None, alignment_config=None,
155
- plot_path=None, callbacks=None):
239
+ def align_images(img_ref, img_0, feature_config=None, matching_config=None, alignment_config=None,
240
+ plot_path=None, callbacks=None,
241
+ affine_thresholds=_AFFINE_THRESHOLDS,
242
+ homography_thresholds=_HOMOGRAPHY_THRESHOLDS):
156
243
  feature_config = {**_DEFAULT_FEATURE_CONFIG, **(feature_config or {})}
157
244
  matching_config = {**_DEFAULT_MATCHING_CONFIG, **(matching_config or {})}
158
245
  alignment_config = {**_DEFAULT_ALIGNMENT_CONFIG, **(alignment_config or {})}
@@ -163,19 +250,23 @@ def align_images(img_1, img_0, feature_config=None, matching_config=None, alignm
163
250
  min_matches = 4 if alignment_config['transform'] == constants.ALIGN_HOMOGRAPHY else 3
164
251
  if callbacks and 'message' in callbacks:
165
252
  callbacks['message']()
166
- h_ref, w_ref = img_1.shape[:2]
253
+ h_ref, w_ref = img_ref.shape[:2]
167
254
  h0, w0 = img_0.shape[:2]
168
255
  subsample = alignment_config['subsample']
256
+ if subsample == 0:
257
+ img_res = (float(h0) / constants.ONE_KILO) * (float(w0) / constants.ONE_KILO)
258
+ target_res = constants.DEFAULT_ALIGN_RES_TARGET_MPX
259
+ subsample = int(1 + math.floor(img_res / target_res))
169
260
  fast_subsampling = alignment_config['fast_subsampling']
170
261
  min_good_matches = alignment_config['min_good_matches']
171
262
  while True:
172
263
  if subsample > 1:
173
264
  img_0_sub = img_subsample(img_0, subsample, fast_subsampling)
174
- img_1_sub = img_subsample(img_1, subsample, fast_subsampling)
265
+ img_ref_sub = img_subsample(img_ref, subsample, fast_subsampling)
175
266
  else:
176
- img_0_sub, img_1_sub = img_0, img_1
177
- kp_0, kp_1, good_matches = detect_and_compute(img_0_sub, img_1_sub,
178
- feature_config, matching_config)
267
+ 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)
179
270
  n_good_matches = len(good_matches)
180
271
  if n_good_matches > min_good_matches or subsample == 1:
181
272
  break
@@ -191,7 +282,7 @@ def align_images(img_1, img_0, feature_config=None, matching_config=None, alignm
191
282
  if n_good_matches >= min_matches:
192
283
  transform = alignment_config['transform']
193
284
  src_pts = np.float32([kp_0[m.queryIdx].pt for m in good_matches]).reshape(-1, 1, 2)
194
- dst_pts = np.float32([kp_1[m.trainIdx].pt for m in good_matches]).reshape(-1, 1, 2)
285
+ dst_pts = np.float32([kp_ref[m.trainIdx].pt for m in good_matches]).reshape(-1, 1, 2)
195
286
  m, msk = find_transform(src_pts, dst_pts, transform, alignment_config['align_method'],
196
287
  *(alignment_config[k]
197
288
  for k in ['rans_threshold', 'max_iters',
@@ -199,11 +290,11 @@ def align_images(img_1, img_0, feature_config=None, matching_config=None, alignm
199
290
  if plot_path is not None:
200
291
  matches_mask = msk.ravel().tolist()
201
292
  img_match = cv2.cvtColor(cv2.drawMatches(
202
- img_8bit(img_0_sub), kp_0, img_8bit(img_1_sub),
203
- kp_1, good_matches, None, matchColor=(0, 255, 0),
293
+ img_8bit(img_0_sub), kp_0, img_8bit(img_ref_sub),
294
+ kp_ref, good_matches, None, matchColor=(0, 255, 0),
204
295
  singlePointColor=None, matchesMask=matches_mask,
205
296
  flags=2), cv2.COLOR_BGR2RGB)
206
- plt.figure(figsize=(10, 5))
297
+ plt.figure(figsize=constants.PLT_FIG_SIZE)
207
298
  plt.imshow(img_match, 'gray')
208
299
  save_plot(plot_path)
209
300
  if callbacks and 'save_plot' in callbacks:
@@ -225,6 +316,20 @@ def align_images(img_1, img_0, feature_config=None, matching_config=None, alignm
225
316
  m[:, 2] = translation_fullres
226
317
  else:
227
318
  raise InvalidOptionError("transform", transform)
319
+
320
+ 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)
329
+ if not is_valid:
330
+ if callbacks and 'warning' in callbacks:
331
+ callbacks['warning'](f"invalid transformation: {reason}")
332
+ return n_good_matches, None, None
228
333
  if callbacks and 'align_message' in callbacks:
229
334
  callbacks['align_message']()
230
335
  img_mask = np.ones_like(img_0, dtype=np.uint8)
@@ -284,7 +389,7 @@ class AlignFrames(SubAction):
284
389
  def sub_msg(self, msg, color=constants.LOG_COLOR_LEVEL_3):
285
390
  self.process.sub_message_r(color_str(msg, color))
286
391
 
287
- def align_images(self, idx, img_1, img_0):
392
+ def align_images(self, idx, img_ref, img_0):
288
393
  idx_str = f"{idx:04d}"
289
394
  callbacks = {
290
395
  'message': lambda: self.sub_msg(': find matches'),
@@ -303,37 +408,47 @@ class AlignFrames(SubAction):
303
408
  f"{self.process.name}-matches-{idx_str}.pdf"
304
409
  else:
305
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
306
417
  n_good_matches, _m, img = align_images(
307
- img_1, img_0,
418
+ img_ref, img_0,
308
419
  feature_config=self.feature_config,
309
420
  matching_config=self.matching_config,
310
421
  alignment_config=self.alignment_config,
311
422
  plot_path=plot_path,
312
- callbacks=callbacks
423
+ callbacks=callbacks,
424
+ affine_thresholds=affine_thresholds,
425
+ homography_thresholds=homography_thresholds
313
426
  )
314
427
  self.n_matches[idx] = n_good_matches
315
428
  if n_good_matches < self.min_matches:
316
- self.process.sub_message(f": image not aligned, too few matches found: "
317
- f"{n_good_matches}", level=logging.CRITICAL)
318
- raise AlignmentError(idx, f"Image not aligned, too few matches found: "
319
- f"{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
320
433
  return img
321
434
 
322
435
  def begin(self, process):
323
436
  self.process = process
324
- self.n_matches = np.zeros(process.counts)
437
+ self.n_matches = np.zeros(process.total_action_counts)
325
438
 
326
439
  def end(self):
327
440
  if self.plot_summary:
328
- plt.figure(figsize=(10, 5))
441
+ plt.figure(figsize=constants.PLT_FIG_SIZE)
329
442
  x = np.arange(1, len(self.n_matches) + 1, dtype=int)
330
443
  no_ref = x != self.process.ref_idx + 1
331
444
  x = x[no_ref]
332
445
  y = self.n_matches[no_ref]
333
- y_max = y[1] \
334
- if self.process.ref_idx == 0 \
335
- else y[-1] if self.process.ref_idx == len(y) - 1 \
336
- else (y[self.process.ref_idx - 1] + y[self.process.ref_idx]) / 2
446
+ if self.process.ref_idx == 0:
447
+ y_max = y[1]
448
+ elif self.process.ref_idx >= len(y):
449
+ y_max = y[-1]
450
+ else:
451
+ y_max = (y[self.process.ref_idx - 1] + y[self.process.ref_idx]) / 2
337
452
 
338
453
  plt.plot([self.process.ref_idx + 1, self.process.ref_idx + 1],
339
454
  [0, y_max], color='cornflowerblue', linestyle='--', label='reference frame')