shinestacker 1.2.0__py3-none-any.whl → 1.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.

Files changed (43) hide show
  1. shinestacker/_version.py +1 -1
  2. shinestacker/algorithms/align.py +148 -115
  3. shinestacker/algorithms/align_auto.py +64 -0
  4. shinestacker/algorithms/align_parallel.py +296 -0
  5. shinestacker/algorithms/balance.py +14 -13
  6. shinestacker/algorithms/base_stack_algo.py +11 -2
  7. shinestacker/algorithms/multilayer.py +14 -15
  8. shinestacker/algorithms/noise_detection.py +13 -14
  9. shinestacker/algorithms/pyramid.py +4 -4
  10. shinestacker/algorithms/pyramid_auto.py +16 -10
  11. shinestacker/algorithms/pyramid_tiles.py +19 -11
  12. shinestacker/algorithms/stack.py +30 -26
  13. shinestacker/algorithms/stack_framework.py +200 -178
  14. shinestacker/algorithms/vignetting.py +16 -13
  15. shinestacker/app/main.py +7 -3
  16. shinestacker/config/constants.py +63 -26
  17. shinestacker/config/gui_constants.py +1 -1
  18. shinestacker/core/core_utils.py +4 -0
  19. shinestacker/core/framework.py +114 -33
  20. shinestacker/gui/action_config.py +57 -5
  21. shinestacker/gui/action_config_dialog.py +156 -17
  22. shinestacker/gui/base_form_dialog.py +2 -2
  23. shinestacker/gui/folder_file_selection.py +101 -0
  24. shinestacker/gui/gui_images.py +10 -10
  25. shinestacker/gui/gui_run.py +13 -11
  26. shinestacker/gui/main_window.py +10 -5
  27. shinestacker/gui/menu_manager.py +4 -0
  28. shinestacker/gui/new_project.py +171 -74
  29. shinestacker/gui/project_controller.py +13 -9
  30. shinestacker/gui/project_converter.py +4 -2
  31. shinestacker/gui/project_editor.py +72 -53
  32. shinestacker/gui/select_path_widget.py +1 -1
  33. shinestacker/gui/sys_mon.py +96 -0
  34. shinestacker/gui/tab_widget.py +3 -3
  35. shinestacker/gui/time_progress_bar.py +4 -3
  36. shinestacker/retouch/exif_data.py +1 -1
  37. shinestacker/retouch/image_editor_ui.py +2 -0
  38. {shinestacker-1.2.0.dist-info → shinestacker-1.3.0.dist-info}/METADATA +6 -6
  39. {shinestacker-1.2.0.dist-info → shinestacker-1.3.0.dist-info}/RECORD +43 -39
  40. {shinestacker-1.2.0.dist-info → shinestacker-1.3.0.dist-info}/WHEEL +0 -0
  41. {shinestacker-1.2.0.dist-info → shinestacker-1.3.0.dist-info}/entry_points.txt +0 -0
  42. {shinestacker-1.2.0.dist-info → shinestacker-1.3.0.dist-info}/licenses/LICENSE +0 -0
  43. {shinestacker-1.2.0.dist-info → shinestacker-1.3.0.dist-info}/top_level.txt +0 -0
@@ -17,8 +17,9 @@ class PyramidAutoStack(BaseStackAlgo):
17
17
  n_tiled_layers=constants.DEFAULT_PY_N_TILED_LAYERS,
18
18
  memory_limit=constants.DEFAULT_PY_MEMORY_LIMIT_GB,
19
19
  max_threads=constants.DEFAULT_PY_MAX_THREADS,
20
- max_tile_size=2048,
21
- min_n_tiled_layers=1,
20
+ max_tile_size=constants.DEFAULT_PY_MAX_TILE_SIZE,
21
+ min_tile_size=constants.DEFAULT_PY_MIN_TILE_SIZE,
22
+ min_n_tiled_layers=constants.DEFAULT_PY_MIN_N_TILED_LAYERS,
22
23
  mode='auto'):
23
24
  super().__init__("auto_pyramid", 2, float_type)
24
25
  self.min_size = min_size
@@ -32,6 +33,7 @@ class PyramidAutoStack(BaseStackAlgo):
32
33
  available_cores = os.cpu_count() or 1
33
34
  self.num_threads = min(max_threads, available_cores)
34
35
  self.max_tile_size = max_tile_size
36
+ self.min_tile_size = min_tile_size
35
37
  self.min_n_tiled_layers = min_n_tiled_layers
36
38
  self.mode = mode
37
39
  self._implementation = None
@@ -39,10 +41,10 @@ class PyramidAutoStack(BaseStackAlgo):
39
41
  self.shape = None
40
42
  self.n_levels = None
41
43
  self.n_frames = 0
42
- self.channels = 3
44
+ self.channels = 3 # r, g, b
43
45
  dtype = np.float32 if self.float_type == constants.FLOAT_32 else np.float64
44
46
  self.bytes_per_pixel = self.channels * np.dtype(dtype).itemsize
45
- self.overhead = 1.5
47
+ self.overhead = constants.PY_MEMORY_OVERHEAD
46
48
 
47
49
  def init(self, filenames):
48
50
  first_img_file = None
@@ -97,15 +99,19 @@ class PyramidAutoStack(BaseStackAlgo):
97
99
  return self.overhead * total_memory * self.n_frames
98
100
 
99
101
  def _find_optimal_tile_params(self):
100
- tile_size_max = int(np.sqrt(self.memory_limit /
101
- (self.num_threads * self.n_frames *
102
- self.bytes_per_pixel * self.overhead)))
102
+ h, w = self.shape[:2]
103
+ base_level_memory = h * w * self.bytes_per_pixel
104
+ available_memory = self.memory_limit - base_level_memory
105
+ available_memory /= self.overhead
106
+ tile_size_max = int(np.sqrt(available_memory /
107
+ (self.num_threads * self.n_frames * self.bytes_per_pixel)))
103
108
  tile_size = min(self.max_tile_size, tile_size_max, self.shape[0], self.shape[1])
109
+ tile_size = max(self.min_tile_size, tile_size)
104
110
  n_tiled_layers = 0
105
111
  for layer in range(self.n_levels):
106
- h = max(1, self.shape[0] // (2 ** layer))
107
- w = max(1, self.shape[1] // (2 ** layer))
108
- if h > tile_size or w > tile_size:
112
+ h_layer = max(1, self.shape[0] // (2 ** layer))
113
+ w_layer = max(1, self.shape[1] // (2 ** layer))
114
+ if h_layer > tile_size or w_layer > tile_size:
109
115
  n_tiled_layers = layer + 1
110
116
  else:
111
117
  break
@@ -2,10 +2,11 @@
2
2
  # pylint: disable=C0114, C0115, C0116, E1101, R0914, R1702, R1732, R0913
3
3
  # pylint: disable=R0917, R0912, R0915, R0902, W0718
4
4
  import os
5
+ import gc
5
6
  import time
6
7
  import shutil
7
8
  import tempfile
8
- import concurrent.futures
9
+ from concurrent.futures import ThreadPoolExecutor, as_completed
9
10
  import numpy as np
10
11
  from .. config.constants import constants
11
12
  from .. core.exceptions import RunStopException
@@ -121,13 +122,13 @@ class PyramidTilesStack(PyramidBase):
121
122
  for x in range(0, w, self.tile_size):
122
123
  tiles.append((y, x))
123
124
  self.print_message(f': starting parallel propcessging on {self.num_threads} cores')
124
- with concurrent.futures.ThreadPoolExecutor(max_workers=self.num_threads) as executor:
125
+ with ThreadPoolExecutor(max_workers=self.num_threads) as executor:
125
126
  future_to_tile = {
126
127
  executor.submit(
127
128
  self._process_tile, level, num_images, all_level_counts, y, x, h, w): (y, x)
128
129
  for y, x in tiles
129
130
  }
130
- for future in concurrent.futures.as_completed(future_to_tile):
131
+ for future in as_completed(future_to_tile):
131
132
  y, x = future_to_tile[future]
132
133
  try:
133
134
  fused_tile = future.result()
@@ -154,7 +155,9 @@ class PyramidTilesStack(PyramidBase):
154
155
  if laplacians:
155
156
  stacked = np.stack(laplacians, axis=0)
156
157
  return self.fuse_laplacian(stacked)
157
- y_end, x_end = min(y + self.tile_size, h), min(x + self.tile_size, w)
158
+ y_end = min(y + self.tile_size, h)
159
+ x_end = min(x + self.tile_size, w)
160
+ gc.collect()
158
161
  return np.zeros((y_end - y, x_end - x, 3), dtype=self.float_type)
159
162
 
160
163
  def fuse_pyramids(self, all_level_counts, num_images):
@@ -202,26 +205,29 @@ class PyramidTilesStack(PyramidBase):
202
205
  self.focus_stack_validate(self.cleanup_temp_files)
203
206
  all_level_counts = [0] * n
204
207
  if self.num_threads > 1:
205
- self.print_message(f': starting parallel image processing on {self.num_threads} cores')
208
+ self.print_message(f': starting parallel processing on {self.num_threads} cores')
206
209
  args_list = [(file_path, i, n) for i, file_path in enumerate(self.filenames)]
207
210
  executor = None
208
211
  try:
209
- executor = concurrent.futures.ThreadPoolExecutor(max_workers=self.num_threads)
212
+ executor = ThreadPoolExecutor(max_workers=self.num_threads)
210
213
  future_to_index = {
211
214
  executor.submit(self._process_single_image_wrapper, args): i
212
215
  for i, args in enumerate(args_list)
213
216
  }
214
217
  completed_count = 0
215
- for future in concurrent.futures.as_completed(future_to_index):
218
+ for future in as_completed(future_to_index):
216
219
  i = future_to_index[future]
217
220
  try:
218
221
  img_index, level_count = future.result()
219
222
  all_level_counts[img_index] = level_count
220
223
  completed_count += 1
221
- self.print_message(f': completed processing image {completed_count}/{n}')
224
+ self.print_message(
225
+ ": processing completed, image "
226
+ f"{self.idx_tot_str(completed_count - 1)}")
222
227
  except Exception as e:
223
- self.print_message(f"Error processing image {i + 1}: {str(e)}")
224
- self.after_step(i + n + 1)
228
+ self.print_message(
229
+ f"Error processing image {self.idx_tot_str(i)}: {str(e)}")
230
+ self.after_step(completed_count + n + 1)
225
231
  self.check_running(lambda: None)
226
232
  except RunStopException:
227
233
  self.print_message(": stopping image processing...")
@@ -235,7 +241,9 @@ class PyramidTilesStack(PyramidBase):
235
241
  executor.shutdown(wait=True)
236
242
  else:
237
243
  for i, file_path in enumerate(self.filenames):
238
- self.print_message(f": processing file {file_path.split('/')[-1]}, {i + 1}/{n}")
244
+ self.print_message(
245
+ f": processing file {os.path.basename(file_path)}, "
246
+ f"{self.idx_tot_str(i)}")
239
247
  img = read_img(file_path)
240
248
  level_count = self.process_single_image(img, self.n_levels, i)
241
249
  all_level_counts[i] = level_count
@@ -2,19 +2,19 @@
2
2
  import os
3
3
  import numpy as np
4
4
  from .. config.constants import constants
5
- from .. core.framework import JobBase
5
+ from .. core.framework import TaskBase
6
6
  from .. core.colors import color_str
7
7
  from .. core.exceptions import InvalidOptionError
8
8
  from .utils import write_img, extension_tif_jpg
9
- from .stack_framework import FrameDirectory, ActionList
9
+ from .stack_framework import ImageSequenceManager, SequentialTask
10
10
  from .exif import copy_exif_from_file_to_file
11
11
  from .denoise import denoise
12
12
 
13
13
 
14
- class FocusStackBase(JobBase, FrameDirectory):
14
+ class FocusStackBase(TaskBase, ImageSequenceManager):
15
15
  def __init__(self, name, stack_algo, enabled=True, **kwargs):
16
- FrameDirectory.__init__(self, name, **kwargs)
17
- JobBase.__init__(self, name, enabled)
16
+ ImageSequenceManager.__init__(self, name, **kwargs)
17
+ TaskBase.__init__(self, name, enabled)
18
18
  self.stack_algo = stack_algo
19
19
  self.exif_path = kwargs.pop('exif_path', '')
20
20
  self.prefix = kwargs.pop('prefix', constants.DEFAULT_STACK_PREFIX)
@@ -26,9 +26,10 @@ class FocusStackBase(JobBase, FrameDirectory):
26
26
  def focus_stack(self, filenames):
27
27
  self.sub_message_r(color_str(': reading input files', constants.LOG_COLOR_LEVEL_3))
28
28
  stacked_img = self.stack_algo.focus_stack()
29
- in_filename = filenames[0].split(".")
30
- out_filename = f"{self.output_dir}/{self.prefix}{in_filename[0]}." + \
31
- '.'.join(in_filename[1:])
29
+ in_filename = os.path.basename(filenames[0]).split(".")
30
+ out_filename = os.path.join(
31
+ self.output_full_path(),
32
+ f"{self.prefix}{in_filename[0]}." + '.'.join(in_filename[1:]))
32
33
  if self.denoise_amount > 0:
33
34
  self.sub_message_r(': denoise image')
34
35
  stacked_img = denoise(stacked_img, self.denoise_amount, self.denoise_amount)
@@ -45,14 +46,14 @@ class FocusStackBase(JobBase, FrameDirectory):
45
46
  name = f"{self.name}: {self.stack_algo.name()}"
46
47
  if idx_str != '':
47
48
  name += f"\nbunch: {idx_str}"
48
- self.callback('save_plot', self.id, name, out_filename)
49
+ self.callback(constants.CALLBACK_SAVE_PLOT, self.id, name, out_filename)
49
50
  if self.frame_count >= 0:
50
51
  self.frame_count += 1
51
52
 
52
53
  def init(self, job, working_path=''):
53
- FrameDirectory.init(self, job)
54
+ ImageSequenceManager.init(self, job)
54
55
  if self.exif_path is None:
55
- self.exif_path = job.paths[0]
56
+ self.exif_path = job.action_path(0)
56
57
  if self.exif_path != '':
57
58
  self.exif_path = working_path + "/" + self.exif_path
58
59
 
@@ -63,9 +64,9 @@ def get_bunches(collection, n_frames, n_overlap):
63
64
  return bunches
64
65
 
65
66
 
66
- class FocusStackBunch(ActionList, FocusStackBase):
67
+ class FocusStackBunch(SequentialTask, FocusStackBase):
67
68
  def __init__(self, name, stack_algo, enabled=True, **kwargs):
68
- ActionList.__init__(self, name, enabled)
69
+ SequentialTask.__init__(self, name, enabled)
69
70
  FocusStackBase.__init__(self, name, stack_algo, enabled, **kwargs)
70
71
  self._chunks = None
71
72
  self.frame_count = 0
@@ -77,25 +78,28 @@ class FocusStackBunch(ActionList, FocusStackBase):
77
78
  raise InvalidOptionError("overlap", self.overlap,
78
79
  "overlap must be smaller than batch size")
79
80
 
81
+ def sequential_processing(self):
82
+ return True
83
+
80
84
  def init(self, job, _working_path=''):
81
85
  FocusStackBase.init(self, job, self.working_path)
82
86
 
83
87
  def begin(self):
84
- ActionList.begin(self)
85
- fnames = self.folder_filelist()
86
- self._chunks = get_bunches(fnames, self.frames, self.overlap)
88
+ SequentialTask.begin(self)
89
+ self._chunks = get_bunches(self.input_filepaths(), self.frames, self.overlap)
87
90
  self.set_counts(len(self._chunks))
88
91
 
89
92
  def end(self):
90
- ActionList.end(self)
93
+ SequentialTask.end(self)
91
94
 
92
- def run_step(self):
93
- self.print_message_r(color_str(f"fusing bunch: {self.count + 1}/{self.counts}",
94
- constants.LOG_COLOR_LEVEL_2))
95
- img_files = [os.path.join(self.input_full_path, name)
96
- for name in self._chunks[self.count - 1]]
95
+ def run_step(self, action_count=-1):
96
+ self.print_message(
97
+ color_str(f"fusing bunch: {action_count + 1}/{self.total_action_counts}",
98
+ constants.LOG_COLOR_LEVEL_2))
99
+ img_files = self._chunks[action_count - 1]
97
100
  self.stack_algo.init(img_files)
98
- self.focus_stack(self._chunks[self.count - 1])
101
+ self.focus_stack(self._chunks[action_count - 1])
102
+ return True
99
103
 
100
104
 
101
105
  class FocusStack(FocusStackBase):
@@ -106,11 +110,11 @@ class FocusStack(FocusStackBase):
106
110
 
107
111
  def run_core(self):
108
112
  self.set_filelist()
109
- img_files = sorted([os.path.join(self.input_full_path, name) for name in self.filenames])
113
+ img_files = sorted(self.input_filepaths())
110
114
  self.stack_algo.init(img_files)
111
115
  self.callback('step_counts', self.id, self.name,
112
- self.stack_algo.total_steps(len(self.filenames)))
113
- self.focus_stack(self.filenames)
116
+ self.stack_algo.total_steps(self.num_input_filepaths()))
117
+ self.focus_stack(img_files)
114
118
 
115
119
  def init(self, job, _working_path=''):
116
120
  FocusStackBase.init(self, job, self.working_path)