shinestacker 1.6.1__py3-none-any.whl → 1.8.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of shinestacker might be problematic. Click here for more details.
- shinestacker/_version.py +1 -1
- shinestacker/algorithms/corrections.py +26 -0
- shinestacker/algorithms/stack.py +9 -0
- shinestacker/algorithms/stack_framework.py +35 -16
- shinestacker/algorithms/utils.py +5 -1
- shinestacker/app/args_parser_opts.py +39 -0
- shinestacker/app/gui_utils.py +19 -2
- shinestacker/app/main.py +16 -27
- shinestacker/app/project.py +12 -23
- shinestacker/app/retouch.py +12 -25
- shinestacker/app/settings_dialog.py +46 -3
- shinestacker/config/settings.py +4 -1
- shinestacker/core/core_utils.py +2 -2
- shinestacker/core/framework.py +7 -2
- shinestacker/core/logging.py +2 -2
- shinestacker/gui/action_config_dialog.py +72 -45
- shinestacker/gui/gui_run.py +1 -2
- shinestacker/gui/ico/shinestacker.icns +0 -0
- shinestacker/gui/img/dark/close-round-line-icon.png +0 -0
- shinestacker/gui/img/dark/forward-button-icon.png +0 -0
- shinestacker/gui/img/dark/play-button-round-icon.png +0 -0
- shinestacker/gui/img/dark/plus-round-line-icon.png +0 -0
- shinestacker/gui/img/dark/shinestacker_bkg.png +0 -0
- shinestacker/gui/img/light/shinestacker_bkg.png +0 -0
- shinestacker/gui/main_window.py +20 -7
- shinestacker/gui/menu_manager.py +18 -7
- shinestacker/gui/new_project.py +0 -2
- shinestacker/gui/tab_widget.py +16 -10
- shinestacker/retouch/adjustments.py +98 -0
- shinestacker/retouch/base_filter.py +62 -7
- shinestacker/retouch/denoise_filter.py +1 -1
- shinestacker/retouch/image_editor_ui.py +26 -4
- shinestacker/retouch/unsharp_mask_filter.py +13 -28
- shinestacker/retouch/vignetting_filter.py +1 -1
- {shinestacker-1.6.1.dist-info → shinestacker-1.8.0.dist-info}/METADATA +4 -4
- {shinestacker-1.6.1.dist-info → shinestacker-1.8.0.dist-info}/RECORD +44 -37
- shinestacker/gui/ico/focus_stack_bkg.png +0 -0
- /shinestacker/gui/img/{close-round-line-icon.png → light/close-round-line-icon.png} +0 -0
- /shinestacker/gui/img/{forward-button-icon.png → light/forward-button-icon.png} +0 -0
- /shinestacker/gui/img/{play-button-round-icon.png → light/play-button-round-icon.png} +0 -0
- /shinestacker/gui/img/{plus-round-line-icon.png → light/plus-round-line-icon.png} +0 -0
- {shinestacker-1.6.1.dist-info → shinestacker-1.8.0.dist-info}/WHEEL +0 -0
- {shinestacker-1.6.1.dist-info → shinestacker-1.8.0.dist-info}/entry_points.txt +0 -0
- {shinestacker-1.6.1.dist-info → shinestacker-1.8.0.dist-info}/licenses/LICENSE +0 -0
- {shinestacker-1.6.1.dist-info → shinestacker-1.8.0.dist-info}/top_level.txt +0 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# pylint: disable=C0114, C0115, C0116, E0611, R0913, R0917, R0915, R0912
|
|
2
|
-
# pylint: disable=E0606, W0718, R1702, W0102, W0221, R0914, C0302
|
|
2
|
+
# pylint: disable=E0606, W0718, R1702, W0102, W0221, R0914, C0302, R0903
|
|
3
3
|
import os
|
|
4
4
|
import traceback
|
|
5
5
|
from PySide6.QtCore import QTimer
|
|
@@ -214,7 +214,7 @@ class FocusStackBaseConfigurator(DefaultActionConfigurator):
|
|
|
214
214
|
expert=True,
|
|
215
215
|
placeholder='relative to working path')
|
|
216
216
|
self.add_field_to_layout(
|
|
217
|
-
layout, 'scratch_output_dir', FIELD_BOOL, 'Scratch output
|
|
217
|
+
layout, 'scratch_output_dir', FIELD_BOOL, 'Scratch output folder before run',
|
|
218
218
|
required=False, default=True)
|
|
219
219
|
|
|
220
220
|
def create_algorithm_tab(self, layout):
|
|
@@ -366,6 +366,14 @@ class FocusStackBunchConfigurator(FocusStackBaseConfigurator):
|
|
|
366
366
|
self.add_field_to_layout(
|
|
367
367
|
self.general_tab_layout, 'overlap', FIELD_INT, 'Overlapping frames', required=False,
|
|
368
368
|
default=constants.DEFAULT_OVERLAP, min_val=0, max_val=100)
|
|
369
|
+
self.add_field_to_layout(
|
|
370
|
+
self.general_tab_layout, 'scratch_output_dir', FIELD_BOOL,
|
|
371
|
+
'Scratch output folder before run',
|
|
372
|
+
required=False, default=True)
|
|
373
|
+
self.add_field_to_layout(
|
|
374
|
+
self.general_tab_layout, 'delete_output_at_end', FIELD_BOOL,
|
|
375
|
+
'Delete output at end of job',
|
|
376
|
+
required=False, default=False)
|
|
369
377
|
self.add_field_to_layout(
|
|
370
378
|
self.general_tab_layout, 'plot_stack', FIELD_BOOL, 'Plot stack', required=False,
|
|
371
379
|
default=constants.DEFAULT_PLOT_STACK_BUNCH)
|
|
@@ -391,7 +399,7 @@ class MultiLayerConfigurator(DefaultActionConfigurator):
|
|
|
391
399
|
expert=True,
|
|
392
400
|
placeholder='relative to working path')
|
|
393
401
|
self.add_field(
|
|
394
|
-
'scratch_output_dir', FIELD_BOOL, 'Scratch output
|
|
402
|
+
'scratch_output_dir', FIELD_BOOL, 'Scratch output folder before run',
|
|
395
403
|
required=False, default=True)
|
|
396
404
|
self.add_field(
|
|
397
405
|
'reverse_order', FIELD_BOOL, 'Reverse file order', required=False,
|
|
@@ -413,8 +421,11 @@ class CombinedActionsConfigurator(DefaultActionConfigurator):
|
|
|
413
421
|
expert=True,
|
|
414
422
|
placeholder='relative to working path')
|
|
415
423
|
self.add_field(
|
|
416
|
-
'scratch_output_dir', FIELD_BOOL, 'Scratch output
|
|
424
|
+
'scratch_output_dir', FIELD_BOOL, 'Scratch output folder before run',
|
|
417
425
|
required=False, default=True)
|
|
426
|
+
self.add_field(
|
|
427
|
+
'delete_output_at_end', FIELD_BOOL, 'Delete output at end of job',
|
|
428
|
+
required=False, default=False)
|
|
418
429
|
self.add_field(
|
|
419
430
|
'plot_path', FIELD_REL_PATH, 'Plots path', required=False,
|
|
420
431
|
expert=True,
|
|
@@ -490,22 +501,22 @@ class SubsampleActionConfigurator(DefaultActionConfigurator):
|
|
|
490
501
|
self.subsample_field.currentText() not in constants.FIELD_SUBSAMPLE_OPTIONS[:2])
|
|
491
502
|
|
|
492
503
|
|
|
493
|
-
class
|
|
494
|
-
BORDER_MODE_OPTIONS = ['Constant', 'Replicate', 'Replicate and blur']
|
|
495
|
-
TRANSFORM_OPTIONS = ['Rigid', 'Homography']
|
|
496
|
-
METHOD_OPTIONS = ['Random Sample Consensus (RANSAC)', 'Least Median (LMEDS)']
|
|
504
|
+
class AlignFramesConfigBase:
|
|
497
505
|
MATCHING_METHOD_OPTIONS = ['K-nearest neighbors', 'Hamming distance']
|
|
498
|
-
|
|
506
|
+
DETECTOR_DESCRIPTOR_TOOLTIPS = {
|
|
507
|
+
'detector':
|
|
508
|
+
"SIFT: Requires SIFT descriptor and K-NN matching\n"
|
|
509
|
+
"ORB/AKAZE: Work best with Hamming distance",
|
|
510
|
+
'descriptor':
|
|
511
|
+
"SIFT: Requires K-NN matching\n"
|
|
512
|
+
"ORB/AKAZE: Require Hamming distance with ORB/AKAZE detectors",
|
|
513
|
+
'match_method':
|
|
514
|
+
"Automatically selected based on detector/descriptor combination"
|
|
499
515
|
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
def __init__(self):
|
|
503
519
|
self.info_label = None
|
|
504
|
-
self.detector_field = None
|
|
505
|
-
self.descriptor_field = None
|
|
506
|
-
self.matching_method_field = None
|
|
507
|
-
self.tab_widget = None
|
|
508
|
-
self.current_tab_layout = None
|
|
509
520
|
|
|
510
521
|
def show_info(self, message, timeout=3000):
|
|
511
522
|
self.info_label.setText(message)
|
|
@@ -514,32 +525,50 @@ class AlignFramesConfigurator(SubsampleActionConfigurator):
|
|
|
514
525
|
timer.timeout.connect(lambda: self.info_label.setText(''))
|
|
515
526
|
timer.start(timeout)
|
|
516
527
|
|
|
517
|
-
def change_match_config(
|
|
518
|
-
|
|
519
|
-
|
|
528
|
+
def change_match_config(
|
|
529
|
+
self, detector_field, descriptor_field, matching_method_field, show_info):
|
|
530
|
+
detector = detector_field.currentText()
|
|
531
|
+
descriptor = descriptor_field.currentText()
|
|
520
532
|
match_method = dict(
|
|
521
533
|
zip(self.MATCHING_METHOD_OPTIONS,
|
|
522
|
-
constants.VALID_MATCHING_METHODS))[
|
|
534
|
+
constants.VALID_MATCHING_METHODS))[matching_method_field.currentText()]
|
|
523
535
|
try:
|
|
524
536
|
validate_align_config(detector, descriptor, match_method)
|
|
525
537
|
except Exception as e:
|
|
526
|
-
|
|
538
|
+
show_info(str(e))
|
|
527
539
|
if descriptor == constants.DETECTOR_SIFT and \
|
|
528
540
|
match_method == constants.MATCHING_NORM_HAMMING:
|
|
529
|
-
|
|
541
|
+
matching_method_field.setCurrentText(self.MATCHING_METHOD_OPTIONS[0])
|
|
530
542
|
if detector == constants.DETECTOR_ORB and descriptor == constants.DESCRIPTOR_AKAZE and \
|
|
531
543
|
match_method == constants.MATCHING_NORM_HAMMING:
|
|
532
|
-
|
|
544
|
+
matching_method_field.setCurrentText(constants.MATCHING_NORM_HAMMING)
|
|
533
545
|
if detector == constants.DETECTOR_BRISK and descriptor == constants.DESCRIPTOR_AKAZE:
|
|
534
|
-
|
|
546
|
+
descriptor_field.setCurrentText('BRISK')
|
|
535
547
|
if detector == constants.DETECTOR_SURF and descriptor == constants.DESCRIPTOR_AKAZE:
|
|
536
|
-
|
|
548
|
+
descriptor_field.setCurrentText('SIFT')
|
|
537
549
|
if detector == constants.DETECTOR_SIFT and descriptor != constants.DESCRIPTOR_SIFT:
|
|
538
|
-
|
|
550
|
+
descriptor_field.setCurrentText('SIFT')
|
|
539
551
|
if detector in constants.NOKNN_METHODS['detectors'] and \
|
|
540
552
|
descriptor in constants.NOKNN_METHODS['descriptors']:
|
|
541
553
|
if match_method == constants.MATCHING_KNN:
|
|
542
|
-
|
|
554
|
+
matching_method_field.setCurrentText(self.MATCHING_METHOD_OPTIONS[1])
|
|
555
|
+
|
|
556
|
+
|
|
557
|
+
class AlignFramesConfigurator(SubsampleActionConfigurator, AlignFramesConfigBase):
|
|
558
|
+
BORDER_MODE_OPTIONS = ['Constant', 'Replicate', 'Replicate and blur']
|
|
559
|
+
TRANSFORM_OPTIONS = ['Rigid', 'Homography']
|
|
560
|
+
METHOD_OPTIONS = ['Random Sample Consensus (RANSAC)', 'Least Median (LMEDS)']
|
|
561
|
+
MODE_OPTIONS = ['Auto', 'Sequential', 'Parallel']
|
|
562
|
+
|
|
563
|
+
def __init__(self, expert, current_wd):
|
|
564
|
+
SubsampleActionConfigurator.__init__(self, expert, current_wd)
|
|
565
|
+
AlignFramesConfigBase.__init__(self)
|
|
566
|
+
self.matching_method_field = None
|
|
567
|
+
self.detector_field = None
|
|
568
|
+
self.descriptor_field = None
|
|
569
|
+
self.matching_method_field = None
|
|
570
|
+
self.tab_widget = None
|
|
571
|
+
self.current_tab_layout = None
|
|
543
572
|
|
|
544
573
|
def create_form(self, layout, action):
|
|
545
574
|
super().create_form(layout, action)
|
|
@@ -557,23 +586,23 @@ class AlignFramesConfigurator(SubsampleActionConfigurator):
|
|
|
557
586
|
self.create_miscellanea_tab(misc_layout)
|
|
558
587
|
|
|
559
588
|
def create_feature_tab(self, layout):
|
|
589
|
+
|
|
590
|
+
def change_match_config():
|
|
591
|
+
self.change_match_config(
|
|
592
|
+
self.detector_field, self.descriptor_field,
|
|
593
|
+
self. matching_method_field, self.show_info)
|
|
594
|
+
|
|
560
595
|
self.add_bold_label_to_layout(layout, "Feature identification:")
|
|
561
596
|
self.detector_field = self.add_field_to_layout(
|
|
562
597
|
layout, 'detector', FIELD_COMBO, 'Detector', required=False,
|
|
563
|
-
options=constants.VALID_DETECTORS, default=
|
|
598
|
+
options=constants.VALID_DETECTORS, default=AppConfig.get('detector'))
|
|
564
599
|
self.descriptor_field = self.add_field_to_layout(
|
|
565
600
|
layout, 'descriptor', FIELD_COMBO, 'Descriptor', required=False,
|
|
566
|
-
options=constants.VALID_DESCRIPTORS, default=
|
|
567
|
-
self.detector_field.setToolTip(
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
)
|
|
571
|
-
self.descriptor_field.setToolTip(
|
|
572
|
-
"SIFT: Requires K-NN matching\n"
|
|
573
|
-
"ORB/AKAZE: Require Hamming distance with ORB/AKAZE detectors"
|
|
574
|
-
)
|
|
575
|
-
self.detector_field.currentIndexChanged.connect(self.change_match_config)
|
|
576
|
-
self.descriptor_field.currentIndexChanged.connect(self.change_match_config)
|
|
601
|
+
options=constants.VALID_DESCRIPTORS, default=AppConfig.get('descriptor'))
|
|
602
|
+
self.detector_field.setToolTip(self.DETECTOR_DESCRIPTOR_TOOLTIPS['detector'])
|
|
603
|
+
self.descriptor_field.setToolTip(self.DETECTOR_DESCRIPTOR_TOOLTIPS['descriptor'])
|
|
604
|
+
self.detector_field.currentIndexChanged.connect(change_match_config)
|
|
605
|
+
self.descriptor_field.currentIndexChanged.connect(change_match_config)
|
|
577
606
|
self.info_label = QLabel()
|
|
578
607
|
self.info_label.setStyleSheet("color: orange; font-style: italic;")
|
|
579
608
|
layout.addRow(self.info_label)
|
|
@@ -581,11 +610,9 @@ class AlignFramesConfigurator(SubsampleActionConfigurator):
|
|
|
581
610
|
self.matching_method_field = self.add_field_to_layout(
|
|
582
611
|
layout, 'match_method', FIELD_COMBO, 'Match method', required=False,
|
|
583
612
|
options=self.MATCHING_METHOD_OPTIONS, values=constants.VALID_MATCHING_METHODS,
|
|
584
|
-
default=
|
|
585
|
-
self.matching_method_field.setToolTip(
|
|
586
|
-
|
|
587
|
-
)
|
|
588
|
-
self.matching_method_field.currentIndexChanged.connect(self.change_match_config)
|
|
613
|
+
default=AppConfig.get('match_method'))
|
|
614
|
+
self.matching_method_field.setToolTip(self.DETECTOR_DESCRIPTOR_TOOLTIPS['match_method'])
|
|
615
|
+
self.matching_method_field.currentIndexChanged.connect(change_match_config)
|
|
589
616
|
self.add_field_to_layout(
|
|
590
617
|
layout, 'flann_idx_kdtree', FIELD_INT, 'Flann idx kdtree', required=False,
|
|
591
618
|
expert=True,
|
shinestacker/gui/gui_run.py
CHANGED
|
@@ -215,8 +215,7 @@ class RunWindow(QTextEditLogger):
|
|
|
215
215
|
raise RuntimeError(f"Can't visualize file type {os.path.splitext(path)[1]}.")
|
|
216
216
|
self.image_views.append(image_view)
|
|
217
217
|
self.image_layout.addWidget(image_view)
|
|
218
|
-
|
|
219
|
-
needed_width = max_width + 20
|
|
218
|
+
needed_width = gui_constants.GUI_IMG_WIDTH + 20
|
|
220
219
|
self.right_area.setFixedWidth(needed_width)
|
|
221
220
|
self.image_area_widget.setFixedWidth(needed_width)
|
|
222
221
|
self.right_area.updateGeometry()
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
shinestacker/gui/main_window.py
CHANGED
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
import os
|
|
4
4
|
import subprocess
|
|
5
5
|
from PySide6.QtCore import Qt
|
|
6
|
-
from PySide6.QtGui import QGuiApplication, QAction,
|
|
6
|
+
from PySide6.QtGui import QGuiApplication, QAction, QPalette
|
|
7
7
|
from PySide6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QLabel, QMessageBox,
|
|
8
|
-
QSplitter, QToolBar, QMenu, QMainWindow)
|
|
8
|
+
QSplitter, QToolBar, QMenu, QMainWindow, QApplication)
|
|
9
9
|
from .. config.constants import constants
|
|
10
10
|
from .. config.app_config import AppConfig
|
|
11
11
|
from .. core.core_utils import running_under_windows, running_under_macos
|
|
@@ -82,7 +82,9 @@ class MainWindow(QMainWindow, LogManager):
|
|
|
82
82
|
"Run Job": self.run_job,
|
|
83
83
|
"Run All Jobs": self.run_all_jobs,
|
|
84
84
|
}
|
|
85
|
-
|
|
85
|
+
dark_theme = self.is_dark_theme()
|
|
86
|
+
self.menu_manager = MenuManager(
|
|
87
|
+
self.menuBar(), actions, self.project_editor, dark_theme, self)
|
|
86
88
|
self.script_dir = os.path.dirname(__file__)
|
|
87
89
|
self._windows = []
|
|
88
90
|
self._workers = []
|
|
@@ -107,7 +109,7 @@ class MainWindow(QMainWindow, LogManager):
|
|
|
107
109
|
top_widget = QWidget()
|
|
108
110
|
top_widget.setLayout(h_layout)
|
|
109
111
|
h_splitter.addWidget(top_widget)
|
|
110
|
-
self.tab_widget = TabWidgetWithPlaceholder()
|
|
112
|
+
self.tab_widget = TabWidgetWithPlaceholder(dark_theme)
|
|
111
113
|
self.tab_widget.resize(1000, 500)
|
|
112
114
|
h_splitter.addWidget(self.tab_widget)
|
|
113
115
|
self.job_list().currentRowChanged.connect(self.project_editor.on_job_selected)
|
|
@@ -128,6 +130,7 @@ class MainWindow(QMainWindow, LogManager):
|
|
|
128
130
|
layout.addWidget(h_splitter)
|
|
129
131
|
self.central_widget.setLayout(layout)
|
|
130
132
|
self.update_title()
|
|
133
|
+
QApplication.instance().paletteChanged.connect(self.on_theme_changed)
|
|
131
134
|
|
|
132
135
|
def handle_modified(modified):
|
|
133
136
|
self.save_actions_set_enabled(modified)
|
|
@@ -367,9 +370,6 @@ class MainWindow(QMainWindow, LogManager):
|
|
|
367
370
|
menu.exec(event.globalPos())
|
|
368
371
|
# pylint: enable=C0103
|
|
369
372
|
|
|
370
|
-
def get_icon(self, icon):
|
|
371
|
-
return QIcon(os.path.join(self.script_dir, f"img/{icon}.png"))
|
|
372
|
-
|
|
373
373
|
def get_retouch_path(self, job):
|
|
374
374
|
frames_path = [get_action_output_path(action)[0]
|
|
375
375
|
for action in job.sub_actions
|
|
@@ -581,3 +581,16 @@ class MainWindow(QMainWindow, LogManager):
|
|
|
581
581
|
for action in self.findChildren(QAction):
|
|
582
582
|
if action.property("requires_file"):
|
|
583
583
|
action.setEnabled(enabled)
|
|
584
|
+
|
|
585
|
+
def is_dark_theme(self):
|
|
586
|
+
palette = QApplication.palette()
|
|
587
|
+
window_color = palette.color(QPalette.Window)
|
|
588
|
+
brightness = (window_color.red() * 0.299 +
|
|
589
|
+
window_color.green() * 0.587 +
|
|
590
|
+
window_color.blue() * 0.114)
|
|
591
|
+
return brightness < 128
|
|
592
|
+
|
|
593
|
+
def on_theme_changed(self):
|
|
594
|
+
dark_theme = self.is_dark_theme()
|
|
595
|
+
self.menu_manager.change_theme(dark_theme)
|
|
596
|
+
self.tab_widget.change_theme(dark_theme)
|
shinestacker/gui/menu_manager.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# pylint: disable=C0114, C0115, C0116, R0904, E0611, R0902, W0201
|
|
1
|
+
# pylint: disable=C0114, C0115, C0116, R0904, E0611, R0902, W0201, R0913, R0917
|
|
2
2
|
import os
|
|
3
3
|
from functools import partial
|
|
4
4
|
from PySide6.QtCore import Signal, QObject
|
|
@@ -12,11 +12,12 @@ from .recent_file_manager import RecentFileManager
|
|
|
12
12
|
class MenuManager(QObject):
|
|
13
13
|
open_file_requested = Signal(str)
|
|
14
14
|
|
|
15
|
-
def __init__(self, menubar, actions, project_editor, parent):
|
|
15
|
+
def __init__(self, menubar, actions, project_editor, dark_theme, parent):
|
|
16
16
|
super().__init__(parent)
|
|
17
17
|
self.script_dir = os.path.dirname(__file__)
|
|
18
18
|
self._recent_file_manager = RecentFileManager("shinestacker-recent-project-files.txt")
|
|
19
19
|
self.project_editor = project_editor
|
|
20
|
+
self.dark_theme = dark_theme
|
|
20
21
|
self.parent = parent
|
|
21
22
|
self.menubar = menubar
|
|
22
23
|
self.actions = actions
|
|
@@ -58,8 +59,9 @@ class MenuManager(QObject):
|
|
|
58
59
|
"Run All Jobs": "Run all jobs",
|
|
59
60
|
}
|
|
60
61
|
|
|
61
|
-
def get_icon(self,
|
|
62
|
-
|
|
62
|
+
def get_icon(self, icon_name):
|
|
63
|
+
icon_dir = 'dark' if self.dark_theme else 'light'
|
|
64
|
+
return QIcon(os.path.join(self.script_dir, f"img/{icon_dir}/{icon_name}.png"))
|
|
63
65
|
|
|
64
66
|
def action(self, name, requires_file=False):
|
|
65
67
|
action = QAction(name, self.parent)
|
|
@@ -68,9 +70,11 @@ class MenuManager(QObject):
|
|
|
68
70
|
shortcut = self.shortcuts.get(name, '')
|
|
69
71
|
if shortcut:
|
|
70
72
|
action.setShortcut(shortcut)
|
|
71
|
-
|
|
72
|
-
if
|
|
73
|
-
action.setIcon(self.get_icon(
|
|
73
|
+
icon_name = self.icons.get(name, '')
|
|
74
|
+
if icon_name:
|
|
75
|
+
action.setIcon(self.get_icon(icon_name))
|
|
76
|
+
action.setProperty('theme_dependent', True)
|
|
77
|
+
action.setProperty('base_icon_name', icon_name)
|
|
74
78
|
tooltip = self.tooltips.get(name, '')
|
|
75
79
|
if tooltip:
|
|
76
80
|
action.setToolTip(tooltip)
|
|
@@ -79,6 +83,13 @@ class MenuManager(QObject):
|
|
|
79
83
|
action.triggered.connect(action_fun)
|
|
80
84
|
return action
|
|
81
85
|
|
|
86
|
+
def change_theme(self, dark_theme):
|
|
87
|
+
self.dark_theme = dark_theme
|
|
88
|
+
for action in self.parent.findChildren(QAction):
|
|
89
|
+
if action.property("theme_dependent"):
|
|
90
|
+
base_name = action.property("base_icon_name")
|
|
91
|
+
action.setIcon(self.get_icon(base_name))
|
|
92
|
+
|
|
82
93
|
def update_recent_files(self):
|
|
83
94
|
self.recent_files_menu.clear()
|
|
84
95
|
recent_files = self._recent_file_manager.get_files_with_display_names()
|
shinestacker/gui/new_project.py
CHANGED
|
@@ -169,13 +169,11 @@ class NewProjectDialog(BaseFormDialog):
|
|
|
169
169
|
border-radius: 5px;
|
|
170
170
|
margin-top: 10px;
|
|
171
171
|
padding-top: 15px;
|
|
172
|
-
background-color: #f8f8f8;
|
|
173
172
|
}
|
|
174
173
|
QGroupBox::title {
|
|
175
174
|
subcontrol-origin: margin;
|
|
176
175
|
left: 10px;
|
|
177
176
|
padding: 0 5px 0 5px;
|
|
178
|
-
background-color: #f8f8f8;
|
|
179
177
|
}
|
|
180
178
|
"""
|
|
181
179
|
for group in [step1_group, step2_group, step3_group, step4_group]:
|
shinestacker/gui/tab_widget.py
CHANGED
|
@@ -3,15 +3,16 @@ import os
|
|
|
3
3
|
from PySide6.QtCore import Qt, Signal
|
|
4
4
|
from PySide6.QtGui import QPixmap
|
|
5
5
|
from PySide6.QtWidgets import QWidget, QVBoxLayout, QTabWidget, QLabel, QStackedWidget
|
|
6
|
-
from .. core.core_utils import get_app_base_path
|
|
7
6
|
|
|
8
7
|
|
|
9
8
|
class TabWidgetWithPlaceholder(QWidget):
|
|
10
9
|
currentChanged = Signal(int)
|
|
11
10
|
tabCloseRequested = Signal(int)
|
|
12
11
|
|
|
13
|
-
def __init__(self, parent=None):
|
|
12
|
+
def __init__(self, dark_theme, parent=None):
|
|
14
13
|
super().__init__(parent)
|
|
14
|
+
self.script_dir = os.path.dirname(__file__)
|
|
15
|
+
self.dark_theme = dark_theme
|
|
15
16
|
self.main_layout = QVBoxLayout(self)
|
|
16
17
|
self.main_layout.setContentsMargins(0, 0, 0, 0)
|
|
17
18
|
self.stacked_widget = QStackedWidget()
|
|
@@ -20,10 +21,15 @@ class TabWidgetWithPlaceholder(QWidget):
|
|
|
20
21
|
self.stacked_widget.addWidget(self.tab_widget)
|
|
21
22
|
self.placeholder = QLabel()
|
|
22
23
|
self.placeholder.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
self.set_bkg_icon()
|
|
25
|
+
self.stacked_widget.addWidget(self.placeholder)
|
|
26
|
+
self.tab_widget.currentChanged.connect(self._on_current_changed)
|
|
27
|
+
self.tab_widget.tabCloseRequested.connect(self._on_tab_close_requested)
|
|
28
|
+
self.update_placeholder_visibility()
|
|
29
|
+
|
|
30
|
+
def set_bkg_icon(self):
|
|
31
|
+
icon_dir = 'dark' if self.dark_theme else 'light'
|
|
32
|
+
icon_path = os.path.join(self.script_dir, f"img/{icon_dir}/shinestacker_bkg.png")
|
|
27
33
|
if os.path.exists(icon_path):
|
|
28
34
|
pixmap = QPixmap(icon_path)
|
|
29
35
|
pixmap = pixmap.scaled(250, 250, Qt.AspectRatioMode.KeepAspectRatio,
|
|
@@ -31,10 +37,10 @@ class TabWidgetWithPlaceholder(QWidget):
|
|
|
31
37
|
self.placeholder.setPixmap(pixmap)
|
|
32
38
|
else:
|
|
33
39
|
self.placeholder.setText("Run logs will appear here.")
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
self.
|
|
37
|
-
self.
|
|
40
|
+
|
|
41
|
+
def change_theme(self, dark_theme):
|
|
42
|
+
self.dark_theme = dark_theme
|
|
43
|
+
self.set_bkg_icon()
|
|
38
44
|
|
|
39
45
|
def _on_current_changed(self, index):
|
|
40
46
|
self.currentChanged.emit(index)
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# pylint: disable=C0114, C0115, C0116, E0611, W0221, R0913, R0917, R0902, R0914, E1101
|
|
2
|
+
from abc import abstractmethod
|
|
3
|
+
import math
|
|
4
|
+
import cv2
|
|
5
|
+
from .base_filter import BaseFilter
|
|
6
|
+
from .. algorithms.utils import bgr_to_hls, hls_to_bgr
|
|
7
|
+
from .. algorithms.corrections import gamma_correction, contrast_correction
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class GammaSCurveFilter(BaseFilter):
|
|
11
|
+
def __init__(
|
|
12
|
+
self, name, parent, image_viewer, layer_collection, undo_manager,
|
|
13
|
+
window_title, gamma_label, scurve_label):
|
|
14
|
+
super().__init__(name, parent, image_viewer, layer_collection, undo_manager,
|
|
15
|
+
preview_at_startup=True)
|
|
16
|
+
self.window_title = window_title
|
|
17
|
+
self.gamma_label = gamma_label
|
|
18
|
+
self.scurve_label = scurve_label
|
|
19
|
+
self.min_gamma = -1
|
|
20
|
+
self.max_gamma = +1
|
|
21
|
+
self.initial_gamma = 0
|
|
22
|
+
self.min_scurve = -1
|
|
23
|
+
self.max_scurve = 1
|
|
24
|
+
self.initial_scurve = 0
|
|
25
|
+
self.lumi_slider = None
|
|
26
|
+
self.contrast_slider = None
|
|
27
|
+
|
|
28
|
+
@abstractmethod
|
|
29
|
+
def apply(self, image, *params):
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
def setup_ui(self, dlg, layout, do_preview, restore_original, **kwargs):
|
|
33
|
+
dlg.setWindowTitle(self.window_title)
|
|
34
|
+
dlg.setMinimumWidth(600)
|
|
35
|
+
params = {
|
|
36
|
+
self.gamma_label: (self.min_gamma, self.max_gamma, self.initial_gamma, "{:.1%}"),
|
|
37
|
+
self.scurve_label: (self.min_scurve, self.max_scurve, self.initial_scurve, "{:.1%}"),
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
def set_slider(name, slider):
|
|
41
|
+
if name == self.gamma_label:
|
|
42
|
+
self.lumi_slider = slider
|
|
43
|
+
elif name == self.scurve_label:
|
|
44
|
+
self.contrast_slider = slider
|
|
45
|
+
|
|
46
|
+
value_labels = self.create_sliders(params, dlg, layout, set_slider)
|
|
47
|
+
|
|
48
|
+
def update_value(name, slider_value, min_val, max_val, fmt):
|
|
49
|
+
value = self.value_from_slider(slider_value, min_val, max_val)
|
|
50
|
+
value_labels[name].setText(fmt.format(value))
|
|
51
|
+
if self.preview_check.isChecked():
|
|
52
|
+
self.preview_timer.start()
|
|
53
|
+
|
|
54
|
+
self.lumi_slider.valueChanged.connect(
|
|
55
|
+
lambda v: update_value(
|
|
56
|
+
self.gamma_label, v, self.min_gamma,
|
|
57
|
+
self.max_gamma, params[self.gamma_label][3]))
|
|
58
|
+
self.contrast_slider.valueChanged.connect(
|
|
59
|
+
lambda v: update_value(
|
|
60
|
+
self.scurve_label, v, self.min_scurve,
|
|
61
|
+
self.max_scurve, params[self.scurve_label][3]))
|
|
62
|
+
self.set_timer(do_preview, restore_original, dlg)
|
|
63
|
+
|
|
64
|
+
def get_params(self):
|
|
65
|
+
return (
|
|
66
|
+
self.value_from_slider(
|
|
67
|
+
self.lumi_slider.value(), self.min_gamma, self.max_gamma),
|
|
68
|
+
self.value_from_slider(
|
|
69
|
+
self.contrast_slider.value(), self.min_scurve, self.max_scurve)
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class LumiContrastFilter(GammaSCurveFilter):
|
|
74
|
+
def __init__(self, name, parent, image_viewer, layer_collection, undo_manager):
|
|
75
|
+
super().__init__(
|
|
76
|
+
name, parent, image_viewer, layer_collection, undo_manager,
|
|
77
|
+
"Luminosity, Contrast", "Luminosity", "Constrat")
|
|
78
|
+
|
|
79
|
+
def apply(self, image, luminosity, contrast):
|
|
80
|
+
img_corr = contrast_correction(image, 0.5 * contrast)
|
|
81
|
+
img_corr = gamma_correction(img_corr, math.exp(0.5 * luminosity))
|
|
82
|
+
return img_corr
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class SaturationVibranceFilter(GammaSCurveFilter):
|
|
86
|
+
def __init__(self, name, parent, image_viewer, layer_collection, undo_manager):
|
|
87
|
+
super().__init__(
|
|
88
|
+
name, parent, image_viewer, layer_collection, undo_manager,
|
|
89
|
+
"Saturation, Vibrance", "Saturation", "Vibrance")
|
|
90
|
+
|
|
91
|
+
def apply(self, image, stauration, vibrance):
|
|
92
|
+
img_corr = bgr_to_hls(image)
|
|
93
|
+
h, l, s = cv2.split(img_corr)
|
|
94
|
+
s_corr = contrast_correction(s, - vibrance)
|
|
95
|
+
s_corr = gamma_correction(s_corr, math.exp(0.5 * stauration))
|
|
96
|
+
img_corr = cv2.merge([h, l, s_corr])
|
|
97
|
+
img_corr = hls_to_bgr(img_corr)
|
|
98
|
+
return img_corr
|
|
@@ -3,6 +3,7 @@ import traceback
|
|
|
3
3
|
from abc import abstractmethod
|
|
4
4
|
import numpy as np
|
|
5
5
|
from PySide6.QtCore import Qt, QThread, QTimer, QObject, Signal
|
|
6
|
+
from PySide6.QtGui import QFontMetrics
|
|
6
7
|
from PySide6.QtWidgets import (
|
|
7
8
|
QHBoxLayout, QLabel, QSlider, QDialog, QVBoxLayout, QCheckBox, QDialogButtonBox)
|
|
8
9
|
from .layer_collection import LayerCollectionHandler
|
|
@@ -27,6 +28,7 @@ class BaseFilter(QObject, LayerCollectionHandler):
|
|
|
27
28
|
self.preview_check = None
|
|
28
29
|
self.button_box = None
|
|
29
30
|
self.preview_timer = None
|
|
31
|
+
self.max_range = 500
|
|
30
32
|
|
|
31
33
|
@abstractmethod
|
|
32
34
|
def setup_ui(self, dlg, layout, do_preview, restore_original, **kwargs):
|
|
@@ -40,6 +42,47 @@ class BaseFilter(QObject, LayerCollectionHandler):
|
|
|
40
42
|
def apply(self, image, *params):
|
|
41
43
|
pass
|
|
42
44
|
|
|
45
|
+
def slider_from_value(self, value, min_val, max_val):
|
|
46
|
+
return (value - min_val) / (max_val - min_val) * self.max_range
|
|
47
|
+
|
|
48
|
+
def value_from_slider(self, slider_value, min_val, max_val):
|
|
49
|
+
return min_val + (max_val - min_val) * float(slider_value) / self.max_range
|
|
50
|
+
|
|
51
|
+
def create_sliders(self, params, dlg, layout, set_slider):
|
|
52
|
+
value_labels = {}
|
|
53
|
+
font_metrics = QFontMetrics(dlg.font())
|
|
54
|
+
max_name_width = 0
|
|
55
|
+
max_value_width = 0
|
|
56
|
+
for name, (min_val, max_val, init_val, fmt) in params.items():
|
|
57
|
+
name_width = font_metrics.horizontalAdvance(f"{name}:")
|
|
58
|
+
max_name_width = max(max_name_width, name_width)
|
|
59
|
+
sample_values = [min_val, max_val, init_val]
|
|
60
|
+
for val in sample_values:
|
|
61
|
+
value_width = font_metrics.horizontalAdvance(fmt.format(val))
|
|
62
|
+
max_value_width = max(max_value_width, value_width)
|
|
63
|
+
max_name_width += 10
|
|
64
|
+
max_value_width += 10
|
|
65
|
+
for name, (min_val, max_val, init_val, fmt) in params.items():
|
|
66
|
+
param_layout = QHBoxLayout()
|
|
67
|
+
name_label = QLabel(f"{name}:")
|
|
68
|
+
name_label.setFixedWidth(max_name_width)
|
|
69
|
+
name_label.setAlignment(Qt.AlignRight | Qt.AlignVCenter)
|
|
70
|
+
param_layout.addWidget(name_label)
|
|
71
|
+
slider = QSlider(Qt.Horizontal)
|
|
72
|
+
slider.setRange(0, self.max_range)
|
|
73
|
+
slider.setValue(self.slider_from_value(init_val, min_val, max_val))
|
|
74
|
+
param_layout.addWidget(slider, 1)
|
|
75
|
+
value_label = QLabel(fmt.format(init_val))
|
|
76
|
+
value_label.setFixedWidth(max_value_width)
|
|
77
|
+
value_label.setAlignment(Qt.AlignLeft | Qt.AlignVCenter)
|
|
78
|
+
param_layout.addWidget(value_label)
|
|
79
|
+
layout.addLayout(param_layout)
|
|
80
|
+
set_slider(name, slider)
|
|
81
|
+
value_labels[name] = value_label
|
|
82
|
+
self.create_base_widgets(
|
|
83
|
+
layout, QDialogButtonBox.Ok | QDialogButtonBox.Cancel, 200, dlg)
|
|
84
|
+
return value_labels
|
|
85
|
+
|
|
43
86
|
def connect_signals(self, update_master_thumbnail, mark_as_modified, filter_gui_set_enabled):
|
|
44
87
|
self.update_master_thumbnail_requested.connect(update_master_thumbnail)
|
|
45
88
|
self.mark_as_modified_requested.connect(mark_as_modified)
|
|
@@ -191,6 +234,13 @@ class BaseFilter(QObject, LayerCollectionHandler):
|
|
|
191
234
|
self.preview_timer.setSingleShot(True)
|
|
192
235
|
self.preview_timer.setInterval(preview_latency)
|
|
193
236
|
|
|
237
|
+
def set_timer(self, do_preview, restore_original, dlg):
|
|
238
|
+
self.preview_timer.timeout.connect(do_preview)
|
|
239
|
+
self.connect_preview_toggle(self.preview_check, do_preview, restore_original)
|
|
240
|
+
self.button_box.accepted.connect(dlg.accept)
|
|
241
|
+
self.button_box.rejected.connect(dlg.reject)
|
|
242
|
+
QTimer.singleShot(0, do_preview)
|
|
243
|
+
|
|
194
244
|
class PreviewWorker(QThread):
|
|
195
245
|
finished = Signal(np.ndarray, int, tuple)
|
|
196
246
|
|
|
@@ -222,14 +272,14 @@ class BaseFilter(QObject, LayerCollectionHandler):
|
|
|
222
272
|
|
|
223
273
|
class OneSliderBaseFilter(BaseFilter):
|
|
224
274
|
def __init__(self, name, parent, image_viewer, layer_collection, undo_manager,
|
|
225
|
-
max_value, initial_value, title,
|
|
275
|
+
min_value, max_value, initial_value, title,
|
|
226
276
|
allow_partial_preview=True, partial_preview_threshold=0.5,
|
|
227
277
|
preview_at_startup=True):
|
|
228
278
|
super().__init__(name, parent, image_viewer, layer_collection, undo_manager,
|
|
229
279
|
allow_partial_preview,
|
|
230
280
|
partial_preview_threshold, preview_at_startup)
|
|
231
|
-
self.max_range = 500
|
|
232
281
|
self.max_value = max_value
|
|
282
|
+
self.min_value = min_value
|
|
233
283
|
self.initial_value = initial_value
|
|
234
284
|
self.slider = None
|
|
235
285
|
self.value_label = None
|
|
@@ -239,6 +289,13 @@ class OneSliderBaseFilter(BaseFilter):
|
|
|
239
289
|
def add_widgets(self, layout, dlg):
|
|
240
290
|
pass
|
|
241
291
|
|
|
292
|
+
def slider_from_value_1(self, value):
|
|
293
|
+
return int((value - self.min_value) / (self.max_value - self.min_value) * self.max_range)
|
|
294
|
+
|
|
295
|
+
def value_from_slider_1(self, slider_value):
|
|
296
|
+
return self.min_value + \
|
|
297
|
+
(self.max_value - self.min_value) * float(slider_value) / self.max_range
|
|
298
|
+
|
|
242
299
|
def setup_ui(self, dlg, layout, do_preview, restore_original, **kwargs):
|
|
243
300
|
dlg.setWindowTitle(self.title)
|
|
244
301
|
dlg.setMinimumWidth(600)
|
|
@@ -246,7 +303,7 @@ class OneSliderBaseFilter(BaseFilter):
|
|
|
246
303
|
slider_layout.addWidget(QLabel("Amount:"))
|
|
247
304
|
slider_local = QSlider(Qt.Horizontal)
|
|
248
305
|
slider_local.setRange(0, self.max_range)
|
|
249
|
-
slider_local.setValue(
|
|
306
|
+
slider_local.setValue(self.slider_from_value_1(self.initial_value))
|
|
250
307
|
slider_layout.addWidget(slider_local)
|
|
251
308
|
self.value_label = QLabel(self.format.format(self.initial_value))
|
|
252
309
|
slider_layout.addWidget(self.value_label)
|
|
@@ -254,9 +311,7 @@ class OneSliderBaseFilter(BaseFilter):
|
|
|
254
311
|
self.add_widgets(layout, dlg)
|
|
255
312
|
self.create_base_widgets(
|
|
256
313
|
layout, QDialogButtonBox.Ok | QDialogButtonBox.Cancel, 200, dlg)
|
|
257
|
-
|
|
258
314
|
self.preview_timer.timeout.connect(do_preview)
|
|
259
|
-
|
|
260
315
|
slider_local.valueChanged.connect(self.config_changed)
|
|
261
316
|
self.connect_preview_toggle(
|
|
262
317
|
self.preview_check, self.do_preview_delayed, restore_original)
|
|
@@ -269,7 +324,7 @@ class OneSliderBaseFilter(BaseFilter):
|
|
|
269
324
|
self.do_preview_delayed()
|
|
270
325
|
|
|
271
326
|
def config_changed(self, val):
|
|
272
|
-
float_val = self.
|
|
327
|
+
float_val = self.value_from_slider_1(val)
|
|
273
328
|
self.value_label.setText(self.format.format(float_val))
|
|
274
329
|
self.param_changed(val)
|
|
275
330
|
|
|
@@ -277,7 +332,7 @@ class OneSliderBaseFilter(BaseFilter):
|
|
|
277
332
|
self.preview_timer.start()
|
|
278
333
|
|
|
279
334
|
def get_params(self):
|
|
280
|
-
return (self.
|
|
335
|
+
return (self.value_from_slider_1(self.slider.value()),)
|
|
281
336
|
|
|
282
337
|
def apply(self, image, *params):
|
|
283
338
|
assert False
|