shinestacker 0.3.0__py3-none-any.whl → 0.3.2__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 +6 -6
- shinestacker/_version.py +1 -1
- shinestacker/algorithms/balance.py +6 -7
- shinestacker/algorithms/noise_detection.py +2 -0
- shinestacker/algorithms/utils.py +4 -0
- shinestacker/algorithms/white_balance.py +13 -0
- shinestacker/app/open_frames.py +6 -4
- shinestacker/config/__init__.py +2 -1
- shinestacker/config/config.py +1 -0
- shinestacker/config/constants.py +1 -0
- shinestacker/config/gui_constants.py +1 -0
- shinestacker/core/__init__.py +4 -3
- shinestacker/core/colors.py +1 -0
- shinestacker/core/core_utils.py +6 -6
- shinestacker/core/exceptions.py +1 -0
- shinestacker/core/framework.py +2 -1
- shinestacker/gui/action_config.py +47 -42
- shinestacker/gui/actions_window.py +8 -5
- shinestacker/gui/new_project.py +1 -0
- shinestacker/retouch/brush_gradient.py +20 -0
- shinestacker/retouch/brush_preview.py +10 -14
- shinestacker/retouch/brush_tool.py +164 -0
- shinestacker/retouch/denoise_filter.py +56 -0
- shinestacker/retouch/display_manager.py +177 -0
- shinestacker/retouch/exif_data.py +2 -1
- shinestacker/retouch/filter_base.py +114 -0
- shinestacker/retouch/filter_manager.py +14 -0
- shinestacker/retouch/image_editor.py +108 -543
- shinestacker/retouch/image_editor_ui.py +42 -75
- shinestacker/retouch/image_filters.py +27 -423
- shinestacker/retouch/image_viewer.py +31 -31
- shinestacker/retouch/io_gui_handler.py +208 -0
- shinestacker/retouch/io_manager.py +53 -0
- shinestacker/retouch/layer_collection.py +118 -0
- shinestacker/retouch/unsharp_mask_filter.py +84 -0
- shinestacker/retouch/white_balance_filter.py +111 -0
- {shinestacker-0.3.0.dist-info → shinestacker-0.3.2.dist-info}/METADATA +3 -2
- {shinestacker-0.3.0.dist-info → shinestacker-0.3.2.dist-info}/RECORD +42 -31
- shinestacker/retouch/brush_controller.py +0 -57
- {shinestacker-0.3.0.dist-info → shinestacker-0.3.2.dist-info}/WHEEL +0 -0
- {shinestacker-0.3.0.dist-info → shinestacker-0.3.2.dist-info}/entry_points.txt +0 -0
- {shinestacker-0.3.0.dist-info → shinestacker-0.3.2.dist-info}/licenses/LICENSE +0 -0
- {shinestacker-0.3.0.dist-info → shinestacker-0.3.2.dist-info}/top_level.txt +0 -0
shinestacker/__init__.py
CHANGED
|
@@ -3,14 +3,14 @@ from ._version import __version__
|
|
|
3
3
|
from . import config
|
|
4
4
|
from . import core
|
|
5
5
|
from . import algorithms
|
|
6
|
-
from .config import __all__
|
|
7
|
-
from .core import __all__
|
|
8
|
-
from .algorithms import __all__
|
|
6
|
+
from .config import __all__ as config_all
|
|
7
|
+
from .core import __all__ as core_all
|
|
8
|
+
from .algorithms import __all__ as algorithms_all
|
|
9
9
|
from .config import *
|
|
10
10
|
from .core import *
|
|
11
11
|
from .algorithms import *
|
|
12
12
|
|
|
13
13
|
__all__ = ['__version__']
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
__all__ += config_all
|
|
15
|
+
__all__ += core_all
|
|
16
|
+
__all__ += algorithms_all
|
shinestacker/_version.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = '0.3.
|
|
1
|
+
__version__ = '0.3.2'
|
|
@@ -133,17 +133,16 @@ class Correction:
|
|
|
133
133
|
self.corrections = np.ones((size, self.channels))
|
|
134
134
|
|
|
135
135
|
def calc_hist_1ch(self, image):
|
|
136
|
+
img_subsample = image if self.subsample == 1 else image[::self.subsample, ::self.subsample]
|
|
136
137
|
if self.mask_size == 0:
|
|
137
|
-
image_sel =
|
|
138
|
+
image_sel = img_subsample
|
|
138
139
|
else:
|
|
139
|
-
height, width =
|
|
140
|
+
height, width = img_subsample.shape[:2]
|
|
140
141
|
xv, yv = np.meshgrid(np.linspace(0, width - 1, width), np.linspace(0, height - 1, height))
|
|
141
142
|
mask_radius = (min(width, height) * self.mask_size / 2)
|
|
142
|
-
image_sel =
|
|
143
|
-
hist, bins = np.histogram(
|
|
144
|
-
|
|
145
|
-
bins=np.linspace(-0.5, self.num_pixel_values - 0.5,
|
|
146
|
-
self.num_pixel_values + 1))
|
|
143
|
+
image_sel = img_subsample[(xv - width / 2) ** 2 + (yv - height / 2) ** 2 <= mask_radius ** 2]
|
|
144
|
+
hist, bins = np.histogram(image_sel, bins=np.linspace(-0.5, self.num_pixel_values - 0.5,
|
|
145
|
+
self.num_pixel_values + 1))
|
|
147
146
|
return hist
|
|
148
147
|
|
|
149
148
|
def balance(self, image, idx):
|
|
@@ -86,6 +86,8 @@ class NoiseDetection(FrameMultiDirectory, JobBase):
|
|
|
86
86
|
progress_callback=progress_callback)
|
|
87
87
|
if not config.DISABLE_TQDM:
|
|
88
88
|
self.bar.close()
|
|
89
|
+
if mean_img is None:
|
|
90
|
+
raise RuntimeError("Mean image is None")
|
|
89
91
|
blurred = cv2.GaussianBlur(mean_img, (self.blur_size, self.blur_size), 0)
|
|
90
92
|
diff = cv2.absdiff(mean_img, blurred)
|
|
91
93
|
channels = cv2.split(diff)
|
shinestacker/algorithms/utils.py
CHANGED
|
@@ -47,10 +47,14 @@ def img_bw(img):
|
|
|
47
47
|
|
|
48
48
|
|
|
49
49
|
def get_img_metadata(img):
|
|
50
|
+
if img is None:
|
|
51
|
+
return None, None
|
|
50
52
|
return img.shape[:2], img.dtype
|
|
51
53
|
|
|
52
54
|
|
|
53
55
|
def validate_image(img, expected_shape=None, expected_dtype=None):
|
|
56
|
+
if img is None:
|
|
57
|
+
raise RuntimeError("Image is None")
|
|
54
58
|
shape, dtype = get_img_metadata(img)
|
|
55
59
|
if expected_shape and shape[:2] != expected_shape[:2]:
|
|
56
60
|
raise ShapeError(expected_shape, shape)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def white_balance_from_rgb(img, target_rgb):
|
|
5
|
+
img_float = img.astype(np.float64)
|
|
6
|
+
target_bgr = (target_rgb[2], target_rgb[1], target_rgb[0])
|
|
7
|
+
target_gray = sum(target_bgr) / 3.0
|
|
8
|
+
scales = [target_gray / val if val != 0 else 1.0 for val in target_bgr]
|
|
9
|
+
for c in range(3):
|
|
10
|
+
img_float[..., c] *= scales[c]
|
|
11
|
+
max_val = np.iinfo(img.dtype).max
|
|
12
|
+
img_float = np.clip(img_float, 0, max_val)
|
|
13
|
+
return img_float.astype(img.dtype)
|
shinestacker/app/open_frames.py
CHANGED
|
@@ -5,15 +5,17 @@ from PySide6.QtCore import QTimer
|
|
|
5
5
|
|
|
6
6
|
def open_files(editor, filenames):
|
|
7
7
|
if len(filenames) == 1:
|
|
8
|
-
QTimer.singleShot(100, lambda: editor.open_file(filenames[0]))
|
|
8
|
+
QTimer.singleShot(100, lambda: editor.io_gui_handler.open_file(filenames[0]))
|
|
9
9
|
else:
|
|
10
10
|
def check_thread():
|
|
11
|
-
|
|
11
|
+
thread = editor.io_gui_handler.loader_thread
|
|
12
|
+
if thread is None or thread.isRunning():
|
|
12
13
|
QTimer.singleShot(100, check_thread)
|
|
13
14
|
else:
|
|
14
|
-
editor.import_frames_from_files(filenames[1:])
|
|
15
|
+
editor.io_gui_handler.import_frames_from_files(filenames[1:])
|
|
16
|
+
|
|
15
17
|
QTimer.singleShot(100, lambda: (
|
|
16
|
-
editor.open_file(filenames[0]),
|
|
18
|
+
editor.io_gui_handler.open_file(filenames[0]),
|
|
17
19
|
QTimer.singleShot(100, check_thread)
|
|
18
20
|
))
|
|
19
21
|
|
shinestacker/config/__init__.py
CHANGED
shinestacker/config/config.py
CHANGED
shinestacker/config/constants.py
CHANGED
shinestacker/core/__init__.py
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
# flake8: noqa F401
|
|
2
|
+
# pylint: disable=C0114
|
|
2
3
|
from .logging import setup_logging
|
|
3
|
-
from .exceptions import (FocusStackError, InvalidOptionError, ImageLoadError, ImageSaveError,
|
|
4
|
-
BitDepthError, ShapeError, RunStopException)
|
|
4
|
+
from .exceptions import (FocusStackError, InvalidOptionError, ImageLoadError, ImageSaveError,
|
|
5
|
+
AlignmentError, BitDepthError, ShapeError, RunStopException)
|
|
5
6
|
from .framework import Job
|
|
6
7
|
|
|
7
8
|
__all__ = [
|
|
8
9
|
'setup_logging',
|
|
9
10
|
'FocusStackError', 'InvalidOptionError', 'ImageLoadError', 'ImageSaveError',
|
|
10
11
|
'AlignmentError','BitDepthError', 'ShapeError', 'RunStopException',
|
|
11
|
-
'Job']
|
|
12
|
+
'Job']
|
shinestacker/core/colors.py
CHANGED
shinestacker/core/core_utils.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
# pylint: disable=C0114, C0116, C0201
|
|
1
2
|
import os
|
|
2
3
|
import sys
|
|
3
4
|
import platform
|
|
@@ -10,18 +11,17 @@ if not config.DISABLE_TQDM:
|
|
|
10
11
|
|
|
11
12
|
def check_path_exists(path):
|
|
12
13
|
if not os.path.exists(path):
|
|
13
|
-
raise
|
|
14
|
+
raise RuntimeError('Path does not exist: ' + path)
|
|
14
15
|
|
|
15
16
|
|
|
16
17
|
def make_tqdm_bar(name, size, ncols=80):
|
|
17
18
|
if not config.DISABLE_TQDM:
|
|
18
19
|
if config.JUPYTER_NOTEBOOK:
|
|
19
|
-
|
|
20
|
+
tbar = tqdm_notebook(desc=name, total=size)
|
|
20
21
|
else:
|
|
21
|
-
|
|
22
|
-
return
|
|
23
|
-
|
|
24
|
-
return None
|
|
22
|
+
tbar = tqdm(desc=name, total=size, ncols=ncols)
|
|
23
|
+
return tbar
|
|
24
|
+
return None
|
|
25
25
|
|
|
26
26
|
|
|
27
27
|
def get_app_base_path():
|
shinestacker/core/exceptions.py
CHANGED
shinestacker/core/framework.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
# pylint: disable=C0114, C0115, C0116
|
|
1
2
|
import time
|
|
2
3
|
import logging
|
|
3
4
|
from .. config.config import config
|
|
@@ -57,7 +58,7 @@ def elapsed_time_str(start):
|
|
|
57
58
|
ss = dt - mm * 60
|
|
58
59
|
hh = mm // 60
|
|
59
60
|
mm -= hh * 60
|
|
60
|
-
return
|
|
61
|
+
return "{:02d}:{:02d}:{:05.2f}s".format(hh, mm, ss)
|
|
61
62
|
|
|
62
63
|
|
|
63
64
|
class JobBase:
|
|
@@ -22,8 +22,9 @@ FIELD_TYPES = [FIELD_TEXT, FIELD_ABS_PATH, FIELD_REL_PATH, FIELD_FLOAT,
|
|
|
22
22
|
|
|
23
23
|
|
|
24
24
|
class ActionConfigurator(ABC):
|
|
25
|
-
def __init__(self, expert
|
|
25
|
+
def __init__(self, expert, current_wd):
|
|
26
26
|
self.expert = expert
|
|
27
|
+
self.current_wd = current_wd
|
|
27
28
|
|
|
28
29
|
@abstractmethod
|
|
29
30
|
def create_form(self, layout: QFormLayout, params: Dict[str, Any]):
|
|
@@ -35,9 +36,10 @@ class ActionConfigurator(ABC):
|
|
|
35
36
|
|
|
36
37
|
|
|
37
38
|
class FieldBuilder:
|
|
38
|
-
def __init__(self, layout, action):
|
|
39
|
+
def __init__(self, layout, action, current_wd):
|
|
39
40
|
self.layout = layout
|
|
40
41
|
self.action = action
|
|
42
|
+
self.current_wd = current_wd
|
|
41
43
|
self.fields = {}
|
|
42
44
|
|
|
43
45
|
def add_field(self, tag: str, field_type: str, label: str,
|
|
@@ -159,6 +161,9 @@ class FieldBuilder:
|
|
|
159
161
|
if field['type'] == FIELD_REL_PATH and 'working_path' in params:
|
|
160
162
|
try:
|
|
161
163
|
working_path = self.get_working_path()
|
|
164
|
+
working_path_abs = os.path.isabs(working_path)
|
|
165
|
+
if not working_path_abs:
|
|
166
|
+
working_path = os.path.join(self.current_wd, working_path)
|
|
162
167
|
abs_path = os.path.normpath(os.path.join(working_path, params[tag]))
|
|
163
168
|
if not abs_path.startswith(os.path.normpath(working_path)):
|
|
164
169
|
QMessageBox.warning(None, "Invalid Path",
|
|
@@ -370,8 +375,9 @@ class FieldBuilder:
|
|
|
370
375
|
|
|
371
376
|
|
|
372
377
|
class ActionConfigDialog(QDialog):
|
|
373
|
-
def __init__(self, action: ActionConfig, parent=None):
|
|
378
|
+
def __init__(self, action: ActionConfig, current_wd, parent=None):
|
|
374
379
|
super().__init__(parent)
|
|
380
|
+
self.current_wd = current_wd
|
|
375
381
|
self.action = action
|
|
376
382
|
self.setWindowTitle(f"Configure {action.type_name}")
|
|
377
383
|
self.resize(500, self.height())
|
|
@@ -397,18 +403,18 @@ class ActionConfigDialog(QDialog):
|
|
|
397
403
|
|
|
398
404
|
def get_configurator(self, action_type: str) -> ActionConfigurator:
|
|
399
405
|
configurators = {
|
|
400
|
-
constants.ACTION_JOB: JobConfigurator(self.expert()),
|
|
401
|
-
constants.ACTION_COMBO: CombinedActionsConfigurator(self.expert()),
|
|
402
|
-
constants.ACTION_NOISEDETECTION: NoiseDetectionConfigurator(self.expert()),
|
|
403
|
-
constants.ACTION_FOCUSSTACK: FocusStackConfigurator(self.expert()),
|
|
404
|
-
constants.ACTION_FOCUSSTACKBUNCH: FocusStackBunchConfigurator(self.expert()),
|
|
405
|
-
constants.ACTION_MULTILAYER: MultiLayerConfigurator(self.expert()),
|
|
406
|
-
constants.ACTION_MASKNOISE: MaskNoiseConfigurator(self.expert()),
|
|
407
|
-
constants.ACTION_VIGNETTING: VignettingConfigurator(self.expert()),
|
|
408
|
-
constants.ACTION_ALIGNFRAMES: AlignFramesConfigurator(self.expert()),
|
|
409
|
-
constants.ACTION_BALANCEFRAMES: BalanceFramesConfigurator(self.expert()),
|
|
406
|
+
constants.ACTION_JOB: JobConfigurator(self.expert(), self.current_wd),
|
|
407
|
+
constants.ACTION_COMBO: CombinedActionsConfigurator(self.expert(), self.current_wd),
|
|
408
|
+
constants.ACTION_NOISEDETECTION: NoiseDetectionConfigurator(self.expert(), self.current_wd),
|
|
409
|
+
constants.ACTION_FOCUSSTACK: FocusStackConfigurator(self.expert(), self.current_wd),
|
|
410
|
+
constants.ACTION_FOCUSSTACKBUNCH: FocusStackBunchConfigurator(self.expert(), self.current_wd),
|
|
411
|
+
constants.ACTION_MULTILAYER: MultiLayerConfigurator(self.expert(), self.current_wd),
|
|
412
|
+
constants.ACTION_MASKNOISE: MaskNoiseConfigurator(self.expert(), self.current_wd),
|
|
413
|
+
constants.ACTION_VIGNETTING: VignettingConfigurator(self.expert(), self.current_wd),
|
|
414
|
+
constants.ACTION_ALIGNFRAMES: AlignFramesConfigurator(self.expert(), self.current_wd),
|
|
415
|
+
constants.ACTION_BALANCEFRAMES: BalanceFramesConfigurator(self.expert(), self.current_wd),
|
|
410
416
|
}
|
|
411
|
-
return configurators.get(action_type, DefaultActionConfigurator(self.expert()))
|
|
417
|
+
return configurators.get(action_type, DefaultActionConfigurator(self.expert(), self.current_wd))
|
|
412
418
|
|
|
413
419
|
def accept(self):
|
|
414
420
|
self.parent()._project_buffer.append(self.parent().project.clone())
|
|
@@ -428,8 +434,8 @@ class ActionConfigDialog(QDialog):
|
|
|
428
434
|
|
|
429
435
|
|
|
430
436
|
class NoNameActionConfigurator(ActionConfigurator):
|
|
431
|
-
def __init__(self, expert
|
|
432
|
-
super().__init__(expert)
|
|
437
|
+
def __init__(self, expert, current_wd):
|
|
438
|
+
super().__init__(expert, current_wd)
|
|
433
439
|
|
|
434
440
|
def get_builder(self):
|
|
435
441
|
return self.builder
|
|
@@ -444,17 +450,17 @@ class NoNameActionConfigurator(ActionConfigurator):
|
|
|
444
450
|
|
|
445
451
|
|
|
446
452
|
class DefaultActionConfigurator(NoNameActionConfigurator):
|
|
447
|
-
def __init__(self, expert
|
|
448
|
-
super().__init__(expert)
|
|
453
|
+
def __init__(self, expert, current_wd):
|
|
454
|
+
super().__init__(expert, current_wd)
|
|
449
455
|
|
|
450
456
|
def create_form(self, layout, action, tag='Action'):
|
|
451
|
-
self.builder = FieldBuilder(layout, action)
|
|
457
|
+
self.builder = FieldBuilder(layout, action, self.current_wd)
|
|
452
458
|
self.builder.add_field('name', FIELD_TEXT, f'{tag} name', required=True)
|
|
453
459
|
|
|
454
460
|
|
|
455
461
|
class JobConfigurator(DefaultActionConfigurator):
|
|
456
|
-
def __init__(self, expert
|
|
457
|
-
super().__init__(expert)
|
|
462
|
+
def __init__(self, expert, current_wd):
|
|
463
|
+
super().__init__(expert, current_wd)
|
|
458
464
|
|
|
459
465
|
def create_form(self, layout, action):
|
|
460
466
|
super().create_form(layout, action, "Job")
|
|
@@ -464,8 +470,8 @@ class JobConfigurator(DefaultActionConfigurator):
|
|
|
464
470
|
|
|
465
471
|
|
|
466
472
|
class NoiseDetectionConfigurator(DefaultActionConfigurator):
|
|
467
|
-
def __init__(self, expert
|
|
468
|
-
super().__init__(expert)
|
|
473
|
+
def __init__(self, expert, current_wd):
|
|
474
|
+
super().__init__(expert, current_wd)
|
|
469
475
|
|
|
470
476
|
def create_form(self, layout, action):
|
|
471
477
|
super().create_form(layout, action)
|
|
@@ -492,8 +498,8 @@ class NoiseDetectionConfigurator(DefaultActionConfigurator):
|
|
|
492
498
|
|
|
493
499
|
|
|
494
500
|
class FocusStackBaseConfigurator(DefaultActionConfigurator):
|
|
495
|
-
def __init__(self, expert
|
|
496
|
-
super().__init__(expert)
|
|
501
|
+
def __init__(self, expert, current_wd):
|
|
502
|
+
super().__init__(expert, current_wd)
|
|
497
503
|
|
|
498
504
|
ENERGY_OPTIONS = ['Laplacian', 'Sobel']
|
|
499
505
|
MAP_TYPE_OPTIONS = ['Average', 'Maximum']
|
|
@@ -583,8 +589,8 @@ class FocusStackBaseConfigurator(DefaultActionConfigurator):
|
|
|
583
589
|
|
|
584
590
|
|
|
585
591
|
class FocusStackConfigurator(FocusStackBaseConfigurator):
|
|
586
|
-
def __init__(self, expert
|
|
587
|
-
super().__init__(expert)
|
|
592
|
+
def __init__(self, expert, current_wd):
|
|
593
|
+
super().__init__(expert, current_wd)
|
|
588
594
|
|
|
589
595
|
def create_form(self, layout, action):
|
|
590
596
|
super().create_form(layout, action)
|
|
@@ -599,8 +605,8 @@ class FocusStackConfigurator(FocusStackBaseConfigurator):
|
|
|
599
605
|
|
|
600
606
|
|
|
601
607
|
class FocusStackBunchConfigurator(FocusStackBaseConfigurator):
|
|
602
|
-
def __init__(self, expert
|
|
603
|
-
super().__init__(expert)
|
|
608
|
+
def __init__(self, expert, current_wd):
|
|
609
|
+
super().__init__(expert, current_wd)
|
|
604
610
|
|
|
605
611
|
def create_form(self, layout, action):
|
|
606
612
|
super().create_form(layout, action)
|
|
@@ -614,8 +620,8 @@ class FocusStackBunchConfigurator(FocusStackBaseConfigurator):
|
|
|
614
620
|
|
|
615
621
|
|
|
616
622
|
class MultiLayerConfigurator(DefaultActionConfigurator):
|
|
617
|
-
def __init__(self, expert
|
|
618
|
-
super().__init__(expert)
|
|
623
|
+
def __init__(self, expert, current_wd):
|
|
624
|
+
super().__init__(expert, current_wd)
|
|
619
625
|
|
|
620
626
|
def create_form(self, layout, action):
|
|
621
627
|
super().create_form(layout, action)
|
|
@@ -635,8 +641,8 @@ class MultiLayerConfigurator(DefaultActionConfigurator):
|
|
|
635
641
|
|
|
636
642
|
|
|
637
643
|
class CombinedActionsConfigurator(DefaultActionConfigurator):
|
|
638
|
-
def __init__(self, expert
|
|
639
|
-
super().__init__(expert)
|
|
644
|
+
def __init__(self, expert, current_wd):
|
|
645
|
+
super().__init__(expert, current_wd)
|
|
640
646
|
|
|
641
647
|
def create_form(self, layout, action):
|
|
642
648
|
DefaultActionConfigurator.create_form(self, layout, action)
|
|
@@ -659,8 +665,8 @@ class CombinedActionsConfigurator(DefaultActionConfigurator):
|
|
|
659
665
|
|
|
660
666
|
|
|
661
667
|
class MaskNoiseConfigurator(NoNameActionConfigurator):
|
|
662
|
-
def __init__(self, expert
|
|
663
|
-
super().__init__(expert)
|
|
668
|
+
def __init__(self, expert, current_wd):
|
|
669
|
+
super().__init__(expert, current_wd)
|
|
664
670
|
|
|
665
671
|
def create_form(self, layout, action):
|
|
666
672
|
DefaultActionConfigurator.create_form(self, layout, action)
|
|
@@ -675,8 +681,8 @@ class MaskNoiseConfigurator(NoNameActionConfigurator):
|
|
|
675
681
|
|
|
676
682
|
|
|
677
683
|
class VignettingConfigurator(NoNameActionConfigurator):
|
|
678
|
-
def __init__(self, expert
|
|
679
|
-
super().__init__(expert)
|
|
684
|
+
def __init__(self, expert, current_wd):
|
|
685
|
+
super().__init__(expert, current_wd)
|
|
680
686
|
|
|
681
687
|
def create_form(self, layout, action):
|
|
682
688
|
DefaultActionConfigurator.create_form(self, layout, action)
|
|
@@ -699,8 +705,8 @@ class AlignFramesConfigurator(NoNameActionConfigurator):
|
|
|
699
705
|
METHOD_OPTIONS = ['Random Sample Consensus (RANSAC)', 'Least Median (LMEDS)']
|
|
700
706
|
MATCHING_METHOD_OPTIONS = ['K-nearest neighbors', 'Hamming distance']
|
|
701
707
|
|
|
702
|
-
def __init__(self, expert
|
|
703
|
-
super().__init__(expert)
|
|
708
|
+
def __init__(self, expert, current_wd):
|
|
709
|
+
super().__init__(expert, current_wd)
|
|
704
710
|
|
|
705
711
|
def show_info(self, message, timeout=3000):
|
|
706
712
|
self.info_label.setText(message)
|
|
@@ -713,7 +719,6 @@ class AlignFramesConfigurator(NoNameActionConfigurator):
|
|
|
713
719
|
match_method = {k: v for k, v in zip(self.MATCHING_METHOD_OPTIONS,
|
|
714
720
|
constants.VALID_MATCHING_METHODS)}[self.matching_method_field.currentText()]
|
|
715
721
|
try:
|
|
716
|
-
print(detector, descriptor, match_method)
|
|
717
722
|
validate_align_config(detector, descriptor, match_method)
|
|
718
723
|
except Exception as e:
|
|
719
724
|
self.show_info(str(e))
|
|
@@ -855,8 +860,8 @@ class BalanceFramesConfigurator(NoNameActionConfigurator):
|
|
|
855
860
|
CORRECTION_MAP_OPTIONS = ['Linear', 'Gamma', 'Match histograms']
|
|
856
861
|
CHANNEL_OPTIONS = ['Luminosity', 'RGB', 'HSV', 'HLS']
|
|
857
862
|
|
|
858
|
-
def __init__(self, expert
|
|
859
|
-
super().__init__(expert)
|
|
863
|
+
def __init__(self, expert, current_wd):
|
|
864
|
+
super().__init__(expert, current_wd)
|
|
860
865
|
|
|
861
866
|
def create_form(self, layout, action):
|
|
862
867
|
DefaultActionConfigurator.create_form(self, layout, action)
|
|
@@ -126,7 +126,11 @@ class ActionsWindow(ProjectEditor):
|
|
|
126
126
|
file_path, _ = QFileDialog.getOpenFileName(self, "Open Project", "", "Project Files (*.fsp);;All Files (*)")
|
|
127
127
|
if file_path:
|
|
128
128
|
try:
|
|
129
|
-
self.
|
|
129
|
+
self._current_file = file_path
|
|
130
|
+
self._current_file_wd = '' if os.path.isabs(file_path) else os.path.dirname(file_path)
|
|
131
|
+
if not os.path.isabs(self._current_file_wd):
|
|
132
|
+
self._current_file_wd = os.path.abspath(self._current_file_wd)
|
|
133
|
+
self._current_file = os.path.basename(self._current_file)
|
|
130
134
|
file = open(file_path, 'r')
|
|
131
135
|
pp = file_path.split('/')
|
|
132
136
|
if len(pp) > 1:
|
|
@@ -137,7 +141,6 @@ class ActionsWindow(ProjectEditor):
|
|
|
137
141
|
raise RuntimeError(f"Project from file {file_path} produced a null project.")
|
|
138
142
|
self.set_project(project)
|
|
139
143
|
self._modified_project = False
|
|
140
|
-
self._current_file = file_path
|
|
141
144
|
self.update_title()
|
|
142
145
|
self.refresh_ui(0, -1)
|
|
143
146
|
if self.job_list.count() > 0:
|
|
@@ -223,7 +226,7 @@ class ActionsWindow(ProjectEditor):
|
|
|
223
226
|
index = self.job_list.row(item)
|
|
224
227
|
if 0 <= index < len(self.project.jobs):
|
|
225
228
|
job = self.project.jobs[index]
|
|
226
|
-
dialog = ActionConfigDialog(job, self)
|
|
229
|
+
dialog = ActionConfigDialog(job, self._current_file_wd, self)
|
|
227
230
|
if dialog.exec() == QDialog.Accepted:
|
|
228
231
|
current_row = self.job_list.currentRow()
|
|
229
232
|
if current_row >= 0:
|
|
@@ -255,7 +258,7 @@ class ActionsWindow(ProjectEditor):
|
|
|
255
258
|
if current_action:
|
|
256
259
|
if not is_sub_action:
|
|
257
260
|
self.set_enabled_sub_actions_gui(current_action.type_name == constants.ACTION_COMBO)
|
|
258
|
-
dialog = ActionConfigDialog(current_action, self)
|
|
261
|
+
dialog = ActionConfigDialog(current_action, self._current_file_wd, self)
|
|
259
262
|
if dialog.exec() == QDialog.Accepted:
|
|
260
263
|
self.on_job_selected(job_index)
|
|
261
264
|
self.refresh_ui()
|
|
@@ -277,7 +280,7 @@ class ActionsWindow(ProjectEditor):
|
|
|
277
280
|
self.edit_action(current_action)
|
|
278
281
|
|
|
279
282
|
def edit_action(self, action):
|
|
280
|
-
dialog = ActionConfigDialog(action, self)
|
|
283
|
+
dialog = ActionConfigDialog(action, self._current_file_wd, self)
|
|
281
284
|
if dialog.exec() == QDialog.Accepted:
|
|
282
285
|
self.on_job_selected(self.job_list.currentRow())
|
|
283
286
|
self.mark_as_modified()
|
shinestacker/gui/new_project.py
CHANGED
|
@@ -85,6 +85,7 @@ class NewProjectDialog(QDialog):
|
|
|
85
85
|
bunch_overlap_range = gui_constants.NEW_PROJECT_BUNCH_OVERLAP
|
|
86
86
|
self.bunch_overlap.setRange(bunch_overlap_range['min'], bunch_overlap_range['max'])
|
|
87
87
|
self.bunch_overlap.setValue(constants.DEFAULT_OVERLAP)
|
|
88
|
+
self.update_bunch_options(gui_constants.NEW_PROJECT_BUNCH_STACK)
|
|
88
89
|
self.focus_stack_pyramid = QCheckBox()
|
|
89
90
|
self.focus_stack_pyramid.setChecked(gui_constants.NEW_PROJECT_FOCUS_STACK_PYRAMID)
|
|
90
91
|
self.focus_stack_depth_map = QCheckBox()
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from PySide6.QtGui import QRadialGradient
|
|
2
|
+
from PySide6.QtGui import QColor
|
|
3
|
+
from .. config.gui_constants import gui_constants
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def create_brush_gradient(center_x, center_y, radius, hardness, inner_color=None, outer_color=None, opacity=100):
|
|
7
|
+
gradient = QRadialGradient(center_x, center_y, float(radius))
|
|
8
|
+
inner = inner_color if inner_color is not None else QColor(*gui_constants.BRUSH_COLORS['inner'])
|
|
9
|
+
outer = outer_color if outer_color is not None else QColor(*gui_constants.BRUSH_COLORS['gradient_end'])
|
|
10
|
+
inner_with_opacity = QColor(inner)
|
|
11
|
+
inner_with_opacity.setAlpha(int(float(inner.alpha()) * float(opacity) / 100.0))
|
|
12
|
+
if hardness < 100:
|
|
13
|
+
hardness_normalized = float(hardness) / 100.0
|
|
14
|
+
gradient.setColorAt(0.0, inner_with_opacity)
|
|
15
|
+
gradient.setColorAt(hardness_normalized, inner_with_opacity)
|
|
16
|
+
gradient.setColorAt(1.0, outer)
|
|
17
|
+
else:
|
|
18
|
+
gradient.setColorAt(0.0, inner_with_opacity)
|
|
19
|
+
gradient.setColorAt(1.0, inner_with_opacity)
|
|
20
|
+
return gradient
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import numpy as np
|
|
2
2
|
from PySide6.QtWidgets import QGraphicsPixmapItem
|
|
3
|
-
from PySide6.QtCore import Qt
|
|
3
|
+
from PySide6.QtCore import Qt
|
|
4
4
|
from PySide6.QtGui import QPixmap, QPainter, QImage
|
|
5
5
|
|
|
6
6
|
|
|
@@ -42,6 +42,7 @@ def create_brush_mask(size, hardness_percent, opacity_percent):
|
|
|
42
42
|
class BrushPreviewItem(QGraphicsPixmapItem):
|
|
43
43
|
def __init__(self):
|
|
44
44
|
super().__init__()
|
|
45
|
+
self.layer_collection = None
|
|
45
46
|
self.setVisible(False)
|
|
46
47
|
self.setZValue(500)
|
|
47
48
|
self.setTransformationMode(Qt.SmoothTransformation)
|
|
@@ -68,31 +69,26 @@ class BrushPreviewItem(QGraphicsPixmapItem):
|
|
|
68
69
|
else:
|
|
69
70
|
raise Exception("Bitmas is neither 8 bit nor 16, but of type " + area.dtype)
|
|
70
71
|
|
|
71
|
-
def update(self,
|
|
72
|
+
def update(self, scene_pos, size):
|
|
72
73
|
try:
|
|
73
|
-
if
|
|
74
|
+
if self.layer_collection is None or self.number_of_layers() == 0 or size <= 0:
|
|
74
75
|
self.hide()
|
|
75
76
|
return
|
|
76
77
|
radius = size // 2
|
|
77
|
-
if isinstance(pos, QPointF):
|
|
78
|
-
scene_pos = pos
|
|
79
|
-
else:
|
|
80
|
-
cursor_pos = editor.image_viewer.mapFromGlobal(pos)
|
|
81
|
-
scene_pos = editor.image_viewer.mapToScene(cursor_pos)
|
|
82
78
|
x = int(scene_pos.x() - radius + 0.5)
|
|
83
79
|
y = int(scene_pos.y() - radius)
|
|
84
80
|
w = h = size
|
|
85
|
-
if
|
|
81
|
+
if not self.valid_current_layer_idx():
|
|
86
82
|
self.hide()
|
|
87
83
|
return
|
|
88
|
-
layer_area = self.get_layer_area(
|
|
89
|
-
master_area = self.get_layer_area(
|
|
84
|
+
layer_area = self.get_layer_area(self.current_layer(), x, y, w, h)
|
|
85
|
+
master_area = self.get_layer_area(self.master_layer(), x, y, w, h)
|
|
90
86
|
if layer_area is None or master_area is None:
|
|
91
87
|
self.hide()
|
|
92
88
|
return
|
|
93
|
-
height, width =
|
|
94
|
-
full_mask = create_brush_mask(size=size, hardness_percent=
|
|
95
|
-
opacity_percent=
|
|
89
|
+
height, width = self.current_layer().shape[:2]
|
|
90
|
+
full_mask = create_brush_mask(size=size, hardness_percent=self.brush.hardness,
|
|
91
|
+
opacity_percent=self.brush.opacity)[:, :, np.newaxis]
|
|
96
92
|
mask_x_start = max(0, -x) if x < 0 else 0
|
|
97
93
|
mask_y_start = max(0, -y) if y < 0 else 0
|
|
98
94
|
mask_x_end = size - (max(0, (x + w) - width)) if (x + w) > width else size
|