shinestacker 0.2.1__py3-none-any.whl → 0.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of shinestacker might be problematic. Click here for more details.
- shinestacker/__init__.py +13 -0
- shinestacker/_version.py +1 -1
- shinestacker/algorithms/__init__.py +5 -0
- shinestacker/algorithms/align.py +0 -1
- shinestacker/algorithms/core_utils.py +1 -1
- shinestacker/algorithms/denoise.py +9 -0
- shinestacker/algorithms/sharpen.py +22 -0
- shinestacker/algorithms/stack.py +2 -2
- shinestacker/app/main.py +0 -1
- shinestacker/app/project.py +0 -1
- shinestacker/app/retouch.py +0 -1
- shinestacker/config/__init__.py +2 -0
- shinestacker/core/__init__.py +10 -4
- shinestacker/gui/new_project.py +0 -1
- shinestacker/gui/project_editor.py +0 -1
- shinestacker/retouch/brush_controller.py +4 -4
- shinestacker/retouch/exif_data.py +0 -1
- shinestacker/retouch/image_editor.py +22 -1
- shinestacker/retouch/image_editor_ui.py +31 -9
- shinestacker/retouch/image_filters.py +463 -0
- shinestacker/retouch/shortcuts_help.py +0 -1
- shinestacker/retouch/undo_manager.py +49 -11
- shinestacker-0.3.0.dist-info/METADATA +84 -0
- {shinestacker-0.2.1.dist-info → shinestacker-0.3.0.dist-info}/RECORD +28 -25
- shinestacker-0.2.1.dist-info/METADATA +0 -57
- {shinestacker-0.2.1.dist-info → shinestacker-0.3.0.dist-info}/WHEEL +0 -0
- {shinestacker-0.2.1.dist-info → shinestacker-0.3.0.dist-info}/entry_points.txt +0 -0
- {shinestacker-0.2.1.dist-info → shinestacker-0.3.0.dist-info}/licenses/LICENSE +0 -0
- {shinestacker-0.2.1.dist-info → shinestacker-0.3.0.dist-info}/top_level.txt +0 -0
shinestacker/__init__.py
CHANGED
|
@@ -1,3 +1,16 @@
|
|
|
1
|
+
# flake8: noqa F401 F403
|
|
1
2
|
from ._version import __version__
|
|
3
|
+
from . import config
|
|
4
|
+
from . import core
|
|
5
|
+
from . import algorithms
|
|
6
|
+
from .config import __all__
|
|
7
|
+
from .core import __all__
|
|
8
|
+
from .algorithms import __all__
|
|
9
|
+
from .config import *
|
|
10
|
+
from .core import *
|
|
11
|
+
from .algorithms import *
|
|
2
12
|
|
|
3
13
|
__all__ = ['__version__']
|
|
14
|
+
#__all__ += config.__all__
|
|
15
|
+
#__all__ += core.__all__
|
|
16
|
+
#__all__ += algorithms.__all__
|
shinestacker/_version.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = '0.
|
|
1
|
+
__version__ = '0.3.0'
|
|
@@ -12,3 +12,8 @@ from .vignetting import Vignetting
|
|
|
12
12
|
import logging
|
|
13
13
|
logger = logging.getLogger(__name__)
|
|
14
14
|
logger.addHandler(logging.NullHandler())
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
'StackJob', 'CombinedActions', 'AlignFrames', 'BalanceFrames', 'FocusStackBunch', 'FocusStack',
|
|
18
|
+
'DepthMapStack', 'PyramidStack', 'MultiLayer', 'NoiseDetection', 'MaskNoise', 'Vignetting'
|
|
19
|
+
]
|
shinestacker/algorithms/align.py
CHANGED
|
@@ -62,7 +62,6 @@ def get_good_matches(des_0, des_1, matching_config=None):
|
|
|
62
62
|
|
|
63
63
|
|
|
64
64
|
def validate_align_config(detector, descriptor, match_method):
|
|
65
|
-
print(detector, descriptor, match_method)
|
|
66
65
|
if descriptor == constants.DESCRIPTOR_SIFT and match_method == constants.MATCHING_NORM_HAMMING:
|
|
67
66
|
raise ValueError("Descriptor SIFT requires matching method KNN")
|
|
68
67
|
if detector == constants.DETECTOR_ORB and descriptor == constants.DESCRIPTOR_AKAZE and \
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import cv2
|
|
2
|
+
import numpy as np
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def denoise(image, h_luminance, template_window_size=7, search_window_size=21):
|
|
6
|
+
norm_type = cv2.NORM_L2 if image.dtype == np.uint8 else cv2.NORM_L1
|
|
7
|
+
if image.dtype == np.uint16:
|
|
8
|
+
h_luminance = h_luminance * 256
|
|
9
|
+
return cv2.fastNlMeansDenoising(image, [h_luminance], None, template_window_size, search_window_size, norm_type)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import cv2
|
|
2
|
+
import numpy as np
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def unsharp_mask(image, radius=1.0, amount=1.0, threshold=0.0):
|
|
6
|
+
if image.dtype == np.uint16:
|
|
7
|
+
threshold = threshold * 256
|
|
8
|
+
blurred = cv2.GaussianBlur(image, (0, 0), radius)
|
|
9
|
+
if threshold == 0:
|
|
10
|
+
sharpened = cv2.addWeighted(image, 1.0 + amount, blurred, -amount, 0)
|
|
11
|
+
else:
|
|
12
|
+
image_float = image.astype(np.float32)
|
|
13
|
+
blurred_float = blurred.astype(np.float32)
|
|
14
|
+
diff = image_float - blurred_float
|
|
15
|
+
mask = np.abs(diff) > threshold
|
|
16
|
+
sharpened_float = np.where(mask, image_float + amount * diff, image_float)
|
|
17
|
+
if np.issubdtype(image.dtype, np.integer):
|
|
18
|
+
min_val, max_val = np.iinfo(image.dtype).min, np.iinfo(image.dtype).max
|
|
19
|
+
sharpened = np.clip(sharpened_float, min_val, max_val).astype(image.dtype)
|
|
20
|
+
else:
|
|
21
|
+
sharpened = sharpened_float.astype(image.dtype)
|
|
22
|
+
return sharpened
|
shinestacker/algorithms/stack.py
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import numpy as np
|
|
2
|
-
import cv2
|
|
3
2
|
import os
|
|
4
3
|
from .. config.constants import constants
|
|
5
4
|
from .. core.colors import color_str
|
|
@@ -8,6 +7,7 @@ from .. core.exceptions import InvalidOptionError
|
|
|
8
7
|
from .utils import write_img
|
|
9
8
|
from .stack_framework import FrameDirectory, ActionList
|
|
10
9
|
from .exif import copy_exif_from_file_to_file
|
|
10
|
+
from .denoise import denoise
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
class FocusStackBase:
|
|
@@ -30,7 +30,7 @@ class FocusStackBase:
|
|
|
30
30
|
out_filename = f"{self.output_dir}/{self.prefix}{in_filename[0]}." + '.'.join(in_filename[1:])
|
|
31
31
|
if self.denoise > 0:
|
|
32
32
|
self.sub_message_r(': denoise image')
|
|
33
|
-
stacked_img =
|
|
33
|
+
stacked_img = denoise(stacked_img, self.denoise, self.denoise)
|
|
34
34
|
write_img(out_filename, stacked_img)
|
|
35
35
|
if self.exif_path != '' and stacked_img.dtype == np.uint8:
|
|
36
36
|
self.sub_message_r(': copy exif data')
|
shinestacker/app/main.py
CHANGED
|
@@ -12,7 +12,6 @@ from shinestacker.config.config import config
|
|
|
12
12
|
config.init(DISABLE_TQDM=True, COMBINED_APP=True, DONT_USE_NATIVE_MENU=True)
|
|
13
13
|
from shinestacker.config.constants import constants
|
|
14
14
|
from shinestacker.core.logging import setup_logging
|
|
15
|
-
from shinestacker.core.core_utils import get_app_base_path
|
|
16
15
|
from shinestacker.gui.main_window import MainWindow
|
|
17
16
|
from shinestacker.retouch.image_editor_ui import ImageEditorUI
|
|
18
17
|
from shinestacker.app.gui_utils import disable_macos_special_menu_items
|
shinestacker/app/project.py
CHANGED
|
@@ -12,7 +12,6 @@ from shinestacker.config.config import config
|
|
|
12
12
|
config.init(DISABLE_TQDM=True, DONT_USE_NATIVE_MENU=True)
|
|
13
13
|
from shinestacker.config.constants import constants
|
|
14
14
|
from shinestacker.core.logging import setup_logging
|
|
15
|
-
from shinestacker.core.core_utils import get_app_base_path
|
|
16
15
|
from shinestacker.gui.main_window import MainWindow
|
|
17
16
|
from shinestacker.app.gui_utils import disable_macos_special_menu_items
|
|
18
17
|
from shinestacker.app.help_menu import add_help_action
|
shinestacker/app/retouch.py
CHANGED
|
@@ -6,7 +6,6 @@ from PySide6.QtGui import QIcon, QAction
|
|
|
6
6
|
from PySide6.QtCore import Qt, QEvent
|
|
7
7
|
from shinestacker.config.config import config
|
|
8
8
|
config.init(DISABLE_TQDM=True, DONT_USE_NATIVE_MENU=True)
|
|
9
|
-
from shinestacker.core.core_utils import get_app_base_path
|
|
10
9
|
from shinestacker.config.config import config
|
|
11
10
|
from shinestacker.config.constants import constants
|
|
12
11
|
from shinestacker.retouch.image_editor_ui import ImageEditorUI
|
shinestacker/config/__init__.py
CHANGED
shinestacker/core/__init__.py
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# flake8: noqa F401
|
|
2
|
-
from .logging import setup_logging
|
|
3
|
-
from .exceptions import (FocusStackError, InvalidOptionError, ImageLoadError, AlignmentError,
|
|
4
|
-
|
|
5
|
-
from .framework import
|
|
2
|
+
from .logging import setup_logging
|
|
3
|
+
from .exceptions import (FocusStackError, InvalidOptionError, ImageLoadError, ImageSaveError, AlignmentError,
|
|
4
|
+
BitDepthError, ShapeError, RunStopException)
|
|
5
|
+
from .framework import Job
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
'setup_logging',
|
|
9
|
+
'FocusStackError', 'InvalidOptionError', 'ImageLoadError', 'ImageSaveError',
|
|
10
|
+
'AlignmentError','BitDepthError', 'ShapeError', 'RunStopException',
|
|
11
|
+
'Job']
|
shinestacker/gui/new_project.py
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import os
|
|
2
2
|
from dataclasses import dataclass
|
|
3
3
|
from PySide6.QtWidgets import QMainWindow, QListWidget, QMessageBox, QDialog, QListWidgetItem, QLabel
|
|
4
|
-
from PySide6.QtGui import QIcon
|
|
5
4
|
from PySide6.QtCore import Qt
|
|
6
5
|
from .. config.constants import constants
|
|
7
6
|
from .colors import ColorPalette
|
|
@@ -22,7 +22,7 @@ class BrushController:
|
|
|
22
22
|
y_start, y_end = max(0, y_center - radius), min(h, y_center + radius + 1)
|
|
23
23
|
if x_start >= x_end or y_start >= y_end:
|
|
24
24
|
return 0, 0, 0, 0
|
|
25
|
-
mask = self.
|
|
25
|
+
mask = self.get_brush_mask(radius)
|
|
26
26
|
if mask is None:
|
|
27
27
|
return 0, 0, 0, 0
|
|
28
28
|
master_area = master_layer[y_start:y_end, x_start:x_end]
|
|
@@ -31,10 +31,10 @@ class BrushController:
|
|
|
31
31
|
mask_layer_area = mask_layer[y_start:y_end, x_start:x_end]
|
|
32
32
|
mask_area = mask[y_start - (y_center - radius):y_end - (y_center - radius), x_start - (x_center - radius):x_end - (x_center - radius)]
|
|
33
33
|
mask_layer_area[:] = np.clip(mask_layer_area + mask_area * self.brush.flow / 100.0, 0.0, 1.0) # np.maximum(mask_layer_area, mask_area)
|
|
34
|
-
self.
|
|
34
|
+
self.apply_mask(master_area, source_area, mask_layer_area, dest_area)
|
|
35
35
|
return x_start, y_start, x_end, y_end
|
|
36
36
|
|
|
37
|
-
def
|
|
37
|
+
def get_brush_mask(self, radius):
|
|
38
38
|
mask_key = (radius, self.brush.hardness)
|
|
39
39
|
if mask_key not in self._brush_mask_cache.keys():
|
|
40
40
|
full_mask = create_brush_mask(size=radius * 2 + 1, hardness_percent=self.brush.hardness,
|
|
@@ -42,7 +42,7 @@ class BrushController:
|
|
|
42
42
|
self._brush_mask_cache[mask_key] = full_mask
|
|
43
43
|
return self._brush_mask_cache[mask_key]
|
|
44
44
|
|
|
45
|
-
def
|
|
45
|
+
def apply_mask(self, master_area, source_area, mask_area, dest_area):
|
|
46
46
|
opacity_factor = float(self.brush.opacity) / 100.0
|
|
47
47
|
effective_mask = np.clip(mask_area * opacity_factor, 0, 1)
|
|
48
48
|
dtype = master_area.dtype
|
|
@@ -3,7 +3,6 @@ from PIL.TiffImagePlugin import IFDRational
|
|
|
3
3
|
from PySide6.QtWidgets import QFormLayout, QHBoxLayout, QPushButton, QDialog, QLabel
|
|
4
4
|
from PySide6.QtGui import QIcon
|
|
5
5
|
from PySide6.QtCore import Qt
|
|
6
|
-
from .. core.core_utils import get_app_base_path
|
|
7
6
|
from .. algorithms.exif import exif_dict
|
|
8
7
|
|
|
9
8
|
|
|
@@ -65,6 +65,9 @@ class ImageEditor(QMainWindow):
|
|
|
65
65
|
self.brush = Brush()
|
|
66
66
|
self.brush_controller = BrushController(self.brush)
|
|
67
67
|
self.undo_manager = UndoManager()
|
|
68
|
+
self.undo_action = None
|
|
69
|
+
self.redo_action = None
|
|
70
|
+
self.undo_manager.stack_changed.connect(self.update_undo_redo_actions)
|
|
68
71
|
self.loader_thread = None
|
|
69
72
|
|
|
70
73
|
def keyPressEvent(self, event):
|
|
@@ -201,6 +204,7 @@ class ImageEditor(QMainWindow):
|
|
|
201
204
|
self.shape = np.array(master_layer).shape
|
|
202
205
|
self.dtype = master_layer.dtype
|
|
203
206
|
self.modified = False
|
|
207
|
+
self.undo_manager.reset()
|
|
204
208
|
self.blank_layer = np.zeros(master_layer.shape[:2])
|
|
205
209
|
self.update_thumbnails()
|
|
206
210
|
self.image_viewer.setup_brush_cursor()
|
|
@@ -381,6 +385,7 @@ class ImageEditor(QMainWindow):
|
|
|
381
385
|
self.current_layer = 0
|
|
382
386
|
self.current_file_path = ''
|
|
383
387
|
self.modified = False
|
|
388
|
+
self.undo_manager.reset()
|
|
384
389
|
self.image_viewer.clear_image()
|
|
385
390
|
self.update_thumbnails()
|
|
386
391
|
self.update_title()
|
|
@@ -644,6 +649,22 @@ class ImageEditor(QMainWindow):
|
|
|
644
649
|
if self.update_timer.isActive():
|
|
645
650
|
self.display_master_layer()
|
|
646
651
|
self.update_master_thumbnail()
|
|
647
|
-
self.undo_manager.save_undo_state(self.master_layer_copy)
|
|
652
|
+
self.undo_manager.save_undo_state(self.master_layer_copy, 'Brush Stroke')
|
|
648
653
|
self.update_timer.stop()
|
|
649
654
|
self.mark_as_modified()
|
|
655
|
+
|
|
656
|
+
def update_undo_redo_actions(self, has_undo, undo_desc, has_redo, redo_desc):
|
|
657
|
+
if self.undo_action:
|
|
658
|
+
if has_undo:
|
|
659
|
+
self.undo_action.setText(f"Undo {undo_desc}")
|
|
660
|
+
self.undo_action.setEnabled(True)
|
|
661
|
+
else:
|
|
662
|
+
self.undo_action.setText("Undo")
|
|
663
|
+
self.undo_action.setEnabled(False)
|
|
664
|
+
if self.redo_action:
|
|
665
|
+
if has_redo:
|
|
666
|
+
self.redo_action.setText(f"Redo {redo_desc}")
|
|
667
|
+
self.redo_action.setEnabled(True)
|
|
668
|
+
else:
|
|
669
|
+
self.redo_action.setText("Redo")
|
|
670
|
+
self.redo_action.setEnabled(False)
|
|
@@ -3,7 +3,7 @@ from PySide6.QtGui import QShortcut, QKeySequence, QAction, QActionGroup
|
|
|
3
3
|
from PySide6.QtCore import Qt, QSize, Signal
|
|
4
4
|
from PySide6.QtGui import QGuiApplication
|
|
5
5
|
from .. config.gui_constants import gui_constants
|
|
6
|
-
from .
|
|
6
|
+
from .image_filters import ImageFilters
|
|
7
7
|
from .image_viewer import ImageViewer
|
|
8
8
|
from .shortcuts_help import ShortcutsHelp
|
|
9
9
|
|
|
@@ -18,21 +18,19 @@ def brush_size_to_slider(size):
|
|
|
18
18
|
|
|
19
19
|
|
|
20
20
|
class ClickableLabel(QLabel):
|
|
21
|
-
|
|
22
|
-
doubleClicked = Signal() # PySide6 usa Signal invece di pyqtSignal
|
|
21
|
+
doubleClicked = Signal()
|
|
23
22
|
|
|
24
23
|
def __init__(self, text, parent=None):
|
|
25
24
|
super().__init__(text, parent)
|
|
26
25
|
self.setMouseTracking(True)
|
|
27
26
|
|
|
28
27
|
def mouseDoubleClickEvent(self, event):
|
|
29
|
-
"""Override del doppio click"""
|
|
30
28
|
if event.button() == Qt.LeftButton:
|
|
31
29
|
self.doubleClicked.emit()
|
|
32
30
|
super().mouseDoubleClickEvent(event)
|
|
33
31
|
|
|
34
32
|
|
|
35
|
-
class ImageEditorUI(
|
|
33
|
+
class ImageEditorUI(ImageFilters):
|
|
36
34
|
def __init__(self):
|
|
37
35
|
super().__init__()
|
|
38
36
|
self.setup_ui()
|
|
@@ -225,10 +223,16 @@ class ImageEditorUI(ImageEditor):
|
|
|
225
223
|
file_menu.addAction("Import &EXIF data", self.select_exif_path)
|
|
226
224
|
|
|
227
225
|
edit_menu = menubar.addMenu("&Edit")
|
|
228
|
-
undo_action = QAction("Undo
|
|
229
|
-
undo_action.
|
|
230
|
-
undo_action.
|
|
231
|
-
|
|
226
|
+
self.undo_action = QAction("Undo", self)
|
|
227
|
+
self.undo_action.setEnabled(False)
|
|
228
|
+
self.undo_action.setShortcut("Ctrl+Z")
|
|
229
|
+
self.undo_action.triggered.connect(self.undo_last_brush)
|
|
230
|
+
edit_menu.addAction(self.undo_action)
|
|
231
|
+
self.redo_action = QAction("Redo", self)
|
|
232
|
+
self.redo_action.setEnabled(False)
|
|
233
|
+
self.redo_action.setShortcut("Ctrl+Y")
|
|
234
|
+
self.redo_action.triggered.connect(self.redo_last_brush)
|
|
235
|
+
edit_menu.addAction(self.redo_action)
|
|
232
236
|
edit_menu.addSeparator()
|
|
233
237
|
|
|
234
238
|
copy_action = QAction("Copy Layer to Master", self)
|
|
@@ -314,6 +318,18 @@ class ImageEditorUI(ImageEditor):
|
|
|
314
318
|
cursor_group.addAction(brush_action)
|
|
315
319
|
cursor_group.setExclusive(True)
|
|
316
320
|
|
|
321
|
+
filter_menu = menubar.addMenu("&Filter")
|
|
322
|
+
filter_menu.setObjectName("Filter")
|
|
323
|
+
denoise_action = QAction("Denoise", self)
|
|
324
|
+
denoise_action.triggered.connect(self.denoise)
|
|
325
|
+
filter_menu.addAction(denoise_action)
|
|
326
|
+
unsharp_mask_action = QAction("Unsharp Mask", self)
|
|
327
|
+
unsharp_mask_action.triggered.connect(self.unsharp_mask)
|
|
328
|
+
filter_menu.addAction(unsharp_mask_action)
|
|
329
|
+
white_balance_action = QAction("White Balance", self)
|
|
330
|
+
white_balance_action.triggered.connect(self.white_balance)
|
|
331
|
+
filter_menu.addAction(white_balance_action)
|
|
332
|
+
|
|
317
333
|
help_menu = menubar.addMenu("&Help")
|
|
318
334
|
help_menu.setObjectName("Help")
|
|
319
335
|
shortcuts_help_action = QAction("Shortcuts and mouse", self)
|
|
@@ -373,6 +389,12 @@ class ImageEditorUI(ImageEditor):
|
|
|
373
389
|
self.mark_as_modified()
|
|
374
390
|
self.statusBar().showMessage("Undo applied", 2000)
|
|
375
391
|
|
|
392
|
+
def redo_last_brush(self):
|
|
393
|
+
if self.undo_manager.redo(self.master_layer):
|
|
394
|
+
self.display_current_view()
|
|
395
|
+
self.mark_as_modified()
|
|
396
|
+
self.statusBar().showMessage("Redo applied", 2000)
|
|
397
|
+
|
|
376
398
|
def handle_temp_view(self, start):
|
|
377
399
|
if start:
|
|
378
400
|
self.start_temp_view()
|
|
@@ -0,0 +1,463 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from PySide6.QtWidgets import (QHBoxLayout,
|
|
3
|
+
QPushButton, QFrame, QVBoxLayout, QLabel, QDialog, QApplication, QSlider,
|
|
4
|
+
QCheckBox, QDialogButtonBox)
|
|
5
|
+
from PySide6.QtGui import QCursor
|
|
6
|
+
from PySide6.QtCore import Qt, QTimer, QThread, Signal
|
|
7
|
+
from .. algorithms.denoise import denoise
|
|
8
|
+
from .. algorithms.sharpen import unsharp_mask
|
|
9
|
+
from .. algorithms.white_balance import white_balance_from_rgb
|
|
10
|
+
from .image_editor import ImageEditor
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ImageFilters(ImageEditor):
|
|
14
|
+
def __init__(self):
|
|
15
|
+
super().__init__()
|
|
16
|
+
|
|
17
|
+
def denoise(self):
|
|
18
|
+
max_range = 500.0
|
|
19
|
+
max_value = 5.00
|
|
20
|
+
initial_value = 2.5
|
|
21
|
+
self.master_layer_copy = self.master_layer.copy()
|
|
22
|
+
dlg = QDialog(self)
|
|
23
|
+
dlg.setWindowTitle("Denoise")
|
|
24
|
+
dlg.setMinimumWidth(600)
|
|
25
|
+
layout = QVBoxLayout(dlg)
|
|
26
|
+
slider_layout = QHBoxLayout()
|
|
27
|
+
slider = QSlider(Qt.Horizontal)
|
|
28
|
+
slider.setRange(0, max_range)
|
|
29
|
+
slider.setValue(initial_value / max_value * max_range)
|
|
30
|
+
slider_layout.addWidget(slider)
|
|
31
|
+
value_label = QLabel(f"{max_value:.2f}")
|
|
32
|
+
slider_layout.addWidget(value_label)
|
|
33
|
+
layout.addLayout(slider_layout)
|
|
34
|
+
preview_check = QCheckBox("Preview")
|
|
35
|
+
preview_check.setChecked(True)
|
|
36
|
+
layout.addWidget(preview_check)
|
|
37
|
+
button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
|
|
38
|
+
layout.addWidget(button_box)
|
|
39
|
+
last_preview_strength = None
|
|
40
|
+
preview_timer = QTimer()
|
|
41
|
+
preview_timer.setSingleShot(True)
|
|
42
|
+
preview_timer.setInterval(200)
|
|
43
|
+
active_worker = None
|
|
44
|
+
last_request_id = 0
|
|
45
|
+
|
|
46
|
+
class PreviewWorker(QThread):
|
|
47
|
+
finished = Signal(np.ndarray, int)
|
|
48
|
+
|
|
49
|
+
def __init__(self, image, strength, request_id):
|
|
50
|
+
super().__init__()
|
|
51
|
+
self.image = image
|
|
52
|
+
self.strength = strength
|
|
53
|
+
self.request_id = request_id
|
|
54
|
+
|
|
55
|
+
def run(self):
|
|
56
|
+
result = denoise(self.image, self.strength)
|
|
57
|
+
self.finished.emit(result, self.request_id)
|
|
58
|
+
|
|
59
|
+
def slider_changed(value):
|
|
60
|
+
float_value = max_value * value / max_range
|
|
61
|
+
value_label.setText(f"{float_value:.2f}")
|
|
62
|
+
if preview_check.isChecked():
|
|
63
|
+
nonlocal last_preview_strength
|
|
64
|
+
last_preview_strength = float_value
|
|
65
|
+
preview_timer.start()
|
|
66
|
+
|
|
67
|
+
def do_preview():
|
|
68
|
+
nonlocal active_worker, last_request_id
|
|
69
|
+
if last_preview_strength is None:
|
|
70
|
+
return
|
|
71
|
+
if active_worker and active_worker.isRunning():
|
|
72
|
+
active_worker.quit()
|
|
73
|
+
active_worker.wait()
|
|
74
|
+
last_request_id += 1
|
|
75
|
+
current_request_id = last_request_id
|
|
76
|
+
active_worker = PreviewWorker(
|
|
77
|
+
self.master_layer_copy.copy(),
|
|
78
|
+
last_preview_strength,
|
|
79
|
+
current_request_id
|
|
80
|
+
)
|
|
81
|
+
active_worker.finished.connect(
|
|
82
|
+
lambda img, rid: set_preview(img, rid, current_request_id)
|
|
83
|
+
)
|
|
84
|
+
active_worker.start()
|
|
85
|
+
|
|
86
|
+
def set_preview(img, request_id, expected_id):
|
|
87
|
+
if request_id != expected_id:
|
|
88
|
+
return
|
|
89
|
+
self.master_layer = img
|
|
90
|
+
self.display_master_layer()
|
|
91
|
+
dlg.activateWindow()
|
|
92
|
+
slider.setFocus()
|
|
93
|
+
|
|
94
|
+
def on_preview_toggled(checked):
|
|
95
|
+
nonlocal last_preview_strength
|
|
96
|
+
if checked:
|
|
97
|
+
last_preview_strength = max_value * slider.value() / max_range
|
|
98
|
+
do_preview()
|
|
99
|
+
else:
|
|
100
|
+
self.master_layer = self.master_layer_copy.copy()
|
|
101
|
+
self.display_master_layer()
|
|
102
|
+
dlg.activateWindow()
|
|
103
|
+
slider.setFocus()
|
|
104
|
+
button_box.setFocus()
|
|
105
|
+
|
|
106
|
+
slider.valueChanged.connect(slider_changed)
|
|
107
|
+
preview_timer.timeout.connect(do_preview)
|
|
108
|
+
preview_check.stateChanged.connect(on_preview_toggled)
|
|
109
|
+
button_box.accepted.connect(dlg.accept)
|
|
110
|
+
button_box.rejected.connect(dlg.reject)
|
|
111
|
+
|
|
112
|
+
def run_initial_preview():
|
|
113
|
+
slider_changed(slider.value())
|
|
114
|
+
|
|
115
|
+
QTimer.singleShot(0, run_initial_preview)
|
|
116
|
+
slider.setFocus()
|
|
117
|
+
if dlg.exec_() == QDialog.Accepted:
|
|
118
|
+
strength = max_value * float(slider.value()) / max_range
|
|
119
|
+
h, w = self.master_layer.shape[:2]
|
|
120
|
+
self.undo_manager.extend_undo_area(0, 0, w, h)
|
|
121
|
+
self.undo_manager.save_undo_state(self.master_layer_copy, 'Denoise')
|
|
122
|
+
self.master_layer = denoise(self.master_layer_copy, strength)
|
|
123
|
+
self.master_layer_copy = self.master_layer.copy()
|
|
124
|
+
self.display_master_layer()
|
|
125
|
+
self.update_master_thumbnail()
|
|
126
|
+
self.mark_as_modified()
|
|
127
|
+
else:
|
|
128
|
+
self.master_layer = self.master_layer_copy.copy()
|
|
129
|
+
self.display_master_layer()
|
|
130
|
+
|
|
131
|
+
def unsharp_mask(self):
|
|
132
|
+
max_range = 500.0
|
|
133
|
+
max_radius = 4.0
|
|
134
|
+
max_amount = 3.0
|
|
135
|
+
max_threshold = 100.0
|
|
136
|
+
initial_radius = 1.0
|
|
137
|
+
initial_amount = 0.5
|
|
138
|
+
initial_threshold = 0.0
|
|
139
|
+
self.master_layer_copy = self.master_layer.copy()
|
|
140
|
+
dlg = QDialog(self)
|
|
141
|
+
dlg.setWindowTitle("Unsharp Mask")
|
|
142
|
+
dlg.setMinimumWidth(600)
|
|
143
|
+
layout = QVBoxLayout(dlg)
|
|
144
|
+
params = {
|
|
145
|
+
"Radius": (max_radius, initial_radius, "{:.2f}"),
|
|
146
|
+
"Amount": (max_amount, initial_amount, "{:.2%}"),
|
|
147
|
+
"Threshold": (max_threshold, initial_threshold, "{:.2f}")
|
|
148
|
+
}
|
|
149
|
+
sliders = {}
|
|
150
|
+
value_labels = {}
|
|
151
|
+
for name, (max_val, init_val, fmt) in params.items():
|
|
152
|
+
param_layout = QHBoxLayout()
|
|
153
|
+
name_label = QLabel(f"{name}:")
|
|
154
|
+
param_layout.addWidget(name_label)
|
|
155
|
+
slider = QSlider(Qt.Horizontal)
|
|
156
|
+
slider.setRange(0, max_range)
|
|
157
|
+
slider.setValue(init_val / max_val * max_range)
|
|
158
|
+
param_layout.addWidget(slider)
|
|
159
|
+
value_label = QLabel(fmt.format(init_val))
|
|
160
|
+
param_layout.addWidget(value_label)
|
|
161
|
+
layout.addLayout(param_layout)
|
|
162
|
+
sliders[name] = slider
|
|
163
|
+
value_labels[name] = value_label
|
|
164
|
+
preview_check = QCheckBox("Preview")
|
|
165
|
+
preview_check.setChecked(True)
|
|
166
|
+
layout.addWidget(preview_check)
|
|
167
|
+
button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
|
|
168
|
+
layout.addWidget(button_box)
|
|
169
|
+
last_preview_params = None
|
|
170
|
+
preview_timer = QTimer()
|
|
171
|
+
preview_timer.setSingleShot(True)
|
|
172
|
+
preview_timer.setInterval(200)
|
|
173
|
+
active_worker = None
|
|
174
|
+
last_request_id = 0
|
|
175
|
+
|
|
176
|
+
class UnsharpWorker(QThread):
|
|
177
|
+
finished = Signal(np.ndarray, int)
|
|
178
|
+
|
|
179
|
+
def __init__(self, image, radius, amount, threshold, request_id):
|
|
180
|
+
super().__init__()
|
|
181
|
+
self.image = image
|
|
182
|
+
self.radius = radius
|
|
183
|
+
self.amount = amount
|
|
184
|
+
self.threshold = threshold
|
|
185
|
+
self.request_id = request_id
|
|
186
|
+
|
|
187
|
+
def run(self):
|
|
188
|
+
result = unsharp_mask(self.image, max(0.01, self.radius), self.amount, self.threshold)
|
|
189
|
+
self.finished.emit(result, self.request_id)
|
|
190
|
+
|
|
191
|
+
def update_param_value(name, value, max_val, fmt):
|
|
192
|
+
float_value = max_val * value / max_range
|
|
193
|
+
value_labels[name].setText(fmt.format(float_value))
|
|
194
|
+
if preview_check.isChecked():
|
|
195
|
+
nonlocal last_preview_params
|
|
196
|
+
last_preview_params = (
|
|
197
|
+
max_radius * sliders["Radius"].value() / max_range,
|
|
198
|
+
max_amount * sliders["Amount"].value() / max_range,
|
|
199
|
+
max_threshold * sliders["Threshold"].value() / max_range
|
|
200
|
+
)
|
|
201
|
+
preview_timer.start()
|
|
202
|
+
sliders["Radius"].valueChanged.connect(
|
|
203
|
+
lambda v: update_param_value("Radius", v, params["Radius"][0], params["Radius"][2]))
|
|
204
|
+
sliders["Amount"].valueChanged.connect(
|
|
205
|
+
lambda v: update_param_value("Amount", v, params["Amount"][0], params["Amount"][2]))
|
|
206
|
+
sliders["Threshold"].valueChanged.connect(
|
|
207
|
+
lambda v: update_param_value("Threshold", v, params["Threshold"][0], params["Threshold"][2]))
|
|
208
|
+
|
|
209
|
+
def do_preview():
|
|
210
|
+
nonlocal active_worker, last_request_id
|
|
211
|
+
if last_preview_params is None:
|
|
212
|
+
return
|
|
213
|
+
if active_worker and active_worker.isRunning():
|
|
214
|
+
active_worker.quit()
|
|
215
|
+
active_worker.wait()
|
|
216
|
+
last_request_id += 1
|
|
217
|
+
current_request_id = last_request_id
|
|
218
|
+
radius, amount, threshold = last_preview_params
|
|
219
|
+
active_worker = UnsharpWorker(
|
|
220
|
+
self.master_layer_copy.copy(),
|
|
221
|
+
radius,
|
|
222
|
+
amount,
|
|
223
|
+
threshold,
|
|
224
|
+
current_request_id
|
|
225
|
+
)
|
|
226
|
+
active_worker.finished.connect(lambda img, rid: set_preview(img, rid, current_request_id))
|
|
227
|
+
active_worker.start()
|
|
228
|
+
|
|
229
|
+
def set_preview(img, request_id, expected_id):
|
|
230
|
+
if request_id != expected_id:
|
|
231
|
+
return
|
|
232
|
+
self.master_layer = img
|
|
233
|
+
self.display_master_layer()
|
|
234
|
+
dlg.activateWindow()
|
|
235
|
+
sliders["Radius"].setFocus()
|
|
236
|
+
|
|
237
|
+
def on_preview_toggled(checked):
|
|
238
|
+
nonlocal last_preview_params
|
|
239
|
+
if checked:
|
|
240
|
+
last_preview_params = (
|
|
241
|
+
max_radius * sliders["Radius"].value() / max_range,
|
|
242
|
+
max_amount * sliders["Amount"].value() / max_range,
|
|
243
|
+
max_threshold * sliders["Threshold"].value() / max_range
|
|
244
|
+
)
|
|
245
|
+
do_preview()
|
|
246
|
+
else:
|
|
247
|
+
self.master_layer = self.master_layer_copy.copy()
|
|
248
|
+
self.display_master_layer()
|
|
249
|
+
dlg.activateWindow()
|
|
250
|
+
sliders["Radius"].setFocus()
|
|
251
|
+
|
|
252
|
+
preview_timer.timeout.connect(do_preview)
|
|
253
|
+
preview_check.stateChanged.connect(on_preview_toggled)
|
|
254
|
+
button_box.accepted.connect(dlg.accept)
|
|
255
|
+
button_box.rejected.connect(dlg.reject)
|
|
256
|
+
|
|
257
|
+
def run_initial_preview():
|
|
258
|
+
nonlocal last_preview_params
|
|
259
|
+
last_preview_params = (
|
|
260
|
+
initial_radius,
|
|
261
|
+
initial_amount,
|
|
262
|
+
initial_threshold
|
|
263
|
+
)
|
|
264
|
+
do_preview()
|
|
265
|
+
|
|
266
|
+
QTimer.singleShot(0, run_initial_preview)
|
|
267
|
+
sliders["Radius"].setFocus()
|
|
268
|
+
if dlg.exec_() == QDialog.Accepted:
|
|
269
|
+
radius = max_radius * sliders["Radius"].value() / max_range
|
|
270
|
+
amount = max_amount * sliders["Amount"].value() / max_range
|
|
271
|
+
threshold = max_threshold * sliders["Threshold"].value() / max_range
|
|
272
|
+
h, w = self.master_layer.shape[:2]
|
|
273
|
+
self.undo_manager.extend_undo_area(0, 0, w, h)
|
|
274
|
+
self.undo_manager.save_undo_state(self.master_layer_copy, 'Unsharp Mask')
|
|
275
|
+
self.master_layer = unsharp_mask(self.master_layer_copy, max(0.01, radius), amount, threshold)
|
|
276
|
+
self.master_layer_copy = self.master_layer.copy()
|
|
277
|
+
self.display_master_layer()
|
|
278
|
+
self.update_master_thumbnail()
|
|
279
|
+
self.mark_as_modified()
|
|
280
|
+
else:
|
|
281
|
+
self.master_layer = self.master_layer_copy.copy()
|
|
282
|
+
self.display_master_layer()
|
|
283
|
+
|
|
284
|
+
def white_balance(self):
|
|
285
|
+
if hasattr(self, 'wb_dialog') and self.wb_dialog:
|
|
286
|
+
self.wb_dialog.activateWindow()
|
|
287
|
+
self.wb_dialog.raise_()
|
|
288
|
+
return
|
|
289
|
+
max_range = 255
|
|
290
|
+
initial_val = 128
|
|
291
|
+
initial_rgb = (initial_val, initial_val, initial_val)
|
|
292
|
+
cursor_style = self.image_viewer.cursor_style
|
|
293
|
+
self.image_viewer.set_cursor_style('outline')
|
|
294
|
+
if self.image_viewer.brush_cursor:
|
|
295
|
+
self.image_viewer.brush_cursor.hide()
|
|
296
|
+
self.master_layer_copy = self.master_layer.copy()
|
|
297
|
+
self.brush_preview.hide()
|
|
298
|
+
self.wb_dialog = dlg = QDialog(self)
|
|
299
|
+
dlg.setWindowTitle("White Balance")
|
|
300
|
+
dlg.setMinimumWidth(600)
|
|
301
|
+
layout = QVBoxLayout(dlg)
|
|
302
|
+
row_layout = QHBoxLayout()
|
|
303
|
+
color_preview = QFrame()
|
|
304
|
+
color_preview.setFixedHeight(80)
|
|
305
|
+
color_preview.setFixedWidth(80)
|
|
306
|
+
color_preview.setStyleSheet("background-color: rgb(128,128,128);")
|
|
307
|
+
row_layout.addWidget(color_preview)
|
|
308
|
+
sliders_layout = QVBoxLayout()
|
|
309
|
+
sliders = {}
|
|
310
|
+
value_labels = {}
|
|
311
|
+
rgb_layouts = {}
|
|
312
|
+
for name, init_val in zip(("R", "G", "B"), initial_rgb):
|
|
313
|
+
row = QHBoxLayout()
|
|
314
|
+
label = QLabel(f"{name}:")
|
|
315
|
+
row.addWidget(label)
|
|
316
|
+
slider = QSlider(Qt.Horizontal)
|
|
317
|
+
slider.setRange(0, max_range)
|
|
318
|
+
slider.setValue(init_val)
|
|
319
|
+
row.addWidget(slider)
|
|
320
|
+
val_label = QLabel(str(init_val))
|
|
321
|
+
row.addWidget(val_label)
|
|
322
|
+
sliders_layout.addLayout(row)
|
|
323
|
+
sliders[name] = slider
|
|
324
|
+
value_labels[name] = val_label
|
|
325
|
+
rgb_layouts[name] = row
|
|
326
|
+
row_layout.addLayout(sliders_layout)
|
|
327
|
+
layout.addLayout(row_layout)
|
|
328
|
+
pick_button = QPushButton("Pick Color")
|
|
329
|
+
layout.addWidget(pick_button)
|
|
330
|
+
|
|
331
|
+
def update_preview_color():
|
|
332
|
+
rgb = tuple(sliders[n].value() for n in ("R", "G", "B"))
|
|
333
|
+
color_preview.setStyleSheet(f"background-color: rgb{rgb};")
|
|
334
|
+
|
|
335
|
+
def schedule_preview():
|
|
336
|
+
nonlocal last_preview_rgb
|
|
337
|
+
rgb = tuple(sliders[n].value() for n in ("R", "G", "B"))
|
|
338
|
+
for n in ("R", "G", "B"):
|
|
339
|
+
value_labels[n].setText(str(sliders[n].value()))
|
|
340
|
+
update_preview_color()
|
|
341
|
+
if preview_check.isChecked() and rgb != last_preview_rgb:
|
|
342
|
+
last_preview_rgb = rgb
|
|
343
|
+
preview_timer.start(100)
|
|
344
|
+
|
|
345
|
+
def apply_preview():
|
|
346
|
+
rgb = tuple(sliders[n].value() for n in ("R", "G", "B"))
|
|
347
|
+
processed = white_balance_from_rgb(self.master_layer_copy, rgb)
|
|
348
|
+
self.master_layer = processed
|
|
349
|
+
self.display_master_layer()
|
|
350
|
+
dlg.activateWindow()
|
|
351
|
+
|
|
352
|
+
def on_preview_toggled(checked):
|
|
353
|
+
nonlocal last_preview_rgb
|
|
354
|
+
if checked:
|
|
355
|
+
last_preview_rgb = tuple(sliders[n].value() for n in ("R", "G", "B"))
|
|
356
|
+
preview_timer.start(100)
|
|
357
|
+
else:
|
|
358
|
+
self.master_layer = self.master_layer_copy.copy()
|
|
359
|
+
self.display_master_layer()
|
|
360
|
+
dlg.activateWindow()
|
|
361
|
+
|
|
362
|
+
preview_check = QCheckBox("Preview")
|
|
363
|
+
preview_check.setChecked(True)
|
|
364
|
+
preview_check.stateChanged.connect(on_preview_toggled)
|
|
365
|
+
layout.addWidget(preview_check)
|
|
366
|
+
button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Reset | QDialogButtonBox.Cancel)
|
|
367
|
+
layout.addWidget(button_box)
|
|
368
|
+
last_preview_rgb = None
|
|
369
|
+
preview_timer = QTimer()
|
|
370
|
+
preview_timer.setSingleShot(True)
|
|
371
|
+
preview_timer.timeout.connect(apply_preview)
|
|
372
|
+
for slider in sliders.values():
|
|
373
|
+
slider.valueChanged.connect(schedule_preview)
|
|
374
|
+
|
|
375
|
+
def start_color_pick():
|
|
376
|
+
dlg.hide()
|
|
377
|
+
QApplication.setOverrideCursor(QCursor(Qt.CrossCursor))
|
|
378
|
+
self.image_viewer.setCursor(Qt.CrossCursor)
|
|
379
|
+
self.master_layer = self.master_layer_copy
|
|
380
|
+
self.display_master_layer()
|
|
381
|
+
self._original_mouse_press = self.image_viewer.mousePressEvent
|
|
382
|
+
self.image_viewer.mousePressEvent = pick_color_from_click
|
|
383
|
+
|
|
384
|
+
def pick_color_from_click(event):
|
|
385
|
+
if event.button() == Qt.LeftButton:
|
|
386
|
+
pos = event.pos()
|
|
387
|
+
bgr = self.get_pixel_color_at(pos)
|
|
388
|
+
rgb = (bgr[2], bgr[1], bgr[0])
|
|
389
|
+
for name, val in zip(("R", "G", "B"), rgb):
|
|
390
|
+
sliders[name].setValue(val)
|
|
391
|
+
QApplication.restoreOverrideCursor()
|
|
392
|
+
self.image_viewer.unsetCursor()
|
|
393
|
+
if hasattr(self, "_original_mouse_press"):
|
|
394
|
+
self.image_viewer.mousePressEvent = self._original_mouse_press
|
|
395
|
+
dlg.show()
|
|
396
|
+
dlg.activateWindow()
|
|
397
|
+
dlg.raise_()
|
|
398
|
+
|
|
399
|
+
pick_button.clicked.connect(start_color_pick)
|
|
400
|
+
button_box.accepted.connect(dlg.accept)
|
|
401
|
+
|
|
402
|
+
def cancel_changes():
|
|
403
|
+
self.master_layer = self.master_layer_copy
|
|
404
|
+
self.display_master_layer()
|
|
405
|
+
dlg.reject()
|
|
406
|
+
|
|
407
|
+
def reset_rgb():
|
|
408
|
+
for k, s in sliders.items():
|
|
409
|
+
s.setValue(initial_val)
|
|
410
|
+
|
|
411
|
+
button_box.rejected.connect(cancel_changes)
|
|
412
|
+
button_box.button(QDialogButtonBox.Reset).clicked.connect(reset_rgb)
|
|
413
|
+
|
|
414
|
+
def finish_white_balance(result):
|
|
415
|
+
if result == QDialog.Accepted:
|
|
416
|
+
apply_preview()
|
|
417
|
+
h, w = self.master_layer.shape[:2]
|
|
418
|
+
self.undo_manager.extend_undo_area(0, 0, w, h)
|
|
419
|
+
self.undo_manager.save_undo_state(self.master_layer_copy, 'White Balance')
|
|
420
|
+
self.master_layer_copy = self.master_layer.copy()
|
|
421
|
+
self.display_master_layer()
|
|
422
|
+
self.update_master_thumbnail()
|
|
423
|
+
self.mark_as_modified()
|
|
424
|
+
self.image_viewer.set_cursor_style(cursor_style)
|
|
425
|
+
self.wb_dialog = None
|
|
426
|
+
|
|
427
|
+
dlg.finished.connect(finish_white_balance)
|
|
428
|
+
dlg.show()
|
|
429
|
+
|
|
430
|
+
def get_pixel_color_at(self, pos, radius=None):
|
|
431
|
+
scene_pos = self.image_viewer.mapToScene(pos)
|
|
432
|
+
item_pos = self.image_viewer.pixmap_item.mapFromScene(scene_pos)
|
|
433
|
+
x = int(item_pos.x())
|
|
434
|
+
y = int(item_pos.y())
|
|
435
|
+
if (0 <= x < self.master_layer.shape[1]) and (0 <= y < self.master_layer.shape[0]):
|
|
436
|
+
if radius is None:
|
|
437
|
+
radius = int(self.brush.size)
|
|
438
|
+
if radius > 0:
|
|
439
|
+
y_indices, x_indices = np.ogrid[-radius:radius + 1, -radius:radius + 1]
|
|
440
|
+
mask = x_indices**2 + y_indices**2 <= radius**2
|
|
441
|
+
x0 = max(0, x - radius)
|
|
442
|
+
x1 = min(self.master_layer.shape[1], x + radius + 1)
|
|
443
|
+
y0 = max(0, y - radius)
|
|
444
|
+
y1 = min(self.master_layer.shape[0], y + radius + 1)
|
|
445
|
+
mask = mask[radius - (y - y0): radius + (y1 - y), radius - (x - x0): radius + (x1 - x)]
|
|
446
|
+
region = self.master_layer[y0:y1, x0:x1]
|
|
447
|
+
if region.size == 0:
|
|
448
|
+
pixel = self.master_layer[y, x]
|
|
449
|
+
else:
|
|
450
|
+
if region.ndim == 3:
|
|
451
|
+
pixel = [region[:, :, c][mask].mean() for c in range(region.shape[2])]
|
|
452
|
+
else:
|
|
453
|
+
pixel = region[mask].mean()
|
|
454
|
+
else:
|
|
455
|
+
pixel = self.master_layer[y, x]
|
|
456
|
+
if np.isscalar(pixel):
|
|
457
|
+
pixel = [pixel, pixel, pixel]
|
|
458
|
+
pixel = [np.float32(x) for x in pixel]
|
|
459
|
+
if self.master_layer.dtype == np.uint16:
|
|
460
|
+
pixel = [x / 256.0 for x in pixel]
|
|
461
|
+
return tuple(int(v) for v in pixel)
|
|
462
|
+
else:
|
|
463
|
+
return (0, 0, 0)
|
|
@@ -1,11 +1,19 @@
|
|
|
1
|
+
from PySide6.QtCore import QObject, Signal
|
|
1
2
|
from .. config.gui_constants import gui_constants
|
|
2
3
|
|
|
3
4
|
|
|
4
|
-
class UndoManager:
|
|
5
|
+
class UndoManager(QObject):
|
|
6
|
+
stack_changed = Signal(bool, str, bool, str)
|
|
7
|
+
|
|
5
8
|
def __init__(self):
|
|
9
|
+
super().__init__()
|
|
10
|
+
self.reset()
|
|
11
|
+
|
|
12
|
+
def reset(self):
|
|
6
13
|
self.undo_stack = []
|
|
7
|
-
self.
|
|
14
|
+
self.redo_stack = []
|
|
8
15
|
self.reset_undo_area()
|
|
16
|
+
self.stack_changed.emit(False, "", False, "")
|
|
9
17
|
|
|
10
18
|
def reset_undo_area(self):
|
|
11
19
|
self.x_end = self.y_end = 0
|
|
@@ -17,22 +25,52 @@ class UndoManager:
|
|
|
17
25
|
self.x_end = max(self.x_end, x_end)
|
|
18
26
|
self.y_end = max(self.y_end, y_end)
|
|
19
27
|
|
|
20
|
-
def save_undo_state(self, layer):
|
|
28
|
+
def save_undo_state(self, layer, description):
|
|
21
29
|
if layer is None:
|
|
22
30
|
return
|
|
31
|
+
self.redo_stack = []
|
|
23
32
|
undo_state = {
|
|
24
33
|
'master': layer[self.y_start:self.y_end, self.x_start:self.x_end],
|
|
25
|
-
'area': (self.x_start, self.y_start, self.x_end, self.y_end)
|
|
34
|
+
'area': (self.x_start, self.y_start, self.x_end, self.y_end),
|
|
35
|
+
'description': description
|
|
26
36
|
}
|
|
27
|
-
if len(self.undo_stack) >=
|
|
37
|
+
if len(self.undo_stack) >= gui_constants.MAX_UNDO_SIZE:
|
|
28
38
|
self.undo_stack.pop(0)
|
|
29
39
|
self.undo_stack.append(undo_state)
|
|
40
|
+
undo_desc = description
|
|
41
|
+
redo_desc = self.redo_stack[-1]['description'] if self.redo_stack else ""
|
|
42
|
+
self.stack_changed.emit(bool(self.undo_stack), undo_desc, bool(self.redo_stack), redo_desc)
|
|
30
43
|
|
|
31
44
|
def undo(self, layer):
|
|
32
|
-
if layer is None or not self.undo_stack
|
|
45
|
+
if layer is None or not self.undo_stack:
|
|
46
|
+
return False
|
|
47
|
+
undo_state = self.undo_stack.pop()
|
|
48
|
+
x_start, y_start, x_end, y_end = undo_state['area']
|
|
49
|
+
redo_state = {
|
|
50
|
+
'master': layer[y_start:y_end, x_start:x_end].copy(),
|
|
51
|
+
'area': (x_start, y_start, x_end, y_end),
|
|
52
|
+
'description': undo_state['description']
|
|
53
|
+
}
|
|
54
|
+
self.redo_stack.append(redo_state)
|
|
55
|
+
layer[y_start:y_end, x_start:x_end] = undo_state['master']
|
|
56
|
+
undo_desc = self.undo_stack[-1]['description'] if self.undo_stack else ""
|
|
57
|
+
redo_desc = redo_state['description']
|
|
58
|
+
self.stack_changed.emit(bool(self.undo_stack), undo_desc, bool(self.redo_stack), redo_desc)
|
|
59
|
+
return True
|
|
60
|
+
|
|
61
|
+
def redo(self, layer):
|
|
62
|
+
if layer is None or not self.redo_stack:
|
|
33
63
|
return False
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
layer[y_start:y_end, x_start:x_end]
|
|
38
|
-
|
|
64
|
+
redo_state = self.redo_stack.pop()
|
|
65
|
+
x_start, y_start, x_end, y_end = redo_state['area']
|
|
66
|
+
undo_state = {
|
|
67
|
+
'master': layer[y_start:y_end, x_start:x_end].copy(),
|
|
68
|
+
'area': (x_start, y_start, x_end, y_end),
|
|
69
|
+
'description': redo_state['description']
|
|
70
|
+
}
|
|
71
|
+
self.undo_stack.append(undo_state)
|
|
72
|
+
layer[y_start:y_end, x_start:x_end] = redo_state['master']
|
|
73
|
+
undo_desc = undo_state['description']
|
|
74
|
+
redo_desc = self.redo_stack[-1]['description'] if self.redo_stack else ""
|
|
75
|
+
self.stack_changed.emit(bool(self.undo_stack), undo_desc, bool(self.redo_stack), redo_desc)
|
|
76
|
+
return True
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: shinestacker
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: ShineStacker
|
|
5
|
+
Author-email: Luca Lista <luka.lista@gmail.com>
|
|
6
|
+
License-Expression: LGPL-3.0
|
|
7
|
+
Project-URL: Homepage, https://github.com/lucalista/shinestacker
|
|
8
|
+
Project-URL: Documentation, https://shinestacker.readthedocs.io/
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Requires-Python: >=3.12
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
Requires-Dist: argparse
|
|
15
|
+
Requires-Dist: imagecodecs
|
|
16
|
+
Requires-Dist: ipywidgets
|
|
17
|
+
Requires-Dist: jsonpickle
|
|
18
|
+
Requires-Dist: matplotlib
|
|
19
|
+
Requires-Dist: numpy
|
|
20
|
+
Requires-Dist: opencv_python
|
|
21
|
+
Requires-Dist: pillow
|
|
22
|
+
Requires-Dist: psdtags
|
|
23
|
+
Requires-Dist: PySide6
|
|
24
|
+
Requires-Dist: scipy
|
|
25
|
+
Requires-Dist: tifffile
|
|
26
|
+
Requires-Dist: tqdm
|
|
27
|
+
Requires-Dist: setuptools-scm
|
|
28
|
+
Provides-Extra: dev
|
|
29
|
+
Requires-Dist: pytest; extra == "dev"
|
|
30
|
+
Dynamic: license-file
|
|
31
|
+
|
|
32
|
+
# Shine Stacker
|
|
33
|
+
|
|
34
|
+
## Focus Stacking Processing Framework and GUI
|
|
35
|
+
|
|
36
|
+
[](https://github.com/lucalista/shinestacker/actions/workflows/ci-multiplatform.yml)
|
|
37
|
+
[](https://pypi.org/project/shinestacker/)
|
|
38
|
+
[](https://pypi.org/project/shinestacker/)
|
|
39
|
+
[](https://codecov.io/github/lucalista/shinestacker)
|
|
40
|
+
[](https://shinestacker.readthedocs.io/en/latest/?badge=latest)
|
|
41
|
+
|
|
42
|
+
<img src='https://raw.githubusercontent.com/lucalista/shinestacker/main/img/flies.gif' width="400" referrerpolicy="no-referrer"> <img src='https://raw.githubusercontent.com/lucalista/shinestacker/main/img/flies_stack.jpg' width="400" referrerpolicy="no-referrer">
|
|
43
|
+
|
|
44
|
+
> **Focus stacking** for microscopy, macro photography, and computational imaging
|
|
45
|
+
|
|
46
|
+
## Key Features
|
|
47
|
+
- 🚀 **Batch Processing**: Align, balance, and stack hundreds of images
|
|
48
|
+
- 🎨 **Hybrid Workflows**: Combine Python scripting with GUI refinement
|
|
49
|
+
- 🧩 **Modular Architecture**: Mix-and-match processing modules
|
|
50
|
+
- 🖌️ **Non-Destructive Editing**: Save multilayer TIFFs for retouching
|
|
51
|
+
- 📊 **Jupyter Integration**: Reproducible research notebooks
|
|
52
|
+
|
|
53
|
+
## Interactive GUI
|
|
54
|
+
|
|
55
|
+
The GUI has two main working areas:
|
|
56
|
+
|
|
57
|
+
* *Project*: manage and run focus stacking workflows in a flexible and configurable way, with optional intermediate batch stacking.
|
|
58
|
+
|
|
59
|
+
<img src='https://raw.githubusercontent.com/lucalista/shinestacker/main/img/gui-project-run.png' width="600" referrerpolicy="no-referrer">
|
|
60
|
+
|
|
61
|
+
* *Retouch*: interactive final refinements to final blended image from individual frames.
|
|
62
|
+
|
|
63
|
+
<img src='https://raw.githubusercontent.com/lucalista/shinestacker/main/img/gui-retouch.png' width="600" referrerpolicy="no-referrer">
|
|
64
|
+
|
|
65
|
+
# Documentation
|
|
66
|
+
|
|
67
|
+
📖 [Main documentation](https://github.com/lucalista/shinestacker/blob/main/docs/main.md) • 📝 [Changelog](https://github.com/lucalista/shinestacker/blob/main/CHANGELOG.md)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
# Credits
|
|
71
|
+
|
|
72
|
+
The main pyramid stack algorithm was initially inspired by the [Laplacian pyramids method](https://github.com/sjawhar/focus-stacking) implementation by Sami Jawhar that was used under permission of the author for initial versions of this package. The implementation in the latest releases was rewritten from the original code.
|
|
73
|
+
|
|
74
|
+
# Resources
|
|
75
|
+
|
|
76
|
+
* [Pyramid Methods in Image Processing](https://www.researchgate.net/publication/246727904_Pyramid_Methods_in_Image_Processing), E. H. Adelson, C. H. Anderson, J. R. Bergen, P. J. Burt, J. M. Ogden, RCA Engineer, 29-6, Nov/Dec 1984
|
|
77
|
+
Pyramid methods in image processing
|
|
78
|
+
* [A Multi-focus Image Fusion Method Based on Laplacian Pyramid](http://www.jcomputers.us/vol6/jcp0612-07.pdf), Wencheng Wang, Faliang Chang, Journal of Computers 6 (12), 2559, December 2011
|
|
79
|
+
* Another [original implementation on GitHub](https://github.com/bznick98/Focus_Stacking) by Zongnan Bao
|
|
80
|
+
|
|
81
|
+
# License
|
|
82
|
+
|
|
83
|
+
The software is provided as is under the [GNU Lesser General Public License v3.0](https://choosealicense.com/licenses/lgpl-3.0/).
|
|
84
|
+
|
|
@@ -1,15 +1,17 @@
|
|
|
1
|
-
shinestacker/__init__.py,sha256=
|
|
2
|
-
shinestacker/_version.py,sha256=
|
|
3
|
-
shinestacker/algorithms/__init__.py,sha256=
|
|
4
|
-
shinestacker/algorithms/align.py,sha256=
|
|
1
|
+
shinestacker/__init__.py,sha256=sxG9J11a6Qpu_VcqsARRqGSPByHtDvQqR5ozWYjHfZU,387
|
|
2
|
+
shinestacker/_version.py,sha256=gTggO06fb2c9XKEwlQYUSPlUfy82yVlM9pzLMOUqVcY,21
|
|
3
|
+
shinestacker/algorithms/__init__.py,sha256=X9nwToaB13GAACoaE-K0o9R1Jt7g9xSzeJI8Ra2GcL8,722
|
|
4
|
+
shinestacker/algorithms/align.py,sha256=As7MIfjoJc_1vJ058iwP7b_PnDnfYsBQ1JNZ4GX5ZU4,16529
|
|
5
5
|
shinestacker/algorithms/balance.py,sha256=UOmyUPJVBswUYWvYIB8WdlfTAxUAahZrnxQUSrYJ3I4,15649
|
|
6
|
-
shinestacker/algorithms/core_utils.py,sha256=
|
|
6
|
+
shinestacker/algorithms/core_utils.py,sha256=3mvJVDDfzMOboP724iM_5WT-pLFFoGAhwcXsQN8dBYo,552
|
|
7
|
+
shinestacker/algorithms/denoise.py,sha256=C59la4DEh6uPZIDgmaZIqgxdxDYBxXRxgQBLLBvSlBA,374
|
|
7
8
|
shinestacker/algorithms/depth_map.py,sha256=hx6yyCjLKiu3Wi5uZFXiixEfOBxfieNQ9xH9cp5zD0Y,7844
|
|
8
9
|
shinestacker/algorithms/exif.py,sha256=VS3qbLRVlbZ8_z8hYgGVV4BSmwWzR0reN_G0XV3hzAI,9364
|
|
9
10
|
shinestacker/algorithms/multilayer.py,sha256=Fm-WZqH4DAEOAyC0QpPaMH3VaG5cEPX9rqKsAyWpELs,8694
|
|
10
11
|
shinestacker/algorithms/noise_detection.py,sha256=ra4mkprxPcb5WqHsOdKFUflAmIJ4_nQegYv1EhwH7ts,8280
|
|
11
12
|
shinestacker/algorithms/pyramid.py,sha256=iUbgRI0p0uzEXcZetAm3hgzwiXhF4mIaNxYMUUpJFeE,8589
|
|
12
|
-
shinestacker/algorithms/
|
|
13
|
+
shinestacker/algorithms/sharpen.py,sha256=KtTDIytJW7z4Oj-E1FcxKvhzz27xLX2CCSwJAG_QRUY,911
|
|
14
|
+
shinestacker/algorithms/stack.py,sha256=EZm2SIuoh8c7hgySB2tQGydKYQjJa2hEqR8YNzIeRuQ,5194
|
|
13
15
|
shinestacker/algorithms/stack_framework.py,sha256=aJBjkQFxiPNjnsnZyoF8lXKR17tm0rTO7puEu_ZvASU,10928
|
|
14
16
|
shinestacker/algorithms/utils.py,sha256=TV3NaGe6_2JTgGdFe4kKNzgihjt6vcK-SLy-HHnbts0,2054
|
|
15
17
|
shinestacker/algorithms/vignetting.py,sha256=EiD4O8GJPGOqByjDAfFj-de4cb64Qf685RujqlBgvX0,6774
|
|
@@ -18,15 +20,15 @@ shinestacker/app/about_dialog.py,sha256=G_2hRQFpo96q0H8z02fMZ0Jl_2-PpnwNxqISY7Xn
|
|
|
18
20
|
shinestacker/app/app_config.py,sha256=tQ1JBTG2gtHO1UbJYJjjYUBCakmtZpafvMUTHb5OyUo,1117
|
|
19
21
|
shinestacker/app/gui_utils.py,sha256=C5ehbYyoIcBweFTfdQcjsILAcWpMPrVLMbYz0ZM-EcM,1571
|
|
20
22
|
shinestacker/app/help_menu.py,sha256=ofFhZMPTz7lQ-_rsu30yAxbLA-Zq_kkRGXxPMukaG08,476
|
|
21
|
-
shinestacker/app/main.py,sha256=
|
|
23
|
+
shinestacker/app/main.py,sha256=insz1oxeDlZQWNKo7DGaLmRrc8PZVD7brPRrWE1SWvk,6894
|
|
22
24
|
shinestacker/app/open_frames.py,sha256=uqUihAkP1K2p4bQ8LAQK02XwVddkRJUPy_vzwUgI8jc,1356
|
|
23
|
-
shinestacker/app/project.py,sha256=
|
|
24
|
-
shinestacker/app/retouch.py,sha256=
|
|
25
|
-
shinestacker/config/__init__.py,sha256=
|
|
25
|
+
shinestacker/app/project.py,sha256=xtuA8SRoWINpHcLTApBL3RYRd0ADrczrAaq_xtT3cZI,3234
|
|
26
|
+
shinestacker/app/retouch.py,sha256=6Sa3ngUqj_Wn5gZBJNZeM-JxeGvXWmJZAa2Jj99t-xk,3012
|
|
27
|
+
shinestacker/config/__init__.py,sha256=gMotpjZz5KyGdytdHhudCVlSKONw8kqMYNRACmlWUw0,172
|
|
26
28
|
shinestacker/config/config.py,sha256=Vif1_-zFZQhI6KW1CAX3Yt-cgwXMJ7YDwDJrzoGVE48,1459
|
|
27
29
|
shinestacker/config/constants.py,sha256=yOt1L7LiJyBPrGezIW-Vx_1I4r1Os0rPibfqroN30nk,5724
|
|
28
30
|
shinestacker/config/gui_constants.py,sha256=PtizNIsLHM_P_GdkrhsSVZuuS5abxOETXozu5eSRt9w,2440
|
|
29
|
-
shinestacker/core/__init__.py,sha256=
|
|
31
|
+
shinestacker/core/__init__.py,sha256=tZOPV21QOxH6bEZG21fLxIdGNu9UbbwkFWHgSJkReRk,459
|
|
30
32
|
shinestacker/core/colors.py,sha256=f4_iaNgDYpHfLaoooCsltDxem5fI950GPZlw2lFPqYM,1330
|
|
31
33
|
shinestacker/core/core_utils.py,sha256=EkDJY8g3zLdAqT9hTRZ2_jh3jo8GMF2L6ZBINyG6L6Y,1398
|
|
32
34
|
shinestacker/core/exceptions.py,sha256=TzguZ88XxjgbPs5jMFdrXV2cQRkl5mFfXZin9GRMSRY,1555
|
|
@@ -40,9 +42,9 @@ shinestacker/gui/gui_images.py,sha256=3HySCrldbYhUyxO0B1ckC2TLVg5nq94bBXjqsQzCyC
|
|
|
40
42
|
shinestacker/gui/gui_logging.py,sha256=sN82OsGhMcZdgFMY4z-VbUYiRIsReN-ICaxi31M1J6E,8147
|
|
41
43
|
shinestacker/gui/gui_run.py,sha256=LpBi1V91NrJpVpgS098lSgLtiege0aqcWIGwSbB8cL4,15701
|
|
42
44
|
shinestacker/gui/main_window.py,sha256=KtMe6Hm74xk-lP3qcUsWAw0dzT7CynpdbZA_H651XN0,27203
|
|
43
|
-
shinestacker/gui/new_project.py,sha256=
|
|
45
|
+
shinestacker/gui/new_project.py,sha256=kGTt8zf24w5o8wUOc0QgXVDbvi4XcM4ia0CZftUImOc,7002
|
|
44
46
|
shinestacker/gui/project_converter.py,sha256=d66pbBzaBgANpudJLW0tGUSfSy0PXNhs1M6R2o_Fd5E,7390
|
|
45
|
-
shinestacker/gui/project_editor.py,sha256=
|
|
47
|
+
shinestacker/gui/project_editor.py,sha256=s0IU3l3xO_JGovazOHhKkT1rraUWNVzyAua1dsUqJyg,21657
|
|
46
48
|
shinestacker/gui/project_model.py,sha256=buzpxppLuoNWao7M2_FOPVpCBex2WgEcvqyq9dxvrz8,4524
|
|
47
49
|
shinestacker/gui/ico/focus_stack_bkg.png,sha256=Q86TgqvKEi_IzKI8m6aZB2a3T40UkDtexf2PdeBM9XE,163151
|
|
48
50
|
shinestacker/gui/ico/shinestacker.icns,sha256=m_6WQBx8sE9jQKwIRa_B5oa7_VcNn6e2TyijeQXPjwM,337563
|
|
@@ -54,18 +56,19 @@ shinestacker/gui/img/play-button-round-icon.png,sha256=9j6Ks9mOGa-2cXyRFpimepAAv
|
|
|
54
56
|
shinestacker/gui/img/plus-round-line-icon.png,sha256=LS068Hlu-CeBvJuB3dwwdJg1lZq6D5MUIv53lu1yKJA,7534
|
|
55
57
|
shinestacker/retouch/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
56
58
|
shinestacker/retouch/brush.py,sha256=49YNdZp1dbfowh6HmLfxuHKz7Py9wkFQsN9-pH38P7Q,319
|
|
57
|
-
shinestacker/retouch/brush_controller.py,sha256=
|
|
59
|
+
shinestacker/retouch/brush_controller.py,sha256=R_pWs7Cf_5sC07TkkqqHnX3L_vV5j9CP8S3TkoxYbvs,2945
|
|
58
60
|
shinestacker/retouch/brush_preview.py,sha256=ePyoZPdSY4ytK4uXV-eByQgfsqvhHJqXpleycSxshDg,5247
|
|
59
|
-
shinestacker/retouch/exif_data.py,sha256=
|
|
61
|
+
shinestacker/retouch/exif_data.py,sha256=cYl0ZAAEOmimh2O0REtY0fU_1Y_4uIUtk9orpVLyQKY,2234
|
|
60
62
|
shinestacker/retouch/file_loader.py,sha256=TbPjiD9Vv-i3LCsPVcumYJ2DxgnLmQA6xYCiLCqbEcg,4565
|
|
61
|
-
shinestacker/retouch/image_editor.py,sha256=
|
|
62
|
-
shinestacker/retouch/image_editor_ui.py,sha256
|
|
63
|
+
shinestacker/retouch/image_editor.py,sha256=j-IMm8vWrUgI8XROWgGWDpeQ2HKK5MgrC_BPIr4uijI,28461
|
|
64
|
+
shinestacker/retouch/image_editor_ui.py,sha256=-N_ZSuzC6AjDgf0t04un-WStBQcH3c3-C-6uJjHsXCw,16723
|
|
65
|
+
shinestacker/retouch/image_filters.py,sha256=un1CxlMdfzlBEQ1x7RHPLrb5iDsyUihDYel2uyk40qA,19129
|
|
63
66
|
shinestacker/retouch/image_viewer.py,sha256=5t7JRNoNwSgS7btn7zWmSXPPzXvZKbWBiZMm-YA7Xhg,14827
|
|
64
|
-
shinestacker/retouch/shortcuts_help.py,sha256=
|
|
65
|
-
shinestacker/retouch/undo_manager.py,sha256=
|
|
66
|
-
shinestacker-0.
|
|
67
|
-
shinestacker-0.
|
|
68
|
-
shinestacker-0.
|
|
69
|
-
shinestacker-0.
|
|
70
|
-
shinestacker-0.
|
|
71
|
-
shinestacker-0.
|
|
67
|
+
shinestacker/retouch/shortcuts_help.py,sha256=iYwD7YGV2epNDwOBVDc99zqB9bbxLR_IUtanMJ-abPQ,3749
|
|
68
|
+
shinestacker/retouch/undo_manager.py,sha256=5jWPsaERSh7TuWV5EbvqL6EsRS88Ct6VmM7FHxYzufs,3042
|
|
69
|
+
shinestacker-0.3.0.dist-info/licenses/LICENSE,sha256=cBN0P3F6BWFkfOabkhuTxwJnK1B0v50jmmzZJjGGous,80
|
|
70
|
+
shinestacker-0.3.0.dist-info/METADATA,sha256=lqCr530LfhBtToLsuwCNx2LUN37n0pq4QMDgI4LbETE,4219
|
|
71
|
+
shinestacker-0.3.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
72
|
+
shinestacker-0.3.0.dist-info/entry_points.txt,sha256=SY6g1LqtMmp23q1DGwLUDT_dhLX9iss8DvWkiWLyo_4,166
|
|
73
|
+
shinestacker-0.3.0.dist-info/top_level.txt,sha256=MhijwnBVX5psfsyX8JZjqp3SYiWPsKe69f3Gnyze4Fw,13
|
|
74
|
+
shinestacker-0.3.0.dist-info/RECORD,,
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: shinestacker
|
|
3
|
-
Version: 0.2.1
|
|
4
|
-
Summary: ShineStacker
|
|
5
|
-
Author-email: Luca Lista <luka.lista@gmail.com>
|
|
6
|
-
License-Expression: LGPL-3.0
|
|
7
|
-
Classifier: Programming Language :: Python :: 3
|
|
8
|
-
Classifier: Operating System :: OS Independent
|
|
9
|
-
Requires-Python: >=3.12
|
|
10
|
-
Description-Content-Type: text/markdown
|
|
11
|
-
License-File: LICENSE
|
|
12
|
-
Requires-Dist: argparse
|
|
13
|
-
Requires-Dist: imagecodecs
|
|
14
|
-
Requires-Dist: ipywidgets
|
|
15
|
-
Requires-Dist: jsonpickle
|
|
16
|
-
Requires-Dist: matplotlib
|
|
17
|
-
Requires-Dist: numpy
|
|
18
|
-
Requires-Dist: opencv_python
|
|
19
|
-
Requires-Dist: pillow
|
|
20
|
-
Requires-Dist: psdtags
|
|
21
|
-
Requires-Dist: PySide6
|
|
22
|
-
Requires-Dist: scipy
|
|
23
|
-
Requires-Dist: tifffile
|
|
24
|
-
Requires-Dist: tqdm
|
|
25
|
-
Requires-Dist: setuptools-scm
|
|
26
|
-
Provides-Extra: dev
|
|
27
|
-
Requires-Dist: pytest; extra == "dev"
|
|
28
|
-
Dynamic: license-file
|
|
29
|
-
|
|
30
|
-
# Shine Stacker Processing Framework
|
|
31
|
-
|
|
32
|
-
[](https://github.com/lucalista/shinestacker/actions/workflows/ci-multiplatform.yml)
|
|
33
|
-
[](https://pypi.org/project/shinestacker/)
|
|
34
|
-
[](https://pypi.org/project/shinestacker/)
|
|
35
|
-
|
|
36
|
-
<img src='https://raw.githubusercontent.com/lucalista/shinestacker/main/img/flies.gif' width="400"> <img src='https://raw.githubusercontent.com/lucalista/shinestacker/main/img/flies_stack.jpg' width="400">
|
|
37
|
-
|
|
38
|
-
## Documentation
|
|
39
|
-
|
|
40
|
-
📖 [Main documentation](https://github.com/lucalista/shinestacker/blob/main/docs/main.md) • 📝 [Changelog](https://github.com/lucalista/shinestacker/blob/main/CHANGELOG.md)
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
# Credits:
|
|
44
|
-
|
|
45
|
-
The main pyramid stack algorithm was inspired by the [Laplacian pyramids method](https://github.com/sjawhar/focus-stacking) implementation by Sami Jawhar. The latest implementation was rewritten from the original code that was used under permission of the author for initial versions of this package.
|
|
46
|
-
|
|
47
|
-
# Resources
|
|
48
|
-
|
|
49
|
-
* [Pyramid Methods in Image Processing](https://www.researchgate.net/publication/246727904_Pyramid_Methods_in_Image_Processing), E. H. Adelson, C. H. Anderson, J. R. Bergen, P. J. Burt, J. M. Ogden, RCA Engineer, 29-6, Nov/Dec 1984
|
|
50
|
-
Pyramid methods in image processing
|
|
51
|
-
* [A Multi-focus Image Fusion Method Based on Laplacian Pyramid](http://www.jcomputers.us/vol6/jcp0612-07.pdf), Wencheng Wang, Faliang Chang, Journal of Computers 6 (12), 2559, December 2011
|
|
52
|
-
* Another [original implementation on GitHub](https://github.com/bznick98/Focus_Stacking) by Zongnan Bao
|
|
53
|
-
|
|
54
|
-
# License
|
|
55
|
-
|
|
56
|
-
The software is provided as is under the [GNU Lesser General Public License v3.0](https://choosealicense.com/licenses/lgpl-3.0/).
|
|
57
|
-
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|