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.

Files changed (67) hide show
  1. shinestacker/__init__.py +3 -0
  2. shinestacker/_version.py +1 -0
  3. shinestacker/algorithms/__init__.py +14 -0
  4. shinestacker/algorithms/align.py +307 -0
  5. shinestacker/algorithms/balance.py +367 -0
  6. shinestacker/algorithms/core_utils.py +22 -0
  7. shinestacker/algorithms/depth_map.py +164 -0
  8. shinestacker/algorithms/exif.py +238 -0
  9. shinestacker/algorithms/multilayer.py +187 -0
  10. shinestacker/algorithms/noise_detection.py +182 -0
  11. shinestacker/algorithms/pyramid.py +176 -0
  12. shinestacker/algorithms/stack.py +112 -0
  13. shinestacker/algorithms/stack_framework.py +248 -0
  14. shinestacker/algorithms/utils.py +71 -0
  15. shinestacker/algorithms/vignetting.py +137 -0
  16. shinestacker/app/__init__.py +0 -0
  17. shinestacker/app/about_dialog.py +24 -0
  18. shinestacker/app/app_config.py +39 -0
  19. shinestacker/app/gui_utils.py +35 -0
  20. shinestacker/app/help_menu.py +16 -0
  21. shinestacker/app/main.py +176 -0
  22. shinestacker/app/open_frames.py +39 -0
  23. shinestacker/app/project.py +91 -0
  24. shinestacker/app/retouch.py +82 -0
  25. shinestacker/config/__init__.py +4 -0
  26. shinestacker/config/config.py +53 -0
  27. shinestacker/config/constants.py +174 -0
  28. shinestacker/config/gui_constants.py +85 -0
  29. shinestacker/core/__init__.py +5 -0
  30. shinestacker/core/colors.py +60 -0
  31. shinestacker/core/core_utils.py +52 -0
  32. shinestacker/core/exceptions.py +50 -0
  33. shinestacker/core/framework.py +210 -0
  34. shinestacker/core/logging.py +89 -0
  35. shinestacker/gui/__init__.py +0 -0
  36. shinestacker/gui/action_config.py +879 -0
  37. shinestacker/gui/actions_window.py +283 -0
  38. shinestacker/gui/colors.py +57 -0
  39. shinestacker/gui/gui_images.py +152 -0
  40. shinestacker/gui/gui_logging.py +213 -0
  41. shinestacker/gui/gui_run.py +393 -0
  42. shinestacker/gui/img/close-round-line-icon.png +0 -0
  43. shinestacker/gui/img/forward-button-icon.png +0 -0
  44. shinestacker/gui/img/play-button-round-icon.png +0 -0
  45. shinestacker/gui/img/plus-round-line-icon.png +0 -0
  46. shinestacker/gui/main_window.py +599 -0
  47. shinestacker/gui/new_project.py +170 -0
  48. shinestacker/gui/project_converter.py +148 -0
  49. shinestacker/gui/project_editor.py +539 -0
  50. shinestacker/gui/project_model.py +138 -0
  51. shinestacker/retouch/__init__.py +0 -0
  52. shinestacker/retouch/brush.py +9 -0
  53. shinestacker/retouch/brush_controller.py +57 -0
  54. shinestacker/retouch/brush_preview.py +126 -0
  55. shinestacker/retouch/exif_data.py +65 -0
  56. shinestacker/retouch/file_loader.py +104 -0
  57. shinestacker/retouch/image_editor.py +651 -0
  58. shinestacker/retouch/image_editor_ui.py +380 -0
  59. shinestacker/retouch/image_viewer.py +356 -0
  60. shinestacker/retouch/shortcuts_help.py +98 -0
  61. shinestacker/retouch/undo_manager.py +38 -0
  62. shinestacker-0.2.0.post1.dev1.dist-info/METADATA +55 -0
  63. shinestacker-0.2.0.post1.dev1.dist-info/RECORD +67 -0
  64. shinestacker-0.2.0.post1.dev1.dist-info/WHEEL +5 -0
  65. shinestacker-0.2.0.post1.dev1.dist-info/entry_points.txt +4 -0
  66. shinestacker-0.2.0.post1.dev1.dist-info/licenses/LICENSE +1 -0
  67. 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")
@@ -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()
@@ -0,0 +1,4 @@
1
+ # flake8: noqa F401
2
+ from .config import config
3
+ from .constants import constants
4
+ from .gui_constants import gui_constants