shinestacker 0.3.2__py3-none-any.whl → 0.3.4__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/__init__.py +2 -1
- shinestacker/_version.py +1 -1
- shinestacker/algorithms/__init__.py +3 -2
- shinestacker/algorithms/align.py +102 -64
- shinestacker/algorithms/balance.py +89 -42
- shinestacker/algorithms/base_stack_algo.py +42 -0
- shinestacker/algorithms/core_utils.py +6 -6
- shinestacker/algorithms/denoise.py +4 -1
- shinestacker/algorithms/depth_map.py +28 -39
- shinestacker/algorithms/exif.py +43 -38
- shinestacker/algorithms/multilayer.py +48 -28
- shinestacker/algorithms/noise_detection.py +34 -23
- shinestacker/algorithms/pyramid.py +42 -42
- shinestacker/algorithms/sharpen.py +1 -0
- shinestacker/algorithms/stack.py +42 -41
- shinestacker/algorithms/stack_framework.py +111 -65
- shinestacker/algorithms/utils.py +12 -11
- shinestacker/algorithms/vignetting.py +48 -22
- shinestacker/algorithms/white_balance.py +1 -0
- shinestacker/app/about_dialog.py +6 -2
- shinestacker/app/app_config.py +1 -0
- shinestacker/app/gui_utils.py +20 -0
- shinestacker/app/help_menu.py +1 -0
- shinestacker/app/main.py +9 -18
- shinestacker/app/open_frames.py +5 -4
- shinestacker/app/project.py +5 -16
- shinestacker/app/retouch.py +5 -17
- shinestacker/core/colors.py +4 -4
- shinestacker/core/core_utils.py +1 -1
- shinestacker/core/exceptions.py +2 -1
- shinestacker/core/framework.py +46 -33
- shinestacker/core/logging.py +9 -10
- shinestacker/gui/action_config.py +253 -197
- shinestacker/gui/actions_window.py +32 -28
- shinestacker/gui/colors.py +1 -0
- shinestacker/gui/gui_images.py +7 -3
- shinestacker/gui/gui_logging.py +3 -2
- shinestacker/gui/gui_run.py +53 -38
- shinestacker/gui/main_window.py +69 -25
- shinestacker/gui/new_project.py +35 -2
- shinestacker/gui/project_converter.py +21 -20
- shinestacker/gui/project_editor.py +45 -52
- shinestacker/gui/project_model.py +15 -23
- shinestacker/retouch/{filter_base.py → base_filter.py} +7 -4
- shinestacker/retouch/brush.py +1 -0
- shinestacker/retouch/brush_gradient.py +17 -3
- shinestacker/retouch/brush_preview.py +14 -10
- shinestacker/retouch/brush_tool.py +28 -19
- shinestacker/retouch/denoise_filter.py +3 -2
- shinestacker/retouch/display_manager.py +11 -5
- shinestacker/retouch/exif_data.py +1 -0
- shinestacker/retouch/file_loader.py +13 -9
- shinestacker/retouch/filter_manager.py +1 -0
- shinestacker/retouch/image_editor.py +14 -48
- shinestacker/retouch/image_editor_ui.py +10 -5
- shinestacker/retouch/image_filters.py +4 -2
- shinestacker/retouch/image_viewer.py +33 -31
- shinestacker/retouch/io_gui_handler.py +25 -13
- shinestacker/retouch/io_manager.py +3 -2
- shinestacker/retouch/layer_collection.py +79 -23
- shinestacker/retouch/shortcuts_help.py +1 -0
- shinestacker/retouch/undo_manager.py +7 -0
- shinestacker/retouch/unsharp_mask_filter.py +3 -2
- shinestacker/retouch/white_balance_filter.py +11 -6
- {shinestacker-0.3.2.dist-info → shinestacker-0.3.4.dist-info}/METADATA +10 -4
- shinestacker-0.3.4.dist-info/RECORD +86 -0
- shinestacker-0.3.2.dist-info/RECORD +0 -85
- {shinestacker-0.3.2.dist-info → shinestacker-0.3.4.dist-info}/WHEEL +0 -0
- {shinestacker-0.3.2.dist-info → shinestacker-0.3.4.dist-info}/entry_points.txt +0 -0
- {shinestacker-0.3.2.dist-info → shinestacker-0.3.4.dist-info}/licenses/LICENSE +0 -0
- {shinestacker-0.3.2.dist-info → shinestacker-0.3.4.dist-info}/top_level.txt +0 -0
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# pylint: disable=C0114, C0115, C0116, W0102, R0902, R0903
|
|
2
|
+
# pylint: disable=R0917, R0913, R1702, R0912, E1111, E1121, W0613
|
|
1
3
|
import logging
|
|
2
4
|
import os
|
|
3
5
|
from .. config.constants import constants
|
|
@@ -23,21 +25,34 @@ class StackJob(Job):
|
|
|
23
25
|
|
|
24
26
|
|
|
25
27
|
class FramePaths:
|
|
26
|
-
def __init__(self, name, input_path='', output_path='', working_path='',
|
|
27
|
-
|
|
28
|
+
def __init__(self, name, input_path='', output_path='', working_path='',
|
|
29
|
+
plot_path=constants.DEFAULT_PLOTS_PATH,
|
|
30
|
+
scratch_output_dir=True, resample=1,
|
|
31
|
+
reverse_order=constants.DEFAULT_FILE_REVERSE_ORDER, **_kwargs):
|
|
28
32
|
self.name = name
|
|
29
33
|
self.working_path = working_path
|
|
30
34
|
self.plot_path = plot_path
|
|
31
35
|
self.input_path = input_path
|
|
32
36
|
self.output_path = output_path
|
|
37
|
+
self.output_dir = None
|
|
33
38
|
self.resample = resample
|
|
34
39
|
self.reverse_order = reverse_order
|
|
35
40
|
self.scratch_output_dir = scratch_output_dir
|
|
41
|
+
self.input_full_path = None
|
|
42
|
+
self.enabled = None
|
|
43
|
+
self.filenames = None
|
|
44
|
+
|
|
45
|
+
def folder_filelist(self):
|
|
46
|
+
assert False, "this method should be overwritten"
|
|
47
|
+
|
|
48
|
+
def print_message(self, msg='', level=logging.INFO, end=None, begin='', tqdm=False):
|
|
49
|
+
assert False, "this method should be overwritten"
|
|
36
50
|
|
|
37
51
|
def set_filelist(self):
|
|
38
|
-
self.filenames = self.folder_filelist(
|
|
52
|
+
self.filenames = self.folder_filelist()
|
|
39
53
|
file_list = self.input_full_path.replace(self.working_path, '').lstrip('/')
|
|
40
|
-
self.print_message(color_str(": {
|
|
54
|
+
self.print_message(color_str(f": {len(self.filenames)} files in folder: {file_list}",
|
|
55
|
+
'blue'))
|
|
41
56
|
self.print_message(color_str("focus stacking", 'blue'))
|
|
42
57
|
|
|
43
58
|
def init(self, job):
|
|
@@ -46,7 +61,9 @@ class FramePaths:
|
|
|
46
61
|
check_path_exists(self.working_path)
|
|
47
62
|
if self.output_path == '':
|
|
48
63
|
self.output_path = self.name
|
|
49
|
-
self.output_dir = self.working_path +
|
|
64
|
+
self.output_dir = self.working_path + \
|
|
65
|
+
('' if self.working_path[-1] == '/' else '/') + \
|
|
66
|
+
self.output_path
|
|
50
67
|
if not os.path.exists(self.output_dir):
|
|
51
68
|
os.makedirs(self.output_dir)
|
|
52
69
|
else:
|
|
@@ -58,19 +75,27 @@ class FramePaths:
|
|
|
58
75
|
file_path = os.path.join(self.output_dir, filename)
|
|
59
76
|
if os.path.isfile(file_path):
|
|
60
77
|
os.remove(file_path)
|
|
61
|
-
self.print_message(
|
|
78
|
+
self.print_message(
|
|
79
|
+
color_str(f": output directory {self.output_path} content erased",
|
|
80
|
+
'yellow'))
|
|
62
81
|
else:
|
|
63
|
-
self.print_message(
|
|
82
|
+
self.print_message(
|
|
83
|
+
color_str(f": module disabled, output directory {self.output_path}"
|
|
84
|
+
" not scratched", 'yellow'))
|
|
64
85
|
else:
|
|
65
|
-
self.print_message(
|
|
66
|
-
|
|
86
|
+
self.print_message(
|
|
87
|
+
color_str(
|
|
88
|
+
f": output directory {self.output_path} not empty, "
|
|
89
|
+
"files may be overwritten or merged with existing ones.", 'yellow'
|
|
90
|
+
), level=logging.WARNING)
|
|
67
91
|
if self.plot_path == '':
|
|
68
|
-
self.plot_path = self.working_path +
|
|
92
|
+
self.plot_path = self.working_path + \
|
|
93
|
+
('' if self.working_path[-1] == '/' else '/') + self.plot_path
|
|
69
94
|
if not os.path.exists(self.plot_path):
|
|
70
95
|
os.makedirs(self.plot_path)
|
|
71
96
|
if self.input_path == '':
|
|
72
97
|
if len(job.paths) == 0:
|
|
73
|
-
raise
|
|
98
|
+
raise RuntimeError(f"Job {job.name} does not have any configured path")
|
|
74
99
|
self.input_path = job.paths[-1]
|
|
75
100
|
job.paths.append(self.output_path)
|
|
76
101
|
|
|
@@ -81,15 +106,16 @@ class FrameDirectory(FramePaths):
|
|
|
81
106
|
|
|
82
107
|
def folder_list_str(self):
|
|
83
108
|
if isinstance(self.input_full_path, list):
|
|
84
|
-
file_list = ", ".join(
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
109
|
+
file_list = ", ".join(
|
|
110
|
+
list(self.input_full_path.replace(self.working_path, '').lstrip('/')))
|
|
111
|
+
return "folder" + ('s' if len(self.input_full_path) > 1 else '') + f": {file_list}"
|
|
112
|
+
return "folder: " + self.input_full_path.replace(self.working_path, '').lstrip('/')
|
|
88
113
|
|
|
89
|
-
def folder_filelist(self
|
|
114
|
+
def folder_filelist(self):
|
|
90
115
|
src_contents = os.walk(self.input_full_path)
|
|
91
|
-
|
|
92
|
-
filelist = [name for name in filenames
|
|
116
|
+
_dirpath, _, filenames = next(src_contents)
|
|
117
|
+
filelist = [name for name in filenames
|
|
118
|
+
if os.path.splitext(name)[-1][1:].lower() in constants.EXTENSIONS]
|
|
93
119
|
filelist.sort()
|
|
94
120
|
if self.reverse_order:
|
|
95
121
|
filelist.reverse()
|
|
@@ -97,24 +123,29 @@ class FrameDirectory(FramePaths):
|
|
|
97
123
|
filelist = filelist[0::self.resample]
|
|
98
124
|
return filelist
|
|
99
125
|
|
|
100
|
-
def init(self, job):
|
|
126
|
+
def init(self, job, _working_path=''):
|
|
101
127
|
FramePaths.init(self, job)
|
|
102
|
-
self.input_full_path = self.working_path +
|
|
128
|
+
self.input_full_path = self.working_path + \
|
|
129
|
+
('' if self.working_path[-1] == '/' else '/') + self.input_path
|
|
103
130
|
check_path_exists(self.input_full_path)
|
|
104
131
|
job.paths.append(self.output_path)
|
|
105
132
|
|
|
106
133
|
|
|
107
|
-
class FrameMultiDirectory:
|
|
108
|
-
def __init__(self, name, input_path='', output_path='', working_path='',
|
|
109
|
-
|
|
110
|
-
|
|
134
|
+
class FrameMultiDirectory(FramePaths):
|
|
135
|
+
def __init__(self, name, input_path='', output_path='', working_path='',
|
|
136
|
+
plot_path=constants.DEFAULT_PLOTS_PATH,
|
|
137
|
+
scratch_output_dir=True, resample=1,
|
|
138
|
+
reverse_order=constants.DEFAULT_FILE_REVERSE_ORDER, **_kwargs):
|
|
139
|
+
FramePaths.__init__(self, name, input_path, output_path, working_path, plot_path,
|
|
140
|
+
scratch_output_dir, resample, reverse_order)
|
|
141
|
+
self.input_full_path = None
|
|
111
142
|
|
|
112
143
|
def folder_list_str(self):
|
|
113
144
|
if isinstance(self.input_full_path, list):
|
|
114
|
-
file_list = ", ".join([d.replace(self.working_path, '').lstrip('/')
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
145
|
+
file_list = ", ".join([d.replace(self.working_path, '').lstrip('/')
|
|
146
|
+
for d in self.input_full_path])
|
|
147
|
+
return "folder" + ('s' if len(self.input_full_path) > 1 else '') + f": {file_list}"
|
|
148
|
+
return "folder: " + self.input_full_path.replace(self.working_path, '').lstrip('/')
|
|
118
149
|
|
|
119
150
|
def folder_filelist(self):
|
|
120
151
|
if isinstance(self.input_full_path, str):
|
|
@@ -124,39 +155,49 @@ class FrameMultiDirectory:
|
|
|
124
155
|
dirs = self.input_full_path
|
|
125
156
|
paths = self.input_path
|
|
126
157
|
else:
|
|
127
|
-
raise
|
|
158
|
+
raise RuntimeError("input_full_path option must contain a path or an array of paths")
|
|
128
159
|
files = []
|
|
129
160
|
for d, p in zip(dirs, paths):
|
|
130
161
|
filelist = []
|
|
131
|
-
for
|
|
132
|
-
filelist = [p + "/" + name
|
|
162
|
+
for _dirpath, _, filenames in os.walk(d):
|
|
163
|
+
filelist = [p + "/" + name
|
|
164
|
+
for name in filenames
|
|
165
|
+
if os.path.splitext(name)[-1][1:].lower() in constants.EXTENSIONS]
|
|
133
166
|
if self.reverse_order:
|
|
134
167
|
filelist.reverse()
|
|
135
168
|
if self.resample > 1:
|
|
136
169
|
filelist = filelist[0::self.resample]
|
|
137
170
|
files += filelist
|
|
138
171
|
if len(files) == 0:
|
|
139
|
-
self.print_message(color_str(f"input folder {p} does not contain any image", "red"),
|
|
172
|
+
self.print_message(color_str(f"input folder {p} does not contain any image", "red"),
|
|
173
|
+
level=logging.WARNING)
|
|
140
174
|
return files
|
|
141
175
|
|
|
142
176
|
def init(self, job):
|
|
143
177
|
FramePaths.init(self, job)
|
|
144
178
|
if isinstance(self.input_path, str):
|
|
145
|
-
self.input_full_path = self.working_path +
|
|
179
|
+
self.input_full_path = self.working_path + \
|
|
180
|
+
('' if self.working_path[-1] == '/' else '/') + \
|
|
181
|
+
self.input_path
|
|
146
182
|
check_path_exists(self.input_full_path)
|
|
147
183
|
elif hasattr(self.input_path, "__len__"):
|
|
148
184
|
self.input_full_path = []
|
|
149
185
|
for path in self.input_path:
|
|
150
|
-
self.input_full_path.append(self.working_path +
|
|
186
|
+
self.input_full_path.append(self.working_path +
|
|
187
|
+
('' if self.working_path[-1] == '/' else '/') +
|
|
188
|
+
path)
|
|
151
189
|
job.paths.append(self.output_path)
|
|
152
190
|
|
|
153
191
|
|
|
154
|
-
class FramesRefActions(
|
|
192
|
+
class FramesRefActions(ActionList, FrameDirectory):
|
|
155
193
|
def __init__(self, name, enabled=True, ref_idx=-1, step_process=False, **kwargs):
|
|
156
194
|
FrameDirectory.__init__(self, name, **kwargs)
|
|
157
195
|
ActionList.__init__(self, name, enabled)
|
|
158
196
|
self.ref_idx = ref_idx
|
|
159
197
|
self.step_process = step_process
|
|
198
|
+
self._idx = None
|
|
199
|
+
self._ref_idx = None
|
|
200
|
+
self._idx_step = None
|
|
160
201
|
|
|
161
202
|
def begin(self):
|
|
162
203
|
ActionList.begin(self)
|
|
@@ -168,28 +209,28 @@ class FramesRefActions(FrameDirectory, ActionList):
|
|
|
168
209
|
def end(self):
|
|
169
210
|
ActionList.end(self)
|
|
170
211
|
|
|
171
|
-
def run_frame(self,
|
|
172
|
-
|
|
212
|
+
def run_frame(self, _idx, _ref_idx):
|
|
213
|
+
pass
|
|
173
214
|
|
|
174
215
|
def run_step(self):
|
|
175
216
|
if self.count == 1:
|
|
176
|
-
self.
|
|
177
|
-
self.
|
|
178
|
-
self.
|
|
217
|
+
self._idx = self.ref_idx if self.step_process else 0
|
|
218
|
+
self._ref_idx = self.ref_idx
|
|
219
|
+
self._idx_step = +1
|
|
179
220
|
ll = len(self.filenames)
|
|
180
221
|
self.print_message_r(
|
|
181
|
-
color_str("step {}/{}: process file: {
|
|
182
|
-
|
|
183
|
-
self.run_frame(self.
|
|
184
|
-
if self.
|
|
222
|
+
color_str(f"step {self.count}/{ll}: process file: {self.filenames[self._idx]}, "
|
|
223
|
+
f"reference: {self.filenames[self._ref_idx]}", "blue"))
|
|
224
|
+
self.run_frame(self._idx, self._ref_idx)
|
|
225
|
+
if self._idx < ll:
|
|
185
226
|
if self.step_process:
|
|
186
|
-
self.
|
|
187
|
-
self.
|
|
188
|
-
if self.
|
|
189
|
-
self.
|
|
227
|
+
self._ref_idx = self._idx
|
|
228
|
+
self._idx += self._idx_step
|
|
229
|
+
if self._idx == ll:
|
|
230
|
+
self._idx = self.ref_idx - 1
|
|
190
231
|
if self.step_process:
|
|
191
|
-
self.
|
|
192
|
-
self.
|
|
232
|
+
self._ref_idx = self.ref_idx
|
|
233
|
+
self._idx_step = -1
|
|
193
234
|
|
|
194
235
|
|
|
195
236
|
class SubAction:
|
|
@@ -200,38 +241,42 @@ class SubAction:
|
|
|
200
241
|
class CombinedActions(FramesRefActions):
|
|
201
242
|
def __init__(self, name, actions=[], enabled=True, **kwargs):
|
|
202
243
|
FramesRefActions.__init__(self, name, enabled, **kwargs)
|
|
203
|
-
self.
|
|
244
|
+
self._actions = actions
|
|
245
|
+
self.dtype = None
|
|
246
|
+
self.shape = None
|
|
204
247
|
|
|
205
248
|
def begin(self):
|
|
206
249
|
FramesRefActions.begin(self)
|
|
207
|
-
for a in self.
|
|
250
|
+
for a in self._actions:
|
|
208
251
|
if a.enabled:
|
|
209
252
|
a.begin(self)
|
|
210
253
|
|
|
211
254
|
def img_ref(self, idx):
|
|
212
255
|
filename = self.filenames[idx]
|
|
213
|
-
img = read_img((self.output_dir
|
|
256
|
+
img = read_img((self.output_dir
|
|
257
|
+
if self.step_process else self.input_full_path) + f"/{filename}")
|
|
214
258
|
self.dtype = img.dtype
|
|
215
259
|
self.shape = img.shape
|
|
216
260
|
if img is None:
|
|
217
|
-
raise
|
|
261
|
+
raise RuntimeError(f"Invalid file: {self.input_full_path}/{filename}")
|
|
218
262
|
return img
|
|
219
263
|
|
|
220
264
|
def run_frame(self, idx, ref_idx):
|
|
221
265
|
filename = self.filenames[idx]
|
|
222
266
|
self.sub_message_r(': read input image')
|
|
223
|
-
img = read_img(self.input_full_path
|
|
224
|
-
if
|
|
225
|
-
raise BitDepthError(
|
|
226
|
-
if
|
|
227
|
-
raise ShapeError(
|
|
267
|
+
img = read_img(f"{self.input_full_path}/{filename}")
|
|
268
|
+
if self.dtype is not None and img.dtype != self.dtype:
|
|
269
|
+
raise BitDepthError(self.dtype, img.dtype, )
|
|
270
|
+
if self.shape is not None and img.shape != self.shape:
|
|
271
|
+
raise ShapeError(self.shape, img.shape)
|
|
228
272
|
if img is None:
|
|
229
|
-
raise
|
|
230
|
-
if len(self.
|
|
273
|
+
raise RuntimeError(f"Invalid file: {self.input_full_path}/{filename}")
|
|
274
|
+
if len(self._actions) == 0:
|
|
231
275
|
self.sub_message(color_str(": no actions specified.", "red"), level=logging.WARNING)
|
|
232
|
-
for a in self.
|
|
276
|
+
for a in self._actions:
|
|
233
277
|
if not a.enabled:
|
|
234
|
-
self.get_logger().warning(color_str(self.base_message
|
|
278
|
+
self.get_logger().warning(color_str(f"{self.base_message}: sub-action disabled",
|
|
279
|
+
'red'))
|
|
235
280
|
else:
|
|
236
281
|
if self.callback('check_running', self.id, self.name) is False:
|
|
237
282
|
raise RunStopException(self.name)
|
|
@@ -240,9 +285,10 @@ class CombinedActions(FramesRefActions):
|
|
|
240
285
|
if img is not None:
|
|
241
286
|
write_img(self.output_dir + "/" + filename, img)
|
|
242
287
|
else:
|
|
243
|
-
self.print_message("No output file resulted from processing input file: "
|
|
288
|
+
self.print_message("No output file resulted from processing input file: "
|
|
289
|
+
f"{self.input_full_path}/{filename}", level=logging.WARNING)
|
|
244
290
|
|
|
245
291
|
def end(self):
|
|
246
|
-
for a in self.
|
|
292
|
+
for a in self._actions:
|
|
247
293
|
if a.enabled:
|
|
248
294
|
a.end()
|
shinestacker/algorithms/utils.py
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
|
|
1
|
+
# pylint: disable=C0114, C0116, E1101
|
|
2
2
|
import os
|
|
3
|
-
import numpy as np
|
|
4
3
|
import logging
|
|
4
|
+
import numpy as np
|
|
5
|
+
import cv2
|
|
5
6
|
import matplotlib.pyplot as plt
|
|
6
7
|
from .. config.config import config
|
|
7
8
|
from .. core.exceptions import ShapeError, BitDepthError
|
|
@@ -9,20 +10,21 @@ from .. core.exceptions import ShapeError, BitDepthError
|
|
|
9
10
|
|
|
10
11
|
def read_img(file_path):
|
|
11
12
|
if not os.path.isfile(file_path):
|
|
12
|
-
raise
|
|
13
|
+
raise RuntimeError("File does not exist: " + file_path)
|
|
13
14
|
ext = file_path.split(".")[-1]
|
|
14
|
-
|
|
15
|
+
img = None
|
|
16
|
+
if ext in ['jpeg', 'jpg']:
|
|
15
17
|
img = cv2.imread(file_path)
|
|
16
|
-
elif ext
|
|
18
|
+
elif ext in ['tiff', 'tif', 'png']:
|
|
17
19
|
img = cv2.imread(file_path, cv2.IMREAD_UNCHANGED)
|
|
18
20
|
return img
|
|
19
21
|
|
|
20
22
|
|
|
21
23
|
def write_img(file_path, img):
|
|
22
24
|
ext = file_path.split(".")[-1]
|
|
23
|
-
if ext
|
|
25
|
+
if ext in ['jpeg', 'jpg']:
|
|
24
26
|
cv2.imwrite(file_path, img, [int(cv2.IMWRITE_JPEG_QUALITY), 100])
|
|
25
|
-
elif ext
|
|
27
|
+
elif ext in ['tiff', 'tif']:
|
|
26
28
|
cv2.imwrite(file_path, img, [int(cv2.IMWRITE_TIFF_COMPRESSION), 1])
|
|
27
29
|
elif ext == 'png':
|
|
28
30
|
cv2.imwrite(file_path, img)
|
|
@@ -36,10 +38,9 @@ def img_bw_8bit(img):
|
|
|
36
38
|
img = img_8bit(img)
|
|
37
39
|
if len(img.shape) == 3:
|
|
38
40
|
return cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
|
|
39
|
-
|
|
41
|
+
if len(img.shape) == 2:
|
|
40
42
|
return img
|
|
41
|
-
|
|
42
|
-
raise ValueError(f"Unsupported image format: {img.shape}")
|
|
43
|
+
raise ValueError(f"Unsupported image format: {img.shape}")
|
|
43
44
|
|
|
44
45
|
|
|
45
46
|
def img_bw(img):
|
|
@@ -63,7 +64,7 @@ def validate_image(img, expected_shape=None, expected_dtype=None):
|
|
|
63
64
|
|
|
64
65
|
|
|
65
66
|
def save_plot(filename):
|
|
66
|
-
logging.getLogger(__name__).debug("save plot file: "
|
|
67
|
+
logging.getLogger(__name__).debug(msg=f"save plot file: {filename}")
|
|
67
68
|
dir_path = os.path.dirname(filename)
|
|
68
69
|
if not dir_path:
|
|
69
70
|
dir_path = '.'
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
|
|
1
|
+
# pylint: disable=C0114, C0115, C0116, R0902, E1101, W0718, W0640
|
|
2
2
|
import logging
|
|
3
3
|
import numpy as np
|
|
4
4
|
import matplotlib.pyplot as plt
|
|
5
5
|
from scipy.optimize import curve_fit, fsolve
|
|
6
|
+
import cv2
|
|
6
7
|
from .. core.colors import color_str
|
|
7
8
|
from .. config.constants import constants
|
|
8
9
|
from .utils import img_8bit, save_plot
|
|
@@ -21,6 +22,12 @@ class Vignetting(SubAction):
|
|
|
21
22
|
self.plot_summary = kwargs.get('plot_summary', False)
|
|
22
23
|
self.max_correction = kwargs.get('max_correction', constants.DEFAULT_MAX_CORRECTION)
|
|
23
24
|
self.percentiles = np.sort(percentiles)
|
|
25
|
+
self.w_2 = None
|
|
26
|
+
self.h_2 = None
|
|
27
|
+
self.v0 = None
|
|
28
|
+
self.r_max = None
|
|
29
|
+
self.process = None
|
|
30
|
+
self.corrections = None
|
|
24
31
|
|
|
25
32
|
def radial_mean_intensity(self, image):
|
|
26
33
|
if len(image.shape) > 2:
|
|
@@ -40,8 +47,12 @@ class Vignetting(SubAction):
|
|
|
40
47
|
mean_intensities[i] = np.nan
|
|
41
48
|
return (radii[1:] + radii[:-1]) / 2, mean_intensities
|
|
42
49
|
|
|
50
|
+
@staticmethod
|
|
43
51
|
def sigmoid(r, i0, k, r0):
|
|
44
|
-
return i0 / (1.0 +
|
|
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)))))
|
|
45
56
|
|
|
46
57
|
def fit_sigmoid(self, radii, intensities):
|
|
47
58
|
valid_mask = ~np.isnan(intensities)
|
|
@@ -50,7 +61,9 @@ class Vignetting(SubAction):
|
|
|
50
61
|
res = curve_fit(Vignetting.sigmoid, r_valid, i_valid,
|
|
51
62
|
p0=[np.max(i_valid), 0.01, np.median(r_valid)])[0]
|
|
52
63
|
except Exception:
|
|
53
|
-
self.process.sub_message(
|
|
64
|
+
self.process.sub_message(
|
|
65
|
+
color_str(": could not find vignetting model", "red"),
|
|
66
|
+
level=logging.WARNING)
|
|
54
67
|
res = None
|
|
55
68
|
return res
|
|
56
69
|
|
|
@@ -66,9 +79,10 @@ class Vignetting(SubAction):
|
|
|
66
79
|
vignette[np.min(image, axis=2) < self.black_threshold, :] = 1
|
|
67
80
|
else:
|
|
68
81
|
vignette[image < self.black_threshold] = 1
|
|
69
|
-
return np.clip(image / vignette, 0, 255
|
|
82
|
+
return np.clip(image / vignette, 0, 255
|
|
83
|
+
if image.dtype == np.uint8 else 65535).astype(image.dtype)
|
|
70
84
|
|
|
71
|
-
def run_frame(self, idx,
|
|
85
|
+
def run_frame(self, idx, _ref_idx, img_0):
|
|
72
86
|
self.process.sub_message_r(color_str(": compute vignetting", "light_blue"))
|
|
73
87
|
img = cv2.cvtColor(img_8bit(img_0), cv2.COLOR_BGR2GRAY)
|
|
74
88
|
radii, intensities = self.radial_mean_intensity(img)
|
|
@@ -77,8 +91,9 @@ class Vignetting(SubAction):
|
|
|
77
91
|
return img_0
|
|
78
92
|
self.v0 = Vignetting.sigmoid(0, *pars)
|
|
79
93
|
i0_fit, k_fit, r0_fit = pars
|
|
80
|
-
self.process.sub_message(
|
|
81
|
-
|
|
94
|
+
self.process.sub_message(
|
|
95
|
+
f": fit parameters: i0={i0_fit:.4f}, k={k_fit:.4f}, r0={r0_fit:.4f}",
|
|
96
|
+
level=logging.DEBUG)
|
|
82
97
|
if self.plot_correction:
|
|
83
98
|
plt.figure(figsize=(10, 5))
|
|
84
99
|
plt.plot(radii, intensities, label="image mean intensity")
|
|
@@ -88,22 +103,27 @@ class Vignetting(SubAction):
|
|
|
88
103
|
plt.legend()
|
|
89
104
|
plt.xlim(radii[0], radii[-1])
|
|
90
105
|
plt.ylim(0)
|
|
91
|
-
idx_str = "{:04d}"
|
|
92
|
-
plot_path = f"{self.process.working_path}/
|
|
106
|
+
idx_str = f"{idx:04d}"
|
|
107
|
+
plot_path = f"{self.process.working_path}/" \
|
|
108
|
+
f"{self.process.plot_path}/{self.process.name}-" \
|
|
109
|
+
f"radial-intensity-{idx_str}.pdf"
|
|
93
110
|
save_plot(plot_path)
|
|
94
111
|
plt.close('all')
|
|
95
|
-
self.process.callback(
|
|
112
|
+
self.process.callback(
|
|
113
|
+
'save_plot', self.process.id,
|
|
114
|
+
f"{self.process.name}: intensity\nframe {idx_str}", plot_path)
|
|
96
115
|
for i, p in enumerate(self.percentiles):
|
|
97
|
-
self.corrections[i][idx] = fsolve(lambda x: Vignetting.sigmoid(x, *pars) /
|
|
116
|
+
self.corrections[i][idx] = fsolve(lambda x: Vignetting.sigmoid(x, *pars) /
|
|
117
|
+
self.v0 - p, r0_fit)[0]
|
|
98
118
|
if self.apply_correction:
|
|
99
119
|
self.process.sub_message_r(color_str(": correct vignetting", "light_blue"))
|
|
100
120
|
return self.correct_vignetting(img_0, pars)
|
|
101
|
-
|
|
102
|
-
return img_0
|
|
121
|
+
return img_0
|
|
103
122
|
|
|
104
123
|
def begin(self, process):
|
|
105
124
|
self.process = process
|
|
106
|
-
self.corrections = [np.full(self.process.counts, None, dtype=float)
|
|
125
|
+
self.corrections = [np.full(self.process.counts, None, dtype=float)
|
|
126
|
+
for p in self.percentiles]
|
|
107
127
|
|
|
108
128
|
def end(self):
|
|
109
129
|
if self.plot_summary:
|
|
@@ -113,7 +133,7 @@ class Vignetting(SubAction):
|
|
|
113
133
|
linestyle = 'solid'
|
|
114
134
|
if p == 0.5:
|
|
115
135
|
linestyle = '-.'
|
|
116
|
-
elif i
|
|
136
|
+
elif i in (0, len(self.percentiles) - 1):
|
|
117
137
|
linestyle = 'dotted'
|
|
118
138
|
plt.plot(xs, self.corrections[i], label=f"{p:.0%} correction",
|
|
119
139
|
linestyle=linestyle, color="blue")
|
|
@@ -121,17 +141,23 @@ class Vignetting(SubAction):
|
|
|
121
141
|
iis = np.where(self.percentiles == 0.5)
|
|
122
142
|
if len(iis) > 0:
|
|
123
143
|
i = iis[0][0]
|
|
124
|
-
if
|
|
125
|
-
plt.fill_between(xs, self.corrections[i - 1], self.corrections[i + 1],
|
|
126
|
-
|
|
127
|
-
plt.plot(xs[[0, -1]], [self.
|
|
128
|
-
|
|
144
|
+
if 1 <= i < len(self.percentiles) - 1:
|
|
145
|
+
plt.fill_between(xs, self.corrections[i - 1], self.corrections[i + 1],
|
|
146
|
+
color="#0000ff20")
|
|
147
|
+
plt.plot(xs[[0, -1]], [self.r_max] * 2,
|
|
148
|
+
linestyle="--", label="max. radius", color="darkred")
|
|
149
|
+
plt.plot(xs[[0, -1]], [self.w_2] * 2,
|
|
150
|
+
linestyle="--", label="half width", color="limegreen")
|
|
151
|
+
plt.plot(xs[[0, -1]], [self.h_2] * 2,
|
|
152
|
+
linestyle="--", label="half height", color="darkgreen")
|
|
129
153
|
plt.xlabel('frame')
|
|
130
154
|
plt.ylabel('distance from center (pixels)')
|
|
131
155
|
plt.legend(ncols=2)
|
|
132
156
|
plt.xlim(xs[0], xs[-1])
|
|
133
157
|
plt.ylim(0, self.r_max * 1.05)
|
|
134
|
-
plot_path = self.process.working_path
|
|
158
|
+
plot_path = f"{self.process.working_path}/{self.process.plot_path}/" \
|
|
159
|
+
f"{self.process.name}-r0.pdf"
|
|
135
160
|
save_plot(plot_path)
|
|
136
161
|
plt.close('all')
|
|
137
|
-
self.process.callback('save_plot', self.process.id,
|
|
162
|
+
self.process.callback('save_plot', self.process.id,
|
|
163
|
+
f"{self.process.name}: vignetting", plot_path)
|
shinestacker/app/about_dialog.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
# pylint: disable=C0114, C0116, E0611
|
|
1
2
|
from PySide6.QtWidgets import QMessageBox
|
|
2
3
|
from PySide6.QtCore import Qt
|
|
3
4
|
from .. import __version__
|
|
@@ -5,7 +6,7 @@ from .. config.constants import constants
|
|
|
5
6
|
|
|
6
7
|
|
|
7
8
|
def show_about_dialog():
|
|
8
|
-
version_clean = __version__.split("+")[0]
|
|
9
|
+
version_clean = __version__.split("+", maxsplit=1)[0]
|
|
9
10
|
about_text = f"""
|
|
10
11
|
<h3>{constants.APP_TITLE}</h3>
|
|
11
12
|
<h4>version: v{version_clean}</h4>
|
|
@@ -13,7 +14,10 @@ def show_about_dialog():
|
|
|
13
14
|
into a single focused image.</p>
|
|
14
15
|
<p>Author: Luca Lista<br/>
|
|
15
16
|
Email: <a href="mailto:luka.lista@gmail.com">luka.lista@gmail.com</a></p>
|
|
16
|
-
<
|
|
17
|
+
<ul>
|
|
18
|
+
<li><a href="https://shinestacker.wordpress.com/">Website on Wordpress</a></li>
|
|
19
|
+
<li><a href="https://github.com/lucalista/shinestacker">GitHub project repository</a></li>
|
|
20
|
+
</ul>
|
|
17
21
|
"""
|
|
18
22
|
msg = QMessageBox()
|
|
19
23
|
msg.setWindowTitle(f"About {constants.APP_STRING}")
|
shinestacker/app/app_config.py
CHANGED
shinestacker/app/gui_utils.py
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
|
+
# pylint: disable=C0114, C0116, E0611
|
|
1
2
|
import os
|
|
2
3
|
import sys
|
|
3
4
|
from PySide6.QtCore import QCoreApplication, QProcess
|
|
5
|
+
from PySide6.QtGui import QAction
|
|
6
|
+
from shinestacker.config.constants import constants
|
|
7
|
+
from shinestacker.config.config import config
|
|
8
|
+
from shinestacker.app.about_dialog import show_about_dialog
|
|
4
9
|
|
|
5
10
|
|
|
6
11
|
def disable_macos_special_menu_items():
|
|
@@ -33,3 +38,18 @@ def disable_macos_special_menu_items():
|
|
|
33
38
|
if user:
|
|
34
39
|
QProcess.startDetached("pkill", ["-u", user, "-f", "cfprefsd"])
|
|
35
40
|
QProcess.startDetached("pkill", ["-u", user, "-f", "SystemUIServer"])
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def fill_app_menu(app, app_menu):
|
|
44
|
+
about_action = QAction(f"About {constants.APP_STRING}", app)
|
|
45
|
+
about_action.triggered.connect(show_about_dialog)
|
|
46
|
+
app_menu.addAction(about_action)
|
|
47
|
+
app_menu.addSeparator()
|
|
48
|
+
if config.DONT_USE_NATIVE_MENU:
|
|
49
|
+
quit_txt, quit_short = "&Quit", "Ctrl+Q"
|
|
50
|
+
else:
|
|
51
|
+
quit_txt, quit_short = "Shut dw&wn", "Ctrl+Q"
|
|
52
|
+
exit_action = QAction(quit_txt, app)
|
|
53
|
+
exit_action.setShortcut(quit_short)
|
|
54
|
+
exit_action.triggered.connect(app.quit)
|
|
55
|
+
app_menu.addAction(exit_action)
|
shinestacker/app/help_menu.py
CHANGED