shinestacker 0.2.0.post1.dev1__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 +3 -0
- shinestacker/_version.py +1 -0
- shinestacker/algorithms/__init__.py +14 -0
- shinestacker/algorithms/align.py +307 -0
- shinestacker/algorithms/balance.py +367 -0
- shinestacker/algorithms/core_utils.py +22 -0
- shinestacker/algorithms/depth_map.py +164 -0
- shinestacker/algorithms/exif.py +238 -0
- shinestacker/algorithms/multilayer.py +187 -0
- shinestacker/algorithms/noise_detection.py +182 -0
- shinestacker/algorithms/pyramid.py +176 -0
- shinestacker/algorithms/stack.py +112 -0
- shinestacker/algorithms/stack_framework.py +248 -0
- shinestacker/algorithms/utils.py +71 -0
- shinestacker/algorithms/vignetting.py +137 -0
- shinestacker/app/__init__.py +0 -0
- shinestacker/app/about_dialog.py +24 -0
- shinestacker/app/app_config.py +39 -0
- shinestacker/app/gui_utils.py +35 -0
- shinestacker/app/help_menu.py +16 -0
- shinestacker/app/main.py +176 -0
- shinestacker/app/open_frames.py +39 -0
- shinestacker/app/project.py +91 -0
- shinestacker/app/retouch.py +82 -0
- shinestacker/config/__init__.py +4 -0
- shinestacker/config/config.py +53 -0
- shinestacker/config/constants.py +174 -0
- shinestacker/config/gui_constants.py +85 -0
- shinestacker/core/__init__.py +5 -0
- shinestacker/core/colors.py +60 -0
- shinestacker/core/core_utils.py +52 -0
- shinestacker/core/exceptions.py +50 -0
- shinestacker/core/framework.py +210 -0
- shinestacker/core/logging.py +89 -0
- shinestacker/gui/__init__.py +0 -0
- shinestacker/gui/action_config.py +879 -0
- shinestacker/gui/actions_window.py +283 -0
- shinestacker/gui/colors.py +57 -0
- shinestacker/gui/gui_images.py +152 -0
- shinestacker/gui/gui_logging.py +213 -0
- shinestacker/gui/gui_run.py +393 -0
- shinestacker/gui/img/close-round-line-icon.png +0 -0
- shinestacker/gui/img/forward-button-icon.png +0 -0
- shinestacker/gui/img/play-button-round-icon.png +0 -0
- shinestacker/gui/img/plus-round-line-icon.png +0 -0
- shinestacker/gui/main_window.py +599 -0
- shinestacker/gui/new_project.py +170 -0
- shinestacker/gui/project_converter.py +148 -0
- shinestacker/gui/project_editor.py +539 -0
- shinestacker/gui/project_model.py +138 -0
- shinestacker/retouch/__init__.py +0 -0
- shinestacker/retouch/brush.py +9 -0
- shinestacker/retouch/brush_controller.py +57 -0
- shinestacker/retouch/brush_preview.py +126 -0
- shinestacker/retouch/exif_data.py +65 -0
- shinestacker/retouch/file_loader.py +104 -0
- shinestacker/retouch/image_editor.py +651 -0
- shinestacker/retouch/image_editor_ui.py +380 -0
- shinestacker/retouch/image_viewer.py +356 -0
- shinestacker/retouch/shortcuts_help.py +98 -0
- shinestacker/retouch/undo_manager.py +38 -0
- shinestacker-0.2.0.post1.dev1.dist-info/METADATA +55 -0
- shinestacker-0.2.0.post1.dev1.dist-info/RECORD +67 -0
- shinestacker-0.2.0.post1.dev1.dist-info/WHEEL +5 -0
- shinestacker-0.2.0.post1.dev1.dist-info/entry_points.txt +4 -0
- shinestacker-0.2.0.post1.dev1.dist-info/licenses/LICENSE +1 -0
- shinestacker-0.2.0.post1.dev1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import cv2
|
|
2
|
+
import logging
|
|
3
|
+
import numpy as np
|
|
4
|
+
import matplotlib.pyplot as plt
|
|
5
|
+
from scipy.optimize import curve_fit, fsolve
|
|
6
|
+
from .. core.colors import color_str
|
|
7
|
+
from .. config.constants import constants
|
|
8
|
+
from .utils import img_8bit, save_plot
|
|
9
|
+
from .stack_framework import SubAction
|
|
10
|
+
|
|
11
|
+
CLIP_EXP = 10
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Vignetting(SubAction):
|
|
15
|
+
def __init__(self, enabled=True, percentiles=(0.05, 0.1, 0.25, 0.5, 0.75, 0.9, 0.95), **kwargs):
|
|
16
|
+
super().__init__(enabled)
|
|
17
|
+
self.r_steps = kwargs.get('r_steps', constants.DEFAULT_R_STEPS)
|
|
18
|
+
self.black_threshold = kwargs.get('black_threshold', constants.DEFAULT_BLACK_THRESHOLD)
|
|
19
|
+
self.apply_correction = kwargs.get('apply_correction', True)
|
|
20
|
+
self.plot_correction = kwargs.get('plot_correction', False)
|
|
21
|
+
self.plot_summary = kwargs.get('plot_summary', False)
|
|
22
|
+
self.max_correction = kwargs.get('max_correction', constants.DEFAULT_MAX_CORRECTION)
|
|
23
|
+
self.percentiles = np.sort(percentiles)
|
|
24
|
+
|
|
25
|
+
def radial_mean_intensity(self, image):
|
|
26
|
+
if len(image.shape) > 2:
|
|
27
|
+
raise ValueError("The image must be grayscale")
|
|
28
|
+
h, w = image.shape
|
|
29
|
+
self.w_2, self.h_2 = w / 2, h / 2
|
|
30
|
+
self.r_max = np.sqrt((w / 2)**2 + (h / 2)**2)
|
|
31
|
+
radii = np.linspace(0, self.r_max, self.r_steps + 1)
|
|
32
|
+
mean_intensities = np.zeros(self.r_steps)
|
|
33
|
+
y, x = np.ogrid[:h, :w]
|
|
34
|
+
dist_from_center = np.sqrt((x - self.w_2)**2 + (y - self.h_2)**2)
|
|
35
|
+
for i in range(self.r_steps):
|
|
36
|
+
mask = (dist_from_center >= radii[i]) & (dist_from_center < radii[i + 1])
|
|
37
|
+
if np.any(mask):
|
|
38
|
+
mean_intensities[i] = np.mean(image[mask])
|
|
39
|
+
else:
|
|
40
|
+
mean_intensities[i] = np.nan
|
|
41
|
+
return (radii[1:] + radii[:-1]) / 2, mean_intensities
|
|
42
|
+
|
|
43
|
+
def sigmoid(r, i0, k, r0):
|
|
44
|
+
return i0 / (1.0 + np.exp(np.minimum(CLIP_EXP, np.exp(np.clip(k * (r - r0), -CLIP_EXP, CLIP_EXP)))))
|
|
45
|
+
|
|
46
|
+
def fit_sigmoid(self, radii, intensities):
|
|
47
|
+
valid_mask = ~np.isnan(intensities)
|
|
48
|
+
i_valid, r_valid = intensities[valid_mask], radii[valid_mask]
|
|
49
|
+
try:
|
|
50
|
+
res = curve_fit(Vignetting.sigmoid, r_valid, i_valid,
|
|
51
|
+
p0=[np.max(i_valid), 0.01, np.median(r_valid)])[0]
|
|
52
|
+
except Exception:
|
|
53
|
+
self.process.sub_message(color_str(": could not find vignetting model", "red"), level=logging.WARNING)
|
|
54
|
+
res = None
|
|
55
|
+
return res
|
|
56
|
+
|
|
57
|
+
def correct_vignetting(self, image, params):
|
|
58
|
+
h, w = image.shape[:2]
|
|
59
|
+
y, x = np.ogrid[:h, :w]
|
|
60
|
+
r = np.sqrt((x - w / 2)**2 + (y - h / 2)**2)
|
|
61
|
+
vignette = np.clip(Vignetting.sigmoid(r, *params) / self.v0, 1e-6, 1)
|
|
62
|
+
if self.max_correction < 1:
|
|
63
|
+
vignette = (1.0 - self.max_correction) + vignette * self.max_correction
|
|
64
|
+
if len(image.shape) == 3:
|
|
65
|
+
vignette = vignette[:, :, np.newaxis]
|
|
66
|
+
vignette[np.min(image, axis=2) < self.black_threshold, :] = 1
|
|
67
|
+
else:
|
|
68
|
+
vignette[image < self.black_threshold] = 1
|
|
69
|
+
return np.clip(image / vignette, 0, 255 if image.dtype == np.uint8 else 65535).astype(image.dtype)
|
|
70
|
+
|
|
71
|
+
def run_frame(self, idx, ref_idx, img_0):
|
|
72
|
+
self.process.sub_message_r(color_str(": compute vignetting", "light_blue"))
|
|
73
|
+
img = cv2.cvtColor(img_8bit(img_0), cv2.COLOR_BGR2GRAY)
|
|
74
|
+
radii, intensities = self.radial_mean_intensity(img)
|
|
75
|
+
pars = self.fit_sigmoid(radii, intensities)
|
|
76
|
+
if pars is None:
|
|
77
|
+
return img_0
|
|
78
|
+
self.v0 = Vignetting.sigmoid(0, *pars)
|
|
79
|
+
i0_fit, k_fit, r0_fit = pars
|
|
80
|
+
self.process.sub_message(f": fit parameters: i0={i0_fit:.4f}, k={k_fit:.4f}, r0={r0_fit:.4f}",
|
|
81
|
+
level=logging.DEBUG)
|
|
82
|
+
if self.plot_correction:
|
|
83
|
+
plt.figure(figsize=(10, 5))
|
|
84
|
+
plt.plot(radii, intensities, label="image mean intensity")
|
|
85
|
+
plt.plot(radii, Vignetting.sigmoid(radii, *pars), label="sigmoid fit")
|
|
86
|
+
plt.xlabel('radius (pixels)')
|
|
87
|
+
plt.ylabel('mean intensity')
|
|
88
|
+
plt.legend()
|
|
89
|
+
plt.xlim(radii[0], radii[-1])
|
|
90
|
+
plt.ylim(0)
|
|
91
|
+
idx_str = "{:04d}".format(idx)
|
|
92
|
+
plot_path = f"{self.process.working_path}/{self.process.plot_path}/{self.process.name}-radial-intensity-{idx_str}.pdf"
|
|
93
|
+
save_plot(plot_path)
|
|
94
|
+
plt.close('all')
|
|
95
|
+
self.process.callback('save_plot', self.process.id, f"{self.process.name}: intensity\nframe {idx_str}", plot_path)
|
|
96
|
+
for i, p in enumerate(self.percentiles):
|
|
97
|
+
self.corrections[i][idx] = fsolve(lambda x: Vignetting.sigmoid(x, *pars) / self.v0 - p, r0_fit)[0]
|
|
98
|
+
if self.apply_correction:
|
|
99
|
+
self.process.sub_message_r(color_str(": correct vignetting", "light_blue"))
|
|
100
|
+
return self.correct_vignetting(img_0, pars)
|
|
101
|
+
else:
|
|
102
|
+
return img_0
|
|
103
|
+
|
|
104
|
+
def begin(self, process):
|
|
105
|
+
self.process = process
|
|
106
|
+
self.corrections = [np.full(self.process.counts, None, dtype=float) for p in self.percentiles]
|
|
107
|
+
|
|
108
|
+
def end(self):
|
|
109
|
+
if self.plot_summary:
|
|
110
|
+
plt.figure(figsize=(10, 5))
|
|
111
|
+
xs = np.arange(1, len(self.corrections[0]) + 1, dtype=int)
|
|
112
|
+
for i, p in enumerate(self.percentiles):
|
|
113
|
+
linestyle = 'solid'
|
|
114
|
+
if p == 0.5:
|
|
115
|
+
linestyle = '-.'
|
|
116
|
+
elif i == 0 or i == len(self.percentiles) - 1:
|
|
117
|
+
linestyle = 'dotted'
|
|
118
|
+
plt.plot(xs, self.corrections[i], label=f"{p:.0%} correction",
|
|
119
|
+
linestyle=linestyle, color="blue")
|
|
120
|
+
plt.fill_between(xs, self.corrections[-1], self.corrections[0], color="#0000ff20")
|
|
121
|
+
iis = np.where(self.percentiles == 0.5)
|
|
122
|
+
if len(iis) > 0:
|
|
123
|
+
i = iis[0][0]
|
|
124
|
+
if i >= 1 and i < len(self.percentiles) - 1:
|
|
125
|
+
plt.fill_between(xs, self.corrections[i - 1], self.corrections[i + 1], color="#0000ff20")
|
|
126
|
+
plt.plot(xs[[0, -1]], [self.r_max] * 2, linestyle="--", label="max. radius", color="darkred")
|
|
127
|
+
plt.plot(xs[[0, -1]], [self.w_2] * 2, linestyle="--", label="half width", color="limegreen")
|
|
128
|
+
plt.plot(xs[[0, -1]], [self.h_2] * 2, linestyle="--", label="half height", color="darkgreen")
|
|
129
|
+
plt.xlabel('frame')
|
|
130
|
+
plt.ylabel('distance from center (pixels)')
|
|
131
|
+
plt.legend(ncols=2)
|
|
132
|
+
plt.xlim(xs[0], xs[-1])
|
|
133
|
+
plt.ylim(0, self.r_max * 1.05)
|
|
134
|
+
plot_path = self.process.working_path + "/" + self.process.plot_path + "/" + self.process.name + "-r0.pdf"
|
|
135
|
+
save_plot(plot_path)
|
|
136
|
+
plt.close('all')
|
|
137
|
+
self.process.callback('save_plot', self.process.id, f"{self.process.name}: vignetting", plot_path)
|
|
File without changes
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from PySide6.QtWidgets import QMessageBox
|
|
2
|
+
from PySide6.QtCore import Qt
|
|
3
|
+
from .. import __version__
|
|
4
|
+
from .. config.constants import constants
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def show_about_dialog():
|
|
8
|
+
version_clean = __version__.split("+")[0]
|
|
9
|
+
about_text = f"""
|
|
10
|
+
<h3>{constants.APP_TITLE}</h3>
|
|
11
|
+
<h4>version: v{version_clean}</h4>
|
|
12
|
+
<p style='font-weight: normal;'>App and framework to combine multiple images
|
|
13
|
+
into a single focused image.</p>
|
|
14
|
+
<p>Author: Luca Lista<br/>
|
|
15
|
+
Email: <a href="mailto:luka.lista@gmail.com">luka.lista@gmail.com</a></p>
|
|
16
|
+
<p><a href="https://github.com/lucalista/shinestacker">GitHub homepage</a></p>
|
|
17
|
+
"""
|
|
18
|
+
msg = QMessageBox()
|
|
19
|
+
msg.setWindowTitle(f"About {constants.APP_STRING}")
|
|
20
|
+
msg.setIcon(QMessageBox.Icon.Information)
|
|
21
|
+
msg.setTextFormat(Qt.TextFormat.RichText)
|
|
22
|
+
msg.setText(about_text)
|
|
23
|
+
msg.setIcon(QMessageBox.Icon.NoIcon)
|
|
24
|
+
msg.exec_()
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
class _AppConfig:
|
|
2
|
+
_initialized = False
|
|
3
|
+
_instance = None
|
|
4
|
+
|
|
5
|
+
def __new__(cls):
|
|
6
|
+
if cls._instance is None:
|
|
7
|
+
cls._instance = super().__new__(cls)
|
|
8
|
+
cls._instance._init_defaults()
|
|
9
|
+
return cls._instance
|
|
10
|
+
|
|
11
|
+
def _init_defaults(self):
|
|
12
|
+
self._DONT_USE_NATIVE_MENU = True
|
|
13
|
+
self._COMBINED_APP = False
|
|
14
|
+
|
|
15
|
+
def init(self, **kwargs):
|
|
16
|
+
if self._initialized:
|
|
17
|
+
raise RuntimeError("Config already initialized")
|
|
18
|
+
for k, v in kwargs.items():
|
|
19
|
+
if hasattr(self, f"_{k}"):
|
|
20
|
+
setattr(self, f"_{k}", v)
|
|
21
|
+
else:
|
|
22
|
+
raise AttributeError(f"Invalid config key: {k}")
|
|
23
|
+
self._initialized = True
|
|
24
|
+
|
|
25
|
+
@property
|
|
26
|
+
def DONT_USE_NATIVE_MENU(self):
|
|
27
|
+
return self._DONT_USE_NATIVE_MENU
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def COMBINED_APP(self):
|
|
31
|
+
return self._COMBINED_APP
|
|
32
|
+
|
|
33
|
+
def __setattr__(self, name, value):
|
|
34
|
+
if self._initialized and name.startswith('_'):
|
|
35
|
+
raise AttributeError("Can't change config after initialization")
|
|
36
|
+
super().__setattr__(name, value)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
app_config = _AppConfig()
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
from PySide6.QtCore import QCoreApplication, QProcess
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def disable_macos_special_menu_items():
|
|
7
|
+
if not (sys.platform == "darwin" and QCoreApplication.instance().platformName() == "cocoa"):
|
|
8
|
+
return
|
|
9
|
+
prefs = [
|
|
10
|
+
("NSDisabledCharacterPaletteMenuItem", "YES"),
|
|
11
|
+
("NSDisabledDictationMenuItem", "YES"),
|
|
12
|
+
("NSDisabledInputMenu", "YES"),
|
|
13
|
+
("NSDisabledServicesMenu", "YES"),
|
|
14
|
+
("WebAutomaticTextReplacementEnabled", "NO"),
|
|
15
|
+
("WebAutomaticSpellingCorrectionEnabled", "NO"),
|
|
16
|
+
("WebContinuousSpellCheckingEnabled", "NO"),
|
|
17
|
+
("NSTextReplacementEnabled", "NO"),
|
|
18
|
+
("NSAllowCharacterPalette", "NO"),
|
|
19
|
+
("NSDisabledHelpSearch", "YES"),
|
|
20
|
+
("NSDisabledSpellingMenuItems", "YES"),
|
|
21
|
+
("NSDisabledTextSubstitutionMenuItems", "YES"),
|
|
22
|
+
("NSDisabledGrammarMenuItems", "YES"),
|
|
23
|
+
("NSAutomaticPeriodSubstitutionEnabled", "NO"),
|
|
24
|
+
("NSAutomaticQuoteSubstitutionEnabled", "NO"),
|
|
25
|
+
("NSAutomaticDashSubstitutionEnabled", "NO"),
|
|
26
|
+
("WebAutomaticFormCompletionEnabled", "NO"),
|
|
27
|
+
("WebAutomaticPasswordAutoFillEnabled", "NO")
|
|
28
|
+
]
|
|
29
|
+
for key, value in prefs:
|
|
30
|
+
QProcess.execute("defaults", ["write", "-g", key, "-bool", value])
|
|
31
|
+
QProcess.execute("defaults", ["write", "-g", "NSAutomaticTextCompletionEnabled", "-bool", "NO"])
|
|
32
|
+
user = os.getenv('USER') or os.getenv('LOGNAME')
|
|
33
|
+
if user:
|
|
34
|
+
QProcess.startDetached("pkill", ["-u", user, "-f", "cfprefsd"])
|
|
35
|
+
QProcess.startDetached("pkill", ["-u", user, "-f", "SystemUIServer"])
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import webbrowser
|
|
2
|
+
from PySide6.QtWidgets import QMenu
|
|
3
|
+
from PySide6.QtGui import QAction
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def add_help_action(app):
|
|
7
|
+
help_menu = app.menuBar().findChild(QMenu, "Help")
|
|
8
|
+
if help_menu:
|
|
9
|
+
help_action = QAction("Online Help", app)
|
|
10
|
+
help_action.triggered.connect(browse_website)
|
|
11
|
+
help_menu.addSeparator()
|
|
12
|
+
help_menu.addAction(help_action)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def browse_website():
|
|
16
|
+
webbrowser.open("https://github.com/lucalista/shinestacker/blob/main/README.md")
|
shinestacker/app/main.py
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import os
|
|
3
|
+
import logging
|
|
4
|
+
import argparse
|
|
5
|
+
import matplotlib
|
|
6
|
+
import matplotlib.backends.backend_pdf
|
|
7
|
+
matplotlib.use('agg')
|
|
8
|
+
from PySide6.QtWidgets import QApplication, QMainWindow, QStackedWidget, QMenu
|
|
9
|
+
from PySide6.QtGui import QAction, QIcon, QGuiApplication
|
|
10
|
+
from PySide6.QtCore import Qt, QEvent, QTimer
|
|
11
|
+
from shinestacker.config.config import config
|
|
12
|
+
config.init(DISABLE_TQDM=True, COMBINED_APP=True, DONT_USE_NATIVE_MENU=True)
|
|
13
|
+
from shinestacker.config.constants import constants
|
|
14
|
+
from shinestacker.core.logging import setup_logging
|
|
15
|
+
from shinestacker.core.core_utils import get_app_base_path
|
|
16
|
+
from shinestacker.gui.main_window import MainWindow
|
|
17
|
+
from shinestacker.retouch.image_editor_ui import ImageEditorUI
|
|
18
|
+
from shinestacker.app.gui_utils import disable_macos_special_menu_items
|
|
19
|
+
from shinestacker.app.help_menu import add_help_action
|
|
20
|
+
from shinestacker.app.about_dialog import show_about_dialog
|
|
21
|
+
from shinestacker.app.open_frames import open_frames
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class MainApp(QMainWindow):
|
|
25
|
+
def __init__(self):
|
|
26
|
+
super().__init__()
|
|
27
|
+
self.setWindowTitle(constants.APP_TITLE)
|
|
28
|
+
self.resize(1400, 900)
|
|
29
|
+
center = QGuiApplication.primaryScreen().geometry().center()
|
|
30
|
+
self.move(center - self.rect().center())
|
|
31
|
+
self.stacked_widget = QStackedWidget()
|
|
32
|
+
self.setCentralWidget(self.stacked_widget)
|
|
33
|
+
self.project_window = MainWindow()
|
|
34
|
+
self.project_window.set_retouch_callback(self.retouch_callback)
|
|
35
|
+
self.retouch_window = ImageEditorUI()
|
|
36
|
+
self.stacked_widget.addWidget(self.project_window)
|
|
37
|
+
self.stacked_widget.addWidget(self.retouch_window)
|
|
38
|
+
self.app_menu = self.create_menu()
|
|
39
|
+
self.project_window.menuBar().insertMenu(self.project_window.menuBar().actions()[0], self.app_menu)
|
|
40
|
+
self.retouch_window.menuBar().insertMenu(self.retouch_window.menuBar().actions()[0], self.app_menu)
|
|
41
|
+
add_help_action(self.project_window)
|
|
42
|
+
add_help_action(self.retouch_window)
|
|
43
|
+
|
|
44
|
+
def switch_to_project(self):
|
|
45
|
+
self.switch_app(0)
|
|
46
|
+
self.switch_to_project_action.setChecked(True)
|
|
47
|
+
self.switch_to_retouch_action.setChecked(False)
|
|
48
|
+
self.switch_to_project_action.setEnabled(False)
|
|
49
|
+
self.switch_to_retouch_action.setEnabled(True)
|
|
50
|
+
self.project_window.update_title()
|
|
51
|
+
self.project_window.activateWindow()
|
|
52
|
+
self.project_window.setFocus()
|
|
53
|
+
|
|
54
|
+
def switch_to_retouch(self):
|
|
55
|
+
self.switch_app(1)
|
|
56
|
+
self.switch_to_project_action.setChecked(False)
|
|
57
|
+
self.switch_to_retouch_action.setChecked(True)
|
|
58
|
+
self.switch_to_project_action.setEnabled(True)
|
|
59
|
+
self.switch_to_retouch_action.setEnabled(False)
|
|
60
|
+
self.retouch_window.update_title()
|
|
61
|
+
self.retouch_window.activateWindow()
|
|
62
|
+
self.retouch_window.setFocus()
|
|
63
|
+
|
|
64
|
+
def create_menu(self):
|
|
65
|
+
app_menu = QMenu(constants.APP_STRING)
|
|
66
|
+
self.switch_to_project_action = QAction("Project", self)
|
|
67
|
+
self.switch_to_project_action.setCheckable(True)
|
|
68
|
+
self.switch_to_project_action.triggered.connect(self.switch_to_project)
|
|
69
|
+
self.switch_to_retouch_action = QAction("Retouch", self)
|
|
70
|
+
self.switch_to_retouch_action.setCheckable(True)
|
|
71
|
+
self.switch_to_retouch_action.triggered.connect(self.switch_to_retouch)
|
|
72
|
+
app_menu.addAction(self.switch_to_project_action)
|
|
73
|
+
app_menu.addAction(self.switch_to_retouch_action)
|
|
74
|
+
app_menu.addSeparator()
|
|
75
|
+
about_action = QAction(f"About {constants.APP_STRING}", self)
|
|
76
|
+
about_action.triggered.connect(show_about_dialog)
|
|
77
|
+
app_menu.addAction(about_action)
|
|
78
|
+
app_menu.addSeparator()
|
|
79
|
+
if config.DONT_USE_NATIVE_MENU:
|
|
80
|
+
quit_txt, quit_short = "&Quit", "Ctrl+Q"
|
|
81
|
+
else:
|
|
82
|
+
quit_txt, quit_short = "Shut dw&wn", "Ctrl+Q"
|
|
83
|
+
exit_action = QAction(quit_txt, self)
|
|
84
|
+
exit_action.setShortcut(quit_short)
|
|
85
|
+
exit_action.triggered.connect(self.quit)
|
|
86
|
+
app_menu.addAction(exit_action)
|
|
87
|
+
return app_menu
|
|
88
|
+
|
|
89
|
+
def quit(self):
|
|
90
|
+
self.retouch_window.quit()
|
|
91
|
+
self.project_window.quit()
|
|
92
|
+
self.close()
|
|
93
|
+
|
|
94
|
+
def switch_app(self, index):
|
|
95
|
+
self.stacked_widget.setCurrentIndex(index)
|
|
96
|
+
|
|
97
|
+
def retouch_callback(self, filename):
|
|
98
|
+
self.switch_to_retouch()
|
|
99
|
+
if isinstance(filename, list):
|
|
100
|
+
open_frames(self.retouch_window, None, ";".join(filename))
|
|
101
|
+
else:
|
|
102
|
+
self.retouch_window.open_file(filename)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class Application(QApplication):
|
|
106
|
+
def event(self, event):
|
|
107
|
+
if event.type() == QEvent.Quit and event.spontaneous():
|
|
108
|
+
self.main_app.quit()
|
|
109
|
+
return super().event(event)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def main():
|
|
113
|
+
parser = argparse.ArgumentParser(
|
|
114
|
+
prog=f'{constants.APP_STRING.lower()}-retouch',
|
|
115
|
+
description='Focus stacking App.',
|
|
116
|
+
epilog=f'This app is part of the {constants.APP_STRING} package.')
|
|
117
|
+
parser.add_argument('-f', '--filename', nargs='?', help='''
|
|
118
|
+
if a single file is specified, it can be either a project or an image.
|
|
119
|
+
Multiple frames can be specified as a list of files.
|
|
120
|
+
Multiple files can be specified separated by ';'.
|
|
121
|
+
''')
|
|
122
|
+
parser.add_argument('-p', '--path', nargs='?', help='''
|
|
123
|
+
import frames from one or more directories.
|
|
124
|
+
Multiple directories can be specified separated by ';'.
|
|
125
|
+
''')
|
|
126
|
+
parser.add_argument('-r', '--retouch', action='store_true', help='''
|
|
127
|
+
open retouch window at startup instead of project windows.
|
|
128
|
+
''')
|
|
129
|
+
parser.add_argument('-x', '--expert', action='store_true', help='''
|
|
130
|
+
expert options are visible by default.
|
|
131
|
+
''')
|
|
132
|
+
args = vars(parser.parse_args(sys.argv[1:]))
|
|
133
|
+
filename = args['filename']
|
|
134
|
+
path = args['path']
|
|
135
|
+
setup_logging(console_level=logging.DEBUG, file_level=logging.DEBUG, disable_console=True)
|
|
136
|
+
app = Application(sys.argv)
|
|
137
|
+
if config.DONT_USE_NATIVE_MENU:
|
|
138
|
+
app.setAttribute(Qt.AA_DontUseNativeMenuBar)
|
|
139
|
+
else:
|
|
140
|
+
disable_macos_special_menu_items()
|
|
141
|
+
icon_path = f'{get_app_base_path()}'
|
|
142
|
+
if os.path.exists(f'{icon_path}/ico'):
|
|
143
|
+
icon_path = f'{icon_path}/ico'
|
|
144
|
+
else:
|
|
145
|
+
icon_path = f'{icon_path}/../ico'
|
|
146
|
+
icon_path = f'{icon_path}/shinestacker.png'
|
|
147
|
+
app.setWindowIcon(QIcon(icon_path))
|
|
148
|
+
main_app = MainApp()
|
|
149
|
+
app.main_app = main_app
|
|
150
|
+
main_app.show()
|
|
151
|
+
main_app.activateWindow()
|
|
152
|
+
if args['expert']:
|
|
153
|
+
main_app.project_window.set_expert_options()
|
|
154
|
+
if filename:
|
|
155
|
+
filenames = filename.split(';')
|
|
156
|
+
filename = filenames[0]
|
|
157
|
+
extension = filename.split('.')[-1]
|
|
158
|
+
if len(filenames) == 1 and extension == 'fsp':
|
|
159
|
+
main_app.project_window.open_project(filename)
|
|
160
|
+
main_app.project_window.setFocus()
|
|
161
|
+
else:
|
|
162
|
+
main_app.switch_to_retouch()
|
|
163
|
+
open_frames(main_app.retouch_window, filename, path)
|
|
164
|
+
else:
|
|
165
|
+
retouch = args['retouch']
|
|
166
|
+
if retouch:
|
|
167
|
+
main_app.switch_to_retouch()
|
|
168
|
+
else:
|
|
169
|
+
main_app.switch_to_project()
|
|
170
|
+
QTimer.singleShot(100, lambda: main_app.project_window.new_project())
|
|
171
|
+
QTimer.singleShot(100, lambda: main_app.setFocus())
|
|
172
|
+
sys.exit(app.exec())
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
if __name__ == "__main__":
|
|
176
|
+
main()
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
from PySide6.QtCore import QTimer
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def open_files(editor, filenames):
|
|
7
|
+
if len(filenames) == 1:
|
|
8
|
+
QTimer.singleShot(100, lambda: editor.open_file(filenames[0]))
|
|
9
|
+
else:
|
|
10
|
+
def check_thread():
|
|
11
|
+
if editor.loader_thread is None or editor.loader_thread.isRunning():
|
|
12
|
+
QTimer.singleShot(100, check_thread)
|
|
13
|
+
else:
|
|
14
|
+
editor.import_frames_from_files(filenames[1:])
|
|
15
|
+
QTimer.singleShot(100, lambda: (
|
|
16
|
+
editor.open_file(filenames[0]),
|
|
17
|
+
QTimer.singleShot(100, check_thread)
|
|
18
|
+
))
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def open_frames(editor, filename, path):
|
|
22
|
+
if filename:
|
|
23
|
+
filenames = filename.split(';')
|
|
24
|
+
open_files(editor, filenames)
|
|
25
|
+
elif path:
|
|
26
|
+
reverse = False
|
|
27
|
+
paths = path.split(';')
|
|
28
|
+
filenames = []
|
|
29
|
+
for path in paths:
|
|
30
|
+
if os.path.exists(path) and os.path.isdir(path):
|
|
31
|
+
all_entries = os.listdir(path)
|
|
32
|
+
files = sorted([f for f in all_entries if os.path.isfile(os.path.join(path, f))],
|
|
33
|
+
reverse=reverse)
|
|
34
|
+
full_paths = [os.path.join(path, f) for f in files]
|
|
35
|
+
filenames += full_paths
|
|
36
|
+
else:
|
|
37
|
+
print(f"path {path} is invalid", file=sys.stderr)
|
|
38
|
+
exit(1)
|
|
39
|
+
open_files(editor, filenames)
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
import logging
|
|
4
|
+
import argparse
|
|
5
|
+
import matplotlib
|
|
6
|
+
import matplotlib.backends.backend_pdf
|
|
7
|
+
matplotlib.use('agg')
|
|
8
|
+
from PySide6.QtWidgets import QApplication, QMenu
|
|
9
|
+
from PySide6.QtGui import QIcon, QAction
|
|
10
|
+
from PySide6.QtCore import Qt, QTimer, QEvent
|
|
11
|
+
from shinestacker.config.config import config
|
|
12
|
+
config.init(DISABLE_TQDM=True, DONT_USE_NATIVE_MENU=True)
|
|
13
|
+
from shinestacker.config.constants import constants
|
|
14
|
+
from shinestacker.core.logging import setup_logging
|
|
15
|
+
from shinestacker.core.core_utils import get_app_base_path
|
|
16
|
+
from shinestacker.gui.main_window import MainWindow
|
|
17
|
+
from shinestacker.app.gui_utils import disable_macos_special_menu_items
|
|
18
|
+
from shinestacker.app.help_menu import add_help_action
|
|
19
|
+
from shinestacker.app.about_dialog import show_about_dialog
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ProjectApp(MainWindow):
|
|
23
|
+
def __init__(self):
|
|
24
|
+
super().__init__()
|
|
25
|
+
self.app_menu = self.create_menu()
|
|
26
|
+
self.menuBar().insertMenu(self.menuBar().actions()[0], self.app_menu)
|
|
27
|
+
add_help_action(self)
|
|
28
|
+
self.set_retouch_callback(self._retouch_callback)
|
|
29
|
+
|
|
30
|
+
def create_menu(self):
|
|
31
|
+
app_menu = QMenu(constants.APP_STRING)
|
|
32
|
+
about_action = QAction(f"About {constants.APP_STRING}", self)
|
|
33
|
+
about_action.triggered.connect(show_about_dialog)
|
|
34
|
+
app_menu.addAction(about_action)
|
|
35
|
+
app_menu.addSeparator()
|
|
36
|
+
if config.DONT_USE_NATIVE_MENU:
|
|
37
|
+
quit_txt, quit_short = "&Quit", "Ctrl+Q"
|
|
38
|
+
else:
|
|
39
|
+
quit_txt, quit_short = "Shut dw&wn", "Ctrl+Q"
|
|
40
|
+
exit_action = QAction(quit_txt, self)
|
|
41
|
+
exit_action.setShortcut(quit_short)
|
|
42
|
+
exit_action.triggered.connect(self.quit)
|
|
43
|
+
app_menu.addAction(exit_action)
|
|
44
|
+
return app_menu
|
|
45
|
+
|
|
46
|
+
def _retouch_callback(self, filename):
|
|
47
|
+
p = ";".join(filename)
|
|
48
|
+
os.system(f'{constants.RETOUCH_APP} -p "{p}" &')
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class Application(QApplication):
|
|
52
|
+
def event(self, event):
|
|
53
|
+
if event.type() == QEvent.Quit and event.spontaneous():
|
|
54
|
+
self.window.quit()
|
|
55
|
+
return super().event(event)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def main():
|
|
59
|
+
parser = argparse.ArgumentParser(
|
|
60
|
+
prog=f'{constants.APP_STRING.lower()}-project',
|
|
61
|
+
description='Manage and run focus stack jobs.',
|
|
62
|
+
epilog=f'This app is part of the {constants.APP_STRING} package.')
|
|
63
|
+
parser.add_argument('-f', '--filename', nargs='?', help='''
|
|
64
|
+
project filename.
|
|
65
|
+
''')
|
|
66
|
+
parser.add_argument('-x', '--expert', action='store_true', help='''
|
|
67
|
+
expert options are visible by default.
|
|
68
|
+
''')
|
|
69
|
+
args = vars(parser.parse_args(sys.argv[1:]))
|
|
70
|
+
setup_logging(console_level=logging.DEBUG, file_level=logging.DEBUG, disable_console=True)
|
|
71
|
+
app = Application(sys.argv)
|
|
72
|
+
if config.DONT_USE_NATIVE_MENU:
|
|
73
|
+
app.setAttribute(Qt.AA_DontUseNativeMenuBar)
|
|
74
|
+
else:
|
|
75
|
+
disable_macos_special_menu_items()
|
|
76
|
+
app.setWindowIcon(QIcon(f'{get_app_base_path()}/ico/shinestacker.png'))
|
|
77
|
+
window = ProjectApp()
|
|
78
|
+
if args['expert']:
|
|
79
|
+
window.set_expert_options()
|
|
80
|
+
app.window = window
|
|
81
|
+
window.show()
|
|
82
|
+
filename = args['filename']
|
|
83
|
+
if filename:
|
|
84
|
+
QTimer.singleShot(100, lambda: window.open_project(filename))
|
|
85
|
+
else:
|
|
86
|
+
QTimer.singleShot(100, lambda: window.new_project())
|
|
87
|
+
sys.exit(app.exec())
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
if __name__ == "__main__":
|
|
91
|
+
main()
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import argparse
|
|
3
|
+
from PySide6.QtWidgets import QApplication, QMenu
|
|
4
|
+
from PySide6.QtGui import QIcon, QAction
|
|
5
|
+
from PySide6.QtCore import Qt, QEvent
|
|
6
|
+
from shinestacker.config.config import config
|
|
7
|
+
config.init(DISABLE_TQDM=True, DONT_USE_NATIVE_MENU=True)
|
|
8
|
+
from shinestacker.core.core_utils import get_app_base_path
|
|
9
|
+
from shinestacker.config.config import config
|
|
10
|
+
from shinestacker.config.constants import constants
|
|
11
|
+
from shinestacker.retouch.image_editor_ui import ImageEditorUI
|
|
12
|
+
from shinestacker.app.gui_utils import disable_macos_special_menu_items
|
|
13
|
+
from shinestacker.app.help_menu import add_help_action
|
|
14
|
+
from shinestacker.app.about_dialog import show_about_dialog
|
|
15
|
+
from shinestacker.app.open_frames import open_frames
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class RetouchApp(ImageEditorUI):
|
|
19
|
+
def __init__(self):
|
|
20
|
+
super().__init__()
|
|
21
|
+
self.app_menu = self.create_menu()
|
|
22
|
+
self.menuBar().insertMenu(self.menuBar().actions()[0], self.app_menu)
|
|
23
|
+
add_help_action(self)
|
|
24
|
+
|
|
25
|
+
def create_menu(self):
|
|
26
|
+
app_menu = QMenu(constants.APP_STRING)
|
|
27
|
+
about_action = QAction(f"About {constants.APP_STRING}", self)
|
|
28
|
+
about_action.triggered.connect(show_about_dialog)
|
|
29
|
+
app_menu.addAction(about_action)
|
|
30
|
+
app_menu.addSeparator()
|
|
31
|
+
if config.DONT_USE_NATIVE_MENU:
|
|
32
|
+
quit_txt, quit_short = "&Quit", "Ctrl+Q"
|
|
33
|
+
else:
|
|
34
|
+
quit_txt, quit_short = "Shut dw&wn", "Ctrl+Q"
|
|
35
|
+
exit_action = QAction(quit_txt, self)
|
|
36
|
+
exit_action.setShortcut(quit_short)
|
|
37
|
+
exit_action.triggered.connect(self.quit)
|
|
38
|
+
app_menu.addAction(exit_action)
|
|
39
|
+
return app_menu
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class Application(QApplication):
|
|
43
|
+
def event(self, event):
|
|
44
|
+
if event.type() == QEvent.Quit and event.spontaneous():
|
|
45
|
+
self.editor.quit()
|
|
46
|
+
return super().event(event)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def main():
|
|
50
|
+
parser = argparse.ArgumentParser(
|
|
51
|
+
prog=f'{constants.APP_STRING.lower()}-retouch',
|
|
52
|
+
description='Final retouch focus stack image from individual frames.',
|
|
53
|
+
epilog=f'This app is part of the {constants.APP_STRING} package.')
|
|
54
|
+
parser.add_argument('-f', '--filename', nargs='?', help='''
|
|
55
|
+
import frames from files.
|
|
56
|
+
Multiple files can be specified separated by ';'.
|
|
57
|
+
''')
|
|
58
|
+
parser.add_argument('-p', '--path', nargs='?', help='''
|
|
59
|
+
import frames from one or more directories.
|
|
60
|
+
Multiple directories can be specified separated by ';'.
|
|
61
|
+
''')
|
|
62
|
+
args = vars(parser.parse_args(sys.argv[1:]))
|
|
63
|
+
filename = args['filename']
|
|
64
|
+
path = args['path']
|
|
65
|
+
if filename and path:
|
|
66
|
+
print("can't specify both arguments --filename and --path", file=sys.stderr)
|
|
67
|
+
exit(1)
|
|
68
|
+
app = Application(sys.argv)
|
|
69
|
+
if config.DONT_USE_NATIVE_MENU:
|
|
70
|
+
app.setAttribute(Qt.AA_DontUseNativeMenuBar)
|
|
71
|
+
else:
|
|
72
|
+
disable_macos_special_menu_items()
|
|
73
|
+
app.setWindowIcon(QIcon(f'{get_app_base_path()}/ico/shinestacker.png'))
|
|
74
|
+
editor = RetouchApp()
|
|
75
|
+
app.editor = editor
|
|
76
|
+
editor.show()
|
|
77
|
+
open_frames(editor, filename, path)
|
|
78
|
+
sys.exit(app.exec())
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
if __name__ == "__main__":
|
|
82
|
+
main()
|