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.
- shinestacker/_version.py +1 -1
- shinestacker/algorithms/align.py +37 -20
- shinestacker/algorithms/balance.py +2 -1
- shinestacker/algorithms/base_stack_algo.py +2 -1
- shinestacker/algorithms/multilayer.py +11 -8
- shinestacker/algorithms/noise_detection.py +13 -7
- shinestacker/algorithms/pyramid.py +7 -4
- shinestacker/algorithms/stack.py +5 -4
- shinestacker/algorithms/stack_framework.py +12 -10
- shinestacker/app/app_config.py +4 -22
- shinestacker/app/main.py +1 -1
- shinestacker/config/config.py +22 -16
- shinestacker/config/constants.py +8 -1
- shinestacker/core/framework.py +15 -10
- shinestacker/gui/action_config.py +20 -41
- shinestacker/gui/actions_window.py +18 -47
- shinestacker/gui/gui_logging.py +8 -7
- shinestacker/gui/gui_run.py +8 -8
- shinestacker/gui/main_window.py +4 -4
- shinestacker/gui/new_project.py +34 -37
- shinestacker/gui/project_converter.py +0 -1
- shinestacker/gui/project_editor.py +43 -20
- shinestacker/gui/select_path_widget.py +32 -0
- shinestacker/retouch/base_filter.py +12 -1
- shinestacker/retouch/denoise_filter.py +4 -10
- shinestacker/retouch/exif_data.py +3 -9
- shinestacker/retouch/icon_container.py +19 -0
- shinestacker/retouch/image_editor.py +1 -1
- shinestacker/retouch/image_editor_ui.py +2 -1
- shinestacker/retouch/image_viewer.py +104 -20
- shinestacker/retouch/io_gui_handler.py +17 -16
- shinestacker/retouch/io_manager.py +0 -1
- shinestacker/retouch/layer_collection.py +2 -1
- shinestacker/retouch/shortcuts_help.py +2 -13
- shinestacker/retouch/unsharp_mask_filter.py +3 -10
- shinestacker/retouch/white_balance_filter.py +5 -13
- {shinestacker-0.3.5.dist-info → shinestacker-0.4.0.dist-info}/METADATA +8 -11
- {shinestacker-0.3.5.dist-info → shinestacker-0.4.0.dist-info}/RECORD +42 -41
- shinestacker-0.4.0.dist-info/licenses/LICENSE +165 -0
- shinestacker/algorithms/core_utils.py +0 -22
- shinestacker-0.3.5.dist-info/licenses/LICENSE +0 -1
- {shinestacker-0.3.5.dist-info → shinestacker-0.4.0.dist-info}/WHEEL +0 -0
- {shinestacker-0.3.5.dist-info → shinestacker-0.4.0.dist-info}/entry_points.txt +0 -0
- {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.
|
|
1
|
+
__version__ = '0.4.0'
|
shinestacker/algorithms/align.py
CHANGED
|
@@ -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
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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 =
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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.
|
|
284
|
-
'matches_message': lambda n: self.
|
|
285
|
-
'align_message': lambda: self.
|
|
286
|
-
'ecc_message': lambda: self.
|
|
287
|
-
'blur_message': lambda: self.
|
|
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",
|
|
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
|
-
|
|
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(),
|
|
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]),
|
|
194
|
-
|
|
195
|
-
|
|
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}",
|
|
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}",
|
|
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()}",
|
|
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]}",
|
|
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
|
-
|
|
107
|
-
|
|
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(
|
|
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",
|
|
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)
|
shinestacker/algorithms/stack.py
CHANGED
|
@@ -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
|
-
|
|
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 ==
|
|
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]}",
|
|
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.",
|
|
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(
|
|
295
|
-
|
|
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/app_config.py
CHANGED
|
@@ -1,28 +1,15 @@
|
|
|
1
1
|
# pylint: disable=C0114, C0115, C0116, C0103, W0201
|
|
2
|
-
|
|
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
|
-
|
|
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):
|
shinestacker/config/config.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# pylint: disable=C0114, C0115, C0116, C0103, R0903, W0718, W0104, W0201, E0602
|
|
2
|
-
class
|
|
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()
|
shinestacker/config/constants.py
CHANGED
|
@@ -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 =
|
|
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"
|
shinestacker/core/framework.py
CHANGED
|
@@ -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",
|
|
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 + ":",
|
|
97
|
-
msg_time = color_str(f"elapsed time: {elapsed_time_str(self._t0)}",
|
|
98
|
-
|
|
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,
|
|
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}{
|
|
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",
|
|
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 =
|
|
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
|
|
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)
|