shinestacker 1.2.0__py3-none-any.whl → 1.2.1__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 +34 -33
- shinestacker/algorithms/balance.py +11 -12
- shinestacker/algorithms/multilayer.py +10 -11
- shinestacker/algorithms/noise_detection.py +6 -7
- shinestacker/algorithms/stack.py +17 -17
- shinestacker/algorithms/stack_framework.py +125 -154
- shinestacker/algorithms/vignetting.py +3 -3
- shinestacker/config/constants.py +3 -1
- shinestacker/core/framework.py +12 -12
- shinestacker/gui/action_config.py +53 -0
- shinestacker/gui/action_config_dialog.py +4 -5
- shinestacker/gui/gui_images.py +10 -10
- shinestacker/gui/gui_run.py +1 -1
- shinestacker/gui/main_window.py +4 -4
- shinestacker/gui/menu_manager.py +4 -0
- shinestacker/gui/new_project.py +0 -1
- shinestacker/gui/project_controller.py +4 -4
- shinestacker/gui/project_editor.py +35 -26
- shinestacker/gui/tab_widget.py +3 -3
- {shinestacker-1.2.0.dist-info → shinestacker-1.2.1.dist-info}/METADATA +1 -1
- {shinestacker-1.2.0.dist-info → shinestacker-1.2.1.dist-info}/RECORD +26 -26
- {shinestacker-1.2.0.dist-info → shinestacker-1.2.1.dist-info}/WHEEL +0 -0
- {shinestacker-1.2.0.dist-info → shinestacker-1.2.1.dist-info}/entry_points.txt +0 -0
- {shinestacker-1.2.0.dist-info → shinestacker-1.2.1.dist-info}/licenses/LICENSE +0 -0
- {shinestacker-1.2.0.dist-info → shinestacker-1.2.1.dist-info}/top_level.txt +0 -0
shinestacker/_version.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = '1.2.
|
|
1
|
+
__version__ = '1.2.1'
|
shinestacker/algorithms/align.py
CHANGED
|
@@ -5,7 +5,7 @@ import numpy as np
|
|
|
5
5
|
import matplotlib.pyplot as plt
|
|
6
6
|
import cv2
|
|
7
7
|
from .. config.constants import constants
|
|
8
|
-
from .. core.exceptions import
|
|
8
|
+
from .. core.exceptions import InvalidOptionError
|
|
9
9
|
from .. core.colors import color_str
|
|
10
10
|
from .utils import img_8bit, img_bw_8bit, save_plot, img_subsample
|
|
11
11
|
from .stack_framework import SubAction
|
|
@@ -130,7 +130,7 @@ def check_homography_distortion(m, img_shape, homography_thresholds=_HOMOGRAPHY_
|
|
|
130
130
|
return True, "Transformation within acceptable limits"
|
|
131
131
|
|
|
132
132
|
|
|
133
|
-
def get_good_matches(des_0,
|
|
133
|
+
def get_good_matches(des_0, des_ref, matching_config=None):
|
|
134
134
|
matching_config = {**_DEFAULT_MATCHING_CONFIG, **(matching_config or {})}
|
|
135
135
|
match_method = matching_config['match_method']
|
|
136
136
|
good_matches = []
|
|
@@ -139,12 +139,12 @@ def get_good_matches(des_0, des_1, matching_config=None):
|
|
|
139
139
|
{'algorithm': matching_config['flann_idx_kdtree'],
|
|
140
140
|
'trees': matching_config['flann_trees']},
|
|
141
141
|
{'checks': matching_config['flann_checks']})
|
|
142
|
-
matches = flann.knnMatch(des_0,
|
|
142
|
+
matches = flann.knnMatch(des_0, des_ref, k=2)
|
|
143
143
|
good_matches = [m for m, n in matches
|
|
144
144
|
if m.distance < matching_config['threshold'] * n.distance]
|
|
145
145
|
elif match_method == constants.MATCHING_NORM_HAMMING:
|
|
146
146
|
bf = cv2.BFMatcher(cv2.NORM_HAMMING, crossCheck=True)
|
|
147
|
-
good_matches = sorted(bf.match(des_0,
|
|
147
|
+
good_matches = sorted(bf.match(des_0, des_ref), key=lambda x: x.distance)
|
|
148
148
|
else:
|
|
149
149
|
raise InvalidOptionError(
|
|
150
150
|
'match_method', match_method,
|
|
@@ -172,14 +172,14 @@ def validate_align_config(detector, descriptor, match_method):
|
|
|
172
172
|
" require matching method Hamming distance")
|
|
173
173
|
|
|
174
174
|
|
|
175
|
-
def detect_and_compute(img_0,
|
|
175
|
+
def detect_and_compute(img_0, img_ref, feature_config=None, matching_config=None):
|
|
176
176
|
feature_config = {**_DEFAULT_FEATURE_CONFIG, **(feature_config or {})}
|
|
177
177
|
matching_config = {**_DEFAULT_MATCHING_CONFIG, **(matching_config or {})}
|
|
178
178
|
feature_config_detector = feature_config['detector']
|
|
179
179
|
feature_config_descriptor = feature_config['descriptor']
|
|
180
180
|
match_method = matching_config['match_method']
|
|
181
181
|
validate_align_config(feature_config_detector, feature_config_descriptor, match_method)
|
|
182
|
-
img_bw_0,
|
|
182
|
+
img_bw_0, img_bw_ref = img_bw_8bit(img_0), img_bw_8bit(img_ref)
|
|
183
183
|
detector_map = {
|
|
184
184
|
constants.DETECTOR_SIFT: cv2.SIFT_create,
|
|
185
185
|
constants.DETECTOR_ORB: cv2.ORB_create,
|
|
@@ -199,12 +199,12 @@ def detect_and_compute(img_0, img_1, feature_config=None, matching_config=None):
|
|
|
199
199
|
constants.DETECTOR_AKAZE,
|
|
200
200
|
constants.DETECTOR_BRISK):
|
|
201
201
|
kp_0, des_0 = detector.detectAndCompute(img_bw_0, None)
|
|
202
|
-
|
|
202
|
+
kp_ref, des_ref = detector.detectAndCompute(img_bw_ref, None)
|
|
203
203
|
else:
|
|
204
204
|
descriptor = descriptor_map[feature_config_descriptor]()
|
|
205
205
|
kp_0, des_0 = descriptor.compute(img_bw_0, detector.detect(img_bw_0, None))
|
|
206
|
-
|
|
207
|
-
return kp_0,
|
|
206
|
+
kp_ref, des_ref = descriptor.compute(img_bw_ref, detector.detect(img_bw_ref, None))
|
|
207
|
+
return kp_0, kp_ref, get_good_matches(des_0, des_ref, matching_config)
|
|
208
208
|
|
|
209
209
|
|
|
210
210
|
def find_transform(src_pts, dst_pts, transform=constants.DEFAULT_TRANSFORM,
|
|
@@ -236,7 +236,7 @@ def find_transform(src_pts, dst_pts, transform=constants.DEFAULT_TRANSFORM,
|
|
|
236
236
|
return result
|
|
237
237
|
|
|
238
238
|
|
|
239
|
-
def align_images(
|
|
239
|
+
def align_images(img_ref, img_0, feature_config=None, matching_config=None, alignment_config=None,
|
|
240
240
|
plot_path=None, callbacks=None,
|
|
241
241
|
affine_thresholds=_AFFINE_THRESHOLDS,
|
|
242
242
|
homography_thresholds=_HOMOGRAPHY_THRESHOLDS):
|
|
@@ -250,11 +250,11 @@ def align_images(img_1, img_0, feature_config=None, matching_config=None, alignm
|
|
|
250
250
|
min_matches = 4 if alignment_config['transform'] == constants.ALIGN_HOMOGRAPHY else 3
|
|
251
251
|
if callbacks and 'message' in callbacks:
|
|
252
252
|
callbacks['message']()
|
|
253
|
-
h_ref, w_ref =
|
|
253
|
+
h_ref, w_ref = img_ref.shape[:2]
|
|
254
254
|
h0, w0 = img_0.shape[:2]
|
|
255
255
|
subsample = alignment_config['subsample']
|
|
256
256
|
if subsample == 0:
|
|
257
|
-
img_res = (float(h0) /
|
|
257
|
+
img_res = (float(h0) / constants.ONE_KILO) * (float(w0) / constants.ONE_KILO)
|
|
258
258
|
target_res = constants.DEFAULT_ALIGN_RES_TARGET_MPX
|
|
259
259
|
subsample = int(1 + math.floor(img_res / target_res))
|
|
260
260
|
fast_subsampling = alignment_config['fast_subsampling']
|
|
@@ -262,11 +262,11 @@ def align_images(img_1, img_0, feature_config=None, matching_config=None, alignm
|
|
|
262
262
|
while True:
|
|
263
263
|
if subsample > 1:
|
|
264
264
|
img_0_sub = img_subsample(img_0, subsample, fast_subsampling)
|
|
265
|
-
|
|
265
|
+
img_ref_sub = img_subsample(img_ref, subsample, fast_subsampling)
|
|
266
266
|
else:
|
|
267
|
-
img_0_sub,
|
|
268
|
-
kp_0,
|
|
269
|
-
|
|
267
|
+
img_0_sub, img_ref_sub = img_0, img_ref
|
|
268
|
+
kp_0, kp_ref, good_matches = detect_and_compute(img_0_sub, img_ref_sub,
|
|
269
|
+
feature_config, matching_config)
|
|
270
270
|
n_good_matches = len(good_matches)
|
|
271
271
|
if n_good_matches > min_good_matches or subsample == 1:
|
|
272
272
|
break
|
|
@@ -282,7 +282,7 @@ def align_images(img_1, img_0, feature_config=None, matching_config=None, alignm
|
|
|
282
282
|
if n_good_matches >= min_matches:
|
|
283
283
|
transform = alignment_config['transform']
|
|
284
284
|
src_pts = np.float32([kp_0[m.queryIdx].pt for m in good_matches]).reshape(-1, 1, 2)
|
|
285
|
-
dst_pts = np.float32([
|
|
285
|
+
dst_pts = np.float32([kp_ref[m.trainIdx].pt for m in good_matches]).reshape(-1, 1, 2)
|
|
286
286
|
m, msk = find_transform(src_pts, dst_pts, transform, alignment_config['align_method'],
|
|
287
287
|
*(alignment_config[k]
|
|
288
288
|
for k in ['rans_threshold', 'max_iters',
|
|
@@ -290,11 +290,11 @@ def align_images(img_1, img_0, feature_config=None, matching_config=None, alignm
|
|
|
290
290
|
if plot_path is not None:
|
|
291
291
|
matches_mask = msk.ravel().tolist()
|
|
292
292
|
img_match = cv2.cvtColor(cv2.drawMatches(
|
|
293
|
-
img_8bit(img_0_sub), kp_0, img_8bit(
|
|
294
|
-
|
|
293
|
+
img_8bit(img_0_sub), kp_0, img_8bit(img_ref_sub),
|
|
294
|
+
kp_ref, good_matches, None, matchColor=(0, 255, 0),
|
|
295
295
|
singlePointColor=None, matchesMask=matches_mask,
|
|
296
296
|
flags=2), cv2.COLOR_BGR2RGB)
|
|
297
|
-
plt.figure(figsize=
|
|
297
|
+
plt.figure(figsize=constants.PLT_FIG_SIZE)
|
|
298
298
|
plt.imshow(img_match, 'gray')
|
|
299
299
|
save_plot(plot_path)
|
|
300
300
|
if callbacks and 'save_plot' in callbacks:
|
|
@@ -330,7 +330,6 @@ def align_images(img_1, img_0, feature_config=None, matching_config=None, alignm
|
|
|
330
330
|
if callbacks and 'warning' in callbacks:
|
|
331
331
|
callbacks['warning'](f"invalid transformation: {reason}")
|
|
332
332
|
return n_good_matches, None, None
|
|
333
|
-
|
|
334
333
|
if callbacks and 'align_message' in callbacks:
|
|
335
334
|
callbacks['align_message']()
|
|
336
335
|
img_mask = np.ones_like(img_0, dtype=np.uint8)
|
|
@@ -390,7 +389,7 @@ class AlignFrames(SubAction):
|
|
|
390
389
|
def sub_msg(self, msg, color=constants.LOG_COLOR_LEVEL_3):
|
|
391
390
|
self.process.sub_message_r(color_str(msg, color))
|
|
392
391
|
|
|
393
|
-
def align_images(self, idx,
|
|
392
|
+
def align_images(self, idx, img_ref, img_0):
|
|
394
393
|
idx_str = f"{idx:04d}"
|
|
395
394
|
callbacks = {
|
|
396
395
|
'message': lambda: self.sub_msg(': find matches'),
|
|
@@ -416,7 +415,7 @@ class AlignFrames(SubAction):
|
|
|
416
415
|
affine_thresholds = None
|
|
417
416
|
homography_thresholds = None
|
|
418
417
|
n_good_matches, _m, img = align_images(
|
|
419
|
-
|
|
418
|
+
img_ref, img_0,
|
|
420
419
|
feature_config=self.feature_config,
|
|
421
420
|
matching_config=self.matching_config,
|
|
422
421
|
alignment_config=self.alignment_config,
|
|
@@ -427,27 +426,29 @@ class AlignFrames(SubAction):
|
|
|
427
426
|
)
|
|
428
427
|
self.n_matches[idx] = n_good_matches
|
|
429
428
|
if n_good_matches < self.min_matches:
|
|
430
|
-
self.process.sub_message(f": image not aligned, too few matches found: "
|
|
431
|
-
f"{n_good_matches}",
|
|
432
|
-
|
|
433
|
-
|
|
429
|
+
self.process.sub_message(color_str(f": image not aligned, too few matches found: "
|
|
430
|
+
f"{n_good_matches}", constants.LOG_COLOR_WARNING),
|
|
431
|
+
level=logging.WARNING)
|
|
432
|
+
return None
|
|
434
433
|
return img
|
|
435
434
|
|
|
436
435
|
def begin(self, process):
|
|
437
436
|
self.process = process
|
|
438
|
-
self.n_matches = np.zeros(process.
|
|
437
|
+
self.n_matches = np.zeros(process.total_action_counts)
|
|
439
438
|
|
|
440
439
|
def end(self):
|
|
441
440
|
if self.plot_summary:
|
|
442
|
-
plt.figure(figsize=
|
|
441
|
+
plt.figure(figsize=constants.PLT_FIG_SIZE)
|
|
443
442
|
x = np.arange(1, len(self.n_matches) + 1, dtype=int)
|
|
444
443
|
no_ref = x != self.process.ref_idx + 1
|
|
445
444
|
x = x[no_ref]
|
|
446
445
|
y = self.n_matches[no_ref]
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
446
|
+
if self.process.ref_idx == 0:
|
|
447
|
+
y_max = y[1]
|
|
448
|
+
elif self.process.ref_idx >= len(y):
|
|
449
|
+
y_max = y[-1]
|
|
450
|
+
else:
|
|
451
|
+
y_max = (y[self.process.ref_idx - 1] + y[self.process.ref_idx]) / 2
|
|
451
452
|
|
|
452
453
|
plt.plot([self.process.ref_idx + 1, self.process.ref_idx + 1],
|
|
453
454
|
[0, y_max], color='cornflowerblue', linestyle='--', label='reference frame')
|
|
@@ -24,7 +24,6 @@ class BaseHistogrammer:
|
|
|
24
24
|
self.plot_summary = plot_summary
|
|
25
25
|
self.process = process
|
|
26
26
|
self.corrections = None
|
|
27
|
-
self.figsize = (10, 5)
|
|
28
27
|
|
|
29
28
|
def begin(self, size):
|
|
30
29
|
self.corrections = np.ones((size, self.channels))
|
|
@@ -70,7 +69,7 @@ class LumiHistogrammer(BaseHistogrammer):
|
|
|
70
69
|
self.colors = ("r", "g", "b")
|
|
71
70
|
|
|
72
71
|
def generate_frame_plot(self, idx, hist, chans, calc_hist_func):
|
|
73
|
-
_fig, axs = plt.subplots(1, 2, figsize=
|
|
72
|
+
_fig, axs = plt.subplots(1, 2, figsize=constants.PLT_FIG_SIZE, sharey=True)
|
|
74
73
|
self.histo_plot(axs[0], hist, "pixel luminosity", 'black')
|
|
75
74
|
for (chan, color) in zip(chans, self.colors):
|
|
76
75
|
hist_col = calc_hist_func(chan)
|
|
@@ -79,7 +78,7 @@ class LumiHistogrammer(BaseHistogrammer):
|
|
|
79
78
|
self.save_plot(idx)
|
|
80
79
|
|
|
81
80
|
def generate_summary_plot(self, ref_idx):
|
|
82
|
-
plt.figure(figsize=
|
|
81
|
+
plt.figure(figsize=constants.PLT_FIG_SIZE)
|
|
83
82
|
x = np.arange(0, len(self.corrections), dtype=int)
|
|
84
83
|
y = self.corrections
|
|
85
84
|
plt.plot([ref_idx, ref_idx], [0, np.max(y)], color='cornflowerblue',
|
|
@@ -101,14 +100,14 @@ class RGBHistogrammer(BaseHistogrammer):
|
|
|
101
100
|
self.colors = ("r", "g", "b")
|
|
102
101
|
|
|
103
102
|
def generate_frame_plot(self, idx, hists):
|
|
104
|
-
_fig, axs = plt.subplots(1, 3, figsize=
|
|
103
|
+
_fig, axs = plt.subplots(1, 3, figsize=constants.PLT_FIG_SIZE, sharey=True)
|
|
105
104
|
for c in [2, 1, 0]:
|
|
106
105
|
self.histo_plot(axs[c], hists[c], self.colors[c] + " luminosity", self.colors[c])
|
|
107
106
|
plt.xlim(0, self.max_pixel_value)
|
|
108
107
|
self.save_plot(idx)
|
|
109
108
|
|
|
110
109
|
def generate_summary_plot(self, ref_idx):
|
|
111
|
-
plt.figure(figsize=
|
|
110
|
+
plt.figure(figsize=constants.PLT_FIG_SIZE)
|
|
112
111
|
x = np.arange(0, len(self.corrections), dtype=int)
|
|
113
112
|
y = self.corrections
|
|
114
113
|
max_val = np.max(y) if np.any(y) else 1.0
|
|
@@ -134,14 +133,14 @@ class Ch1Histogrammer(BaseHistogrammer):
|
|
|
134
133
|
self.colors = colors
|
|
135
134
|
|
|
136
135
|
def generate_frame_plot(self, idx, hists):
|
|
137
|
-
_fig, axs = plt.subplots(1, 3, figsize=
|
|
136
|
+
_fig, axs = plt.subplots(1, 3, figsize=constants.PLT_FIG_SIZE, sharey=True)
|
|
138
137
|
for c in range(3):
|
|
139
138
|
self.histo_plot(axs[c], hists[c], self.labels[c], self.colors[c])
|
|
140
139
|
plt.xlim(0, self.max_pixel_value)
|
|
141
140
|
self.save_plot(idx)
|
|
142
141
|
|
|
143
142
|
def generate_summary_plot(self, ref_idx):
|
|
144
|
-
plt.figure(figsize=
|
|
143
|
+
plt.figure(figsize=constants.PLT_FIG_SIZE)
|
|
145
144
|
x = np.arange(0, len(self.corrections), dtype=int)
|
|
146
145
|
y = self.corrections
|
|
147
146
|
max_val = np.max(y) if np.any(y) else 1.0
|
|
@@ -165,14 +164,14 @@ class Ch2Histogrammer(BaseHistogrammer):
|
|
|
165
164
|
self.colors = colors
|
|
166
165
|
|
|
167
166
|
def generate_frame_plot(self, idx, hists):
|
|
168
|
-
_fig, axs = plt.subplots(1, 3, figsize=
|
|
167
|
+
_fig, axs = plt.subplots(1, 3, figsize=constants.PLT_FIG_SIZE, sharey=True)
|
|
169
168
|
for c in range(3):
|
|
170
169
|
self.histo_plot(axs[c], hists[c], self.labels[c], self.colors[c])
|
|
171
170
|
plt.xlim(0, self.max_pixel_value)
|
|
172
171
|
self.save_plot(idx)
|
|
173
172
|
|
|
174
173
|
def generate_summary_plot(self, ref_idx):
|
|
175
|
-
plt.figure(figsize=
|
|
174
|
+
plt.figure(figsize=constants.PLT_FIG_SIZE)
|
|
176
175
|
x = np.arange(0, len(self.corrections), dtype=int)
|
|
177
176
|
y = self.corrections
|
|
178
177
|
max_val = np.max(y) if np.any(y) else 1.0
|
|
@@ -591,9 +590,9 @@ class BalanceFrames(SubAction):
|
|
|
591
590
|
def begin(self, process):
|
|
592
591
|
self.process = process
|
|
593
592
|
self.correction.process = process
|
|
594
|
-
img = read_img(self.process.
|
|
593
|
+
img = read_img(self.process.input_filepath(process.ref_idx))
|
|
595
594
|
self.shape = img.shape
|
|
596
|
-
self.correction.begin(img, self.process.
|
|
595
|
+
self.correction.begin(img, self.process.total_action_counts, process.ref_idx)
|
|
597
596
|
|
|
598
597
|
def end(self):
|
|
599
598
|
self.process.print_message(' ' * 60)
|
|
@@ -603,7 +602,7 @@ class BalanceFrames(SubAction):
|
|
|
603
602
|
img = np.zeros(shape)
|
|
604
603
|
mask_radius = int(min(*shape) * self.mask_size / 2)
|
|
605
604
|
cv2.circle(img, (shape[1] // 2, shape[0] // 2), mask_radius, 255, -1)
|
|
606
|
-
plt.figure(figsize=
|
|
605
|
+
plt.figure(figsize=constants.PLT_FIG_SIZE)
|
|
607
606
|
plt.title('Mask')
|
|
608
607
|
plt.imshow(img, 'gray')
|
|
609
608
|
self.correction.histogrammer.save_summary_plot("mask")
|
|
@@ -14,7 +14,7 @@ from .. config.config import config
|
|
|
14
14
|
from .. core.colors import color_str
|
|
15
15
|
from .. core.framework import JobBase
|
|
16
16
|
from .utils import EXTENSIONS_TIF, EXTENSIONS_JPG, EXTENSIONS_PNG
|
|
17
|
-
from .stack_framework import
|
|
17
|
+
from .stack_framework import FramePaths
|
|
18
18
|
from .exif import exif_extra_tags_for_tif, get_exif
|
|
19
19
|
|
|
20
20
|
|
|
@@ -159,9 +159,9 @@ def write_multilayer_tiff_from_images(image_dict, output_file, exif_path='', cal
|
|
|
159
159
|
compression=compression, metadata=None, **tiff_tags)
|
|
160
160
|
|
|
161
161
|
|
|
162
|
-
class MultiLayer(JobBase,
|
|
162
|
+
class MultiLayer(JobBase, FramePaths):
|
|
163
163
|
def __init__(self, name, enabled=True, **kwargs):
|
|
164
|
-
|
|
164
|
+
FramePaths.__init__(self, name, **kwargs)
|
|
165
165
|
JobBase.__init__(self, name, enabled)
|
|
166
166
|
self.exif_path = kwargs.get('exif_path', '')
|
|
167
167
|
self.reverse_order = kwargs.get(
|
|
@@ -170,16 +170,16 @@ class MultiLayer(JobBase, FrameMultiDirectory):
|
|
|
170
170
|
)
|
|
171
171
|
|
|
172
172
|
def init(self, job):
|
|
173
|
-
|
|
173
|
+
FramePaths.init(self, job)
|
|
174
174
|
if self.exif_path == '':
|
|
175
175
|
self.exif_path = job.paths[0]
|
|
176
176
|
if self.exif_path != '':
|
|
177
177
|
self.exif_path = self.working_path + "/" + self.exif_path
|
|
178
178
|
|
|
179
179
|
def run_core(self):
|
|
180
|
-
if isinstance(self.input_full_path, str):
|
|
180
|
+
if isinstance(self.input_full_path(), str):
|
|
181
181
|
paths = [self.input_path]
|
|
182
|
-
elif hasattr(self.input_full_path, "__len__"):
|
|
182
|
+
elif hasattr(self.input_full_path(), "__len__"):
|
|
183
183
|
paths = self.input_path
|
|
184
184
|
else:
|
|
185
185
|
raise RuntimeError("input_path option must contain a path or an array of paths")
|
|
@@ -188,8 +188,8 @@ class MultiLayer(JobBase, FrameMultiDirectory):
|
|
|
188
188
|
constants.LOG_COLOR_ALERT),
|
|
189
189
|
level=logging.WARNING)
|
|
190
190
|
return
|
|
191
|
-
|
|
192
|
-
if len(
|
|
191
|
+
input_files = self.input_filepaths()
|
|
192
|
+
if len(input_files) == 0:
|
|
193
193
|
self.print_message(
|
|
194
194
|
color_str(f"no input in {len(paths)} specified path" +
|
|
195
195
|
('s' if len(paths) > 1 else '') + ": "
|
|
@@ -199,12 +199,11 @@ class MultiLayer(JobBase, FrameMultiDirectory):
|
|
|
199
199
|
return
|
|
200
200
|
self.print_message(color_str("merging frames in " + self.folder_list_str(),
|
|
201
201
|
constants.LOG_COLOR_LEVEL_2))
|
|
202
|
-
input_files = [f"{self.working_path}/{f}" for f in files]
|
|
203
202
|
self.print_message(
|
|
204
|
-
color_str("frames: " + ", ".join([
|
|
203
|
+
color_str("frames: " + ", ".join([os.path.basename(i) for i in input_files]),
|
|
205
204
|
constants.LOG_COLOR_LEVEL_2))
|
|
206
205
|
self.print_message(color_str("reading files", constants.LOG_COLOR_LEVEL_2))
|
|
207
|
-
filename = ".".join(
|
|
206
|
+
filename = ".".join(os.path.basename(input_files[0]).split(".")[:-1])
|
|
208
207
|
output_file = f"{self.working_path}/{self.output_path}/{filename}.tif"
|
|
209
208
|
callbacks = {
|
|
210
209
|
'exif_msg': lambda path: self.print_message(
|
|
@@ -12,7 +12,7 @@ from .. core.exceptions import ImageLoadError
|
|
|
12
12
|
from .. core.framework import JobBase
|
|
13
13
|
from .. core.core_utils import make_tqdm_bar
|
|
14
14
|
from .. core.exceptions import RunStopException, ShapeError
|
|
15
|
-
from .stack_framework import
|
|
15
|
+
from .stack_framework import FramePaths, SubAction
|
|
16
16
|
from .utils import read_img, save_plot, get_img_metadata, validate_image
|
|
17
17
|
|
|
18
18
|
MAX_NOISY_PIXELS = 1000
|
|
@@ -45,9 +45,9 @@ def mean_image(file_paths, max_frames=-1, message_callback=None, progress_callba
|
|
|
45
45
|
return None if mean_img is None else (mean_img / counter).astype(np.uint8)
|
|
46
46
|
|
|
47
47
|
|
|
48
|
-
class NoiseDetection(JobBase,
|
|
48
|
+
class NoiseDetection(JobBase, FramePaths):
|
|
49
49
|
def __init__(self, name="noise-map", enabled=True, **kwargs):
|
|
50
|
-
|
|
50
|
+
FramePaths.__init__(self, name, **kwargs)
|
|
51
51
|
JobBase.__init__(self, name, enabled)
|
|
52
52
|
self.max_frames = kwargs.get('max_frames', constants.DEFAULT_NOISE_MAX_FRAMES)
|
|
53
53
|
self.blur_size = kwargs.get('blur_size', constants.DEFAULT_BLUR_SIZE)
|
|
@@ -76,8 +76,7 @@ class NoiseDetection(JobBase, FrameMultiDirectory):
|
|
|
76
76
|
f"map noisy pixels from frames in {self.folder_list_str()}",
|
|
77
77
|
constants.LOG_COLOR_LEVEL_2
|
|
78
78
|
))
|
|
79
|
-
|
|
80
|
-
in_paths = [self.working_path + "/" + f for f in files]
|
|
79
|
+
in_paths = self.input_filepaths()
|
|
81
80
|
n_frames = min(len(in_paths), self.max_frames) if self.max_frames > 0 else len(in_paths)
|
|
82
81
|
self.callback('step_counts', self.id, self.name, n_frames)
|
|
83
82
|
if not config.DISABLE_TQDM:
|
|
@@ -90,7 +89,7 @@ class NoiseDetection(JobBase, FrameMultiDirectory):
|
|
|
90
89
|
mean_img = mean_image(
|
|
91
90
|
file_paths=in_paths, max_frames=self.max_frames,
|
|
92
91
|
message_callback=lambda path: self.print_message_r(
|
|
93
|
-
color_str(f"reading frame: {path.
|
|
92
|
+
color_str(f"reading frame: {os.path.basename(path)}", constants.LOG_COLOR_LEVEL_2)
|
|
94
93
|
),
|
|
95
94
|
progress_callback=progress_callback)
|
|
96
95
|
if not config.DISABLE_TQDM:
|
|
@@ -123,7 +122,7 @@ class NoiseDetection(JobBase, FrameMultiDirectory):
|
|
|
123
122
|
plot_range[1] = max_th + 1
|
|
124
123
|
th_range = np.arange(self.plot_range[0], self.plot_range[1] + 1)
|
|
125
124
|
if self.plot_histograms:
|
|
126
|
-
plt.figure(figsize=
|
|
125
|
+
plt.figure(figsize=constants.PLT_FIG_SIZE)
|
|
127
126
|
x = np.array(list(th_range))
|
|
128
127
|
ys = [[np.count_nonzero(self.hot_map(ch, th) > 0)
|
|
129
128
|
for th in th_range] for ch in channels]
|
shinestacker/algorithms/stack.py
CHANGED
|
@@ -6,14 +6,14 @@ from .. core.framework import JobBase
|
|
|
6
6
|
from .. core.colors import color_str
|
|
7
7
|
from .. core.exceptions import InvalidOptionError
|
|
8
8
|
from .utils import write_img, extension_tif_jpg
|
|
9
|
-
from .stack_framework import
|
|
9
|
+
from .stack_framework import FramePaths, ActionList
|
|
10
10
|
from .exif import copy_exif_from_file_to_file
|
|
11
11
|
from .denoise import denoise
|
|
12
12
|
|
|
13
13
|
|
|
14
|
-
class FocusStackBase(JobBase,
|
|
14
|
+
class FocusStackBase(JobBase, FramePaths):
|
|
15
15
|
def __init__(self, name, stack_algo, enabled=True, **kwargs):
|
|
16
|
-
|
|
16
|
+
FramePaths.__init__(self, name, **kwargs)
|
|
17
17
|
JobBase.__init__(self, name, enabled)
|
|
18
18
|
self.stack_algo = stack_algo
|
|
19
19
|
self.exif_path = kwargs.pop('exif_path', '')
|
|
@@ -26,9 +26,10 @@ class FocusStackBase(JobBase, FrameDirectory):
|
|
|
26
26
|
def focus_stack(self, filenames):
|
|
27
27
|
self.sub_message_r(color_str(': reading input files', constants.LOG_COLOR_LEVEL_3))
|
|
28
28
|
stacked_img = self.stack_algo.focus_stack()
|
|
29
|
-
in_filename = filenames[0].split(".")
|
|
30
|
-
out_filename =
|
|
31
|
-
|
|
29
|
+
in_filename = os.path.basename(filenames[0]).split(".")
|
|
30
|
+
out_filename = os.path.join(
|
|
31
|
+
self.output_full_path(),
|
|
32
|
+
f"{self.prefix}{in_filename[0]}." + '.'.join(in_filename[1:]))
|
|
32
33
|
if self.denoise_amount > 0:
|
|
33
34
|
self.sub_message_r(': denoise image')
|
|
34
35
|
stacked_img = denoise(stacked_img, self.denoise_amount, self.denoise_amount)
|
|
@@ -50,7 +51,7 @@ class FocusStackBase(JobBase, FrameDirectory):
|
|
|
50
51
|
self.frame_count += 1
|
|
51
52
|
|
|
52
53
|
def init(self, job, working_path=''):
|
|
53
|
-
|
|
54
|
+
FramePaths.init(self, job)
|
|
54
55
|
if self.exif_path is None:
|
|
55
56
|
self.exif_path = job.paths[0]
|
|
56
57
|
if self.exif_path != '':
|
|
@@ -82,20 +83,19 @@ class FocusStackBunch(ActionList, FocusStackBase):
|
|
|
82
83
|
|
|
83
84
|
def begin(self):
|
|
84
85
|
ActionList.begin(self)
|
|
85
|
-
|
|
86
|
-
self._chunks = get_bunches(fnames, self.frames, self.overlap)
|
|
86
|
+
self._chunks = get_bunches(self.input_filepaths(), self.frames, self.overlap)
|
|
87
87
|
self.set_counts(len(self._chunks))
|
|
88
88
|
|
|
89
89
|
def end(self):
|
|
90
90
|
ActionList.end(self)
|
|
91
91
|
|
|
92
92
|
def run_step(self):
|
|
93
|
-
self.print_message_r(
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
93
|
+
self.print_message_r(
|
|
94
|
+
color_str(f"fusing bunch: {self.current_action_count + 1}/{self.total_action_counts}",
|
|
95
|
+
constants.LOG_COLOR_LEVEL_2))
|
|
96
|
+
img_files = self._chunks[self.current_action_count - 1]
|
|
97
97
|
self.stack_algo.init(img_files)
|
|
98
|
-
self.focus_stack(self._chunks[self.
|
|
98
|
+
self.focus_stack(self._chunks[self.current_action_count - 1])
|
|
99
99
|
|
|
100
100
|
|
|
101
101
|
class FocusStack(FocusStackBase):
|
|
@@ -106,11 +106,11 @@ class FocusStack(FocusStackBase):
|
|
|
106
106
|
|
|
107
107
|
def run_core(self):
|
|
108
108
|
self.set_filelist()
|
|
109
|
-
img_files = sorted(
|
|
109
|
+
img_files = sorted(self.input_filepaths())
|
|
110
110
|
self.stack_algo.init(img_files)
|
|
111
111
|
self.callback('step_counts', self.id, self.name,
|
|
112
|
-
self.stack_algo.total_steps(
|
|
113
|
-
self.focus_stack(
|
|
112
|
+
self.stack_algo.total_steps(self.num_input_filepaths()))
|
|
113
|
+
self.focus_stack(img_files)
|
|
114
114
|
|
|
115
115
|
def init(self, job, _working_path=''):
|
|
116
116
|
FocusStackBase.init(self, job, self.working_path)
|