shinestacker 1.3.0__py3-none-any.whl → 1.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 +229 -41
- shinestacker/algorithms/align_auto.py +15 -3
- shinestacker/algorithms/align_parallel.py +81 -25
- shinestacker/algorithms/balance.py +23 -13
- shinestacker/algorithms/base_stack_algo.py +14 -20
- shinestacker/algorithms/depth_map.py +9 -14
- shinestacker/algorithms/noise_detection.py +3 -1
- shinestacker/algorithms/pyramid.py +8 -22
- shinestacker/algorithms/pyramid_auto.py +5 -14
- shinestacker/algorithms/pyramid_tiles.py +18 -20
- shinestacker/algorithms/stack_framework.py +1 -1
- shinestacker/algorithms/utils.py +37 -10
- shinestacker/algorithms/vignetting.py +2 -0
- shinestacker/app/gui_utils.py +10 -0
- shinestacker/app/main.py +3 -1
- shinestacker/app/project.py +3 -1
- shinestacker/app/retouch.py +3 -1
- shinestacker/config/gui_constants.py +2 -2
- shinestacker/core/core_utils.py +10 -1
- shinestacker/gui/action_config.py +172 -7
- shinestacker/gui/action_config_dialog.py +443 -452
- shinestacker/gui/colors.py +1 -0
- shinestacker/gui/folder_file_selection.py +5 -0
- shinestacker/gui/gui_run.py +2 -2
- shinestacker/gui/main_window.py +18 -9
- shinestacker/gui/menu_manager.py +26 -2
- shinestacker/gui/new_project.py +5 -5
- shinestacker/gui/project_controller.py +4 -0
- shinestacker/gui/project_editor.py +6 -4
- shinestacker/gui/recent_file_manager.py +93 -0
- shinestacker/gui/sys_mon.py +24 -23
- shinestacker/retouch/base_filter.py +5 -5
- shinestacker/retouch/brush_preview.py +3 -0
- shinestacker/retouch/brush_tool.py +11 -11
- shinestacker/retouch/display_manager.py +21 -37
- shinestacker/retouch/image_editor_ui.py +129 -71
- shinestacker/retouch/image_view_status.py +61 -0
- shinestacker/retouch/image_viewer.py +89 -431
- shinestacker/retouch/io_gui_handler.py +12 -2
- shinestacker/retouch/overlaid_view.py +212 -0
- shinestacker/retouch/shortcuts_help.py +13 -3
- shinestacker/retouch/sidebyside_view.py +479 -0
- shinestacker/retouch/view_strategy.py +466 -0
- {shinestacker-1.3.0.dist-info → shinestacker-1.4.0.dist-info}/METADATA +1 -1
- {shinestacker-1.3.0.dist-info → shinestacker-1.4.0.dist-info}/RECORD +50 -45
- {shinestacker-1.3.0.dist-info → shinestacker-1.4.0.dist-info}/WHEEL +0 -0
- {shinestacker-1.3.0.dist-info → shinestacker-1.4.0.dist-info}/entry_points.txt +0 -0
- {shinestacker-1.3.0.dist-info → shinestacker-1.4.0.dist-info}/licenses/LICENSE +0 -0
- {shinestacker-1.3.0.dist-info → shinestacker-1.4.0.dist-info}/top_level.txt +0 -0
|
@@ -8,9 +8,11 @@ from scipy.interpolate import interp1d
|
|
|
8
8
|
from .. config.constants import constants
|
|
9
9
|
from .. core.exceptions import InvalidOptionError
|
|
10
10
|
from .. core.colors import color_str
|
|
11
|
+
from .. core.core_utils import setup_matplotlib_mode
|
|
11
12
|
from .utils import (read_img, save_plot, img_subsample, bgr_to_hsv, bgr_to_hls,
|
|
12
13
|
hsv_to_bgr, hls_to_bgr, bgr_to_lab, lab_to_bgr)
|
|
13
14
|
from .stack_framework import SubAction
|
|
15
|
+
setup_matplotlib_mode()
|
|
14
16
|
|
|
15
17
|
|
|
16
18
|
class BaseHistogrammer:
|
|
@@ -40,12 +42,11 @@ class BaseHistogrammer:
|
|
|
40
42
|
x_values = np.linspace(0, self.max_pixel_value, len(hist))
|
|
41
43
|
ax.plot(x_values, hist, color=color, alpha=alpha)
|
|
42
44
|
|
|
43
|
-
def save_plot(self, idx):
|
|
45
|
+
def save_plot(self, idx, fig=None):
|
|
44
46
|
idx_str = f"{idx:04d}"
|
|
45
47
|
plot_path = f"{self.process.working_path}/{self.process.plot_path}/" \
|
|
46
48
|
f"{self.process.name}-hist-{idx_str}.pdf"
|
|
47
|
-
save_plot(plot_path)
|
|
48
|
-
plt.close('all')
|
|
49
|
+
save_plot(plot_path, fig)
|
|
49
50
|
self.process.callback(
|
|
50
51
|
'save_plot',
|
|
51
52
|
self.process.id, f"{self.process.name}: balance\nframe {idx_str}",
|
|
@@ -56,7 +57,6 @@ class BaseHistogrammer:
|
|
|
56
57
|
plot_path = f"{self.process.working_path}/{self.process.plot_path}/" \
|
|
57
58
|
f"{self.process.name}-{name}.pdf"
|
|
58
59
|
save_plot(plot_path)
|
|
59
|
-
plt.close('all')
|
|
60
60
|
self.process.callback(
|
|
61
61
|
'save_plot', self.process.id,
|
|
62
62
|
f"{self.process.name}: {name}", plot_path
|
|
@@ -69,13 +69,14 @@ class LumiHistogrammer(BaseHistogrammer):
|
|
|
69
69
|
self.colors = ("r", "g", "b")
|
|
70
70
|
|
|
71
71
|
def generate_frame_plot(self, idx, hist, chans, calc_hist_func):
|
|
72
|
-
|
|
72
|
+
fig, axs = plt.subplots(1, 2, figsize=constants.PLT_FIG_SIZE, sharey=True)
|
|
73
73
|
self.histo_plot(axs[0], hist, "pixel luminosity", 'black')
|
|
74
74
|
for (chan, color) in zip(chans, self.colors):
|
|
75
75
|
hist_col = calc_hist_func(chan)
|
|
76
76
|
self.histo_plot(axs[1], hist_col, "R, G, B intensity", color, alpha=0.5)
|
|
77
|
+
fig.suptitle("Image histograms")
|
|
77
78
|
plt.xlim(0, self.max_pixel_value)
|
|
78
|
-
self.save_plot(idx)
|
|
79
|
+
self.save_plot(idx, fig)
|
|
79
80
|
|
|
80
81
|
def generate_summary_plot(self, ref_idx):
|
|
81
82
|
plt.figure(figsize=constants.PLT_FIG_SIZE)
|
|
@@ -86,6 +87,7 @@ class LumiHistogrammer(BaseHistogrammer):
|
|
|
86
87
|
plt.plot([x[0], x[-1]], [1, 1], color='lightgray', linestyle='--',
|
|
87
88
|
label='no correction')
|
|
88
89
|
plt.plot(x, y, color='navy', label='luminosity correction')
|
|
90
|
+
plt.title("Image balance correction")
|
|
89
91
|
plt.xlabel('frame')
|
|
90
92
|
plt.ylabel('correction')
|
|
91
93
|
plt.legend()
|
|
@@ -100,11 +102,12 @@ class RGBHistogrammer(BaseHistogrammer):
|
|
|
100
102
|
self.colors = ("r", "g", "b")
|
|
101
103
|
|
|
102
104
|
def generate_frame_plot(self, idx, hists):
|
|
103
|
-
|
|
105
|
+
fig, axs = plt.subplots(1, 3, figsize=constants.PLT_FIG_SIZE, sharey=True)
|
|
104
106
|
for c in [2, 1, 0]:
|
|
105
107
|
self.histo_plot(axs[c], hists[c], self.colors[c] + " luminosity", self.colors[c])
|
|
108
|
+
fig.suptitle("Image histograms")
|
|
106
109
|
plt.xlim(0, self.max_pixel_value)
|
|
107
|
-
self.save_plot(idx)
|
|
110
|
+
self.save_plot(idx, fig)
|
|
108
111
|
|
|
109
112
|
def generate_summary_plot(self, ref_idx):
|
|
110
113
|
plt.figure(figsize=constants.PLT_FIG_SIZE)
|
|
@@ -118,6 +121,7 @@ class RGBHistogrammer(BaseHistogrammer):
|
|
|
118
121
|
plt.plot(x, y[:, 0], color='r', label='R correction')
|
|
119
122
|
plt.plot(x, y[:, 1], color='g', label='G correction')
|
|
120
123
|
plt.plot(x, y[:, 2], color='b', label='B correction')
|
|
124
|
+
plt.title("Image balance correction")
|
|
121
125
|
plt.xlabel('frame')
|
|
122
126
|
plt.ylabel('correction')
|
|
123
127
|
plt.legend()
|
|
@@ -133,10 +137,12 @@ class Ch1Histogrammer(BaseHistogrammer):
|
|
|
133
137
|
self.colors = colors
|
|
134
138
|
|
|
135
139
|
def generate_frame_plot(self, idx, hists):
|
|
136
|
-
|
|
140
|
+
fig, axs = plt.subplots(1, 3, figsize=constants.PLT_FIG_SIZE, sharey=True)
|
|
137
141
|
for c in range(3):
|
|
138
142
|
self.histo_plot(axs[c], hists[c], self.labels[c], self.colors[c])
|
|
139
|
-
|
|
143
|
+
fig.suptitle("Image histograms")
|
|
144
|
+
for ax in axs:
|
|
145
|
+
ax.set_xlim(0, self.max_pixel_value)
|
|
140
146
|
self.save_plot(idx)
|
|
141
147
|
|
|
142
148
|
def generate_summary_plot(self, ref_idx):
|
|
@@ -149,6 +155,7 @@ class Ch1Histogrammer(BaseHistogrammer):
|
|
|
149
155
|
plt.plot([x[0], x[-1]], [1, 1], color='lightgray', linestyle='--',
|
|
150
156
|
label='no correction')
|
|
151
157
|
plt.plot(x, y[:, 0], color=self.colors[0], label=self.labels[0] + ' correction')
|
|
158
|
+
plt.title("Image balance correction")
|
|
152
159
|
plt.xlabel('frame')
|
|
153
160
|
plt.ylabel('correction')
|
|
154
161
|
plt.legend()
|
|
@@ -164,10 +171,12 @@ class Ch2Histogrammer(BaseHistogrammer):
|
|
|
164
171
|
self.colors = colors
|
|
165
172
|
|
|
166
173
|
def generate_frame_plot(self, idx, hists):
|
|
167
|
-
|
|
174
|
+
fig, axs = plt.subplots(1, 3, figsize=constants.PLT_FIG_SIZE, sharey=True)
|
|
168
175
|
for c in range(3):
|
|
169
176
|
self.histo_plot(axs[c], hists[c], self.labels[c], self.colors[c])
|
|
170
|
-
|
|
177
|
+
fig.suptitle("Image histograms")
|
|
178
|
+
for ax in axs:
|
|
179
|
+
ax.set_xlim(0, self.max_pixel_value)
|
|
171
180
|
self.save_plot(idx)
|
|
172
181
|
|
|
173
182
|
def generate_summary_plot(self, ref_idx):
|
|
@@ -181,6 +190,7 @@ class Ch2Histogrammer(BaseHistogrammer):
|
|
|
181
190
|
label='no correction')
|
|
182
191
|
plt.plot(x, y[:, 0], color=self.colors[1], label=self.labels[1] + ' correction')
|
|
183
192
|
plt.plot(x, y[:, 1], color=self.colors[2], label=self.labels[2] + ' correction')
|
|
193
|
+
plt.title("Image balance correction")
|
|
184
194
|
plt.xlabel('frame')
|
|
185
195
|
plt.ylabel('correction')
|
|
186
196
|
plt.legend()
|
|
@@ -603,7 +613,7 @@ class BalanceFrames(SubAction):
|
|
|
603
613
|
mask_radius = int(min(*shape) * self.mask_size / 2)
|
|
604
614
|
cv2.circle(img, (shape[1] // 2, shape[0] // 2), mask_radius, 255, -1)
|
|
605
615
|
plt.figure(figsize=constants.PLT_FIG_SIZE)
|
|
606
|
-
plt.title('
|
|
616
|
+
plt.title('Image balance mask')
|
|
607
617
|
plt.imshow(img, 'gray')
|
|
608
618
|
self.correction.histogrammer.save_summary_plot("mask")
|
|
609
619
|
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
# pylint: disable=C0114, C0115, C0116, E0602, R0903
|
|
1
|
+
# pylint: disable=C0114, C0115, C0116, E0602, R0903, R0902
|
|
2
2
|
import os
|
|
3
3
|
import numpy as np
|
|
4
|
-
from .. core.exceptions import InvalidOptionError,
|
|
4
|
+
from .. core.exceptions import InvalidOptionError, RunStopException
|
|
5
5
|
from .. config.constants import constants
|
|
6
6
|
from .. core.colors import color_str
|
|
7
|
-
from .utils import read_img, get_img_metadata,
|
|
7
|
+
from .utils import read_img, get_img_metadata, get_first_image_file
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
class BaseStackAlgo:
|
|
@@ -14,6 +14,9 @@ class BaseStackAlgo:
|
|
|
14
14
|
self.process = None
|
|
15
15
|
self.filenames = None
|
|
16
16
|
self.shape = None
|
|
17
|
+
self.dtype = None
|
|
18
|
+
self.num_pixel_values = None
|
|
19
|
+
self.max_pixel_value = None
|
|
17
20
|
self.do_step_callback = False
|
|
18
21
|
if float_type == constants.FLOAT_32:
|
|
19
22
|
self.float_type = np.float32
|
|
@@ -41,14 +44,16 @@ class BaseStackAlgo:
|
|
|
41
44
|
return f"image: {self.idx_tot_str(idx)}, " \
|
|
42
45
|
f"{os.path.basename(self.filenames[idx])}"
|
|
43
46
|
|
|
47
|
+
def num_images(self):
|
|
48
|
+
return len(self.filenames)
|
|
49
|
+
|
|
44
50
|
def init(self, filenames):
|
|
45
51
|
self.filenames = filenames
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
if
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
self.shape = get_img_file_shape(first_img_file)
|
|
52
|
+
self.shape, self.dtype = get_img_metadata(read_img(get_first_image_file(filenames)))
|
|
53
|
+
self.num_pixel_values = constants.NUM_UINT8 \
|
|
54
|
+
if self.dtype == np.uint8 else constants.NUM_UINT16
|
|
55
|
+
self.max_pixel_value = constants.MAX_UINT8 \
|
|
56
|
+
if self.dtype == np.uint8 else constants.MAX_UINT16
|
|
52
57
|
|
|
53
58
|
def total_steps(self, n_frames):
|
|
54
59
|
return self._steps_per_frame * n_frames
|
|
@@ -56,17 +61,6 @@ class BaseStackAlgo:
|
|
|
56
61
|
def print_message(self, msg):
|
|
57
62
|
self.process.sub_message_r(color_str(msg, constants.LOG_COLOR_LEVEL_3))
|
|
58
63
|
|
|
59
|
-
def read_image_and_update_metadata(self, img_path, metadata):
|
|
60
|
-
img = read_img(img_path)
|
|
61
|
-
if img is None:
|
|
62
|
-
raise ImageLoadError(img_path)
|
|
63
|
-
updated = metadata is None
|
|
64
|
-
if updated:
|
|
65
|
-
metadata = get_img_metadata(img)
|
|
66
|
-
else:
|
|
67
|
-
validate_image(img, *metadata)
|
|
68
|
-
return img, metadata, updated
|
|
69
|
-
|
|
70
64
|
def check_running(self, cleanup_callback=None):
|
|
71
65
|
if self.process.callback(constants.CALLBACK_CHECK_RUNNING,
|
|
72
66
|
self.process.id, self.process.name) is False:
|
|
@@ -3,7 +3,7 @@ import numpy as np
|
|
|
3
3
|
import cv2
|
|
4
4
|
from .. config.constants import constants
|
|
5
5
|
from .. core.exceptions import InvalidOptionError
|
|
6
|
-
from .utils import read_img, img_bw
|
|
6
|
+
from .utils import read_img, read_and_validate_img, img_bw
|
|
7
7
|
from .base_stack_algo import BaseStackAlgo
|
|
8
8
|
|
|
9
9
|
|
|
@@ -62,19 +62,15 @@ class DepthMapStack(BaseStackAlgo):
|
|
|
62
62
|
f"{constants.DM_MAP_AVERAGE} and {constants.DM_MAP_MAX}.")
|
|
63
63
|
|
|
64
64
|
def focus_stack(self):
|
|
65
|
-
|
|
66
|
-
|
|
65
|
+
n_images = len(self.filenames)
|
|
66
|
+
gray_images = np.empty((n_images, *self.shape), dtype=self.float_type)
|
|
67
67
|
for i, img_path in enumerate(self.filenames):
|
|
68
|
-
self.print_message(f": reading
|
|
69
|
-
|
|
70
|
-
img, metadata, _updated = self.read_image_and_update_metadata(img_path, metadata)
|
|
71
|
-
|
|
68
|
+
self.print_message(f": reading and validating {self.image_str(i)}")
|
|
69
|
+
img = read_and_validate_img(img_path, self.shape, self.dtype)
|
|
72
70
|
gray = img_bw(img)
|
|
73
|
-
gray_images.
|
|
71
|
+
gray_images[i] = gray.astype(self.float_type)
|
|
74
72
|
self.after_step(i)
|
|
75
73
|
self.check_running()
|
|
76
|
-
dtype = metadata[1]
|
|
77
|
-
gray_images = np.array(gray_images, dtype=self.float_type)
|
|
78
74
|
if self.energy == constants.DM_ENERGY_SOBEL:
|
|
79
75
|
energies = self.get_sobel_map(gray_images)
|
|
80
76
|
elif self.energy == constants.DM_ENERGY_LAPLACIAN:
|
|
@@ -92,7 +88,7 @@ class DepthMapStack(BaseStackAlgo):
|
|
|
92
88
|
weights = self.get_focus_map(energies)
|
|
93
89
|
blended_pyramid = None
|
|
94
90
|
for i, img_path in enumerate(self.filenames):
|
|
95
|
-
self.print_message(f": reading
|
|
91
|
+
self.print_message(f": reading {self.image_str(i)}")
|
|
96
92
|
img = read_img(img_path).astype(self.float_type)
|
|
97
93
|
weight = weights[i]
|
|
98
94
|
gp_img = [img]
|
|
@@ -109,12 +105,11 @@ class DepthMapStack(BaseStackAlgo):
|
|
|
109
105
|
for j in range(self.levels)]
|
|
110
106
|
blended_pyramid = current_blend if blended_pyramid is None \
|
|
111
107
|
else [np.add(bp, cb) for bp, cb in zip(blended_pyramid, current_blend)]
|
|
112
|
-
self.after_step(i +
|
|
108
|
+
self.after_step(i + n_images)
|
|
113
109
|
self.check_running()
|
|
114
110
|
result = blended_pyramid[0]
|
|
115
111
|
self.print_message(': blend levels')
|
|
116
112
|
for j in range(1, self.levels):
|
|
117
113
|
size = (blended_pyramid[j].shape[1], blended_pyramid[j].shape[0])
|
|
118
114
|
result = cv2.pyrUp(result, dstsize=size) + blended_pyramid[j]
|
|
119
|
-
|
|
120
|
-
return np.clip(np.absolute(result), 0, n_values).astype(dtype)
|
|
115
|
+
return np.clip(np.absolute(result), 0, self.num_pixel_values).astype(self.dtype)
|
|
@@ -10,10 +10,12 @@ from .. config.constants import constants
|
|
|
10
10
|
from .. core.colors import color_str
|
|
11
11
|
from .. core.exceptions import ImageLoadError
|
|
12
12
|
from .. core.framework import TaskBase
|
|
13
|
-
from .. core.core_utils import make_tqdm_bar
|
|
13
|
+
from .. core.core_utils import make_tqdm_bar, setup_matplotlib_mode
|
|
14
14
|
from .. core.exceptions import RunStopException, ShapeError
|
|
15
15
|
from .stack_framework import ImageSequenceManager, SubAction
|
|
16
16
|
from .utils import read_img, save_plot, get_img_metadata, validate_image
|
|
17
|
+
setup_matplotlib_mode()
|
|
18
|
+
|
|
17
19
|
|
|
18
20
|
MAX_NOISY_PIXELS = 1000
|
|
19
21
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import numpy as np
|
|
3
3
|
import cv2
|
|
4
4
|
from .. config.constants import constants
|
|
5
|
-
from .utils import
|
|
5
|
+
from .utils import read_and_validate_img
|
|
6
6
|
from .base_stack_algo import BaseStackAlgo
|
|
7
7
|
|
|
8
8
|
|
|
@@ -11,7 +11,7 @@ class PyramidBase(BaseStackAlgo):
|
|
|
11
11
|
kernel_size=constants.DEFAULT_PY_KERNEL_SIZE,
|
|
12
12
|
gen_kernel=constants.DEFAULT_PY_GEN_KERNEL,
|
|
13
13
|
float_type=constants.DEFAULT_PY_FLOAT):
|
|
14
|
-
super().__init__(name,
|
|
14
|
+
super().__init__(name, 1, float_type)
|
|
15
15
|
self.min_size = min_size
|
|
16
16
|
self.kernel_size = kernel_size
|
|
17
17
|
self.pad_amount = (kernel_size - 1) // 2
|
|
@@ -30,7 +30,7 @@ class PyramidBase(BaseStackAlgo):
|
|
|
30
30
|
|
|
31
31
|
def total_steps(self, n_frames):
|
|
32
32
|
self.n_frames = n_frames
|
|
33
|
-
return
|
|
33
|
+
return super().total_steps(n_frames) + self.n_levels
|
|
34
34
|
|
|
35
35
|
def convolve(self, image):
|
|
36
36
|
return cv2.filter2D(image, -1, self.gen_kernel, borderType=cv2.BORDER_REFLECT101)
|
|
@@ -122,21 +122,6 @@ class PyramidBase(BaseStackAlgo):
|
|
|
122
122
|
fused += np.where(best_d[:, :, np.newaxis] == layer, img, 0)
|
|
123
123
|
return (fused / 2).astype(images.dtype)
|
|
124
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(
|
|
129
|
-
f": validating file {self.image_str(i)}")
|
|
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)
|
|
139
|
-
|
|
140
125
|
def single_image_laplacian(self, img, levels):
|
|
141
126
|
pyramid = [img.astype(self.float_type)]
|
|
142
127
|
for _ in range(levels):
|
|
@@ -180,15 +165,16 @@ class PyramidStack(PyramidBase):
|
|
|
180
165
|
return fused[::-1]
|
|
181
166
|
|
|
182
167
|
def focus_stack(self):
|
|
183
|
-
n = len(self.filenames)
|
|
184
|
-
self.focus_stack_validate()
|
|
185
168
|
all_laplacians = []
|
|
186
169
|
for i, img_path in enumerate(self.filenames):
|
|
170
|
+
self.print_message(
|
|
171
|
+
f": reading and validating {self.image_str(i)}")
|
|
172
|
+
img = read_and_validate_img(img_path, self.shape, self.dtype)
|
|
173
|
+
self.check_running()
|
|
187
174
|
self.print_message(
|
|
188
175
|
f": processing {self.image_str(i)}")
|
|
189
|
-
img = read_img(img_path)
|
|
190
176
|
all_laplacians.append(self.process_single_image(img, self.n_levels))
|
|
191
|
-
self.after_step(i +
|
|
177
|
+
self.after_step(i + 1)
|
|
192
178
|
self.check_running()
|
|
193
179
|
stacked_image = self.collapse(self.fuse_pyramids(all_laplacians))
|
|
194
180
|
return stacked_image.astype(self.dtype)
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
import os
|
|
3
3
|
import numpy as np
|
|
4
4
|
from .. config.constants import constants
|
|
5
|
-
from .utils import extension_tif_jpg
|
|
6
5
|
from .base_stack_algo import BaseStackAlgo
|
|
7
6
|
from .pyramid import PyramidStack
|
|
8
7
|
from .pyramid_tiles import PyramidTilesStack
|
|
@@ -21,7 +20,7 @@ class PyramidAutoStack(BaseStackAlgo):
|
|
|
21
20
|
min_tile_size=constants.DEFAULT_PY_MIN_TILE_SIZE,
|
|
22
21
|
min_n_tiled_layers=constants.DEFAULT_PY_MIN_N_TILED_LAYERS,
|
|
23
22
|
mode='auto'):
|
|
24
|
-
super().__init__("auto_pyramid",
|
|
23
|
+
super().__init__("auto_pyramid", 1, float_type)
|
|
25
24
|
self.min_size = min_size
|
|
26
25
|
self.kernel_size = kernel_size
|
|
27
26
|
self.gen_kernel = gen_kernel
|
|
@@ -47,15 +46,7 @@ class PyramidAutoStack(BaseStackAlgo):
|
|
|
47
46
|
self.overhead = constants.PY_MEMORY_OVERHEAD
|
|
48
47
|
|
|
49
48
|
def init(self, filenames):
|
|
50
|
-
|
|
51
|
-
for filename in filenames:
|
|
52
|
-
if os.path.isfile(filename) and extension_tif_jpg(filename):
|
|
53
|
-
first_img_file = filename
|
|
54
|
-
break
|
|
55
|
-
if first_img_file is None:
|
|
56
|
-
raise ValueError("No valid image files found")
|
|
57
|
-
_img, metadata, _ = self.read_image_and_update_metadata(first_img_file, None)
|
|
58
|
-
self.shape, self.dtype = metadata
|
|
49
|
+
super().init(filenames)
|
|
59
50
|
self.n_levels = int(np.log2(min(self.shape) / self.min_size))
|
|
60
51
|
self.n_frames = len(filenames)
|
|
61
52
|
memory_required_memory = self._estimate_memory_memory()
|
|
@@ -79,9 +70,9 @@ class PyramidAutoStack(BaseStackAlgo):
|
|
|
79
70
|
n_tiled_layers=optimal_params['n_tiled_layers'],
|
|
80
71
|
max_threads=self.num_threads
|
|
81
72
|
)
|
|
82
|
-
self.print_message(f": using tile-based pyramid stacking "
|
|
83
|
-
f"
|
|
84
|
-
f"
|
|
73
|
+
self.print_message(f": using tile-based pyramid stacking, "
|
|
74
|
+
f"tile size: {optimal_params['tile_size']}, "
|
|
75
|
+
f"n. tiled layers: {optimal_params['n_tiled_layers']}, "
|
|
85
76
|
f"{self.num_threads} cores.")
|
|
86
77
|
self._implementation.init(filenames)
|
|
87
78
|
self._implementation.set_do_step_callback(self.do_step_callback)
|
|
@@ -10,7 +10,7 @@ from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
|
10
10
|
import numpy as np
|
|
11
11
|
from .. config.constants import constants
|
|
12
12
|
from .. core.exceptions import RunStopException
|
|
13
|
-
from .utils import read_img
|
|
13
|
+
from .utils import read_img, read_and_validate_img
|
|
14
14
|
from .pyramid import PyramidBase
|
|
15
15
|
|
|
16
16
|
|
|
@@ -47,11 +47,11 @@ class PyramidTilesStack(PyramidBase):
|
|
|
47
47
|
return n_steps + self.n_tiles
|
|
48
48
|
|
|
49
49
|
def _process_single_image_wrapper(self, args):
|
|
50
|
-
img_path,
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
level_count = self.process_single_image(img, self.n_levels,
|
|
54
|
-
return
|
|
50
|
+
img_path, idx, _n = args
|
|
51
|
+
img = read_and_validate_img(img_path, self.shape, self.dtype)
|
|
52
|
+
self.check_running(self.cleanup_temp_files)
|
|
53
|
+
level_count = self.process_single_image(img, self.n_levels, idx)
|
|
54
|
+
return idx, level_count
|
|
55
55
|
|
|
56
56
|
def process_single_image(self, img, levels, img_index):
|
|
57
57
|
laplacian = self.single_image_laplacian(img, levels)
|
|
@@ -160,10 +160,11 @@ class PyramidTilesStack(PyramidBase):
|
|
|
160
160
|
gc.collect()
|
|
161
161
|
return np.zeros((y_end - y, x_end - x, 3), dtype=self.float_type)
|
|
162
162
|
|
|
163
|
-
def fuse_pyramids(self, all_level_counts
|
|
163
|
+
def fuse_pyramids(self, all_level_counts):
|
|
164
|
+
num_images = self.num_images()
|
|
164
165
|
max_levels = max(all_level_counts)
|
|
165
166
|
fused = []
|
|
166
|
-
count =
|
|
167
|
+
count = super().total_steps(num_images)
|
|
167
168
|
for level in range(max_levels - 1, -1, -1):
|
|
168
169
|
self.print_message(f': fusing pyramids, layer: {level + 1}')
|
|
169
170
|
if level < self.n_tiled_layers:
|
|
@@ -201,12 +202,11 @@ class PyramidTilesStack(PyramidBase):
|
|
|
201
202
|
return fused[::-1]
|
|
202
203
|
|
|
203
204
|
def focus_stack(self):
|
|
204
|
-
|
|
205
|
-
self.focus_stack_validate(self.cleanup_temp_files)
|
|
206
|
-
all_level_counts = [0] * n
|
|
205
|
+
all_level_counts = [0] * self.num_images()
|
|
207
206
|
if self.num_threads > 1:
|
|
208
207
|
self.print_message(f': starting parallel processing on {self.num_threads} cores')
|
|
209
|
-
args_list = [(file_path, i,
|
|
208
|
+
args_list = [(file_path, i, self.num_images())
|
|
209
|
+
for i, file_path in enumerate(self.filenames)]
|
|
210
210
|
executor = None
|
|
211
211
|
try:
|
|
212
212
|
executor = ThreadPoolExecutor(max_workers=self.num_threads)
|
|
@@ -222,12 +222,11 @@ class PyramidTilesStack(PyramidBase):
|
|
|
222
222
|
all_level_counts[img_index] = level_count
|
|
223
223
|
completed_count += 1
|
|
224
224
|
self.print_message(
|
|
225
|
-
": processing completed,
|
|
226
|
-
f"{self.idx_tot_str(completed_count - 1)}")
|
|
225
|
+
f": processing completed, {self.image_str(completed_count - 1)}")
|
|
227
226
|
except Exception as e:
|
|
228
227
|
self.print_message(
|
|
229
|
-
f"Error processing
|
|
230
|
-
self.after_step(completed_count
|
|
228
|
+
f"Error processing {self.image_str(i)}: {str(e)}")
|
|
229
|
+
self.after_step(completed_count)
|
|
231
230
|
self.check_running(lambda: None)
|
|
232
231
|
except RunStopException:
|
|
233
232
|
self.print_message(": stopping image processing...")
|
|
@@ -242,16 +241,15 @@ class PyramidTilesStack(PyramidBase):
|
|
|
242
241
|
else:
|
|
243
242
|
for i, file_path in enumerate(self.filenames):
|
|
244
243
|
self.print_message(
|
|
245
|
-
f": processing
|
|
246
|
-
f"{self.idx_tot_str(i)}")
|
|
244
|
+
f": processing {self.image_str(i)}")
|
|
247
245
|
img = read_img(file_path)
|
|
248
246
|
level_count = self.process_single_image(img, self.n_levels, i)
|
|
249
247
|
all_level_counts[i] = level_count
|
|
250
|
-
self.after_step(i +
|
|
248
|
+
self.after_step(i + 1)
|
|
251
249
|
self.check_running(lambda: None)
|
|
252
250
|
try:
|
|
253
251
|
self.check_running(lambda: None)
|
|
254
|
-
fused_pyramid = self.fuse_pyramids(all_level_counts
|
|
252
|
+
fused_pyramid = self.fuse_pyramids(all_level_counts)
|
|
255
253
|
stacked_image = self.collapse(fused_pyramid)
|
|
256
254
|
return stacked_image.astype(self.dtype)
|
|
257
255
|
except RunStopException:
|
|
@@ -117,7 +117,7 @@ class ImageSequenceManager:
|
|
|
117
117
|
assert False, "this method should be overwritten"
|
|
118
118
|
|
|
119
119
|
def set_filelist(self):
|
|
120
|
-
file_folder = self.input_full_path()
|
|
120
|
+
file_folder = os.path.relpath(self.input_full_path(), self.working_path)
|
|
121
121
|
self.print_message(color_str(f"{self.num_input_filepaths()} files in folder: {file_folder}",
|
|
122
122
|
constants.LOG_COLOR_LEVEL_2))
|
|
123
123
|
self.base_message = color_str(self.name, constants.LOG_COLOR_LEVEL_1, "bold")
|
shinestacker/algorithms/utils.py
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# pylint: disable=C0114, C0116, E1101, R0914
|
|
2
2
|
import os
|
|
3
|
+
import gc
|
|
3
4
|
import logging
|
|
5
|
+
import threading
|
|
4
6
|
import numpy as np
|
|
5
7
|
import cv2
|
|
6
8
|
import matplotlib.pyplot as plt
|
|
@@ -50,6 +52,10 @@ def extension_jpg_png(path):
|
|
|
50
52
|
return extension_in(path, EXTENSIONS_JPG + EXTENSIONS_PNG)
|
|
51
53
|
|
|
52
54
|
|
|
55
|
+
def extension_jpg_tif_png(path):
|
|
56
|
+
return extension_in(path, EXTENSIONS_JPG + EXTENSIONS_TIF + EXTENSIONS_PNG)
|
|
57
|
+
|
|
58
|
+
|
|
53
59
|
def read_img(file_path):
|
|
54
60
|
if not os.path.isfile(file_path):
|
|
55
61
|
raise RuntimeError("File does not exist: " + file_path)
|
|
@@ -87,6 +93,17 @@ def img_bw(img):
|
|
|
87
93
|
return cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
|
|
88
94
|
|
|
89
95
|
|
|
96
|
+
def get_first_image_file(filenames):
|
|
97
|
+
first_img_file = None
|
|
98
|
+
for filename in filenames:
|
|
99
|
+
if os.path.isfile(filename) and extension_tif_jpg(filename):
|
|
100
|
+
first_img_file = filename
|
|
101
|
+
break
|
|
102
|
+
if first_img_file is None:
|
|
103
|
+
raise ValueError("No valid image files found")
|
|
104
|
+
return first_img_file
|
|
105
|
+
|
|
106
|
+
|
|
90
107
|
def get_img_file_shape(file_path):
|
|
91
108
|
img = read_img(file_path)
|
|
92
109
|
return img.shape[:2]
|
|
@@ -106,19 +123,29 @@ def validate_image(img, expected_shape=None, expected_dtype=None):
|
|
|
106
123
|
raise ShapeError(expected_shape, shape)
|
|
107
124
|
if expected_dtype and dtype != expected_dtype:
|
|
108
125
|
raise BitDepthError(expected_dtype, dtype)
|
|
126
|
+
return img
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def read_and_validate_img(filename, expected_shape=None, expected_dtype=None):
|
|
130
|
+
return validate_image(read_img(filename), expected_shape, expected_dtype)
|
|
109
131
|
|
|
110
132
|
|
|
111
|
-
def save_plot(filename):
|
|
133
|
+
def save_plot(filename, fig=None):
|
|
112
134
|
logging.getLogger(__name__).debug(msg=f"save plot file: {filename}")
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
dir_path =
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
135
|
+
save_lock = threading.Lock()
|
|
136
|
+
with save_lock:
|
|
137
|
+
dir_path = os.path.dirname(filename)
|
|
138
|
+
if not dir_path:
|
|
139
|
+
dir_path = '.'
|
|
140
|
+
if not os.path.isdir(dir_path):
|
|
141
|
+
os.makedirs(dir_path)
|
|
142
|
+
if fig is None:
|
|
143
|
+
fig = plt.gcf()
|
|
144
|
+
fig.savefig(filename, dpi=150)
|
|
145
|
+
if config.JUPYTER_NOTEBOOK:
|
|
146
|
+
plt.show()
|
|
147
|
+
plt.close(fig)
|
|
148
|
+
gc.collect()
|
|
122
149
|
|
|
123
150
|
|
|
124
151
|
def img_subsample(img, subsample, fast=True):
|
|
@@ -7,9 +7,11 @@ import matplotlib.pyplot as plt
|
|
|
7
7
|
from scipy.optimize import curve_fit, fsolve
|
|
8
8
|
import cv2
|
|
9
9
|
from .. core.colors import color_str
|
|
10
|
+
from .. core.core_utils import setup_matplotlib_mode
|
|
10
11
|
from .. config.constants import constants
|
|
11
12
|
from .utils import img_8bit, save_plot, img_subsample
|
|
12
13
|
from .stack_framework import SubAction
|
|
14
|
+
setup_matplotlib_mode()
|
|
13
15
|
|
|
14
16
|
CLIP_EXP = 10
|
|
15
17
|
|
shinestacker/app/gui_utils.py
CHANGED
|
@@ -53,3 +53,13 @@ def fill_app_menu(app, app_menu):
|
|
|
53
53
|
exit_action.setShortcut(quit_short)
|
|
54
54
|
exit_action.triggered.connect(app.quit)
|
|
55
55
|
app_menu.addAction(exit_action)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def set_css_style(app):
|
|
59
|
+
css_style = """
|
|
60
|
+
QToolTip {
|
|
61
|
+
color: black;
|
|
62
|
+
border: 1px solid black;
|
|
63
|
+
}
|
|
64
|
+
"""
|
|
65
|
+
app.setStyleSheet(css_style)
|
shinestacker/app/main.py
CHANGED
|
@@ -16,7 +16,8 @@ from shinestacker.config.constants import constants
|
|
|
16
16
|
from shinestacker.core.logging import setup_logging
|
|
17
17
|
from shinestacker.gui.main_window import MainWindow
|
|
18
18
|
from shinestacker.retouch.image_editor_ui import ImageEditorUI
|
|
19
|
-
from shinestacker.app.gui_utils import
|
|
19
|
+
from shinestacker.app.gui_utils import (
|
|
20
|
+
disable_macos_special_menu_items, fill_app_menu, set_css_style)
|
|
20
21
|
from shinestacker.app.help_menu import add_help_action
|
|
21
22
|
from shinestacker.app.open_frames import open_frames
|
|
22
23
|
|
|
@@ -233,6 +234,7 @@ expert options are visible by default.
|
|
|
233
234
|
app.setWindowIcon(QIcon(icon_path))
|
|
234
235
|
main_app = MainApp()
|
|
235
236
|
app.main_app = main_app
|
|
237
|
+
set_css_style(app)
|
|
236
238
|
main_app.show()
|
|
237
239
|
main_app.activateWindow()
|
|
238
240
|
if args['expert']:
|
shinestacker/app/project.py
CHANGED
|
@@ -14,7 +14,8 @@ config.init(DISABLE_TQDM=True, DONT_USE_NATIVE_MENU=True)
|
|
|
14
14
|
from shinestacker.config.constants import constants
|
|
15
15
|
from shinestacker.core.logging import setup_logging
|
|
16
16
|
from shinestacker.gui.main_window import MainWindow
|
|
17
|
-
from shinestacker.app.gui_utils import
|
|
17
|
+
from shinestacker.app.gui_utils import (
|
|
18
|
+
disable_macos_special_menu_items, fill_app_menu, set_css_style)
|
|
18
19
|
from shinestacker.app.help_menu import add_help_action
|
|
19
20
|
|
|
20
21
|
|
|
@@ -63,6 +64,7 @@ expert options are visible by default.
|
|
|
63
64
|
disable_macos_special_menu_items()
|
|
64
65
|
icon_path = f"{os.path.dirname(__file__)}/../gui/ico/shinestacker.png"
|
|
65
66
|
app.setWindowIcon(QIcon(icon_path))
|
|
67
|
+
set_css_style(app)
|
|
66
68
|
window = ProjectApp()
|
|
67
69
|
if args['expert']:
|
|
68
70
|
window.set_expert_options()
|
shinestacker/app/retouch.py
CHANGED
|
@@ -9,7 +9,8 @@ from shinestacker.config.config import config
|
|
|
9
9
|
config.init(DISABLE_TQDM=True, DONT_USE_NATIVE_MENU=True)
|
|
10
10
|
from shinestacker.config.constants import constants
|
|
11
11
|
from shinestacker.retouch.image_editor_ui import ImageEditorUI
|
|
12
|
-
from shinestacker.app.gui_utils import
|
|
12
|
+
from shinestacker.app.gui_utils import (
|
|
13
|
+
disable_macos_special_menu_items, fill_app_menu, set_css_style)
|
|
13
14
|
from shinestacker.app.help_menu import add_help_action
|
|
14
15
|
from shinestacker.app.open_frames import open_frames
|
|
15
16
|
|
|
@@ -60,6 +61,7 @@ Multiple directories can be specified separated by ';'.
|
|
|
60
61
|
disable_macos_special_menu_items()
|
|
61
62
|
icon_path = f"{os.path.dirname(__file__)}/../gui/ico/shinestacker.png"
|
|
62
63
|
app.setWindowIcon(QIcon(icon_path))
|
|
64
|
+
set_css_style(app)
|
|
63
65
|
editor = RetouchApp()
|
|
64
66
|
app.editor = editor
|
|
65
67
|
editor.show()
|
|
@@ -63,8 +63,8 @@ class _GuiConstants:
|
|
|
63
63
|
DEFAULT_CURSOR_STYLE = 'preview'
|
|
64
64
|
BRUSH_LINE_WIDTH = 2
|
|
65
65
|
BRUSH_PREVIEW_LINE_WIDTH = 1.5
|
|
66
|
-
ZOOM_IN_FACTOR = 1.
|
|
67
|
-
ZOOM_OUT_FACTOR =
|
|
66
|
+
ZOOM_IN_FACTOR = 1.10
|
|
67
|
+
ZOOM_OUT_FACTOR = 1 / ZOOM_IN_FACTOR
|
|
68
68
|
|
|
69
69
|
def calculate_gamma(self):
|
|
70
70
|
if self.BRUSH_SIZES['mid'] <= self.BRUSH_SIZES['min'] or self.BRUSH_SIZES['max'] <= 0:
|