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 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.2.1'
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
+ ]
@@ -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 \
@@ -1,5 +1,5 @@
1
1
  import os
2
- from config.config import config
2
+ from ..config.config import config
3
3
 
4
4
  if not config.DISABLE_TQDM:
5
5
  from tqdm import tqdm
@@ -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
@@ -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 = cv2.fastNlMeansDenoisingColored(stacked_img, None, self.denoise, self.denoise, 7, 21)
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
@@ -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
@@ -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
@@ -2,3 +2,5 @@
2
2
  from .config import config
3
3
  from .constants import constants
4
4
  from .gui_constants import gui_constants
5
+
6
+ __all__ = ['config', 'constants', 'gui_constants']
@@ -1,5 +1,11 @@
1
1
  # flake8: noqa F401
2
- from .logging import setup_logging, console_logging_overwrite, console_logging_newline
3
- from .exceptions import (FocusStackError, InvalidOptionError, ImageLoadError, AlignmentError,
4
- BitDepthError, ShapeError)
5
- from .framework import TqdmCallbacks
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']
@@ -6,7 +6,6 @@ from PySide6.QtGui import QIcon
6
6
  from PySide6.QtCore import Qt
7
7
  from .. config.gui_constants import gui_constants
8
8
  from .. config.constants import constants
9
- from .. core.core_utils import get_app_base_path
10
9
 
11
10
 
12
11
  class NewProjectDialog(QDialog):
@@ -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._get_brush_mask(radius)
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._apply_mask(master_area, source_area, mask_layer_area, dest_area)
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 _get_brush_mask(self, radius):
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 _apply_mask(self, master_area, source_area, mask_area, dest_area):
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 .image_editor import ImageEditor
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
- """QLabel personalizzata che emette un segnale quando viene doppio-clickata"""
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(ImageEditor):
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 Brush", self)
229
- undo_action.setShortcut("Ctrl+Z")
230
- undo_action.triggered.connect(self.undo_last_brush)
231
- edit_menu.addAction(undo_action)
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)
@@ -3,7 +3,6 @@ from PySide6.QtWidgets import (QFormLayout, QHBoxLayout, QPushButton, QDialog,
3
3
  QLabel, QVBoxLayout, QWidget)
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
 
8
7
 
9
8
  class ShortcutsHelp(QDialog):
@@ -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.max_undo_steps = gui_constants.MAX_UNDO_STEPS
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) >= self.max_undo_steps:
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 or len(self.undo_stack) == 0:
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
- else:
35
- undo_state = self.undo_stack.pop()
36
- x_start, y_start, x_end, y_end = undo_state['area']
37
- layer[y_start:y_end, x_start:x_end] = undo_state['master']
38
- return True
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
+ [![CI multiplatform](https://github.com/lucalista/shinestacker/actions/workflows/ci-multiplatform.yml/badge.svg)](https://github.com/lucalista/shinestacker/actions/workflows/ci-multiplatform.yml)
37
+ [![PyPI version](https://img.shields.io/pypi/v/shinestacker?color=success)](https://pypi.org/project/shinestacker/)
38
+ [![Python Versions](https://img.shields.io/pypi/pyversions/shinestacker)](https://pypi.org/project/shinestacker/)
39
+ [![codecov](https://codecov.io/github/lucalista/shinestacker/graph/badge.svg?token=Y5NKW6VH5G)](https://codecov.io/github/lucalista/shinestacker)
40
+ [![Documentation Status](https://readthedocs.org/projects/shinestacker/badge/?version=latest)](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=ZZ2O_m9OFJm18AxMSuYJt4UjSuSqyJlYRaZMoets498,61
2
- shinestacker/_version.py,sha256=nx8uT9NOkkA2KzjU9Q36cvZlTi-7bCHVPIkXCQkwZf8,21
3
- shinestacker/algorithms/__init__.py,sha256=XKMSOCBqcpeXng5PJ88wLhxhvSIwBJ6xuBFHfjda4ow,519
4
- shinestacker/algorithms/align.py,sha256=93szP69KN6BVRmMlU4TsNwBMgpYxEU9fxNzoK07n0Rw,16575
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=u1aw2-cdA1-RiALxA4rnj38oN2pe2s3BP3T_flvuKMQ,550
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/stack.py,sha256=kEaV2tTYcLbCv_rClYrD_VEQpKy-v-vlQEhu7OLervg,5213
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=cB2uTswNTdKv4Fx8p4CCnSSPp1nPCe7XBzG3PcN1Q-M,6953
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=HjLEDkMXzFL-wXEEnXfHmO317TNhbk8NQfANvISWImk,3293
24
- shinestacker/app/retouch.py,sha256=rSl-oLeM9a90MH_HrqnKs9EwnDQyCrhN2iuhfdR3xOI,3071
25
- shinestacker/config/__init__.py,sha256=l9Cg0Rp9sUsY_F7gR2kznuVBkQ-arO7ZOcjS1FoWPtM,121
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=1Iyzh0CXNlRQhpBJg1QR5Ip3bVS0e1hlOBhTDyrTUBw,301
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=YIWjvxzvWsPoU5_xFJjrs7ceyjxoQtGO3JTLxM3xaoI,7051
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=LpopXgw1dPcnH8XulJ7BKpzv9LKoAYUEZ7-d7sEe2tk,21689
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=M1vXrgL8ETJheT9PJ8EVIHlMqiVRLePvL5GlEcEaZW4,2949
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=MC5kWBpqFXDgRXg15umWrURU_L2gDREjG0wzVaL4qm4,2283
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=ehOJgGqvvUOriKyL8s-G0Y-CSYStLo4WCcPzsRy_sys,27579
62
- shinestacker/retouch/image_editor_ui.py,sha256=wKHqNhcjrPB_qm4zKsTQUw_rXIWzWO3uQUDZvhsbnys,15752
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=femx0BT03XVYVOEyIL0OGVGw00_q0E7zCe8wMA-lQlU,3798
65
- shinestacker/retouch/undo_manager.py,sha256=R7O0fbdGFRK2ZZMVpbOcqB5stJLpFa-o2p7Kto8daSI,1355
66
- shinestacker-0.2.1.dist-info/licenses/LICENSE,sha256=cBN0P3F6BWFkfOabkhuTxwJnK1B0v50jmmzZJjGGous,80
67
- shinestacker-0.2.1.dist-info/METADATA,sha256=Y2fygrS6lSd31sPnpTjhdEdrW5u2i3QdjgCUfeqSPrw,2651
68
- shinestacker-0.2.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
69
- shinestacker-0.2.1.dist-info/entry_points.txt,sha256=SY6g1LqtMmp23q1DGwLUDT_dhLX9iss8DvWkiWLyo_4,166
70
- shinestacker-0.2.1.dist-info/top_level.txt,sha256=MhijwnBVX5psfsyX8JZjqp3SYiWPsKe69f3Gnyze4Fw,13
71
- shinestacker-0.2.1.dist-info/RECORD,,
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
- [![CI multiplatform](https://github.com/lucalista/shinestacker/actions/workflows/ci-multiplatform.yml/badge.svg)](https://github.com/lucalista/shinestacker/actions/workflows/ci-multiplatform.yml)
33
- [![PyPI version](https://badge.fury.io/py/shinestacker.svg)](https://pypi.org/project/shinestacker/)
34
- [![Python Versions](https://img.shields.io/pypi/pyversions/shinestacker)](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
-