shinestacker 1.0.4__py3-none-any.whl → 1.1.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 +11 -11
- shinestacker/algorithms/base_stack_algo.py +27 -4
- shinestacker/algorithms/depth_map.py +8 -11
- shinestacker/algorithms/noise_detection.py +7 -2
- shinestacker/algorithms/pyramid.py +55 -41
- shinestacker/algorithms/pyramid_tiles.py +109 -0
- shinestacker/algorithms/stack.py +11 -8
- shinestacker/algorithms/stack_framework.py +5 -8
- shinestacker/algorithms/utils.py +5 -0
- shinestacker/config/constants.py +5 -2
- shinestacker/gui/action_config_dialog.py +26 -4
- shinestacker/gui/flow_layout.py +105 -0
- shinestacker/gui/gui_run.py +24 -19
- shinestacker/gui/main_window.py +2 -2
- shinestacker/gui/new_project.py +1 -0
- shinestacker/gui/project_controller.py +1 -0
- shinestacker/gui/project_converter.py +10 -4
- shinestacker/gui/time_progress_bar.py +2 -2
- {shinestacker-1.0.4.dist-info → shinestacker-1.1.0.dist-info}/METADATA +20 -1
- {shinestacker-1.0.4.dist-info → shinestacker-1.1.0.dist-info}/RECORD +25 -23
- {shinestacker-1.0.4.dist-info → shinestacker-1.1.0.dist-info}/WHEEL +0 -0
- {shinestacker-1.0.4.dist-info → shinestacker-1.1.0.dist-info}/entry_points.txt +0 -0
- {shinestacker-1.0.4.dist-info → shinestacker-1.1.0.dist-info}/licenses/LICENSE +0 -0
- {shinestacker-1.0.4.dist-info → shinestacker-1.1.0.dist-info}/top_level.txt +0 -0
shinestacker/_version.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = '1.0
|
|
1
|
+
__version__ = '1.1.0'
|
shinestacker/algorithms/align.py
CHANGED
|
@@ -6,7 +6,7 @@ import cv2
|
|
|
6
6
|
from .. config.constants import constants
|
|
7
7
|
from .. core.exceptions import AlignmentError, InvalidOptionError
|
|
8
8
|
from .. core.colors import color_str
|
|
9
|
-
from .utils import img_8bit, img_bw_8bit, save_plot,
|
|
9
|
+
from .utils import img_8bit, img_bw_8bit, save_plot, img_subsample
|
|
10
10
|
from .stack_framework import SubAction
|
|
11
11
|
|
|
12
12
|
_DEFAULT_FEATURE_CONFIG = {
|
|
@@ -161,9 +161,10 @@ def align_images(img_1, img_0, feature_config=None, matching_config=None, alignm
|
|
|
161
161
|
except KeyError as e:
|
|
162
162
|
raise InvalidOptionError("border_mode", alignment_config['border_mode']) from e
|
|
163
163
|
min_matches = 4 if alignment_config['transform'] == constants.ALIGN_HOMOGRAPHY else 3
|
|
164
|
-
validate_image(img_0, *get_img_metadata(img_1))
|
|
165
164
|
if callbacks and 'message' in callbacks:
|
|
166
165
|
callbacks['message']()
|
|
166
|
+
h_ref, w_ref = img_1.shape[:2]
|
|
167
|
+
h0, w0 = img_0.shape[:2]
|
|
167
168
|
subsample = alignment_config['subsample']
|
|
168
169
|
fast_subsampling = alignment_config['fast_subsampling']
|
|
169
170
|
min_good_matches = alignment_config['min_good_matches']
|
|
@@ -204,15 +205,14 @@ def align_images(img_1, img_0, feature_config=None, matching_config=None, alignm
|
|
|
204
205
|
flags=2), cv2.COLOR_BGR2RGB)
|
|
205
206
|
plt.figure(figsize=(10, 5))
|
|
206
207
|
plt.imshow(img_match, 'gray')
|
|
207
|
-
|
|
208
|
+
save_plot(plot_path)
|
|
208
209
|
if callbacks and 'save_plot' in callbacks:
|
|
209
210
|
callbacks['save_plot'](plot_path)
|
|
210
|
-
h, w = img_0.shape[:2]
|
|
211
211
|
h_sub, w_sub = img_0_sub.shape[:2]
|
|
212
212
|
if subsample > 1:
|
|
213
213
|
if transform == constants.ALIGN_HOMOGRAPHY:
|
|
214
214
|
low_size = np.float32([[0, 0], [0, h_sub], [w_sub, h_sub], [w_sub, 0]])
|
|
215
|
-
high_size = np.float32([[0, 0], [0,
|
|
215
|
+
high_size = np.float32([[0, 0], [0, h0], [w0, h0], [w0, 0]])
|
|
216
216
|
scale_up = cv2.getPerspectiveTransform(low_size, high_size)
|
|
217
217
|
scale_down = cv2.getPerspectiveTransform(high_size, low_size)
|
|
218
218
|
m = scale_up @ m @ scale_down
|
|
@@ -230,17 +230,17 @@ def align_images(img_1, img_0, feature_config=None, matching_config=None, alignm
|
|
|
230
230
|
img_mask = np.ones_like(img_0, dtype=np.uint8)
|
|
231
231
|
if alignment_config['transform'] == constants.ALIGN_HOMOGRAPHY:
|
|
232
232
|
img_warp = cv2.warpPerspective(
|
|
233
|
-
img_0, m, (
|
|
233
|
+
img_0, m, (w_ref, h_ref),
|
|
234
234
|
borderMode=cv2_border_mode, borderValue=alignment_config['border_value'])
|
|
235
235
|
if alignment_config['border_mode'] == constants.BORDER_REPLICATE_BLUR:
|
|
236
|
-
mask = cv2.warpPerspective(img_mask, m, (
|
|
236
|
+
mask = cv2.warpPerspective(img_mask, m, (w_ref, h_ref),
|
|
237
237
|
borderMode=cv2.BORDER_CONSTANT, borderValue=0)
|
|
238
238
|
elif alignment_config['transform'] == constants.ALIGN_RIGID:
|
|
239
239
|
img_warp = cv2.warpAffine(
|
|
240
|
-
img_0, m, (
|
|
240
|
+
img_0, m, (w_ref, h_ref),
|
|
241
241
|
borderMode=cv2_border_mode, borderValue=alignment_config['border_value'])
|
|
242
242
|
if alignment_config['border_mode'] == constants.BORDER_REPLICATE_BLUR:
|
|
243
|
-
mask = cv2.warpAffine(img_mask, m, (
|
|
243
|
+
mask = cv2.warpAffine(img_mask, m, (w_ref, h_ref),
|
|
244
244
|
borderMode=cv2.BORDER_CONSTANT, borderValue=0)
|
|
245
245
|
if alignment_config['border_mode'] == constants.BORDER_REPLICATE_BLUR:
|
|
246
246
|
if callbacks and 'blur_message' in callbacks:
|
|
@@ -293,7 +293,7 @@ class AlignFrames(SubAction):
|
|
|
293
293
|
'ecc_message': lambda: self.sub_msg(": ecc refinement"),
|
|
294
294
|
'blur_message': lambda: self.sub_msg(': blur borders'),
|
|
295
295
|
'warning': lambda msg: self.sub_msg(
|
|
296
|
-
f': {msg}', constants.
|
|
296
|
+
f': {msg}', constants.LOG_COLOR_WARNING),
|
|
297
297
|
'save_plot': lambda plot_path: self.process.callback(
|
|
298
298
|
'save_plot', self.process.id,
|
|
299
299
|
f"{self.process.name}: matches\nframe {idx_str}", plot_path)
|
|
@@ -315,7 +315,7 @@ class AlignFrames(SubAction):
|
|
|
315
315
|
if n_good_matches < self.min_matches:
|
|
316
316
|
self.process.sub_message(f": image not aligned, too few matches found: "
|
|
317
317
|
f"{n_good_matches}", level=logging.CRITICAL)
|
|
318
|
-
raise AlignmentError(idx, f"too few matches found: "
|
|
318
|
+
raise AlignmentError(idx, f"Image not aligned, too few matches found: "
|
|
319
319
|
f"{n_good_matches} < {self.min_matches}")
|
|
320
320
|
return img
|
|
321
321
|
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
# pylint: disable=C0114, C0115, C0116, E0602, R0903
|
|
2
|
+
import os
|
|
2
3
|
import numpy as np
|
|
3
|
-
from .. core.exceptions import InvalidOptionError, ImageLoadError
|
|
4
|
+
from .. core.exceptions import InvalidOptionError, ImageLoadError, RunStopException
|
|
4
5
|
from .. config.constants import constants
|
|
5
6
|
from .. core.colors import color_str
|
|
6
|
-
from .utils import read_img, get_img_metadata, validate_image
|
|
7
|
+
from .utils import read_img, get_img_metadata, validate_image, get_img_file_shape, extension_tif_jpg
|
|
7
8
|
|
|
8
9
|
|
|
9
10
|
class BaseStackAlgo:
|
|
@@ -11,6 +12,9 @@ class BaseStackAlgo:
|
|
|
11
12
|
self._name = name
|
|
12
13
|
self._steps_per_frame = steps_per_frame
|
|
13
14
|
self.process = None
|
|
15
|
+
self.filenames = None
|
|
16
|
+
self.shape = None
|
|
17
|
+
self.do_step_callback = False
|
|
14
18
|
if float_type == constants.FLOAT_32:
|
|
15
19
|
self.float_type = np.float32
|
|
16
20
|
elif float_type == constants.FLOAT_64:
|
|
@@ -24,8 +28,17 @@ class BaseStackAlgo:
|
|
|
24
28
|
def name(self):
|
|
25
29
|
return self._name
|
|
26
30
|
|
|
27
|
-
def
|
|
28
|
-
|
|
31
|
+
def init(self, filenames):
|
|
32
|
+
self.filenames = filenames
|
|
33
|
+
first_img_file = ''
|
|
34
|
+
for filename in filenames:
|
|
35
|
+
if os.path.isfile(filename) and extension_tif_jpg(filename):
|
|
36
|
+
first_img_file = filename
|
|
37
|
+
break
|
|
38
|
+
self.shape = get_img_file_shape(first_img_file)
|
|
39
|
+
|
|
40
|
+
def total_steps(self, n_frames):
|
|
41
|
+
return self._steps_per_frame * n_frames
|
|
29
42
|
|
|
30
43
|
def print_message(self, msg):
|
|
31
44
|
self.process.sub_message_r(color_str(msg, constants.LOG_COLOR_LEVEL_3))
|
|
@@ -40,3 +53,13 @@ class BaseStackAlgo:
|
|
|
40
53
|
else:
|
|
41
54
|
validate_image(img, *metadata)
|
|
42
55
|
return img, metadata, updated
|
|
56
|
+
|
|
57
|
+
def check_running(self, cleanup_callback=None):
|
|
58
|
+
if self.process.callback('check_running', self.process.id, self.process.name) is False:
|
|
59
|
+
if cleanup_callback is not None:
|
|
60
|
+
cleanup_callback()
|
|
61
|
+
raise RunStopException(self.name)
|
|
62
|
+
|
|
63
|
+
def after_step(self, step):
|
|
64
|
+
if self.do_step_callback:
|
|
65
|
+
self.process.callback('after_step', self.process.id, self.process.name, step)
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import numpy as np
|
|
3
3
|
import cv2
|
|
4
4
|
from .. config.constants import constants
|
|
5
|
-
from .. core.exceptions import InvalidOptionError
|
|
5
|
+
from .. core.exceptions import InvalidOptionError
|
|
6
6
|
from .utils import read_img, img_bw
|
|
7
7
|
from .base_stack_algo import BaseStackAlgo
|
|
8
8
|
|
|
@@ -61,19 +61,18 @@ class DepthMapStack(BaseStackAlgo):
|
|
|
61
61
|
raise InvalidOptionError("map_type", self.map_type, details=f" valid values are "
|
|
62
62
|
f"{constants.DM_MAP_AVERAGE} and {constants.DM_MAP_MAX}.")
|
|
63
63
|
|
|
64
|
-
def focus_stack(self
|
|
64
|
+
def focus_stack(self):
|
|
65
65
|
gray_images = []
|
|
66
66
|
metadata = None
|
|
67
|
-
for i, img_path in enumerate(filenames):
|
|
67
|
+
for i, img_path in enumerate(self.filenames):
|
|
68
68
|
self.print_message(f": reading file (1/2) {img_path.split('/')[-1]}")
|
|
69
69
|
|
|
70
70
|
img, metadata, _updated = self.read_image_and_update_metadata(img_path, metadata)
|
|
71
71
|
|
|
72
72
|
gray = img_bw(img)
|
|
73
73
|
gray_images.append(gray)
|
|
74
|
-
self.
|
|
75
|
-
|
|
76
|
-
raise RunStopException(self.name)
|
|
74
|
+
self.after_step(i)
|
|
75
|
+
self.check_running()
|
|
77
76
|
dtype = metadata[1]
|
|
78
77
|
gray_images = np.array(gray_images, dtype=self.float_type)
|
|
79
78
|
if self.energy == constants.DM_ENERGY_SOBEL:
|
|
@@ -92,7 +91,7 @@ class DepthMapStack(BaseStackAlgo):
|
|
|
92
91
|
energies = self.smooth_energy(energies)
|
|
93
92
|
weights = self.get_focus_map(energies)
|
|
94
93
|
blended_pyramid = None
|
|
95
|
-
for i, img_path in enumerate(filenames):
|
|
94
|
+
for i, img_path in enumerate(self.filenames):
|
|
96
95
|
self.print_message(f": reading file (2/2) {img_path.split('/')[-1]}")
|
|
97
96
|
img = read_img(img_path).astype(self.float_type)
|
|
98
97
|
weight = weights[i]
|
|
@@ -110,10 +109,8 @@ class DepthMapStack(BaseStackAlgo):
|
|
|
110
109
|
for j in range(self.levels)]
|
|
111
110
|
blended_pyramid = current_blend if blended_pyramid is None \
|
|
112
111
|
else [np.add(bp, cb) for bp, cb in zip(blended_pyramid, current_blend)]
|
|
113
|
-
self.
|
|
114
|
-
|
|
115
|
-
if self.process.callback('check_running', self.process.id, self.process.name) is False:
|
|
116
|
-
raise RunStopException(self.name)
|
|
112
|
+
self.after_step(i + len(self.filenames))
|
|
113
|
+
self.check_running()
|
|
117
114
|
result = blended_pyramid[0]
|
|
118
115
|
self.print_message(': blend levels')
|
|
119
116
|
for j in range(1, self.levels):
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# pylint: disable=C0114, C0115, C0116, E1101, W0718, R0914, R0915
|
|
1
|
+
# pylint: disable=C0114, C0115, C0116, E1101, W0718, R0914, R0915, R0902
|
|
2
2
|
import os
|
|
3
3
|
import errno
|
|
4
4
|
import logging
|
|
@@ -11,7 +11,7 @@ from .. core.colors import color_str
|
|
|
11
11
|
from .. core.exceptions import ImageLoadError
|
|
12
12
|
from .. core.framework import JobBase
|
|
13
13
|
from .. core.core_utils import make_tqdm_bar
|
|
14
|
-
from .. core.exceptions import RunStopException
|
|
14
|
+
from .. core.exceptions import RunStopException, ShapeError
|
|
15
15
|
from .stack_framework import FrameMultiDirectory, SubAction
|
|
16
16
|
from .utils import read_img, save_plot, get_img_metadata, validate_image
|
|
17
17
|
|
|
@@ -154,6 +154,7 @@ class MaskNoise(SubAction):
|
|
|
154
154
|
self.method = method
|
|
155
155
|
self.process = None
|
|
156
156
|
self.noise_mask_img = None
|
|
157
|
+
self.expected_shape = None
|
|
157
158
|
|
|
158
159
|
def begin(self, process):
|
|
159
160
|
self.process = process
|
|
@@ -163,6 +164,7 @@ class MaskNoise(SubAction):
|
|
|
163
164
|
f': reading noisy pixel mask file: {self.noise_mask}',
|
|
164
165
|
constants.LOG_COLOR_LEVEL_3))
|
|
165
166
|
self.noise_mask_img = cv2.imread(path, cv2.IMREAD_GRAYSCALE)
|
|
167
|
+
self.expected_shape = self.noise_mask_img.shape[:2]
|
|
166
168
|
if self.noise_mask_img is None:
|
|
167
169
|
raise ImageLoadError(path, f"failed to load image file {self.noise_mask}.")
|
|
168
170
|
else:
|
|
@@ -170,6 +172,9 @@ class MaskNoise(SubAction):
|
|
|
170
172
|
|
|
171
173
|
def run_frame(self, _idx, _ref_idx, image):
|
|
172
174
|
self.process.sub_message_r(color_str(': mask noisy pixels', constants.LOG_COLOR_LEVEL_3))
|
|
175
|
+
shape = image.shape[:2]
|
|
176
|
+
if shape != self.expected_shape:
|
|
177
|
+
raise ShapeError(self.expected_shape, shape)
|
|
173
178
|
if len(image.shape) == 3:
|
|
174
179
|
corrected = image.copy()
|
|
175
180
|
for c in range(3):
|
|
@@ -1,25 +1,36 @@
|
|
|
1
|
-
# pylint: disable=C0114, C0115, C0116, E1101
|
|
1
|
+
# pylint: disable=C0114, C0115, C0116, E1101, R0913, R0917, R0902
|
|
2
2
|
import numpy as np
|
|
3
3
|
import cv2
|
|
4
4
|
from .. config.constants import constants
|
|
5
|
-
from .. core.exceptions import RunStopException
|
|
6
5
|
from .utils import read_img
|
|
7
6
|
from .base_stack_algo import BaseStackAlgo
|
|
8
7
|
|
|
9
8
|
|
|
10
9
|
class PyramidBase(BaseStackAlgo):
|
|
11
|
-
def __init__(self, min_size=constants.DEFAULT_PY_MIN_SIZE,
|
|
10
|
+
def __init__(self, name, min_size=constants.DEFAULT_PY_MIN_SIZE,
|
|
12
11
|
kernel_size=constants.DEFAULT_PY_KERNEL_SIZE,
|
|
13
12
|
gen_kernel=constants.DEFAULT_PY_GEN_KERNEL,
|
|
14
13
|
float_type=constants.DEFAULT_PY_FLOAT):
|
|
15
|
-
super().__init__(
|
|
14
|
+
super().__init__(name, 2, float_type)
|
|
16
15
|
self.min_size = min_size
|
|
17
16
|
self.kernel_size = kernel_size
|
|
18
17
|
self.pad_amount = (kernel_size - 1) // 2
|
|
19
|
-
self.do_step_callback = False
|
|
20
18
|
kernel = np.array([0.25 - gen_kernel / 2.0, 0.25,
|
|
21
19
|
gen_kernel, 0.25, 0.25 - gen_kernel / 2.0])
|
|
22
20
|
self.gen_kernel = np.outer(kernel, kernel)
|
|
21
|
+
self.dtype = None
|
|
22
|
+
self.num_pixel_values = None
|
|
23
|
+
self.max_pixel_value = None
|
|
24
|
+
self.n_levels = 0
|
|
25
|
+
self.n_frames = 0
|
|
26
|
+
|
|
27
|
+
def init(self, filenames):
|
|
28
|
+
super().init(filenames)
|
|
29
|
+
self.n_levels = int(np.log2(min(self.shape) / self.min_size))
|
|
30
|
+
|
|
31
|
+
def total_steps(self, n_frames):
|
|
32
|
+
self.n_frames = n_frames
|
|
33
|
+
return self._steps_per_frame * n_frames + self.n_levels
|
|
23
34
|
|
|
24
35
|
def convolve(self, image):
|
|
25
36
|
return cv2.filter2D(image, -1, self.gen_kernel, borderType=cv2.BORDER_REFLECT101)
|
|
@@ -55,6 +66,7 @@ class PyramidBase(BaseStackAlgo):
|
|
|
55
66
|
return fused
|
|
56
67
|
|
|
57
68
|
def collapse(self, pyramid):
|
|
69
|
+
self.print_message(': collapsing pyramid')
|
|
58
70
|
img = pyramid[-1]
|
|
59
71
|
for layer in pyramid[-2::-1]:
|
|
60
72
|
expanded = self.expand_layer(img)
|
|
@@ -110,19 +122,22 @@ class PyramidBase(BaseStackAlgo):
|
|
|
110
122
|
fused += np.where(best_d[:, :, np.newaxis] == layer, img, 0)
|
|
111
123
|
return (fused / 2).astype(images.dtype)
|
|
112
124
|
|
|
125
|
+
def focus_stack_validate(self, cleanup_callback=None):
|
|
126
|
+
metadata = None
|
|
127
|
+
for i, img_path in enumerate(self.filenames):
|
|
128
|
+
self.print_message(f": validating file {img_path.split('/')[-1]}")
|
|
113
129
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
self.max_pixel_value = None
|
|
130
|
+
_img, metadata, updated = self.read_image_and_update_metadata(img_path, metadata)
|
|
131
|
+
if updated:
|
|
132
|
+
self.dtype = metadata[1]
|
|
133
|
+
self.num_pixel_values = constants.NUM_UINT8 \
|
|
134
|
+
if self.dtype == np.uint8 else constants.NUM_UINT16
|
|
135
|
+
self.max_pixel_value = constants.MAX_UINT8 \
|
|
136
|
+
if self.dtype == np.uint8 else constants.MAX_UINT16
|
|
137
|
+
self.after_step(i + 1)
|
|
138
|
+
self.check_running(cleanup_callback)
|
|
124
139
|
|
|
125
|
-
def
|
|
140
|
+
def single_image_laplacian(self, img, levels):
|
|
126
141
|
pyramid = [img.astype(self.float_type)]
|
|
127
142
|
for _ in range(levels):
|
|
128
143
|
next_layer = self.reduce_layer(pyramid[-1])
|
|
@@ -136,44 +151,43 @@ class PyramidStack(PyramidBase):
|
|
|
136
151
|
h, w = pyr.shape[:2]
|
|
137
152
|
expanded = expanded[:h, :w]
|
|
138
153
|
laplacian.append(pyr - expanded)
|
|
154
|
+
return laplacian
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
class PyramidStack(PyramidBase):
|
|
158
|
+
def __init__(self, min_size=constants.DEFAULT_PY_MIN_SIZE,
|
|
159
|
+
kernel_size=constants.DEFAULT_PY_KERNEL_SIZE,
|
|
160
|
+
gen_kernel=constants.DEFAULT_PY_GEN_KERNEL,
|
|
161
|
+
float_type=constants.DEFAULT_PY_FLOAT):
|
|
162
|
+
super().__init__("pyramid", min_size, kernel_size, gen_kernel, float_type)
|
|
163
|
+
self.offset = np.arange(-self.pad_amount, self.pad_amount + 1)
|
|
164
|
+
|
|
165
|
+
def process_single_image(self, img, levels):
|
|
166
|
+
laplacian = self.single_image_laplacian(img, levels)
|
|
139
167
|
return laplacian[::-1]
|
|
140
168
|
|
|
141
169
|
def fuse_pyramids(self, all_laplacians):
|
|
142
170
|
fused = [self.get_fused_base(np.stack([p[-1] for p in all_laplacians], axis=0))]
|
|
171
|
+
count = 0
|
|
143
172
|
for layer in range(len(all_laplacians[0]) - 2, -1, -1):
|
|
144
173
|
self.print_message(f': fusing pyramids, layer: {layer + 1}')
|
|
145
174
|
laplacians = np.stack([p[layer] for p in all_laplacians], axis=0)
|
|
146
175
|
fused.append(self.fuse_laplacian(laplacians))
|
|
176
|
+
count += 1
|
|
177
|
+
self.after_step(self._steps_per_frame * self.n_frames + count)
|
|
178
|
+
self.check_running()
|
|
147
179
|
self.print_message(': pyramids fusion completed')
|
|
148
180
|
return fused[::-1]
|
|
149
181
|
|
|
150
|
-
def focus_stack(self
|
|
151
|
-
|
|
182
|
+
def focus_stack(self):
|
|
183
|
+
n = len(self.filenames)
|
|
184
|
+
self.focus_stack_validate()
|
|
152
185
|
all_laplacians = []
|
|
153
|
-
|
|
154
|
-
n = len(filenames)
|
|
155
|
-
for i, img_path in enumerate(filenames):
|
|
156
|
-
self.print_message(f": validating file {img_path.split('/')[-1]}")
|
|
157
|
-
|
|
158
|
-
img, metadata, updated = self.read_image_and_update_metadata(img_path, metadata)
|
|
159
|
-
if updated:
|
|
160
|
-
self.dtype = metadata[1]
|
|
161
|
-
self.num_pixel_values = constants.NUM_UINT8 \
|
|
162
|
-
if self.dtype == np.uint8 else constants.NUM_UINT16
|
|
163
|
-
self.max_pixel_value = constants.MAX_UINT8 \
|
|
164
|
-
if self.dtype == np.uint8 else constants.MAX_UINT16
|
|
165
|
-
levels = int(np.log2(min(img.shape[:2]) / self.min_size))
|
|
166
|
-
if self.do_step_callback:
|
|
167
|
-
self.process.callback('after_step', self.process.id, self.process.name, i)
|
|
168
|
-
if self.process.callback('check_running', self.process.id, self.process.name) is False:
|
|
169
|
-
raise RunStopException(self.name)
|
|
170
|
-
for i, img_path in enumerate(filenames):
|
|
186
|
+
for i, img_path in enumerate(self.filenames):
|
|
171
187
|
self.print_message(f": processing file {img_path.split('/')[-1]}")
|
|
172
188
|
img = read_img(img_path)
|
|
173
|
-
all_laplacians.append(self.process_single_image(img,
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
if self.process.callback('check_running', self.process.id, self.process.name) is False:
|
|
177
|
-
raise RunStopException(self.name)
|
|
189
|
+
all_laplacians.append(self.process_single_image(img, self.n_levels))
|
|
190
|
+
self.after_step(i + n + 1)
|
|
191
|
+
self.check_running()
|
|
178
192
|
stacked_image = self.collapse(self.fuse_pyramids(all_laplacians))
|
|
179
193
|
return stacked_image.astype(self.dtype)
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# pylint: disable=C0114, C0115, C0116, E1101, R0914, R1702, R1732, R0913, R0917, R0912, R0915
|
|
2
|
+
import os
|
|
3
|
+
import tempfile
|
|
4
|
+
import numpy as np
|
|
5
|
+
from .. config.constants import constants
|
|
6
|
+
from .utils import read_img
|
|
7
|
+
from .pyramid import PyramidBase
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class PyramidTilesStack(PyramidBase):
|
|
11
|
+
def __init__(self, min_size=constants.DEFAULT_PY_MIN_SIZE,
|
|
12
|
+
kernel_size=constants.DEFAULT_PY_KERNEL_SIZE,
|
|
13
|
+
gen_kernel=constants.DEFAULT_PY_GEN_KERNEL,
|
|
14
|
+
float_type=constants.DEFAULT_PY_FLOAT,
|
|
15
|
+
tile_size=constants.DEFAULT_PY_TILE_SIZE):
|
|
16
|
+
super().__init__("fast_pyramid", min_size, kernel_size, gen_kernel, float_type)
|
|
17
|
+
self.offset = np.arange(-self.pad_amount, self.pad_amount + 1)
|
|
18
|
+
self.dtype = None
|
|
19
|
+
self.num_pixel_values = None
|
|
20
|
+
self.max_pixel_value = None
|
|
21
|
+
self.tile_size = tile_size
|
|
22
|
+
self.temp_dir = tempfile.TemporaryDirectory()
|
|
23
|
+
self.n_tiles = 0
|
|
24
|
+
|
|
25
|
+
def init(self, filenames):
|
|
26
|
+
super().init(filenames)
|
|
27
|
+
self.n_tiles = (self.shape[0] // self.tile_size + 1) * (self.shape[1] // self.tile_size + 1)
|
|
28
|
+
|
|
29
|
+
def total_steps(self, n_frames):
|
|
30
|
+
n_steps = super().total_steps(n_frames)
|
|
31
|
+
return n_steps + self.n_tiles
|
|
32
|
+
|
|
33
|
+
def process_single_image(self, img, levels, img_index):
|
|
34
|
+
laplacian = self.single_image_laplacian(img, levels)
|
|
35
|
+
for i, level_data in enumerate(laplacian[::-1]):
|
|
36
|
+
np.save(os.path.join(self.temp_dir.name, f'img_{img_index}_level_{i}.npy'), level_data)
|
|
37
|
+
return len(laplacian)
|
|
38
|
+
|
|
39
|
+
def load_level(self, img_index, level):
|
|
40
|
+
return np.load(os.path.join(self.temp_dir.name, f'img_{img_index}_level_{level}.npy'))
|
|
41
|
+
|
|
42
|
+
def cleanup_temp_files(self):
|
|
43
|
+
self.temp_dir.cleanup()
|
|
44
|
+
|
|
45
|
+
def fuse_pyramids(self, all_level_counts, num_images):
|
|
46
|
+
max_levels = max(all_level_counts)
|
|
47
|
+
fused = []
|
|
48
|
+
count = self._steps_per_frame * self.n_frames
|
|
49
|
+
for level in range(max_levels - 1, -1, -1):
|
|
50
|
+
self.print_message(f': fusing pyramids, layer: {level + 1}')
|
|
51
|
+
if level == 0:
|
|
52
|
+
sample_level = self.load_level(0, 0)
|
|
53
|
+
h, w = sample_level.shape[:2]
|
|
54
|
+
del sample_level
|
|
55
|
+
fused_level = np.zeros((h, w, 3), dtype=self.float_type)
|
|
56
|
+
for y in range(0, h, self.tile_size):
|
|
57
|
+
for x in range(0, w, self.tile_size):
|
|
58
|
+
y_end = min(y + self.tile_size, h)
|
|
59
|
+
x_end = min(x + self.tile_size, w)
|
|
60
|
+
self.print_message(f': fusing tile [{x}, {x_end - 1}]×[{y}, {y_end - 1}]')
|
|
61
|
+
laplacians = []
|
|
62
|
+
for img_index in range(num_images):
|
|
63
|
+
if level < all_level_counts[img_index]:
|
|
64
|
+
full_laplacian = self.load_level(img_index, level)
|
|
65
|
+
tile = full_laplacian[y:y_end, x:x_end]
|
|
66
|
+
laplacians.append(tile)
|
|
67
|
+
del full_laplacian
|
|
68
|
+
stacked = np.stack(laplacians, axis=0)
|
|
69
|
+
fused_tile = self.fuse_laplacian(stacked)
|
|
70
|
+
fused_level[y:y_end, x:x_end] = fused_tile
|
|
71
|
+
del laplacians, stacked, fused_tile
|
|
72
|
+
self.after_step(count)
|
|
73
|
+
self.check_running(self.cleanup_temp_files)
|
|
74
|
+
count += 1
|
|
75
|
+
else:
|
|
76
|
+
laplacians = []
|
|
77
|
+
for img_index in range(num_images):
|
|
78
|
+
if level < all_level_counts[img_index]:
|
|
79
|
+
laplacian = self.load_level(img_index, level)
|
|
80
|
+
laplacians.append(laplacian)
|
|
81
|
+
if level == max_levels - 1:
|
|
82
|
+
stacked = np.stack(laplacians, axis=0)
|
|
83
|
+
fused_level = self.get_fused_base(stacked)
|
|
84
|
+
else:
|
|
85
|
+
stacked = np.stack(laplacians, axis=0)
|
|
86
|
+
fused_level = self.fuse_laplacian(stacked)
|
|
87
|
+
self.check_running(self.cleanup_temp_files)
|
|
88
|
+
fused.append(fused_level)
|
|
89
|
+
count += 1
|
|
90
|
+
self.after_step(count)
|
|
91
|
+
self.check_running(self.cleanup_temp_files)
|
|
92
|
+
self.print_message(': pyramids fusion completed')
|
|
93
|
+
return fused[::-1]
|
|
94
|
+
|
|
95
|
+
def focus_stack(self):
|
|
96
|
+
n = len(self.filenames)
|
|
97
|
+
self.focus_stack_validate(self.cleanup_temp_files)
|
|
98
|
+
all_level_counts = []
|
|
99
|
+
for i, img_path in enumerate(self.filenames):
|
|
100
|
+
self.print_message(f": processing file {img_path.split('/')[-1]}")
|
|
101
|
+
img = read_img(img_path)
|
|
102
|
+
level_count = self.process_single_image(img, self.n_levels, i)
|
|
103
|
+
all_level_counts.append(level_count)
|
|
104
|
+
self.after_step(i + n + 1)
|
|
105
|
+
self.check_running(self.cleanup_temp_files)
|
|
106
|
+
fused_pyramid = self.fuse_pyramids(all_level_counts, n)
|
|
107
|
+
stacked_image = self.collapse(fused_pyramid)
|
|
108
|
+
self.cleanup_temp_files()
|
|
109
|
+
return stacked_image.astype(self.dtype)
|
shinestacker/algorithms/stack.py
CHANGED
|
@@ -5,7 +5,7 @@ from .. config.constants import constants
|
|
|
5
5
|
from .. core.framework import JobBase
|
|
6
6
|
from .. core.colors import color_str
|
|
7
7
|
from .. core.exceptions import InvalidOptionError
|
|
8
|
-
from .utils import write_img
|
|
8
|
+
from .utils import write_img, extension_tif_jpg
|
|
9
9
|
from .stack_framework import FrameDirectory, ActionList
|
|
10
10
|
from .exif import copy_exif_from_file_to_file
|
|
11
11
|
from .denoise import denoise
|
|
@@ -25,8 +25,7 @@ class FocusStackBase(JobBase, FrameDirectory):
|
|
|
25
25
|
|
|
26
26
|
def focus_stack(self, filenames):
|
|
27
27
|
self.sub_message_r(color_str(': reading input files', constants.LOG_COLOR_LEVEL_3))
|
|
28
|
-
|
|
29
|
-
stacked_img = self.stack_algo.focus_stack(img_files)
|
|
28
|
+
stacked_img = self.stack_algo.focus_stack()
|
|
30
29
|
in_filename = filenames[0].split(".")
|
|
31
30
|
out_filename = f"{self.output_dir}/{self.prefix}{in_filename[0]}." + \
|
|
32
31
|
'.'.join(in_filename[1:])
|
|
@@ -37,8 +36,7 @@ class FocusStackBase(JobBase, FrameDirectory):
|
|
|
37
36
|
if self.exif_path != '' and stacked_img.dtype == np.uint8:
|
|
38
37
|
self.sub_message_r(': copy exif data')
|
|
39
38
|
_dirpath, _, fnames = next(os.walk(self.exif_path))
|
|
40
|
-
fnames = [name for name in fnames
|
|
41
|
-
if os.path.splitext(name)[-1][1:].lower() in constants.EXTENSIONS]
|
|
39
|
+
fnames = [name for name in fnames if extension_tif_jpg(name)]
|
|
42
40
|
exif_filename = f"{self.exif_path}/{fnames[0]}"
|
|
43
41
|
copy_exif_from_file_to_file(exif_filename, out_filename)
|
|
44
42
|
self.sub_message_r(' ' * 60)
|
|
@@ -52,6 +50,7 @@ class FocusStackBase(JobBase, FrameDirectory):
|
|
|
52
50
|
self.frame_count += 1
|
|
53
51
|
|
|
54
52
|
def init(self, job, working_path=''):
|
|
53
|
+
FrameDirectory.init(self, job)
|
|
55
54
|
if self.exif_path is None:
|
|
56
55
|
self.exif_path = job.paths[0]
|
|
57
56
|
if self.exif_path != '':
|
|
@@ -79,7 +78,6 @@ class FocusStackBunch(ActionList, FocusStackBase):
|
|
|
79
78
|
"overlap must be smaller than batch size")
|
|
80
79
|
|
|
81
80
|
def init(self, job, _working_path=''):
|
|
82
|
-
FrameDirectory.init(self, job)
|
|
83
81
|
FocusStackBase.init(self, job, self.working_path)
|
|
84
82
|
|
|
85
83
|
def begin(self):
|
|
@@ -94,6 +92,9 @@ class FocusStackBunch(ActionList, FocusStackBase):
|
|
|
94
92
|
def run_step(self):
|
|
95
93
|
self.print_message_r(color_str(f"fusing bunch: {self.count + 1}/{self.counts}",
|
|
96
94
|
constants.LOG_COLOR_LEVEL_2))
|
|
95
|
+
img_files = [os.path.join(self.input_full_path, name)
|
|
96
|
+
for name in self._chunks[self.count - 1]]
|
|
97
|
+
self.stack_algo.init(img_files)
|
|
97
98
|
self.focus_stack(self._chunks[self.count - 1])
|
|
98
99
|
|
|
99
100
|
|
|
@@ -101,13 +102,15 @@ class FocusStack(FocusStackBase):
|
|
|
101
102
|
def __init__(self, name, stack_algo, enabled=True, **kwargs):
|
|
102
103
|
super().__init__(name, stack_algo, enabled, **kwargs)
|
|
103
104
|
self.stack_algo.do_step_callback = True
|
|
105
|
+
self.shape = None
|
|
104
106
|
|
|
105
107
|
def run_core(self):
|
|
106
108
|
self.set_filelist()
|
|
109
|
+
img_files = sorted([os.path.join(self.input_full_path, name) for name in self.filenames])
|
|
110
|
+
self.stack_algo.init(img_files)
|
|
107
111
|
self.callback('step_counts', self.id, self.name,
|
|
108
|
-
self.stack_algo.
|
|
112
|
+
self.stack_algo.total_steps(len(self.filenames)))
|
|
109
113
|
self.focus_stack(self.filenames)
|
|
110
114
|
|
|
111
115
|
def init(self, job, _working_path=''):
|
|
112
|
-
FrameDirectory.init(self, job)
|
|
113
116
|
FocusStackBase.init(self, job, self.working_path)
|
|
@@ -7,7 +7,7 @@ from .. core.colors import color_str
|
|
|
7
7
|
from .. core.framework import Job, ActionList
|
|
8
8
|
from .. core.core_utils import check_path_exists
|
|
9
9
|
from .. core.exceptions import ShapeError, BitDepthError, RunStopException
|
|
10
|
-
from .utils import read_img, write_img
|
|
10
|
+
from .utils import read_img, write_img, extension_tif_jpg
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
class StackJob(Job):
|
|
@@ -50,8 +50,8 @@ class FramePaths:
|
|
|
50
50
|
|
|
51
51
|
def set_filelist(self):
|
|
52
52
|
self.filenames = self.folder_filelist()
|
|
53
|
-
|
|
54
|
-
self.print_message(color_str(f": {len(self.filenames)} files in folder: {
|
|
53
|
+
file_folder = self.input_full_path.replace(self.working_path, '').lstrip('/')
|
|
54
|
+
self.print_message(color_str(f": {len(self.filenames)} files in folder: {file_folder}",
|
|
55
55
|
constants.LOG_COLOR_LEVEL_2))
|
|
56
56
|
|
|
57
57
|
def init(self, job):
|
|
@@ -113,8 +113,7 @@ class FrameDirectory(FramePaths):
|
|
|
113
113
|
def folder_filelist(self):
|
|
114
114
|
src_contents = os.walk(self.input_full_path)
|
|
115
115
|
_dirpath, _, filenames = next(src_contents)
|
|
116
|
-
filelist = [name for name in filenames
|
|
117
|
-
if os.path.splitext(name)[-1][1:].lower() in constants.EXTENSIONS]
|
|
116
|
+
filelist = [name for name in filenames if extension_tif_jpg(name)]
|
|
118
117
|
filelist.sort()
|
|
119
118
|
if self.reverse_order:
|
|
120
119
|
filelist.reverse()
|
|
@@ -159,9 +158,7 @@ class FrameMultiDirectory(FramePaths):
|
|
|
159
158
|
for d, p in zip(dirs, paths):
|
|
160
159
|
filelist = []
|
|
161
160
|
for _dirpath, _, filenames in os.walk(d):
|
|
162
|
-
filelist = [p
|
|
163
|
-
for name in filenames
|
|
164
|
-
if os.path.splitext(name)[-1][1:].lower() in constants.EXTENSIONS]
|
|
161
|
+
filelist = [f"{p}/{name}" for name in filenames if extension_tif_jpg(name)]
|
|
165
162
|
if self.reverse_order:
|
|
166
163
|
filelist.reverse()
|
|
167
164
|
if self.resample > 1:
|
shinestacker/algorithms/utils.py
CHANGED
shinestacker/config/constants.py
CHANGED
|
@@ -34,14 +34,16 @@ class _Constants:
|
|
|
34
34
|
SUB_ACTION_TYPES = [ACTION_MASKNOISE, ACTION_VIGNETTING, ACTION_ALIGNFRAMES,
|
|
35
35
|
ACTION_BALANCEFRAMES]
|
|
36
36
|
STACK_ALGO_PYRAMID = 'Pyramid'
|
|
37
|
+
STACK_ALGO_PYRAMID_TILES = 'Pyramid Tiles'
|
|
37
38
|
STACK_ALGO_DEPTH_MAP = 'Depth map'
|
|
38
|
-
STACK_ALGO_OPTIONS = [STACK_ALGO_PYRAMID, STACK_ALGO_DEPTH_MAP]
|
|
39
|
+
STACK_ALGO_OPTIONS = [STACK_ALGO_PYRAMID, STACK_ALGO_PYRAMID_TILES, STACK_ALGO_DEPTH_MAP]
|
|
39
40
|
STACK_ALGO_DEFAULT = STACK_ALGO_PYRAMID
|
|
40
41
|
DEFAULT_PLOTS_PATH = 'plots'
|
|
41
42
|
|
|
42
43
|
PATH_SEPARATOR = ';'
|
|
43
44
|
|
|
44
45
|
LOG_COLOR_ALERT = 'red'
|
|
46
|
+
LOG_COLOR_WARNING = 'yellow'
|
|
45
47
|
LOG_COLOR_LEVEL_JOB = 'green'
|
|
46
48
|
LOG_COLOR_LEVEL_1 = 'blue'
|
|
47
49
|
LOG_COLOR_LEVEL_2 = 'magenta'
|
|
@@ -107,7 +109,7 @@ class _Constants:
|
|
|
107
109
|
DEFAULT_BORDER_BLUR = 50
|
|
108
110
|
DEFAULT_ALIGN_SUBSAMPLE = 2
|
|
109
111
|
DEFAULT_ALIGN_FAST_SUBSAMPLING = False
|
|
110
|
-
DEFAULT_ALIGN_MIN_GOOD_MATCHES =
|
|
112
|
+
DEFAULT_ALIGN_MIN_GOOD_MATCHES = 50
|
|
111
113
|
|
|
112
114
|
BALANCE_LINEAR = "LINEAR"
|
|
113
115
|
BALANCE_GAMMA = "GAMMA"
|
|
@@ -160,6 +162,7 @@ class _Constants:
|
|
|
160
162
|
DEFAULT_PY_MIN_SIZE = 32
|
|
161
163
|
DEFAULT_PY_KERNEL_SIZE = 5
|
|
162
164
|
DEFAULT_PY_GEN_KERNEL = 0.4
|
|
165
|
+
DEFAULT_PY_TILE_SIZE = 512
|
|
163
166
|
|
|
164
167
|
DEFAULT_PLOT_STACK_BUNCH = False
|
|
165
168
|
DEFAULT_PLOT_STACK = True
|
|
@@ -156,8 +156,8 @@ class FocusStackBaseConfigurator(DefaultActionConfigurator):
|
|
|
156
156
|
combo = self.builder.add_field('stacker', FIELD_COMBO, 'Stacking algorithm', required=True,
|
|
157
157
|
options=constants.STACK_ALGO_OPTIONS,
|
|
158
158
|
default=constants.STACK_ALGO_DEFAULT)
|
|
159
|
-
q_pyramid, q_depthmap = QWidget(), QWidget()
|
|
160
|
-
for q in [q_pyramid, q_depthmap]:
|
|
159
|
+
q_pyramid, q_pyramid_tiles, q_depthmap = QWidget(), QWidget(), QWidget()
|
|
160
|
+
for q in [q_pyramid, q_pyramid_tiles, q_depthmap]:
|
|
161
161
|
layout = QFormLayout()
|
|
162
162
|
layout.setFieldGrowthPolicy(QFormLayout.AllNonFixedFieldsGrow)
|
|
163
163
|
layout.setRowWrapPolicy(QFormLayout.DontWrapRows)
|
|
@@ -166,13 +166,16 @@ class FocusStackBaseConfigurator(DefaultActionConfigurator):
|
|
|
166
166
|
q.setLayout(layout)
|
|
167
167
|
stacked = QStackedWidget()
|
|
168
168
|
stacked.addWidget(q_pyramid)
|
|
169
|
+
stacked.addWidget(q_pyramid_tiles)
|
|
169
170
|
stacked.addWidget(q_depthmap)
|
|
170
171
|
|
|
171
172
|
def change():
|
|
172
173
|
text = combo.currentText()
|
|
173
|
-
if text ==
|
|
174
|
+
if text == constants.STACK_ALGO_PYRAMID:
|
|
174
175
|
stacked.setCurrentWidget(q_pyramid)
|
|
175
|
-
|
|
176
|
+
if text == constants.STACK_ALGO_PYRAMID_TILES:
|
|
177
|
+
stacked.setCurrentWidget(q_pyramid_tiles)
|
|
178
|
+
elif text == constants.STACK_ALGO_DEPTH_MAP:
|
|
176
179
|
stacked.setCurrentWidget(q_depthmap)
|
|
177
180
|
change()
|
|
178
181
|
if self.expert:
|
|
@@ -191,6 +194,25 @@ class FocusStackBaseConfigurator(DefaultActionConfigurator):
|
|
|
191
194
|
options=self.FLOAT_OPTIONS, values=constants.VALID_FLOATS,
|
|
192
195
|
default=dict(zip(constants.VALID_FLOATS,
|
|
193
196
|
self.FLOAT_OPTIONS))[constants.DEFAULT_PY_FLOAT])
|
|
197
|
+
self.builder.add_field('tiles_pyramid_min_size', FIELD_INT, 'Minimum size (px)',
|
|
198
|
+
required=False, add_to_layout=q_pyramid_tiles.layout(),
|
|
199
|
+
default=constants.DEFAULT_PY_MIN_SIZE, min_val=2, max_val=256)
|
|
200
|
+
self.builder.add_field('tiles_pyramid_kernel_size', FIELD_INT, 'Kernel size (px)',
|
|
201
|
+
required=False, add_to_layout=q_pyramid_tiles.layout(),
|
|
202
|
+
default=constants.DEFAULT_PY_KERNEL_SIZE, min_val=3, max_val=21)
|
|
203
|
+
self.builder.add_field('tiles_pyramid_gen_kernel', FIELD_FLOAT, 'Gen. kernel',
|
|
204
|
+
required=False, add_to_layout=q_pyramid_tiles.layout(),
|
|
205
|
+
default=constants.DEFAULT_PY_GEN_KERNEL,
|
|
206
|
+
min_val=0.0, max_val=2.0)
|
|
207
|
+
self.builder.add_field('tiles_pyramid_float_type', FIELD_COMBO, 'Precision',
|
|
208
|
+
required=False, add_to_layout=q_pyramid_tiles.layout(),
|
|
209
|
+
options=self.FLOAT_OPTIONS, values=constants.VALID_FLOATS,
|
|
210
|
+
default=dict(zip(constants.VALID_FLOATS,
|
|
211
|
+
self.FLOAT_OPTIONS))[constants.DEFAULT_PY_FLOAT])
|
|
212
|
+
self.builder.add_field('tiles_pyramid_tile_size', FIELD_INT, 'Tile size (px)',
|
|
213
|
+
required=False, add_to_layout=q_pyramid_tiles.layout(),
|
|
214
|
+
default=constants.DEFAULT_PY_TILE_SIZE,
|
|
215
|
+
min_val=128, max_val=2048)
|
|
194
216
|
self.builder.add_field('depthmap_energy', FIELD_COMBO, 'Energy', required=False,
|
|
195
217
|
add_to_layout=q_depthmap.layout(),
|
|
196
218
|
options=self.ENERGY_OPTIONS, values=constants.VALID_DM_ENERGY,
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# pylint: disable=C0114, C0115, C0116, E0611, C0103, R0914
|
|
2
|
+
from PySide6.QtWidgets import QLayout
|
|
3
|
+
from PySide6.QtCore import Qt, QRect, QSize, QPoint
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class FlowLayout(QLayout):
|
|
7
|
+
def __init__(self, parent=None, margin=0, spacing=-1, justify=True):
|
|
8
|
+
super().__init__(parent)
|
|
9
|
+
self._item_list = []
|
|
10
|
+
self._justify = justify
|
|
11
|
+
self.setContentsMargins(margin, margin, margin, margin)
|
|
12
|
+
self.setSpacing(spacing)
|
|
13
|
+
|
|
14
|
+
def addItem(self, item):
|
|
15
|
+
self._item_list.append(item)
|
|
16
|
+
|
|
17
|
+
def count(self):
|
|
18
|
+
return len(self._item_list)
|
|
19
|
+
|
|
20
|
+
def itemAt(self, index):
|
|
21
|
+
if 0 <= index < len(self._item_list):
|
|
22
|
+
return self._item_list[index]
|
|
23
|
+
return None
|
|
24
|
+
|
|
25
|
+
def takeAt(self, index):
|
|
26
|
+
if 0 <= index < len(self._item_list):
|
|
27
|
+
return self._item_list.pop(index)
|
|
28
|
+
return None
|
|
29
|
+
|
|
30
|
+
def expandingDirections(self):
|
|
31
|
+
return Qt.Orientations(0)
|
|
32
|
+
|
|
33
|
+
def hasHeightForWidth(self):
|
|
34
|
+
return True
|
|
35
|
+
|
|
36
|
+
def heightForWidth(self, width):
|
|
37
|
+
return self._do_layout(QRect(0, 0, width, 0), True)
|
|
38
|
+
|
|
39
|
+
def setGeometry(self, rect):
|
|
40
|
+
super().setGeometry(rect)
|
|
41
|
+
self._do_layout(rect, False)
|
|
42
|
+
|
|
43
|
+
def sizeHint(self):
|
|
44
|
+
return self.minimumSize()
|
|
45
|
+
|
|
46
|
+
def minimumSize(self):
|
|
47
|
+
size = QSize()
|
|
48
|
+
for item in self._item_list:
|
|
49
|
+
size = size.expandedTo(item.minimumSize())
|
|
50
|
+
margins = self.contentsMargins()
|
|
51
|
+
size += QSize(margins.left() + margins.right(), margins.top() + margins.bottom())
|
|
52
|
+
return size
|
|
53
|
+
|
|
54
|
+
def setJustify(self, justify):
|
|
55
|
+
self._justify = justify
|
|
56
|
+
self.invalidate()
|
|
57
|
+
|
|
58
|
+
def justify(self):
|
|
59
|
+
return self._justify
|
|
60
|
+
|
|
61
|
+
def _do_layout(self, rect, test_only):
|
|
62
|
+
x = rect.x()
|
|
63
|
+
y = rect.y()
|
|
64
|
+
line_height = 0
|
|
65
|
+
spacing = self.spacing()
|
|
66
|
+
lines = []
|
|
67
|
+
current_line = []
|
|
68
|
+
current_line_width = 0
|
|
69
|
+
for item in self._item_list:
|
|
70
|
+
space_x = spacing
|
|
71
|
+
next_x = x + item.sizeHint().width() + space_x
|
|
72
|
+
if next_x - space_x > rect.right() and line_height > 0:
|
|
73
|
+
lines.append((current_line, current_line_width, line_height))
|
|
74
|
+
x = rect.x()
|
|
75
|
+
y = y + line_height + spacing
|
|
76
|
+
next_x = x + item.sizeHint().width() + space_x
|
|
77
|
+
current_line = []
|
|
78
|
+
current_line_width = 0
|
|
79
|
+
line_height = 0
|
|
80
|
+
current_line.append(item)
|
|
81
|
+
current_line_width += item.sizeHint().width()
|
|
82
|
+
x = next_x
|
|
83
|
+
line_height = max(line_height, item.sizeHint().height())
|
|
84
|
+
if current_line:
|
|
85
|
+
lines.append((current_line, current_line_width, line_height))
|
|
86
|
+
y_offset = rect.y()
|
|
87
|
+
for line, line_width, line_height in lines:
|
|
88
|
+
if not test_only:
|
|
89
|
+
available_width = rect.width() - (len(line) - 1) * spacing
|
|
90
|
+
if self._justify and len(line) > 1:
|
|
91
|
+
stretch_factor = available_width / line_width if line_width > 0 else 1
|
|
92
|
+
x_offset = rect.x()
|
|
93
|
+
for item in line:
|
|
94
|
+
item_width = int(item.sizeHint().width() * stretch_factor)
|
|
95
|
+
item.setGeometry(QRect(QPoint(x_offset, y_offset),
|
|
96
|
+
QSize(item_width, line_height)))
|
|
97
|
+
x_offset += item_width + spacing
|
|
98
|
+
else:
|
|
99
|
+
x_offset = rect.x()
|
|
100
|
+
for item in line:
|
|
101
|
+
item.setGeometry(QRect(QPoint(x_offset, y_offset),
|
|
102
|
+
item.sizeHint()))
|
|
103
|
+
x_offset += item.sizeHint().width() + spacing
|
|
104
|
+
y_offset += line_height + spacing
|
|
105
|
+
return y_offset - spacing - rect.y()
|
shinestacker/gui/gui_run.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# pylint: disable=C0114, C0115, C0116, E0611, R0903, R0915, R0914, R0917, R0913, R0902
|
|
2
2
|
import os
|
|
3
|
+
import traceback
|
|
3
4
|
from PySide6.QtWidgets import (QWidget, QPushButton, QVBoxLayout, QHBoxLayout,
|
|
4
5
|
QMessageBox, QScrollArea, QSizePolicy, QFrame, QLabel, QComboBox)
|
|
5
6
|
from PySide6.QtGui import QColor
|
|
@@ -16,6 +17,7 @@ from .colors import (
|
|
|
16
17
|
ACTION_RUNNING_COLOR, ACTION_COMPLETED_COLOR,
|
|
17
18
|
ACTION_STOPPED_COLOR, ACTION_FAILED_COLOR)
|
|
18
19
|
from .time_progress_bar import TimerProgressBar
|
|
20
|
+
from .flow_layout import FlowLayout
|
|
19
21
|
|
|
20
22
|
|
|
21
23
|
class ColorButton(QPushButton):
|
|
@@ -55,12 +57,12 @@ class RunWindow(QTextEditLogger):
|
|
|
55
57
|
for label_row in labels:
|
|
56
58
|
self.color_widgets.append([])
|
|
57
59
|
row = QWidget(self)
|
|
58
|
-
h_layout = QHBoxLayout(row)
|
|
60
|
+
h_layout = FlowLayout(row) # QHBoxLayout(row)
|
|
59
61
|
h_layout.setContentsMargins(0, 0, 0, 0)
|
|
60
62
|
h_layout.setSpacing(2)
|
|
61
63
|
for label, enabled in label_row:
|
|
62
64
|
widget = ColorButton(label, enabled)
|
|
63
|
-
h_layout.addWidget(widget, stretch=1)
|
|
65
|
+
h_layout.addWidget(widget) # addWidget(widget, stretch=1)
|
|
64
66
|
self.color_widgets[-1].append(widget)
|
|
65
67
|
layout.addWidget(row)
|
|
66
68
|
self.progress_bar = TimerProgressBar()
|
|
@@ -202,23 +204,26 @@ class RunWindow(QTextEditLogger):
|
|
|
202
204
|
label = QLabel(name, self)
|
|
203
205
|
label.setStyleSheet("QLabel {margin-top: 5px; font-weight: bold;}")
|
|
204
206
|
self.image_layout.addWidget(label)
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
self.right_area.verticalScrollBar().
|
|
207
|
+
try:
|
|
208
|
+
if extension_pdf(path):
|
|
209
|
+
image_view = GuiPdfView(path, self)
|
|
210
|
+
elif extension_tif_jpg(path):
|
|
211
|
+
image_view = GuiImageView(path, self)
|
|
212
|
+
else:
|
|
213
|
+
raise RuntimeError(f"Can't visualize file type {os.path.splitext(path)[1]}.")
|
|
214
|
+
self.image_views.append(image_view)
|
|
215
|
+
self.image_layout.addWidget(image_view)
|
|
216
|
+
max_width = max(pv.size().width() for pv in self.image_views) if self.image_views else 0
|
|
217
|
+
needed_width = max_width + 20
|
|
218
|
+
self.right_area.setFixedWidth(needed_width)
|
|
219
|
+
self.image_area_widget.setFixedWidth(needed_width)
|
|
220
|
+
self.right_area.updateGeometry()
|
|
221
|
+
self.image_area_widget.updateGeometry()
|
|
222
|
+
QTimer.singleShot(
|
|
223
|
+
0, lambda: self.right_area.verticalScrollBar().setValue(
|
|
224
|
+
self.right_area.verticalScrollBar().maximum()))
|
|
225
|
+
except RuntimeError as e:
|
|
226
|
+
traceback.print_tb(e.__traceback__)
|
|
222
227
|
|
|
223
228
|
@Slot(int, str, str, str)
|
|
224
229
|
def handle_open_app(self, _run_id, name, app, path):
|
shinestacker/gui/main_window.py
CHANGED
|
@@ -406,8 +406,8 @@ class MainWindow(QMainWindow, LogManager):
|
|
|
406
406
|
if self.job_list_count() == 0:
|
|
407
407
|
self.menu_manager.add_action_entry_action.setEnabled(False)
|
|
408
408
|
self.menu_manager.action_selector.setEnabled(False)
|
|
409
|
-
self.run_job_action.setEnabled(False)
|
|
410
|
-
self.run_all_jobs_action.setEnabled(False)
|
|
409
|
+
self.menu_manager.run_job_action.setEnabled(False)
|
|
410
|
+
self.menu_manager.run_all_jobs_action.setEnabled(False)
|
|
411
411
|
else:
|
|
412
412
|
self.menu_manager.add_action_entry_action.setEnabled(True)
|
|
413
413
|
self.menu_manager.action_selector.setEnabled(True)
|
shinestacker/gui/new_project.py
CHANGED
|
@@ -110,6 +110,7 @@ class NewProjectDialog(BaseFormDialog):
|
|
|
110
110
|
self.layout.addRow("Focus stack (depth map):", self.focus_stack_depth_map)
|
|
111
111
|
else:
|
|
112
112
|
self.layout.addRow("Focus stack:", self.focus_stack_pyramid)
|
|
113
|
+
if self.expert():
|
|
113
114
|
self.layout.addRow("Save multi layer TIFF:", self.multi_layer)
|
|
114
115
|
self.add_label("")
|
|
115
116
|
self.add_bold_label("3️⃣ Push 🆗 for further options, then press ▶️ to run.")
|
|
@@ -290,6 +290,7 @@ class ProjectController(QObject):
|
|
|
290
290
|
with open(file_path, 'w', encoding="utf-8") as f:
|
|
291
291
|
f.write(json_obj)
|
|
292
292
|
self.mark_as_modified(False)
|
|
293
|
+
self.update_title_requested.emit()
|
|
293
294
|
except Exception as e:
|
|
294
295
|
QMessageBox.critical(self.parent, "Error", f"Cannot save file:\n{str(e)}")
|
|
295
296
|
|
|
@@ -10,6 +10,7 @@ from .. algorithms.align import AlignFrames
|
|
|
10
10
|
from .. algorithms.balance import BalanceFrames
|
|
11
11
|
from .. algorithms.stack import FocusStack, FocusStackBunch
|
|
12
12
|
from .. algorithms.pyramid import PyramidStack
|
|
13
|
+
from .. algorithms.pyramid_tiles import PyramidTilesStack
|
|
13
14
|
from .. algorithms.depth_map import DepthMapStack
|
|
14
15
|
from .. algorithms.multilayer import MultiLayer
|
|
15
16
|
from .project_model import Project, ActionConfig
|
|
@@ -104,21 +105,26 @@ class ProjectConverter:
|
|
|
104
105
|
constants.ACTION_FOCUSSTACKBUNCH):
|
|
105
106
|
stacker = action_config.params.get('stacker', constants.STACK_ALGO_DEFAULT)
|
|
106
107
|
if stacker == constants.STACK_ALGO_PYRAMID:
|
|
107
|
-
algo_dict, module_dict = self.filter_dict_keys(
|
|
108
|
+
algo_dict, module_dict = self.filter_dict_keys(
|
|
109
|
+
action_config.params, 'pyramid_')
|
|
108
110
|
stack_algo = PyramidStack(**algo_dict)
|
|
111
|
+
elif stacker == constants.STACK_ALGO_PYRAMID_TILES:
|
|
112
|
+
algo_dict, module_dict = self.filter_dict_keys(
|
|
113
|
+
action_config.params, 'tiles_pyramid_')
|
|
114
|
+
stack_algo = PyramidTilesStack(**algo_dict)
|
|
109
115
|
elif stacker == constants.STACK_ALGO_DEPTH_MAP:
|
|
110
|
-
algo_dict, module_dict = self.filter_dict_keys(
|
|
116
|
+
algo_dict, module_dict = self.filter_dict_keys(
|
|
117
|
+
action_config.params, 'depthmap_')
|
|
111
118
|
stack_algo = DepthMapStack(**algo_dict)
|
|
112
119
|
else:
|
|
113
120
|
raise InvalidOptionError('stacker', stacker, f"valid options are: "
|
|
114
121
|
f"{constants.STACK_ALGO_PYRAMID}, "
|
|
122
|
+
f"{constants.STACK_ALGO_PYRAMID_TILES}, "
|
|
115
123
|
f"{constants.STACK_ALGO_DEPTH_MAP}")
|
|
116
124
|
if action_config.type_name == constants.ACTION_FOCUSSTACK:
|
|
117
125
|
return FocusStack(**module_dict, stack_algo=stack_algo)
|
|
118
126
|
if action_config.type_name == constants.ACTION_FOCUSSTACKBUNCH:
|
|
119
127
|
return FocusStackBunch(**module_dict, stack_algo=stack_algo)
|
|
120
|
-
raise InvalidOptionError(
|
|
121
|
-
"stracker", stacker, details="valid values are: Pyramid, Depth map.")
|
|
122
128
|
if action_config.type_name == constants.ACTION_MULTILAYER:
|
|
123
129
|
input_path = list(filter(lambda p: p != '',
|
|
124
130
|
action_config.params.get('input_path', '').split(";")))
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: shinestacker
|
|
3
|
-
Version: 1.0
|
|
3
|
+
Version: 1.1.0
|
|
4
4
|
Summary: ShineStacker
|
|
5
5
|
Author-email: Luca Lista <luka.lista@gmail.com>
|
|
6
6
|
License-Expression: LGPL-3.0
|
|
@@ -69,6 +69,25 @@ The GUI has two main working areas:
|
|
|
69
69
|
|
|
70
70
|
<img src='https://raw.githubusercontent.com/lucalista/shinestacker/main/img/gui-retouch.png' width="600" referrerpolicy="no-referrer">
|
|
71
71
|
|
|
72
|
+
# Note for macOS users
|
|
73
|
+
|
|
74
|
+
**The following note is only relevant if you download the application as compressed archive from the [release page](https://github.com/lucalista/shinestacker/releases).**
|
|
75
|
+
|
|
76
|
+
The macOS system security protection prevent to run applications downloaded from the web that come from developers that don't hold an Apple Developer Certificate.
|
|
77
|
+
|
|
78
|
+
In order to prevent this, follow the instructions below:
|
|
79
|
+
|
|
80
|
+
1. Download the compressed archive ```shinestacker-macos.tar.gz``` in your ```Download``` folder.
|
|
81
|
+
2. Double-click the archive to uncompress it. You will find a new folder ```shinestacker```.
|
|
82
|
+
3. Open a terminal (*Applications > Utilities > Terminal*)
|
|
83
|
+
4. Type the folliwng command on the terminal:
|
|
84
|
+
```bash
|
|
85
|
+
xattr -cr ~/Downloads/shinestacker/shinestacker.app
|
|
86
|
+
```
|
|
87
|
+
5. Now you can double-click the Sine Stacker icon app in the ```shiestacker``` folder and it should run.
|
|
88
|
+
|
|
89
|
+
macOS adds a quarantine flag to all files downloaded from the internet. The above command removes that flag while preserving all other application functionality.
|
|
90
|
+
|
|
72
91
|
# Resources
|
|
73
92
|
|
|
74
93
|
🌍 [Website on WordPress](https://shinestacker.wordpress.com) • 📖 [Main documentation](https://shinestacker.readthedocs.io) • 📝 [Changelog](https://github.com/lucalista/shinestacker/blob/main/CHANGELOG.md)
|
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
shinestacker/__init__.py,sha256=uq2fjAw2z_6TpH3mOcWFZ98GoEPRsNhTAK8N0MMm_e8,448
|
|
2
|
-
shinestacker/_version.py,sha256=
|
|
2
|
+
shinestacker/_version.py,sha256=XIz3qAg9G9YysQi3Ryp0CN3rtc_JiecHZ9L2vEzcM6s,21
|
|
3
3
|
shinestacker/algorithms/__init__.py,sha256=c4kRrdTLlVI70Q16XkI1RSmz5MD7npDqIpO_02jTG6g,747
|
|
4
|
-
shinestacker/algorithms/align.py,sha256=
|
|
4
|
+
shinestacker/algorithms/align.py,sha256=EhsV50QtpLSJG0uDMfOJw89u8CGFJvBC2sYuJg5cv6g,17516
|
|
5
5
|
shinestacker/algorithms/balance.py,sha256=iSjO-pl0vQv58iEQ077EUcDTAExMKDBdtXmJXbMhazk,16721
|
|
6
|
-
shinestacker/algorithms/base_stack_algo.py,sha256=
|
|
6
|
+
shinestacker/algorithms/base_stack_algo.py,sha256=O7pDXqLM8MBdLR634Vk3UNV6cEV2q0U7CNcnpC_AOig,2363
|
|
7
7
|
shinestacker/algorithms/denoise.py,sha256=GL3Z4_6MHxSa7Wo4ZzQECZS87tHBFqO0sIVF_jPuYQU,426
|
|
8
|
-
shinestacker/algorithms/depth_map.py,sha256=
|
|
8
|
+
shinestacker/algorithms/depth_map.py,sha256=KVThrnynPKuijlh-DrenSkdkZ0Qm6TaNMYKhRByhcN4,5682
|
|
9
9
|
shinestacker/algorithms/exif.py,sha256=SM4ZDDe8hCJ3xY6053FNndOiwzEStzdp0WrXurlcHVc,9429
|
|
10
10
|
shinestacker/algorithms/multilayer.py,sha256=-pQXDlooSMGKPhMgF-_naXdkGdolclYvSD-RrjwLiyI,9328
|
|
11
|
-
shinestacker/algorithms/noise_detection.py,sha256=
|
|
12
|
-
shinestacker/algorithms/pyramid.py,sha256=
|
|
11
|
+
shinestacker/algorithms/noise_detection.py,sha256=CJb57mE7ePJBgrwnsEkeK8xVIl2Hrzti11ZEI6JQczs,9218
|
|
12
|
+
shinestacker/algorithms/pyramid.py,sha256=cxwA6gf02009dFv5-m79NpJkD58-Wbu3im4bfA5HVUc,8822
|
|
13
|
+
shinestacker/algorithms/pyramid_tiles.py,sha256=967L42MfwSOewisqpAzuXivZgVoKZbIjDIQVWP1_rHk,5094
|
|
13
14
|
shinestacker/algorithms/sharpen.py,sha256=h7PMJBYxucg194Usp_6pvItPUMFYbT-ebAc_-7XBFUw,949
|
|
14
|
-
shinestacker/algorithms/stack.py,sha256=
|
|
15
|
-
shinestacker/algorithms/stack_framework.py,sha256=
|
|
16
|
-
shinestacker/algorithms/utils.py,sha256=
|
|
15
|
+
shinestacker/algorithms/stack.py,sha256=T1y-qoYUNzcIpnhKcou_4ifiKtGC2ZA1bOZXlfnKB6A,5045
|
|
16
|
+
shinestacker/algorithms/stack_framework.py,sha256=fHdU8uYZquRut6NW_1vHbTCjPD99gQfOhVDdaaLZH34,12334
|
|
17
|
+
shinestacker/algorithms/utils.py,sha256=GSKPUxU98Q8F0k4TgY9ydEgBul1gf2I0ypdmyDm--Mg,3371
|
|
17
18
|
shinestacker/algorithms/vignetting.py,sha256=yW-1TF4tesLWfKQOS0XxRkOEN82U-YDmMaj09C9cH4M,9552
|
|
18
19
|
shinestacker/algorithms/white_balance.py,sha256=PMKsBtxOSn5aRr_Gkx1StHS4eN6kBN2EhNnhg4UG24g,501
|
|
19
20
|
shinestacker/app/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -26,7 +27,7 @@ shinestacker/app/project.py,sha256=W0u715LZne_PNJvg9msSy27ybIjgDXiEAQdJ7_6BjYI,2
|
|
|
26
27
|
shinestacker/app/retouch.py,sha256=ZQ-nRKnHo6xurcP34RNqaAWkmuGBjJ5jE05hTQ_ycis,2482
|
|
27
28
|
shinestacker/config/__init__.py,sha256=aXxi-LmAvXd0daIFrVnTHE5OCaYeK1uf1BKMr7oaXQs,197
|
|
28
29
|
shinestacker/config/config.py,sha256=eBko2D3ADhLTIm9X6hB_a_WsIjwgfE-qmBVkhP1XSvc,1636
|
|
29
|
-
shinestacker/config/constants.py,sha256=
|
|
30
|
+
shinestacker/config/constants.py,sha256=X9e0fXr7ZHN9DCEMiObpanNJfZ5cMgWJdm3Xybmh-Wk,6232
|
|
30
31
|
shinestacker/config/gui_constants.py,sha256=5DR-ET1oeMMD7lIsjvAwSuln89A7I9wy9VuAeRo2G64,2575
|
|
31
32
|
shinestacker/core/__init__.py,sha256=IUEIx6SQ3DygDEHN3_E6uKpHjHtUa4a_U_1dLd_8yEU,484
|
|
32
33
|
shinestacker/core/colors.py,sha256=kr_tJA1iRsdck2JaYDb2lS-codZ4Ty9gdu3kHfiWvuM,1340
|
|
@@ -36,22 +37,23 @@ shinestacker/core/framework.py,sha256=zCnJuQrHNpwEgJW23_BgS7iQrLolRWTAMB1oRp_a7K
|
|
|
36
37
|
shinestacker/core/logging.py,sha256=9SuSSy9Usbh7zqmLYMqkmy-VBkOJW000lwqAR0XQs30,3067
|
|
37
38
|
shinestacker/gui/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
38
39
|
shinestacker/gui/action_config.py,sha256=yXNDv0MyONbHk4iUrkvMkLKKaDvpJyzA5Yr0Eikgo0c,16986
|
|
39
|
-
shinestacker/gui/action_config_dialog.py,sha256=
|
|
40
|
+
shinestacker/gui/action_config_dialog.py,sha256=Xb8DryEQt9R3VvM2DVoT_SdlJMU_qzJs2Z7NdLeqsgY,34520
|
|
40
41
|
shinestacker/gui/base_form_dialog.py,sha256=yYqMee1mzw9VBx8siBS0jDk1qqsTIKJUgdjh92aprQk,687
|
|
41
42
|
shinestacker/gui/colors.py,sha256=m0pQQ-uvtIN1xmb_-N06BvC7pZYZZnq59ZSEJwutHuk,1432
|
|
43
|
+
shinestacker/gui/flow_layout.py,sha256=3yBU_z7VtvHKpx1H97CHVd81eq9pe1Dcja2EZBGGKcI,3791
|
|
42
44
|
shinestacker/gui/gui_images.py,sha256=e0KAXSPruZoRHrajfdlmOKBYoRJJQBDan1jgs7YFltY,5678
|
|
43
45
|
shinestacker/gui/gui_logging.py,sha256=kiZcrC2AFYCWgPZo0O5SKw-E5cFrezwf4anS3HjPuNw,8168
|
|
44
|
-
shinestacker/gui/gui_run.py,sha256=
|
|
45
|
-
shinestacker/gui/main_window.py,sha256=
|
|
46
|
+
shinestacker/gui/gui_run.py,sha256=ahbl6xMFR78QrcBbEDMuaQpkxw6DBFtSX8DCMIyr_7I,15439
|
|
47
|
+
shinestacker/gui/main_window.py,sha256=KVr3ApbQSjJgmhHnrcqTjGQNTj1LoTN2PD6bWLBjsh8,24280
|
|
46
48
|
shinestacker/gui/menu_manager.py,sha256=_L6LOikB3impEYqilqwXc0WJuunishjz57ozZlrBn7Q,9616
|
|
47
|
-
shinestacker/gui/new_project.py,sha256=
|
|
48
|
-
shinestacker/gui/project_controller.py,sha256=
|
|
49
|
-
shinestacker/gui/project_converter.py,sha256=
|
|
49
|
+
shinestacker/gui/new_project.py,sha256=c0y2BjnAVaf5Z88UDmuOGR5rdju0Q72ltqiE7T3QivY,10807
|
|
50
|
+
shinestacker/gui/project_controller.py,sha256=zVMH8kcNJ75dXPjaTa0IQiavqcWxG1URlVVYWnnu1C0,15123
|
|
51
|
+
shinestacker/gui/project_converter.py,sha256=8ko3D4D7x4hhwENxwpTeElnLtEex3lpR51nZbq30Uco,7655
|
|
50
52
|
shinestacker/gui/project_editor.py,sha256=uouzmUkrqouQlq-dqPOgSO16r1WOnGNV2v8jTcZlRXU,23749
|
|
51
53
|
shinestacker/gui/project_model.py,sha256=eRUmH3QmRzDtPtZoxgT6amKzN8_5XzwjHgEJeL-_JOE,4263
|
|
52
54
|
shinestacker/gui/select_path_widget.py,sha256=OfQImOmkzbvl5BBshmb7ePWrSGDJQ8VvyaAOypHAGd4,1023
|
|
53
55
|
shinestacker/gui/tab_widget.py,sha256=6iUifK-wu0EzjVFccKHirhA2fENglVi6xREKiD96aaY,2950
|
|
54
|
-
shinestacker/gui/time_progress_bar.py,sha256=
|
|
56
|
+
shinestacker/gui/time_progress_bar.py,sha256=4_5DT_EzFdVJi5bgd9TEpoTJXeU3M08CF91cZLi75Wc,3016
|
|
55
57
|
shinestacker/gui/ico/focus_stack_bkg.png,sha256=Q86TgqvKEi_IzKI8m6aZB2a3T40UkDtexf2PdeBM9XE,163151
|
|
56
58
|
shinestacker/gui/ico/shinestacker.icns,sha256=3IshIOv0uFexYsAEPkE9xiyuw8mB5X5gffekOUhFlt0,45278
|
|
57
59
|
shinestacker/gui/ico/shinestacker.ico,sha256=8IMRk-toObWUz8iDXA-zHBWQ8Ps3vXN5u5ZEyw7sP3c,109613
|
|
@@ -83,9 +85,9 @@ shinestacker/retouch/undo_manager.py,sha256=_ekbcOLcPbQLY7t-o8wf-b1uA6OPY9rRyLM-
|
|
|
83
85
|
shinestacker/retouch/unsharp_mask_filter.py,sha256=uFnth8fpZFGhdIgJCnS8x5v6lBQgJ3hX0CBke9pFXeM,3510
|
|
84
86
|
shinestacker/retouch/vignetting_filter.py,sha256=3WuoF38lQOIaU1MWmqviItuQn8NnbMN0nwV7pM9IJqU,3453
|
|
85
87
|
shinestacker/retouch/white_balance_filter.py,sha256=glMBYlmrF-i_OrB3sGUpjZE6X4FQdyLC4GBy2bWtaFc,6056
|
|
86
|
-
shinestacker-1.0.
|
|
87
|
-
shinestacker-1.0.
|
|
88
|
-
shinestacker-1.0.
|
|
89
|
-
shinestacker-1.0.
|
|
90
|
-
shinestacker-1.0.
|
|
91
|
-
shinestacker-1.0.
|
|
88
|
+
shinestacker-1.1.0.dist-info/licenses/LICENSE,sha256=pWgb-bBdsU2Gd2kwAXxketnm5W_2u8_fIeWEgojfrxs,7651
|
|
89
|
+
shinestacker-1.1.0.dist-info/METADATA,sha256=cg9TAx9qmIME39z02b5lLxjbtXhf_sT0aGNsfHh296E,6951
|
|
90
|
+
shinestacker-1.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
91
|
+
shinestacker-1.1.0.dist-info/entry_points.txt,sha256=SY6g1LqtMmp23q1DGwLUDT_dhLX9iss8DvWkiWLyo_4,166
|
|
92
|
+
shinestacker-1.1.0.dist-info/top_level.txt,sha256=MhijwnBVX5psfsyX8JZjqp3SYiWPsKe69f3Gnyze4Fw,13
|
|
93
|
+
shinestacker-1.1.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|