shinestacker 1.8.0__py3-none-any.whl → 1.9.3__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 (46) hide show
  1. shinestacker/_version.py +1 -1
  2. shinestacker/algorithms/align.py +202 -81
  3. shinestacker/algorithms/align_auto.py +13 -11
  4. shinestacker/algorithms/align_parallel.py +50 -21
  5. shinestacker/algorithms/balance.py +1 -1
  6. shinestacker/algorithms/base_stack_algo.py +1 -1
  7. shinestacker/algorithms/exif.py +848 -127
  8. shinestacker/algorithms/multilayer.py +6 -4
  9. shinestacker/algorithms/noise_detection.py +10 -8
  10. shinestacker/algorithms/pyramid_tiles.py +1 -1
  11. shinestacker/algorithms/stack.py +33 -17
  12. shinestacker/algorithms/stack_framework.py +16 -11
  13. shinestacker/algorithms/utils.py +18 -2
  14. shinestacker/algorithms/vignetting.py +16 -3
  15. shinestacker/app/main.py +1 -1
  16. shinestacker/app/settings_dialog.py +297 -173
  17. shinestacker/config/constants.py +10 -6
  18. shinestacker/config/settings.py +25 -7
  19. shinestacker/core/exceptions.py +1 -1
  20. shinestacker/core/framework.py +2 -2
  21. shinestacker/gui/action_config.py +23 -20
  22. shinestacker/gui/action_config_dialog.py +38 -25
  23. shinestacker/gui/config_dialog.py +6 -5
  24. shinestacker/gui/folder_file_selection.py +3 -2
  25. shinestacker/gui/gui_images.py +27 -3
  26. shinestacker/gui/gui_run.py +2 -2
  27. shinestacker/gui/main_window.py +6 -0
  28. shinestacker/gui/menu_manager.py +8 -2
  29. shinestacker/gui/new_project.py +23 -12
  30. shinestacker/gui/project_controller.py +14 -6
  31. shinestacker/gui/project_editor.py +12 -2
  32. shinestacker/gui/project_model.py +4 -4
  33. shinestacker/retouch/brush_tool.py +20 -0
  34. shinestacker/retouch/exif_data.py +106 -38
  35. shinestacker/retouch/file_loader.py +3 -3
  36. shinestacker/retouch/image_editor_ui.py +79 -3
  37. shinestacker/retouch/image_viewer.py +6 -1
  38. shinestacker/retouch/io_gui_handler.py +13 -16
  39. shinestacker/retouch/shortcuts_help.py +15 -8
  40. shinestacker/retouch/view_strategy.py +12 -2
  41. {shinestacker-1.8.0.dist-info → shinestacker-1.9.3.dist-info}/METADATA +37 -39
  42. {shinestacker-1.8.0.dist-info → shinestacker-1.9.3.dist-info}/RECORD +46 -46
  43. {shinestacker-1.8.0.dist-info → shinestacker-1.9.3.dist-info}/WHEEL +0 -0
  44. {shinestacker-1.8.0.dist-info → shinestacker-1.9.3.dist-info}/entry_points.txt +0 -0
  45. {shinestacker-1.8.0.dist-info → shinestacker-1.9.3.dist-info}/licenses/LICENSE +0 -0
  46. {shinestacker-1.8.0.dist-info → shinestacker-1.9.3.dist-info}/top_level.txt +0 -0
@@ -13,7 +13,7 @@ from .. config.constants import constants
13
13
  from .. config.config import config
14
14
  from .. core.colors import color_str
15
15
  from .. core.framework import TaskBase
16
- from .utils import EXTENSIONS_TIF, EXTENSIONS_JPG, EXTENSIONS_PNG
16
+ from .utils import EXTENSIONS_TIF, EXTENSIONS_JPG, EXTENSIONS_PNG, EXTENSIONS_SUPPORTED
17
17
  from .stack_framework import ImageSequenceManager
18
18
  from .exif import exif_extra_tags_for_tif, get_exif
19
19
 
@@ -142,14 +142,16 @@ def write_multilayer_tiff_from_images(image_dict, output_file, exif_path='', cal
142
142
  elif os.path.isdir(exif_path):
143
143
  _dirpath, _, fnames = next(os.walk(exif_path))
144
144
  fnames = [name for name in fnames
145
- if os.path.splitext(name)[-1][1:].lower() in constants.EXTENSIONS]
146
- extra_tags, exif_tags = exif_extra_tags_for_tif(get_exif(exif_path + '/' + fnames[0]))
145
+ if os.path.splitext(name)[-1][1:].lower() in EXTENSIONS_SUPPORTED]
146
+ file_path = os.path.join(exif_path, fnames[0])
147
+ extra_tags, exif_tags = exif_extra_tags_for_tif(get_exif(file_path))
148
+ extra_tags = [tag for tag in extra_tags if isinstance(tag[0], int)]
147
149
  tiff_tags['extratags'] += extra_tags
148
150
  tiff_tags = {**tiff_tags, **exif_tags}
149
151
  if callbacks:
150
152
  callback = callbacks.get('write_msg', None)
151
153
  if callback:
152
- callback(output_file.split('/')[-1])
154
+ callback(os.path.basename(output_file))
153
155
  compression = 'adobe_deflate'
154
156
  overlayed_images = overlay(
155
157
  *((np.concatenate((image, np.expand_dims(transp, axis=-1)),
@@ -109,13 +109,14 @@ class NoiseDetection(TaskBase, ImageSequenceManager):
109
109
  {'rgb': 'black', 'r': 'red', 'g': 'green', 'b': 'blue'}[ch])
110
110
  msg.append(hpx)
111
111
  self.print_message(color_str("hot pixels: " + ", ".join(msg), constants.LOG_COLOR_LEVEL_2))
112
- path = "/".join(self.file_name.split("/")[:-1])
113
- if not os.path.exists(f"{self.working_path}/{path}"):
114
- self.print_message(f"create directory: {path}")
115
- os.mkdir(f"{self.working_path}/{path}")
116
- self.print_message(color_str(f"writing hot pixels map file: {self.file_name}",
112
+ output_full_path = os.path.join(self.working_path, self.output_path)
113
+ if not os.path.exists(output_full_path):
114
+ self.print_message(f"create directory: {self.output_path}")
115
+ os.mkdir(output_full_path)
116
+ file_path = os.path.join(self.output_path, self.file_name)
117
+ self.print_message(color_str(f"writing hot pixels map file: {file_path}",
117
118
  constants.LOG_COLOR_LEVEL_2))
118
- cv2.imwrite(f"{self.working_path}/{self.file_name}", hot_rgb)
119
+ cv2.imwrite(os.path.join(output_full_path, self.file_name), hot_rgb)
119
120
  plot_range = self.plot_range
120
121
  min_th, max_th = min(self.channel_thresholds), max(self.channel_thresholds)
121
122
  if min_th < plot_range[0]:
@@ -171,8 +172,9 @@ class MaskNoise(SubAction):
171
172
  else:
172
173
  raise ImageLoadError(path, "file not found.")
173
174
 
174
- def run_frame(self, _idx, _ref_idx, image):
175
- self.process.sub_message_r(color_str(': mask noisy pixels', constants.LOG_COLOR_LEVEL_3))
175
+ def run_frame(self, idx, _ref_idx, image):
176
+ self.process.print_message(color_str(
177
+ f'{self.process.frame_str(idx)}: mask noisy pixels', constants.LOG_COLOR_LEVEL_3))
176
178
  shape = image.shape[:2]
177
179
  if shape != self.expected_shape:
178
180
  raise ShapeError(self.expected_shape, shape)
@@ -222,7 +222,7 @@ class PyramidTilesStack(PyramidBase):
222
222
  all_level_counts[img_index] = level_count
223
223
  completed_count += 1
224
224
  self.print_message(
225
- f": processing completed, {self.image_str(completed_count - 1)}")
225
+ f": preprocessing completed, {self.image_str(completed_count - 1)}")
226
226
  except Exception as e:
227
227
  self.print_message(
228
228
  f"Error processing {self.image_str(i)}: {str(e)}")
@@ -1,11 +1,13 @@
1
- # pylint: disable=C0114, C0115, C0116, R0913, R0917
1
+ # pylint: disable=C0114, C0115, C0116, R0913, R0917, W0718
2
2
  import os
3
3
  import traceback
4
+ import logging
5
+ import numpy as np
4
6
  from .. config.constants import constants
5
7
  from .. core.framework import TaskBase
6
8
  from .. core.colors import color_str
7
9
  from .. core.exceptions import InvalidOptionError
8
- from .utils import write_img, extension_tif_jpg
10
+ from .utils import write_img, extension_supported
9
11
  from .stack_framework import ImageSequenceManager, SequentialTask
10
12
  from .exif import copy_exif_from_file_to_file
11
13
  from .denoise import denoise
@@ -35,18 +37,28 @@ class FocusStackBase(TaskBase, ImageSequenceManager):
35
37
  stacked_img = denoise(stacked_img, self.denoise_amount, self.denoise_amount)
36
38
  write_img(out_filename, stacked_img)
37
39
  if self.exif_path != '':
38
- self.sub_message_r(color_str(': copy exif data', constants.LOG_COLOR_LEVEL_3))
39
- if not os.path.exists(self.exif_path):
40
- raise RuntimeError(f"Path {self.exif_path} does not exist.")
41
- try:
42
- _dirpath, _, fnames = next(os.walk(self.exif_path))
43
- fnames = [name for name in fnames if extension_tif_jpg(name)]
44
- exif_filename = os.path.join(self.exif_path, fnames[0])
45
- copy_exif_from_file_to_file(exif_filename, out_filename)
46
- self.sub_message_r(' ' * 60)
47
- except Exception as e:
48
- traceback.print_tb(e.__traceback__)
49
- raise RuntimeError("Can't copy EXIF data") from e
40
+ if stacked_img.dtype == np.uint16 and \
41
+ os.path.splitext(out_filename)[-1].lower() == '.png':
42
+ self.sub_message_r(color_str(': exif not supported for 16-bit PNG format',
43
+ constants.LOG_COLOR_WARNING),
44
+ level=logging.WARNING)
45
+ else:
46
+ self.sub_message_r(color_str(': copy exif data', constants.LOG_COLOR_LEVEL_3))
47
+ if not os.path.exists(self.exif_path):
48
+ raise RuntimeError(f"path {self.exif_path} does not exist.")
49
+ try:
50
+ _dirpath, _, fnames = next(os.walk(self.exif_path))
51
+ fnames = [name for name in fnames if extension_supported(name)]
52
+ if len(fnames) == 0:
53
+ raise RuntimeError(f"path {self.exif_path} does not contain image files.")
54
+ exif_filename = os.path.join(self.exif_path, fnames[0])
55
+ copy_exif_from_file_to_file(exif_filename, out_filename)
56
+ self.sub_message_r(' ' * 60)
57
+ except Exception as e:
58
+ traceback.print_tb(e.__traceback__)
59
+ self.sub_message_r(color_str(f': failed to copy EXIF data: {str(e)}',
60
+ constants.LOG_COLOR_WARNING),
61
+ level=logging.WARNING)
50
62
  if self.plot_stack:
51
63
  idx_str = f"{self.frame_count + 1:04d}" if self.frame_count >= 0 else ''
52
64
  name = f"{self.name}: {self.stack_algo.name()}"
@@ -70,6 +82,10 @@ class FocusStackBase(TaskBase, ImageSequenceManager):
70
82
 
71
83
 
72
84
  def get_bunches(collection, n_frames, n_overlap):
85
+ if n_frames == n_overlap:
86
+ raise RuntimeError(
87
+ f"Can't get bunch collection, total number of frames ({n_frames}) "
88
+ "is equal to the number of overlapping grames")
73
89
  bunches = [collection[x:x + n_frames]
74
90
  for x in range(0, len(collection) - n_overlap, n_frames - n_overlap)]
75
91
  return bunches
@@ -97,7 +113,7 @@ class FocusStackBunch(SequentialTask, FocusStackBase):
97
113
 
98
114
  def begin(self):
99
115
  SequentialTask.begin(self)
100
- self._chunks = get_bunches(self.input_filepaths(), self.frames, self.overlap)
116
+ self._chunks = get_bunches(sorted(self.input_filepaths()), self.frames, self.overlap)
101
117
  self.set_counts(len(self._chunks))
102
118
 
103
119
  def end(self):
@@ -110,9 +126,9 @@ class FocusStackBunch(SequentialTask, FocusStackBase):
110
126
  self.print_message(
111
127
  color_str(f"fusing bunch: {action_count + 1}/{self.total_action_counts}",
112
128
  constants.LOG_COLOR_LEVEL_2))
113
- img_files = self._chunks[action_count - 1]
129
+ img_files = self._chunks[action_count]
114
130
  self.stack_algo.init(img_files)
115
- self.focus_stack(self._chunks[action_count - 1])
131
+ self.focus_stack(self._chunks[action_count])
116
132
  return True
117
133
 
118
134
 
@@ -7,7 +7,7 @@ from .. core.colors import color_str
7
7
  from .. core.framework import Job, SequentialTask
8
8
  from .. core.core_utils import check_path_exists
9
9
  from .. core.exceptions import RunStopException
10
- from .utils import read_img, write_img, extension_tif_jpg, get_img_metadata, validate_image
10
+ from .utils import read_img, write_img, extension_supported, get_img_metadata, validate_image
11
11
 
12
12
 
13
13
  class StackJob(Job):
@@ -95,7 +95,7 @@ class ImageSequenceManager:
95
95
  filelist = []
96
96
  for _dirpath, _, filenames in os.walk(d):
97
97
  filelist = [os.path.join(_dirpath, name)
98
- for name in filenames if extension_tif_jpg(name)]
98
+ for name in filenames if extension_supported(name)]
99
99
  filelist.sort()
100
100
  if self.reverse_order:
101
101
  filelist.reverse()
@@ -191,7 +191,8 @@ class ImageSequenceManager:
191
191
 
192
192
 
193
193
  class ReferenceFrameTask(SequentialTask, ImageSequenceManager):
194
- def __init__(self, name, enabled=True, reference_index=0, step_process=False, **kwargs):
194
+ def __init__(self, name, enabled=True, reference_index=0,
195
+ step_process=constants.DEFAULT_COMBINED_ACTIONS_STEP_PROCESS, **kwargs):
195
196
  ImageSequenceManager.__init__(self, name, **kwargs)
196
197
  SequentialTask.__init__(self, name, enabled)
197
198
  self.ref_idx = reference_index
@@ -276,7 +277,8 @@ class SubAction:
276
277
 
277
278
  class CombinedActions(ReferenceFrameTask):
278
279
  def __init__(self, name, actions=[], enabled=True, **kwargs):
279
- ReferenceFrameTask.__init__(self, name, enabled, **kwargs)
280
+ step_process = kwargs.pop('step_process', constants.DEFAULT_COMBINED_ACTIONS_STEP_PROCESS)
281
+ ReferenceFrameTask.__init__(self, name, enabled, step_process=step_process, **kwargs)
280
282
  self._actions = actions
281
283
  self._metadata = (None, None)
282
284
 
@@ -294,11 +296,15 @@ class CombinedActions(ReferenceFrameTask):
294
296
  self._metadata = get_img_metadata(img)
295
297
  return img
296
298
 
299
+ def frame_str(self, idx=-1):
300
+ if self.run_sequential():
301
+ idx = self.current_action_count
302
+ return f"frame {idx + 1}/{self.total_action_counts}"
303
+
297
304
  def run_frame(self, idx, ref_idx):
298
305
  input_path = self.input_filepath(idx)
299
306
  self.print_message(
300
- color_str(f'read input image '
301
- f'{idx + 1}/{self.total_action_counts}, '
307
+ color_str(f'read input {self.frame_str(idx)}, '
302
308
  f'{os.path.basename(input_path)}', constants.LOG_COLOR_LEVEL_3))
303
309
  img = read_img(input_path)
304
310
  validate_image(img, *(self._metadata))
@@ -317,20 +323,19 @@ class CombinedActions(ReferenceFrameTask):
317
323
  if img is not None:
318
324
  img = a.run_frame(idx, ref_idx, img)
319
325
  else:
320
- self.sub_message(
321
- color_str(": null input received, action skipped",
326
+ self.print_message(
327
+ color_str("null input received, action skipped",
322
328
  constants.LOG_COLOR_ALERT),
323
329
  level=logging.WARNING)
324
330
  if img is not None:
325
331
  output_path = os.path.join(self.output_full_path(), os.path.basename(input_path))
326
332
  self.print_message(
327
- color_str(f'write output image '
328
- f'{idx + 1}/{self.total_action_counts}, '
333
+ color_str(f'write output {self.frame_str(idx)}, '
329
334
  f'{os.path.basename(output_path)}', constants.LOG_COLOR_LEVEL_3))
330
335
  write_img(output_path, img)
331
336
  return img
332
337
  self.print_message(color_str(
333
- f"no output file resulted from processing input file: {os.path.basename(input_path)}",
338
+ f"no output resulted from processing input file: {os.path.basename(input_path)}",
334
339
  constants.LOG_COLOR_ALERT), level=logging.WARNING)
335
340
  return None
336
341
 
@@ -18,6 +18,15 @@ EXTENSIONS_TIF = ['tif', 'tiff']
18
18
  EXTENSIONS_JPG = ['jpg', 'jpeg']
19
19
  EXTENSIONS_PNG = ['png']
20
20
  EXTENSIONS_PDF = ['pdf']
21
+ EXTENSIONS_SUPPORTED = EXTENSIONS_TIF + EXTENSIONS_JPG + EXTENSIONS_PNG
22
+ EXTENSIONS_GUI_STR = " ".join([f"*.{ext}" for ext in EXTENSIONS_SUPPORTED])
23
+ EXTENSION_GUI_TIF = " ".join([f"*.{ext}" for ext in EXTENSIONS_TIF])
24
+ EXTENSION_GUI_JPG = " ".join([f"*.{ext}" for ext in EXTENSIONS_JPG])
25
+ EXTENSION_GUI_PNG = " ".join([f"*.{ext}" for ext in EXTENSIONS_PNG])
26
+ EXTENSIONS_GUI_SAVE_STR = f"TIFF Files ({EXTENSION_GUI_TIF});;" \
27
+ f"JPEG Files ({EXTENSION_GUI_JPG});;" \
28
+ f"PNG Files ({EXTENSION_GUI_PNG});;" \
29
+ "All Files (*)"
21
30
 
22
31
 
23
32
  def extension_in(path, exts):
@@ -56,6 +65,10 @@ def extension_jpg_tif_png(path):
56
65
  return extension_in(path, EXTENSIONS_JPG + EXTENSIONS_TIF + EXTENSIONS_PNG)
57
66
 
58
67
 
68
+ def extension_supported(path):
69
+ return extension_in(path, EXTENSIONS_SUPPORTED)
70
+
71
+
59
72
  def read_img(file_path):
60
73
  if not os.path.isfile(file_path):
61
74
  raise RuntimeError("File does not exist: " + file_path)
@@ -73,7 +86,10 @@ def write_img(file_path, img):
73
86
  elif extension_tif(file_path):
74
87
  cv2.imwrite(file_path, img, [int(cv2.IMWRITE_TIFF_COMPRESSION), 1])
75
88
  elif extension_png(file_path):
76
- cv2.imwrite(file_path, img)
89
+ cv2.imwrite(file_path, img, [
90
+ int(cv2.IMWRITE_PNG_COMPRESSION), 9,
91
+ int(cv2.IMWRITE_PNG_STRATEGY), cv2.IMWRITE_PNG_STRATEGY_HUFFMAN_ONLY
92
+ ])
77
93
 
78
94
 
79
95
  def img_8bit(img):
@@ -96,7 +112,7 @@ def img_bw(img):
96
112
  def get_first_image_file(filenames):
97
113
  first_img_file = None
98
114
  for filename in filenames:
99
- if os.path.isfile(filename) and extension_tif_jpg(filename):
115
+ if os.path.isfile(filename) and extension_supported(filename):
100
116
  first_img_file = filename
101
117
  break
102
118
  if first_img_file is None:
@@ -4,7 +4,7 @@ import traceback
4
4
  import logging
5
5
  import numpy as np
6
6
  import matplotlib.pyplot as plt
7
- from scipy.optimize import curve_fit, fsolve
7
+ from scipy.optimize import curve_fit, bisect
8
8
  import cv2
9
9
  from .. core.colors import color_str
10
10
  from .. core.core_utils import setup_matplotlib_mode
@@ -181,9 +181,22 @@ class Vignetting(SubAction):
181
181
  self.process.callback(
182
182
  constants.CALLBACK_SAVE_PLOT, self.process.id,
183
183
  f"{self.process.name}: intensity\nframe {idx_str}", plot_path)
184
+
184
185
  for i, p in enumerate(self.percentiles):
185
- self.corrections[i][idx] = fsolve(lambda x: sigmoid_model(x, *params) /
186
- self.v0 - p, r0_fit)[0]
186
+ s1 = sigmoid_model(0, *params) / self.v0
187
+ s2 = sigmoid_model(self.r_max, *params) / self.v0
188
+ if s1 > p > s2:
189
+ try:
190
+ c = bisect(lambda x: sigmoid_model(x, *params) / self.v0 - p, 0, self.r_max)
191
+ except Exception as e:
192
+ traceback.print_tb(e.__traceback__)
193
+ self.process.sub_message(color_str(f": {str(e).lower()}", "yellow"),
194
+ level=logging.WARNING)
195
+ elif s1 <= p:
196
+ c = 0
197
+ else:
198
+ c = self.r_max
199
+ self.corrections[i][idx] = c
187
200
  self.process.print_message(
188
201
  color_str(f"{self.process.idx_tot_str(idx)}: correct vignetting", "cyan"))
189
202
  return correct_vignetting(
shinestacker/app/main.py CHANGED
@@ -199,7 +199,7 @@ class MainApp(QMainWindow):
199
199
  class Application(QApplication):
200
200
  def event(self, event):
201
201
  if event.type() == QEvent.Quit and event.spontaneous():
202
- if not self.quit():
202
+ if not self.main_app.quit():
203
203
  return True
204
204
  return super().event(event)
205
205