shinestacker 0.3.4__py3-none-any.whl → 0.3.6__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/base_stack_algo.py +1 -2
- shinestacker/algorithms/noise_detection.py +0 -3
- shinestacker/algorithms/pyramid.py +7 -4
- shinestacker/algorithms/stack.py +1 -2
- shinestacker/algorithms/stack_framework.py +8 -2
- shinestacker/algorithms/vignetting.py +5 -4
- shinestacker/app/app_config.py +4 -22
- shinestacker/app/help_menu.py +1 -1
- shinestacker/config/config.py +21 -16
- shinestacker/gui/action_config.py +10 -35
- shinestacker/gui/actions_window.py +22 -54
- shinestacker/gui/new_project.py +5 -22
- shinestacker/gui/project_editor.py +49 -20
- shinestacker/gui/select_path_widget.py +30 -0
- shinestacker/retouch/base_filter.py +12 -1
- shinestacker/retouch/denoise_filter.py +4 -10
- shinestacker/retouch/exif_data.py +3 -9
- shinestacker/retouch/icon_container.py +19 -0
- shinestacker/retouch/shortcuts_help.py +2 -13
- shinestacker/retouch/unsharp_mask_filter.py +3 -10
- shinestacker/retouch/white_balance_filter.py +5 -13
- {shinestacker-0.3.4.dist-info → shinestacker-0.3.6.dist-info}/METADATA +10 -8
- {shinestacker-0.3.4.dist-info → shinestacker-0.3.6.dist-info}/RECORD +28 -27
- shinestacker/algorithms/core_utils.py +0 -22
- {shinestacker-0.3.4.dist-info → shinestacker-0.3.6.dist-info}/WHEEL +0 -0
- {shinestacker-0.3.4.dist-info → shinestacker-0.3.6.dist-info}/entry_points.txt +0 -0
- {shinestacker-0.3.4.dist-info → shinestacker-0.3.6.dist-info}/licenses/LICENSE +0 -0
- {shinestacker-0.3.4.dist-info → shinestacker-0.3.6.dist-info}/top_level.txt +0 -0
shinestacker/_version.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = '0.3.
|
|
1
|
+
__version__ = '0.3.6'
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
# pylint: disable=C0114, C0115, C0116, E0602, R0903
|
|
2
2
|
import numpy as np
|
|
3
|
-
from .. core.colors import color_str
|
|
4
3
|
from .. core.exceptions import InvalidOptionError, ImageLoadError
|
|
5
4
|
from .. config.constants import constants
|
|
6
5
|
from .utils import read_img, get_img_metadata, validate_image
|
|
@@ -28,7 +27,7 @@ class BaseStackAlgo:
|
|
|
28
27
|
return self._steps_per_frame
|
|
29
28
|
|
|
30
29
|
def print_message(self, msg):
|
|
31
|
-
self.process.sub_message_r(
|
|
30
|
+
self.process.sub_message_r(msg)
|
|
32
31
|
|
|
33
32
|
def read_image_and_update_metadata(self, img_path, metadata):
|
|
34
33
|
img = read_img(img_path)
|
|
@@ -162,9 +162,6 @@ class MaskNoise(SubAction):
|
|
|
162
162
|
else:
|
|
163
163
|
raise ImageLoadError(path, "file not found.")
|
|
164
164
|
|
|
165
|
-
def end(self):
|
|
166
|
-
pass
|
|
167
|
-
|
|
168
165
|
def run_frame(self, _idx, _ref_idx, image):
|
|
169
166
|
self.process.sub_message_r(': mask noisy pixels')
|
|
170
167
|
if len(image.shape) == 3:
|
|
@@ -12,7 +12,7 @@ class PyramidBase(BaseStackAlgo):
|
|
|
12
12
|
kernel_size=constants.DEFAULT_PY_KERNEL_SIZE,
|
|
13
13
|
gen_kernel=constants.DEFAULT_PY_GEN_KERNEL,
|
|
14
14
|
float_type=constants.DEFAULT_PY_FLOAT):
|
|
15
|
-
super().__init__("pyramid",
|
|
15
|
+
super().__init__("pyramid", 2, float_type)
|
|
16
16
|
self.min_size = min_size
|
|
17
17
|
self.kernel_size = kernel_size
|
|
18
18
|
self.pad_amount = (kernel_size - 1) // 2
|
|
@@ -151,11 +151,11 @@ class PyramidStack(PyramidBase):
|
|
|
151
151
|
metadata = None
|
|
152
152
|
all_laplacians = []
|
|
153
153
|
levels = None
|
|
154
|
+
n = len(filenames)
|
|
154
155
|
for i, img_path in enumerate(filenames):
|
|
155
156
|
self.print_message(f": validating file {img_path.split('/')[-1]}")
|
|
156
157
|
|
|
157
158
|
img, metadata, updated = self.read_image_and_update_metadata(img_path, metadata)
|
|
158
|
-
|
|
159
159
|
if updated:
|
|
160
160
|
self.dtype = metadata[1]
|
|
161
161
|
self.num_pixel_values = constants.NUM_UINT8 \
|
|
@@ -163,14 +163,17 @@ class PyramidStack(PyramidBase):
|
|
|
163
163
|
self.max_pixel_value = constants.MAX_UINT8 \
|
|
164
164
|
if self.dtype == np.uint8 else constants.MAX_UINT16
|
|
165
165
|
levels = int(np.log2(min(img.shape[:2]) / self.min_size))
|
|
166
|
-
|
|
167
166
|
if self.do_step_callback:
|
|
168
167
|
self.process.callback('after_step', self.process.id, self.process.name, i)
|
|
169
168
|
if self.process.callback('check_running', self.process.id, self.process.name) is False:
|
|
170
169
|
raise RunStopException(self.name)
|
|
171
|
-
for img_path in filenames:
|
|
170
|
+
for i, img_path in enumerate(filenames):
|
|
172
171
|
self.print_message(f": processing file {img_path.split('/')[-1]}")
|
|
173
172
|
img = read_img(img_path)
|
|
174
173
|
all_laplacians.append(self.process_single_image(img, levels))
|
|
174
|
+
if self.do_step_callback:
|
|
175
|
+
self.process.callback('after_step', self.process.id, self.process.name, i + n)
|
|
176
|
+
if self.process.callback('check_running', self.process.id, self.process.name) is False:
|
|
177
|
+
raise RunStopException(self.name)
|
|
175
178
|
stacked_image = self.collapse(self.fuse_pyramids(all_laplacians))
|
|
176
179
|
return stacked_image.astype(self.dtype)
|
shinestacker/algorithms/stack.py
CHANGED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
import os
|
|
3
3
|
import numpy as np
|
|
4
4
|
from .. config.constants import constants
|
|
5
|
-
from .. core.colors import color_str
|
|
6
5
|
from .. core.framework import JobBase
|
|
7
6
|
from .. core.exceptions import InvalidOptionError
|
|
8
7
|
from .utils import write_img
|
|
@@ -92,7 +91,7 @@ class FocusStackBunch(ActionList, FocusStackBase):
|
|
|
92
91
|
ActionList.end(self)
|
|
93
92
|
|
|
94
93
|
def run_step(self):
|
|
95
|
-
self.print_message_r(
|
|
94
|
+
self.print_message_r(f"fusing bunch: {self.count}")
|
|
96
95
|
self.focus_stack(self._chunks[self.count - 1])
|
|
97
96
|
self.callback('after_step', self.id, self.name, self.count)
|
|
98
97
|
|
|
@@ -237,6 +237,12 @@ class SubAction:
|
|
|
237
237
|
def __init__(self, enabled=True):
|
|
238
238
|
self.enabled = enabled
|
|
239
239
|
|
|
240
|
+
def begin(self, process):
|
|
241
|
+
pass
|
|
242
|
+
|
|
243
|
+
def end(self):
|
|
244
|
+
pass
|
|
245
|
+
|
|
240
246
|
|
|
241
247
|
class CombinedActions(FramesRefActions):
|
|
242
248
|
def __init__(self, name, actions=[], enabled=True, **kwargs):
|
|
@@ -255,10 +261,10 @@ class CombinedActions(FramesRefActions):
|
|
|
255
261
|
filename = self.filenames[idx]
|
|
256
262
|
img = read_img((self.output_dir
|
|
257
263
|
if self.step_process else self.input_full_path) + f"/{filename}")
|
|
258
|
-
self.dtype = img.dtype
|
|
259
|
-
self.shape = img.shape
|
|
260
264
|
if img is None:
|
|
261
265
|
raise RuntimeError(f"Invalid file: {self.input_full_path}/{filename}")
|
|
266
|
+
self.dtype = img.dtype
|
|
267
|
+
self.shape = img.shape
|
|
262
268
|
return img
|
|
263
269
|
|
|
264
270
|
def run_frame(self, idx, ref_idx):
|
|
@@ -59,7 +59,8 @@ class Vignetting(SubAction):
|
|
|
59
59
|
i_valid, r_valid = intensities[valid_mask], radii[valid_mask]
|
|
60
60
|
try:
|
|
61
61
|
res = curve_fit(Vignetting.sigmoid, r_valid, i_valid,
|
|
62
|
-
p0=[np.max(i_valid), 0.01, np.median(r_valid)]
|
|
62
|
+
p0=[np.max(i_valid), 0.01, np.median(r_valid)],
|
|
63
|
+
bounds=([0, 0, 0], ['inf', 'inf', 'inf']))[0]
|
|
63
64
|
except Exception:
|
|
64
65
|
self.process.sub_message(
|
|
65
66
|
color_str(": could not find vignetting model", "red"),
|
|
@@ -83,7 +84,7 @@ class Vignetting(SubAction):
|
|
|
83
84
|
if image.dtype == np.uint8 else 65535).astype(image.dtype)
|
|
84
85
|
|
|
85
86
|
def run_frame(self, idx, _ref_idx, img_0):
|
|
86
|
-
self.process.sub_message_r(
|
|
87
|
+
self.process.sub_message_r(": compute vignetting")
|
|
87
88
|
img = cv2.cvtColor(img_8bit(img_0), cv2.COLOR_BGR2GRAY)
|
|
88
89
|
radii, intensities = self.radial_mean_intensity(img)
|
|
89
90
|
pars = self.fit_sigmoid(radii, intensities)
|
|
@@ -92,7 +93,7 @@ class Vignetting(SubAction):
|
|
|
92
93
|
self.v0 = Vignetting.sigmoid(0, *pars)
|
|
93
94
|
i0_fit, k_fit, r0_fit = pars
|
|
94
95
|
self.process.sub_message(
|
|
95
|
-
f":
|
|
96
|
+
f": vignetting model parameters: i0={i0_fit:.4f}, k={k_fit:.4f}, r0={r0_fit:.4f}",
|
|
96
97
|
level=logging.DEBUG)
|
|
97
98
|
if self.plot_correction:
|
|
98
99
|
plt.figure(figsize=(10, 5))
|
|
@@ -116,7 +117,7 @@ class Vignetting(SubAction):
|
|
|
116
117
|
self.corrections[i][idx] = fsolve(lambda x: Vignetting.sigmoid(x, *pars) /
|
|
117
118
|
self.v0 - p, r0_fit)[0]
|
|
118
119
|
if self.apply_correction:
|
|
119
|
-
self.process.sub_message_r(
|
|
120
|
+
self.process.sub_message_r(": correct vignetting")
|
|
120
121
|
return self.correct_vignetting(img_0, pars)
|
|
121
122
|
return img_0
|
|
122
123
|
|
shinestacker/app/app_config.py
CHANGED
|
@@ -1,28 +1,15 @@
|
|
|
1
1
|
# pylint: disable=C0114, C0115, C0116, C0103, W0201
|
|
2
|
-
|
|
3
|
-
_initialized = False
|
|
4
|
-
_instance = None
|
|
2
|
+
from .. config.config import _ConfigBase
|
|
5
3
|
|
|
4
|
+
|
|
5
|
+
class _AppConfig(_ConfigBase):
|
|
6
6
|
def __new__(cls):
|
|
7
|
-
|
|
8
|
-
cls._instance = super().__new__(cls)
|
|
9
|
-
cls._instance._init_defaults()
|
|
10
|
-
return cls._instance
|
|
7
|
+
return _ConfigBase.__new__(cls)
|
|
11
8
|
|
|
12
9
|
def _init_defaults(self):
|
|
13
10
|
self._DONT_USE_NATIVE_MENU = True
|
|
14
11
|
self._COMBINED_APP = False
|
|
15
12
|
|
|
16
|
-
def init(self, **kwargs):
|
|
17
|
-
if self._initialized:
|
|
18
|
-
raise RuntimeError("Config already initialized")
|
|
19
|
-
for k, v in kwargs.items():
|
|
20
|
-
if hasattr(self, f"_{k}"):
|
|
21
|
-
setattr(self, f"_{k}", v)
|
|
22
|
-
else:
|
|
23
|
-
raise AttributeError(f"Invalid config key: {k}")
|
|
24
|
-
self._initialized = True
|
|
25
|
-
|
|
26
13
|
@property
|
|
27
14
|
def DONT_USE_NATIVE_MENU(self):
|
|
28
15
|
return self._DONT_USE_NATIVE_MENU
|
|
@@ -31,10 +18,5 @@ class _AppConfig:
|
|
|
31
18
|
def COMBINED_APP(self):
|
|
32
19
|
return self._COMBINED_APP
|
|
33
20
|
|
|
34
|
-
def __setattr__(self, name, value):
|
|
35
|
-
if self._initialized and name.startswith('_'):
|
|
36
|
-
raise AttributeError("Can't change config after initialization")
|
|
37
|
-
super().__setattr__(name, value)
|
|
38
|
-
|
|
39
21
|
|
|
40
22
|
app_config = _AppConfig()
|
shinestacker/app/help_menu.py
CHANGED
shinestacker/config/config.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# pylint: disable=C0114, C0115, C0116, C0103, R0903, W0718, W0104, W0201, E0602
|
|
2
|
-
class
|
|
2
|
+
class _ConfigBase:
|
|
3
3
|
_initialized = False
|
|
4
4
|
_instance = None
|
|
5
5
|
|
|
@@ -9,16 +9,6 @@ class _Config:
|
|
|
9
9
|
cls._instance._init_defaults()
|
|
10
10
|
return cls._instance
|
|
11
11
|
|
|
12
|
-
def _init_defaults(self):
|
|
13
|
-
self._DISABLE_TQDM = False
|
|
14
|
-
self._COMBINED_APP = False
|
|
15
|
-
self._DONT_USE_NATIVE_MENU = True
|
|
16
|
-
try:
|
|
17
|
-
__IPYTHON__ # noqa
|
|
18
|
-
self._JUPYTER_NOTEBOOK = True
|
|
19
|
-
except Exception:
|
|
20
|
-
self._JUPYTER_NOTEBOOK = False
|
|
21
|
-
|
|
22
12
|
def init(self, **kwargs):
|
|
23
13
|
if self._initialized:
|
|
24
14
|
raise RuntimeError("Config already initialized")
|
|
@@ -29,6 +19,26 @@ class _Config:
|
|
|
29
19
|
raise AttributeError(f"Invalid config key: {k}")
|
|
30
20
|
self._initialized = True
|
|
31
21
|
|
|
22
|
+
def __setattr__(self, name, value):
|
|
23
|
+
if self._initialized and name.startswith('_'):
|
|
24
|
+
raise AttributeError("Can't change config after initialization")
|
|
25
|
+
super().__setattr__(name, value)
|
|
26
|
+
|
|
27
|
+
class _Config(_ConfigBase):
|
|
28
|
+
|
|
29
|
+
def __new__(cls):
|
|
30
|
+
return _ConfigBase.__new__(cls)
|
|
31
|
+
|
|
32
|
+
def _init_defaults(self):
|
|
33
|
+
self._DISABLE_TQDM = False
|
|
34
|
+
self._COMBINED_APP = False
|
|
35
|
+
self._DONT_USE_NATIVE_MENU = True
|
|
36
|
+
try:
|
|
37
|
+
__IPYTHON__ # noqa
|
|
38
|
+
self._JUPYTER_NOTEBOOK = True
|
|
39
|
+
except Exception:
|
|
40
|
+
self._JUPYTER_NOTEBOOK = False
|
|
41
|
+
|
|
32
42
|
@property
|
|
33
43
|
def DISABLE_TQDM(self):
|
|
34
44
|
return self._DISABLE_TQDM
|
|
@@ -45,10 +55,5 @@ class _Config:
|
|
|
45
55
|
def COMBINED_APP(self):
|
|
46
56
|
return self._COMBINED_APP
|
|
47
57
|
|
|
48
|
-
def __setattr__(self, name, value):
|
|
49
|
-
if self._initialized and name.startswith('_'):
|
|
50
|
-
raise AttributeError("Can't change config after initialization")
|
|
51
|
-
super().__setattr__(name, value)
|
|
52
|
-
|
|
53
58
|
|
|
54
59
|
config = _Config()
|
|
@@ -10,8 +10,10 @@ from PySide6.QtWidgets import (QWidget, QPushButton, QHBoxLayout, QFileDialog, Q
|
|
|
10
10
|
QAbstractItemView, QListView)
|
|
11
11
|
from PySide6.QtCore import Qt, QTimer
|
|
12
12
|
from .. config.constants import constants
|
|
13
|
-
from .project_model import ActionConfig
|
|
14
13
|
from .. algorithms.align import validate_align_config
|
|
14
|
+
from .project_model import ActionConfig
|
|
15
|
+
from .select_path_widget import (create_select_file_paths_widget, create_layout_widget_no_margins,
|
|
16
|
+
create_layout_widget_and_connect)
|
|
15
17
|
|
|
16
18
|
FIELD_TEXT = 'text'
|
|
17
19
|
FIELD_ABS_PATH = 'abs_path'
|
|
@@ -200,25 +202,11 @@ class FieldBuilder:
|
|
|
200
202
|
return edit
|
|
201
203
|
|
|
202
204
|
def create_abs_path_field(self, tag, **kwargs):
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
def browse():
|
|
209
|
-
path = QFileDialog.getExistingDirectory(None, f"Select {tag.replace('_', ' ')}")
|
|
210
|
-
if path:
|
|
211
|
-
edit.setText(path)
|
|
212
|
-
button.clicked.connect(browse)
|
|
213
|
-
button.setAutoDefault(False)
|
|
214
|
-
layout = QHBoxLayout()
|
|
215
|
-
layout.addWidget(edit)
|
|
216
|
-
layout.addWidget(button)
|
|
217
|
-
layout.setContentsMargins(0, 0, 0, 0)
|
|
218
|
-
container = QWidget()
|
|
219
|
-
container.setLayout(layout)
|
|
220
|
-
container.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
|
|
221
|
-
return container
|
|
205
|
+
return create_select_file_paths_widget(
|
|
206
|
+
self.action.params.get(tag, ''),
|
|
207
|
+
kwargs.get('placeholder', ''),
|
|
208
|
+
tag.replace('_', ' ')
|
|
209
|
+
)
|
|
222
210
|
|
|
223
211
|
def create_rel_path_field(self, tag, **kwargs):
|
|
224
212
|
value = self.action.params.get(tag, kwargs.get('default', ''))
|
|
@@ -326,17 +314,8 @@ class FieldBuilder:
|
|
|
326
314
|
except ValueError as e:
|
|
327
315
|
traceback.print_tb(e.__traceback__)
|
|
328
316
|
QMessageBox.warning(None, "Error", "Could not compute relative path")
|
|
317
|
+
return create_layout_widget_and_connect(button, edit, browse)
|
|
329
318
|
|
|
330
|
-
button.clicked.connect(browse)
|
|
331
|
-
button.setAutoDefault(False)
|
|
332
|
-
layout = QHBoxLayout()
|
|
333
|
-
layout.addWidget(edit)
|
|
334
|
-
layout.addWidget(button)
|
|
335
|
-
layout.setContentsMargins(0, 0, 0, 0)
|
|
336
|
-
container = QWidget()
|
|
337
|
-
container.setLayout(layout)
|
|
338
|
-
container.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
|
|
339
|
-
return container
|
|
340
319
|
|
|
341
320
|
def create_float_field(self, tag, default=0.0, min_val=0.0, max_val=1.0,
|
|
342
321
|
step=0.1, decimals=2):
|
|
@@ -369,11 +348,7 @@ class FieldBuilder:
|
|
|
369
348
|
layout.addWidget(label)
|
|
370
349
|
layout.addWidget(spin)
|
|
371
350
|
layout.setStretch(layout.count() - 1, 1)
|
|
372
|
-
layout
|
|
373
|
-
container = QWidget()
|
|
374
|
-
container.setLayout(layout)
|
|
375
|
-
container.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
|
|
376
|
-
return container
|
|
351
|
+
return create_layout_widget_no_margins(layout)
|
|
377
352
|
|
|
378
353
|
def create_combo_field(self, tag, options=None, default=None, **kwargs):
|
|
379
354
|
options = options or []
|
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
# pylint: disable=C0114, C0115, C0116, E0611, R0914, R0912, R0915, W0718
|
|
2
2
|
import os.path
|
|
3
3
|
import os
|
|
4
|
-
import traceback
|
|
4
|
+
# import traceback
|
|
5
5
|
import json
|
|
6
6
|
import jsonpickle
|
|
7
7
|
from PySide6.QtWidgets import QMessageBox, QFileDialog, QDialog
|
|
8
8
|
from .. core.core_utils import get_app_base_path
|
|
9
9
|
from .. config.constants import constants
|
|
10
10
|
from .project_model import ActionConfig
|
|
11
|
-
from .action_config import ActionConfigDialog
|
|
12
11
|
from .project_editor import ProjectEditor
|
|
13
12
|
from .new_project import NewProjectDialog
|
|
14
13
|
from .project_model import Project
|
|
@@ -17,15 +16,13 @@ from .project_model import Project
|
|
|
17
16
|
class ActionsWindow(ProjectEditor):
|
|
18
17
|
def __init__(self):
|
|
19
18
|
super().__init__()
|
|
20
|
-
self._current_file = None
|
|
21
|
-
self._current_file_wd = ''
|
|
22
|
-
self._modified_project = False
|
|
23
19
|
self.update_title()
|
|
24
20
|
|
|
25
21
|
def update_title(self):
|
|
26
22
|
title = constants.APP_TITLE
|
|
27
|
-
|
|
28
|
-
|
|
23
|
+
file_name = self.current_file_name()
|
|
24
|
+
if file_name:
|
|
25
|
+
title += f" - {file_name}"
|
|
29
26
|
if self._modified_project:
|
|
30
27
|
title += " *"
|
|
31
28
|
self.window().setWindowTitle(title)
|
|
@@ -38,7 +35,7 @@ class ActionsWindow(ProjectEditor):
|
|
|
38
35
|
def close_project(self):
|
|
39
36
|
if self._check_unsaved_changes():
|
|
40
37
|
self.set_project(Project())
|
|
41
|
-
self.
|
|
38
|
+
self.set_current_file_path('')
|
|
42
39
|
self.update_title()
|
|
43
40
|
self.job_list.clear()
|
|
44
41
|
self.action_list.clear()
|
|
@@ -48,17 +45,17 @@ class ActionsWindow(ProjectEditor):
|
|
|
48
45
|
if not self._check_unsaved_changes():
|
|
49
46
|
return
|
|
50
47
|
os.chdir(get_app_base_path())
|
|
51
|
-
self.
|
|
48
|
+
self.set_current_file_path('')
|
|
52
49
|
self._modified_project = False
|
|
53
50
|
self.update_title()
|
|
54
51
|
self.job_list.clear()
|
|
55
52
|
self.action_list.clear()
|
|
53
|
+
self.set_project(Project())
|
|
56
54
|
dialog = NewProjectDialog(self)
|
|
57
55
|
if dialog.exec() == QDialog.Accepted:
|
|
58
56
|
input_folder = dialog.get_input_folder().split('/')
|
|
59
57
|
working_path = '/'.join(input_folder[:-1])
|
|
60
58
|
input_path = input_folder[-1]
|
|
61
|
-
project = Project()
|
|
62
59
|
if dialog.get_noise_detection():
|
|
63
60
|
job_noise = ActionConfig(constants.ACTION_JOB,
|
|
64
61
|
{'name': 'detect-noise', 'working_path': working_path,
|
|
@@ -66,7 +63,7 @@ class ActionsWindow(ProjectEditor):
|
|
|
66
63
|
noise_detection = ActionConfig(constants.ACTION_NOISEDETECTION,
|
|
67
64
|
{'name': 'detect-noise'})
|
|
68
65
|
job_noise.add_sub_action(noise_detection)
|
|
69
|
-
project.jobs.append(job_noise)
|
|
66
|
+
self.project.jobs.append(job_noise)
|
|
70
67
|
job = ActionConfig(constants.ACTION_JOB,
|
|
71
68
|
{'name': 'focus-stack', 'working_path': working_path,
|
|
72
69
|
'input_path': input_path})
|
|
@@ -115,8 +112,7 @@ class ActionsWindow(ProjectEditor):
|
|
|
115
112
|
{'name': 'multi-layer',
|
|
116
113
|
'input_path': ','.join(input_path)})
|
|
117
114
|
job.add_sub_action(multi_layer)
|
|
118
|
-
project.jobs.append(job)
|
|
119
|
-
self.set_project(project)
|
|
115
|
+
self.project.jobs.append(job)
|
|
120
116
|
self._modified_project = True
|
|
121
117
|
self.refresh_ui(0, -1)
|
|
122
118
|
|
|
@@ -128,17 +124,9 @@ class ActionsWindow(ProjectEditor):
|
|
|
128
124
|
self, "Open Project", "", "Project Files (*.fsp);;All Files (*)")
|
|
129
125
|
if file_path:
|
|
130
126
|
try:
|
|
131
|
-
self.
|
|
132
|
-
self.
|
|
133
|
-
else os.path.dirname(file_path)
|
|
134
|
-
if not os.path.isabs(self._current_file_wd):
|
|
135
|
-
self._current_file_wd = os.path.abspath(self._current_file_wd)
|
|
136
|
-
self._current_file = os.path.basename(self._current_file)
|
|
137
|
-
with open(file_path, 'r', encoding="utf-8") as file:
|
|
127
|
+
self.set_current_file_path(file_path)
|
|
128
|
+
with open(self.current_file_path(), 'r', encoding="utf-8") as file:
|
|
138
129
|
json_obj = json.load(file)
|
|
139
|
-
pp = file_path.split('/')
|
|
140
|
-
if len(pp) > 1:
|
|
141
|
-
os.chdir('/'.join(pp[:-1]))
|
|
142
130
|
project = Project.from_dict(json_obj['project'])
|
|
143
131
|
if project is None:
|
|
144
132
|
raise RuntimeError(f"Project from file {file_path} produced a null project.")
|
|
@@ -149,7 +137,7 @@ class ActionsWindow(ProjectEditor):
|
|
|
149
137
|
if self.job_list.count() > 0:
|
|
150
138
|
self.job_list.setCurrentRow(0)
|
|
151
139
|
except Exception as e:
|
|
152
|
-
traceback.print_tb(e.__traceback__)
|
|
140
|
+
# traceback.print_tb(e.__traceback__)
|
|
153
141
|
QMessageBox.critical(self, "Error", f"Cannot open file {file_path}:\n{str(e)}")
|
|
154
142
|
if len(self.project.jobs) > 0:
|
|
155
143
|
self.job_list.setCurrentRow(0)
|
|
@@ -177,12 +165,10 @@ class ActionsWindow(ProjectEditor):
|
|
|
177
165
|
Please, select a valid working path.''')
|
|
178
166
|
self.edit_action(action)
|
|
179
167
|
|
|
180
|
-
def current_file_name(self):
|
|
181
|
-
return os.path.basename(self._current_file) if self._current_file else ''
|
|
182
|
-
|
|
183
168
|
def save_project(self):
|
|
184
|
-
|
|
185
|
-
|
|
169
|
+
path = self.current_file_path()
|
|
170
|
+
if path:
|
|
171
|
+
self.do_save(path)
|
|
186
172
|
else:
|
|
187
173
|
self.save_project_as()
|
|
188
174
|
|
|
@@ -192,11 +178,11 @@ class ActionsWindow(ProjectEditor):
|
|
|
192
178
|
if file_path:
|
|
193
179
|
if not file_path.endswith('.fsp'):
|
|
194
180
|
file_path += '.fsp'
|
|
195
|
-
self._current_file_wd = ''
|
|
196
181
|
self.do_save(file_path)
|
|
197
|
-
self.
|
|
182
|
+
self.set_current_file_path(file_path)
|
|
198
183
|
self._modified_project = False
|
|
199
184
|
self.update_title()
|
|
185
|
+
os.chdir(os.path.dirname(file_path))
|
|
200
186
|
|
|
201
187
|
def do_save(self, file_path):
|
|
202
188
|
try:
|
|
@@ -204,9 +190,7 @@ class ActionsWindow(ProjectEditor):
|
|
|
204
190
|
'project': self.project.to_dict(),
|
|
205
191
|
'version': 1
|
|
206
192
|
})
|
|
207
|
-
|
|
208
|
-
if self._current_file_wd != '' else file_path
|
|
209
|
-
with open(path, 'w', encoding="utf-8") as f:
|
|
193
|
+
with open(file_path, 'w', encoding="utf-8") as f:
|
|
210
194
|
f.write(json_obj)
|
|
211
195
|
self._modified_project = False
|
|
212
196
|
except Exception as e:
|
|
@@ -229,7 +213,7 @@ class ActionsWindow(ProjectEditor):
|
|
|
229
213
|
index = self.job_list.row(item)
|
|
230
214
|
if 0 <= index < len(self.project.jobs):
|
|
231
215
|
job = self.project.jobs[index]
|
|
232
|
-
dialog =
|
|
216
|
+
dialog = self.action_config_dialog(job)
|
|
233
217
|
if dialog.exec() == QDialog.Accepted:
|
|
234
218
|
current_row = self.job_list.currentRow()
|
|
235
219
|
if current_row >= 0:
|
|
@@ -241,28 +225,12 @@ class ActionsWindow(ProjectEditor):
|
|
|
241
225
|
if 0 <= job_index < len(self.project.jobs):
|
|
242
226
|
job = self.project.jobs[job_index]
|
|
243
227
|
action_index = self.action_list.row(item)
|
|
244
|
-
|
|
245
|
-
current_action = None
|
|
246
|
-
is_sub_action = False
|
|
247
|
-
for action in job.sub_actions:
|
|
248
|
-
action_counter += 1
|
|
249
|
-
if action_counter == action_index:
|
|
250
|
-
current_action = action
|
|
251
|
-
break
|
|
252
|
-
if len(action.type_name) > 0:
|
|
253
|
-
for sub_action in action.sub_actions:
|
|
254
|
-
action_counter += 1
|
|
255
|
-
if action_counter == action_index:
|
|
256
|
-
current_action = sub_action
|
|
257
|
-
is_sub_action = True
|
|
258
|
-
break
|
|
259
|
-
if current_action:
|
|
260
|
-
break
|
|
228
|
+
current_action, is_sub_action = self.get_current_action_at(job, action_index)
|
|
261
229
|
if current_action:
|
|
262
230
|
if not is_sub_action:
|
|
263
231
|
self.set_enabled_sub_actions_gui(
|
|
264
232
|
current_action.type_name == constants.ACTION_COMBO)
|
|
265
|
-
dialog =
|
|
233
|
+
dialog = self.action_config_dialog(current_action)
|
|
266
234
|
if dialog.exec() == QDialog.Accepted:
|
|
267
235
|
self.on_job_selected(job_index)
|
|
268
236
|
self.refresh_ui()
|
|
@@ -284,7 +252,7 @@ class ActionsWindow(ProjectEditor):
|
|
|
284
252
|
self.edit_action(current_action)
|
|
285
253
|
|
|
286
254
|
def edit_action(self, action):
|
|
287
|
-
dialog =
|
|
255
|
+
dialog = self.action_config_dialog(action)
|
|
288
256
|
if dialog.exec() == QDialog.Accepted:
|
|
289
257
|
self.on_job_selected(self.job_list.currentRow())
|
|
290
258
|
self.mark_as_modified()
|
shinestacker/gui/new_project.py
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
# pylint: disable=C0114, C0115, C0116, E0611, R0915, R0902
|
|
2
2
|
import os
|
|
3
|
-
from PySide6.QtWidgets import (
|
|
4
|
-
QDialog,
|
|
5
|
-
QSpinBox, QMessageBox)
|
|
3
|
+
from PySide6.QtWidgets import (QFormLayout, QHBoxLayout, QPushButton,
|
|
4
|
+
QDialog, QLabel, QCheckBox, QSpinBox, QMessageBox)
|
|
6
5
|
from PySide6.QtGui import QIcon
|
|
7
6
|
from PySide6.QtCore import Qt
|
|
8
7
|
from .. config.gui_constants import gui_constants
|
|
9
8
|
from .. config.constants import constants
|
|
10
9
|
from .. algorithms.stack import get_bunches
|
|
10
|
+
from .select_path_widget import create_select_file_paths_widget
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
class NewProjectDialog(QDialog):
|
|
@@ -50,25 +50,8 @@ class NewProjectDialog(QDialog):
|
|
|
50
50
|
spacer = QLabel("")
|
|
51
51
|
spacer.setFixedHeight(10)
|
|
52
52
|
self.layout.addRow(spacer)
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
self.input_folder.textChanged.connect(self.update_bunches_label)
|
|
56
|
-
button = QPushButton("Browse...")
|
|
57
|
-
|
|
58
|
-
def browse():
|
|
59
|
-
path = QFileDialog.getExistingDirectory(None, "Select input files folder")
|
|
60
|
-
if path:
|
|
61
|
-
self.input_folder.setText(path)
|
|
62
|
-
|
|
63
|
-
button.clicked.connect(browse)
|
|
64
|
-
button.setAutoDefault(False)
|
|
65
|
-
layout = QHBoxLayout()
|
|
66
|
-
layout.addWidget(self.input_folder)
|
|
67
|
-
layout.addWidget(button)
|
|
68
|
-
layout.setContentsMargins(0, 0, 0, 0)
|
|
69
|
-
container = QWidget()
|
|
70
|
-
container.setLayout(layout)
|
|
71
|
-
container.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
|
|
53
|
+
|
|
54
|
+
container = create_select_file_paths_widget('', 'input files folder', 'input files folder')
|
|
72
55
|
|
|
73
56
|
self.noise_detection = QCheckBox()
|
|
74
57
|
self.noise_detection.setChecked(gui_constants.NEW_PROJECT_NOISE_DETECTION)
|
|
@@ -89,6 +89,27 @@ class ProjectEditor(QMainWindow):
|
|
|
89
89
|
self.expert_options = False
|
|
90
90
|
self.script_dir = os.path.dirname(__file__)
|
|
91
91
|
self.dialog = None
|
|
92
|
+
self._current_file_path = ''
|
|
93
|
+
self._modified_project = False
|
|
94
|
+
|
|
95
|
+
def current_file_path(self):
|
|
96
|
+
return self._current_file_path
|
|
97
|
+
|
|
98
|
+
def current_file_directory(self):
|
|
99
|
+
if os.path.isdir(self._current_file_path):
|
|
100
|
+
return self._current_file_path
|
|
101
|
+
return os.path.dirname(self._current_file_path)
|
|
102
|
+
|
|
103
|
+
def current_file_name(self):
|
|
104
|
+
if os.path.isfile(self._current_file_path):
|
|
105
|
+
return os.path.basename(self._current_file_path)
|
|
106
|
+
return ''
|
|
107
|
+
|
|
108
|
+
def set_current_file_path(self, path):
|
|
109
|
+
if path and not os.path.exists(path):
|
|
110
|
+
raise RuntimeError(f"Path: {path} does not exist.")
|
|
111
|
+
self._current_file_path = os.path.abspath(path)
|
|
112
|
+
os.chdir(self.current_file_directory())
|
|
92
113
|
|
|
93
114
|
def set_project(self, project):
|
|
94
115
|
self.project = project
|
|
@@ -300,9 +321,12 @@ class ProjectEditor(QMainWindow):
|
|
|
300
321
|
self.delete_element_action.setEnabled(True)
|
|
301
322
|
return element
|
|
302
323
|
|
|
324
|
+
def action_config_dialog(self, action):
|
|
325
|
+
return ActionConfigDialog(action, self.current_file_directory(), self)
|
|
326
|
+
|
|
303
327
|
def add_job(self):
|
|
304
328
|
job_action = ActionConfig("Job")
|
|
305
|
-
self.dialog =
|
|
329
|
+
self.dialog = self.action_config_dialog(job_action)
|
|
306
330
|
if self.dialog.exec() == QDialog.Accepted:
|
|
307
331
|
self.mark_as_modified()
|
|
308
332
|
self.project.jobs.append(job_action)
|
|
@@ -323,7 +347,7 @@ class ProjectEditor(QMainWindow):
|
|
|
323
347
|
type_name = self.action_selector.currentText()
|
|
324
348
|
action = ActionConfig(type_name)
|
|
325
349
|
action.parent = self.get_current_job()
|
|
326
|
-
self.dialog =
|
|
350
|
+
self.dialog = self.action_config_dialog(action)
|
|
327
351
|
if self.dialog.exec() == QDialog.Accepted:
|
|
328
352
|
self.mark_as_modified()
|
|
329
353
|
self.project.jobs[current_index].add_sub_action(action)
|
|
@@ -365,7 +389,7 @@ class ProjectEditor(QMainWindow):
|
|
|
365
389
|
if type_name is False:
|
|
366
390
|
type_name = self.sub_action_selector.currentText()
|
|
367
391
|
sub_action = ActionConfig(type_name)
|
|
368
|
-
self.dialog =
|
|
392
|
+
self.dialog = self.action_config_dialog(sub_action)
|
|
369
393
|
if self.dialog.exec() == QDialog.Accepted:
|
|
370
394
|
self.mark_as_modified()
|
|
371
395
|
action.add_sub_action(sub_action)
|
|
@@ -493,6 +517,27 @@ class ProjectEditor(QMainWindow):
|
|
|
493
517
|
self.add_list_item(self.action_list, sub_action, True)
|
|
494
518
|
self.update_delete_action_state()
|
|
495
519
|
|
|
520
|
+
def get_current_action_at(self, job, action_index):
|
|
521
|
+
action_counter = -1
|
|
522
|
+
current_action = None
|
|
523
|
+
is_sub_action = False
|
|
524
|
+
for action in job.sub_actions:
|
|
525
|
+
action_counter += 1
|
|
526
|
+
if action_counter == action_index:
|
|
527
|
+
current_action = action
|
|
528
|
+
break
|
|
529
|
+
if len(action.sub_actions) > 0:
|
|
530
|
+
for sub_action in action.sub_actions:
|
|
531
|
+
action_counter += 1
|
|
532
|
+
if action_counter == action_index:
|
|
533
|
+
current_action = sub_action
|
|
534
|
+
is_sub_action = True
|
|
535
|
+
break
|
|
536
|
+
if current_action:
|
|
537
|
+
break
|
|
538
|
+
|
|
539
|
+
return current_action, is_sub_action
|
|
540
|
+
|
|
496
541
|
def update_delete_action_state(self):
|
|
497
542
|
has_job_selected = len(self.job_list.selectedItems()) > 0
|
|
498
543
|
has_action_selected = len(self.action_list.selectedItems()) > 0
|
|
@@ -504,23 +549,7 @@ class ProjectEditor(QMainWindow):
|
|
|
504
549
|
action_index = self.action_list.currentRow()
|
|
505
550
|
if job_index >= 0:
|
|
506
551
|
job = self.project.jobs[job_index]
|
|
507
|
-
|
|
508
|
-
current_action = None
|
|
509
|
-
is_sub_action = False
|
|
510
|
-
for action in job.sub_actions:
|
|
511
|
-
action_counter += 1
|
|
512
|
-
if action_counter == action_index:
|
|
513
|
-
current_action = action
|
|
514
|
-
break
|
|
515
|
-
if len(action.sub_actions) > 0:
|
|
516
|
-
for sub_action in action.sub_actions:
|
|
517
|
-
action_counter += 1
|
|
518
|
-
if action_counter == action_index:
|
|
519
|
-
current_action = sub_action
|
|
520
|
-
is_sub_action = True
|
|
521
|
-
break
|
|
522
|
-
if current_action:
|
|
523
|
-
break
|
|
552
|
+
current_action, is_sub_action = self.get_current_action_at(job, action_index)
|
|
524
553
|
enable_sub_actions = current_action is not None and \
|
|
525
554
|
not is_sub_action and current_action.type_name == constants.ACTION_COMBO
|
|
526
555
|
self.set_enabled_sub_actions_gui(enable_sub_actions)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# pylint: disable=C0114, C0116, E0611
|
|
2
|
+
from PySide6.QtWidgets import QWidget, QPushButton, QHBoxLayout, QFileDialog, QSizePolicy, QLineEdit
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def create_layout_widget_no_margins(layout):
|
|
6
|
+
layout.setContentsMargins(0, 0, 0, 0)
|
|
7
|
+
container = QWidget()
|
|
8
|
+
container.setLayout(layout)
|
|
9
|
+
container.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
|
|
10
|
+
return container
|
|
11
|
+
|
|
12
|
+
def create_layout_widget_and_connect(button, edit, browse):
|
|
13
|
+
button.clicked.connect(browse)
|
|
14
|
+
button.setAutoDefault(False)
|
|
15
|
+
layout = QHBoxLayout()
|
|
16
|
+
layout.addWidget(edit)
|
|
17
|
+
layout.addWidget(button)
|
|
18
|
+
return create_layout_widget_no_margins(layout)
|
|
19
|
+
|
|
20
|
+
def create_select_file_paths_widget(value, placeholder, tag):
|
|
21
|
+
edit = QLineEdit(value)
|
|
22
|
+
edit.setPlaceholderText(placeholder)
|
|
23
|
+
button = QPushButton("Browse...")
|
|
24
|
+
|
|
25
|
+
def browse():
|
|
26
|
+
path = QFileDialog.getExistingDirectory(None, f"Select {tag}")
|
|
27
|
+
if path:
|
|
28
|
+
edit.setText(path)
|
|
29
|
+
|
|
30
|
+
return create_layout_widget_and_connect(button, edit, browse)
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import traceback
|
|
3
3
|
from abc import ABC, abstractmethod
|
|
4
4
|
import numpy as np
|
|
5
|
-
from PySide6.QtWidgets import QDialog, QVBoxLayout
|
|
5
|
+
from PySide6.QtWidgets import QDialog, QVBoxLayout, QCheckBox, QDialogButtonBox
|
|
6
6
|
from PySide6.QtCore import Signal, QThread, QTimer
|
|
7
7
|
|
|
8
8
|
|
|
@@ -98,6 +98,17 @@ class BaseFilter(ABC):
|
|
|
98
98
|
else:
|
|
99
99
|
restore_original()
|
|
100
100
|
|
|
101
|
+
def create_base_widgets(self, layout, buttons, preview_latency):
|
|
102
|
+
preview_check = QCheckBox("Preview")
|
|
103
|
+
preview_check.setChecked(True)
|
|
104
|
+
layout.addWidget(preview_check)
|
|
105
|
+
button_box = QDialogButtonBox(buttons)
|
|
106
|
+
layout.addWidget(button_box)
|
|
107
|
+
preview_timer = QTimer()
|
|
108
|
+
preview_timer.setSingleShot(True)
|
|
109
|
+
preview_timer.setInterval(preview_latency)
|
|
110
|
+
return preview_check, preview_timer, button_box
|
|
111
|
+
|
|
101
112
|
class PreviewWorker(QThread):
|
|
102
113
|
finished = Signal(np.ndarray, int)
|
|
103
114
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# pylint: disable=C0114, C0115, C0116, E0611, W0221
|
|
2
|
-
from PySide6.QtWidgets import QHBoxLayout, QLabel, QSlider,
|
|
3
|
-
from PySide6.QtCore import Qt
|
|
2
|
+
from PySide6.QtWidgets import QHBoxLayout, QLabel, QSlider, QDialogButtonBox
|
|
3
|
+
from PySide6.QtCore import Qt
|
|
4
4
|
from .base_filter import BaseFilter
|
|
5
5
|
from .. algorithms.denoise import denoise
|
|
6
6
|
|
|
@@ -24,14 +24,8 @@ class DenoiseFilter(BaseFilter):
|
|
|
24
24
|
value_label = QLabel(f"{self.max_value:.2f}")
|
|
25
25
|
slider_layout.addWidget(value_label)
|
|
26
26
|
layout.addLayout(slider_layout)
|
|
27
|
-
preview_check =
|
|
28
|
-
|
|
29
|
-
layout.addWidget(preview_check)
|
|
30
|
-
button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
|
|
31
|
-
layout.addWidget(button_box)
|
|
32
|
-
preview_timer = QTimer()
|
|
33
|
-
preview_timer.setSingleShot(True)
|
|
34
|
-
preview_timer.setInterval(200)
|
|
27
|
+
preview_check, preview_timer, button_box = self.create_base_widgets(
|
|
28
|
+
layout, QDialogButtonBox.Ok | QDialogButtonBox.Cancel, 200)
|
|
35
29
|
|
|
36
30
|
def do_preview_delayed():
|
|
37
31
|
preview_timer.start()
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
# pylint: disable=C0114, C0115, C0116, E0611
|
|
2
|
-
import os
|
|
3
2
|
from PIL.TiffImagePlugin import IFDRational
|
|
4
3
|
from PySide6.QtWidgets import QFormLayout, QHBoxLayout, QPushButton, QDialog, QLabel
|
|
5
|
-
from PySide6.QtGui import QIcon
|
|
6
4
|
from PySide6.QtCore import Qt
|
|
7
5
|
from .. algorithms.exif import exif_dict
|
|
6
|
+
from .icon_container import icon_container
|
|
8
7
|
|
|
9
8
|
|
|
10
9
|
class ExifData(QDialog):
|
|
@@ -32,13 +31,8 @@ class ExifData(QDialog):
|
|
|
32
31
|
self.layout.addRow(label)
|
|
33
32
|
|
|
34
33
|
def create_form(self):
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
icon_pixmap = app_icon.pixmap(128, 128)
|
|
38
|
-
icon_label = QLabel()
|
|
39
|
-
icon_label.setPixmap(icon_pixmap)
|
|
40
|
-
icon_label.setAlignment(Qt.AlignCenter)
|
|
41
|
-
self.layout.addRow(icon_label)
|
|
34
|
+
self.layout.addRow(icon_container())
|
|
35
|
+
|
|
42
36
|
spacer = QLabel("")
|
|
43
37
|
spacer.setFixedHeight(10)
|
|
44
38
|
self.layout.addRow(spacer)
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# pylint: disable=C0114, C0115, C0116, E0611
|
|
2
|
+
import os
|
|
3
|
+
from PySide6.QtWidgets import QHBoxLayout, QLabel, QWidget
|
|
4
|
+
from PySide6.QtGui import QIcon
|
|
5
|
+
from PySide6.QtCore import Qt
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def icon_container():
|
|
9
|
+
icon_path = f"{os.path.dirname(__file__)}/../gui/ico/shinestacker.png"
|
|
10
|
+
app_icon = QIcon(icon_path)
|
|
11
|
+
pixmap = app_icon.pixmap(128, 128)
|
|
12
|
+
label = QLabel()
|
|
13
|
+
label.setPixmap(pixmap)
|
|
14
|
+
label.setAlignment(Qt.AlignCenter)
|
|
15
|
+
container = QWidget()
|
|
16
|
+
layout = QHBoxLayout(container)
|
|
17
|
+
layout.addWidget(label)
|
|
18
|
+
layout.setAlignment(Qt.AlignCenter)
|
|
19
|
+
return container
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
# pylint: disable=C0114, C0115, C0116, E0611
|
|
2
|
-
import os
|
|
3
2
|
from PySide6.QtWidgets import (QFormLayout, QHBoxLayout, QPushButton, QDialog,
|
|
4
3
|
QLabel, QVBoxLayout, QWidget)
|
|
5
|
-
from PySide6.QtGui import QIcon
|
|
6
4
|
from PySide6.QtCore import Qt
|
|
5
|
+
from .icon_container import icon_container
|
|
7
6
|
|
|
8
7
|
|
|
9
8
|
class ShortcutsHelp(QDialog):
|
|
@@ -44,17 +43,7 @@ class ShortcutsHelp(QDialog):
|
|
|
44
43
|
layout.addRow(label)
|
|
45
44
|
|
|
46
45
|
def create_form(self, left_layout, right_layout):
|
|
47
|
-
|
|
48
|
-
app_icon = QIcon(icon_path)
|
|
49
|
-
icon_pixmap = app_icon.pixmap(128, 128)
|
|
50
|
-
icon_label = QLabel()
|
|
51
|
-
icon_label.setPixmap(icon_pixmap)
|
|
52
|
-
icon_label.setAlignment(Qt.AlignCenter)
|
|
53
|
-
icon_container = QWidget()
|
|
54
|
-
icon_container_layout = QHBoxLayout(icon_container)
|
|
55
|
-
icon_container_layout.addWidget(icon_label)
|
|
56
|
-
icon_container_layout.setAlignment(Qt.AlignCenter)
|
|
57
|
-
self.layout.insertWidget(0, icon_container)
|
|
46
|
+
self.layout.insertWidget(0, icon_container())
|
|
58
47
|
|
|
59
48
|
shortcuts = {
|
|
60
49
|
"M": "show master layer",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# pylint: disable=C0114, C0115, C0116, E0611, W0221, R0902, R0914
|
|
2
|
-
from PySide6.QtWidgets import QHBoxLayout, QLabel, QSlider,
|
|
2
|
+
from PySide6.QtWidgets import QHBoxLayout, QLabel, QSlider, QDialogButtonBox
|
|
3
3
|
from PySide6.QtCore import Qt, QTimer
|
|
4
4
|
from .. algorithms.sharpen import unsharp_mask
|
|
5
5
|
from .base_filter import BaseFilter
|
|
@@ -46,15 +46,8 @@ class UnsharpMaskFilter(BaseFilter):
|
|
|
46
46
|
elif name == "Threshold":
|
|
47
47
|
self.threshold_slider = slider
|
|
48
48
|
value_labels[name] = value_label
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
preview_check.setChecked(True)
|
|
52
|
-
layout.addWidget(preview_check)
|
|
53
|
-
button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
|
|
54
|
-
layout.addWidget(button_box)
|
|
55
|
-
preview_timer = QTimer()
|
|
56
|
-
preview_timer.setSingleShot(True)
|
|
57
|
-
preview_timer.setInterval(200)
|
|
49
|
+
preview_check, preview_timer, button_box = self.create_base_widgets(
|
|
50
|
+
layout, QDialogButtonBox.Ok | QDialogButtonBox.Cancel, 200)
|
|
58
51
|
|
|
59
52
|
def update_value(name, value, max_val, fmt):
|
|
60
53
|
float_value = max_val * value / self.max_range
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# pylint: disable=C0114, C0115, C0116, E0611, W0221, R0913, R0914, R0917
|
|
2
2
|
from PySide6.QtWidgets import (QHBoxLayout, QPushButton, QFrame, QVBoxLayout, QLabel, QDialog,
|
|
3
|
-
QApplication, QSlider,
|
|
3
|
+
QApplication, QSlider, QDialogButtonBox)
|
|
4
4
|
from PySide6.QtCore import Qt, QTimer
|
|
5
5
|
from PySide6.QtGui import QCursor
|
|
6
6
|
from .. algorithms.white_balance import white_balance_from_rgb
|
|
@@ -47,18 +47,10 @@ class WhiteBalanceFilter(BaseFilter):
|
|
|
47
47
|
layout.addLayout(row_layout)
|
|
48
48
|
pick_button = QPushButton("Pick Color")
|
|
49
49
|
layout.addWidget(pick_button)
|
|
50
|
-
preview_check =
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
QDialogButtonBox.Ok |
|
|
55
|
-
QDialogButtonBox.Reset |
|
|
56
|
-
QDialogButtonBox.Cancel
|
|
57
|
-
)
|
|
58
|
-
layout.addWidget(button_box)
|
|
59
|
-
self.preview_timer = QTimer()
|
|
60
|
-
self.preview_timer.setSingleShot(True)
|
|
61
|
-
self.preview_timer.setInterval(200)
|
|
50
|
+
preview_check, self.preview_timer, button_box = self.create_base_widgets(
|
|
51
|
+
layout,
|
|
52
|
+
QDialogButtonBox.Ok | QDialogButtonBox.Reset | QDialogButtonBox.Cancel,
|
|
53
|
+
200)
|
|
62
54
|
for slider in self.sliders.values():
|
|
63
55
|
slider.valueChanged.connect(self.on_slider_change)
|
|
64
56
|
self.preview_timer.timeout.connect(do_preview)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: shinestacker
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.6
|
|
4
4
|
Summary: ShineStacker
|
|
5
5
|
Author-email: Luca Lista <luka.lista@gmail.com>
|
|
6
6
|
License-Expression: LGPL-3.0
|
|
@@ -37,25 +37,22 @@ Dynamic: license-file
|
|
|
37
37
|
[](https://pypi.org/project/shinestacker/)
|
|
38
38
|
[](https://pypi.org/project/shinestacker/)
|
|
39
39
|
[](https://www.qt.io/qt-for-python)
|
|
40
|
-
[](https://codecov.io/github/lucalista/shinestacker)
|
|
41
40
|
[](https://github.com/lucalista/shinestacker/blob/main/.github/workflows/pylint.yml)
|
|
41
|
+
[](https://codecov.io/github/lucalista/shinestacker)
|
|
42
42
|
[](https://shinestacker.readthedocs.io/en/latest/?badge=latest)
|
|
43
43
|
|
|
44
44
|
|
|
45
|
-
|
|
46
45
|
<img src='https://raw.githubusercontent.com/lucalista/shinestacker/main/img/flies.gif' width="400" referrerpolicy="no-referrer"> <img src='https://raw.githubusercontent.com/lucalista/shinestacker/main/img/flies_stack.jpg' width="400" referrerpolicy="no-referrer">
|
|
47
46
|
|
|
48
47
|
<img src='https://raw.githubusercontent.com/lucalista/shinestacker/main/img/coffee.gif' width="400" referrerpolicy="no-referrer"> <img src='https://raw.githubusercontent.com/lucalista/shinestacker/main/img/coffee_stack.jpg' width="400" referrerpolicy="no-referrer">
|
|
49
48
|
|
|
50
|
-
<img src='https://raw.githubusercontent.com/lucalista/shinestacker/main/img/coins.gif' width="400" referrerpolicy="no-referrer"> <img src='https://raw.githubusercontent.com/lucalista/shinestacker/main/img/coins_stack.jpg' width="400" referrerpolicy="no-referrer">
|
|
51
49
|
> **Focus stacking** for microscopy, macro photography, and computational imaging
|
|
52
50
|
|
|
53
51
|
## Key Features
|
|
54
52
|
- 🚀 **Batch Processing**: Align, balance, and stack hundreds of images
|
|
55
|
-
- 🎨 **Hybrid Workflows**: Combine Python scripting with GUI refinement
|
|
56
53
|
- 🧩 **Modular Architecture**: Mix-and-match processing modules
|
|
57
54
|
- 🖌️ **Retouch Editing**: Final interactive retouch of stacked image from individual frames
|
|
58
|
-
- 📊 **Jupyter Integration**:
|
|
55
|
+
- 📊 **Jupyter Integration**: Image processing python notebooks
|
|
59
56
|
|
|
60
57
|
## Interactive GUI
|
|
61
58
|
|
|
@@ -76,16 +73,21 @@ The GUI has two main working areas:
|
|
|
76
73
|
|
|
77
74
|
# Credits
|
|
78
75
|
|
|
79
|
-
The
|
|
76
|
+
The first version of the core focus stack algorithm was initially inspired by the [Laplacian pyramids method](https://github.com/sjawhar/focus-stacking) implementation by Sami Jawhar that was used under permission of the author. The implementation in the latest releases was rewritten from the original code.
|
|
80
77
|
|
|
81
78
|
# Resources
|
|
82
79
|
|
|
83
80
|
* [Pyramid Methods in Image Processing](https://www.researchgate.net/publication/246727904_Pyramid_Methods_in_Image_Processing), E. H. Adelson, C. H. Anderson, J. R. Bergen, P. J. Burt, J. M. Ogden, RCA Engineer, 29-6, Nov/Dec 1984
|
|
84
81
|
Pyramid methods in image processing
|
|
85
82
|
* [A Multi-focus Image Fusion Method Based on Laplacian Pyramid](http://www.jcomputers.us/vol6/jcp0612-07.pdf), Wencheng Wang, Faliang Chang, Journal of Computers 6 (12), 2559, December 2011
|
|
86
|
-
* Another [original implementation on GitHub](https://github.com/bznick98/Focus_Stacking) by Zongnan Bao
|
|
87
83
|
|
|
88
84
|
# License
|
|
89
85
|
|
|
90
86
|
The software is provided as is under the [GNU Lesser General Public License v3.0](https://choosealicense.com/licenses/lgpl-3.0/).
|
|
91
87
|
|
|
88
|
+
# Attribution request
|
|
89
|
+
📸 If you publish images created with Shine Stacker, please consider adding a note such as:
|
|
90
|
+
|
|
91
|
+
*Created with Shine Stacker – https://github.com/lucalista/shinestacker*
|
|
92
|
+
|
|
93
|
+
This is not mandatory, but highly appreciated.
|
|
@@ -1,33 +1,32 @@
|
|
|
1
1
|
shinestacker/__init__.py,sha256=uq2fjAw2z_6TpH3mOcWFZ98GoEPRsNhTAK8N0MMm_e8,448
|
|
2
|
-
shinestacker/_version.py,sha256=
|
|
2
|
+
shinestacker/_version.py,sha256=IbpUPwvtjLOqowcOFsWQ6LKq-FH6cI19IpvfQlxufq0,21
|
|
3
3
|
shinestacker/algorithms/__init__.py,sha256=c4kRrdTLlVI70Q16XkI1RSmz5MD7npDqIpO_02jTG6g,747
|
|
4
4
|
shinestacker/algorithms/align.py,sha256=1CAnVhxaYO-SUd86Mmj7lTmaqlrmUWlF-HEM5341gcs,17166
|
|
5
5
|
shinestacker/algorithms/balance.py,sha256=ZBcw2Ck-CfuobIG1FxFsGVjnLvD1rtVNrTO-GrFbi3Q,16441
|
|
6
|
-
shinestacker/algorithms/base_stack_algo.py,sha256=
|
|
7
|
-
shinestacker/algorithms/core_utils.py,sha256=XJyHDUFXmN4JhbOjJqhP4dJW75B69oZaaWQrSXHUk5o,575
|
|
6
|
+
shinestacker/algorithms/base_stack_algo.py,sha256=EAdVcO2UDq6UwqWZwGZFF7XXJvysyxqqVoswz4PLCdo,1353
|
|
8
7
|
shinestacker/algorithms/denoise.py,sha256=GL3Z4_6MHxSa7Wo4ZzQECZS87tHBFqO0sIVF_jPuYQU,426
|
|
9
8
|
shinestacker/algorithms/depth_map.py,sha256=b88GqbRXEU3wCXBxMcStlgZ4sFJicoiZfJMD30Z4b98,7364
|
|
10
9
|
shinestacker/algorithms/exif.py,sha256=gY9s6Cd4g4swo5qEjSbzuVIvl1GImCYu6ytOO9WrV0I,9435
|
|
11
10
|
shinestacker/algorithms/multilayer.py,sha256=4Y6XlNJHFW74iNDFIeq_zdVtwLBnrieeMd708zJX-lo,8994
|
|
12
|
-
shinestacker/algorithms/noise_detection.py,sha256=
|
|
13
|
-
shinestacker/algorithms/pyramid.py,sha256=
|
|
11
|
+
shinestacker/algorithms/noise_detection.py,sha256=PmscQWi2v3ERTSf8SejkkSZXmTixKvh4NV9CtfuoUfM,8564
|
|
12
|
+
shinestacker/algorithms/pyramid.py,sha256=_Pk19lRQ21b3W3aHQ6DgAe9VVOfbsi2a9jrynF0qFVw,8610
|
|
14
13
|
shinestacker/algorithms/sharpen.py,sha256=h7PMJBYxucg194Usp_6pvItPUMFYbT-ebAc_-7XBFUw,949
|
|
15
|
-
shinestacker/algorithms/stack.py,sha256=
|
|
16
|
-
shinestacker/algorithms/stack_framework.py,sha256=
|
|
14
|
+
shinestacker/algorithms/stack.py,sha256=IAa24rPMXl7F5yfcy0nw-fjsgGPpUkxqeKMqLHqvee8,4796
|
|
15
|
+
shinestacker/algorithms/stack_framework.py,sha256=WZLudhjk6piQz2JULShxcoC3-3mSD-Bgh4_VT7JeG7c,12293
|
|
17
16
|
shinestacker/algorithms/utils.py,sha256=VLm6eZmcAk2QPvomT4d1q56laJSYfbCQmiwI2Rmuu_s,2171
|
|
18
|
-
shinestacker/algorithms/vignetting.py,sha256=
|
|
17
|
+
shinestacker/algorithms/vignetting.py,sha256=wFwi20ob1O3Memav1XQrtrOHgOtKRiK1RV4E-ex69r8,7470
|
|
19
18
|
shinestacker/algorithms/white_balance.py,sha256=PMKsBtxOSn5aRr_Gkx1StHS4eN6kBN2EhNnhg4UG24g,501
|
|
20
19
|
shinestacker/app/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
21
20
|
shinestacker/app/about_dialog.py,sha256=QzZgTcLvkSP3_FhmPOUnwQ_YSxwJdeFrU2IAVYKDgeg,1050
|
|
22
|
-
shinestacker/app/app_config.py,sha256=
|
|
21
|
+
shinestacker/app/app_config.py,sha256=eTIRxp0t7Wic46jMTe_oY3kz7ktZbdM43C3bjshVDKg,494
|
|
23
22
|
shinestacker/app/gui_utils.py,sha256=ptbUKjv5atbx5vW912_j8BVmDZpovAqZDEC48d0R2vA,2331
|
|
24
|
-
shinestacker/app/help_menu.py,sha256=
|
|
23
|
+
shinestacker/app/help_menu.py,sha256=UOlabEY_EKV2Q1BoiU2JAM1udSSBAwXlL7d58bqxKe0,516
|
|
25
24
|
shinestacker/app/main.py,sha256=RAf9WiCipYLK1rrwnXyL1sWq_28zDl9Z_eipfrdtSuY,6421
|
|
26
25
|
shinestacker/app/open_frames.py,sha256=bsu32iJSYJQLe_tQQbvAU5DuMDVX6MRuNdE7B5lojZc,1488
|
|
27
26
|
shinestacker/app/project.py,sha256=ir98-zogYmvx2QYvFbAaBUqLL03qWYkoMOIvLvmQy_w,2736
|
|
28
27
|
shinestacker/app/retouch.py,sha256=ZQ-nRKnHo6xurcP34RNqaAWkmuGBjJ5jE05hTQ_ycis,2482
|
|
29
28
|
shinestacker/config/__init__.py,sha256=aXxi-LmAvXd0daIFrVnTHE5OCaYeK1uf1BKMr7oaXQs,197
|
|
30
|
-
shinestacker/config/config.py,sha256=
|
|
29
|
+
shinestacker/config/config.py,sha256=BshNb20Dx5HqdlpsTQbx4p-LnQ5uBP2q-h9v3pl84ss,1635
|
|
31
30
|
shinestacker/config/constants.py,sha256=79bOcE44MZ0WuAVPjDwwhvNrsQTlHGyIOwmqwlLOfMU,5776
|
|
32
31
|
shinestacker/config/gui_constants.py,sha256=002r96jtxV4Acel7q5NgECrcsDJzW-kOStEHqam-5Gg,2492
|
|
33
32
|
shinestacker/core/__init__.py,sha256=IUEIx6SQ3DygDEHN3_E6uKpHjHtUa4a_U_1dLd_8yEU,484
|
|
@@ -37,17 +36,18 @@ shinestacker/core/exceptions.py,sha256=2-noG-ORAGdvDhL8jBQFs0xxZS4fI6UIkMqrWekgk
|
|
|
37
36
|
shinestacker/core/framework.py,sha256=3Q3zalyZeCiUHXYbBiYadWNdtyD_3j3dcymk5_3NajM,7063
|
|
38
37
|
shinestacker/core/logging.py,sha256=9SuSSy9Usbh7zqmLYMqkmy-VBkOJW000lwqAR0XQs30,3067
|
|
39
38
|
shinestacker/gui/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
40
|
-
shinestacker/gui/action_config.py,sha256=
|
|
41
|
-
shinestacker/gui/actions_window.py,sha256
|
|
39
|
+
shinestacker/gui/action_config.py,sha256=RpUHjq8lmiGJQPnp55O1yd3ZPiLQG3R7jZ52m1VmiQc,48678
|
|
40
|
+
shinestacker/gui/actions_window.py,sha256=-ehMkGshsH22HSnn33ThAMXy7tR_cqWr14mEnXDTfXk,12025
|
|
42
41
|
shinestacker/gui/colors.py,sha256=zgLRcC3fAzklx7zzyjLEsMX2i64YTxGUmQM2woYBZuw,1344
|
|
43
42
|
shinestacker/gui/gui_images.py,sha256=e0KAXSPruZoRHrajfdlmOKBYoRJJQBDan1jgs7YFltY,5678
|
|
44
43
|
shinestacker/gui/gui_logging.py,sha256=ESlk1EAQMdoT8pCZDFsvtU1UtF4h2GKv3wAhxJYxNis,8213
|
|
45
44
|
shinestacker/gui/gui_run.py,sha256=bYXX4N__Ez7JMIJtVcTmLF2PJ3y9bCd-uvlOHsV-4gg,16230
|
|
46
45
|
shinestacker/gui/main_window.py,sha256=z14PWRVDRbuySM05YCCFrn1DPU3U96xwQHy730oiLkw,28577
|
|
47
|
-
shinestacker/gui/new_project.py,sha256=
|
|
46
|
+
shinestacker/gui/new_project.py,sha256=cs3641RW3Uiy2VfwxeM-k254rH4BNykJyojmwxzrEi8,7758
|
|
48
47
|
shinestacker/gui/project_converter.py,sha256=v-oldaw77VLsywhQcl5uhTtPD7GbGFeJo33JJRl3aG4,7453
|
|
49
|
-
shinestacker/gui/project_editor.py,sha256=
|
|
48
|
+
shinestacker/gui/project_editor.py,sha256=zwmj7PFs7X06GY4tkoDBcOL4Tl0IGo4Mf13n2qGwaJY,22245
|
|
50
49
|
shinestacker/gui/project_model.py,sha256=89L0IDSAqRK2mvU1EVIrcsJas8CU-aTzUIjdL1Cv0mw,4421
|
|
50
|
+
shinestacker/gui/select_path_widget.py,sha256=JAxAkbQukPwBc27-EdeobxxJBG4IBfooiV-JZq3ttsY,1015
|
|
51
51
|
shinestacker/gui/ico/focus_stack_bkg.png,sha256=Q86TgqvKEi_IzKI8m6aZB2a3T40UkDtexf2PdeBM9XE,163151
|
|
52
52
|
shinestacker/gui/ico/shinestacker.icns,sha256=m_6WQBx8sE9jQKwIRa_B5oa7_VcNn6e2TyijeQXPjwM,337563
|
|
53
53
|
shinestacker/gui/ico/shinestacker.ico,sha256=yO0NaBWA0uFov_GqHuHQbymoqLtQKt5DPWpGGmRKie0,186277
|
|
@@ -57,16 +57,17 @@ shinestacker/gui/img/forward-button-icon.png,sha256=lNw86T4TOEd_uokHYF8myGSGUXzd
|
|
|
57
57
|
shinestacker/gui/img/play-button-round-icon.png,sha256=9j6Ks9mOGa-2cXyRFpimepAAvSaHzqJKBfxShRb4_dE,4595
|
|
58
58
|
shinestacker/gui/img/plus-round-line-icon.png,sha256=LS068Hlu-CeBvJuB3dwwdJg1lZq6D5MUIv53lu1yKJA,7534
|
|
59
59
|
shinestacker/retouch/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
60
|
-
shinestacker/retouch/base_filter.py,sha256=
|
|
60
|
+
shinestacker/retouch/base_filter.py,sha256=74GmzLjpPtn0Um0YOS8qLC1ZvhbvQX4L_AEDk-5H5nE,4717
|
|
61
61
|
shinestacker/retouch/brush.py,sha256=dzD2FzSpBIPdJRmTZobcrQ1FrVd3tF__ZPnUplNE72s,357
|
|
62
62
|
shinestacker/retouch/brush_gradient.py,sha256=F5SFhyzl8YTMqjJU3jK8BrIlLCYLUvITd5wz3cQE4xk,1453
|
|
63
63
|
shinestacker/retouch/brush_preview.py,sha256=KlUOqA1uvLZRsz2peJ9NgsukyzsppJUw3XXr0NFCuhQ,5181
|
|
64
64
|
shinestacker/retouch/brush_tool.py,sha256=kwX58_gC_Fep6zowDqOs3nG2wCIc8wrJdokDADGm6K0,8016
|
|
65
|
-
shinestacker/retouch/denoise_filter.py,sha256=
|
|
65
|
+
shinestacker/retouch/denoise_filter.py,sha256=eO0Cxo9xwsuiE6-JiWCFB5jf6U1kf2N3ftsDAEQ5sek,1982
|
|
66
66
|
shinestacker/retouch/display_manager.py,sha256=54NRdlyVj_7GzJ7hmKf8Hnf3lJJx2jVSSpWedj-5pIc,7298
|
|
67
|
-
shinestacker/retouch/exif_data.py,sha256=
|
|
67
|
+
shinestacker/retouch/exif_data.py,sha256=uA9ck9skp8ztSUdX1SFrApgtqmxrHtfWW3vsry82H94,2026
|
|
68
68
|
shinestacker/retouch/file_loader.py,sha256=723A_2w3cjn4rhvAzCq-__SWFelDRsMhkazgnb2h7Ig,4810
|
|
69
69
|
shinestacker/retouch/filter_manager.py,sha256=SkioWTr6iFFpugUgZLg0a3m5b9EHdZAeyNFy39qk0z8,453
|
|
70
|
+
shinestacker/retouch/icon_container.py,sha256=6gw1HO1bC2FrdB4dc_iH81DQuLjzuvRGksZ2hKLT9yA,585
|
|
70
71
|
shinestacker/retouch/image_editor.py,sha256=co5zUufgeb1WrD3aF1RVPh1MbcC9-92HSUa2iROnKk4,8503
|
|
71
72
|
shinestacker/retouch/image_editor_ui.py,sha256=vfBALDuHtqSWIPmyfUirkygM1guwQG-gHo0AH0x8_jU,15712
|
|
72
73
|
shinestacker/retouch/image_filters.py,sha256=JF2a7VATO3CGQr5_OOIPi2k7b9HvHzrhhWS73x32t-A,2883
|
|
@@ -74,13 +75,13 @@ shinestacker/retouch/image_viewer.py,sha256=oqBgaanPXWjzIaox5KSRhYOHoGvoYnWm7sqW
|
|
|
74
75
|
shinestacker/retouch/io_gui_handler.py,sha256=2jkbPXew95rMKO2aC9hwZJGtZRg3wCCtXI0SFFiNHUI,9089
|
|
75
76
|
shinestacker/retouch/io_manager.py,sha256=sNcZVEttiVdxNBVs39ZvexqOcvtjl2CvJs6BVqmGvOM,2148
|
|
76
77
|
shinestacker/retouch/layer_collection.py,sha256=j1NiGGtLZ3OwrftBVNT4rb0Kq0CfWAB3t2bUrqHx1Sk,5608
|
|
77
|
-
shinestacker/retouch/shortcuts_help.py,sha256=
|
|
78
|
+
shinestacker/retouch/shortcuts_help.py,sha256=dlt7OSAr9thYuoEPlirTU_YRzv5xP9vy2-9mZO7GVAA,3308
|
|
78
79
|
shinestacker/retouch/undo_manager.py,sha256=_ekbcOLcPbQLY7t-o8wf-b1uA6OPY9rRyLM-KqMlQRo,3257
|
|
79
|
-
shinestacker/retouch/unsharp_mask_filter.py,sha256=
|
|
80
|
-
shinestacker/retouch/white_balance_filter.py,sha256=
|
|
81
|
-
shinestacker-0.3.
|
|
82
|
-
shinestacker-0.3.
|
|
83
|
-
shinestacker-0.3.
|
|
84
|
-
shinestacker-0.3.
|
|
85
|
-
shinestacker-0.3.
|
|
86
|
-
shinestacker-0.3.
|
|
80
|
+
shinestacker/retouch/unsharp_mask_filter.py,sha256=hNJlqXYjf9Nd8KlVy09fd4TxrHa9Ofef0ZLSMHjLL6I,3481
|
|
81
|
+
shinestacker/retouch/white_balance_filter.py,sha256=2krwdz0X6qLWuCIEQcPtSQA_txfAsl7QUzfdsOLBrBU,4878
|
|
82
|
+
shinestacker-0.3.6.dist-info/licenses/LICENSE,sha256=cBN0P3F6BWFkfOabkhuTxwJnK1B0v50jmmzZJjGGous,80
|
|
83
|
+
shinestacker-0.3.6.dist-info/METADATA,sha256=1xiC2Bgn2sR8bqwuapC0QNodFy9fOO9p8ybGpqvHOwc,4915
|
|
84
|
+
shinestacker-0.3.6.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
85
|
+
shinestacker-0.3.6.dist-info/entry_points.txt,sha256=SY6g1LqtMmp23q1DGwLUDT_dhLX9iss8DvWkiWLyo_4,166
|
|
86
|
+
shinestacker-0.3.6.dist-info/top_level.txt,sha256=MhijwnBVX5psfsyX8JZjqp3SYiWPsKe69f3Gnyze4Fw,13
|
|
87
|
+
shinestacker-0.3.6.dist-info/RECORD,,
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
# pylint: disable=C0114, C0116
|
|
2
|
-
import os
|
|
3
|
-
from ..config.config import config
|
|
4
|
-
|
|
5
|
-
if not config.DISABLE_TQDM:
|
|
6
|
-
from tqdm import tqdm
|
|
7
|
-
from tqdm.notebook import tqdm_notebook
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
def check_path_exists(path):
|
|
11
|
-
if not os.path.exists(path):
|
|
12
|
-
raise RuntimeError('Path does not exist: ' + path)
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
def make_tqdm_bar(name, size, ncols=80):
|
|
16
|
-
if not config.DISABLE_TQDM:
|
|
17
|
-
if config.JUPYTER_NOTEBOOK:
|
|
18
|
-
tbar = tqdm_notebook(desc=name, total=size)
|
|
19
|
-
else:
|
|
20
|
-
tbar = tqdm(desc=name, total=size, ncols=ncols)
|
|
21
|
-
return tbar
|
|
22
|
-
return None
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|