shinestacker 0.3.4__py3-none-any.whl → 0.3.6__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of shinestacker might be problematic. Click here for more details.

Files changed (29) hide show
  1. shinestacker/_version.py +1 -1
  2. shinestacker/algorithms/base_stack_algo.py +1 -2
  3. shinestacker/algorithms/noise_detection.py +0 -3
  4. shinestacker/algorithms/pyramid.py +7 -4
  5. shinestacker/algorithms/stack.py +1 -2
  6. shinestacker/algorithms/stack_framework.py +8 -2
  7. shinestacker/algorithms/vignetting.py +5 -4
  8. shinestacker/app/app_config.py +4 -22
  9. shinestacker/app/help_menu.py +1 -1
  10. shinestacker/config/config.py +21 -16
  11. shinestacker/gui/action_config.py +10 -35
  12. shinestacker/gui/actions_window.py +22 -54
  13. shinestacker/gui/new_project.py +5 -22
  14. shinestacker/gui/project_editor.py +49 -20
  15. shinestacker/gui/select_path_widget.py +30 -0
  16. shinestacker/retouch/base_filter.py +12 -1
  17. shinestacker/retouch/denoise_filter.py +4 -10
  18. shinestacker/retouch/exif_data.py +3 -9
  19. shinestacker/retouch/icon_container.py +19 -0
  20. shinestacker/retouch/shortcuts_help.py +2 -13
  21. shinestacker/retouch/unsharp_mask_filter.py +3 -10
  22. shinestacker/retouch/white_balance_filter.py +5 -13
  23. {shinestacker-0.3.4.dist-info → shinestacker-0.3.6.dist-info}/METADATA +10 -8
  24. {shinestacker-0.3.4.dist-info → shinestacker-0.3.6.dist-info}/RECORD +28 -27
  25. shinestacker/algorithms/core_utils.py +0 -22
  26. {shinestacker-0.3.4.dist-info → shinestacker-0.3.6.dist-info}/WHEEL +0 -0
  27. {shinestacker-0.3.4.dist-info → shinestacker-0.3.6.dist-info}/entry_points.txt +0 -0
  28. {shinestacker-0.3.4.dist-info → shinestacker-0.3.6.dist-info}/licenses/LICENSE +0 -0
  29. {shinestacker-0.3.4.dist-info → shinestacker-0.3.6.dist-info}/top_level.txt +0 -0
shinestacker/_version.py CHANGED
@@ -1 +1 @@
1
- __version__ = '0.3.4'
1
+ __version__ = '0.3.6'
@@ -1,6 +1,5 @@
1
1
  # pylint: disable=C0114, C0115, C0116, E0602, R0903
2
2
  import numpy as np
3
- from .. core.colors import color_str
4
3
  from .. core.exceptions import InvalidOptionError, ImageLoadError
5
4
  from .. config.constants import constants
6
5
  from .utils import read_img, get_img_metadata, validate_image
@@ -28,7 +27,7 @@ class BaseStackAlgo:
28
27
  return self._steps_per_frame
29
28
 
30
29
  def print_message(self, msg):
31
- self.process.sub_message_r(color_str(msg, "light_blue"))
30
+ self.process.sub_message_r(msg)
32
31
 
33
32
  def read_image_and_update_metadata(self, img_path, metadata):
34
33
  img = read_img(img_path)
@@ -162,9 +162,6 @@ class MaskNoise(SubAction):
162
162
  else:
163
163
  raise ImageLoadError(path, "file not found.")
164
164
 
165
- def end(self):
166
- pass
167
-
168
165
  def run_frame(self, _idx, _ref_idx, image):
169
166
  self.process.sub_message_r(': mask noisy pixels')
170
167
  if len(image.shape) == 3:
@@ -12,7 +12,7 @@ class PyramidBase(BaseStackAlgo):
12
12
  kernel_size=constants.DEFAULT_PY_KERNEL_SIZE,
13
13
  gen_kernel=constants.DEFAULT_PY_GEN_KERNEL,
14
14
  float_type=constants.DEFAULT_PY_FLOAT):
15
- super().__init__("pyramid", 1, float_type)
15
+ super().__init__("pyramid", 2, float_type)
16
16
  self.min_size = min_size
17
17
  self.kernel_size = kernel_size
18
18
  self.pad_amount = (kernel_size - 1) // 2
@@ -151,11 +151,11 @@ class PyramidStack(PyramidBase):
151
151
  metadata = None
152
152
  all_laplacians = []
153
153
  levels = None
154
+ n = len(filenames)
154
155
  for i, img_path in enumerate(filenames):
155
156
  self.print_message(f": validating file {img_path.split('/')[-1]}")
156
157
 
157
158
  img, metadata, updated = self.read_image_and_update_metadata(img_path, metadata)
158
-
159
159
  if updated:
160
160
  self.dtype = metadata[1]
161
161
  self.num_pixel_values = constants.NUM_UINT8 \
@@ -163,14 +163,17 @@ class PyramidStack(PyramidBase):
163
163
  self.max_pixel_value = constants.MAX_UINT8 \
164
164
  if self.dtype == np.uint8 else constants.MAX_UINT16
165
165
  levels = int(np.log2(min(img.shape[:2]) / self.min_size))
166
-
167
166
  if self.do_step_callback:
168
167
  self.process.callback('after_step', self.process.id, self.process.name, i)
169
168
  if self.process.callback('check_running', self.process.id, self.process.name) is False:
170
169
  raise RunStopException(self.name)
171
- for img_path in filenames:
170
+ for i, img_path in enumerate(filenames):
172
171
  self.print_message(f": processing file {img_path.split('/')[-1]}")
173
172
  img = read_img(img_path)
174
173
  all_laplacians.append(self.process_single_image(img, levels))
174
+ if self.do_step_callback:
175
+ self.process.callback('after_step', self.process.id, self.process.name, i + n)
176
+ if self.process.callback('check_running', self.process.id, self.process.name) is False:
177
+ raise RunStopException(self.name)
175
178
  stacked_image = self.collapse(self.fuse_pyramids(all_laplacians))
176
179
  return stacked_image.astype(self.dtype)
@@ -2,7 +2,6 @@
2
2
  import os
3
3
  import numpy as np
4
4
  from .. config.constants import constants
5
- from .. core.colors import color_str
6
5
  from .. core.framework import JobBase
7
6
  from .. core.exceptions import InvalidOptionError
8
7
  from .utils import write_img
@@ -92,7 +91,7 @@ class FocusStackBunch(ActionList, FocusStackBase):
92
91
  ActionList.end(self)
93
92
 
94
93
  def run_step(self):
95
- self.print_message_r(color_str(f"fusing bunch: {self.count}", "blue"))
94
+ self.print_message_r(f"fusing bunch: {self.count}")
96
95
  self.focus_stack(self._chunks[self.count - 1])
97
96
  self.callback('after_step', self.id, self.name, self.count)
98
97
 
@@ -237,6 +237,12 @@ class SubAction:
237
237
  def __init__(self, enabled=True):
238
238
  self.enabled = enabled
239
239
 
240
+ def begin(self, process):
241
+ pass
242
+
243
+ def end(self):
244
+ pass
245
+
240
246
 
241
247
  class CombinedActions(FramesRefActions):
242
248
  def __init__(self, name, actions=[], enabled=True, **kwargs):
@@ -255,10 +261,10 @@ class CombinedActions(FramesRefActions):
255
261
  filename = self.filenames[idx]
256
262
  img = read_img((self.output_dir
257
263
  if self.step_process else self.input_full_path) + f"/{filename}")
258
- self.dtype = img.dtype
259
- self.shape = img.shape
260
264
  if img is None:
261
265
  raise RuntimeError(f"Invalid file: {self.input_full_path}/{filename}")
266
+ self.dtype = img.dtype
267
+ self.shape = img.shape
262
268
  return img
263
269
 
264
270
  def run_frame(self, idx, ref_idx):
@@ -59,7 +59,8 @@ class Vignetting(SubAction):
59
59
  i_valid, r_valid = intensities[valid_mask], radii[valid_mask]
60
60
  try:
61
61
  res = curve_fit(Vignetting.sigmoid, r_valid, i_valid,
62
- p0=[np.max(i_valid), 0.01, np.median(r_valid)])[0]
62
+ p0=[np.max(i_valid), 0.01, np.median(r_valid)],
63
+ bounds=([0, 0, 0], ['inf', 'inf', 'inf']))[0]
63
64
  except Exception:
64
65
  self.process.sub_message(
65
66
  color_str(": could not find vignetting model", "red"),
@@ -83,7 +84,7 @@ class Vignetting(SubAction):
83
84
  if image.dtype == np.uint8 else 65535).astype(image.dtype)
84
85
 
85
86
  def run_frame(self, idx, _ref_idx, img_0):
86
- self.process.sub_message_r(color_str(": compute vignetting", "light_blue"))
87
+ self.process.sub_message_r(": compute vignetting")
87
88
  img = cv2.cvtColor(img_8bit(img_0), cv2.COLOR_BGR2GRAY)
88
89
  radii, intensities = self.radial_mean_intensity(img)
89
90
  pars = self.fit_sigmoid(radii, intensities)
@@ -92,7 +93,7 @@ class Vignetting(SubAction):
92
93
  self.v0 = Vignetting.sigmoid(0, *pars)
93
94
  i0_fit, k_fit, r0_fit = pars
94
95
  self.process.sub_message(
95
- f": fit parameters: i0={i0_fit:.4f}, k={k_fit:.4f}, r0={r0_fit:.4f}",
96
+ f": vignetting model parameters: i0={i0_fit:.4f}, k={k_fit:.4f}, r0={r0_fit:.4f}",
96
97
  level=logging.DEBUG)
97
98
  if self.plot_correction:
98
99
  plt.figure(figsize=(10, 5))
@@ -116,7 +117,7 @@ class Vignetting(SubAction):
116
117
  self.corrections[i][idx] = fsolve(lambda x: Vignetting.sigmoid(x, *pars) /
117
118
  self.v0 - p, r0_fit)[0]
118
119
  if self.apply_correction:
119
- self.process.sub_message_r(color_str(": correct vignetting", "light_blue"))
120
+ self.process.sub_message_r(": correct vignetting")
120
121
  return self.correct_vignetting(img_0, pars)
121
122
  return img_0
122
123
 
@@ -1,28 +1,15 @@
1
1
  # pylint: disable=C0114, C0115, C0116, C0103, W0201
2
- class _AppConfig:
3
- _initialized = False
4
- _instance = None
2
+ from .. config.config import _ConfigBase
5
3
 
4
+
5
+ class _AppConfig(_ConfigBase):
6
6
  def __new__(cls):
7
- if cls._instance is None:
8
- cls._instance = super().__new__(cls)
9
- cls._instance._init_defaults()
10
- return cls._instance
7
+ return _ConfigBase.__new__(cls)
11
8
 
12
9
  def _init_defaults(self):
13
10
  self._DONT_USE_NATIVE_MENU = True
14
11
  self._COMBINED_APP = False
15
12
 
16
- def init(self, **kwargs):
17
- if self._initialized:
18
- raise RuntimeError("Config already initialized")
19
- for k, v in kwargs.items():
20
- if hasattr(self, f"_{k}"):
21
- setattr(self, f"_{k}", v)
22
- else:
23
- raise AttributeError(f"Invalid config key: {k}")
24
- self._initialized = True
25
-
26
13
  @property
27
14
  def DONT_USE_NATIVE_MENU(self):
28
15
  return self._DONT_USE_NATIVE_MENU
@@ -31,10 +18,5 @@ class _AppConfig:
31
18
  def COMBINED_APP(self):
32
19
  return self._COMBINED_APP
33
20
 
34
- def __setattr__(self, name, value):
35
- if self._initialized and name.startswith('_'):
36
- raise AttributeError("Can't change config after initialization")
37
- super().__setattr__(name, value)
38
-
39
21
 
40
22
  app_config = _AppConfig()
@@ -14,4 +14,4 @@ def add_help_action(app):
14
14
 
15
15
 
16
16
  def browse_website():
17
- webbrowser.open("https://github.com/lucalista/shinestacker/blob/main/README.md")
17
+ webbrowser.open("https://github.com/lucalista/shinestacker/blob/main/docs/gui.md")
@@ -1,5 +1,5 @@
1
1
  # pylint: disable=C0114, C0115, C0116, C0103, R0903, W0718, W0104, W0201, E0602
2
- class _Config:
2
+ class _ConfigBase:
3
3
  _initialized = False
4
4
  _instance = None
5
5
 
@@ -9,16 +9,6 @@ class _Config:
9
9
  cls._instance._init_defaults()
10
10
  return cls._instance
11
11
 
12
- def _init_defaults(self):
13
- self._DISABLE_TQDM = False
14
- self._COMBINED_APP = False
15
- self._DONT_USE_NATIVE_MENU = True
16
- try:
17
- __IPYTHON__ # noqa
18
- self._JUPYTER_NOTEBOOK = True
19
- except Exception:
20
- self._JUPYTER_NOTEBOOK = False
21
-
22
12
  def init(self, **kwargs):
23
13
  if self._initialized:
24
14
  raise RuntimeError("Config already initialized")
@@ -29,6 +19,26 @@ class _Config:
29
19
  raise AttributeError(f"Invalid config key: {k}")
30
20
  self._initialized = True
31
21
 
22
+ def __setattr__(self, name, value):
23
+ if self._initialized and name.startswith('_'):
24
+ raise AttributeError("Can't change config after initialization")
25
+ super().__setattr__(name, value)
26
+
27
+ class _Config(_ConfigBase):
28
+
29
+ def __new__(cls):
30
+ return _ConfigBase.__new__(cls)
31
+
32
+ def _init_defaults(self):
33
+ self._DISABLE_TQDM = False
34
+ self._COMBINED_APP = False
35
+ self._DONT_USE_NATIVE_MENU = True
36
+ try:
37
+ __IPYTHON__ # noqa
38
+ self._JUPYTER_NOTEBOOK = True
39
+ except Exception:
40
+ self._JUPYTER_NOTEBOOK = False
41
+
32
42
  @property
33
43
  def DISABLE_TQDM(self):
34
44
  return self._DISABLE_TQDM
@@ -45,10 +55,5 @@ class _Config:
45
55
  def COMBINED_APP(self):
46
56
  return self._COMBINED_APP
47
57
 
48
- def __setattr__(self, name, value):
49
- if self._initialized and name.startswith('_'):
50
- raise AttributeError("Can't change config after initialization")
51
- super().__setattr__(name, value)
52
-
53
58
 
54
59
  config = _Config()
@@ -10,8 +10,10 @@ from PySide6.QtWidgets import (QWidget, QPushButton, QHBoxLayout, QFileDialog, Q
10
10
  QAbstractItemView, QListView)
11
11
  from PySide6.QtCore import Qt, QTimer
12
12
  from .. config.constants import constants
13
- from .project_model import ActionConfig
14
13
  from .. algorithms.align import validate_align_config
14
+ from .project_model import ActionConfig
15
+ from .select_path_widget import (create_select_file_paths_widget, create_layout_widget_no_margins,
16
+ create_layout_widget_and_connect)
15
17
 
16
18
  FIELD_TEXT = 'text'
17
19
  FIELD_ABS_PATH = 'abs_path'
@@ -200,25 +202,11 @@ class FieldBuilder:
200
202
  return edit
201
203
 
202
204
  def create_abs_path_field(self, tag, **kwargs):
203
- value = self.action.params.get(tag, '')
204
- edit = QLineEdit(value)
205
- edit.setPlaceholderText(kwargs.get('placeholder', ''))
206
- button = QPushButton("Browse...")
207
-
208
- def browse():
209
- path = QFileDialog.getExistingDirectory(None, f"Select {tag.replace('_', ' ')}")
210
- if path:
211
- edit.setText(path)
212
- button.clicked.connect(browse)
213
- button.setAutoDefault(False)
214
- layout = QHBoxLayout()
215
- layout.addWidget(edit)
216
- layout.addWidget(button)
217
- layout.setContentsMargins(0, 0, 0, 0)
218
- container = QWidget()
219
- container.setLayout(layout)
220
- container.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
221
- return container
205
+ return create_select_file_paths_widget(
206
+ self.action.params.get(tag, ''),
207
+ kwargs.get('placeholder', ''),
208
+ tag.replace('_', ' ')
209
+ )
222
210
 
223
211
  def create_rel_path_field(self, tag, **kwargs):
224
212
  value = self.action.params.get(tag, kwargs.get('default', ''))
@@ -326,17 +314,8 @@ class FieldBuilder:
326
314
  except ValueError as e:
327
315
  traceback.print_tb(e.__traceback__)
328
316
  QMessageBox.warning(None, "Error", "Could not compute relative path")
317
+ return create_layout_widget_and_connect(button, edit, browse)
329
318
 
330
- button.clicked.connect(browse)
331
- button.setAutoDefault(False)
332
- layout = QHBoxLayout()
333
- layout.addWidget(edit)
334
- layout.addWidget(button)
335
- layout.setContentsMargins(0, 0, 0, 0)
336
- container = QWidget()
337
- container.setLayout(layout)
338
- container.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
339
- return container
340
319
 
341
320
  def create_float_field(self, tag, default=0.0, min_val=0.0, max_val=1.0,
342
321
  step=0.1, decimals=2):
@@ -369,11 +348,7 @@ class FieldBuilder:
369
348
  layout.addWidget(label)
370
349
  layout.addWidget(spin)
371
350
  layout.setStretch(layout.count() - 1, 1)
372
- layout.setContentsMargins(0, 0, 0, 0)
373
- container = QWidget()
374
- container.setLayout(layout)
375
- container.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
376
- return container
351
+ return create_layout_widget_no_margins(layout)
377
352
 
378
353
  def create_combo_field(self, tag, options=None, default=None, **kwargs):
379
354
  options = options or []
@@ -1,14 +1,13 @@
1
1
  # pylint: disable=C0114, C0115, C0116, E0611, R0914, R0912, R0915, W0718
2
2
  import os.path
3
3
  import os
4
- import traceback
4
+ # import traceback
5
5
  import json
6
6
  import jsonpickle
7
7
  from PySide6.QtWidgets import QMessageBox, QFileDialog, QDialog
8
8
  from .. core.core_utils import get_app_base_path
9
9
  from .. config.constants import constants
10
10
  from .project_model import ActionConfig
11
- from .action_config import ActionConfigDialog
12
11
  from .project_editor import ProjectEditor
13
12
  from .new_project import NewProjectDialog
14
13
  from .project_model import Project
@@ -17,15 +16,13 @@ from .project_model import Project
17
16
  class ActionsWindow(ProjectEditor):
18
17
  def __init__(self):
19
18
  super().__init__()
20
- self._current_file = None
21
- self._current_file_wd = ''
22
- self._modified_project = False
23
19
  self.update_title()
24
20
 
25
21
  def update_title(self):
26
22
  title = constants.APP_TITLE
27
- if self._current_file:
28
- title += f" - {os.path.basename(self._current_file)}"
23
+ file_name = self.current_file_name()
24
+ if file_name:
25
+ title += f" - {file_name}"
29
26
  if self._modified_project:
30
27
  title += " *"
31
28
  self.window().setWindowTitle(title)
@@ -38,7 +35,7 @@ class ActionsWindow(ProjectEditor):
38
35
  def close_project(self):
39
36
  if self._check_unsaved_changes():
40
37
  self.set_project(Project())
41
- self._current_file = None
38
+ self.set_current_file_path('')
42
39
  self.update_title()
43
40
  self.job_list.clear()
44
41
  self.action_list.clear()
@@ -48,17 +45,17 @@ class ActionsWindow(ProjectEditor):
48
45
  if not self._check_unsaved_changes():
49
46
  return
50
47
  os.chdir(get_app_base_path())
51
- self._current_file = None
48
+ self.set_current_file_path('')
52
49
  self._modified_project = False
53
50
  self.update_title()
54
51
  self.job_list.clear()
55
52
  self.action_list.clear()
53
+ self.set_project(Project())
56
54
  dialog = NewProjectDialog(self)
57
55
  if dialog.exec() == QDialog.Accepted:
58
56
  input_folder = dialog.get_input_folder().split('/')
59
57
  working_path = '/'.join(input_folder[:-1])
60
58
  input_path = input_folder[-1]
61
- project = Project()
62
59
  if dialog.get_noise_detection():
63
60
  job_noise = ActionConfig(constants.ACTION_JOB,
64
61
  {'name': 'detect-noise', 'working_path': working_path,
@@ -66,7 +63,7 @@ class ActionsWindow(ProjectEditor):
66
63
  noise_detection = ActionConfig(constants.ACTION_NOISEDETECTION,
67
64
  {'name': 'detect-noise'})
68
65
  job_noise.add_sub_action(noise_detection)
69
- project.jobs.append(job_noise)
66
+ self.project.jobs.append(job_noise)
70
67
  job = ActionConfig(constants.ACTION_JOB,
71
68
  {'name': 'focus-stack', 'working_path': working_path,
72
69
  'input_path': input_path})
@@ -115,8 +112,7 @@ class ActionsWindow(ProjectEditor):
115
112
  {'name': 'multi-layer',
116
113
  'input_path': ','.join(input_path)})
117
114
  job.add_sub_action(multi_layer)
118
- project.jobs.append(job)
119
- self.set_project(project)
115
+ self.project.jobs.append(job)
120
116
  self._modified_project = True
121
117
  self.refresh_ui(0, -1)
122
118
 
@@ -128,17 +124,9 @@ class ActionsWindow(ProjectEditor):
128
124
  self, "Open Project", "", "Project Files (*.fsp);;All Files (*)")
129
125
  if file_path:
130
126
  try:
131
- self._current_file = file_path
132
- self._current_file_wd = '' if os.path.isabs(file_path) \
133
- else os.path.dirname(file_path)
134
- if not os.path.isabs(self._current_file_wd):
135
- self._current_file_wd = os.path.abspath(self._current_file_wd)
136
- self._current_file = os.path.basename(self._current_file)
137
- with open(file_path, 'r', encoding="utf-8") as file:
127
+ self.set_current_file_path(file_path)
128
+ with open(self.current_file_path(), 'r', encoding="utf-8") as file:
138
129
  json_obj = json.load(file)
139
- pp = file_path.split('/')
140
- if len(pp) > 1:
141
- os.chdir('/'.join(pp[:-1]))
142
130
  project = Project.from_dict(json_obj['project'])
143
131
  if project is None:
144
132
  raise RuntimeError(f"Project from file {file_path} produced a null project.")
@@ -149,7 +137,7 @@ class ActionsWindow(ProjectEditor):
149
137
  if self.job_list.count() > 0:
150
138
  self.job_list.setCurrentRow(0)
151
139
  except Exception as e:
152
- traceback.print_tb(e.__traceback__)
140
+ # traceback.print_tb(e.__traceback__)
153
141
  QMessageBox.critical(self, "Error", f"Cannot open file {file_path}:\n{str(e)}")
154
142
  if len(self.project.jobs) > 0:
155
143
  self.job_list.setCurrentRow(0)
@@ -177,12 +165,10 @@ class ActionsWindow(ProjectEditor):
177
165
  Please, select a valid working path.''')
178
166
  self.edit_action(action)
179
167
 
180
- def current_file_name(self):
181
- return os.path.basename(self._current_file) if self._current_file else ''
182
-
183
168
  def save_project(self):
184
- if self._current_file:
185
- self.do_save(self._current_file)
169
+ path = self.current_file_path()
170
+ if path:
171
+ self.do_save(path)
186
172
  else:
187
173
  self.save_project_as()
188
174
 
@@ -192,11 +178,11 @@ class ActionsWindow(ProjectEditor):
192
178
  if file_path:
193
179
  if not file_path.endswith('.fsp'):
194
180
  file_path += '.fsp'
195
- self._current_file_wd = ''
196
181
  self.do_save(file_path)
197
- self._current_file = file_path
182
+ self.set_current_file_path(file_path)
198
183
  self._modified_project = False
199
184
  self.update_title()
185
+ os.chdir(os.path.dirname(file_path))
200
186
 
201
187
  def do_save(self, file_path):
202
188
  try:
@@ -204,9 +190,7 @@ class ActionsWindow(ProjectEditor):
204
190
  'project': self.project.to_dict(),
205
191
  'version': 1
206
192
  })
207
- path = f"{self._current_file_wd}/{file_path}" \
208
- if self._current_file_wd != '' else file_path
209
- with open(path, 'w', encoding="utf-8") as f:
193
+ with open(file_path, 'w', encoding="utf-8") as f:
210
194
  f.write(json_obj)
211
195
  self._modified_project = False
212
196
  except Exception as e:
@@ -229,7 +213,7 @@ class ActionsWindow(ProjectEditor):
229
213
  index = self.job_list.row(item)
230
214
  if 0 <= index < len(self.project.jobs):
231
215
  job = self.project.jobs[index]
232
- dialog = ActionConfigDialog(job, self._current_file_wd, self)
216
+ dialog = self.action_config_dialog(job)
233
217
  if dialog.exec() == QDialog.Accepted:
234
218
  current_row = self.job_list.currentRow()
235
219
  if current_row >= 0:
@@ -241,28 +225,12 @@ class ActionsWindow(ProjectEditor):
241
225
  if 0 <= job_index < len(self.project.jobs):
242
226
  job = self.project.jobs[job_index]
243
227
  action_index = self.action_list.row(item)
244
- action_counter = -1
245
- current_action = None
246
- is_sub_action = False
247
- for action in job.sub_actions:
248
- action_counter += 1
249
- if action_counter == action_index:
250
- current_action = action
251
- break
252
- if len(action.type_name) > 0:
253
- for sub_action in action.sub_actions:
254
- action_counter += 1
255
- if action_counter == action_index:
256
- current_action = sub_action
257
- is_sub_action = True
258
- break
259
- if current_action:
260
- break
228
+ current_action, is_sub_action = self.get_current_action_at(job, action_index)
261
229
  if current_action:
262
230
  if not is_sub_action:
263
231
  self.set_enabled_sub_actions_gui(
264
232
  current_action.type_name == constants.ACTION_COMBO)
265
- dialog = ActionConfigDialog(current_action, self._current_file_wd, self)
233
+ dialog = self.action_config_dialog(current_action)
266
234
  if dialog.exec() == QDialog.Accepted:
267
235
  self.on_job_selected(job_index)
268
236
  self.refresh_ui()
@@ -284,7 +252,7 @@ class ActionsWindow(ProjectEditor):
284
252
  self.edit_action(current_action)
285
253
 
286
254
  def edit_action(self, action):
287
- dialog = ActionConfigDialog(action, self._current_file_wd, self)
255
+ dialog = self.action_config_dialog(action)
288
256
  if dialog.exec() == QDialog.Accepted:
289
257
  self.on_job_selected(self.job_list.currentRow())
290
258
  self.mark_as_modified()
@@ -1,13 +1,13 @@
1
1
  # pylint: disable=C0114, C0115, C0116, E0611, R0915, R0902
2
2
  import os
3
- from PySide6.QtWidgets import (QWidget, QLineEdit, QFormLayout, QHBoxLayout, QPushButton,
4
- QDialog, QSizePolicy, QFileDialog, QLabel, QCheckBox,
5
- QSpinBox, QMessageBox)
3
+ from PySide6.QtWidgets import (QFormLayout, QHBoxLayout, QPushButton,
4
+ QDialog, QLabel, QCheckBox, QSpinBox, QMessageBox)
6
5
  from PySide6.QtGui import QIcon
7
6
  from PySide6.QtCore import Qt
8
7
  from .. config.gui_constants import gui_constants
9
8
  from .. config.constants import constants
10
9
  from .. algorithms.stack import get_bunches
10
+ from .select_path_widget import create_select_file_paths_widget
11
11
 
12
12
 
13
13
  class NewProjectDialog(QDialog):
@@ -50,25 +50,8 @@ class NewProjectDialog(QDialog):
50
50
  spacer = QLabel("")
51
51
  spacer.setFixedHeight(10)
52
52
  self.layout.addRow(spacer)
53
- self.input_folder = QLineEdit()
54
- self.input_folder .setPlaceholderText('input files folder')
55
- self.input_folder.textChanged.connect(self.update_bunches_label)
56
- button = QPushButton("Browse...")
57
-
58
- def browse():
59
- path = QFileDialog.getExistingDirectory(None, "Select input files folder")
60
- if path:
61
- self.input_folder.setText(path)
62
-
63
- button.clicked.connect(browse)
64
- button.setAutoDefault(False)
65
- layout = QHBoxLayout()
66
- layout.addWidget(self.input_folder)
67
- layout.addWidget(button)
68
- layout.setContentsMargins(0, 0, 0, 0)
69
- container = QWidget()
70
- container.setLayout(layout)
71
- container.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
53
+
54
+ container = create_select_file_paths_widget('', 'input files folder', 'input files folder')
72
55
 
73
56
  self.noise_detection = QCheckBox()
74
57
  self.noise_detection.setChecked(gui_constants.NEW_PROJECT_NOISE_DETECTION)
@@ -89,6 +89,27 @@ class ProjectEditor(QMainWindow):
89
89
  self.expert_options = False
90
90
  self.script_dir = os.path.dirname(__file__)
91
91
  self.dialog = None
92
+ self._current_file_path = ''
93
+ self._modified_project = False
94
+
95
+ def current_file_path(self):
96
+ return self._current_file_path
97
+
98
+ def current_file_directory(self):
99
+ if os.path.isdir(self._current_file_path):
100
+ return self._current_file_path
101
+ return os.path.dirname(self._current_file_path)
102
+
103
+ def current_file_name(self):
104
+ if os.path.isfile(self._current_file_path):
105
+ return os.path.basename(self._current_file_path)
106
+ return ''
107
+
108
+ def set_current_file_path(self, path):
109
+ if path and not os.path.exists(path):
110
+ raise RuntimeError(f"Path: {path} does not exist.")
111
+ self._current_file_path = os.path.abspath(path)
112
+ os.chdir(self.current_file_directory())
92
113
 
93
114
  def set_project(self, project):
94
115
  self.project = project
@@ -300,9 +321,12 @@ class ProjectEditor(QMainWindow):
300
321
  self.delete_element_action.setEnabled(True)
301
322
  return element
302
323
 
324
+ def action_config_dialog(self, action):
325
+ return ActionConfigDialog(action, self.current_file_directory(), self)
326
+
303
327
  def add_job(self):
304
328
  job_action = ActionConfig("Job")
305
- self.dialog = ActionConfigDialog(job_action, self)
329
+ self.dialog = self.action_config_dialog(job_action)
306
330
  if self.dialog.exec() == QDialog.Accepted:
307
331
  self.mark_as_modified()
308
332
  self.project.jobs.append(job_action)
@@ -323,7 +347,7 @@ class ProjectEditor(QMainWindow):
323
347
  type_name = self.action_selector.currentText()
324
348
  action = ActionConfig(type_name)
325
349
  action.parent = self.get_current_job()
326
- self.dialog = ActionConfigDialog(action, self)
350
+ self.dialog = self.action_config_dialog(action)
327
351
  if self.dialog.exec() == QDialog.Accepted:
328
352
  self.mark_as_modified()
329
353
  self.project.jobs[current_index].add_sub_action(action)
@@ -365,7 +389,7 @@ class ProjectEditor(QMainWindow):
365
389
  if type_name is False:
366
390
  type_name = self.sub_action_selector.currentText()
367
391
  sub_action = ActionConfig(type_name)
368
- self.dialog = ActionConfigDialog(sub_action, self)
392
+ self.dialog = self.action_config_dialog(sub_action)
369
393
  if self.dialog.exec() == QDialog.Accepted:
370
394
  self.mark_as_modified()
371
395
  action.add_sub_action(sub_action)
@@ -493,6 +517,27 @@ class ProjectEditor(QMainWindow):
493
517
  self.add_list_item(self.action_list, sub_action, True)
494
518
  self.update_delete_action_state()
495
519
 
520
+ def get_current_action_at(self, job, action_index):
521
+ action_counter = -1
522
+ current_action = None
523
+ is_sub_action = False
524
+ for action in job.sub_actions:
525
+ action_counter += 1
526
+ if action_counter == action_index:
527
+ current_action = action
528
+ break
529
+ if len(action.sub_actions) > 0:
530
+ for sub_action in action.sub_actions:
531
+ action_counter += 1
532
+ if action_counter == action_index:
533
+ current_action = sub_action
534
+ is_sub_action = True
535
+ break
536
+ if current_action:
537
+ break
538
+
539
+ return current_action, is_sub_action
540
+
496
541
  def update_delete_action_state(self):
497
542
  has_job_selected = len(self.job_list.selectedItems()) > 0
498
543
  has_action_selected = len(self.action_list.selectedItems()) > 0
@@ -504,23 +549,7 @@ class ProjectEditor(QMainWindow):
504
549
  action_index = self.action_list.currentRow()
505
550
  if job_index >= 0:
506
551
  job = self.project.jobs[job_index]
507
- action_counter = -1
508
- current_action = None
509
- is_sub_action = False
510
- for action in job.sub_actions:
511
- action_counter += 1
512
- if action_counter == action_index:
513
- current_action = action
514
- break
515
- if len(action.sub_actions) > 0:
516
- for sub_action in action.sub_actions:
517
- action_counter += 1
518
- if action_counter == action_index:
519
- current_action = sub_action
520
- is_sub_action = True
521
- break
522
- if current_action:
523
- break
552
+ current_action, is_sub_action = self.get_current_action_at(job, action_index)
524
553
  enable_sub_actions = current_action is not None and \
525
554
  not is_sub_action and current_action.type_name == constants.ACTION_COMBO
526
555
  self.set_enabled_sub_actions_gui(enable_sub_actions)
@@ -0,0 +1,30 @@
1
+ # pylint: disable=C0114, C0116, E0611
2
+ from PySide6.QtWidgets import QWidget, QPushButton, QHBoxLayout, QFileDialog, QSizePolicy, QLineEdit
3
+
4
+
5
+ def create_layout_widget_no_margins(layout):
6
+ layout.setContentsMargins(0, 0, 0, 0)
7
+ container = QWidget()
8
+ container.setLayout(layout)
9
+ container.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
10
+ return container
11
+
12
+ def create_layout_widget_and_connect(button, edit, browse):
13
+ button.clicked.connect(browse)
14
+ button.setAutoDefault(False)
15
+ layout = QHBoxLayout()
16
+ layout.addWidget(edit)
17
+ layout.addWidget(button)
18
+ return create_layout_widget_no_margins(layout)
19
+
20
+ def create_select_file_paths_widget(value, placeholder, tag):
21
+ edit = QLineEdit(value)
22
+ edit.setPlaceholderText(placeholder)
23
+ button = QPushButton("Browse...")
24
+
25
+ def browse():
26
+ path = QFileDialog.getExistingDirectory(None, f"Select {tag}")
27
+ if path:
28
+ edit.setText(path)
29
+
30
+ return create_layout_widget_and_connect(button, edit, browse)
@@ -2,7 +2,7 @@
2
2
  import traceback
3
3
  from abc import ABC, abstractmethod
4
4
  import numpy as np
5
- from PySide6.QtWidgets import QDialog, QVBoxLayout
5
+ from PySide6.QtWidgets import QDialog, QVBoxLayout, QCheckBox, QDialogButtonBox
6
6
  from PySide6.QtCore import Signal, QThread, QTimer
7
7
 
8
8
 
@@ -98,6 +98,17 @@ class BaseFilter(ABC):
98
98
  else:
99
99
  restore_original()
100
100
 
101
+ def create_base_widgets(self, layout, buttons, preview_latency):
102
+ preview_check = QCheckBox("Preview")
103
+ preview_check.setChecked(True)
104
+ layout.addWidget(preview_check)
105
+ button_box = QDialogButtonBox(buttons)
106
+ layout.addWidget(button_box)
107
+ preview_timer = QTimer()
108
+ preview_timer.setSingleShot(True)
109
+ preview_timer.setInterval(preview_latency)
110
+ return preview_check, preview_timer, button_box
111
+
101
112
  class PreviewWorker(QThread):
102
113
  finished = Signal(np.ndarray, int)
103
114
 
@@ -1,6 +1,6 @@
1
1
  # pylint: disable=C0114, C0115, C0116, E0611, W0221
2
- from PySide6.QtWidgets import QHBoxLayout, QLabel, QSlider, QCheckBox, QDialogButtonBox
3
- from PySide6.QtCore import Qt, QTimer
2
+ from PySide6.QtWidgets import QHBoxLayout, QLabel, QSlider, QDialogButtonBox
3
+ from PySide6.QtCore import Qt
4
4
  from .base_filter import BaseFilter
5
5
  from .. algorithms.denoise import denoise
6
6
 
@@ -24,14 +24,8 @@ class DenoiseFilter(BaseFilter):
24
24
  value_label = QLabel(f"{self.max_value:.2f}")
25
25
  slider_layout.addWidget(value_label)
26
26
  layout.addLayout(slider_layout)
27
- preview_check = QCheckBox("Preview")
28
- preview_check.setChecked(True)
29
- layout.addWidget(preview_check)
30
- button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
31
- layout.addWidget(button_box)
32
- preview_timer = QTimer()
33
- preview_timer.setSingleShot(True)
34
- preview_timer.setInterval(200)
27
+ preview_check, preview_timer, button_box = self.create_base_widgets(
28
+ layout, QDialogButtonBox.Ok | QDialogButtonBox.Cancel, 200)
35
29
 
36
30
  def do_preview_delayed():
37
31
  preview_timer.start()
@@ -1,10 +1,9 @@
1
1
  # pylint: disable=C0114, C0115, C0116, E0611
2
- import os
3
2
  from PIL.TiffImagePlugin import IFDRational
4
3
  from PySide6.QtWidgets import QFormLayout, QHBoxLayout, QPushButton, QDialog, QLabel
5
- from PySide6.QtGui import QIcon
6
4
  from PySide6.QtCore import Qt
7
5
  from .. algorithms.exif import exif_dict
6
+ from .icon_container import icon_container
8
7
 
9
8
 
10
9
  class ExifData(QDialog):
@@ -32,13 +31,8 @@ class ExifData(QDialog):
32
31
  self.layout.addRow(label)
33
32
 
34
33
  def create_form(self):
35
- icon_path = f"{os.path.dirname(__file__)}/../gui/ico/shinestacker.png"
36
- app_icon = QIcon(icon_path)
37
- icon_pixmap = app_icon.pixmap(128, 128)
38
- icon_label = QLabel()
39
- icon_label.setPixmap(icon_pixmap)
40
- icon_label.setAlignment(Qt.AlignCenter)
41
- self.layout.addRow(icon_label)
34
+ self.layout.addRow(icon_container())
35
+
42
36
  spacer = QLabel("")
43
37
  spacer.setFixedHeight(10)
44
38
  self.layout.addRow(spacer)
@@ -0,0 +1,19 @@
1
+ # pylint: disable=C0114, C0115, C0116, E0611
2
+ import os
3
+ from PySide6.QtWidgets import QHBoxLayout, QLabel, QWidget
4
+ from PySide6.QtGui import QIcon
5
+ from PySide6.QtCore import Qt
6
+
7
+
8
+ def icon_container():
9
+ icon_path = f"{os.path.dirname(__file__)}/../gui/ico/shinestacker.png"
10
+ app_icon = QIcon(icon_path)
11
+ pixmap = app_icon.pixmap(128, 128)
12
+ label = QLabel()
13
+ label.setPixmap(pixmap)
14
+ label.setAlignment(Qt.AlignCenter)
15
+ container = QWidget()
16
+ layout = QHBoxLayout(container)
17
+ layout.addWidget(label)
18
+ layout.setAlignment(Qt.AlignCenter)
19
+ return container
@@ -1,9 +1,8 @@
1
1
  # pylint: disable=C0114, C0115, C0116, E0611
2
- import os
3
2
  from PySide6.QtWidgets import (QFormLayout, QHBoxLayout, QPushButton, QDialog,
4
3
  QLabel, QVBoxLayout, QWidget)
5
- from PySide6.QtGui import QIcon
6
4
  from PySide6.QtCore import Qt
5
+ from .icon_container import icon_container
7
6
 
8
7
 
9
8
  class ShortcutsHelp(QDialog):
@@ -44,17 +43,7 @@ class ShortcutsHelp(QDialog):
44
43
  layout.addRow(label)
45
44
 
46
45
  def create_form(self, left_layout, right_layout):
47
- icon_path = f"{os.path.dirname(__file__)}/../gui/ico/shinestacker.png"
48
- app_icon = QIcon(icon_path)
49
- icon_pixmap = app_icon.pixmap(128, 128)
50
- icon_label = QLabel()
51
- icon_label.setPixmap(icon_pixmap)
52
- icon_label.setAlignment(Qt.AlignCenter)
53
- icon_container = QWidget()
54
- icon_container_layout = QHBoxLayout(icon_container)
55
- icon_container_layout.addWidget(icon_label)
56
- icon_container_layout.setAlignment(Qt.AlignCenter)
57
- self.layout.insertWidget(0, icon_container)
46
+ self.layout.insertWidget(0, icon_container())
58
47
 
59
48
  shortcuts = {
60
49
  "M": "show master layer",
@@ -1,5 +1,5 @@
1
1
  # pylint: disable=C0114, C0115, C0116, E0611, W0221, R0902, R0914
2
- from PySide6.QtWidgets import QHBoxLayout, QLabel, QSlider, QCheckBox, QDialogButtonBox
2
+ from PySide6.QtWidgets import QHBoxLayout, QLabel, QSlider, QDialogButtonBox
3
3
  from PySide6.QtCore import Qt, QTimer
4
4
  from .. algorithms.sharpen import unsharp_mask
5
5
  from .base_filter import BaseFilter
@@ -46,15 +46,8 @@ class UnsharpMaskFilter(BaseFilter):
46
46
  elif name == "Threshold":
47
47
  self.threshold_slider = slider
48
48
  value_labels[name] = value_label
49
-
50
- preview_check = QCheckBox("Preview")
51
- preview_check.setChecked(True)
52
- layout.addWidget(preview_check)
53
- button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
54
- layout.addWidget(button_box)
55
- preview_timer = QTimer()
56
- preview_timer.setSingleShot(True)
57
- preview_timer.setInterval(200)
49
+ preview_check, preview_timer, button_box = self.create_base_widgets(
50
+ layout, QDialogButtonBox.Ok | QDialogButtonBox.Cancel, 200)
58
51
 
59
52
  def update_value(name, value, max_val, fmt):
60
53
  float_value = max_val * value / self.max_range
@@ -1,6 +1,6 @@
1
1
  # pylint: disable=C0114, C0115, C0116, E0611, W0221, R0913, R0914, R0917
2
2
  from PySide6.QtWidgets import (QHBoxLayout, QPushButton, QFrame, QVBoxLayout, QLabel, QDialog,
3
- QApplication, QSlider, QCheckBox, QDialogButtonBox)
3
+ QApplication, QSlider, QDialogButtonBox)
4
4
  from PySide6.QtCore import Qt, QTimer
5
5
  from PySide6.QtGui import QCursor
6
6
  from .. algorithms.white_balance import white_balance_from_rgb
@@ -47,18 +47,10 @@ class WhiteBalanceFilter(BaseFilter):
47
47
  layout.addLayout(row_layout)
48
48
  pick_button = QPushButton("Pick Color")
49
49
  layout.addWidget(pick_button)
50
- preview_check = QCheckBox("Preview")
51
- preview_check.setChecked(True)
52
- layout.addWidget(preview_check)
53
- button_box = QDialogButtonBox(
54
- QDialogButtonBox.Ok |
55
- QDialogButtonBox.Reset |
56
- QDialogButtonBox.Cancel
57
- )
58
- layout.addWidget(button_box)
59
- self.preview_timer = QTimer()
60
- self.preview_timer.setSingleShot(True)
61
- self.preview_timer.setInterval(200)
50
+ preview_check, self.preview_timer, button_box = self.create_base_widgets(
51
+ layout,
52
+ QDialogButtonBox.Ok | QDialogButtonBox.Reset | QDialogButtonBox.Cancel,
53
+ 200)
62
54
  for slider in self.sliders.values():
63
55
  slider.valueChanged.connect(self.on_slider_change)
64
56
  self.preview_timer.timeout.connect(do_preview)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: shinestacker
3
- Version: 0.3.4
3
+ Version: 0.3.6
4
4
  Summary: ShineStacker
5
5
  Author-email: Luca Lista <luka.lista@gmail.com>
6
6
  License-Expression: LGPL-3.0
@@ -37,25 +37,22 @@ Dynamic: license-file
37
37
  [![PyPI version](https://img.shields.io/pypi/v/shinestacker?color=success)](https://pypi.org/project/shinestacker/)
38
38
  [![Python Versions](https://img.shields.io/pypi/pyversions/shinestacker)](https://pypi.org/project/shinestacker/)
39
39
  [![Qt Versions](https://img.shields.io/badge/Qt-6-blue.svg?&logo=Qt&logoWidth=18&logoColor=white)](https://www.qt.io/qt-for-python)
40
- [![codecov](https://codecov.io/github/lucalista/shinestacker/graph/badge.svg?token=Y5NKW6VH5G)](https://codecov.io/github/lucalista/shinestacker)
41
40
  [![pylint](https://img.shields.io/badge/PyLint-9.98-yellow?logo=python&logoColor=white)](https://github.com/lucalista/shinestacker/blob/main/.github/workflows/pylint.yml)
41
+ [![codecov](https://codecov.io/github/lucalista/shinestacker/graph/badge.svg?token=Y5NKW6VH5G)](https://codecov.io/github/lucalista/shinestacker)
42
42
  [![Documentation Status](https://readthedocs.org/projects/shinestacker/badge/?version=latest)](https://shinestacker.readthedocs.io/en/latest/?badge=latest)
43
43
 
44
44
 
45
-
46
45
  <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">
47
46
 
48
47
  <img src='https://raw.githubusercontent.com/lucalista/shinestacker/main/img/coffee.gif' width="400" referrerpolicy="no-referrer"> <img src='https://raw.githubusercontent.com/lucalista/shinestacker/main/img/coffee_stack.jpg' width="400" referrerpolicy="no-referrer">
49
48
 
50
- <img src='https://raw.githubusercontent.com/lucalista/shinestacker/main/img/coins.gif' width="400" referrerpolicy="no-referrer"> <img src='https://raw.githubusercontent.com/lucalista/shinestacker/main/img/coins_stack.jpg' width="400" referrerpolicy="no-referrer">
51
49
  > **Focus stacking** for microscopy, macro photography, and computational imaging
52
50
 
53
51
  ## Key Features
54
52
  - 🚀 **Batch Processing**: Align, balance, and stack hundreds of images
55
- - 🎨 **Hybrid Workflows**: Combine Python scripting with GUI refinement
56
53
  - 🧩 **Modular Architecture**: Mix-and-match processing modules
57
54
  - 🖌️ **Retouch Editing**: Final interactive retouch of stacked image from individual frames
58
- - 📊 **Jupyter Integration**: Reproducible research notebooks
55
+ - 📊 **Jupyter Integration**: Image processing python notebooks
59
56
 
60
57
  ## Interactive GUI
61
58
 
@@ -76,16 +73,21 @@ The GUI has two main working areas:
76
73
 
77
74
  # Credits
78
75
 
79
- 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.
76
+ The first version of the core focus 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. The implementation in the latest releases was rewritten from the original code.
80
77
 
81
78
  # Resources
82
79
 
83
80
  * [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
84
81
  Pyramid methods in image processing
85
82
  * [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
86
- * Another [original implementation on GitHub](https://github.com/bznick98/Focus_Stacking) by Zongnan Bao
87
83
 
88
84
  # License
89
85
 
90
86
  The software is provided as is under the [GNU Lesser General Public License v3.0](https://choosealicense.com/licenses/lgpl-3.0/).
91
87
 
88
+ # Attribution request
89
+ 📸 If you publish images created with Shine Stacker, please consider adding a note such as:
90
+
91
+ *Created with Shine Stacker – https://github.com/lucalista/shinestacker*
92
+
93
+ This is not mandatory, but highly appreciated.
@@ -1,33 +1,32 @@
1
1
  shinestacker/__init__.py,sha256=uq2fjAw2z_6TpH3mOcWFZ98GoEPRsNhTAK8N0MMm_e8,448
2
- shinestacker/_version.py,sha256=SH0xuWVUkyLHZJwWBZ8GJoeliTeYFcqA6TWJgrkLv-U,21
2
+ shinestacker/_version.py,sha256=IbpUPwvtjLOqowcOFsWQ6LKq-FH6cI19IpvfQlxufq0,21
3
3
  shinestacker/algorithms/__init__.py,sha256=c4kRrdTLlVI70Q16XkI1RSmz5MD7npDqIpO_02jTG6g,747
4
4
  shinestacker/algorithms/align.py,sha256=1CAnVhxaYO-SUd86Mmj7lTmaqlrmUWlF-HEM5341gcs,17166
5
5
  shinestacker/algorithms/balance.py,sha256=ZBcw2Ck-CfuobIG1FxFsGVjnLvD1rtVNrTO-GrFbi3Q,16441
6
- shinestacker/algorithms/base_stack_algo.py,sha256=GeUASZ92bjo_YYpWYmFfoPDJBo-SgCpSTYLFH3gjnSw,1415
7
- shinestacker/algorithms/core_utils.py,sha256=XJyHDUFXmN4JhbOjJqhP4dJW75B69oZaaWQrSXHUk5o,575
6
+ shinestacker/algorithms/base_stack_algo.py,sha256=EAdVcO2UDq6UwqWZwGZFF7XXJvysyxqqVoswz4PLCdo,1353
8
7
  shinestacker/algorithms/denoise.py,sha256=GL3Z4_6MHxSa7Wo4ZzQECZS87tHBFqO0sIVF_jPuYQU,426
9
8
  shinestacker/algorithms/depth_map.py,sha256=b88GqbRXEU3wCXBxMcStlgZ4sFJicoiZfJMD30Z4b98,7364
10
9
  shinestacker/algorithms/exif.py,sha256=gY9s6Cd4g4swo5qEjSbzuVIvl1GImCYu6ytOO9WrV0I,9435
11
10
  shinestacker/algorithms/multilayer.py,sha256=4Y6XlNJHFW74iNDFIeq_zdVtwLBnrieeMd708zJX-lo,8994
12
- shinestacker/algorithms/noise_detection.py,sha256=5PZ3sucWiSK5f4PKY2SW1CIzTtgs_o7g48b6JlM3tTc,8597
13
- shinestacker/algorithms/pyramid.py,sha256=zQdN2cr26M2iIywjr_xtknY9XvA2cc6wjMmiNOJf3Wc,8288
11
+ shinestacker/algorithms/noise_detection.py,sha256=PmscQWi2v3ERTSf8SejkkSZXmTixKvh4NV9CtfuoUfM,8564
12
+ shinestacker/algorithms/pyramid.py,sha256=_Pk19lRQ21b3W3aHQ6DgAe9VVOfbsi2a9jrynF0qFVw,8610
14
13
  shinestacker/algorithms/sharpen.py,sha256=h7PMJBYxucg194Usp_6pvItPUMFYbT-ebAc_-7XBFUw,949
15
- shinestacker/algorithms/stack.py,sha256=KTtoPGZYocSYqK4zqvhbMyk6M69L2HhNSDG9ayomASo,4852
16
- shinestacker/algorithms/stack_framework.py,sha256=8hRvAjoFyx4vT34pq1XZAh-62kw__ycI3fLiHgODMe0,12216
14
+ shinestacker/algorithms/stack.py,sha256=IAa24rPMXl7F5yfcy0nw-fjsgGPpUkxqeKMqLHqvee8,4796
15
+ shinestacker/algorithms/stack_framework.py,sha256=WZLudhjk6piQz2JULShxcoC3-3mSD-Bgh4_VT7JeG7c,12293
17
16
  shinestacker/algorithms/utils.py,sha256=VLm6eZmcAk2QPvomT4d1q56laJSYfbCQmiwI2Rmuu_s,2171
18
- shinestacker/algorithms/vignetting.py,sha256=Z6OB5ahgQCvE4J8Gy7EhtHHoPXoM2_3jnisWM1x_r6c,7436
17
+ shinestacker/algorithms/vignetting.py,sha256=wFwi20ob1O3Memav1XQrtrOHgOtKRiK1RV4E-ex69r8,7470
19
18
  shinestacker/algorithms/white_balance.py,sha256=PMKsBtxOSn5aRr_Gkx1StHS4eN6kBN2EhNnhg4UG24g,501
20
19
  shinestacker/app/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
21
20
  shinestacker/app/about_dialog.py,sha256=QzZgTcLvkSP3_FhmPOUnwQ_YSxwJdeFrU2IAVYKDgeg,1050
22
- shinestacker/app/app_config.py,sha256=Rde_EKfM0tAs6a1qtiNkr6Bkodg5hZc6XW4hmQoQoac,1169
21
+ shinestacker/app/app_config.py,sha256=eTIRxp0t7Wic46jMTe_oY3kz7ktZbdM43C3bjshVDKg,494
23
22
  shinestacker/app/gui_utils.py,sha256=ptbUKjv5atbx5vW912_j8BVmDZpovAqZDEC48d0R2vA,2331
24
- shinestacker/app/help_menu.py,sha256=T6_L3CGOkJs5fvFf3XLkIehk2khrdTE-nXo5Aj8VR94,514
23
+ shinestacker/app/help_menu.py,sha256=UOlabEY_EKV2Q1BoiU2JAM1udSSBAwXlL7d58bqxKe0,516
25
24
  shinestacker/app/main.py,sha256=RAf9WiCipYLK1rrwnXyL1sWq_28zDl9Z_eipfrdtSuY,6421
26
25
  shinestacker/app/open_frames.py,sha256=bsu32iJSYJQLe_tQQbvAU5DuMDVX6MRuNdE7B5lojZc,1488
27
26
  shinestacker/app/project.py,sha256=ir98-zogYmvx2QYvFbAaBUqLL03qWYkoMOIvLvmQy_w,2736
28
27
  shinestacker/app/retouch.py,sha256=ZQ-nRKnHo6xurcP34RNqaAWkmuGBjJ5jE05hTQ_ycis,2482
29
28
  shinestacker/config/__init__.py,sha256=aXxi-LmAvXd0daIFrVnTHE5OCaYeK1uf1BKMr7oaXQs,197
30
- shinestacker/config/config.py,sha256=BmcY79Ng3bN7HvYK7DzkGlrk2kTfV5R4gKTln87tfWs,1539
29
+ shinestacker/config/config.py,sha256=BshNb20Dx5HqdlpsTQbx4p-LnQ5uBP2q-h9v3pl84ss,1635
31
30
  shinestacker/config/constants.py,sha256=79bOcE44MZ0WuAVPjDwwhvNrsQTlHGyIOwmqwlLOfMU,5776
32
31
  shinestacker/config/gui_constants.py,sha256=002r96jtxV4Acel7q5NgECrcsDJzW-kOStEHqam-5Gg,2492
33
32
  shinestacker/core/__init__.py,sha256=IUEIx6SQ3DygDEHN3_E6uKpHjHtUa4a_U_1dLd_8yEU,484
@@ -37,17 +36,18 @@ shinestacker/core/exceptions.py,sha256=2-noG-ORAGdvDhL8jBQFs0xxZS4fI6UIkMqrWekgk
37
36
  shinestacker/core/framework.py,sha256=3Q3zalyZeCiUHXYbBiYadWNdtyD_3j3dcymk5_3NajM,7063
38
37
  shinestacker/core/logging.py,sha256=9SuSSy9Usbh7zqmLYMqkmy-VBkOJW000lwqAR0XQs30,3067
39
38
  shinestacker/gui/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
40
- shinestacker/gui/action_config.py,sha256=z5rztJDmOIyXUm3CiRoXFLn1AWHqHDYkcUkLGK5mzbE,49539
41
- shinestacker/gui/actions_window.py,sha256=W9XNjiTbsBJ-ZFdb5wo1eGD8OlSBpsuqyuJsKdWq_hI,13522
39
+ shinestacker/gui/action_config.py,sha256=RpUHjq8lmiGJQPnp55O1yd3ZPiLQG3R7jZ52m1VmiQc,48678
40
+ shinestacker/gui/actions_window.py,sha256=-ehMkGshsH22HSnn33ThAMXy7tR_cqWr14mEnXDTfXk,12025
42
41
  shinestacker/gui/colors.py,sha256=zgLRcC3fAzklx7zzyjLEsMX2i64YTxGUmQM2woYBZuw,1344
43
42
  shinestacker/gui/gui_images.py,sha256=e0KAXSPruZoRHrajfdlmOKBYoRJJQBDan1jgs7YFltY,5678
44
43
  shinestacker/gui/gui_logging.py,sha256=ESlk1EAQMdoT8pCZDFsvtU1UtF4h2GKv3wAhxJYxNis,8213
45
44
  shinestacker/gui/gui_run.py,sha256=bYXX4N__Ez7JMIJtVcTmLF2PJ3y9bCd-uvlOHsV-4gg,16230
46
45
  shinestacker/gui/main_window.py,sha256=z14PWRVDRbuySM05YCCFrn1DPU3U96xwQHy730oiLkw,28577
47
- shinestacker/gui/new_project.py,sha256=nr05vfmMH-LsKiLL_qOanSr35-6AZwv7q_CXAXXEFFc,8443
46
+ shinestacker/gui/new_project.py,sha256=cs3641RW3Uiy2VfwxeM-k254rH4BNykJyojmwxzrEi8,7758
48
47
  shinestacker/gui/project_converter.py,sha256=v-oldaw77VLsywhQcl5uhTtPD7GbGFeJo33JJRl3aG4,7453
49
- shinestacker/gui/project_editor.py,sha256=5A2CmIfLKoRSGfxgyYEzVNDdrwXyc65de-pzlYPAUKo,21296
48
+ shinestacker/gui/project_editor.py,sha256=zwmj7PFs7X06GY4tkoDBcOL4Tl0IGo4Mf13n2qGwaJY,22245
50
49
  shinestacker/gui/project_model.py,sha256=89L0IDSAqRK2mvU1EVIrcsJas8CU-aTzUIjdL1Cv0mw,4421
50
+ shinestacker/gui/select_path_widget.py,sha256=JAxAkbQukPwBc27-EdeobxxJBG4IBfooiV-JZq3ttsY,1015
51
51
  shinestacker/gui/ico/focus_stack_bkg.png,sha256=Q86TgqvKEi_IzKI8m6aZB2a3T40UkDtexf2PdeBM9XE,163151
52
52
  shinestacker/gui/ico/shinestacker.icns,sha256=m_6WQBx8sE9jQKwIRa_B5oa7_VcNn6e2TyijeQXPjwM,337563
53
53
  shinestacker/gui/ico/shinestacker.ico,sha256=yO0NaBWA0uFov_GqHuHQbymoqLtQKt5DPWpGGmRKie0,186277
@@ -57,16 +57,17 @@ shinestacker/gui/img/forward-button-icon.png,sha256=lNw86T4TOEd_uokHYF8myGSGUXzd
57
57
  shinestacker/gui/img/play-button-round-icon.png,sha256=9j6Ks9mOGa-2cXyRFpimepAAvSaHzqJKBfxShRb4_dE,4595
58
58
  shinestacker/gui/img/plus-round-line-icon.png,sha256=LS068Hlu-CeBvJuB3dwwdJg1lZq6D5MUIv53lu1yKJA,7534
59
59
  shinestacker/retouch/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
60
- shinestacker/retouch/base_filter.py,sha256=sU2reqQpC3b-Q1iiTk2fWTtruakQvhZWC2MTRt74fFM,4228
60
+ shinestacker/retouch/base_filter.py,sha256=74GmzLjpPtn0Um0YOS8qLC1ZvhbvQX4L_AEDk-5H5nE,4717
61
61
  shinestacker/retouch/brush.py,sha256=dzD2FzSpBIPdJRmTZobcrQ1FrVd3tF__ZPnUplNE72s,357
62
62
  shinestacker/retouch/brush_gradient.py,sha256=F5SFhyzl8YTMqjJU3jK8BrIlLCYLUvITd5wz3cQE4xk,1453
63
63
  shinestacker/retouch/brush_preview.py,sha256=KlUOqA1uvLZRsz2peJ9NgsukyzsppJUw3XXr0NFCuhQ,5181
64
64
  shinestacker/retouch/brush_tool.py,sha256=kwX58_gC_Fep6zowDqOs3nG2wCIc8wrJdokDADGm6K0,8016
65
- shinestacker/retouch/denoise_filter.py,sha256=Z6oG5YVG172t1titu1KOgnKFSW8My2vzlTOSf5Ey5jY,2212
65
+ shinestacker/retouch/denoise_filter.py,sha256=eO0Cxo9xwsuiE6-JiWCFB5jf6U1kf2N3ftsDAEQ5sek,1982
66
66
  shinestacker/retouch/display_manager.py,sha256=54NRdlyVj_7GzJ7hmKf8Hnf3lJJx2jVSSpWedj-5pIc,7298
67
- shinestacker/retouch/exif_data.py,sha256=UB_CwoVN5mMLU6PfJCRaZOfarFR77a11mVaxbYZ4v-Y,2301
67
+ shinestacker/retouch/exif_data.py,sha256=uA9ck9skp8ztSUdX1SFrApgtqmxrHtfWW3vsry82H94,2026
68
68
  shinestacker/retouch/file_loader.py,sha256=723A_2w3cjn4rhvAzCq-__SWFelDRsMhkazgnb2h7Ig,4810
69
69
  shinestacker/retouch/filter_manager.py,sha256=SkioWTr6iFFpugUgZLg0a3m5b9EHdZAeyNFy39qk0z8,453
70
+ shinestacker/retouch/icon_container.py,sha256=6gw1HO1bC2FrdB4dc_iH81DQuLjzuvRGksZ2hKLT9yA,585
70
71
  shinestacker/retouch/image_editor.py,sha256=co5zUufgeb1WrD3aF1RVPh1MbcC9-92HSUa2iROnKk4,8503
71
72
  shinestacker/retouch/image_editor_ui.py,sha256=vfBALDuHtqSWIPmyfUirkygM1guwQG-gHo0AH0x8_jU,15712
72
73
  shinestacker/retouch/image_filters.py,sha256=JF2a7VATO3CGQr5_OOIPi2k7b9HvHzrhhWS73x32t-A,2883
@@ -74,13 +75,13 @@ shinestacker/retouch/image_viewer.py,sha256=oqBgaanPXWjzIaox5KSRhYOHoGvoYnWm7sqW
74
75
  shinestacker/retouch/io_gui_handler.py,sha256=2jkbPXew95rMKO2aC9hwZJGtZRg3wCCtXI0SFFiNHUI,9089
75
76
  shinestacker/retouch/io_manager.py,sha256=sNcZVEttiVdxNBVs39ZvexqOcvtjl2CvJs6BVqmGvOM,2148
76
77
  shinestacker/retouch/layer_collection.py,sha256=j1NiGGtLZ3OwrftBVNT4rb0Kq0CfWAB3t2bUrqHx1Sk,5608
77
- shinestacker/retouch/shortcuts_help.py,sha256=wjCw2TiUBaPx1OqpdZlRPsLbyLhIZkoffz9wcODtQnA,3794
78
+ shinestacker/retouch/shortcuts_help.py,sha256=dlt7OSAr9thYuoEPlirTU_YRzv5xP9vy2-9mZO7GVAA,3308
78
79
  shinestacker/retouch/undo_manager.py,sha256=_ekbcOLcPbQLY7t-o8wf-b1uA6OPY9rRyLM-KqMlQRo,3257
79
- shinestacker/retouch/unsharp_mask_filter.py,sha256=u6QDAebzAUqRvJtXyMLdXy-G0zEGb-43LzaKkK4kQ2A,3704
80
- shinestacker/retouch/white_balance_filter.py,sha256=odS_QTTLBzuN0gzhGqCC3T5mNRwE-epxPXraKhj5VQM,5132
81
- shinestacker-0.3.4.dist-info/licenses/LICENSE,sha256=cBN0P3F6BWFkfOabkhuTxwJnK1B0v50jmmzZJjGGous,80
82
- shinestacker-0.3.4.dist-info/METADATA,sha256=vR3xbqQFRJErmGEx0k0Mhnsamt_iHlYeuwghqdbcmus,5136
83
- shinestacker-0.3.4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
84
- shinestacker-0.3.4.dist-info/entry_points.txt,sha256=SY6g1LqtMmp23q1DGwLUDT_dhLX9iss8DvWkiWLyo_4,166
85
- shinestacker-0.3.4.dist-info/top_level.txt,sha256=MhijwnBVX5psfsyX8JZjqp3SYiWPsKe69f3Gnyze4Fw,13
86
- shinestacker-0.3.4.dist-info/RECORD,,
80
+ shinestacker/retouch/unsharp_mask_filter.py,sha256=hNJlqXYjf9Nd8KlVy09fd4TxrHa9Ofef0ZLSMHjLL6I,3481
81
+ shinestacker/retouch/white_balance_filter.py,sha256=2krwdz0X6qLWuCIEQcPtSQA_txfAsl7QUzfdsOLBrBU,4878
82
+ shinestacker-0.3.6.dist-info/licenses/LICENSE,sha256=cBN0P3F6BWFkfOabkhuTxwJnK1B0v50jmmzZJjGGous,80
83
+ shinestacker-0.3.6.dist-info/METADATA,sha256=1xiC2Bgn2sR8bqwuapC0QNodFy9fOO9p8ybGpqvHOwc,4915
84
+ shinestacker-0.3.6.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
85
+ shinestacker-0.3.6.dist-info/entry_points.txt,sha256=SY6g1LqtMmp23q1DGwLUDT_dhLX9iss8DvWkiWLyo_4,166
86
+ shinestacker-0.3.6.dist-info/top_level.txt,sha256=MhijwnBVX5psfsyX8JZjqp3SYiWPsKe69f3Gnyze4Fw,13
87
+ shinestacker-0.3.6.dist-info/RECORD,,
@@ -1,22 +0,0 @@
1
- # pylint: disable=C0114, C0116
2
- import os
3
- from ..config.config import config
4
-
5
- if not config.DISABLE_TQDM:
6
- from tqdm import tqdm
7
- from tqdm.notebook import tqdm_notebook
8
-
9
-
10
- def check_path_exists(path):
11
- if not os.path.exists(path):
12
- raise RuntimeError('Path does not exist: ' + path)
13
-
14
-
15
- def make_tqdm_bar(name, size, ncols=80):
16
- if not config.DISABLE_TQDM:
17
- if config.JUPYTER_NOTEBOOK:
18
- tbar = tqdm_notebook(desc=name, total=size)
19
- else:
20
- tbar = tqdm(desc=name, total=size, ncols=ncols)
21
- return tbar
22
- return None