shinestacker 0.4.0__py3-none-any.whl → 1.0.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 +4 -12
- shinestacker/algorithms/balance.py +11 -9
- shinestacker/algorithms/depth_map.py +0 -30
- shinestacker/algorithms/utils.py +10 -0
- shinestacker/algorithms/vignetting.py +116 -70
- shinestacker/app/about_dialog.py +101 -12
- shinestacker/app/gui_utils.py +1 -1
- shinestacker/app/help_menu.py +1 -1
- shinestacker/app/main.py +2 -2
- shinestacker/app/project.py +2 -2
- shinestacker/config/constants.py +4 -1
- shinestacker/config/gui_constants.py +10 -9
- shinestacker/gui/action_config.py +5 -561
- shinestacker/gui/action_config_dialog.py +567 -0
- shinestacker/gui/base_form_dialog.py +18 -0
- shinestacker/gui/colors.py +5 -6
- shinestacker/gui/gui_logging.py +0 -1
- shinestacker/gui/gui_run.py +54 -106
- shinestacker/gui/ico/shinestacker.icns +0 -0
- shinestacker/gui/ico/shinestacker.ico +0 -0
- shinestacker/gui/ico/shinestacker.png +0 -0
- shinestacker/gui/ico/shinestacker.svg +60 -0
- shinestacker/gui/main_window.py +276 -367
- shinestacker/gui/menu_manager.py +236 -0
- shinestacker/gui/new_project.py +75 -20
- shinestacker/gui/project_converter.py +6 -6
- shinestacker/gui/project_editor.py +248 -165
- shinestacker/gui/project_model.py +2 -7
- shinestacker/gui/tab_widget.py +81 -0
- shinestacker/gui/time_progress_bar.py +95 -0
- shinestacker/retouch/base_filter.py +173 -40
- shinestacker/retouch/brush_preview.py +0 -10
- shinestacker/retouch/brush_tool.py +25 -11
- shinestacker/retouch/denoise_filter.py +5 -44
- shinestacker/retouch/display_manager.py +57 -20
- shinestacker/retouch/exif_data.py +10 -13
- shinestacker/retouch/file_loader.py +1 -1
- shinestacker/retouch/filter_manager.py +1 -4
- shinestacker/retouch/image_editor_ui.py +365 -49
- shinestacker/retouch/image_viewer.py +34 -11
- shinestacker/retouch/io_gui_handler.py +96 -43
- shinestacker/retouch/io_manager.py +23 -7
- shinestacker/retouch/layer_collection.py +2 -0
- shinestacker/retouch/shortcuts_help.py +12 -0
- shinestacker/retouch/unsharp_mask_filter.py +10 -10
- shinestacker/retouch/vignetting_filter.py +69 -0
- shinestacker/retouch/white_balance_filter.py +46 -14
- {shinestacker-0.4.0.dist-info → shinestacker-1.0.0.dist-info}/METADATA +14 -2
- shinestacker-1.0.0.dist-info/RECORD +90 -0
- shinestacker/app/app_config.py +0 -22
- shinestacker/gui/actions_window.py +0 -258
- shinestacker/retouch/image_editor.py +0 -201
- shinestacker/retouch/image_filters.py +0 -69
- shinestacker-0.4.0.dist-info/RECORD +0 -87
- {shinestacker-0.4.0.dist-info → shinestacker-1.0.0.dist-info}/WHEEL +0 -0
- {shinestacker-0.4.0.dist-info → shinestacker-1.0.0.dist-info}/entry_points.txt +0 -0
- {shinestacker-0.4.0.dist-info → shinestacker-1.0.0.dist-info}/licenses/LICENSE +0 -0
- {shinestacker-0.4.0.dist-info → shinestacker-1.0.0.dist-info}/top_level.txt +0 -0
shinestacker/_version.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = '0.
|
|
1
|
+
__version__ = '1.0.0'
|
shinestacker/algorithms/align.py
CHANGED
|
@@ -6,8 +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
|
|
10
|
-
from .utils import get_img_metadata, validate_image
|
|
9
|
+
from .utils import img_8bit, img_bw_8bit, save_plot, get_img_metadata, validate_image, img_subsample
|
|
11
10
|
from .stack_framework import SubAction
|
|
12
11
|
|
|
13
12
|
_DEFAULT_FEATURE_CONFIG = {
|
|
@@ -166,19 +165,12 @@ def align_images(img_1, img_0, feature_config=None, matching_config=None, alignm
|
|
|
166
165
|
if callbacks and 'message' in callbacks:
|
|
167
166
|
callbacks['message']()
|
|
168
167
|
subsample = alignment_config['subsample']
|
|
168
|
+
fast_subsampling = alignment_config['fast_subsampling']
|
|
169
169
|
min_good_matches = alignment_config['min_good_matches']
|
|
170
170
|
while True:
|
|
171
171
|
if subsample > 1:
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
img_0[::subsample, ::subsample], img_1[::subsample, ::subsample]
|
|
175
|
-
else:
|
|
176
|
-
img_0_sub = cv2.resize(img_0, (0, 0),
|
|
177
|
-
fx=1 / subsample, fy=1 / subsample,
|
|
178
|
-
interpolation=cv2.INTER_AREA)
|
|
179
|
-
img_1_sub = cv2.resize(img_1, (0, 0),
|
|
180
|
-
fx=1 / subsample, fy=1 / subsample,
|
|
181
|
-
interpolation=cv2.INTER_AREA)
|
|
172
|
+
img_0_sub = img_subsample(img_0, subsample, fast_subsampling)
|
|
173
|
+
img_1_sub = img_subsample(img_1, subsample, fast_subsampling)
|
|
182
174
|
else:
|
|
183
175
|
img_0_sub, img_1_sub = img_0, img_1
|
|
184
176
|
kp_0, kp_1, good_matches = detect_and_compute(img_0_sub, img_1_sub,
|
|
@@ -7,7 +7,7 @@ from scipy.interpolate import interp1d
|
|
|
7
7
|
from .. config.constants import constants
|
|
8
8
|
from .. core.exceptions import InvalidOptionError
|
|
9
9
|
from .. core.colors import color_str
|
|
10
|
-
from .utils import read_img, save_plot
|
|
10
|
+
from .utils import read_img, save_plot, img_subsample
|
|
11
11
|
from .stack_framework import SubAction
|
|
12
12
|
|
|
13
13
|
|
|
@@ -122,13 +122,15 @@ class LinearMap(CorrectionMap):
|
|
|
122
122
|
|
|
123
123
|
class Correction:
|
|
124
124
|
def __init__(self, channels, mask_size=0, intensity_interval=None,
|
|
125
|
-
subsample=-1,
|
|
125
|
+
subsample=-1, fast_subsampling=constants.DEFAULT_BALANCE_FAST_SUBSAMPLING,
|
|
126
|
+
corr_map=constants.DEFAULT_CORR_MAP,
|
|
126
127
|
plot_histograms=False, plot_summary=False):
|
|
127
128
|
self.mask_size = mask_size
|
|
128
129
|
self.intensity_interval = intensity_interval
|
|
129
130
|
self.plot_histograms = plot_histograms
|
|
130
131
|
self.plot_summary = plot_summary
|
|
131
132
|
self.subsample = constants.DEFAULT_BALANCE_SUBSAMPLE if subsample == -1 else subsample
|
|
133
|
+
self.fast_subsampling = fast_subsampling
|
|
132
134
|
self.corr_map = corr_map
|
|
133
135
|
self.channels = channels
|
|
134
136
|
self.dtype = None
|
|
@@ -154,17 +156,18 @@ class Correction:
|
|
|
154
156
|
self.corrections = np.ones((size, self.channels))
|
|
155
157
|
|
|
156
158
|
def calc_hist_1ch(self, image):
|
|
157
|
-
|
|
159
|
+
img_sub = image if self.subsample == 1 \
|
|
160
|
+
else img_subsample(image, self.subsample, self.fast_subsampling)
|
|
158
161
|
if self.mask_size == 0:
|
|
159
|
-
image_sel =
|
|
162
|
+
image_sel = img_sub
|
|
160
163
|
else:
|
|
161
|
-
height, width =
|
|
164
|
+
height, width = img_sub.shape[:2]
|
|
162
165
|
xv, yv = np.meshgrid(
|
|
163
166
|
np.linspace(0, width - 1, width),
|
|
164
167
|
np.linspace(0, height - 1, height)
|
|
165
168
|
)
|
|
166
169
|
mask_radius = min(width, height) * self.mask_size / 2
|
|
167
|
-
image_sel =
|
|
170
|
+
image_sel = img_sub[
|
|
168
171
|
(xv - width / 2) ** 2 + (yv - height / 2) ** 2 <= mask_radius ** 2
|
|
169
172
|
]
|
|
170
173
|
hist, _bins = np.histogram(
|
|
@@ -305,9 +308,6 @@ class Ch2Correction(Correction):
|
|
|
305
308
|
def preprocess(self, image):
|
|
306
309
|
assert False, 'abstract method'
|
|
307
310
|
|
|
308
|
-
def get_labels(self):
|
|
309
|
-
assert False, 'abstract method'
|
|
310
|
-
|
|
311
311
|
def get_hist(self, image, idx):
|
|
312
312
|
hist = [self.calc_hist_1ch(chan) for chan in cv2.split(image)]
|
|
313
313
|
if self.plot_histograms:
|
|
@@ -370,6 +370,8 @@ class BalanceFrames(SubAction):
|
|
|
370
370
|
self.shape = None
|
|
371
371
|
corr_map = kwargs.get('corr_map', constants.DEFAULT_CORR_MAP)
|
|
372
372
|
subsample = kwargs.get('subsample', constants.DEFAULT_BALANCE_SUBSAMPLE)
|
|
373
|
+
self.fast_subsampling = kwargs.get(
|
|
374
|
+
'fast_subsampling', constants.DEFAULT_BALANCE_FAST_SUBSAMPLING)
|
|
373
375
|
channel = kwargs.pop('channel', constants.DEFAULT_CHANNEL)
|
|
374
376
|
kwargs['subsample'] = (
|
|
375
377
|
1 if corr_map == constants.BALANCE_MATCH_HIST
|
|
@@ -61,36 +61,6 @@ 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 pyramid_blend(self, images, weights):
|
|
65
|
-
blended = None
|
|
66
|
-
for i in range(images.shape[0]):
|
|
67
|
-
img = images[i].astype(self.float_type)
|
|
68
|
-
weight = weights[i]
|
|
69
|
-
gp_img = [img]
|
|
70
|
-
gp_weight = [weight]
|
|
71
|
-
for _ in range(self.levels - 1):
|
|
72
|
-
gp_img.append(cv2.pyrDown(gp_img[-1]))
|
|
73
|
-
gp_weight.append(cv2.pyrDown(gp_weight[-1]))
|
|
74
|
-
lp_img = [gp_img[-1]]
|
|
75
|
-
for j in range(self.levels - 1, 0, -1):
|
|
76
|
-
size = (gp_img[j - 1].shape[1], gp_img[j - 1].shape[0])
|
|
77
|
-
expanded = cv2.pyrUp(gp_img[j], dstsize=size)
|
|
78
|
-
lp_img.append(gp_img[j - 1] - expanded)
|
|
79
|
-
current_blend = []
|
|
80
|
-
for j in range(self.levels):
|
|
81
|
-
w = gp_weight[self.levels - 1 - j][..., np.newaxis]
|
|
82
|
-
current_blend.append(lp_img[j] * w)
|
|
83
|
-
if blended is None:
|
|
84
|
-
blended = current_blend
|
|
85
|
-
else:
|
|
86
|
-
for j in range(self.levels):
|
|
87
|
-
blended[j] += current_blend[j]
|
|
88
|
-
result = blended[0]
|
|
89
|
-
for j in range(1, self.levels):
|
|
90
|
-
size = (blended[j].shape[1], blended[j].shape[0])
|
|
91
|
-
result = cv2.pyrUp(result, dstsize=size) + blended[j]
|
|
92
|
-
return result
|
|
93
|
-
|
|
94
64
|
def focus_stack(self, filenames):
|
|
95
65
|
gray_images = []
|
|
96
66
|
metadata = None
|
shinestacker/algorithms/utils.py
CHANGED
|
@@ -74,3 +74,13 @@ def save_plot(filename):
|
|
|
74
74
|
if config.JUPYTER_NOTEBOOK:
|
|
75
75
|
plt.show()
|
|
76
76
|
plt.close('all')
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def img_subsample(img, subsample, fast=True):
|
|
80
|
+
if fast:
|
|
81
|
+
img_sub = img[::subsample, ::subsample]
|
|
82
|
+
else:
|
|
83
|
+
img_sub = cv2.resize(img, (0, 0),
|
|
84
|
+
fx=1 / subsample, fy=1 / subsample,
|
|
85
|
+
interpolation=cv2.INTER_AREA)
|
|
86
|
+
return img_sub
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
# pylint: disable=C0114, C0115, C0116, R0902, E1101, W0718, W0640
|
|
1
|
+
# pylint: disable=C0114, C0115, C0116, R0902, E1101, W0718, W0640, R0913, R0917, R0914
|
|
2
|
+
import traceback
|
|
2
3
|
import logging
|
|
3
4
|
import numpy as np
|
|
4
5
|
import matplotlib.pyplot as plt
|
|
@@ -6,22 +7,108 @@ from scipy.optimize import curve_fit, fsolve
|
|
|
6
7
|
import cv2
|
|
7
8
|
from .. core.colors import color_str
|
|
8
9
|
from .. config.constants import constants
|
|
9
|
-
from .utils import img_8bit, save_plot
|
|
10
|
+
from .utils import img_8bit, save_plot, img_subsample
|
|
10
11
|
from .stack_framework import SubAction
|
|
11
12
|
|
|
12
13
|
CLIP_EXP = 10
|
|
13
14
|
|
|
14
15
|
|
|
16
|
+
def sigmoid_model(r, i0, k, r0):
|
|
17
|
+
return i0 / (1.0 +
|
|
18
|
+
np.exp(np.minimum(CLIP_EXP,
|
|
19
|
+
np.exp(np.clip(k * (r - r0),
|
|
20
|
+
-CLIP_EXP, CLIP_EXP)))))
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def radial_mean_intensity(image, r_steps):
|
|
24
|
+
if len(image.shape) > 2:
|
|
25
|
+
raise ValueError("The image must be grayscale")
|
|
26
|
+
h, w = image.shape
|
|
27
|
+
w_2, h_2 = w / 2, h / 2
|
|
28
|
+
r_max = np.sqrt((w / 2)**2 + (h / 2)**2)
|
|
29
|
+
radii = np.linspace(0, r_max, r_steps + 1)
|
|
30
|
+
mean_intensities = np.zeros(r_steps)
|
|
31
|
+
y, x = np.ogrid[:h, :w]
|
|
32
|
+
dist_from_center = np.sqrt((x - w_2)**2 + (y - h_2)**2)
|
|
33
|
+
for i in range(r_steps):
|
|
34
|
+
mask = (dist_from_center >= radii[i]) & (dist_from_center < radii[i + 1])
|
|
35
|
+
if np.any(mask):
|
|
36
|
+
mean_intensities[i] = np.mean(image[mask])
|
|
37
|
+
else:
|
|
38
|
+
mean_intensities[i] = np.nan
|
|
39
|
+
return (radii[1:] + radii[:-1]) / 2, mean_intensities
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def fit_sigmoid(radii, intensities):
|
|
43
|
+
valid_mask = ~np.isnan(intensities)
|
|
44
|
+
i_valid, r_valid = intensities[valid_mask], radii[valid_mask]
|
|
45
|
+
r_max = radii.max()
|
|
46
|
+
res = curve_fit(sigmoid_model, r_valid, i_valid,
|
|
47
|
+
p0=[2 * np.max(i_valid), 10 / r_max, 0.8 * r_max],
|
|
48
|
+
bounds=([0, 0, 0], ['inf', 'inf', 'inf']))[0]
|
|
49
|
+
return res
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def img_subsampled(image, subsample=constants.DEFAULT_VIGN_SUBSAMPLE,
|
|
53
|
+
fast_subsampling=constants.DEFAULT_VIGN_FAST_SUBSAMPLING):
|
|
54
|
+
image_bw = cv2.cvtColor(img_8bit(image), cv2.COLOR_BGR2GRAY)
|
|
55
|
+
return image_bw if subsample == 1 else img_subsample(image_bw, subsample, fast_subsampling)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def compute_fit_parameters(
|
|
59
|
+
image, r_steps, radii=None, intensities=None,
|
|
60
|
+
subsample=constants.DEFAULT_VIGN_SUBSAMPLE,
|
|
61
|
+
fast_subsampling=constants.DEFAULT_VIGN_FAST_SUBSAMPLING):
|
|
62
|
+
image_sub = img_subsampled(image, subsample, fast_subsampling)
|
|
63
|
+
if radii is None and intensities is None:
|
|
64
|
+
radii, intensities = radial_mean_intensity(image_sub, r_steps)
|
|
65
|
+
params = fit_sigmoid(radii, intensities)
|
|
66
|
+
params[1] /= subsample # k
|
|
67
|
+
params[2] *= subsample # r0
|
|
68
|
+
return params
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def correct_vignetting(
|
|
72
|
+
image, max_correction=constants.DEFAULT_MAX_CORRECTION,
|
|
73
|
+
black_threshold=constants.DEFAULT_BLACK_THRESHOLD,
|
|
74
|
+
r_steps=constants.DEFAULT_R_STEPS, params=None, v0=None,
|
|
75
|
+
subsample=constants.DEFAULT_VIGN_SUBSAMPLE,
|
|
76
|
+
fast_subsampling=constants.DEFAULT_VIGN_FAST_SUBSAMPLING):
|
|
77
|
+
if params is None:
|
|
78
|
+
if r_steps is None:
|
|
79
|
+
raise RuntimeError("Either r_steps or pars must not be None")
|
|
80
|
+
params = compute_fit_parameters(
|
|
81
|
+
image, r_steps, subsample=subsample, fast_subsampling=fast_subsampling)
|
|
82
|
+
if v0 is None:
|
|
83
|
+
v0 = sigmoid_model(0, *params)
|
|
84
|
+
h, w = image.shape[:2]
|
|
85
|
+
y, x = np.ogrid[:h, :w]
|
|
86
|
+
r = np.sqrt((x - w / 2)**2 + (y - h / 2)**2)
|
|
87
|
+
vignette = np.clip(sigmoid_model(r, *params) / v0, 1e-6, 1)
|
|
88
|
+
if max_correction < 1:
|
|
89
|
+
vignette = (1.0 - max_correction) + vignette * max_correction
|
|
90
|
+
threshold = black_threshold if image.dtype == np.uint8 else black_threshold * 256
|
|
91
|
+
if len(image.shape) == 3:
|
|
92
|
+
vignette = vignette[:, :, np.newaxis]
|
|
93
|
+
vignette[np.min(image, axis=2) < threshold, :] = 1
|
|
94
|
+
else:
|
|
95
|
+
vignette[image < black_threshold] = 1
|
|
96
|
+
return np.clip(image / vignette, 0, 255
|
|
97
|
+
if image.dtype == np.uint8 else 65535).astype(image.dtype)
|
|
98
|
+
|
|
99
|
+
|
|
15
100
|
class Vignetting(SubAction):
|
|
16
101
|
def __init__(self, enabled=True, percentiles=(0.05, 0.1, 0.25, 0.5, 0.75, 0.9, 0.95), **kwargs):
|
|
17
102
|
super().__init__(enabled)
|
|
18
103
|
self.r_steps = kwargs.get('r_steps', constants.DEFAULT_R_STEPS)
|
|
19
104
|
self.black_threshold = kwargs.get('black_threshold', constants.DEFAULT_BLACK_THRESHOLD)
|
|
20
|
-
self.apply_correction = kwargs.get('apply_correction', True)
|
|
21
105
|
self.plot_correction = kwargs.get('plot_correction', False)
|
|
22
106
|
self.plot_summary = kwargs.get('plot_summary', False)
|
|
23
107
|
self.max_correction = kwargs.get('max_correction', constants.DEFAULT_MAX_CORRECTION)
|
|
24
108
|
self.percentiles = np.sort(percentiles)
|
|
109
|
+
self.subsample = kwargs.get('subsample', constants.DEFAULT_VIGN_SUBSAMPLE)
|
|
110
|
+
self.fast_subsampling = kwargs.get(
|
|
111
|
+
'fast_subsampling', constants.DEFAULT_VIGN_FAST_SUBSAMPLING)
|
|
25
112
|
self.w_2 = None
|
|
26
113
|
self.h_2 = None
|
|
27
114
|
self.v0 = None
|
|
@@ -29,76 +116,35 @@ class Vignetting(SubAction):
|
|
|
29
116
|
self.process = None
|
|
30
117
|
self.corrections = None
|
|
31
118
|
|
|
32
|
-
def
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
h, w = image.shape
|
|
119
|
+
def run_frame(self, idx, _ref_idx, img_0):
|
|
120
|
+
self.process.sub_message_r(color_str(": compute vignetting", "cyan"))
|
|
121
|
+
h, w = img_0.shape[:2]
|
|
36
122
|
self.w_2, self.h_2 = w / 2, h / 2
|
|
37
123
|
self.r_max = np.sqrt((w / 2)**2 + (h / 2)**2)
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
y, x = np.ogrid[:h, :w]
|
|
41
|
-
dist_from_center = np.sqrt((x - self.w_2)**2 + (y - self.h_2)**2)
|
|
42
|
-
for i in range(self.r_steps):
|
|
43
|
-
mask = (dist_from_center >= radii[i]) & (dist_from_center < radii[i + 1])
|
|
44
|
-
if np.any(mask):
|
|
45
|
-
mean_intensities[i] = np.mean(image[mask])
|
|
46
|
-
else:
|
|
47
|
-
mean_intensities[i] = np.nan
|
|
48
|
-
return (radii[1:] + radii[:-1]) / 2, mean_intensities
|
|
49
|
-
|
|
50
|
-
@staticmethod
|
|
51
|
-
def sigmoid(r, i0, k, r0):
|
|
52
|
-
return i0 / (1.0 +
|
|
53
|
-
np.exp(np.minimum(CLIP_EXP,
|
|
54
|
-
np.exp(np.clip(k * (r - r0),
|
|
55
|
-
-CLIP_EXP, CLIP_EXP)))))
|
|
56
|
-
|
|
57
|
-
def fit_sigmoid(self, radii, intensities):
|
|
58
|
-
valid_mask = ~np.isnan(intensities)
|
|
59
|
-
i_valid, r_valid = intensities[valid_mask], radii[valid_mask]
|
|
124
|
+
image_sub = img_subsampled(img_0, self.subsample, self.fast_subsampling)
|
|
125
|
+
radii, intensities = radial_mean_intensity(image_sub, self.r_steps)
|
|
60
126
|
try:
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
127
|
+
params = compute_fit_parameters(
|
|
128
|
+
img_0, self.r_steps, radii, intensities, self.subsample, self.fast_subsampling)
|
|
129
|
+
except Exception as e:
|
|
130
|
+
traceback.print_tb(e.__traceback__)
|
|
65
131
|
self.process.sub_message(
|
|
66
|
-
color_str(": could not find vignetting model", "red"),
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
return res
|
|
70
|
-
|
|
71
|
-
def correct_vignetting(self, image, params):
|
|
72
|
-
h, w = image.shape[:2]
|
|
73
|
-
y, x = np.ogrid[:h, :w]
|
|
74
|
-
r = np.sqrt((x - w / 2)**2 + (y - h / 2)**2)
|
|
75
|
-
vignette = np.clip(Vignetting.sigmoid(r, *params) / self.v0, 1e-6, 1)
|
|
76
|
-
if self.max_correction < 1:
|
|
77
|
-
vignette = (1.0 - self.max_correction) + vignette * self.max_correction
|
|
78
|
-
if len(image.shape) == 3:
|
|
79
|
-
vignette = vignette[:, :, np.newaxis]
|
|
80
|
-
vignette[np.min(image, axis=2) < self.black_threshold, :] = 1
|
|
81
|
-
else:
|
|
82
|
-
vignette[image < self.black_threshold] = 1
|
|
83
|
-
return np.clip(image / vignette, 0, 255
|
|
84
|
-
if image.dtype == np.uint8 else 65535).astype(image.dtype)
|
|
85
|
-
|
|
86
|
-
def run_frame(self, idx, _ref_idx, img_0):
|
|
87
|
-
self.process.sub_message_r(": compute vignetting")
|
|
88
|
-
img = cv2.cvtColor(img_8bit(img_0), cv2.COLOR_BGR2GRAY)
|
|
89
|
-
radii, intensities = self.radial_mean_intensity(img)
|
|
90
|
-
pars = self.fit_sigmoid(radii, intensities)
|
|
91
|
-
if pars is None:
|
|
132
|
+
color_str(": could not find vignetting model", "red"), level=logging.WARNING)
|
|
133
|
+
params = None
|
|
134
|
+
if params is None:
|
|
92
135
|
return img_0
|
|
93
|
-
self.v0 =
|
|
94
|
-
i0_fit, k_fit, r0_fit =
|
|
95
|
-
self.process.sub_message(
|
|
96
|
-
|
|
97
|
-
|
|
136
|
+
self.v0 = sigmoid_model(0, *params)
|
|
137
|
+
i0_fit, k_fit, r0_fit = params
|
|
138
|
+
self.process.sub_message(color_str(": vignetting model parameters: ", "cyan") +
|
|
139
|
+
color_str(f"i0={i0_fit / 2:.4f}, "
|
|
140
|
+
f"k={k_fit * self.r_max:.4f}, "
|
|
141
|
+
f"r0={r0_fit / self.r_max:.4f}",
|
|
142
|
+
"light_blue"),
|
|
143
|
+
level=logging.DEBUG)
|
|
98
144
|
if self.plot_correction:
|
|
99
145
|
plt.figure(figsize=(10, 5))
|
|
100
146
|
plt.plot(radii, intensities, label="image mean intensity")
|
|
101
|
-
plt.plot(radii,
|
|
147
|
+
plt.plot(radii, sigmoid_model(radii * self.subsample, *params), label="sigmoid fit")
|
|
102
148
|
plt.xlabel('radius (pixels)')
|
|
103
149
|
plt.ylabel('mean intensity')
|
|
104
150
|
plt.legend()
|
|
@@ -114,12 +160,12 @@ class Vignetting(SubAction):
|
|
|
114
160
|
'save_plot', self.process.id,
|
|
115
161
|
f"{self.process.name}: intensity\nframe {idx_str}", plot_path)
|
|
116
162
|
for i, p in enumerate(self.percentiles):
|
|
117
|
-
self.corrections[i][idx] = fsolve(lambda x:
|
|
163
|
+
self.corrections[i][idx] = fsolve(lambda x: sigmoid_model(x, *params) /
|
|
118
164
|
self.v0 - p, r0_fit)[0]
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
165
|
+
self.process.sub_message_r(color_str(": correct vignetting", "cyan"))
|
|
166
|
+
return correct_vignetting(
|
|
167
|
+
img_0, self.max_correction, self.black_threshold, None, params, self.v0,
|
|
168
|
+
self.subsample, self.fast_subsampling)
|
|
123
169
|
|
|
124
170
|
def begin(self, process):
|
|
125
171
|
self.process = process
|
shinestacker/app/about_dialog.py
CHANGED
|
@@ -1,17 +1,111 @@
|
|
|
1
|
-
# pylint: disable=C0114, C0116, E0611
|
|
2
|
-
|
|
1
|
+
# pylint: disable=C0114, C0115, C0116, E0611, W0718, R0903
|
|
2
|
+
import json
|
|
3
|
+
from urllib.request import urlopen, Request
|
|
4
|
+
from urllib.error import URLError
|
|
5
|
+
from PySide6.QtWidgets import QDialog, QVBoxLayout, QHBoxLayout, QPushButton, QLabel
|
|
3
6
|
from PySide6.QtCore import Qt
|
|
4
7
|
from .. import __version__
|
|
8
|
+
from .. retouch.icon_container import icon_container
|
|
5
9
|
from .. config.constants import constants
|
|
6
10
|
|
|
7
11
|
|
|
8
|
-
|
|
12
|
+
class AboutDialog(QDialog):
|
|
13
|
+
def __init__(self, parent=None, about_text=""):
|
|
14
|
+
super().__init__(parent)
|
|
15
|
+
self.setWindowTitle("About")
|
|
16
|
+
self.resize(400, 300)
|
|
17
|
+
layout = QVBoxLayout(self)
|
|
18
|
+
layout.setAlignment(Qt.AlignTop)
|
|
19
|
+
icon_widget = icon_container()
|
|
20
|
+
icon_layout = QHBoxLayout()
|
|
21
|
+
icon_layout.addStretch()
|
|
22
|
+
icon_layout.addWidget(icon_widget)
|
|
23
|
+
icon_layout.addStretch()
|
|
24
|
+
layout.addLayout(icon_layout)
|
|
25
|
+
about_label = QLabel(about_text)
|
|
26
|
+
about_label.setWordWrap(True)
|
|
27
|
+
about_label.setAlignment(Qt.AlignLeft)
|
|
28
|
+
layout.addWidget(about_label)
|
|
29
|
+
button_layout = QHBoxLayout()
|
|
30
|
+
button_layout.addStretch()
|
|
31
|
+
button = QPushButton("OK")
|
|
32
|
+
button.setFixedWidth(100)
|
|
33
|
+
button.clicked.connect(self.accept)
|
|
34
|
+
button_layout.addWidget(button)
|
|
35
|
+
button_layout.addStretch()
|
|
36
|
+
layout.addLayout(button_layout)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def compare_versions(current, latest):
|
|
40
|
+
def parse_version(v):
|
|
41
|
+
v = v.lstrip('v')
|
|
42
|
+
parts = v.split('.')
|
|
43
|
+
result = []
|
|
44
|
+
for part in parts:
|
|
45
|
+
try:
|
|
46
|
+
result.append(int(part))
|
|
47
|
+
except ValueError:
|
|
48
|
+
result.append(part)
|
|
49
|
+
return result
|
|
50
|
+
current_parts = parse_version(current)
|
|
51
|
+
latest_parts = parse_version(latest)
|
|
52
|
+
for i in range(max(len(current_parts), len(latest_parts))):
|
|
53
|
+
c = current_parts[i] if i < len(current_parts) else 0
|
|
54
|
+
l = latest_parts[i] if i < len(latest_parts) else 0 # noqa: E741
|
|
55
|
+
if isinstance(c, int) and isinstance(l, int):
|
|
56
|
+
if c < l:
|
|
57
|
+
return -1
|
|
58
|
+
if c > l:
|
|
59
|
+
return 1
|
|
60
|
+
else:
|
|
61
|
+
if str(c) < str(l):
|
|
62
|
+
return -1
|
|
63
|
+
if str(c) > str(l):
|
|
64
|
+
return 1
|
|
65
|
+
return 0
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def get_latest_version():
|
|
69
|
+
try:
|
|
70
|
+
url = "https://api.github.com/repos/lucalista/shinestacker/releases/latest"
|
|
71
|
+
headers = {'User-Agent': 'ShineStacker'}
|
|
72
|
+
req = Request(url, headers=headers)
|
|
73
|
+
with urlopen(req, timeout=5) as response:
|
|
74
|
+
data = json.loads(response.read().decode())
|
|
75
|
+
return data['tag_name']
|
|
76
|
+
except (URLError, ValueError, KeyError, TimeoutError):
|
|
77
|
+
return None
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def show_about_dialog(parent):
|
|
9
81
|
version_clean = __version__.split("+", maxsplit=1)[0]
|
|
82
|
+
latest_version = None
|
|
83
|
+
try:
|
|
84
|
+
latest_version = get_latest_version()
|
|
85
|
+
except Exception:
|
|
86
|
+
pass
|
|
87
|
+
update_text = ""
|
|
88
|
+
if latest_version:
|
|
89
|
+
latest_clean = latest_version.lstrip('v')
|
|
90
|
+
if compare_versions(version_clean, latest_clean) < 0:
|
|
91
|
+
update_text = f"""
|
|
92
|
+
<p style="color: red; font-weight: bold;">
|
|
93
|
+
Update available! Latest version: {latest_version}
|
|
94
|
+
<br><a href="https://github.com/lucalista/shinestacker/releases/latest">Download here</a>
|
|
95
|
+
</p>
|
|
96
|
+
""" # noqa E501
|
|
97
|
+
else:
|
|
98
|
+
update_text = """
|
|
99
|
+
<p style="color: green; font-weight: bold;">
|
|
100
|
+
You are using the lastet version.
|
|
101
|
+
</p>
|
|
102
|
+
"""
|
|
10
103
|
about_text = f"""
|
|
11
104
|
<h3>{constants.APP_TITLE}</h3>
|
|
12
105
|
<h4>version: v{version_clean}</h4>
|
|
13
|
-
|
|
14
|
-
|
|
106
|
+
{update_text}
|
|
107
|
+
<p style='font-weight: normal;'>Focus stackign applications and framework.<br>
|
|
108
|
+
Combine multiple frames into a single focused image.</p>
|
|
15
109
|
<p>Author: Luca Lista<br/>
|
|
16
110
|
Email: <a href="mailto:luka.lista@gmail.com">luka.lista@gmail.com</a></p>
|
|
17
111
|
<ul>
|
|
@@ -19,10 +113,5 @@ def show_about_dialog():
|
|
|
19
113
|
<li><a href="https://github.com/lucalista/shinestacker">GitHub project repository</a></li>
|
|
20
114
|
</ul>
|
|
21
115
|
"""
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
msg.setIcon(QMessageBox.Icon.Information)
|
|
25
|
-
msg.setTextFormat(Qt.TextFormat.RichText)
|
|
26
|
-
msg.setText(about_text)
|
|
27
|
-
msg.setIcon(QMessageBox.Icon.NoIcon)
|
|
28
|
-
msg.exec_()
|
|
116
|
+
dialog = AboutDialog(parent, about_text)
|
|
117
|
+
dialog.exec()
|
shinestacker/app/gui_utils.py
CHANGED
|
@@ -42,7 +42,7 @@ def disable_macos_special_menu_items():
|
|
|
42
42
|
|
|
43
43
|
def fill_app_menu(app, app_menu):
|
|
44
44
|
about_action = QAction(f"About {constants.APP_STRING}", app)
|
|
45
|
-
about_action.triggered.connect(show_about_dialog)
|
|
45
|
+
about_action.triggered.connect(lambda: show_about_dialog(app))
|
|
46
46
|
app_menu.addAction(about_action)
|
|
47
47
|
app_menu.addSeparator()
|
|
48
48
|
if config.DONT_USE_NATIVE_MENU:
|
shinestacker/app/help_menu.py
CHANGED
shinestacker/app/main.py
CHANGED
|
@@ -141,7 +141,7 @@ expert options are visible by default.
|
|
|
141
141
|
filename = filenames[0]
|
|
142
142
|
extension = filename.split('.')[-1]
|
|
143
143
|
if len(filenames) == 1 and extension == 'fsp':
|
|
144
|
-
main_app.project_window.open_project(filename)
|
|
144
|
+
main_app.project_window.project_controller.open_project(filename)
|
|
145
145
|
main_app.project_window.setFocus()
|
|
146
146
|
else:
|
|
147
147
|
main_app.switch_to_retouch()
|
|
@@ -152,7 +152,7 @@ expert options are visible by default.
|
|
|
152
152
|
main_app.switch_to_retouch()
|
|
153
153
|
else:
|
|
154
154
|
main_app.switch_to_project()
|
|
155
|
-
QTimer.singleShot(100, main_app.project_window.new_project)
|
|
155
|
+
QTimer.singleShot(100, main_app.project_window.project_controller.new_project)
|
|
156
156
|
QTimer.singleShot(100, main_app.setFocus)
|
|
157
157
|
sys.exit(app.exec())
|
|
158
158
|
|
shinestacker/app/project.py
CHANGED
|
@@ -70,9 +70,9 @@ expert options are visible by default.
|
|
|
70
70
|
window.show()
|
|
71
71
|
filename = args['filename']
|
|
72
72
|
if filename:
|
|
73
|
-
QTimer.singleShot(100, lambda: window.open_project(filename))
|
|
73
|
+
QTimer.singleShot(100, lambda: window.project_controller.open_project(filename))
|
|
74
74
|
else:
|
|
75
|
-
QTimer.singleShot(100, window.new_project)
|
|
75
|
+
QTimer.singleShot(100, window.project_controller.new_project)
|
|
76
76
|
sys.exit(app.exec())
|
|
77
77
|
|
|
78
78
|
|
shinestacker/config/constants.py
CHANGED
|
@@ -121,13 +121,16 @@ class _Constants:
|
|
|
121
121
|
VALID_BALANCE_CHANNELS = [BALANCE_LUMI, BALANCE_RGB, BALANCE_HSV, BALANCE_HLS]
|
|
122
122
|
|
|
123
123
|
DEFAULT_BALANCE_SUBSAMPLE = 8
|
|
124
|
+
DEFAULT_BALANCE_FAST_SUBSAMPLING = False
|
|
124
125
|
DEFAULT_CORR_MAP = BALANCE_LINEAR
|
|
125
126
|
DEFAULT_CHANNEL = BALANCE_LUMI
|
|
126
127
|
DEFAULT_INTENSITY_INTERVAL = {'min': 0, 'max': -1}
|
|
127
128
|
|
|
128
129
|
DEFAULT_R_STEPS = 100
|
|
129
|
-
DEFAULT_BLACK_THRESHOLD = 1
|
|
130
|
+
DEFAULT_BLACK_THRESHOLD = 1.0
|
|
130
131
|
DEFAULT_MAX_CORRECTION = 1
|
|
132
|
+
DEFAULT_VIGN_SUBSAMPLE = 8
|
|
133
|
+
DEFAULT_VIGN_FAST_SUBSAMPLING = False
|
|
131
134
|
|
|
132
135
|
FLOAT_32 = 'float-32'
|
|
133
136
|
FLOAT_64 = 'float-64'
|
|
@@ -6,8 +6,9 @@ class _GuiConstants:
|
|
|
6
6
|
GUI_IMG_WIDTH = 250 # px
|
|
7
7
|
DISABLED_TAG = "" # " <disabled>"
|
|
8
8
|
|
|
9
|
-
MIN_ZOOMED_IMG_WIDTH =
|
|
10
|
-
|
|
9
|
+
MIN_ZOOMED_IMG_WIDTH = 600
|
|
10
|
+
MIN_ZOOMED_IMG_HEIGHT = 600
|
|
11
|
+
MAX_ZOOMED_IMG_PX_SIZE = 40
|
|
11
12
|
MAX_UNDO_SIZE = 65535
|
|
12
13
|
|
|
13
14
|
NEW_PROJECT_NOISE_DETECTION = False
|
|
@@ -36,18 +37,18 @@ class _GuiConstants:
|
|
|
36
37
|
|
|
37
38
|
THUMB_WIDTH = 120 # px
|
|
38
39
|
THUMB_HEIGHT = 80 # px
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
MAX_UNDO_STEPS = 50
|
|
40
|
+
THUMB_HI_COLOR = '#0000FF'
|
|
41
|
+
THUMB_LO_COLOR = '#0000FF'
|
|
42
|
+
THUMB_MASTER_HI_COLOR = '#0000FF'
|
|
43
|
+
THUMB_MASTER_LO_COLOR = 'transparent'
|
|
44
44
|
|
|
45
45
|
BRUSH_SIZE_SLIDER_MAX = 1000
|
|
46
46
|
|
|
47
47
|
UI_SIZES = {
|
|
48
48
|
'brush_preview': (100, 80),
|
|
49
|
-
'
|
|
50
|
-
'master_thumb': (THUMB_WIDTH, THUMB_HEIGHT)
|
|
49
|
+
'thumbnail_width': 100,
|
|
50
|
+
'master_thumb': (THUMB_WIDTH, THUMB_HEIGHT),
|
|
51
|
+
'label_height': 20
|
|
51
52
|
}
|
|
52
53
|
|
|
53
54
|
DEFAULT_BRUSH_HARDNESS = 50
|