shinestacker 1.0.2__py3-none-any.whl → 1.0.4__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/exif.py +10 -13
- shinestacker/algorithms/multilayer.py +7 -6
- shinestacker/algorithms/stack_framework.py +1 -1
- shinestacker/algorithms/utils.py +47 -7
- shinestacker/app/main.py +99 -2
- shinestacker/gui/gui_run.py +5 -4
- shinestacker/gui/main_window.py +2 -2
- shinestacker/gui/new_project.py +6 -11
- shinestacker/gui/project_controller.py +6 -5
- shinestacker/retouch/file_loader.py +3 -4
- shinestacker/retouch/image_editor_ui.py +1 -2
- shinestacker/retouch/io_gui_handler.py +17 -22
- {shinestacker-1.0.2.dist-info → shinestacker-1.0.4.dist-info}/METADATA +2 -1
- {shinestacker-1.0.2.dist-info → shinestacker-1.0.4.dist-info}/RECORD +19 -19
- {shinestacker-1.0.2.dist-info → shinestacker-1.0.4.dist-info}/WHEEL +0 -0
- {shinestacker-1.0.2.dist-info → shinestacker-1.0.4.dist-info}/entry_points.txt +0 -0
- {shinestacker-1.0.2.dist-info → shinestacker-1.0.4.dist-info}/licenses/LICENSE +0 -0
- {shinestacker-1.0.2.dist-info → shinestacker-1.0.4.dist-info}/top_level.txt +0 -0
shinestacker/_version.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = '1.0.
|
|
1
|
+
__version__ = '1.0.4'
|
shinestacker/algorithms/exif.py
CHANGED
|
@@ -10,7 +10,7 @@ from PIL.TiffImagePlugin import IFDRational
|
|
|
10
10
|
from PIL.ExifTags import TAGS
|
|
11
11
|
import tifffile
|
|
12
12
|
from .. config.constants import constants
|
|
13
|
-
from .utils import write_img
|
|
13
|
+
from .utils import write_img, extension_jpg, extension_tif, extension_png
|
|
14
14
|
|
|
15
15
|
IMAGEWIDTH = 256
|
|
16
16
|
IMAGELENGTH = 257
|
|
@@ -48,11 +48,10 @@ def extract_enclosed_data_for_jpg(data, head, foot):
|
|
|
48
48
|
def get_exif(exif_filename):
|
|
49
49
|
if not os.path.isfile(exif_filename):
|
|
50
50
|
raise RuntimeError(f"File does not exist: {exif_filename}")
|
|
51
|
-
ext = exif_filename.split(".")[-1]
|
|
52
51
|
image = Image.open(exif_filename)
|
|
53
|
-
if
|
|
52
|
+
if extension_tif(exif_filename):
|
|
54
53
|
return image.tag_v2 if hasattr(image, 'tag_v2') else image.getexif()
|
|
55
|
-
if
|
|
54
|
+
if extension_jpg(exif_filename):
|
|
56
55
|
exif_data = image.getexif()
|
|
57
56
|
with open(exif_filename, 'rb') as f:
|
|
58
57
|
data = extract_enclosed_data_for_jpg(f.read(), b'<?xpacket', b'<?xpacket end="w"?>')
|
|
@@ -158,42 +157,40 @@ def write_image_with_exif_data(exif, image, out_filename, verbose=False):
|
|
|
158
157
|
if exif is None:
|
|
159
158
|
write_img(out_filename, image)
|
|
160
159
|
return None
|
|
161
|
-
ext = out_filename.split(".")[-1]
|
|
162
160
|
if verbose:
|
|
163
161
|
print_exif(exif)
|
|
164
|
-
if
|
|
162
|
+
if extension_jpg(out_filename):
|
|
165
163
|
cv2.imwrite(out_filename, image, [int(cv2.IMWRITE_JPEG_QUALITY), 100])
|
|
166
164
|
add_exif_data_to_jpg_file(exif, out_filename, out_filename, verbose)
|
|
167
|
-
elif
|
|
165
|
+
elif extension_tif(out_filename):
|
|
168
166
|
metadata = {"description": f"image generated with {constants.APP_STRING} package"}
|
|
169
167
|
extra_tags, exif_tags = exif_extra_tags_for_tif(exif)
|
|
170
168
|
tifffile.imwrite(out_filename, image, metadata=metadata, compression='adobe_deflate',
|
|
171
169
|
extratags=extra_tags, **exif_tags)
|
|
172
|
-
elif
|
|
170
|
+
elif extension_png(out_filename):
|
|
173
171
|
image.save(out_filename, 'PNG', exif=exif, quality=100)
|
|
174
172
|
return exif
|
|
175
173
|
|
|
176
174
|
|
|
177
175
|
def save_exif_data(exif, in_filename, out_filename=None, verbose=False):
|
|
178
|
-
ext = in_filename.split(".")[-1]
|
|
179
176
|
if out_filename is None:
|
|
180
177
|
out_filename = in_filename
|
|
181
178
|
if exif is None:
|
|
182
179
|
raise RuntimeError('No exif data provided.')
|
|
183
180
|
if verbose:
|
|
184
181
|
print_exif(exif)
|
|
185
|
-
if
|
|
182
|
+
if extension_tif(in_filename):
|
|
186
183
|
image_new = tifffile.imread(in_filename)
|
|
187
184
|
else:
|
|
188
185
|
image_new = Image.open(in_filename)
|
|
189
|
-
if
|
|
186
|
+
if extension_jpg(in_filename):
|
|
190
187
|
add_exif_data_to_jpg_file(exif, in_filename, out_filename, verbose)
|
|
191
|
-
elif
|
|
188
|
+
elif extension_tif(in_filename):
|
|
192
189
|
metadata = {"description": f"image generated with {constants.APP_STRING} package"}
|
|
193
190
|
extra_tags, exif_tags = exif_extra_tags_for_tif(exif)
|
|
194
191
|
tifffile.imwrite(out_filename, image_new, metadata=metadata, compression='adobe_deflate',
|
|
195
192
|
extratags=extra_tags, **exif_tags)
|
|
196
|
-
elif
|
|
193
|
+
elif extension_png(in_filename):
|
|
197
194
|
image_new.save(out_filename, 'PNG', exif=exif, quality=100)
|
|
198
195
|
return exif
|
|
199
196
|
|
|
@@ -13,6 +13,7 @@ from .. config.constants import constants
|
|
|
13
13
|
from .. config.config import config
|
|
14
14
|
from .. core.colors import color_str
|
|
15
15
|
from .. core.framework import JobBase
|
|
16
|
+
from .utils import EXTENSIONS_TIF, EXTENSIONS_JPG, EXTENSIONS_PNG
|
|
16
17
|
from .stack_framework import FrameMultiDirectory
|
|
17
18
|
from .exif import exif_extra_tags_for_tif, get_exif
|
|
18
19
|
|
|
@@ -27,13 +28,13 @@ def write_multilayer_tiff(input_files, output_file, labels=None, exif_path='', c
|
|
|
27
28
|
msg = ", ".join(extensions)
|
|
28
29
|
raise RuntimeError("All input files must have the same extension. "
|
|
29
30
|
f"Input list has the following extensions: {msg}.")
|
|
30
|
-
extension = extensions[0]
|
|
31
|
-
if extension in
|
|
31
|
+
extension = extensions[0].lower()
|
|
32
|
+
if extension in EXTENSIONS_TIF:
|
|
32
33
|
images = [tifffile.imread(p) for p in input_files]
|
|
33
|
-
elif extension in
|
|
34
|
+
elif extension in EXTENSIONS_JPG:
|
|
34
35
|
images = [cv2.imread(p) for p in input_files]
|
|
35
36
|
images = [cv2.cvtColor(i, cv2.COLOR_BGR2RGB) for i in images]
|
|
36
|
-
elif extension
|
|
37
|
+
elif extension in EXTENSIONS_PNG:
|
|
37
38
|
images = [cv2.imread(p, cv2.IMREAD_UNCHANGED) for p in input_files]
|
|
38
39
|
images = [cv2.cvtColor(i, cv2.COLOR_BGR2RGB) for i in images]
|
|
39
40
|
if labels is None:
|
|
@@ -177,7 +178,7 @@ class MultiLayer(JobBase, FrameMultiDirectory):
|
|
|
177
178
|
raise RuntimeError("input_path option must contain a path or an array of paths")
|
|
178
179
|
if len(paths) == 0:
|
|
179
180
|
self.print_message(color_str("no input paths specified",
|
|
180
|
-
constants.
|
|
181
|
+
constants.LOG_COLOR_ALERT),
|
|
181
182
|
level=logging.WARNING)
|
|
182
183
|
return
|
|
183
184
|
files = self.folder_filelist()
|
|
@@ -186,7 +187,7 @@ class MultiLayer(JobBase, FrameMultiDirectory):
|
|
|
186
187
|
color_str(f"no input in {len(paths)} specified path" +
|
|
187
188
|
('s' if len(paths) > 1 else '') + ": "
|
|
188
189
|
", ".join([f"'{p}'" for p in paths]),
|
|
189
|
-
constants.
|
|
190
|
+
constants.LOG_COLOR_ALERT),
|
|
190
191
|
level=logging.WARNING)
|
|
191
192
|
return
|
|
192
193
|
self.print_message(color_str("merging frames in " + self.folder_list_str(),
|
|
@@ -92,7 +92,7 @@ class FramePaths:
|
|
|
92
92
|
('' if self.working_path[-1] == '/' else '/') + self.plot_path
|
|
93
93
|
if not os.path.exists(self.plot_path):
|
|
94
94
|
os.makedirs(self.plot_path)
|
|
95
|
-
if self.input_path
|
|
95
|
+
if self.input_path in ['', []]:
|
|
96
96
|
if len(job.paths) == 0:
|
|
97
97
|
raise RuntimeError(f"Job {job.name} does not have any configured path")
|
|
98
98
|
self.input_path = job.paths[-1]
|
shinestacker/algorithms/utils.py
CHANGED
|
@@ -8,25 +8,65 @@ from .. config.config import config
|
|
|
8
8
|
from .. core.exceptions import ShapeError, BitDepthError
|
|
9
9
|
|
|
10
10
|
|
|
11
|
+
def get_path_extension(path):
|
|
12
|
+
return os.path.splitext(path)[1].lstrip('.')
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
EXTENSIONS_TIF = ['tif', 'tiff']
|
|
16
|
+
EXTENSIONS_JPG = ['jpg', 'jpeg']
|
|
17
|
+
EXTENSIONS_PNG = ['png']
|
|
18
|
+
EXTENSIONS_PDF = ['pdf']
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def extension_in(path, exts):
|
|
22
|
+
return get_path_extension(path).lower() in exts
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def extension_tif(path):
|
|
26
|
+
return extension_in(path, EXTENSIONS_TIF)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def extension_jpg(path):
|
|
30
|
+
return extension_in(path, EXTENSIONS_JPG)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def extension_png(path):
|
|
34
|
+
return extension_in(path, EXTENSIONS_PNG)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def extension_pdf(path):
|
|
38
|
+
return extension_in(path, EXTENSIONS_PDF)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def extension_tif_jpg(path):
|
|
42
|
+
return extension_in(path, EXTENSIONS_TIF + EXTENSIONS_JPG)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def extension_tif_png(path):
|
|
46
|
+
return extension_in(path, EXTENSIONS_TIF + EXTENSIONS_PNG)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def extension_jpg_png(path):
|
|
50
|
+
return extension_in(path, EXTENSIONS_JPG + EXTENSIONS_PNG)
|
|
51
|
+
|
|
52
|
+
|
|
11
53
|
def read_img(file_path):
|
|
12
54
|
if not os.path.isfile(file_path):
|
|
13
55
|
raise RuntimeError("File does not exist: " + file_path)
|
|
14
|
-
ext = file_path.split(".")[-1]
|
|
15
56
|
img = None
|
|
16
|
-
if
|
|
57
|
+
if extension_jpg(file_path):
|
|
17
58
|
img = cv2.imread(file_path)
|
|
18
|
-
elif
|
|
59
|
+
elif extension_tif_png(file_path):
|
|
19
60
|
img = cv2.imread(file_path, cv2.IMREAD_UNCHANGED)
|
|
20
61
|
return img
|
|
21
62
|
|
|
22
63
|
|
|
23
64
|
def write_img(file_path, img):
|
|
24
|
-
|
|
25
|
-
if ext in ['jpeg', 'jpg']:
|
|
65
|
+
if extension_jpg(file_path):
|
|
26
66
|
cv2.imwrite(file_path, img, [int(cv2.IMWRITE_JPEG_QUALITY), 100])
|
|
27
|
-
elif
|
|
67
|
+
elif extension_tif(file_path):
|
|
28
68
|
cv2.imwrite(file_path, img, [int(cv2.IMWRITE_TIFF_COMPRESSION), 1])
|
|
29
|
-
elif
|
|
69
|
+
elif extension_png(file_path):
|
|
30
70
|
cv2.imwrite(file_path, img)
|
|
31
71
|
|
|
32
72
|
|
shinestacker/app/main.py
CHANGED
|
@@ -6,9 +6,10 @@ import argparse
|
|
|
6
6
|
import matplotlib
|
|
7
7
|
import matplotlib.backends.backend_pdf
|
|
8
8
|
matplotlib.use('agg')
|
|
9
|
-
from PySide6.QtWidgets import QApplication, QMainWindow, QStackedWidget,
|
|
9
|
+
from PySide6.QtWidgets import (QApplication, QMainWindow, QVBoxLayout, QHBoxLayout, QStackedWidget,
|
|
10
|
+
QMenu, QMessageBox, QDialog, QLabel, QListWidget, QPushButton)
|
|
10
11
|
from PySide6.QtGui import QAction, QIcon, QGuiApplication
|
|
11
|
-
from PySide6.QtCore import Qt, QEvent, QTimer
|
|
12
|
+
from PySide6.QtCore import Qt, QEvent, QTimer, Signal
|
|
12
13
|
from shinestacker.config.config import config
|
|
13
14
|
config.init(DISABLE_TQDM=True, COMBINED_APP=True, DONT_USE_NATIVE_MENU=True)
|
|
14
15
|
from shinestacker.config.constants import constants
|
|
@@ -20,6 +21,59 @@ from shinestacker.app.help_menu import add_help_action
|
|
|
20
21
|
from shinestacker.app.open_frames import open_frames
|
|
21
22
|
|
|
22
23
|
|
|
24
|
+
class SelectionDialog(QDialog):
|
|
25
|
+
selection_made = Signal(str, bool)
|
|
26
|
+
|
|
27
|
+
def __init__(self, title, message, items, parent=None):
|
|
28
|
+
super().__init__(parent)
|
|
29
|
+
self.setWindowTitle(title)
|
|
30
|
+
self.setModal(True)
|
|
31
|
+
self.selected_item = ""
|
|
32
|
+
self.setup_ui(message, items)
|
|
33
|
+
self.setMinimumSize(300, 300)
|
|
34
|
+
|
|
35
|
+
def setup_ui(self, message, items):
|
|
36
|
+
layout = QVBoxLayout(self)
|
|
37
|
+
if message:
|
|
38
|
+
label = QLabel(message)
|
|
39
|
+
layout.addWidget(label)
|
|
40
|
+
self.list_widget = QListWidget()
|
|
41
|
+
self.list_widget.addItems(items)
|
|
42
|
+
self.list_widget.itemSelectionChanged.connect(self.on_selection_changed)
|
|
43
|
+
layout.addWidget(self.list_widget)
|
|
44
|
+
button_layout = QHBoxLayout()
|
|
45
|
+
self.ok_button = QPushButton("OK")
|
|
46
|
+
self.ok_button.clicked.connect(self.accept)
|
|
47
|
+
self.ok_button.setEnabled(False)
|
|
48
|
+
button_layout.addWidget(self.ok_button)
|
|
49
|
+
cancel_button = QPushButton("Cancel")
|
|
50
|
+
cancel_button.clicked.connect(self.reject)
|
|
51
|
+
button_layout.addWidget(cancel_button)
|
|
52
|
+
layout.addLayout(button_layout)
|
|
53
|
+
|
|
54
|
+
def on_selection_changed(self):
|
|
55
|
+
selected_items = self.list_widget.selectedItems()
|
|
56
|
+
self.ok_button.setEnabled(len(selected_items) > 0)
|
|
57
|
+
|
|
58
|
+
def accept(self):
|
|
59
|
+
selected_items = self.list_widget.selectedItems()
|
|
60
|
+
if selected_items:
|
|
61
|
+
self.selected_item = selected_items[0].text()
|
|
62
|
+
self.selection_made.emit(self.selected_item, True)
|
|
63
|
+
super().accept()
|
|
64
|
+
|
|
65
|
+
def reject(self):
|
|
66
|
+
self.selected_item = ""
|
|
67
|
+
self.selection_made.emit("", False)
|
|
68
|
+
super().reject()
|
|
69
|
+
|
|
70
|
+
@staticmethod
|
|
71
|
+
def get_selection(title, message, items, parent=None):
|
|
72
|
+
dialog = SelectionDialog(title, message, items, parent)
|
|
73
|
+
result = dialog.exec()
|
|
74
|
+
return dialog.selected_item if result == QDialog.Accepted else ""
|
|
75
|
+
|
|
76
|
+
|
|
23
77
|
class MainApp(QMainWindow):
|
|
24
78
|
def __init__(self):
|
|
25
79
|
super().__init__()
|
|
@@ -41,6 +95,17 @@ class MainApp(QMainWindow):
|
|
|
41
95
|
self.retouch_window.menuBar().actions()[0], self.app_menu)
|
|
42
96
|
add_help_action(self.project_window)
|
|
43
97
|
add_help_action(self.retouch_window)
|
|
98
|
+
file_menu = None
|
|
99
|
+
for action in self.retouch_window.menuBar().actions():
|
|
100
|
+
if action.text() == "&File":
|
|
101
|
+
file_menu = action.menu()
|
|
102
|
+
break
|
|
103
|
+
if file_menu is not None:
|
|
104
|
+
import_action = QAction("Import From Current Project", self)
|
|
105
|
+
import_action.triggered.connect(self.import_from_project)
|
|
106
|
+
file_menu.addAction(import_action)
|
|
107
|
+
else:
|
|
108
|
+
raise RuntimeError("File menu not found!")
|
|
44
109
|
|
|
45
110
|
def switch_to_project(self):
|
|
46
111
|
self.switch_app(0)
|
|
@@ -91,6 +156,38 @@ class MainApp(QMainWindow):
|
|
|
91
156
|
else:
|
|
92
157
|
self.retouch_window.io_gui_handler.open_file(filename)
|
|
93
158
|
|
|
159
|
+
def import_from_project(self):
|
|
160
|
+
project = self.project_window.project()
|
|
161
|
+
if project is None:
|
|
162
|
+
QMessageBox.warning(self.parent(),
|
|
163
|
+
"No Active Project", "No project has been created or opened.")
|
|
164
|
+
return
|
|
165
|
+
if len(project.jobs) == 0:
|
|
166
|
+
QMessageBox.warning(self.parent(),
|
|
167
|
+
"No Jobs In Project", "The current project has no job. "
|
|
168
|
+
"Create and run a job first.")
|
|
169
|
+
return
|
|
170
|
+
if len(project.jobs) > 1:
|
|
171
|
+
job_names = [job.params['name'] for job in project.jobs]
|
|
172
|
+
job_name = SelectionDialog.get_selection(
|
|
173
|
+
"Job Selection",
|
|
174
|
+
"Please select one of the active jobs:",
|
|
175
|
+
job_names
|
|
176
|
+
)
|
|
177
|
+
job = None
|
|
178
|
+
for job in project.jobs:
|
|
179
|
+
if job.params['name'] == job_name:
|
|
180
|
+
break
|
|
181
|
+
if job is None:
|
|
182
|
+
return
|
|
183
|
+
else:
|
|
184
|
+
job = project.jobs[0]
|
|
185
|
+
retouch_path = self.project_window.get_retouch_path(job)
|
|
186
|
+
if isinstance(retouch_path, list):
|
|
187
|
+
open_frames(self.retouch_window, None, ";".join(retouch_path))
|
|
188
|
+
else:
|
|
189
|
+
self.retouch_window.io_gui_handler.open_file(retouch_path)
|
|
190
|
+
|
|
94
191
|
|
|
95
192
|
class Application(QApplication):
|
|
96
193
|
def event(self, event):
|
shinestacker/gui/gui_run.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
# pylint: disable=C0114, C0115, C0116, E0611, R0903, R0915, R0914, R0917, R0913, R0902
|
|
2
|
+
import os
|
|
2
3
|
from PySide6.QtWidgets import (QWidget, QPushButton, QVBoxLayout, QHBoxLayout,
|
|
3
4
|
QMessageBox, QScrollArea, QSizePolicy, QFrame, QLabel, QComboBox)
|
|
4
5
|
from PySide6.QtGui import QColor
|
|
@@ -7,6 +8,7 @@ from PySide6.QtCore import Signal, Slot
|
|
|
7
8
|
from .. config.constants import constants
|
|
8
9
|
from .. config.gui_constants import gui_constants
|
|
9
10
|
from .colors import RED_BUTTON_STYLE, BLUE_BUTTON_STYLE, BLUE_COMBO_STYLE
|
|
11
|
+
from .. algorithms.utils import extension_tif_jpg, extension_pdf
|
|
10
12
|
from .gui_logging import LogWorker, QTextEditLogger
|
|
11
13
|
from .gui_images import GuiPdfView, GuiImageView, GuiOpenApp
|
|
12
14
|
from .colors import (
|
|
@@ -200,13 +202,12 @@ class RunWindow(QTextEditLogger):
|
|
|
200
202
|
label = QLabel(name, self)
|
|
201
203
|
label.setStyleSheet("QLabel {margin-top: 5px; font-weight: bold;}")
|
|
202
204
|
self.image_layout.addWidget(label)
|
|
203
|
-
|
|
204
|
-
if ext == 'pdf':
|
|
205
|
+
if extension_pdf(path):
|
|
205
206
|
image_view = GuiPdfView(path, self)
|
|
206
|
-
elif
|
|
207
|
+
elif extension_tif_jpg(path):
|
|
207
208
|
image_view = GuiImageView(path, self)
|
|
208
209
|
else:
|
|
209
|
-
raise RuntimeError("Can't visualize file type {
|
|
210
|
+
raise RuntimeError(f"Can't visualize file type {os.path.splitext(path)[1]}.")
|
|
210
211
|
self.image_views.append(image_view)
|
|
211
212
|
self.image_layout.addWidget(image_view)
|
|
212
213
|
max_width = max(pv.size().width() for pv in self.image_views) if self.image_views else 0
|
shinestacker/gui/main_window.py
CHANGED
|
@@ -278,7 +278,7 @@ class MainWindow(QMainWindow, LogManager):
|
|
|
278
278
|
current_action = None
|
|
279
279
|
if item:
|
|
280
280
|
index = self.job_list().row(item)
|
|
281
|
-
current_action = self.get_job_at(index)
|
|
281
|
+
current_action = self.project_editor.get_job_at(index)
|
|
282
282
|
self.set_current_job(index)
|
|
283
283
|
item = self.action_list().itemAt(self.action_list().viewport().mapFrom(self, event.pos()))
|
|
284
284
|
if item:
|
|
@@ -328,7 +328,7 @@ class MainWindow(QMainWindow, LogManager):
|
|
|
328
328
|
self.current_action_output_path = f"{self.current_action_working_path}/{op}"
|
|
329
329
|
if os.path.exists(self.current_action_output_path):
|
|
330
330
|
action_name = "Browse Output Path" + (f" > {name}" if name != '' else '')
|
|
331
|
-
n_files = len(next(os.walk(
|
|
331
|
+
n_files = len(next(os.walk(self.current_action_output_path))[2])
|
|
332
332
|
s = "" if n_files == 1 else "s"
|
|
333
333
|
action_name += f" ({n_files} file{s})"
|
|
334
334
|
self.browse_output_path_action = QAction(action_name)
|
shinestacker/gui/new_project.py
CHANGED
|
@@ -7,13 +7,12 @@ from PySide6.QtGui import QIcon
|
|
|
7
7
|
from PySide6.QtCore import Qt
|
|
8
8
|
from .. config.gui_constants import gui_constants
|
|
9
9
|
from .. config.constants import constants
|
|
10
|
-
from .. algorithms.utils import read_img
|
|
10
|
+
from .. algorithms.utils import read_img, extension_tif_jpg
|
|
11
11
|
from .. algorithms.stack import get_bunches
|
|
12
12
|
from .select_path_widget import create_select_file_paths_widget
|
|
13
13
|
from .base_form_dialog import BaseFormDialog
|
|
14
14
|
|
|
15
15
|
DEFAULT_NO_COUNT_LABEL = " - "
|
|
16
|
-
EXTENSIONS = ['jpg', 'jpeg', 'tif', 'tiff']
|
|
17
16
|
|
|
18
17
|
|
|
19
18
|
class NewProjectDialog(BaseFormDialog):
|
|
@@ -133,10 +132,8 @@ class NewProjectDialog(BaseFormDialog):
|
|
|
133
132
|
return 0
|
|
134
133
|
count = 0
|
|
135
134
|
for filename in os.listdir(path):
|
|
136
|
-
if
|
|
137
|
-
|
|
138
|
-
if ext in EXTENSIONS:
|
|
139
|
-
count += 1
|
|
135
|
+
if extension_tif_jpg(filename):
|
|
136
|
+
count += 1
|
|
140
137
|
return count
|
|
141
138
|
|
|
142
139
|
self.n_image_files = count_image_files(self.input_folder.text())
|
|
@@ -173,11 +170,9 @@ class NewProjectDialog(BaseFormDialog):
|
|
|
173
170
|
file_path = None
|
|
174
171
|
for filename in files:
|
|
175
172
|
full_path = os.path.join(path, filename)
|
|
176
|
-
if
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
file_path = full_path
|
|
180
|
-
break
|
|
173
|
+
if extension_tif_jpg(full_path):
|
|
174
|
+
file_path = full_path
|
|
175
|
+
break
|
|
181
176
|
if file_path is None:
|
|
182
177
|
QMessageBox.warning(
|
|
183
178
|
self, "Invalid input", "Could not find images now in the selected path")
|
|
@@ -204,11 +204,12 @@ class ProjectController(QObject):
|
|
|
204
204
|
input_path.append("focus-stack-depth-map")
|
|
205
205
|
if dialog.get_bunch_stack():
|
|
206
206
|
input_path.append("bunches")
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
207
|
+
multi_layer = ActionConfig(
|
|
208
|
+
constants.ACTION_MULTILAYER,
|
|
209
|
+
{
|
|
210
|
+
'name': 'multi-layer',
|
|
211
|
+
'input_path': constants.PATH_SEPARATOR.join(input_path)
|
|
212
|
+
})
|
|
212
213
|
job.add_sub_action(multi_layer)
|
|
213
214
|
self.add_job_to_project(job)
|
|
214
215
|
self.mark_as_modified(True)
|
|
@@ -5,7 +5,7 @@ import numpy as np
|
|
|
5
5
|
import cv2
|
|
6
6
|
from psdtags import PsdChannelId
|
|
7
7
|
from PySide6.QtCore import QThread, Signal
|
|
8
|
-
from .. algorithms.utils import read_img
|
|
8
|
+
from .. algorithms.utils import read_img, extension_tif, extension_jpg
|
|
9
9
|
from .. algorithms.multilayer import read_multilayer_tiff
|
|
10
10
|
|
|
11
11
|
|
|
@@ -50,15 +50,14 @@ class FileLoader(QThread):
|
|
|
50
50
|
raise RuntimeError(f"Path {path} does not exist.")
|
|
51
51
|
if not os.path.isfile(path):
|
|
52
52
|
raise RuntimeError(f"Path {path} is not a file.")
|
|
53
|
-
|
|
54
|
-
if extension in ['jpg', 'jpeg']:
|
|
53
|
+
if extension_jpg(path):
|
|
55
54
|
try:
|
|
56
55
|
stack = np.array([cv2.cvtColor(read_img(path), cv2.COLOR_BGR2RGB)])
|
|
57
56
|
return stack, [path.split('/')[-1].split('.')[0]]
|
|
58
57
|
except Exception as e:
|
|
59
58
|
traceback.print_tb(e.__traceback__)
|
|
60
59
|
return None, None
|
|
61
|
-
elif
|
|
60
|
+
elif extension_tif(path):
|
|
62
61
|
try:
|
|
63
62
|
psd_data = read_multilayer_tiff(path)
|
|
64
63
|
layers = []
|
|
@@ -330,9 +330,8 @@ class ImageEditorUI(QMainWindow, LayerCollectionHandler):
|
|
|
330
330
|
view_individual_action.setShortcut("L")
|
|
331
331
|
view_individual_action.triggered.connect(self.set_view_individual)
|
|
332
332
|
view_menu.addAction(view_individual_action)
|
|
333
|
-
view_menu.addSeparator()
|
|
334
333
|
|
|
335
|
-
toggle_view_master_individual_action = QAction("
|
|
334
|
+
toggle_view_master_individual_action = QAction("Toggle Master/Individual", self)
|
|
336
335
|
toggle_view_master_individual_action.setShortcut("T")
|
|
337
336
|
toggle_view_master_individual_action.triggered.connect(self.toggle_view_master_individual)
|
|
338
337
|
view_menu.addAction(toggle_view_master_individual_action)
|
|
@@ -56,9 +56,7 @@ class IOGuiHandler(QObject, LayerCollectionHandler):
|
|
|
56
56
|
self.set_master_layer(master_layer)
|
|
57
57
|
self.undo_manager.reset()
|
|
58
58
|
self.blank_layer = np.zeros(master_layer.shape[:2])
|
|
59
|
-
self.finish_loading_setup(
|
|
60
|
-
stack, None, master_layer, False,
|
|
61
|
-
f"Loaded: {self.current_file_path()}")
|
|
59
|
+
self.finish_loading_setup(f"Loaded: {self.current_file_path()}")
|
|
62
60
|
|
|
63
61
|
def on_file_error(self, error_msg):
|
|
64
62
|
QApplication.restoreOverrideCursor()
|
|
@@ -137,26 +135,23 @@ class IOGuiHandler(QObject, LayerCollectionHandler):
|
|
|
137
135
|
msg.setText(str(e))
|
|
138
136
|
msg.exec()
|
|
139
137
|
return
|
|
140
|
-
self.
|
|
141
|
-
stack
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
def finish_loading_setup(self, stack, labels, master, add_layers, message):
|
|
145
|
-
if add_layers:
|
|
146
|
-
if self.layer_stack() is None and len(stack) > 0:
|
|
147
|
-
self.set_layer_stack(np.array(stack))
|
|
148
|
-
if labels is None:
|
|
149
|
-
labels = self.layer_labels()
|
|
150
|
-
else:
|
|
151
|
-
self.set_layer_labels(labels)
|
|
152
|
-
self.set_master_layer(master)
|
|
153
|
-
self.blank_layer = np.zeros(master.shape[:2])
|
|
138
|
+
if self.layer_stack() is None and len(stack) > 0:
|
|
139
|
+
self.set_layer_stack(np.array(stack))
|
|
140
|
+
if labels is None:
|
|
141
|
+
labels = self.layer_labels()
|
|
154
142
|
else:
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
143
|
+
self.set_layer_labels(labels)
|
|
144
|
+
self.set_master_layer(master)
|
|
145
|
+
self.blank_layer = np.zeros(master.shape[:2])
|
|
146
|
+
else:
|
|
147
|
+
if labels is None:
|
|
148
|
+
labels = self.layer_labels()
|
|
149
|
+
for img, label in zip(stack, labels):
|
|
150
|
+
self.add_layer_label(label)
|
|
151
|
+
self.add_layer(img)
|
|
152
|
+
self.finish_loading_setup("Selected frames imported")
|
|
153
|
+
|
|
154
|
+
def finish_loading_setup(self, message):
|
|
160
155
|
self.display_manager.update_thumbnails()
|
|
161
156
|
self.mark_as_modified_requested.emit(True)
|
|
162
157
|
self.change_layer_requested.emit(0)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: shinestacker
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.4
|
|
4
4
|
Summary: ShineStacker
|
|
5
5
|
Author-email: Luca Lista <luka.lista@gmail.com>
|
|
6
6
|
License-Expression: LGPL-3.0
|
|
@@ -87,6 +87,7 @@ Pyramid methods in image processing
|
|
|
87
87
|
# License
|
|
88
88
|
|
|
89
89
|
<img src="https://www.gnu.org/graphics/lgplv3-147x51.png" alt="LGPL 3 logo">
|
|
90
|
+
|
|
90
91
|
- **Code**: The software is provided as is under the [GNU Lesser General Public License v3.0](https://www.gnu.org/licenses/lgpl-3.0.en.html). See [LICENSE](https://github.com/lucalista/shinestacker/blob/main/LICENSE) for details.
|
|
91
92
|
<img src='https://raw.githubusercontent.com/lucalista/shinestacker/main/src/shinestacker/gui/ico/shinestacker.png' width="150" referrerpolicy="no-referrer" alt="Shine Stacker Logo">
|
|
92
93
|
|
|
@@ -1,26 +1,26 @@
|
|
|
1
1
|
shinestacker/__init__.py,sha256=uq2fjAw2z_6TpH3mOcWFZ98GoEPRsNhTAK8N0MMm_e8,448
|
|
2
|
-
shinestacker/_version.py,sha256=
|
|
2
|
+
shinestacker/_version.py,sha256=Oi2b5pm3sFbESQW0xgj8kqwDPX_Hxmx4gNILYpLzYqI,21
|
|
3
3
|
shinestacker/algorithms/__init__.py,sha256=c4kRrdTLlVI70Q16XkI1RSmz5MD7npDqIpO_02jTG6g,747
|
|
4
4
|
shinestacker/algorithms/align.py,sha256=XT4DJoD5ZvpkC1-J3W3GWmWRsXJg3qJ-3zr9erT8oW0,17514
|
|
5
5
|
shinestacker/algorithms/balance.py,sha256=iSjO-pl0vQv58iEQ077EUcDTAExMKDBdtXmJXbMhazk,16721
|
|
6
6
|
shinestacker/algorithms/base_stack_algo.py,sha256=AFV2QkcFNaTcnISpsWHuAVy2De9hhaPcBNjE1O0h50I,1430
|
|
7
7
|
shinestacker/algorithms/denoise.py,sha256=GL3Z4_6MHxSa7Wo4ZzQECZS87tHBFqO0sIVF_jPuYQU,426
|
|
8
8
|
shinestacker/algorithms/depth_map.py,sha256=FOR5M0brO5-9NnXDY7TWpc3OtKKSuzrOSoBMe0cP6Ho,6076
|
|
9
|
-
shinestacker/algorithms/exif.py,sha256=
|
|
10
|
-
shinestacker/algorithms/multilayer.py,sha256
|
|
9
|
+
shinestacker/algorithms/exif.py,sha256=SM4ZDDe8hCJ3xY6053FNndOiwzEStzdp0WrXurlcHVc,9429
|
|
10
|
+
shinestacker/algorithms/multilayer.py,sha256=-pQXDlooSMGKPhMgF-_naXdkGdolclYvSD-RrjwLiyI,9328
|
|
11
11
|
shinestacker/algorithms/noise_detection.py,sha256=CDnN8pglxufY5Y-dT3mVooD4zPySdSq9CMgtDGMXBnA,8970
|
|
12
12
|
shinestacker/algorithms/pyramid.py,sha256=_Pk19lRQ21b3W3aHQ6DgAe9VVOfbsi2a9jrynF0qFVw,8610
|
|
13
13
|
shinestacker/algorithms/sharpen.py,sha256=h7PMJBYxucg194Usp_6pvItPUMFYbT-ebAc_-7XBFUw,949
|
|
14
14
|
shinestacker/algorithms/stack.py,sha256=FCU89Of-s6C_DuMleG06c8V6fnIm9MFInvkkKtTsGBo,4906
|
|
15
|
-
shinestacker/algorithms/stack_framework.py,sha256=
|
|
16
|
-
shinestacker/algorithms/utils.py,sha256=
|
|
15
|
+
shinestacker/algorithms/stack_framework.py,sha256=peAlUUl7y8OcquhjQoXpiwsEhZw6zgZnzwt1IDpf4aU,12466
|
|
16
|
+
shinestacker/algorithms/utils.py,sha256=0AeMVaFuhpUiIpUUFqrqAJ_-ohGVdX7-EdMyLoVflbg,3279
|
|
17
17
|
shinestacker/algorithms/vignetting.py,sha256=yW-1TF4tesLWfKQOS0XxRkOEN82U-YDmMaj09C9cH4M,9552
|
|
18
18
|
shinestacker/algorithms/white_balance.py,sha256=PMKsBtxOSn5aRr_Gkx1StHS4eN6kBN2EhNnhg4UG24g,501
|
|
19
19
|
shinestacker/app/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
20
20
|
shinestacker/app/about_dialog.py,sha256=pkH7nnxUP8yc0D3vRGd1jRb5cwi1nDVbQRk_OC9yLk8,4144
|
|
21
21
|
shinestacker/app/gui_utils.py,sha256=08TrCj2gFGsNsF6hG7ySO2y7wcQakM5PzERkeplqNFs,2344
|
|
22
22
|
shinestacker/app/help_menu.py,sha256=g8lKG_xZmXtNQaC3SIRzyROKVWva_PLEgZsQWh6zUcQ,499
|
|
23
|
-
shinestacker/app/main.py,sha256=
|
|
23
|
+
shinestacker/app/main.py,sha256=rcXlzsPErIN9ItbucsB6nz103vwNvsff6wSADOwFt6I,10301
|
|
24
24
|
shinestacker/app/open_frames.py,sha256=bsu32iJSYJQLe_tQQbvAU5DuMDVX6MRuNdE7B5lojZc,1488
|
|
25
25
|
shinestacker/app/project.py,sha256=W0u715LZne_PNJvg9msSy27ybIjgDXiEAQdJ7_6BjYI,2774
|
|
26
26
|
shinestacker/app/retouch.py,sha256=ZQ-nRKnHo6xurcP34RNqaAWkmuGBjJ5jE05hTQ_ycis,2482
|
|
@@ -41,11 +41,11 @@ shinestacker/gui/base_form_dialog.py,sha256=yYqMee1mzw9VBx8siBS0jDk1qqsTIKJUgdjh
|
|
|
41
41
|
shinestacker/gui/colors.py,sha256=m0pQQ-uvtIN1xmb_-N06BvC7pZYZZnq59ZSEJwutHuk,1432
|
|
42
42
|
shinestacker/gui/gui_images.py,sha256=e0KAXSPruZoRHrajfdlmOKBYoRJJQBDan1jgs7YFltY,5678
|
|
43
43
|
shinestacker/gui/gui_logging.py,sha256=kiZcrC2AFYCWgPZo0O5SKw-E5cFrezwf4anS3HjPuNw,8168
|
|
44
|
-
shinestacker/gui/gui_run.py,sha256=
|
|
45
|
-
shinestacker/gui/main_window.py,sha256=
|
|
44
|
+
shinestacker/gui/gui_run.py,sha256=Lf_hXWPk1bgAYumxqjPHSK8UfiAIUR7A047ECGKD_uU,15183
|
|
45
|
+
shinestacker/gui/main_window.py,sha256=l5iMk5aIi5nXPccnibB1tswc0agatrYgXULVkXo7OV0,24254
|
|
46
46
|
shinestacker/gui/menu_manager.py,sha256=_L6LOikB3impEYqilqwXc0WJuunishjz57ozZlrBn7Q,9616
|
|
47
|
-
shinestacker/gui/new_project.py,sha256=
|
|
48
|
-
shinestacker/gui/project_controller.py,sha256=
|
|
47
|
+
shinestacker/gui/new_project.py,sha256=DbndYiaxwjW0OJGJ3N9JwpwLrXVsdtoSSlFRf6T8yEA,10781
|
|
48
|
+
shinestacker/gui/project_controller.py,sha256=QJSQlwEJeXJyJnkp42D9NSqWBb8q2kLf_GvLfe3pe_c,15076
|
|
49
49
|
shinestacker/gui/project_converter.py,sha256=_AFfU2HYKPX78l6iX6bXJrlKpdjSl63pmKzrc6kQpn8,7348
|
|
50
50
|
shinestacker/gui/project_editor.py,sha256=uouzmUkrqouQlq-dqPOgSO16r1WOnGNV2v8jTcZlRXU,23749
|
|
51
51
|
shinestacker/gui/project_model.py,sha256=eRUmH3QmRzDtPtZoxgT6amKzN8_5XzwjHgEJeL-_JOE,4263
|
|
@@ -70,12 +70,12 @@ shinestacker/retouch/brush_tool.py,sha256=nxnEuvTioPNw1WeWsT20X1zl-LNZ8i-1ExOcih
|
|
|
70
70
|
shinestacker/retouch/denoise_filter.py,sha256=TDUHzhRKlKvCa3D5SCYCZKTpjcl81kGwmONsgSDtO1k,440
|
|
71
71
|
shinestacker/retouch/display_manager.py,sha256=XPbOBmoYc_jNA791WkWkOSaFHb0ztCZechl2p2KSlwQ,9597
|
|
72
72
|
shinestacker/retouch/exif_data.py,sha256=giqoIaMlhN6H3x8BAd73ghVHODmWIcD_QbSoDymQycU,1864
|
|
73
|
-
shinestacker/retouch/file_loader.py,sha256=
|
|
73
|
+
shinestacker/retouch/file_loader.py,sha256=z02-A8_uDZxayI1NFTxT2GVUvEBWStchX9hlN1o5-0U,4784
|
|
74
74
|
shinestacker/retouch/filter_manager.py,sha256=SdYIZkZBUvuB6wDG0moGWav5sfEvIcB9ioUJR5wJFts,388
|
|
75
75
|
shinestacker/retouch/icon_container.py,sha256=6gw1HO1bC2FrdB4dc_iH81DQuLjzuvRGksZ2hKLT9yA,585
|
|
76
|
-
shinestacker/retouch/image_editor_ui.py,sha256=
|
|
76
|
+
shinestacker/retouch/image_editor_ui.py,sha256=cMGiqyPGqJmBaXMAc0WImDPf_hmxO4KiJtaaSpiW9EU,29869
|
|
77
77
|
shinestacker/retouch/image_viewer.py,sha256=3ebrLHTDtGd_EbIT2nNFRUjH836rblmmK7jZ62YcJ2U,19564
|
|
78
|
-
shinestacker/retouch/io_gui_handler.py,sha256=
|
|
78
|
+
shinestacker/retouch/io_gui_handler.py,sha256=pT-49uP0GROMOjZ70LoMLgXHnqSDq8ieAlAKGw0t1TM,11418
|
|
79
79
|
shinestacker/retouch/io_manager.py,sha256=JUAA--AK0mVa1PTErJTnBFjaXIle5Qs7Ow0Wkd8at0o,2437
|
|
80
80
|
shinestacker/retouch/layer_collection.py,sha256=Q7zoCYRn__jYkfrEC2lY1uKHWfOUbsJ27xaYHIoKVxo,5730
|
|
81
81
|
shinestacker/retouch/shortcuts_help.py,sha256=SN4vNa_6yRAFaWxt5HpWn8FHgwmHrIs_wYwjl4iyDmg,3769
|
|
@@ -83,9 +83,9 @@ shinestacker/retouch/undo_manager.py,sha256=_ekbcOLcPbQLY7t-o8wf-b1uA6OPY9rRyLM-
|
|
|
83
83
|
shinestacker/retouch/unsharp_mask_filter.py,sha256=uFnth8fpZFGhdIgJCnS8x5v6lBQgJ3hX0CBke9pFXeM,3510
|
|
84
84
|
shinestacker/retouch/vignetting_filter.py,sha256=3WuoF38lQOIaU1MWmqviItuQn8NnbMN0nwV7pM9IJqU,3453
|
|
85
85
|
shinestacker/retouch/white_balance_filter.py,sha256=glMBYlmrF-i_OrB3sGUpjZE6X4FQdyLC4GBy2bWtaFc,6056
|
|
86
|
-
shinestacker-1.0.
|
|
87
|
-
shinestacker-1.0.
|
|
88
|
-
shinestacker-1.0.
|
|
89
|
-
shinestacker-1.0.
|
|
90
|
-
shinestacker-1.0.
|
|
91
|
-
shinestacker-1.0.
|
|
86
|
+
shinestacker-1.0.4.dist-info/licenses/LICENSE,sha256=pWgb-bBdsU2Gd2kwAXxketnm5W_2u8_fIeWEgojfrxs,7651
|
|
87
|
+
shinestacker-1.0.4.dist-info/METADATA,sha256=tKyo0ogsBjWAHk4lAshkSqx62swN-s01hXEO2mJkHOg,5903
|
|
88
|
+
shinestacker-1.0.4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
89
|
+
shinestacker-1.0.4.dist-info/entry_points.txt,sha256=SY6g1LqtMmp23q1DGwLUDT_dhLX9iss8DvWkiWLyo_4,166
|
|
90
|
+
shinestacker-1.0.4.dist-info/top_level.txt,sha256=MhijwnBVX5psfsyX8JZjqp3SYiWPsKe69f3Gnyze4Fw,13
|
|
91
|
+
shinestacker-1.0.4.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|