shinestacker 0.3.5__py3-none-any.whl → 0.4.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 (44) hide show
  1. shinestacker/_version.py +1 -1
  2. shinestacker/algorithms/align.py +37 -20
  3. shinestacker/algorithms/balance.py +2 -1
  4. shinestacker/algorithms/base_stack_algo.py +2 -1
  5. shinestacker/algorithms/multilayer.py +11 -8
  6. shinestacker/algorithms/noise_detection.py +13 -7
  7. shinestacker/algorithms/pyramid.py +7 -4
  8. shinestacker/algorithms/stack.py +5 -4
  9. shinestacker/algorithms/stack_framework.py +12 -10
  10. shinestacker/app/app_config.py +4 -22
  11. shinestacker/app/main.py +1 -1
  12. shinestacker/config/config.py +22 -16
  13. shinestacker/config/constants.py +8 -1
  14. shinestacker/core/framework.py +15 -10
  15. shinestacker/gui/action_config.py +20 -41
  16. shinestacker/gui/actions_window.py +18 -47
  17. shinestacker/gui/gui_logging.py +8 -7
  18. shinestacker/gui/gui_run.py +8 -8
  19. shinestacker/gui/main_window.py +4 -4
  20. shinestacker/gui/new_project.py +34 -37
  21. shinestacker/gui/project_converter.py +0 -1
  22. shinestacker/gui/project_editor.py +43 -20
  23. shinestacker/gui/select_path_widget.py +32 -0
  24. shinestacker/retouch/base_filter.py +12 -1
  25. shinestacker/retouch/denoise_filter.py +4 -10
  26. shinestacker/retouch/exif_data.py +3 -9
  27. shinestacker/retouch/icon_container.py +19 -0
  28. shinestacker/retouch/image_editor.py +1 -1
  29. shinestacker/retouch/image_editor_ui.py +2 -1
  30. shinestacker/retouch/image_viewer.py +104 -20
  31. shinestacker/retouch/io_gui_handler.py +17 -16
  32. shinestacker/retouch/io_manager.py +0 -1
  33. shinestacker/retouch/layer_collection.py +2 -1
  34. shinestacker/retouch/shortcuts_help.py +2 -13
  35. shinestacker/retouch/unsharp_mask_filter.py +3 -10
  36. shinestacker/retouch/white_balance_filter.py +5 -13
  37. {shinestacker-0.3.5.dist-info → shinestacker-0.4.0.dist-info}/METADATA +8 -11
  38. {shinestacker-0.3.5.dist-info → shinestacker-0.4.0.dist-info}/RECORD +42 -41
  39. shinestacker-0.4.0.dist-info/licenses/LICENSE +165 -0
  40. shinestacker/algorithms/core_utils.py +0 -22
  41. shinestacker-0.3.5.dist-info/licenses/LICENSE +0 -1
  42. {shinestacker-0.3.5.dist-info → shinestacker-0.4.0.dist-info}/WHEEL +0 -0
  43. {shinestacker-0.3.5.dist-info → shinestacker-0.4.0.dist-info}/entry_points.txt +0 -0
  44. {shinestacker-0.3.5.dist-info → shinestacker-0.4.0.dist-info}/top_level.txt +0 -0
shinestacker/_version.py CHANGED
@@ -1 +1 @@
1
- __version__ = '0.3.5'
1
+ __version__ = '0.4.0'
@@ -5,6 +5,7 @@ import matplotlib.pyplot as plt
5
5
  import cv2
6
6
  from .. config.constants import constants
7
7
  from .. core.exceptions import AlignmentError, InvalidOptionError
8
+ from .. core.colors import color_str
8
9
  from .utils import img_8bit, img_bw_8bit, save_plot
9
10
  from .utils import get_img_metadata, validate_image
10
11
  from .stack_framework import SubAction
@@ -33,7 +34,8 @@ _DEFAULT_ALIGNMENT_CONFIG = {
33
34
  'border_value': constants.DEFAULT_BORDER_VALUE,
34
35
  'border_blur': constants.DEFAULT_BORDER_BLUR,
35
36
  'subsample': constants.DEFAULT_ALIGN_SUBSAMPLE,
36
- 'fast_subsampling': constants.DEFAULT_ALIGN_FAST_SUBSAMPLING
37
+ 'fast_subsampling': constants.DEFAULT_ALIGN_FAST_SUBSAMPLING,
38
+ 'min_good_matches': constants.DEFAULT_ALIGN_MIN_GOOD_MATCHES
37
39
  }
38
40
 
39
41
 
@@ -164,21 +166,31 @@ def align_images(img_1, img_0, feature_config=None, matching_config=None, alignm
164
166
  if callbacks and 'message' in callbacks:
165
167
  callbacks['message']()
166
168
  subsample = alignment_config['subsample']
167
- if subsample > 1:
168
- if alignment_config['fast_subsampling']:
169
- img_0_sub, img_1_sub = img_0[::subsample, ::subsample], img_1[::subsample, ::subsample]
169
+ min_good_matches = alignment_config['min_good_matches']
170
+ while True:
171
+ if subsample > 1:
172
+ if alignment_config['fast_subsampling']:
173
+ img_0_sub, img_1_sub = \
174
+ img_0[::subsample, ::subsample], img_1[::subsample, ::subsample]
175
+ else:
176
+ img_0_sub = cv2.resize(img_0, (0, 0),
177
+ fx=1 / subsample, fy=1 / subsample,
178
+ interpolation=cv2.INTER_AREA)
179
+ img_1_sub = cv2.resize(img_1, (0, 0),
180
+ fx=1 / subsample, fy=1 / subsample,
181
+ interpolation=cv2.INTER_AREA)
170
182
  else:
171
- img_0_sub = cv2.resize(img_0, (0, 0),
172
- fx=1 / subsample, fy=1 / subsample,
173
- interpolation=cv2.INTER_AREA)
174
- img_1_sub = cv2.resize(img_1, (0, 0),
175
- fx=1 / subsample, fy=1 / subsample,
176
- interpolation=cv2.INTER_AREA)
177
- else:
178
- img_0_sub, img_1_sub = img_0, img_1
179
- kp_0, kp_1, good_matches = detect_and_compute(img_0_sub, img_1_sub,
180
- feature_config, matching_config)
181
- n_good_matches = len(good_matches)
183
+ img_0_sub, img_1_sub = img_0, img_1
184
+ kp_0, kp_1, good_matches = detect_and_compute(img_0_sub, img_1_sub,
185
+ feature_config, matching_config)
186
+ n_good_matches = len(good_matches)
187
+ if n_good_matches > min_good_matches or subsample == 1:
188
+ break
189
+ subsample = 1
190
+ if callbacks and 'warning' in callbacks:
191
+ callbacks['warning'](
192
+ f"only {n_good_matches} < {min_good_matches} matches found, "
193
+ "retrying without subsampling")
182
194
  if callbacks and 'matches_message' in callbacks:
183
195
  callbacks['matches_message'](n_good_matches)
184
196
  img_warp = None
@@ -277,14 +289,19 @@ class AlignFrames(SubAction):
277
289
  img_ref = self.process.img_ref(ref_idx)
278
290
  return self.align_images(idx, img_ref, img_0)
279
291
 
292
+ def sub_msg(self, msg, color=constants.LOG_COLOR_LEVEL_3):
293
+ self.process.sub_message_r(color_str(msg, color))
294
+
280
295
  def align_images(self, idx, img_1, img_0):
281
296
  idx_str = f"{idx:04d}"
282
297
  callbacks = {
283
- 'message': lambda: self.process.sub_message_r(': find matches'),
284
- 'matches_message': lambda n: self.process.sub_message_r(f": matches: {n}"),
285
- 'align_message': lambda: self.process.sub_message_r(': align images'),
286
- 'ecc_message': lambda: self.process.sub_message_r(": ecc refinement"),
287
- 'blur_message': lambda: self.process.sub_message_r(': blur borders'),
298
+ 'message': lambda: self.sub_msg(': find matches'),
299
+ 'matches_message': lambda n: self.sub_msg(f": good matches: {n}"),
300
+ 'align_message': lambda: self.sub_msg(': align images'),
301
+ 'ecc_message': lambda: self.sub_msg(": ecc refinement"),
302
+ 'blur_message': lambda: self.sub_msg(': blur borders'),
303
+ 'warning': lambda msg: self.sub_msg(
304
+ f': {msg}', constants.LOG_COLOR_ALERT),
288
305
  'save_plot': lambda plot_path: self.process.callback(
289
306
  'save_plot', self.process.id,
290
307
  f"{self.process.name}: matches\nframe {idx_str}", plot_path)
@@ -6,6 +6,7 @@ from scipy.optimize import bisect
6
6
  from scipy.interpolate import interp1d
7
7
  from .. config.constants import constants
8
8
  from .. core.exceptions import InvalidOptionError
9
+ from .. core.colors import color_str
9
10
  from .utils import read_img, save_plot
10
11
  from .stack_framework import SubAction
11
12
 
@@ -408,6 +409,6 @@ class BalanceFrames(SubAction):
408
409
 
409
410
  def run_frame(self, idx, _ref_idx, image):
410
411
  if idx != self.process.ref_idx:
411
- self.process.sub_message_r(': balance image')
412
+ self.process.sub_message_r(color_str(': balance image', constants.LOG_COLOR_LEVEL_3))
412
413
  image = self.correction.apply_correction(idx, image)
413
414
  return image
@@ -2,6 +2,7 @@
2
2
  import numpy as np
3
3
  from .. core.exceptions import InvalidOptionError, ImageLoadError
4
4
  from .. config.constants import constants
5
+ from .. core.colors import color_str
5
6
  from .utils import read_img, get_img_metadata, validate_image
6
7
 
7
8
 
@@ -27,7 +28,7 @@ class BaseStackAlgo:
27
28
  return self._steps_per_frame
28
29
 
29
30
  def print_message(self, msg):
30
- self.process.sub_message_r(msg)
31
+ self.process.sub_message_r(color_str(msg, constants.LOG_COLOR_LEVEL_3))
31
32
 
32
33
  def read_image_and_update_metadata(self, img_path, metadata):
33
34
  img = read_img(img_path)
@@ -176,7 +176,9 @@ class MultiLayer(JobBase, FrameMultiDirectory):
176
176
  else:
177
177
  raise RuntimeError("input_path option must contain a path or an array of paths")
178
178
  if len(paths) == 0:
179
- self.print_message(color_str("no input paths specified", "red"), level=logging.WARNING)
179
+ self.print_message(color_str("no input paths specified",
180
+ constants.LOG_COLOR_LEVEL_ALERT),
181
+ level=logging.WARNING)
180
182
  return
181
183
  files = self.folder_filelist()
182
184
  if len(files) == 0:
@@ -184,22 +186,23 @@ class MultiLayer(JobBase, FrameMultiDirectory):
184
186
  color_str(f"no input in {len(paths)} specified path" +
185
187
  ('s' if len(paths) > 1 else '') + ": "
186
188
  ", ".join([f"'{p}'" for p in paths]),
187
- "red"),
189
+ constants.LOG_COLOR_LEVEL_ALERT),
188
190
  level=logging.WARNING)
189
191
  return
190
- self.print_message(color_str("merging frames in " + self.folder_list_str(), "blue"))
192
+ self.print_message(color_str("merging frames in " + self.folder_list_str(),
193
+ constants.LOG_COLOR_LEVEL_2))
191
194
  input_files = [f"{self.working_path}/{f}" for f in files]
192
195
  self.print_message(
193
- color_str("frames: " + ", ".join([i.split("/")[-1] for i in files]), "blue"))
194
- self.print_message(
195
- color_str("reading files", "blue"))
196
+ color_str("frames: " + ", ".join([i.split("/")[-1] for i in files]),
197
+ constants.LOG_COLOR_LEVEL_2))
198
+ self.print_message(color_str("reading files", constants.LOG_COLOR_LEVEL_2))
196
199
  filename = ".".join(files[0].split("/")[-1].split(".")[:-1])
197
200
  output_file = f"{self.working_path}/{self.output_path}/{filename}.tif"
198
201
  callbacks = {
199
202
  'exif_msg': lambda path: self.print_message(
200
- color_str(f"copying exif data from path: {path}", "blue")),
203
+ color_str(f"copying exif data from path: {path}", constants.LOG_COLOR_LEVEL_2)),
201
204
  'write_msg': lambda path: self.print_message(
202
- color_str(f"writing multilayer tiff file: {path}", "blue"))
205
+ color_str(f"writing multilayer tiff file: {path}", constants.LOG_COLOR_LEVEL_2))
203
206
  }
204
207
  write_multilayer_tiff(input_files, output_file, labels=None, exif_path=self.exif_path,
205
208
  callbacks=callbacks)
@@ -73,7 +73,8 @@ class NoiseDetection(JobBase, FrameMultiDirectory):
73
73
 
74
74
  def run_core(self):
75
75
  self.print_message(color_str(
76
- f"map noisy pixels from frames in {self.folder_list_str()}", "blue"
76
+ f"map noisy pixels from frames in {self.folder_list_str()}",
77
+ constants.LOG_COLOR_LEVEL_2
77
78
  ))
78
79
  files = self.folder_filelist()
79
80
  in_paths = [self.working_path + "/" + f for f in files]
@@ -89,7 +90,7 @@ class NoiseDetection(JobBase, FrameMultiDirectory):
89
90
  mean_img = mean_image(
90
91
  file_paths=in_paths, max_frames=self.max_frames,
91
92
  message_callback=lambda path: self.print_message_r(
92
- color_str(f"reading frame: {path.split('/')[-1]}", "blue")
93
+ color_str(f"reading frame: {path.split('/')[-1]}", constants.LOG_COLOR_LEVEL_2)
93
94
  ),
94
95
  progress_callback=progress_callback)
95
96
  if not config.DISABLE_TQDM:
@@ -103,13 +104,16 @@ class NoiseDetection(JobBase, FrameMultiDirectory):
103
104
  hot_rgb = cv2.bitwise_or(hot_px[0], cv2.bitwise_or(hot_px[1], hot_px[2]))
104
105
  msg = []
105
106
  for ch, hot in zip(['rgb', *constants.RGB_LABELS], [hot_rgb] + hot_px):
106
- msg.append(f"{ch}: {np.count_nonzero(hot > 0)}")
107
- self.print_message("hot pixels: " + ", ".join(msg))
107
+ hpx = color_str(f"{ch}: {np.count_nonzero(hot > 0)}",
108
+ {'rgb': 'black', 'r': 'red', 'g': 'green', 'b': 'blue'}[ch])
109
+ msg.append(hpx)
110
+ self.print_message(color_str("hot pixels: " + ", ".join(msg), constants.LOG_COLOR_LEVEL_2))
108
111
  path = "/".join(self.file_name.split("/")[:-1])
109
112
  if not os.path.exists(f"{self.working_path}/{path}"):
110
113
  self.print_message(f"create directory: {path}")
111
114
  os.mkdir(f"{self.working_path}/{path}")
112
- self.print_message(f"writing hot pixels map file: {self.file_name}")
115
+ self.print_message(color_str(f"writing hot pixels map file: {self.file_name}",
116
+ constants.LOG_COLOR_LEVEL_2))
113
117
  cv2.imwrite(f"{self.working_path}/{self.file_name}", hot_rgb)
114
118
  plot_range = self.plot_range
115
119
  min_th, max_th = min(self.channel_thresholds), max(self.channel_thresholds)
@@ -155,7 +159,9 @@ class MaskNoise(SubAction):
155
159
  self.process = process
156
160
  path = f"{process.working_path}/{self.noise_mask}"
157
161
  if os.path.exists(path):
158
- self.process.sub_message_r(f': reading noisy pixel mask file: {self.noise_mask}')
162
+ self.process.sub_message_r(color_str(
163
+ f': reading noisy pixel mask file: {self.noise_mask}',
164
+ constants.LOG_COLOR_LEVEL_3))
159
165
  self.noise_mask_img = cv2.imread(path, cv2.IMREAD_GRAYSCALE)
160
166
  if self.noise_mask_img is None:
161
167
  raise ImageLoadError(path, f"failed to load image file {self.noise_mask}.")
@@ -163,7 +169,7 @@ class MaskNoise(SubAction):
163
169
  raise ImageLoadError(path, "file not found.")
164
170
 
165
171
  def run_frame(self, _idx, _ref_idx, image):
166
- self.process.sub_message_r(': mask noisy pixels')
172
+ self.process.sub_message_r(color_str(': mask noisy pixels', constants.LOG_COLOR_LEVEL_3))
167
173
  if len(image.shape) == 3:
168
174
  corrected = image.copy()
169
175
  for c in range(3):
@@ -12,7 +12,7 @@ class PyramidBase(BaseStackAlgo):
12
12
  kernel_size=constants.DEFAULT_PY_KERNEL_SIZE,
13
13
  gen_kernel=constants.DEFAULT_PY_GEN_KERNEL,
14
14
  float_type=constants.DEFAULT_PY_FLOAT):
15
- super().__init__("pyramid", 1, float_type)
15
+ super().__init__("pyramid", 2, float_type)
16
16
  self.min_size = min_size
17
17
  self.kernel_size = kernel_size
18
18
  self.pad_amount = (kernel_size - 1) // 2
@@ -151,11 +151,11 @@ class PyramidStack(PyramidBase):
151
151
  metadata = None
152
152
  all_laplacians = []
153
153
  levels = None
154
+ n = len(filenames)
154
155
  for i, img_path in enumerate(filenames):
155
156
  self.print_message(f": validating file {img_path.split('/')[-1]}")
156
157
 
157
158
  img, metadata, updated = self.read_image_and_update_metadata(img_path, metadata)
158
-
159
159
  if updated:
160
160
  self.dtype = metadata[1]
161
161
  self.num_pixel_values = constants.NUM_UINT8 \
@@ -163,14 +163,17 @@ class PyramidStack(PyramidBase):
163
163
  self.max_pixel_value = constants.MAX_UINT8 \
164
164
  if self.dtype == np.uint8 else constants.MAX_UINT16
165
165
  levels = int(np.log2(min(img.shape[:2]) / self.min_size))
166
-
167
166
  if self.do_step_callback:
168
167
  self.process.callback('after_step', self.process.id, self.process.name, i)
169
168
  if self.process.callback('check_running', self.process.id, self.process.name) is False:
170
169
  raise RunStopException(self.name)
171
- for img_path in filenames:
170
+ for i, img_path in enumerate(filenames):
172
171
  self.print_message(f": processing file {img_path.split('/')[-1]}")
173
172
  img = read_img(img_path)
174
173
  all_laplacians.append(self.process_single_image(img, levels))
174
+ if self.do_step_callback:
175
+ self.process.callback('after_step', self.process.id, self.process.name, i + n)
176
+ if self.process.callback('check_running', self.process.id, self.process.name) is False:
177
+ raise RunStopException(self.name)
175
178
  stacked_image = self.collapse(self.fuse_pyramids(all_laplacians))
176
179
  return stacked_image.astype(self.dtype)
@@ -3,6 +3,7 @@ import os
3
3
  import numpy as np
4
4
  from .. config.constants import constants
5
5
  from .. core.framework import JobBase
6
+ from .. core.colors import color_str
6
7
  from .. core.exceptions import InvalidOptionError
7
8
  from .utils import write_img
8
9
  from .stack_framework import FrameDirectory, ActionList
@@ -23,7 +24,7 @@ class FocusStackBase(JobBase, FrameDirectory):
23
24
  self.frame_count = -1
24
25
 
25
26
  def focus_stack(self, filenames):
26
- self.sub_message_r(': reading input files')
27
+ self.sub_message_r(color_str(': reading input files', constants.LOG_COLOR_LEVEL_3))
27
28
  img_files = sorted([os.path.join(self.input_full_path, name) for name in filenames])
28
29
  stacked_img = self.stack_algo.focus_stack(img_files)
29
30
  in_filename = filenames[0].split(".")
@@ -42,7 +43,7 @@ class FocusStackBase(JobBase, FrameDirectory):
42
43
  copy_exif_from_file_to_file(exif_filename, out_filename)
43
44
  self.sub_message_r(' ' * 60)
44
45
  if self.plot_stack:
45
- idx_str = f"{self.frame_count:04d}" if self.frame_count >= 0 else ''
46
+ idx_str = f"{self.frame_count + 1:04d}" if self.frame_count >= 0 else ''
46
47
  name = f"{self.name}: {self.stack_algo.name()}"
47
48
  if idx_str != '':
48
49
  name += f"\nbunch: {idx_str}"
@@ -91,9 +92,9 @@ class FocusStackBunch(ActionList, FocusStackBase):
91
92
  ActionList.end(self)
92
93
 
93
94
  def run_step(self):
94
- self.print_message_r(f"fusing bunch: {self.count}")
95
+ self.print_message_r(color_str(f"fusing bunch: {self.count + 1}/{self.counts}",
96
+ constants.LOG_COLOR_LEVEL_2))
95
97
  self.focus_stack(self._chunks[self.count - 1])
96
- self.callback('after_step', self.id, self.name, self.count)
97
98
 
98
99
 
99
100
  class FocusStack(FocusStackBase):
@@ -52,8 +52,7 @@ class FramePaths:
52
52
  self.filenames = self.folder_filelist()
53
53
  file_list = self.input_full_path.replace(self.working_path, '').lstrip('/')
54
54
  self.print_message(color_str(f": {len(self.filenames)} files in folder: {file_list}",
55
- 'blue'))
56
- self.print_message(color_str("focus stacking", 'blue'))
55
+ constants.LOG_COLOR_LEVEL_2))
57
56
 
58
57
  def init(self, job):
59
58
  if self.working_path == '':
@@ -213,14 +212,14 @@ class FramesRefActions(ActionList, FrameDirectory):
213
212
  pass
214
213
 
215
214
  def run_step(self):
216
- if self.count == 1:
215
+ if self.count == 0:
217
216
  self._idx = self.ref_idx if self.step_process else 0
218
217
  self._ref_idx = self.ref_idx
219
218
  self._idx_step = +1
220
219
  ll = len(self.filenames)
221
220
  self.print_message_r(
222
- color_str(f"step {self.count}/{ll}: process file: {self.filenames[self._idx]}, "
223
- f"reference: {self.filenames[self._ref_idx]}", "blue"))
221
+ color_str(f"step {self.count + 1}/{ll}: process file: {self.filenames[self._idx]}, "
222
+ f"reference: {self.filenames[self._ref_idx]}", constants.LOG_COLOR_LEVEL_2))
224
223
  self.run_frame(self._idx, self._ref_idx)
225
224
  if self._idx < ll:
226
225
  if self.step_process:
@@ -269,7 +268,7 @@ class CombinedActions(FramesRefActions):
269
268
 
270
269
  def run_frame(self, idx, ref_idx):
271
270
  filename = self.filenames[idx]
272
- self.sub_message_r(': read input image')
271
+ self.sub_message_r(color_str(': read input image', constants.LOG_COLOR_LEVEL_3))
273
272
  img = read_img(f"{self.input_full_path}/{filename}")
274
273
  if self.dtype is not None and img.dtype != self.dtype:
275
274
  raise BitDepthError(self.dtype, img.dtype, )
@@ -278,7 +277,8 @@ class CombinedActions(FramesRefActions):
278
277
  if img is None:
279
278
  raise RuntimeError(f"Invalid file: {self.input_full_path}/{filename}")
280
279
  if len(self._actions) == 0:
281
- self.sub_message(color_str(": no actions specified.", "red"), level=logging.WARNING)
280
+ self.sub_message(color_str(": no actions specified.", constants.LOG_COLOR_ALERT),
281
+ level=logging.WARNING)
282
282
  for a in self._actions:
283
283
  if not a.enabled:
284
284
  self.get_logger().warning(color_str(f"{self.base_message}: sub-action disabled",
@@ -287,12 +287,14 @@ class CombinedActions(FramesRefActions):
287
287
  if self.callback('check_running', self.id, self.name) is False:
288
288
  raise RunStopException(self.name)
289
289
  img = a.run_frame(idx, ref_idx, img)
290
- self.sub_message_r(': write output image')
290
+ self.sub_message_r(color_str(': write output image', constants.LOG_COLOR_LEVEL_3))
291
291
  if img is not None:
292
292
  write_img(self.output_dir + "/" + filename, img)
293
293
  else:
294
- self.print_message("No output file resulted from processing input file: "
295
- f"{self.input_full_path}/{filename}", level=logging.WARNING)
294
+ self.print_message(color_str(
295
+ "No output file resulted from processing input file: "
296
+ f"{self.input_full_path}/{filename}",
297
+ constants.LOG_COLOR_ALERT), level=logging.WARNING)
296
298
 
297
299
  def end(self):
298
300
  for a in self._actions:
@@ -1,28 +1,15 @@
1
1
  # pylint: disable=C0114, C0115, C0116, C0103, W0201
2
- class _AppConfig:
3
- _initialized = False
4
- _instance = None
2
+ from .. config.config import _ConfigBase
5
3
 
4
+
5
+ class _AppConfig(_ConfigBase):
6
6
  def __new__(cls):
7
- if cls._instance is None:
8
- cls._instance = super().__new__(cls)
9
- cls._instance._init_defaults()
10
- return cls._instance
7
+ return _ConfigBase.__new__(cls)
11
8
 
12
9
  def _init_defaults(self):
13
10
  self._DONT_USE_NATIVE_MENU = True
14
11
  self._COMBINED_APP = False
15
12
 
16
- def init(self, **kwargs):
17
- if self._initialized:
18
- raise RuntimeError("Config already initialized")
19
- for k, v in kwargs.items():
20
- if hasattr(self, f"_{k}"):
21
- setattr(self, f"_{k}", v)
22
- else:
23
- raise AttributeError(f"Invalid config key: {k}")
24
- self._initialized = True
25
-
26
13
  @property
27
14
  def DONT_USE_NATIVE_MENU(self):
28
15
  return self._DONT_USE_NATIVE_MENU
@@ -31,10 +18,5 @@ class _AppConfig:
31
18
  def COMBINED_APP(self):
32
19
  return self._COMBINED_APP
33
20
 
34
- def __setattr__(self, name, value):
35
- if self._initialized and name.startswith('_'):
36
- raise AttributeError("Can't change config after initialization")
37
- super().__setattr__(name, value)
38
-
39
21
 
40
22
  app_config = _AppConfig()
shinestacker/app/main.py CHANGED
@@ -89,7 +89,7 @@ class MainApp(QMainWindow):
89
89
  if isinstance(filename, list):
90
90
  open_frames(self.retouch_window, None, ";".join(filename))
91
91
  else:
92
- self.retouch_window.open_file(filename)
92
+ self.retouch_window.io_gui_handler.open_file(filename)
93
93
 
94
94
 
95
95
  class Application(QApplication):
@@ -1,5 +1,5 @@
1
1
  # pylint: disable=C0114, C0115, C0116, C0103, R0903, W0718, W0104, W0201, E0602
2
- class _Config:
2
+ class _ConfigBase:
3
3
  _initialized = False
4
4
  _instance = None
5
5
 
@@ -9,16 +9,6 @@ class _Config:
9
9
  cls._instance._init_defaults()
10
10
  return cls._instance
11
11
 
12
- def _init_defaults(self):
13
- self._DISABLE_TQDM = False
14
- self._COMBINED_APP = False
15
- self._DONT_USE_NATIVE_MENU = True
16
- try:
17
- __IPYTHON__ # noqa
18
- self._JUPYTER_NOTEBOOK = True
19
- except Exception:
20
- self._JUPYTER_NOTEBOOK = False
21
-
22
12
  def init(self, **kwargs):
23
13
  if self._initialized:
24
14
  raise RuntimeError("Config already initialized")
@@ -29,6 +19,27 @@ class _Config:
29
19
  raise AttributeError(f"Invalid config key: {k}")
30
20
  self._initialized = True
31
21
 
22
+ def __setattr__(self, name, value):
23
+ if self._initialized and name.startswith('_'):
24
+ raise AttributeError("Can't change config after initialization")
25
+ super().__setattr__(name, value)
26
+
27
+
28
+ class _Config(_ConfigBase):
29
+
30
+ def __new__(cls):
31
+ return _ConfigBase.__new__(cls)
32
+
33
+ def _init_defaults(self):
34
+ self._DISABLE_TQDM = False
35
+ self._COMBINED_APP = False
36
+ self._DONT_USE_NATIVE_MENU = True
37
+ try:
38
+ __IPYTHON__ # noqa
39
+ self._JUPYTER_NOTEBOOK = True
40
+ except Exception:
41
+ self._JUPYTER_NOTEBOOK = False
42
+
32
43
  @property
33
44
  def DISABLE_TQDM(self):
34
45
  return self._DISABLE_TQDM
@@ -45,10 +56,5 @@ class _Config:
45
56
  def COMBINED_APP(self):
46
57
  return self._COMBINED_APP
47
58
 
48
- def __setattr__(self, name, value):
49
- if self._initialized and name.startswith('_'):
50
- raise AttributeError("Can't change config after initialization")
51
- super().__setattr__(name, value)
52
-
53
59
 
54
60
  config = _Config()
@@ -41,6 +41,12 @@ class _Constants:
41
41
 
42
42
  PATH_SEPARATOR = ';'
43
43
 
44
+ LOG_COLOR_ALERT = 'red'
45
+ LOG_COLOR_LEVEL_JOB = 'green'
46
+ LOG_COLOR_LEVEL_1 = 'blue'
47
+ LOG_COLOR_LEVEL_2 = 'magenta'
48
+ LOG_COLOR_LEVEL_3 = 'cyan'
49
+
44
50
  DEFAULT_FILE_REVERSE_ORDER = False
45
51
  DEFAULT_MULTILAYER_FILE_REVERSE_ORDER = True
46
52
 
@@ -99,8 +105,9 @@ class _Constants:
99
105
  DEFAULT_ALIGN_MAX_ITERS = 2000
100
106
  DEFAULT_BORDER_VALUE = [0] * 4
101
107
  DEFAULT_BORDER_BLUR = 50
102
- DEFAULT_ALIGN_SUBSAMPLE = 1
108
+ DEFAULT_ALIGN_SUBSAMPLE = 2
103
109
  DEFAULT_ALIGN_FAST_SUBSAMPLING = False
110
+ DEFAULT_ALIGN_MIN_GOOD_MATCHES = 100
104
111
 
105
112
  BALANCE_LINEAR = "LINEAR"
106
113
  BALANCE_GAMMA = "GAMMA"
@@ -1,6 +1,7 @@
1
1
  # pylint: disable=C0114, C0115, C0116, R0917, R0913, R0902
2
2
  import time
3
3
  import logging
4
+ from .. config.constants import constants
4
5
  from .. config.config import config
5
6
  from .colors import color_str
6
7
  from .logging import setup_logging
@@ -89,13 +90,15 @@ class JobBase:
89
90
  def run(self):
90
91
  self._t0 = time.time()
91
92
  if not self.enabled:
92
- self.get_logger().warning(color_str(self.name + ": entire job disabled", 'red'))
93
+ self.get_logger().warning(color_str(self.name + ": entire job disabled",
94
+ constants.LOG_COLOR_ALERT))
93
95
  self.callback('before_action', self.id, self.name)
94
96
  self.run_core()
95
97
  self.callback('after_action', self.id, self.name)
96
- msg_name = color_str(self.name + ":", "green", "bold")
97
- msg_time = color_str(f"elapsed time: {elapsed_time_str(self._t0)}", "green")
98
- msg_completed = color_str("completed", "green")
98
+ msg_name = color_str(self.name + ":", constants.LOG_COLOR_LEVEL_JOB, "bold")
99
+ msg_time = color_str(f"elapsed time: {elapsed_time_str(self._t0)}",
100
+ constants.LOG_COLOR_LEVEL_JOB)
101
+ msg_completed = color_str("completed", constants.LOG_COLOR_LEVEL_JOB)
99
102
  self.get_logger().info(msg=f"{msg_name} {msg_time}{TRAILING_SPACES}")
100
103
  self.get_logger().info(msg=f"{msg_name} {msg_completed}{TRAILING_SPACES}")
101
104
 
@@ -115,13 +118,14 @@ class JobBase:
115
118
  def print_message(self, msg='', level=logging.INFO, end=None, begin='', tqdm=False):
116
119
  if config.DISABLE_TQDM:
117
120
  tqdm = False
118
- self.base_message = color_str(self.name, "blue", "bold")
121
+ self.base_message = color_str(self.name, constants.LOG_COLOR_LEVEL_1, "bold")
119
122
  if msg != '':
120
123
  self.base_message += (': ' + msg)
121
124
  self.set_terminator(tqdm, end)
125
+ col_str = color_str(self.base_message, constants.LOG_COLOR_LEVEL_1, "bold")
122
126
  self.get_logger(tqdm).log(
123
127
  level=level,
124
- msg=f"{begin}{color_str(self.base_message, 'blue', 'bold')}{TRAILING_SPACES}"
128
+ msg=f"{begin}{col_str}{TRAILING_SPACES}"
125
129
  )
126
130
  self.set_terminator(tqdm)
127
131
 
@@ -176,7 +180,8 @@ class Job(JobBase):
176
180
  if not self.enabled:
177
181
  z.append("job")
178
182
  msg = " and ".join(z)
179
- self.get_logger().warning(color_str(a.name + f": {msg} disabled", 'red'))
183
+ self.get_logger().warning(color_str(a.name + f": {msg} disabled",
184
+ constants.LOG_COLOR_ALERT))
180
185
  else:
181
186
  if self.callback('check_running', self.id, self.name) is False:
182
187
  raise RunStopException(self.name)
@@ -200,14 +205,14 @@ class ActionList(JobBase):
200
205
  self.callback('end_steps', self.id, self.name)
201
206
 
202
207
  def __iter__(self):
203
- self.count = 1
208
+ self.count = 0
204
209
  return self
205
210
 
206
211
  def run_step(self):
207
212
  pass
208
213
 
209
214
  def __next__(self):
210
- if self.count <= self.counts:
215
+ if self.count < self.counts:
211
216
  self.run_step()
212
217
  x = self.count
213
218
  self.count += 1
@@ -215,7 +220,7 @@ class ActionList(JobBase):
215
220
  raise StopIteration
216
221
 
217
222
  def run_core(self):
218
- self.print_message('begin run', end='\n')
223
+ self.print_message(color_str('begin run', constants.LOG_COLOR_LEVEL_2), end='\n')
219
224
  self.begin()
220
225
  for _ in iter(self):
221
226
  self.callback('after_step', self.id, self.name, self.count)