shinestacker 1.0.4.post2__py3-none-any.whl → 1.2.0__py3-none-any.whl

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 (37) hide show
  1. shinestacker/_version.py +1 -1
  2. shinestacker/algorithms/__init__.py +4 -1
  3. shinestacker/algorithms/align.py +128 -14
  4. shinestacker/algorithms/balance.py +362 -163
  5. shinestacker/algorithms/base_stack_algo.py +33 -4
  6. shinestacker/algorithms/depth_map.py +9 -12
  7. shinestacker/algorithms/multilayer.py +12 -2
  8. shinestacker/algorithms/noise_detection.py +8 -3
  9. shinestacker/algorithms/pyramid.py +57 -42
  10. shinestacker/algorithms/pyramid_auto.py +141 -0
  11. shinestacker/algorithms/pyramid_tiles.py +264 -0
  12. shinestacker/algorithms/stack.py +14 -11
  13. shinestacker/algorithms/stack_framework.py +17 -11
  14. shinestacker/algorithms/utils.py +180 -1
  15. shinestacker/algorithms/vignetting.py +23 -5
  16. shinestacker/config/constants.py +31 -5
  17. shinestacker/gui/action_config.py +6 -7
  18. shinestacker/gui/action_config_dialog.py +425 -258
  19. shinestacker/gui/base_form_dialog.py +11 -6
  20. shinestacker/gui/flow_layout.py +105 -0
  21. shinestacker/gui/gui_run.py +24 -19
  22. shinestacker/gui/main_window.py +4 -3
  23. shinestacker/gui/menu_manager.py +12 -2
  24. shinestacker/gui/new_project.py +28 -22
  25. shinestacker/gui/project_controller.py +40 -23
  26. shinestacker/gui/project_converter.py +6 -6
  27. shinestacker/gui/project_editor.py +21 -7
  28. shinestacker/gui/time_progress_bar.py +2 -2
  29. shinestacker/retouch/exif_data.py +5 -5
  30. shinestacker/retouch/shortcuts_help.py +4 -4
  31. shinestacker/retouch/vignetting_filter.py +12 -8
  32. {shinestacker-1.0.4.post2.dist-info → shinestacker-1.2.0.dist-info}/METADATA +20 -1
  33. {shinestacker-1.0.4.post2.dist-info → shinestacker-1.2.0.dist-info}/RECORD +37 -34
  34. {shinestacker-1.0.4.post2.dist-info → shinestacker-1.2.0.dist-info}/WHEEL +0 -0
  35. {shinestacker-1.0.4.post2.dist-info → shinestacker-1.2.0.dist-info}/entry_points.txt +0 -0
  36. {shinestacker-1.0.4.post2.dist-info → shinestacker-1.2.0.dist-info}/licenses/LICENSE +0 -0
  37. {shinestacker-1.0.4.post2.dist-info → shinestacker-1.2.0.dist-info}/top_level.txt +0 -0
@@ -1,4 +1,5 @@
1
1
  # pylint: disable=C0114, C0115, C0116, R0902, E1101, W0718, W0640, R0913, R0917, R0914
2
+ import math
2
3
  import traceback
3
4
  import logging
4
5
  import numpy as np
@@ -49,16 +50,30 @@ def fit_sigmoid(radii, intensities):
49
50
  return res
50
51
 
51
52
 
53
+ def subsample_factor(subsample, image):
54
+ if subsample == 0:
55
+ h, w = image.shape[:2]
56
+ img_res = (float(h) / 1000) * (float(w) / 1000)
57
+ target_res = constants.DEFAULT_BALANCE_RES_TARGET_MPX
58
+ subsample = int(1 + math.floor(img_res / target_res))
59
+ return subsample
60
+
61
+
52
62
  def img_subsampled(image, subsample=constants.DEFAULT_VIGN_SUBSAMPLE,
53
63
  fast_subsampling=constants.DEFAULT_VIGN_FAST_SUBSAMPLING):
54
64
  image_bw = cv2.cvtColor(img_8bit(image), cv2.COLOR_BGR2GRAY)
55
- return image_bw if subsample == 1 else img_subsample(image_bw, subsample, fast_subsampling)
65
+ if subsample == 0:
66
+ subsample = subsample_factor(subsample, image)
67
+ img_sub = image_bw if subsample == 1 else img_subsample(image_bw, subsample, fast_subsampling)
68
+ return img_sub
56
69
 
57
70
 
58
71
  def compute_fit_parameters(
59
72
  image, r_steps, radii=None, intensities=None,
60
73
  subsample=constants.DEFAULT_VIGN_SUBSAMPLE,
61
74
  fast_subsampling=constants.DEFAULT_VIGN_FAST_SUBSAMPLING):
75
+ if subsample == 0:
76
+ subsample = subsample_factor(subsample, image)
62
77
  image_sub = img_subsampled(image, subsample, fast_subsampling)
63
78
  if radii is None and intensities is None:
64
79
  radii, intensities = radial_mean_intensity(image_sub, r_steps)
@@ -77,6 +92,8 @@ def correct_vignetting(
77
92
  if params is None:
78
93
  if r_steps is None:
79
94
  raise RuntimeError("Either r_steps or pars must not be None")
95
+ if subsample == 0:
96
+ subsample = subsample_factor(subsample, image)
80
97
  params = compute_fit_parameters(
81
98
  image, r_steps, subsample=subsample, fast_subsampling=fast_subsampling)
82
99
  if v0 is None:
@@ -121,11 +138,12 @@ class Vignetting(SubAction):
121
138
  h, w = img_0.shape[:2]
122
139
  self.w_2, self.h_2 = w / 2, h / 2
123
140
  self.r_max = np.sqrt((w / 2)**2 + (h / 2)**2)
124
- image_sub = img_subsampled(img_0, self.subsample, self.fast_subsampling)
141
+ subsample = subsample_factor(self.subsample, img_0)
142
+ image_sub = img_subsampled(img_0, subsample, self.fast_subsampling)
125
143
  radii, intensities = radial_mean_intensity(image_sub, self.r_steps)
126
144
  try:
127
145
  params = compute_fit_parameters(
128
- img_0, self.r_steps, radii, intensities, self.subsample, self.fast_subsampling)
146
+ img_0, self.r_steps, radii, intensities, subsample, self.fast_subsampling)
129
147
  except Exception as e:
130
148
  traceback.print_tb(e.__traceback__)
131
149
  self.process.sub_message(
@@ -144,7 +162,7 @@ class Vignetting(SubAction):
144
162
  if self.plot_correction:
145
163
  plt.figure(figsize=(10, 5))
146
164
  plt.plot(radii, intensities, label="image mean intensity")
147
- plt.plot(radii, sigmoid_model(radii * self.subsample, *params), label="sigmoid fit")
165
+ plt.plot(radii, sigmoid_model(radii * subsample, *params), label="sigmoid fit")
148
166
  plt.xlabel('radius (pixels)')
149
167
  plt.ylabel('mean intensity')
150
168
  plt.legend()
@@ -165,7 +183,7 @@ class Vignetting(SubAction):
165
183
  self.process.sub_message_r(color_str(": correct vignetting", "cyan"))
166
184
  return correct_vignetting(
167
185
  img_0, self.max_correction, self.black_threshold, None, params, self.v0,
168
- self.subsample, self.fast_subsampling)
186
+ subsample, self.fast_subsampling)
169
187
 
170
188
  def begin(self, process):
171
189
  self.process = process
@@ -1,6 +1,7 @@
1
1
  # pylint: disable=C0114, C0115, C0116, C0103, R0903
2
2
  import sys
3
3
  import re
4
+ import os
4
5
 
5
6
 
6
7
  class _Constants:
@@ -12,6 +13,9 @@ class _Constants:
12
13
  NUM_UINT16 = 65536
13
14
  MAX_UINT8 = 255
14
15
  MAX_UINT16 = 65535
16
+ ONE_KILO = 1024
17
+ ONE_MEGA = ONE_KILO**2
18
+ ONE_GIGA = ONE_KILO**3
15
19
 
16
20
  LOG_FONTS = ['Monaco', 'Menlo', ' Lucida Console', 'Courier New', 'Courier', 'monospace']
17
21
  LOG_FONTS_STR = ", ".join(LOG_FONTS)
@@ -39,9 +43,16 @@ class _Constants:
39
43
  STACK_ALGO_DEFAULT = STACK_ALGO_PYRAMID
40
44
  DEFAULT_PLOTS_PATH = 'plots'
41
45
 
46
+ FIELD_SUBSAMPLE_VALUES_1 = [2, 3, 4, 6, 8, 12, 16, 24, 32]
47
+ FIELD_SUBSAMPLE_OPTIONS_1 = [f"1/{n} × 1/{n}" for n in FIELD_SUBSAMPLE_VALUES_1]
48
+ FIELD_SUBSAMPLE_VALUES = [0, 1] + FIELD_SUBSAMPLE_VALUES_1
49
+ FIELD_SUBSAMPLE_OPTIONS = ['Auto', 'Full resolution'] + FIELD_SUBSAMPLE_OPTIONS_1
50
+ FIELD_SUBSAMPLE_DEFAULT = FIELD_SUBSAMPLE_VALUES[0]
51
+
42
52
  PATH_SEPARATOR = ';'
43
53
 
44
54
  LOG_COLOR_ALERT = 'red'
55
+ LOG_COLOR_WARNING = 'yellow'
45
56
  LOG_COLOR_LEVEL_JOB = 'green'
46
57
  LOG_COLOR_LEVEL_1 = 'blue'
47
58
  LOG_COLOR_LEVEL_2 = 'magenta'
@@ -49,8 +60,10 @@ class _Constants:
49
60
 
50
61
  DEFAULT_FILE_REVERSE_ORDER = False
51
62
  DEFAULT_MULTILAYER_FILE_REVERSE_ORDER = True
63
+ MULTILAYER_WARNING_MEM_GB = 1
52
64
 
53
65
  DEFAULT_NOISE_MAP_FILENAME = "noise-map/hot_pixels.png"
66
+ DEFAULT_NOISE_MAX_FRAMES = 10
54
67
  DEFAULT_MN_KERNEL_SIZE = 3
55
68
  INTERPOLATE_MEAN = 'MEAN'
56
69
  INTERPOLATE_MEDIAN = 'MEDIAN'
@@ -103,11 +116,13 @@ class _Constants:
103
116
  DEFAULT_REFINE_ITERS = 100
104
117
  DEFAULT_ALIGN_CONFIDENCE = 99.9
105
118
  DEFAULT_ALIGN_MAX_ITERS = 2000
119
+ DEFAULT_ALIGN_ABORT_ABNORMAL = True
106
120
  DEFAULT_BORDER_VALUE = [0] * 4
107
121
  DEFAULT_BORDER_BLUR = 50
108
- DEFAULT_ALIGN_SUBSAMPLE = 2
122
+ DEFAULT_ALIGN_SUBSAMPLE = 0
123
+ DEFAULT_ALIGN_RES_TARGET_MPX = 2
109
124
  DEFAULT_ALIGN_FAST_SUBSAMPLING = False
110
- DEFAULT_ALIGN_MIN_GOOD_MATCHES = 100
125
+ DEFAULT_ALIGN_MIN_GOOD_MATCHES = 50
111
126
 
112
127
  BALANCE_LINEAR = "LINEAR"
113
128
  BALANCE_GAMMA = "GAMMA"
@@ -118,9 +133,12 @@ class _Constants:
118
133
  BALANCE_RGB = "RGB"
119
134
  BALANCE_HSV = "HSV"
120
135
  BALANCE_HLS = "HLS"
121
- VALID_BALANCE_CHANNELS = [BALANCE_LUMI, BALANCE_RGB, BALANCE_HSV, BALANCE_HLS]
136
+ BALANCE_LAB = "LAB"
137
+ VALID_BALANCE_CHANNELS = [BALANCE_LUMI, BALANCE_RGB, BALANCE_HSV, BALANCE_HLS,
138
+ BALANCE_LAB]
122
139
 
123
- DEFAULT_BALANCE_SUBSAMPLE = 8
140
+ DEFAULT_BALANCE_SUBSAMPLE = 0
141
+ DEFAULT_BALANCE_RES_TARGET_MPX = 2
124
142
  DEFAULT_BALANCE_FAST_SUBSAMPLING = False
125
143
  DEFAULT_CORR_MAP = BALANCE_LINEAR
126
144
  DEFAULT_CHANNEL = BALANCE_LUMI
@@ -129,7 +147,8 @@ class _Constants:
129
147
  DEFAULT_R_STEPS = 100
130
148
  DEFAULT_BLACK_THRESHOLD = 1.0
131
149
  DEFAULT_MAX_CORRECTION = 1
132
- DEFAULT_VIGN_SUBSAMPLE = 8
150
+ DEFAULT_VIGN_SUBSAMPLE = 0
151
+ DEFAULT_VIGN_RES_TARGET_MPX = 2
133
152
  DEFAULT_VIGN_FAST_SUBSAMPLING = False
134
153
 
135
154
  FLOAT_32 = 'float-32'
@@ -160,6 +179,13 @@ class _Constants:
160
179
  DEFAULT_PY_MIN_SIZE = 32
161
180
  DEFAULT_PY_KERNEL_SIZE = 5
162
181
  DEFAULT_PY_GEN_KERNEL = 0.4
182
+ DEFAULT_PY_TILE_SIZE = 512
183
+ DEFAULT_PY_N_TILED_LAYERS = 2
184
+ DEFAULT_PY_MEMORY_LIMIT_GB = 8
185
+ DEFAULT_PY_MAX_THREADS = min(os.cpu_count() or 4, 8)
186
+ DEFAULT_PY_MODE = 'auto'
187
+ PY_VALID_MODES = ['auto', 'memory', 'tiled']
188
+ MIN_PY_TILE_SIZE = 256
163
189
 
164
190
  DEFAULT_PLOT_STACK_BUNCH = False
165
191
  DEFAULT_PLOT_STACK = True
@@ -10,7 +10,6 @@ from PySide6.QtWidgets import (QPushButton, QHBoxLayout, QFileDialog, QLabel, QC
10
10
  from .. config.constants import constants
11
11
  from .select_path_widget import (create_select_file_paths_widget, create_layout_widget_no_margins,
12
12
  create_layout_widget_and_connect)
13
- from .project_model import ActionConfig
14
13
 
15
14
  FIELD_TEXT = 'text'
16
15
  FIELD_ABS_PATH = 'abs_path'
@@ -30,23 +29,23 @@ class ActionConfigurator(ABC):
30
29
  self.current_wd = current_wd
31
30
 
32
31
  @abstractmethod
33
- def create_form(self, layout, action: ActionConfig, tag="Action"):
32
+ def create_form(self, layout, action, tag="Action"):
34
33
  pass
35
34
 
36
35
  @abstractmethod
37
- def update_params(self, params: Dict[str, Any]) -> bool:
36
+ def update_params(self, params):
38
37
  pass
39
38
 
40
39
 
41
40
  class FieldBuilder:
42
41
  def __init__(self, layout, action, current_wd):
43
- self.layout = layout
42
+ self.main_layout = layout
44
43
  self.action = action
45
44
  self.current_wd = current_wd
46
45
  self.fields = {}
47
46
 
48
- def add_field(self, tag: str, field_type: str, label: str,
49
- required: bool = False, add_to_layout=None, **kwargs):
47
+ def add_field(self, tag, field_type, label,
48
+ required=False, add_to_layout=None, **kwargs):
50
49
  if field_type == FIELD_TEXT:
51
50
  widget = self.create_text_field(tag, **kwargs)
52
51
  elif field_type == FIELD_ABS_PATH:
@@ -93,7 +92,7 @@ class FieldBuilder:
93
92
  **kwargs
94
93
  }
95
94
  if add_to_layout is None:
96
- add_to_layout = self.layout
95
+ add_to_layout = self.main_layout
97
96
  add_to_layout.addRow(f"{label}:", widget)
98
97
  return widget
99
98