shinestacker 1.3.1__tar.gz → 1.4.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of shinestacker might be problematic. Click here for more details.

Files changed (150) hide show
  1. {shinestacker-1.3.1 → shinestacker-1.4.0}/CHANGELOG.md +24 -0
  2. {shinestacker-1.3.1/src/shinestacker.egg-info → shinestacker-1.4.0}/PKG-INFO +1 -1
  3. {shinestacker-1.3.1 → shinestacker-1.4.0}/docs/alignment.md +11 -2
  4. shinestacker-1.4.0/src/shinestacker/_version.py +1 -0
  5. {shinestacker-1.3.1 → shinestacker-1.4.0}/src/shinestacker/algorithms/align.py +198 -18
  6. {shinestacker-1.3.1 → shinestacker-1.4.0}/src/shinestacker/algorithms/align_parallel.py +17 -1
  7. {shinestacker-1.3.1 → shinestacker-1.4.0}/src/shinestacker/algorithms/balance.py +23 -13
  8. {shinestacker-1.3.1 → shinestacker-1.4.0}/src/shinestacker/algorithms/noise_detection.py +3 -1
  9. {shinestacker-1.3.1 → shinestacker-1.4.0}/src/shinestacker/algorithms/utils.py +21 -10
  10. {shinestacker-1.3.1 → shinestacker-1.4.0}/src/shinestacker/algorithms/vignetting.py +2 -0
  11. {shinestacker-1.3.1 → shinestacker-1.4.0}/src/shinestacker/config/gui_constants.py +2 -2
  12. {shinestacker-1.3.1 → shinestacker-1.4.0}/src/shinestacker/core/core_utils.py +10 -1
  13. {shinestacker-1.3.1 → shinestacker-1.4.0}/src/shinestacker/gui/action_config.py +172 -7
  14. {shinestacker-1.3.1 → shinestacker-1.4.0}/src/shinestacker/gui/action_config_dialog.py +246 -285
  15. {shinestacker-1.3.1 → shinestacker-1.4.0}/src/shinestacker/gui/gui_run.py +2 -2
  16. {shinestacker-1.3.1 → shinestacker-1.4.0}/src/shinestacker/gui/main_window.py +14 -5
  17. {shinestacker-1.3.1 → shinestacker-1.4.0}/src/shinestacker/gui/menu_manager.py +26 -2
  18. {shinestacker-1.3.1 → shinestacker-1.4.0}/src/shinestacker/gui/project_controller.py +4 -0
  19. shinestacker-1.4.0/src/shinestacker/gui/recent_file_manager.py +93 -0
  20. {shinestacker-1.3.1 → shinestacker-1.4.0}/src/shinestacker/retouch/base_filter.py +5 -5
  21. {shinestacker-1.3.1 → shinestacker-1.4.0}/src/shinestacker/retouch/brush_preview.py +3 -0
  22. {shinestacker-1.3.1 → shinestacker-1.4.0}/src/shinestacker/retouch/brush_tool.py +11 -11
  23. {shinestacker-1.3.1 → shinestacker-1.4.0}/src/shinestacker/retouch/display_manager.py +21 -37
  24. {shinestacker-1.3.1 → shinestacker-1.4.0}/src/shinestacker/retouch/image_editor_ui.py +129 -71
  25. shinestacker-1.4.0/src/shinestacker/retouch/image_view_status.py +61 -0
  26. shinestacker-1.4.0/src/shinestacker/retouch/image_viewer.py +123 -0
  27. {shinestacker-1.3.1 → shinestacker-1.4.0}/src/shinestacker/retouch/io_gui_handler.py +12 -2
  28. shinestacker-1.4.0/src/shinestacker/retouch/overlaid_view.py +212 -0
  29. {shinestacker-1.3.1 → shinestacker-1.4.0}/src/shinestacker/retouch/shortcuts_help.py +13 -3
  30. shinestacker-1.4.0/src/shinestacker/retouch/sidebyside_view.py +479 -0
  31. shinestacker-1.4.0/src/shinestacker/retouch/view_strategy.py +466 -0
  32. {shinestacker-1.3.1 → shinestacker-1.4.0/src/shinestacker.egg-info}/PKG-INFO +1 -1
  33. {shinestacker-1.3.1 → shinestacker-1.4.0}/src/shinestacker.egg-info/SOURCES.txt +5 -0
  34. shinestacker-1.3.1/src/shinestacker/_version.py +0 -1
  35. shinestacker-1.3.1/src/shinestacker/retouch/image_viewer.py +0 -465
  36. {shinestacker-1.3.1 → shinestacker-1.4.0}/.coveragerc +0 -0
  37. {shinestacker-1.3.1 → shinestacker-1.4.0}/.flake8 +0 -0
  38. {shinestacker-1.3.1 → shinestacker-1.4.0}/.github/workflows/ci-multiplatform.yml +0 -0
  39. {shinestacker-1.3.1 → shinestacker-1.4.0}/.github/workflows/pylint.yml +0 -0
  40. {shinestacker-1.3.1 → shinestacker-1.4.0}/.github/workflows/pypi-publish.yml +0 -0
  41. {shinestacker-1.3.1 → shinestacker-1.4.0}/.github/workflows/release.yml +0 -0
  42. {shinestacker-1.3.1 → shinestacker-1.4.0}/.gitignore +0 -0
  43. {shinestacker-1.3.1 → shinestacker-1.4.0}/.pylintrc +0 -0
  44. {shinestacker-1.3.1 → shinestacker-1.4.0}/.readthedocs.yaml +0 -0
  45. {shinestacker-1.3.1 → shinestacker-1.4.0}/LICENSE +0 -0
  46. {shinestacker-1.3.1 → shinestacker-1.4.0}/MANIFEST.in +0 -0
  47. {shinestacker-1.3.1 → shinestacker-1.4.0}/README.md +0 -0
  48. {shinestacker-1.3.1 → shinestacker-1.4.0}/THIRD_PARTY_LICENSES.txt +0 -0
  49. {shinestacker-1.3.1 → shinestacker-1.4.0}/docs/api.md +0 -0
  50. {shinestacker-1.3.1 → shinestacker-1.4.0}/docs/balancing.md +0 -0
  51. {shinestacker-1.3.1 → shinestacker-1.4.0}/docs/conf.py +0 -0
  52. {shinestacker-1.3.1 → shinestacker-1.4.0}/docs/focus_stacking.md +0 -0
  53. {shinestacker-1.3.1 → shinestacker-1.4.0}/docs/gui.md +0 -0
  54. {shinestacker-1.3.1 → shinestacker-1.4.0}/docs/index.md +0 -0
  55. {shinestacker-1.3.1 → shinestacker-1.4.0}/docs/job.md +0 -0
  56. {shinestacker-1.3.1 → shinestacker-1.4.0}/docs/main.md +0 -0
  57. {shinestacker-1.3.1 → shinestacker-1.4.0}/docs/multilayer.md +0 -0
  58. {shinestacker-1.3.1 → shinestacker-1.4.0}/docs/noise.md +0 -0
  59. {shinestacker-1.3.1 → shinestacker-1.4.0}/docs/requirements.txt +0 -0
  60. {shinestacker-1.3.1 → shinestacker-1.4.0}/docs/vignetting.md +0 -0
  61. {shinestacker-1.3.1 → shinestacker-1.4.0}/img/coffee.gif +0 -0
  62. {shinestacker-1.3.1 → shinestacker-1.4.0}/img/coffee_stack.jpg +0 -0
  63. {shinestacker-1.3.1 → shinestacker-1.4.0}/img/extreme-vignetting.jpg +0 -0
  64. {shinestacker-1.3.1 → shinestacker-1.4.0}/img/flies.gif +0 -0
  65. {shinestacker-1.3.1 → shinestacker-1.4.0}/img/flies_stack.jpg +0 -0
  66. {shinestacker-1.3.1 → shinestacker-1.4.0}/img/flow-diagram.png +0 -0
  67. {shinestacker-1.3.1 → shinestacker-1.4.0}/img/gui-finder.png +0 -0
  68. {shinestacker-1.3.1 → shinestacker-1.4.0}/img/gui-project-new.png +0 -0
  69. {shinestacker-1.3.1 → shinestacker-1.4.0}/img/gui-project-run.png +0 -0
  70. {shinestacker-1.3.1 → shinestacker-1.4.0}/img/gui-retouch.png +0 -0
  71. {shinestacker-1.3.1 → shinestacker-1.4.0}/index.html +0 -0
  72. {shinestacker-1.3.1 → shinestacker-1.4.0}/pyproject.toml +0 -0
  73. {shinestacker-1.3.1 → shinestacker-1.4.0}/requirements.txt +0 -0
  74. {shinestacker-1.3.1 → shinestacker-1.4.0}/scripts/build_release.py +0 -0
  75. {shinestacker-1.3.1 → shinestacker-1.4.0}/scripts/git-rev-list.sh +0 -0
  76. {shinestacker-1.3.1 → shinestacker-1.4.0}/scripts/validate-tomli.py +0 -0
  77. {shinestacker-1.3.1 → shinestacker-1.4.0}/setup.cfg +0 -0
  78. {shinestacker-1.3.1 → shinestacker-1.4.0}/src/shinestacker/__init__.py +0 -0
  79. {shinestacker-1.3.1 → shinestacker-1.4.0}/src/shinestacker/algorithms/__init__.py +0 -0
  80. {shinestacker-1.3.1 → shinestacker-1.4.0}/src/shinestacker/algorithms/align_auto.py +0 -0
  81. {shinestacker-1.3.1 → shinestacker-1.4.0}/src/shinestacker/algorithms/base_stack_algo.py +0 -0
  82. {shinestacker-1.3.1 → shinestacker-1.4.0}/src/shinestacker/algorithms/denoise.py +0 -0
  83. {shinestacker-1.3.1 → shinestacker-1.4.0}/src/shinestacker/algorithms/depth_map.py +0 -0
  84. {shinestacker-1.3.1 → shinestacker-1.4.0}/src/shinestacker/algorithms/exif.py +0 -0
  85. {shinestacker-1.3.1 → shinestacker-1.4.0}/src/shinestacker/algorithms/multilayer.py +0 -0
  86. {shinestacker-1.3.1 → shinestacker-1.4.0}/src/shinestacker/algorithms/pyramid.py +0 -0
  87. {shinestacker-1.3.1 → shinestacker-1.4.0}/src/shinestacker/algorithms/pyramid_auto.py +0 -0
  88. {shinestacker-1.3.1 → shinestacker-1.4.0}/src/shinestacker/algorithms/pyramid_tiles.py +0 -0
  89. {shinestacker-1.3.1 → shinestacker-1.4.0}/src/shinestacker/algorithms/sharpen.py +0 -0
  90. {shinestacker-1.3.1 → shinestacker-1.4.0}/src/shinestacker/algorithms/stack.py +0 -0
  91. {shinestacker-1.3.1 → shinestacker-1.4.0}/src/shinestacker/algorithms/stack_framework.py +0 -0
  92. {shinestacker-1.3.1 → shinestacker-1.4.0}/src/shinestacker/algorithms/white_balance.py +0 -0
  93. {shinestacker-1.3.1 → shinestacker-1.4.0}/src/shinestacker/app/__init__.py +0 -0
  94. {shinestacker-1.3.1 → shinestacker-1.4.0}/src/shinestacker/app/about_dialog.py +0 -0
  95. {shinestacker-1.3.1 → shinestacker-1.4.0}/src/shinestacker/app/gui_utils.py +0 -0
  96. {shinestacker-1.3.1 → shinestacker-1.4.0}/src/shinestacker/app/help_menu.py +0 -0
  97. {shinestacker-1.3.1 → shinestacker-1.4.0}/src/shinestacker/app/main.py +0 -0
  98. {shinestacker-1.3.1 → shinestacker-1.4.0}/src/shinestacker/app/open_frames.py +0 -0
  99. {shinestacker-1.3.1 → shinestacker-1.4.0}/src/shinestacker/app/project.py +0 -0
  100. {shinestacker-1.3.1 → shinestacker-1.4.0}/src/shinestacker/app/retouch.py +0 -0
  101. {shinestacker-1.3.1 → shinestacker-1.4.0}/src/shinestacker/config/__init__.py +0 -0
  102. {shinestacker-1.3.1 → shinestacker-1.4.0}/src/shinestacker/config/config.py +0 -0
  103. {shinestacker-1.3.1 → shinestacker-1.4.0}/src/shinestacker/config/constants.py +0 -0
  104. {shinestacker-1.3.1 → shinestacker-1.4.0}/src/shinestacker/core/__init__.py +0 -0
  105. {shinestacker-1.3.1 → shinestacker-1.4.0}/src/shinestacker/core/colors.py +0 -0
  106. {shinestacker-1.3.1 → shinestacker-1.4.0}/src/shinestacker/core/exceptions.py +0 -0
  107. {shinestacker-1.3.1 → shinestacker-1.4.0}/src/shinestacker/core/framework.py +0 -0
  108. {shinestacker-1.3.1 → shinestacker-1.4.0}/src/shinestacker/core/logging.py +0 -0
  109. {shinestacker-1.3.1 → shinestacker-1.4.0}/src/shinestacker/gui/__init__.py +0 -0
  110. {shinestacker-1.3.1 → shinestacker-1.4.0}/src/shinestacker/gui/base_form_dialog.py +0 -0
  111. {shinestacker-1.3.1 → shinestacker-1.4.0}/src/shinestacker/gui/colors.py +0 -0
  112. {shinestacker-1.3.1 → shinestacker-1.4.0}/src/shinestacker/gui/flow_layout.py +0 -0
  113. {shinestacker-1.3.1 → shinestacker-1.4.0}/src/shinestacker/gui/folder_file_selection.py +0 -0
  114. {shinestacker-1.3.1 → shinestacker-1.4.0}/src/shinestacker/gui/gui_images.py +0 -0
  115. {shinestacker-1.3.1 → shinestacker-1.4.0}/src/shinestacker/gui/gui_logging.py +0 -0
  116. {shinestacker-1.3.1 → shinestacker-1.4.0}/src/shinestacker/gui/ico/focus_stack_bkg.png +0 -0
  117. {shinestacker-1.3.1 → shinestacker-1.4.0}/src/shinestacker/gui/ico/shinestacker.icns +0 -0
  118. {shinestacker-1.3.1 → shinestacker-1.4.0}/src/shinestacker/gui/ico/shinestacker.ico +0 -0
  119. {shinestacker-1.3.1 → shinestacker-1.4.0}/src/shinestacker/gui/ico/shinestacker.png +0 -0
  120. {shinestacker-1.3.1 → shinestacker-1.4.0}/src/shinestacker/gui/ico/shinestacker.svg +0 -0
  121. {shinestacker-1.3.1 → shinestacker-1.4.0}/src/shinestacker/gui/img/close-round-line-icon.png +0 -0
  122. {shinestacker-1.3.1 → shinestacker-1.4.0}/src/shinestacker/gui/img/forward-button-icon.png +0 -0
  123. {shinestacker-1.3.1 → shinestacker-1.4.0}/src/shinestacker/gui/img/play-button-round-icon.png +0 -0
  124. {shinestacker-1.3.1 → shinestacker-1.4.0}/src/shinestacker/gui/img/plus-round-line-icon.png +0 -0
  125. {shinestacker-1.3.1 → shinestacker-1.4.0}/src/shinestacker/gui/new_project.py +0 -0
  126. {shinestacker-1.3.1 → shinestacker-1.4.0}/src/shinestacker/gui/project_converter.py +0 -0
  127. {shinestacker-1.3.1 → shinestacker-1.4.0}/src/shinestacker/gui/project_editor.py +0 -0
  128. {shinestacker-1.3.1 → shinestacker-1.4.0}/src/shinestacker/gui/project_model.py +0 -0
  129. {shinestacker-1.3.1 → shinestacker-1.4.0}/src/shinestacker/gui/select_path_widget.py +0 -0
  130. {shinestacker-1.3.1 → shinestacker-1.4.0}/src/shinestacker/gui/sys_mon.py +0 -0
  131. {shinestacker-1.3.1 → shinestacker-1.4.0}/src/shinestacker/gui/tab_widget.py +0 -0
  132. {shinestacker-1.3.1 → shinestacker-1.4.0}/src/shinestacker/gui/time_progress_bar.py +0 -0
  133. {shinestacker-1.3.1 → shinestacker-1.4.0}/src/shinestacker/retouch/__init__.py +0 -0
  134. {shinestacker-1.3.1 → shinestacker-1.4.0}/src/shinestacker/retouch/brush.py +0 -0
  135. {shinestacker-1.3.1 → shinestacker-1.4.0}/src/shinestacker/retouch/brush_gradient.py +0 -0
  136. {shinestacker-1.3.1 → shinestacker-1.4.0}/src/shinestacker/retouch/denoise_filter.py +0 -0
  137. {shinestacker-1.3.1 → shinestacker-1.4.0}/src/shinestacker/retouch/exif_data.py +0 -0
  138. {shinestacker-1.3.1 → shinestacker-1.4.0}/src/shinestacker/retouch/file_loader.py +0 -0
  139. {shinestacker-1.3.1 → shinestacker-1.4.0}/src/shinestacker/retouch/filter_manager.py +0 -0
  140. {shinestacker-1.3.1 → shinestacker-1.4.0}/src/shinestacker/retouch/icon_container.py +0 -0
  141. {shinestacker-1.3.1 → shinestacker-1.4.0}/src/shinestacker/retouch/io_manager.py +0 -0
  142. {shinestacker-1.3.1 → shinestacker-1.4.0}/src/shinestacker/retouch/layer_collection.py +0 -0
  143. {shinestacker-1.3.1 → shinestacker-1.4.0}/src/shinestacker/retouch/undo_manager.py +0 -0
  144. {shinestacker-1.3.1 → shinestacker-1.4.0}/src/shinestacker/retouch/unsharp_mask_filter.py +0 -0
  145. {shinestacker-1.3.1 → shinestacker-1.4.0}/src/shinestacker/retouch/vignetting_filter.py +0 -0
  146. {shinestacker-1.3.1 → shinestacker-1.4.0}/src/shinestacker/retouch/white_balance_filter.py +0 -0
  147. {shinestacker-1.3.1 → shinestacker-1.4.0}/src/shinestacker.egg-info/dependency_links.txt +0 -0
  148. {shinestacker-1.3.1 → shinestacker-1.4.0}/src/shinestacker.egg-info/entry_points.txt +0 -0
  149. {shinestacker-1.3.1 → shinestacker-1.4.0}/src/shinestacker.egg-info/requires.txt +0 -0
  150. {shinestacker-1.3.1 → shinestacker-1.4.0}/src/shinestacker.egg-info/top_level.txt +0 -0
@@ -2,11 +2,35 @@
2
2
 
3
3
  This page reports the main releases only and the main changes therein.
4
4
 
5
+ ## [v1.4.0] - 2025-09-14
6
+ **GUI improvements**
7
+
8
+ ### Added
9
+ - added retouch view mode with master and frame side by side and top-bottom
10
+ - implemented "Open Recent" menu entry for both projects and retouch images
11
+ - expert options can be shown with a checkbox in each dialog
12
+ - optional summary plots for alignment transformation parameters
13
+
14
+ ## Fixed
15
+ - fixed bug in plot generation
16
+ - fixes warning due to missing glyph in PDF generation on macOS
17
+ - safer parallel plot generation using a thread locks
18
+
19
+ ### Changed
20
+ - code refactoring in various areas
21
+
22
+ ---
23
+
24
+ ### Changed
25
+ - code cleanup
26
+
5
27
  ## [v1.3.1] - 2025-09-08
28
+ **Fixes and optimizations**
6
29
 
7
30
  ## Fixed
8
31
  - fixed input folder widget in job configuration
9
32
  - better management of patological alignments
33
+ - restored alignment match plots
10
34
 
11
35
  ### Changed
12
36
  - improved automatic parameters for parallel alignment
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: shinestacker
3
- Version: 1.3.1
3
+ Version: 1.4.0
4
4
  Summary: ShineStacker
5
5
  Author-email: Luca Lista <luka.lista@gmail.com>
6
6
  License-Expression: LGPL-3.0
@@ -100,12 +100,21 @@ This class has extra parameters, in addition to the above ones:
100
100
 
101
101
  * ```max_threads``` (optional, default: ```2```): number of parallel processes allowed. The number of actual threads will not be greater than the number of available CPU cores.
102
102
  * ```chunk_submit``` (optional, default: ```True```): submit at most ```max_threads``` parallel processes. If ```chunk_submit``` is greater than ```max_threads``` a moderate performance gain is achieved at the cost of a possibly large memory occupancy.
103
- * ```bw_matching``` (optional, default: ```False```): perform matches on black and white version of the images in order to save memory. Preliminary tests indicate that the gain with this option is marginal, and this option may be dropped in the future.
103
+ * ```bw_matching``` (optional, default: ```False```): perform matches on black and white version of the images in order to save memory. Preliminary tests indicate that the gain with this option is marginal, and this option may be dropped in the future.
104
+
105
+ ## Automatic selection of processing strategy
106
+
107
+ A class ```AlignFramesAuto``` implements alignment with either sequential or parallel processing, and automatically tunes parallel processing parameters.
108
+ This class has extra parameters, in addition to the above ones:
109
+
110
+ * ```mode``` (optional, default: ```auto```): can be ```auto```, ```sequential``` or ```parallel```.
111
+ * ```memory_limit``` (optional, default: 8×1024<sup>3</sup>sup>): memory limit to determine optimal running parameters
112
+
104
113
 
105
114
  ## Allowed configurations
106
115
 
107
116
  ⚠️ Not all combinations of detector, descriptor and match methods are allowed. Combinations that are not allowed
108
- give raise to an exception.
117
+ give raise to an exception. This is automatically prevented if one works with the GUI, but may occur when using python scripting. Below the table of the allowed combination with a comparison of CPU performances.
109
118
 
110
119
  ## CPU performances
111
120
 
@@ -0,0 +1 @@
1
+ __version__ = '1.4.0'
@@ -5,13 +5,13 @@ import logging
5
5
  import numpy as np
6
6
  import cv2
7
7
  import matplotlib.pyplot as plt
8
- import matplotlib
9
8
  from .. config.constants import constants
10
9
  from .. core.exceptions import InvalidOptionError
11
10
  from .. core.colors import color_str
11
+ from .. core.core_utils import setup_matplotlib_mode
12
12
  from .utils import img_8bit, img_bw_8bit, save_plot, img_subsample
13
13
  from .stack_framework import SubAction
14
- matplotlib.use('Agg')
14
+ setup_matplotlib_mode()
15
15
 
16
16
  _DEFAULT_FEATURE_CONFIG = {
17
17
  'detector': constants.DEFAULT_DETECTOR,
@@ -135,14 +135,14 @@ def check_homography_distortion(m, img_shape, homography_thresholds=_HOMOGRAPHY_
135
135
  (area_ratio, aspect_ratio, max_angle_dev)
136
136
 
137
137
 
138
- def check_transform(m, img_0, transform_type,
138
+ def check_transform(m, img_shape, transform_type,
139
139
  affine_thresholds, homography_thresholds):
140
140
  if transform_type == constants.ALIGN_RIGID:
141
141
  return check_affine_matrix(
142
- m, img_0.shape, affine_thresholds)
142
+ m, img_shape, affine_thresholds)
143
143
  if transform_type == constants.ALIGN_HOMOGRAPHY:
144
144
  return check_homography_distortion(
145
- m, img_0.shape, homography_thresholds)
145
+ m, img_shape, homography_thresholds)
146
146
  return False, f'invalid transfrom option {transform_type}', None
147
147
 
148
148
 
@@ -251,7 +251,10 @@ def find_transform(src_pts, dst_pts, transform=constants.DEFAULT_TRANSFORM,
251
251
  confidence=align_confidence / 100.0,
252
252
  refineIters=refine_iters)
253
253
  else:
254
- raise InvalidOptionError("transform", transform)
254
+ raise InvalidOptionError(
255
+ 'transform', method,
256
+ f". Valid options are: {constants.ALIGN_HOMOGRAPHY}, {constants.ALIGN_RIGID}"
257
+ )
255
258
  return result
256
259
 
257
260
 
@@ -349,9 +352,11 @@ def align_images(img_ref, img_0, feature_config=None, matching_config=None, alig
349
352
  if m is None:
350
353
  raise InvalidOptionError("transform", transform)
351
354
  transform_type = alignment_config['transform']
352
- is_valid, reason, _result = check_transform(
353
- m, img_0, transform_type,
355
+ is_valid, reason, result = check_transform(
356
+ m, img_0.shape, transform_type,
354
357
  affine_thresholds, homography_thresholds)
358
+ if callbacks and 'save_transform_result' in callbacks:
359
+ callbacks['save_transform_result'](result)
355
360
  if not is_valid:
356
361
  if callbacks and 'warning' in callbacks:
357
362
  callbacks['warning'](f"invalid transformation: {reason}")
@@ -407,6 +412,18 @@ class AlignFramesBase(SubAction):
407
412
  for k in self.alignment_config:
408
413
  if k in kwargs:
409
414
  self.alignment_config[k] = kwargs[k]
415
+ self._area_ratio = None
416
+ self._aspect_ratio = None
417
+ self._max_angle_dev = None
418
+ self._scale_x = None
419
+ self._scale_y = None
420
+ self._translation_x = None
421
+ self._translation_y = None
422
+ self._rotation = None
423
+ self._shear = None
424
+
425
+ def relative_transformation(self):
426
+ return None
410
427
 
411
428
  def align_images(self, idx, img_ref, img_0):
412
429
  pass
@@ -417,6 +434,15 @@ class AlignFramesBase(SubAction):
417
434
  def begin(self, process):
418
435
  self.process = process
419
436
  self._n_good_matches = np.zeros(process.total_action_counts)
437
+ self._area_ratio = np.ones(process.total_action_counts)
438
+ self._aspect_ratio = np.ones(process.total_action_counts)
439
+ self._max_angle_dev = np.zeros(process.total_action_counts)
440
+ self._scale_x = np.ones(process.total_action_counts)
441
+ self._scale_y = np.ones(process.total_action_counts)
442
+ self._translation_x = np.zeros(process.total_action_counts)
443
+ self._translation_y = np.zeros(process.total_action_counts)
444
+ self._rotation = np.zeros(process.total_action_counts)
445
+ self._shear = np.zeros(process.total_action_counts)
420
446
 
421
447
  def run_frame(self, idx, ref_idx, img_0):
422
448
  if idx == self.process.ref_idx:
@@ -432,24 +458,29 @@ class AlignFramesBase(SubAction):
432
458
  f"{os.path.basename(self.process.input_filepath(idx))}"
433
459
 
434
460
  def end(self):
435
- if self.plot_summary:
436
- plt.figure(figsize=constants.PLT_FIG_SIZE)
437
- x = np.arange(1, len(self._n_good_matches) + 1, dtype=int)
461
+
462
+ def get_coordinates(items):
463
+ x = np.arange(1, len(items) + 1, dtype=int)
438
464
  no_ref = x != self.process.ref_idx + 1
439
465
  x = x[no_ref]
440
- y = np.array(self._n_good_matches)[no_ref]
466
+ y = np.array(items)[no_ref]
441
467
  if self.process.ref_idx == 0:
442
- y_max = y[1]
468
+ y_ref = y[1]
443
469
  elif self.process.ref_idx >= len(y):
444
- y_max = y[-1]
470
+ y_ref = y[-1]
445
471
  else:
446
- y_max = (y[self.process.ref_idx - 1] + y[self.process.ref_idx]) / 2
472
+ y_ref = (y[self.process.ref_idx - 1] + y[self.process.ref_idx]) / 2
473
+ return x, y, y_ref
447
474
 
475
+ if self.plot_summary:
476
+ plt.figure(figsize=constants.PLT_FIG_SIZE)
477
+ x, y, y_ref = get_coordinates(self._n_good_matches)
448
478
  plt.plot([self.process.ref_idx + 1, self.process.ref_idx + 1],
449
- [0, y_max], color='cornflowerblue', linestyle='--', label='reference frame')
479
+ [0, y_ref], color='cornflowerblue', linestyle='--', label='reference frame')
450
480
  plt.plot([x[0], x[-1]], [self.min_matches, self.min_matches], color='lightgray',
451
481
  linestyle='--', label='min. matches')
452
482
  plt.plot(x, y, color='navy', label='matches')
483
+ plt.title("Number of matches")
453
484
  plt.xlabel('frame')
454
485
  plt.ylabel('# of matches')
455
486
  plt.legend()
@@ -458,15 +489,160 @@ class AlignFramesBase(SubAction):
458
489
  plot_path = f"{self.process.working_path}/{self.process.plot_path}/" \
459
490
  f"{self.process.name}-matches.pdf"
460
491
  save_plot(plot_path)
461
- plt.close('all')
462
492
  self.process.callback(constants.CALLBACK_SAVE_PLOT, self.process.id,
463
493
  f"{self.process.name}: matches", plot_path)
494
+ transform = self.alignment_config['transform']
495
+ title = "Transformation parameters rel. to reference frame"
496
+ if transform == constants.ALIGN_RIGID:
497
+ plt.figure(figsize=constants.PLT_FIG_SIZE)
498
+ x, y, y_ref = get_coordinates(self._rotation)
499
+ plt.plot([self.process.ref_idx + 1, self.process.ref_idx + 1],
500
+ [0, y_ref], color='cornflowerblue',
501
+ linestyle='--', label='reference frame')
502
+ plt.plot([x[0], x[-1]], [0, 0], color='cornflowerblue', linestyle='--')
503
+ plt.plot(x, y, color='navy', label='rotation (°)')
504
+ y_lim = max(abs(y.min()), abs(y.max())) * 1.1
505
+ plt.ylim(-y_lim, y_lim)
506
+ plt.title(title)
507
+ plt.xlabel('frame')
508
+ plt.ylabel('rotation angle (degrees)')
509
+ plt.legend()
510
+ plt.xlim(x[0], x[-1])
511
+ plot_path = f"{self.process.working_path}/{self.process.plot_path}/" \
512
+ f"{self.process.name}-rotation.pdf"
513
+ save_plot(plot_path)
514
+ self.process.callback(constants.CALLBACK_SAVE_PLOT, self.process.id,
515
+ f"{self.process.name}: rotation", plot_path)
516
+ plt.figure(figsize=constants.PLT_FIG_SIZE)
517
+ x, y_x, y_x_ref = get_coordinates(self._translation_x)
518
+ x, y_y, y_y_ref = get_coordinates(self._translation_y)
519
+ plt.plot([self.process.ref_idx + 1, self.process.ref_idx + 1],
520
+ [y_x_ref, y_y_ref], color='cornflowerblue',
521
+ linestyle='--', label='reference frame')
522
+ plt.plot([x[0], x[-1]], [0, 0], color='cornflowerblue', linestyle='--')
523
+ plt.plot(x, y_x, color='blue', label='translation, x (px)')
524
+ plt.plot(x, y_y, color='red', label='translation, y (px)')
525
+ y_lim = max(abs(y_x.min()), abs(y_x.max()), abs(y_y.min()), abs(y_y.max())) * 1.1
526
+ plt.ylim(-y_lim, y_lim)
527
+ plt.title(title)
528
+ plt.xlabel('frame')
529
+ plt.ylabel('translation (pixels)')
530
+ plt.legend()
531
+ plt.xlim(x[0], x[-1])
532
+ plot_path = f"{self.process.working_path}/{self.process.plot_path}/" \
533
+ f"{self.process.name}-translation.pdf"
534
+ save_plot(plot_path)
535
+ self.process.callback(constants.CALLBACK_SAVE_PLOT, self.process.id,
536
+ f"{self.process.name}: translation", plot_path)
537
+
538
+ plt.figure(figsize=constants.PLT_FIG_SIZE)
539
+ x, y, y_ref = get_coordinates(self._scale_x)
540
+ plt.plot([self.process.ref_idx + 1, self.process.ref_idx + 1],
541
+ [1, y_ref], color='cornflowerblue',
542
+ linestyle='--', label='reference frame')
543
+ plt.plot([x[0], x[-1]], [1, 1], color='cornflowerblue', linestyle='--')
544
+ plt.plot(x, y, color='blue', label='scale factor')
545
+ d_max = max(abs(y.min() - 1), abs(y.max() - 1)) * 1.1
546
+ plt.ylim(1.0 - d_max, 1.0 + d_max)
547
+ plt.title(title)
548
+ plt.xlabel('frame')
549
+ plt.ylabel('scale factor')
550
+ plt.legend()
551
+ plt.xlim(x[0], x[-1])
552
+ plot_path = f"{self.process.working_path}/{self.process.plot_path}/" \
553
+ f"{self.process.name}-scale.pdf"
554
+ save_plot(plot_path)
555
+ self.process.callback(constants.CALLBACK_SAVE_PLOT, self.process.id,
556
+ f"{self.process.name}: scale", plot_path)
557
+ elif transform == constants.ALIGN_HOMOGRAPHY:
558
+ plt.figure(figsize=constants.PLT_FIG_SIZE)
559
+ x, y, y_ref = get_coordinates(self._area_ratio)
560
+ plt.plot([self.process.ref_idx + 1, self.process.ref_idx + 1],
561
+ [0, y_ref], color='cornflowerblue',
562
+ linestyle='--', label='reference frame')
563
+ plt.plot([x[0], x[-1]], [0, 0], color='cornflowerblue', linestyle='--')
564
+ plt.plot(x, y, color='navy', label='area ratio')
565
+ d_max = max(abs(y.min() - 1), abs(y.max() - 1)) * 1.1
566
+ plt.ylim(1.0 - d_max, 1.0 + d_max)
567
+ plt.title(title)
568
+ plt.xlabel('frame')
569
+ plt.ylabel('warped area ratio')
570
+ plt.legend()
571
+ plt.xlim(x[0], x[-1])
572
+ plot_path = f"{self.process.working_path}/{self.process.plot_path}/" \
573
+ f"{self.process.name}-area-ratio.pdf"
574
+ save_plot(plot_path)
575
+ self.process.callback(constants.CALLBACK_SAVE_PLOT, self.process.id,
576
+ f"{self.process.name}: area ratio", plot_path)
577
+ plt.figure(figsize=constants.PLT_FIG_SIZE)
578
+ x, y, y_ref = get_coordinates(self._aspect_ratio)
579
+ plt.plot([self.process.ref_idx + 1, self.process.ref_idx + 1],
580
+ [0, y_ref], color='cornflowerblue',
581
+ linestyle='--', label='reference frame')
582
+ plt.plot([x[0], x[-1]], [0, 0], color='cornflowerblue', linestyle='--')
583
+ plt.plot(x, y, color='navy', label='aspect ratio')
584
+ y_min, y_max = y.min(), y.max()
585
+ delta = y_max - y_min
586
+ plt.ylim(y_min - 0.05 * delta, y_max + 0.05 * delta)
587
+ plt.title(title)
588
+ plt.xlabel('frame')
589
+ plt.ylabel('aspect ratio')
590
+ plt.legend()
591
+ plt.xlim(x[0], x[-1])
592
+ plot_path = f"{self.process.working_path}/{self.process.plot_path}/" \
593
+ f"{self.process.name}-aspect-ratio.pdf"
594
+ save_plot(plot_path)
595
+ self.process.callback(constants.CALLBACK_SAVE_PLOT, self.process.id,
596
+ f"{self.process.name}: aspect ratio", plot_path)
597
+ plt.figure(figsize=constants.PLT_FIG_SIZE)
598
+ x, y, y_ref = get_coordinates(self._max_angle_dev)
599
+ plt.plot([self.process.ref_idx + 1, self.process.ref_idx + 1],
600
+ [0, y_ref], color='cornflowerblue',
601
+ linestyle='--', label='reference frame')
602
+ plt.plot([x[0], x[-1]], [0, 0], color='cornflowerblue', linestyle='--')
603
+ plt.plot(x, y, color='navy', label='max. dev. ang. (°)')
604
+ y_lim = max(abs(y.min()), abs(y.max())) * 1.1
605
+ plt.ylim(-y_lim, y_lim)
606
+ plt.title(title)
607
+ plt.xlabel('frame')
608
+ plt.ylabel('max deviation angle (degrees)')
609
+ plt.legend()
610
+ plt.xlim(x[0], x[-1])
611
+ plot_path = f"{self.process.working_path}/{self.process.plot_path}/" \
612
+ f"{self.process.name}-rotation.pdf"
613
+ save_plot(plot_path)
614
+ self.process.callback(constants.CALLBACK_SAVE_PLOT, self.process.id,
615
+ f"{self.process.name}: rotation", plot_path)
616
+
617
+ def save_transform_result(self, idx, result):
618
+ if result is None:
619
+ return
620
+ transform = self.alignment_config['transform']
621
+ if transform == constants.ALIGN_HOMOGRAPHY:
622
+ area_ratio, aspect_ratio, max_angle_dev = result
623
+ self._area_ratio[idx] = area_ratio
624
+ self._aspect_ratio[idx] = aspect_ratio
625
+ self._max_angle_dev[idx] = max_angle_dev
626
+ elif transform == constants.ALIGN_RIGID:
627
+ scale_x, scale_y, translation_x, translation_y, rotation, shear = result
628
+ self._scale_x[idx] = scale_x
629
+ self._scale_y[idx] = scale_y
630
+ self._translation_x[idx] = translation_x
631
+ self._translation_y[idx] = translation_y
632
+ self._rotation[idx] = rotation
633
+ self._shear[idx] = shear
634
+ else:
635
+ raise InvalidOptionError(
636
+ 'transform', transform,
637
+ f". Valid options are: {constants.ALIGN_HOMOGRAPHY}, {constants.ALIGN_RIGID}"
638
+ )
464
639
 
465
640
 
466
641
  class AlignFrames(AlignFramesBase):
467
642
  def align_images(self, idx, img_ref, img_0):
468
643
  idx_str = f"{idx:04d}"
469
644
  idx_tot_str = self.process.idx_tot_str(idx)
645
+
470
646
  callbacks = {
471
647
  'message': lambda: self.print_message(f'{idx_tot_str}: find matches'),
472
648
  'matches_message': lambda n: self.print_message(f'{idx_tot_str}: good matches: {n}'),
@@ -476,7 +652,8 @@ class AlignFrames(AlignFramesBase):
476
652
  f': {msg}', constants.LOG_COLOR_WARNING),
477
653
  'save_plot': lambda plot_path: self.process.callback(
478
654
  constants.CALLBACK_SAVE_PLOT, self.process.id,
479
- f"{self.process.name}: matches\nframe {idx_str}", plot_path)
655
+ f"{self.process.name}: matches\nframe {idx_str}", plot_path),
656
+ 'save_transform_result': lambda result: self.save_transform_result(idx, result)
480
657
  }
481
658
  if self.plot_matches:
482
659
  plot_path = os.path.join(
@@ -504,5 +681,8 @@ class AlignFrames(AlignFramesBase):
504
681
  return None
505
682
  return img
506
683
 
684
+ def relative_transformation(self):
685
+ return False
686
+
507
687
  def sequential_processing(self):
508
688
  return True
@@ -39,6 +39,7 @@ class AlignFramesParallel(AlignFramesBase):
39
39
  self.chunk_submit = kwargs.get('chunk_submit', constants.DEFAULT_ALIGN_CHUNK_SUBMIT)
40
40
  self.bw_matching = kwargs.get('bw_matching', constants.DEFAULT_ALIGN_BW_MATCHING)
41
41
  self._img_cache = None
42
+ self._img_shapes = None
42
43
  self._img_locks = None
43
44
  self._cache_locks = None
44
45
  self._target_indices = None
@@ -48,6 +49,9 @@ class AlignFramesParallel(AlignFramesBase):
48
49
  self._kp = None
49
50
  self._des = None
50
51
 
52
+ def relative_transformation(self):
53
+ return True
54
+
51
55
  def cache_img(self, idx):
52
56
  with self._cache_locks[idx]:
53
57
  self._img_locks[idx] += 1
@@ -56,6 +60,8 @@ class AlignFramesParallel(AlignFramesBase):
56
60
  if self.bw_matching:
57
61
  img = img_bw(img)
58
62
  self._img_cache[idx] = img
63
+ if img is not None:
64
+ self._img_shapes[idx] = img.shape
59
65
  return self._img_cache[idx]
60
66
 
61
67
  def submit_threads(self, idxs, imgs):
@@ -112,6 +118,7 @@ class AlignFramesParallel(AlignFramesBase):
112
118
  self.process.id, self.process.name, 2 * n_frames)
113
119
  input_filepaths = self.process.input_filepaths()
114
120
  self._img_cache = [None] * n_frames
121
+ self._img_shapes = [None] * n_frames
115
122
  self._img_locks = [0] * n_frames
116
123
  self._cache_locks = [threading.Lock() for _ in range(n_frames)]
117
124
  self._target_indices = [None] * n_frames
@@ -168,9 +175,17 @@ class AlignFramesParallel(AlignFramesBase):
168
175
  self._transforms[idx] = None
169
176
  gc.collect()
170
177
  missing_transforms = 0
178
+ thresholds = self.get_transform_thresholds()
171
179
  for i in range(n_frames):
172
180
  if self._cumulative_transforms[i] is not None:
173
181
  self._cumulative_transforms[i] = self._cumulative_transforms[i].astype(np.float32)
182
+ is_valid, _reason, result = check_transform(
183
+ self._cumulative_transforms[i], self._img_shapes[i],
184
+ transform_type, *thresholds)
185
+ if is_valid:
186
+ self.save_transform_result(i, result)
187
+ else:
188
+ self._cumulative_transforms[i] = None
174
189
  else:
175
190
  missing_transforms += 1
176
191
  msg = "feature extaction completed"
@@ -277,7 +292,8 @@ class AlignFramesParallel(AlignFramesBase):
277
292
  return self.extract_features(idx, delta + 1)
278
293
  transform_type = self.alignment_config['transform']
279
294
  thresholds = self.get_transform_thresholds()
280
- is_valid, _reason, _result = check_transform(m, img_0, transform_type, *thresholds)
295
+ is_valid, _reason, _result = check_transform(m, img_0.shape, transform_type, *thresholds)
296
+ # self.save_transform_result(idx, result)
281
297
  if not is_valid:
282
298
  msg = f"invalid transformation for {self.image_str(idx)}"
283
299
  do_abort = self.alignment_config['abort_abnormal']
@@ -8,9 +8,11 @@ from scipy.interpolate import interp1d
8
8
  from .. config.constants import constants
9
9
  from .. core.exceptions import InvalidOptionError
10
10
  from .. core.colors import color_str
11
+ from .. core.core_utils import setup_matplotlib_mode
11
12
  from .utils import (read_img, save_plot, img_subsample, bgr_to_hsv, bgr_to_hls,
12
13
  hsv_to_bgr, hls_to_bgr, bgr_to_lab, lab_to_bgr)
13
14
  from .stack_framework import SubAction
15
+ setup_matplotlib_mode()
14
16
 
15
17
 
16
18
  class BaseHistogrammer:
@@ -40,12 +42,11 @@ class BaseHistogrammer:
40
42
  x_values = np.linspace(0, self.max_pixel_value, len(hist))
41
43
  ax.plot(x_values, hist, color=color, alpha=alpha)
42
44
 
43
- def save_plot(self, idx):
45
+ def save_plot(self, idx, fig=None):
44
46
  idx_str = f"{idx:04d}"
45
47
  plot_path = f"{self.process.working_path}/{self.process.plot_path}/" \
46
48
  f"{self.process.name}-hist-{idx_str}.pdf"
47
- save_plot(plot_path)
48
- plt.close('all')
49
+ save_plot(plot_path, fig)
49
50
  self.process.callback(
50
51
  'save_plot',
51
52
  self.process.id, f"{self.process.name}: balance\nframe {idx_str}",
@@ -56,7 +57,6 @@ class BaseHistogrammer:
56
57
  plot_path = f"{self.process.working_path}/{self.process.plot_path}/" \
57
58
  f"{self.process.name}-{name}.pdf"
58
59
  save_plot(plot_path)
59
- plt.close('all')
60
60
  self.process.callback(
61
61
  'save_plot', self.process.id,
62
62
  f"{self.process.name}: {name}", plot_path
@@ -69,13 +69,14 @@ class LumiHistogrammer(BaseHistogrammer):
69
69
  self.colors = ("r", "g", "b")
70
70
 
71
71
  def generate_frame_plot(self, idx, hist, chans, calc_hist_func):
72
- _fig, axs = plt.subplots(1, 2, figsize=constants.PLT_FIG_SIZE, sharey=True)
72
+ fig, axs = plt.subplots(1, 2, figsize=constants.PLT_FIG_SIZE, sharey=True)
73
73
  self.histo_plot(axs[0], hist, "pixel luminosity", 'black')
74
74
  for (chan, color) in zip(chans, self.colors):
75
75
  hist_col = calc_hist_func(chan)
76
76
  self.histo_plot(axs[1], hist_col, "R, G, B intensity", color, alpha=0.5)
77
+ fig.suptitle("Image histograms")
77
78
  plt.xlim(0, self.max_pixel_value)
78
- self.save_plot(idx)
79
+ self.save_plot(idx, fig)
79
80
 
80
81
  def generate_summary_plot(self, ref_idx):
81
82
  plt.figure(figsize=constants.PLT_FIG_SIZE)
@@ -86,6 +87,7 @@ class LumiHistogrammer(BaseHistogrammer):
86
87
  plt.plot([x[0], x[-1]], [1, 1], color='lightgray', linestyle='--',
87
88
  label='no correction')
88
89
  plt.plot(x, y, color='navy', label='luminosity correction')
90
+ plt.title("Image balance correction")
89
91
  plt.xlabel('frame')
90
92
  plt.ylabel('correction')
91
93
  plt.legend()
@@ -100,11 +102,12 @@ class RGBHistogrammer(BaseHistogrammer):
100
102
  self.colors = ("r", "g", "b")
101
103
 
102
104
  def generate_frame_plot(self, idx, hists):
103
- _fig, axs = plt.subplots(1, 3, figsize=constants.PLT_FIG_SIZE, sharey=True)
105
+ fig, axs = plt.subplots(1, 3, figsize=constants.PLT_FIG_SIZE, sharey=True)
104
106
  for c in [2, 1, 0]:
105
107
  self.histo_plot(axs[c], hists[c], self.colors[c] + " luminosity", self.colors[c])
108
+ fig.suptitle("Image histograms")
106
109
  plt.xlim(0, self.max_pixel_value)
107
- self.save_plot(idx)
110
+ self.save_plot(idx, fig)
108
111
 
109
112
  def generate_summary_plot(self, ref_idx):
110
113
  plt.figure(figsize=constants.PLT_FIG_SIZE)
@@ -118,6 +121,7 @@ class RGBHistogrammer(BaseHistogrammer):
118
121
  plt.plot(x, y[:, 0], color='r', label='R correction')
119
122
  plt.plot(x, y[:, 1], color='g', label='G correction')
120
123
  plt.plot(x, y[:, 2], color='b', label='B correction')
124
+ plt.title("Image balance correction")
121
125
  plt.xlabel('frame')
122
126
  plt.ylabel('correction')
123
127
  plt.legend()
@@ -133,10 +137,12 @@ class Ch1Histogrammer(BaseHistogrammer):
133
137
  self.colors = colors
134
138
 
135
139
  def generate_frame_plot(self, idx, hists):
136
- _fig, axs = plt.subplots(1, 3, figsize=constants.PLT_FIG_SIZE, sharey=True)
140
+ fig, axs = plt.subplots(1, 3, figsize=constants.PLT_FIG_SIZE, sharey=True)
137
141
  for c in range(3):
138
142
  self.histo_plot(axs[c], hists[c], self.labels[c], self.colors[c])
139
- plt.xlim(0, self.max_pixel_value)
143
+ fig.suptitle("Image histograms")
144
+ for ax in axs:
145
+ ax.set_xlim(0, self.max_pixel_value)
140
146
  self.save_plot(idx)
141
147
 
142
148
  def generate_summary_plot(self, ref_idx):
@@ -149,6 +155,7 @@ class Ch1Histogrammer(BaseHistogrammer):
149
155
  plt.plot([x[0], x[-1]], [1, 1], color='lightgray', linestyle='--',
150
156
  label='no correction')
151
157
  plt.plot(x, y[:, 0], color=self.colors[0], label=self.labels[0] + ' correction')
158
+ plt.title("Image balance correction")
152
159
  plt.xlabel('frame')
153
160
  plt.ylabel('correction')
154
161
  plt.legend()
@@ -164,10 +171,12 @@ class Ch2Histogrammer(BaseHistogrammer):
164
171
  self.colors = colors
165
172
 
166
173
  def generate_frame_plot(self, idx, hists):
167
- _fig, axs = plt.subplots(1, 3, figsize=constants.PLT_FIG_SIZE, sharey=True)
174
+ fig, axs = plt.subplots(1, 3, figsize=constants.PLT_FIG_SIZE, sharey=True)
168
175
  for c in range(3):
169
176
  self.histo_plot(axs[c], hists[c], self.labels[c], self.colors[c])
170
- plt.xlim(0, self.max_pixel_value)
177
+ fig.suptitle("Image histograms")
178
+ for ax in axs:
179
+ ax.set_xlim(0, self.max_pixel_value)
171
180
  self.save_plot(idx)
172
181
 
173
182
  def generate_summary_plot(self, ref_idx):
@@ -181,6 +190,7 @@ class Ch2Histogrammer(BaseHistogrammer):
181
190
  label='no correction')
182
191
  plt.plot(x, y[:, 0], color=self.colors[1], label=self.labels[1] + ' correction')
183
192
  plt.plot(x, y[:, 1], color=self.colors[2], label=self.labels[2] + ' correction')
193
+ plt.title("Image balance correction")
184
194
  plt.xlabel('frame')
185
195
  plt.ylabel('correction')
186
196
  plt.legend()
@@ -603,7 +613,7 @@ class BalanceFrames(SubAction):
603
613
  mask_radius = int(min(*shape) * self.mask_size / 2)
604
614
  cv2.circle(img, (shape[1] // 2, shape[0] // 2), mask_radius, 255, -1)
605
615
  plt.figure(figsize=constants.PLT_FIG_SIZE)
606
- plt.title('Mask')
616
+ plt.title('Image balance mask')
607
617
  plt.imshow(img, 'gray')
608
618
  self.correction.histogrammer.save_summary_plot("mask")
609
619
 
@@ -10,10 +10,12 @@ from .. config.constants import constants
10
10
  from .. core.colors import color_str
11
11
  from .. core.exceptions import ImageLoadError
12
12
  from .. core.framework import TaskBase
13
- from .. core.core_utils import make_tqdm_bar
13
+ from .. core.core_utils import make_tqdm_bar, setup_matplotlib_mode
14
14
  from .. core.exceptions import RunStopException, ShapeError
15
15
  from .stack_framework import ImageSequenceManager, SubAction
16
16
  from .utils import read_img, save_plot, get_img_metadata, validate_image
17
+ setup_matplotlib_mode()
18
+
17
19
 
18
20
  MAX_NOISY_PIXELS = 1000
19
21
 
@@ -1,6 +1,8 @@
1
1
  # pylint: disable=C0114, C0116, E1101, R0914
2
2
  import os
3
+ import gc
3
4
  import logging
5
+ import threading
4
6
  import numpy as np
5
7
  import cv2
6
8
  import matplotlib.pyplot as plt
@@ -50,6 +52,10 @@ def extension_jpg_png(path):
50
52
  return extension_in(path, EXTENSIONS_JPG + EXTENSIONS_PNG)
51
53
 
52
54
 
55
+ def extension_jpg_tif_png(path):
56
+ return extension_in(path, EXTENSIONS_JPG + EXTENSIONS_TIF + EXTENSIONS_PNG)
57
+
58
+
53
59
  def read_img(file_path):
54
60
  if not os.path.isfile(file_path):
55
61
  raise RuntimeError("File does not exist: " + file_path)
@@ -124,17 +130,22 @@ def read_and_validate_img(filename, expected_shape=None, expected_dtype=None):
124
130
  return validate_image(read_img(filename), expected_shape, expected_dtype)
125
131
 
126
132
 
127
- def save_plot(filename):
133
+ def save_plot(filename, fig=None):
128
134
  logging.getLogger(__name__).debug(msg=f"save plot file: {filename}")
129
- dir_path = os.path.dirname(filename)
130
- if not dir_path:
131
- dir_path = '.'
132
- if not os.path.isdir(dir_path):
133
- os.makedirs(dir_path)
134
- plt.savefig(filename, dpi=150)
135
- if config.JUPYTER_NOTEBOOK:
136
- plt.show()
137
- plt.close('all')
135
+ save_lock = threading.Lock()
136
+ with save_lock:
137
+ dir_path = os.path.dirname(filename)
138
+ if not dir_path:
139
+ dir_path = '.'
140
+ if not os.path.isdir(dir_path):
141
+ os.makedirs(dir_path)
142
+ if fig is None:
143
+ fig = plt.gcf()
144
+ fig.savefig(filename, dpi=150)
145
+ if config.JUPYTER_NOTEBOOK:
146
+ plt.show()
147
+ plt.close(fig)
148
+ gc.collect()
138
149
 
139
150
 
140
151
  def img_subsample(img, subsample, fast=True):
@@ -7,9 +7,11 @@ import matplotlib.pyplot as plt
7
7
  from scipy.optimize import curve_fit, fsolve
8
8
  import cv2
9
9
  from .. core.colors import color_str
10
+ from .. core.core_utils import setup_matplotlib_mode
10
11
  from .. config.constants import constants
11
12
  from .utils import img_8bit, save_plot, img_subsample
12
13
  from .stack_framework import SubAction
14
+ setup_matplotlib_mode()
13
15
 
14
16
  CLIP_EXP = 10
15
17