shinestacker 0.3.6__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 (32) 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/stack.py +5 -4
  8. shinestacker/algorithms/stack_framework.py +12 -10
  9. shinestacker/app/main.py +1 -1
  10. shinestacker/config/config.py +1 -0
  11. shinestacker/config/constants.py +8 -1
  12. shinestacker/core/framework.py +15 -10
  13. shinestacker/gui/action_config.py +11 -7
  14. shinestacker/gui/gui_logging.py +8 -7
  15. shinestacker/gui/gui_run.py +8 -8
  16. shinestacker/gui/main_window.py +4 -4
  17. shinestacker/gui/new_project.py +31 -17
  18. shinestacker/gui/project_converter.py +0 -1
  19. shinestacker/gui/select_path_widget.py +3 -1
  20. shinestacker/retouch/image_editor.py +1 -1
  21. shinestacker/retouch/image_editor_ui.py +2 -1
  22. shinestacker/retouch/image_viewer.py +104 -20
  23. shinestacker/retouch/io_gui_handler.py +17 -16
  24. shinestacker/retouch/io_manager.py +0 -1
  25. shinestacker/retouch/layer_collection.py +2 -1
  26. {shinestacker-0.3.6.dist-info → shinestacker-0.4.0.dist-info}/METADATA +5 -4
  27. {shinestacker-0.3.6.dist-info → shinestacker-0.4.0.dist-info}/RECORD +31 -31
  28. shinestacker-0.4.0.dist-info/licenses/LICENSE +165 -0
  29. shinestacker-0.3.6.dist-info/licenses/LICENSE +0 -1
  30. {shinestacker-0.3.6.dist-info → shinestacker-0.4.0.dist-info}/WHEEL +0 -0
  31. {shinestacker-0.3.6.dist-info → shinestacker-0.4.0.dist-info}/entry_points.txt +0 -0
  32. {shinestacker-0.3.6.dist-info → shinestacker-0.4.0.dist-info}/top_level.txt +0 -0
shinestacker/_version.py CHANGED
@@ -1 +1 @@
1
- __version__ = '0.3.6'
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):
@@ -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:
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):
@@ -24,6 +24,7 @@ class _ConfigBase:
24
24
  raise AttributeError("Can't change config after initialization")
25
25
  super().__setattr__(name, value)
26
26
 
27
+
27
28
  class _Config(_ConfigBase):
28
29
 
29
30
  def __new__(cls):
@@ -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)
@@ -206,7 +206,7 @@ class FieldBuilder:
206
206
  self.action.params.get(tag, ''),
207
207
  kwargs.get('placeholder', ''),
208
208
  tag.replace('_', ' ')
209
- )
209
+ )[1]
210
210
 
211
211
  def create_rel_path_field(self, tag, **kwargs):
212
212
  value = self.action.params.get(tag, kwargs.get('default', ''))
@@ -316,7 +316,6 @@ class FieldBuilder:
316
316
  QMessageBox.warning(None, "Error", "Could not compute relative path")
317
317
  return create_layout_widget_and_connect(button, edit, browse)
318
318
 
319
-
320
319
  def create_float_field(self, tag, default=0.0, min_val=0.0, max_val=1.0,
321
320
  step=0.1, decimals=2):
322
321
  spin = QDoubleSpinBox()
@@ -593,7 +592,8 @@ class FocusStackConfigurator(FocusStackBaseConfigurator):
593
592
  self.builder.add_field('exif_path', FIELD_REL_PATH, 'Exif data path', required=False,
594
593
  placeholder='relative to working path')
595
594
  self.builder.add_field('prefix', FIELD_TEXT, 'Ouptut filename prefix', required=False,
596
- default=constants.DEFAULT_STACK_PREFIX, placeholder="_stack")
595
+ default=constants.DEFAULT_STACK_PREFIX,
596
+ placeholder=constants.DEFAULT_STACK_PREFIX)
597
597
  self.builder.add_field('plot_stack', FIELD_BOOL, 'Plot stack', required=False,
598
598
  default=constants.DEFAULT_PLOT_STACK)
599
599
  super().common_fields(layout)
@@ -616,10 +616,11 @@ class MultiLayerConfigurator(DefaultActionConfigurator):
616
616
  super().create_form(layout, action)
617
617
  if self.expert:
618
618
  self.builder.add_field('working_path', FIELD_ABS_PATH, 'Working path', required=False)
619
- self.builder.add_field('input_path', FIELD_REL_PATH,
620
- f'Input path (separate by {constants.PATH_SEPARATOR})',
621
- required=False, multiple_entries=True,
622
- placeholder='relative to working path')
619
+ self.builder.add_field('input_path', FIELD_REL_PATH,
620
+ f'Input path (separate by {constants.PATH_SEPARATOR})',
621
+ required=False, multiple_entries=True,
622
+ placeholder='relative to working path')
623
+ if self.expert:
623
624
  self.builder.add_field('output_path', FIELD_REL_PATH, 'Output path', required=False,
624
625
  placeholder='relative to working path')
625
626
  self.builder.add_field('exif_path', FIELD_REL_PATH, 'Exif data path', required=False,
@@ -805,6 +806,9 @@ class AlignFramesConfigurator(DefaultActionConfigurator):
805
806
  rans_threshold = self.builder.add_field(
806
807
  'rans_threshold', FIELD_FLOAT, 'RANSAC threshold (px)', required=False,
807
808
  default=constants.DEFAULT_RANS_THRESHOLD, min_val=0, max_val=20, step=0.1)
809
+ self.builder.add_field(
810
+ 'min_good_matches', FIELD_INT, "Min. good matches", required=False,
811
+ default=constants.DEFAULT_ALIGN_MIN_GOOD_MATCHES, min_val=0, max_val=500)
808
812
 
809
813
  def change_method():
810
814
  text = method.currentText()
@@ -17,6 +17,7 @@ class SimpleHtmlFormatter(logging.Formatter):
17
17
  FF = '80'
18
18
  OO = '00'
19
19
  MM = '40'
20
+ GG = 'FF'
20
21
  ANSI_COLORS = {
21
22
  # Reset
22
23
  '\x1b[0m': '</span>',
@@ -32,13 +33,13 @@ class SimpleHtmlFormatter(logging.Formatter):
32
33
  '\x1b[37m': f'<span style="color:#{FF}{FF}{FF}">', # white
33
34
  # Brilliant colors (90-97)
34
35
  '\x1b[90m': f'<span style="color:#{MM}{MM}{MM}">',
35
- '\x1b[91m': f'<span style="color:#{FF}{MM}{MM}">',
36
- '\x1b[92m': f'<span style="color:#{MM}{FF}{MM}">',
37
- '\x1b[93m': f'<span style="color:#{FF}{FF}{MM}">',
38
- '\x1b[94m': f'<span style="color:#{MM}{MM}{FF}">',
39
- '\x1b[95m': f'<span style="color:#{FF}{MM}{FF}">',
40
- '\x1b[96m': f'<span style="color:#{MM}{FF}{FF}">',
41
- '\x1b[97m': f'<span style="color:#{FF}{FF}{FF}">',
36
+ '\x1b[91m': f'<span style="color:#{GG}{MM}{MM}">',
37
+ '\x1b[92m': f'<span style="color:#{MM}{GG}{MM}">',
38
+ '\x1b[93m': f'<span style="color:#{GG}{GG}{MM}">',
39
+ '\x1b[94m': f'<span style="color:#{MM}{MM}{GG}">',
40
+ '\x1b[95m': f'<span style="color:#{GG}{MM}{GG}">',
41
+ '\x1b[96m': f'<span style="color:#{MM}{GG}{GG}">',
42
+ '\x1b[97m': f'<span style="color:#{GG}{GG}{GG}">',
42
43
  # Background (40-47)
43
44
  '\x1b[40m': f'<span style="background-color:#{OO}{OO}{OO}">',
44
45
  '\x1b[41m': f'<span style="background-color:#{FF}{OO}{OO}">',
@@ -13,6 +13,10 @@ from .gui_images import GuiPdfView, GuiImageView, GuiOpenApp
13
13
  from .colors import ColorPalette
14
14
 
15
15
 
16
+ ACTION_RUNNING_COLOR = ColorPalette.MEDIUM_BLUE
17
+ ACTION_DONE_COLOR = ColorPalette.MEDIUM_GREEN
18
+
19
+
16
20
  class ColorButton(QPushButton):
17
21
  def __init__(self, text, enabled, parent=None):
18
22
  super().__init__(text.replace(gui_constants.DISABLED_TAG, ''), parent)
@@ -36,10 +40,6 @@ class ColorButton(QPushButton):
36
40
  """)
37
41
 
38
42
 
39
- action_running_color = ColorPalette.MEDIUM_BLUE
40
- action_done_color = ColorPalette.MEDIUM_GREEN
41
-
42
-
43
43
  class TimerProgressBar(QProgressBar):
44
44
  light_background_color = ColorPalette.LIGHT_BLUE
45
45
  border_color = ColorPalette.DARK_BLUE
@@ -123,10 +123,10 @@ class TimerProgressBar(QProgressBar):
123
123
  # pylint: enable=C0103
124
124
 
125
125
  def set_running_style(self):
126
- self.set_style(action_running_color)
126
+ self.set_style(ACTION_RUNNING_COLOR)
127
127
 
128
128
  def set_done_style(self):
129
- self.set_style(action_done_color)
129
+ self.set_style(ACTION_DONE_COLOR)
130
130
 
131
131
 
132
132
  class RunWindow(QTextEditLogger):
@@ -246,7 +246,7 @@ class RunWindow(QTextEditLogger):
246
246
  @Slot(int, str)
247
247
  def handle_before_action(self, run_id, _name):
248
248
  if 0 <= run_id < len(self.color_widgets[self.row_widget_id]):
249
- self.color_widgets[self.row_widget_id][run_id].set_color(*action_running_color.tuple())
249
+ self.color_widgets[self.row_widget_id][run_id].set_color(*ACTION_RUNNING_COLOR.tuple())
250
250
  self.progress_bar.start(1)
251
251
  if run_id == -1:
252
252
  self.progress_bar.set_running_style()
@@ -254,7 +254,7 @@ class RunWindow(QTextEditLogger):
254
254
  @Slot(int, str)
255
255
  def handle_after_action(self, run_id, _name):
256
256
  if 0 <= run_id < len(self.color_widgets[self.row_widget_id]):
257
- self.color_widgets[self.row_widget_id][run_id].set_color(*action_done_color.tuple())
257
+ self.color_widgets[self.row_widget_id][run_id].set_color(*ACTION_DONE_COLOR.tuple())
258
258
  self.progress_bar.stop()
259
259
  if run_id == -1:
260
260
  self.row_widget_id += 1
@@ -163,11 +163,11 @@ class MainWindow(ActionsWindow, LogManager):
163
163
  self.action_list.itemDoubleClicked.connect(self.on_action_edit)
164
164
  vbox_left = QVBoxLayout()
165
165
  vbox_left.setSpacing(4)
166
- vbox_left.addWidget(QLabel("Jobs"))
166
+ vbox_left.addWidget(QLabel("Job"))
167
167
  vbox_left.addWidget(self.job_list)
168
168
  vbox_right = QVBoxLayout()
169
169
  vbox_right.setSpacing(4)
170
- vbox_right.addWidget(QLabel("Actions"))
170
+ vbox_right.addWidget(QLabel("Action"))
171
171
  vbox_right.addWidget(self.action_list)
172
172
  self.job_list.itemSelectionChanged.connect(self.update_delete_action_state)
173
173
  self.action_list.itemSelectionChanged.connect(self.update_delete_action_state)
@@ -615,7 +615,7 @@ class MainWindow(ActionsWindow, LogManager):
615
615
  labels = [[(self.action_text(a), a.enabled()) for a in job.sub_actions]]
616
616
  r = self.get_retouch_path(job)
617
617
  retouch_paths = [] if len(r) == 0 else [(job_name, r)]
618
- new_window, id_str = self.create_new_window("Job: " + job_name,
618
+ new_window, id_str = self.create_new_window(f"{job_name} [⚙️ Job]",
619
619
  labels, retouch_paths)
620
620
  worker = JobLogWorker(job, id_str)
621
621
  self.connect_signals(worker, new_window)
@@ -638,7 +638,7 @@ class MainWindow(ActionsWindow, LogManager):
638
638
  r = self.get_retouch_path(job)
639
639
  if len(r) > 0:
640
640
  retouch_paths.append((job.params["name"], r))
641
- new_window, id_str = self.create_new_window("Project: " + project_name,
641
+ new_window, id_str = self.create_new_window(f"{project_name} [Project 📚]",
642
642
  labels, retouch_paths)
643
643
  worker = ProjectLogWorker(self.project, id_str)
644
644
  self.connect_signals(worker, new_window)