shinestacker 1.3.0__py3-none-any.whl → 1.4.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 (50) hide show
  1. shinestacker/_version.py +1 -1
  2. shinestacker/algorithms/align.py +229 -41
  3. shinestacker/algorithms/align_auto.py +15 -3
  4. shinestacker/algorithms/align_parallel.py +81 -25
  5. shinestacker/algorithms/balance.py +23 -13
  6. shinestacker/algorithms/base_stack_algo.py +14 -20
  7. shinestacker/algorithms/depth_map.py +9 -14
  8. shinestacker/algorithms/noise_detection.py +3 -1
  9. shinestacker/algorithms/pyramid.py +8 -22
  10. shinestacker/algorithms/pyramid_auto.py +5 -14
  11. shinestacker/algorithms/pyramid_tiles.py +18 -20
  12. shinestacker/algorithms/stack_framework.py +1 -1
  13. shinestacker/algorithms/utils.py +37 -10
  14. shinestacker/algorithms/vignetting.py +2 -0
  15. shinestacker/app/gui_utils.py +10 -0
  16. shinestacker/app/main.py +3 -1
  17. shinestacker/app/project.py +3 -1
  18. shinestacker/app/retouch.py +3 -1
  19. shinestacker/config/gui_constants.py +2 -2
  20. shinestacker/core/core_utils.py +10 -1
  21. shinestacker/gui/action_config.py +172 -7
  22. shinestacker/gui/action_config_dialog.py +443 -452
  23. shinestacker/gui/colors.py +1 -0
  24. shinestacker/gui/folder_file_selection.py +5 -0
  25. shinestacker/gui/gui_run.py +2 -2
  26. shinestacker/gui/main_window.py +18 -9
  27. shinestacker/gui/menu_manager.py +26 -2
  28. shinestacker/gui/new_project.py +5 -5
  29. shinestacker/gui/project_controller.py +4 -0
  30. shinestacker/gui/project_editor.py +6 -4
  31. shinestacker/gui/recent_file_manager.py +93 -0
  32. shinestacker/gui/sys_mon.py +24 -23
  33. shinestacker/retouch/base_filter.py +5 -5
  34. shinestacker/retouch/brush_preview.py +3 -0
  35. shinestacker/retouch/brush_tool.py +11 -11
  36. shinestacker/retouch/display_manager.py +21 -37
  37. shinestacker/retouch/image_editor_ui.py +129 -71
  38. shinestacker/retouch/image_view_status.py +61 -0
  39. shinestacker/retouch/image_viewer.py +89 -431
  40. shinestacker/retouch/io_gui_handler.py +12 -2
  41. shinestacker/retouch/overlaid_view.py +212 -0
  42. shinestacker/retouch/shortcuts_help.py +13 -3
  43. shinestacker/retouch/sidebyside_view.py +479 -0
  44. shinestacker/retouch/view_strategy.py +466 -0
  45. {shinestacker-1.3.0.dist-info → shinestacker-1.4.0.dist-info}/METADATA +1 -1
  46. {shinestacker-1.3.0.dist-info → shinestacker-1.4.0.dist-info}/RECORD +50 -45
  47. {shinestacker-1.3.0.dist-info → shinestacker-1.4.0.dist-info}/WHEEL +0 -0
  48. {shinestacker-1.3.0.dist-info → shinestacker-1.4.0.dist-info}/entry_points.txt +0 -0
  49. {shinestacker-1.3.0.dist-info → shinestacker-1.4.0.dist-info}/licenses/LICENSE +0 -0
  50. {shinestacker-1.3.0.dist-info → shinestacker-1.4.0.dist-info}/top_level.txt +0 -0
@@ -8,9 +8,11 @@ from scipy.interpolate import interp1d
8
8
  from .. config.constants import constants
9
9
  from .. core.exceptions import InvalidOptionError
10
10
  from .. core.colors import color_str
11
+ from .. core.core_utils import setup_matplotlib_mode
11
12
  from .utils import (read_img, save_plot, img_subsample, bgr_to_hsv, bgr_to_hls,
12
13
  hsv_to_bgr, hls_to_bgr, bgr_to_lab, lab_to_bgr)
13
14
  from .stack_framework import SubAction
15
+ setup_matplotlib_mode()
14
16
 
15
17
 
16
18
  class BaseHistogrammer:
@@ -40,12 +42,11 @@ class BaseHistogrammer:
40
42
  x_values = np.linspace(0, self.max_pixel_value, len(hist))
41
43
  ax.plot(x_values, hist, color=color, alpha=alpha)
42
44
 
43
- def save_plot(self, idx):
45
+ def save_plot(self, idx, fig=None):
44
46
  idx_str = f"{idx:04d}"
45
47
  plot_path = f"{self.process.working_path}/{self.process.plot_path}/" \
46
48
  f"{self.process.name}-hist-{idx_str}.pdf"
47
- save_plot(plot_path)
48
- plt.close('all')
49
+ save_plot(plot_path, fig)
49
50
  self.process.callback(
50
51
  'save_plot',
51
52
  self.process.id, f"{self.process.name}: balance\nframe {idx_str}",
@@ -56,7 +57,6 @@ class BaseHistogrammer:
56
57
  plot_path = f"{self.process.working_path}/{self.process.plot_path}/" \
57
58
  f"{self.process.name}-{name}.pdf"
58
59
  save_plot(plot_path)
59
- plt.close('all')
60
60
  self.process.callback(
61
61
  'save_plot', self.process.id,
62
62
  f"{self.process.name}: {name}", plot_path
@@ -69,13 +69,14 @@ class LumiHistogrammer(BaseHistogrammer):
69
69
  self.colors = ("r", "g", "b")
70
70
 
71
71
  def generate_frame_plot(self, idx, hist, chans, calc_hist_func):
72
- _fig, axs = plt.subplots(1, 2, figsize=constants.PLT_FIG_SIZE, sharey=True)
72
+ fig, axs = plt.subplots(1, 2, figsize=constants.PLT_FIG_SIZE, sharey=True)
73
73
  self.histo_plot(axs[0], hist, "pixel luminosity", 'black')
74
74
  for (chan, color) in zip(chans, self.colors):
75
75
  hist_col = calc_hist_func(chan)
76
76
  self.histo_plot(axs[1], hist_col, "R, G, B intensity", color, alpha=0.5)
77
+ fig.suptitle("Image histograms")
77
78
  plt.xlim(0, self.max_pixel_value)
78
- self.save_plot(idx)
79
+ self.save_plot(idx, fig)
79
80
 
80
81
  def generate_summary_plot(self, ref_idx):
81
82
  plt.figure(figsize=constants.PLT_FIG_SIZE)
@@ -86,6 +87,7 @@ class LumiHistogrammer(BaseHistogrammer):
86
87
  plt.plot([x[0], x[-1]], [1, 1], color='lightgray', linestyle='--',
87
88
  label='no correction')
88
89
  plt.plot(x, y, color='navy', label='luminosity correction')
90
+ plt.title("Image balance correction")
89
91
  plt.xlabel('frame')
90
92
  plt.ylabel('correction')
91
93
  plt.legend()
@@ -100,11 +102,12 @@ class RGBHistogrammer(BaseHistogrammer):
100
102
  self.colors = ("r", "g", "b")
101
103
 
102
104
  def generate_frame_plot(self, idx, hists):
103
- _fig, axs = plt.subplots(1, 3, figsize=constants.PLT_FIG_SIZE, sharey=True)
105
+ fig, axs = plt.subplots(1, 3, figsize=constants.PLT_FIG_SIZE, sharey=True)
104
106
  for c in [2, 1, 0]:
105
107
  self.histo_plot(axs[c], hists[c], self.colors[c] + " luminosity", self.colors[c])
108
+ fig.suptitle("Image histograms")
106
109
  plt.xlim(0, self.max_pixel_value)
107
- self.save_plot(idx)
110
+ self.save_plot(idx, fig)
108
111
 
109
112
  def generate_summary_plot(self, ref_idx):
110
113
  plt.figure(figsize=constants.PLT_FIG_SIZE)
@@ -118,6 +121,7 @@ class RGBHistogrammer(BaseHistogrammer):
118
121
  plt.plot(x, y[:, 0], color='r', label='R correction')
119
122
  plt.plot(x, y[:, 1], color='g', label='G correction')
120
123
  plt.plot(x, y[:, 2], color='b', label='B correction')
124
+ plt.title("Image balance correction")
121
125
  plt.xlabel('frame')
122
126
  plt.ylabel('correction')
123
127
  plt.legend()
@@ -133,10 +137,12 @@ class Ch1Histogrammer(BaseHistogrammer):
133
137
  self.colors = colors
134
138
 
135
139
  def generate_frame_plot(self, idx, hists):
136
- _fig, axs = plt.subplots(1, 3, figsize=constants.PLT_FIG_SIZE, sharey=True)
140
+ fig, axs = plt.subplots(1, 3, figsize=constants.PLT_FIG_SIZE, sharey=True)
137
141
  for c in range(3):
138
142
  self.histo_plot(axs[c], hists[c], self.labels[c], self.colors[c])
139
- plt.xlim(0, self.max_pixel_value)
143
+ fig.suptitle("Image histograms")
144
+ for ax in axs:
145
+ ax.set_xlim(0, self.max_pixel_value)
140
146
  self.save_plot(idx)
141
147
 
142
148
  def generate_summary_plot(self, ref_idx):
@@ -149,6 +155,7 @@ class Ch1Histogrammer(BaseHistogrammer):
149
155
  plt.plot([x[0], x[-1]], [1, 1], color='lightgray', linestyle='--',
150
156
  label='no correction')
151
157
  plt.plot(x, y[:, 0], color=self.colors[0], label=self.labels[0] + ' correction')
158
+ plt.title("Image balance correction")
152
159
  plt.xlabel('frame')
153
160
  plt.ylabel('correction')
154
161
  plt.legend()
@@ -164,10 +171,12 @@ class Ch2Histogrammer(BaseHistogrammer):
164
171
  self.colors = colors
165
172
 
166
173
  def generate_frame_plot(self, idx, hists):
167
- _fig, axs = plt.subplots(1, 3, figsize=constants.PLT_FIG_SIZE, sharey=True)
174
+ fig, axs = plt.subplots(1, 3, figsize=constants.PLT_FIG_SIZE, sharey=True)
168
175
  for c in range(3):
169
176
  self.histo_plot(axs[c], hists[c], self.labels[c], self.colors[c])
170
- plt.xlim(0, self.max_pixel_value)
177
+ fig.suptitle("Image histograms")
178
+ for ax in axs:
179
+ ax.set_xlim(0, self.max_pixel_value)
171
180
  self.save_plot(idx)
172
181
 
173
182
  def generate_summary_plot(self, ref_idx):
@@ -181,6 +190,7 @@ class Ch2Histogrammer(BaseHistogrammer):
181
190
  label='no correction')
182
191
  plt.plot(x, y[:, 0], color=self.colors[1], label=self.labels[1] + ' correction')
183
192
  plt.plot(x, y[:, 1], color=self.colors[2], label=self.labels[2] + ' correction')
193
+ plt.title("Image balance correction")
184
194
  plt.xlabel('frame')
185
195
  plt.ylabel('correction')
186
196
  plt.legend()
@@ -603,7 +613,7 @@ class BalanceFrames(SubAction):
603
613
  mask_radius = int(min(*shape) * self.mask_size / 2)
604
614
  cv2.circle(img, (shape[1] // 2, shape[0] // 2), mask_radius, 255, -1)
605
615
  plt.figure(figsize=constants.PLT_FIG_SIZE)
606
- plt.title('Mask')
616
+ plt.title('Image balance mask')
607
617
  plt.imshow(img, 'gray')
608
618
  self.correction.histogrammer.save_summary_plot("mask")
609
619
 
@@ -1,10 +1,10 @@
1
- # pylint: disable=C0114, C0115, C0116, E0602, R0903
1
+ # pylint: disable=C0114, C0115, C0116, E0602, R0903, R0902
2
2
  import os
3
3
  import numpy as np
4
- from .. core.exceptions import InvalidOptionError, ImageLoadError, RunStopException
4
+ from .. core.exceptions import InvalidOptionError, RunStopException
5
5
  from .. config.constants import constants
6
6
  from .. core.colors import color_str
7
- from .utils import read_img, get_img_metadata, validate_image, get_img_file_shape, extension_tif_jpg
7
+ from .utils import read_img, get_img_metadata, get_first_image_file
8
8
 
9
9
 
10
10
  class BaseStackAlgo:
@@ -14,6 +14,9 @@ class BaseStackAlgo:
14
14
  self.process = None
15
15
  self.filenames = None
16
16
  self.shape = None
17
+ self.dtype = None
18
+ self.num_pixel_values = None
19
+ self.max_pixel_value = None
17
20
  self.do_step_callback = False
18
21
  if float_type == constants.FLOAT_32:
19
22
  self.float_type = np.float32
@@ -41,14 +44,16 @@ class BaseStackAlgo:
41
44
  return f"image: {self.idx_tot_str(idx)}, " \
42
45
  f"{os.path.basename(self.filenames[idx])}"
43
46
 
47
+ def num_images(self):
48
+ return len(self.filenames)
49
+
44
50
  def init(self, filenames):
45
51
  self.filenames = filenames
46
- first_img_file = ''
47
- for filename in filenames:
48
- if os.path.isfile(filename) and extension_tif_jpg(filename):
49
- first_img_file = filename
50
- break
51
- self.shape = get_img_file_shape(first_img_file)
52
+ self.shape, self.dtype = get_img_metadata(read_img(get_first_image_file(filenames)))
53
+ self.num_pixel_values = constants.NUM_UINT8 \
54
+ if self.dtype == np.uint8 else constants.NUM_UINT16
55
+ self.max_pixel_value = constants.MAX_UINT8 \
56
+ if self.dtype == np.uint8 else constants.MAX_UINT16
52
57
 
53
58
  def total_steps(self, n_frames):
54
59
  return self._steps_per_frame * n_frames
@@ -56,17 +61,6 @@ class BaseStackAlgo:
56
61
  def print_message(self, msg):
57
62
  self.process.sub_message_r(color_str(msg, constants.LOG_COLOR_LEVEL_3))
58
63
 
59
- def read_image_and_update_metadata(self, img_path, metadata):
60
- img = read_img(img_path)
61
- if img is None:
62
- raise ImageLoadError(img_path)
63
- updated = metadata is None
64
- if updated:
65
- metadata = get_img_metadata(img)
66
- else:
67
- validate_image(img, *metadata)
68
- return img, metadata, updated
69
-
70
64
  def check_running(self, cleanup_callback=None):
71
65
  if self.process.callback(constants.CALLBACK_CHECK_RUNNING,
72
66
  self.process.id, self.process.name) is False:
@@ -3,7 +3,7 @@ import numpy as np
3
3
  import cv2
4
4
  from .. config.constants import constants
5
5
  from .. core.exceptions import InvalidOptionError
6
- from .utils import read_img, img_bw
6
+ from .utils import read_img, read_and_validate_img, img_bw
7
7
  from .base_stack_algo import BaseStackAlgo
8
8
 
9
9
 
@@ -62,19 +62,15 @@ class DepthMapStack(BaseStackAlgo):
62
62
  f"{constants.DM_MAP_AVERAGE} and {constants.DM_MAP_MAX}.")
63
63
 
64
64
  def focus_stack(self):
65
- gray_images = []
66
- metadata = None
65
+ n_images = len(self.filenames)
66
+ gray_images = np.empty((n_images, *self.shape), dtype=self.float_type)
67
67
  for i, img_path in enumerate(self.filenames):
68
- self.print_message(f": reading file (1/2) {img_path.split('/')[-1]}")
69
-
70
- img, metadata, _updated = self.read_image_and_update_metadata(img_path, metadata)
71
-
68
+ self.print_message(f": reading and validating {self.image_str(i)}")
69
+ img = read_and_validate_img(img_path, self.shape, self.dtype)
72
70
  gray = img_bw(img)
73
- gray_images.append(gray)
71
+ gray_images[i] = gray.astype(self.float_type)
74
72
  self.after_step(i)
75
73
  self.check_running()
76
- dtype = metadata[1]
77
- gray_images = np.array(gray_images, dtype=self.float_type)
78
74
  if self.energy == constants.DM_ENERGY_SOBEL:
79
75
  energies = self.get_sobel_map(gray_images)
80
76
  elif self.energy == constants.DM_ENERGY_LAPLACIAN:
@@ -92,7 +88,7 @@ class DepthMapStack(BaseStackAlgo):
92
88
  weights = self.get_focus_map(energies)
93
89
  blended_pyramid = None
94
90
  for i, img_path in enumerate(self.filenames):
95
- self.print_message(f": reading file (2/2) {img_path.split('/')[-1]}")
91
+ self.print_message(f": reading {self.image_str(i)}")
96
92
  img = read_img(img_path).astype(self.float_type)
97
93
  weight = weights[i]
98
94
  gp_img = [img]
@@ -109,12 +105,11 @@ class DepthMapStack(BaseStackAlgo):
109
105
  for j in range(self.levels)]
110
106
  blended_pyramid = current_blend if blended_pyramid is None \
111
107
  else [np.add(bp, cb) for bp, cb in zip(blended_pyramid, current_blend)]
112
- self.after_step(i + len(self.filenames))
108
+ self.after_step(i + n_images)
113
109
  self.check_running()
114
110
  result = blended_pyramid[0]
115
111
  self.print_message(': blend levels')
116
112
  for j in range(1, self.levels):
117
113
  size = (blended_pyramid[j].shape[1], blended_pyramid[j].shape[0])
118
114
  result = cv2.pyrUp(result, dstsize=size) + blended_pyramid[j]
119
- n_values = constants.MAX_UINT8 if dtype == np.uint8 else constants.MAX_UINT16
120
- return np.clip(np.absolute(result), 0, n_values).astype(dtype)
115
+ return np.clip(np.absolute(result), 0, self.num_pixel_values).astype(self.dtype)
@@ -10,10 +10,12 @@ from .. config.constants import constants
10
10
  from .. core.colors import color_str
11
11
  from .. core.exceptions import ImageLoadError
12
12
  from .. core.framework import TaskBase
13
- from .. core.core_utils import make_tqdm_bar
13
+ from .. core.core_utils import make_tqdm_bar, setup_matplotlib_mode
14
14
  from .. core.exceptions import RunStopException, ShapeError
15
15
  from .stack_framework import ImageSequenceManager, SubAction
16
16
  from .utils import read_img, save_plot, get_img_metadata, validate_image
17
+ setup_matplotlib_mode()
18
+
17
19
 
18
20
  MAX_NOISY_PIXELS = 1000
19
21
 
@@ -2,7 +2,7 @@
2
2
  import numpy as np
3
3
  import cv2
4
4
  from .. config.constants import constants
5
- from .utils import read_img
5
+ from .utils import read_and_validate_img
6
6
  from .base_stack_algo import BaseStackAlgo
7
7
 
8
8
 
@@ -11,7 +11,7 @@ class PyramidBase(BaseStackAlgo):
11
11
  kernel_size=constants.DEFAULT_PY_KERNEL_SIZE,
12
12
  gen_kernel=constants.DEFAULT_PY_GEN_KERNEL,
13
13
  float_type=constants.DEFAULT_PY_FLOAT):
14
- super().__init__(name, 2, float_type)
14
+ super().__init__(name, 1, float_type)
15
15
  self.min_size = min_size
16
16
  self.kernel_size = kernel_size
17
17
  self.pad_amount = (kernel_size - 1) // 2
@@ -30,7 +30,7 @@ class PyramidBase(BaseStackAlgo):
30
30
 
31
31
  def total_steps(self, n_frames):
32
32
  self.n_frames = n_frames
33
- return self._steps_per_frame * n_frames + self.n_levels
33
+ return super().total_steps(n_frames) + self.n_levels
34
34
 
35
35
  def convolve(self, image):
36
36
  return cv2.filter2D(image, -1, self.gen_kernel, borderType=cv2.BORDER_REFLECT101)
@@ -122,21 +122,6 @@ class PyramidBase(BaseStackAlgo):
122
122
  fused += np.where(best_d[:, :, np.newaxis] == layer, img, 0)
123
123
  return (fused / 2).astype(images.dtype)
124
124
 
125
- def focus_stack_validate(self, cleanup_callback=None):
126
- metadata = None
127
- for i, img_path in enumerate(self.filenames):
128
- self.print_message(
129
- f": validating file {self.image_str(i)}")
130
- _img, metadata, updated = self.read_image_and_update_metadata(img_path, metadata)
131
- if updated:
132
- self.dtype = metadata[1]
133
- self.num_pixel_values = constants.NUM_UINT8 \
134
- if self.dtype == np.uint8 else constants.NUM_UINT16
135
- self.max_pixel_value = constants.MAX_UINT8 \
136
- if self.dtype == np.uint8 else constants.MAX_UINT16
137
- self.after_step(i + 1)
138
- self.check_running(cleanup_callback)
139
-
140
125
  def single_image_laplacian(self, img, levels):
141
126
  pyramid = [img.astype(self.float_type)]
142
127
  for _ in range(levels):
@@ -180,15 +165,16 @@ class PyramidStack(PyramidBase):
180
165
  return fused[::-1]
181
166
 
182
167
  def focus_stack(self):
183
- n = len(self.filenames)
184
- self.focus_stack_validate()
185
168
  all_laplacians = []
186
169
  for i, img_path in enumerate(self.filenames):
170
+ self.print_message(
171
+ f": reading and validating {self.image_str(i)}")
172
+ img = read_and_validate_img(img_path, self.shape, self.dtype)
173
+ self.check_running()
187
174
  self.print_message(
188
175
  f": processing {self.image_str(i)}")
189
- img = read_img(img_path)
190
176
  all_laplacians.append(self.process_single_image(img, self.n_levels))
191
- self.after_step(i + n + 1)
177
+ self.after_step(i + 1)
192
178
  self.check_running()
193
179
  stacked_image = self.collapse(self.fuse_pyramids(all_laplacians))
194
180
  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 .utils import extension_tif_jpg
6
5
  from .base_stack_algo import BaseStackAlgo
7
6
  from .pyramid import PyramidStack
8
7
  from .pyramid_tiles import PyramidTilesStack
@@ -21,7 +20,7 @@ class PyramidAutoStack(BaseStackAlgo):
21
20
  min_tile_size=constants.DEFAULT_PY_MIN_TILE_SIZE,
22
21
  min_n_tiled_layers=constants.DEFAULT_PY_MIN_N_TILED_LAYERS,
23
22
  mode='auto'):
24
- super().__init__("auto_pyramid", 2, float_type)
23
+ super().__init__("auto_pyramid", 1, float_type)
25
24
  self.min_size = min_size
26
25
  self.kernel_size = kernel_size
27
26
  self.gen_kernel = gen_kernel
@@ -47,15 +46,7 @@ class PyramidAutoStack(BaseStackAlgo):
47
46
  self.overhead = constants.PY_MEMORY_OVERHEAD
48
47
 
49
48
  def init(self, filenames):
50
- first_img_file = None
51
- for filename in filenames:
52
- if os.path.isfile(filename) and extension_tif_jpg(filename):
53
- first_img_file = filename
54
- break
55
- if first_img_file is None:
56
- raise ValueError("No valid image files found")
57
- _img, metadata, _ = self.read_image_and_update_metadata(first_img_file, None)
58
- self.shape, self.dtype = metadata
49
+ super().init(filenames)
59
50
  self.n_levels = int(np.log2(min(self.shape) / self.min_size))
60
51
  self.n_frames = len(filenames)
61
52
  memory_required_memory = self._estimate_memory_memory()
@@ -79,9 +70,9 @@ class PyramidAutoStack(BaseStackAlgo):
79
70
  n_tiled_layers=optimal_params['n_tiled_layers'],
80
71
  max_threads=self.num_threads
81
72
  )
82
- self.print_message(f": using tile-based pyramid stacking "
83
- f"(tile_size: {optimal_params['tile_size']}, "
84
- f"n_tiled_layers: {optimal_params['n_tiled_layers']}), "
73
+ self.print_message(f": using tile-based pyramid stacking, "
74
+ f"tile size: {optimal_params['tile_size']}, "
75
+ f"n. tiled layers: {optimal_params['n_tiled_layers']}, "
85
76
  f"{self.num_threads} cores.")
86
77
  self._implementation.init(filenames)
87
78
  self._implementation.set_do_step_callback(self.do_step_callback)
@@ -10,7 +10,7 @@ from concurrent.futures import ThreadPoolExecutor, as_completed
10
10
  import numpy as np
11
11
  from .. config.constants import constants
12
12
  from .. core.exceptions import RunStopException
13
- from .utils import read_img
13
+ from .utils import read_img, read_and_validate_img
14
14
  from .pyramid import PyramidBase
15
15
 
16
16
 
@@ -47,11 +47,11 @@ class PyramidTilesStack(PyramidBase):
47
47
  return n_steps + self.n_tiles
48
48
 
49
49
  def _process_single_image_wrapper(self, args):
50
- img_path, img_index, _n = args
51
- # self.print_message(f": processing file {img_path.split('/')[-1]}, {img_index + 1}/{n}")
52
- img = read_img(img_path)
53
- level_count = self.process_single_image(img, self.n_levels, img_index)
54
- return img_index, level_count
50
+ img_path, idx, _n = args
51
+ img = read_and_validate_img(img_path, self.shape, self.dtype)
52
+ self.check_running(self.cleanup_temp_files)
53
+ level_count = self.process_single_image(img, self.n_levels, idx)
54
+ return idx, level_count
55
55
 
56
56
  def process_single_image(self, img, levels, img_index):
57
57
  laplacian = self.single_image_laplacian(img, levels)
@@ -160,10 +160,11 @@ class PyramidTilesStack(PyramidBase):
160
160
  gc.collect()
161
161
  return np.zeros((y_end - y, x_end - x, 3), dtype=self.float_type)
162
162
 
163
- def fuse_pyramids(self, all_level_counts, num_images):
163
+ def fuse_pyramids(self, all_level_counts):
164
+ num_images = self.num_images()
164
165
  max_levels = max(all_level_counts)
165
166
  fused = []
166
- count = self._steps_per_frame * self.n_frames
167
+ count = super().total_steps(num_images)
167
168
  for level in range(max_levels - 1, -1, -1):
168
169
  self.print_message(f': fusing pyramids, layer: {level + 1}')
169
170
  if level < self.n_tiled_layers:
@@ -201,12 +202,11 @@ class PyramidTilesStack(PyramidBase):
201
202
  return fused[::-1]
202
203
 
203
204
  def focus_stack(self):
204
- n = len(self.filenames)
205
- self.focus_stack_validate(self.cleanup_temp_files)
206
- all_level_counts = [0] * n
205
+ all_level_counts = [0] * self.num_images()
207
206
  if self.num_threads > 1:
208
207
  self.print_message(f': starting parallel processing on {self.num_threads} cores')
209
- args_list = [(file_path, i, n) for i, file_path in enumerate(self.filenames)]
208
+ args_list = [(file_path, i, self.num_images())
209
+ for i, file_path in enumerate(self.filenames)]
210
210
  executor = None
211
211
  try:
212
212
  executor = ThreadPoolExecutor(max_workers=self.num_threads)
@@ -222,12 +222,11 @@ class PyramidTilesStack(PyramidBase):
222
222
  all_level_counts[img_index] = level_count
223
223
  completed_count += 1
224
224
  self.print_message(
225
- ": processing completed, image "
226
- f"{self.idx_tot_str(completed_count - 1)}")
225
+ f": processing completed, {self.image_str(completed_count - 1)}")
227
226
  except Exception as e:
228
227
  self.print_message(
229
- f"Error processing image {self.idx_tot_str(i)}: {str(e)}")
230
- self.after_step(completed_count + n + 1)
228
+ f"Error processing {self.image_str(i)}: {str(e)}")
229
+ self.after_step(completed_count)
231
230
  self.check_running(lambda: None)
232
231
  except RunStopException:
233
232
  self.print_message(": stopping image processing...")
@@ -242,16 +241,15 @@ class PyramidTilesStack(PyramidBase):
242
241
  else:
243
242
  for i, file_path in enumerate(self.filenames):
244
243
  self.print_message(
245
- f": processing file {os.path.basename(file_path)}, "
246
- f"{self.idx_tot_str(i)}")
244
+ f": processing {self.image_str(i)}")
247
245
  img = read_img(file_path)
248
246
  level_count = self.process_single_image(img, self.n_levels, i)
249
247
  all_level_counts[i] = level_count
250
- self.after_step(i + n + 1)
248
+ self.after_step(i + 1)
251
249
  self.check_running(lambda: None)
252
250
  try:
253
251
  self.check_running(lambda: None)
254
- fused_pyramid = self.fuse_pyramids(all_level_counts, n)
252
+ fused_pyramid = self.fuse_pyramids(all_level_counts)
255
253
  stacked_image = self.collapse(fused_pyramid)
256
254
  return stacked_image.astype(self.dtype)
257
255
  except RunStopException:
@@ -117,7 +117,7 @@ class ImageSequenceManager:
117
117
  assert False, "this method should be overwritten"
118
118
 
119
119
  def set_filelist(self):
120
- file_folder = self.input_full_path().replace(self.working_path, '').lstrip('/')
120
+ file_folder = os.path.relpath(self.input_full_path(), self.working_path)
121
121
  self.print_message(color_str(f"{self.num_input_filepaths()} files in folder: {file_folder}",
122
122
  constants.LOG_COLOR_LEVEL_2))
123
123
  self.base_message = color_str(self.name, constants.LOG_COLOR_LEVEL_1, "bold")
@@ -1,6 +1,8 @@
1
1
  # pylint: disable=C0114, C0116, E1101, R0914
2
2
  import os
3
+ import gc
3
4
  import logging
5
+ import threading
4
6
  import numpy as np
5
7
  import cv2
6
8
  import matplotlib.pyplot as plt
@@ -50,6 +52,10 @@ def extension_jpg_png(path):
50
52
  return extension_in(path, EXTENSIONS_JPG + EXTENSIONS_PNG)
51
53
 
52
54
 
55
+ def extension_jpg_tif_png(path):
56
+ return extension_in(path, EXTENSIONS_JPG + EXTENSIONS_TIF + EXTENSIONS_PNG)
57
+
58
+
53
59
  def read_img(file_path):
54
60
  if not os.path.isfile(file_path):
55
61
  raise RuntimeError("File does not exist: " + file_path)
@@ -87,6 +93,17 @@ def img_bw(img):
87
93
  return cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
88
94
 
89
95
 
96
+ def get_first_image_file(filenames):
97
+ first_img_file = None
98
+ for filename in filenames:
99
+ if os.path.isfile(filename) and extension_tif_jpg(filename):
100
+ first_img_file = filename
101
+ break
102
+ if first_img_file is None:
103
+ raise ValueError("No valid image files found")
104
+ return first_img_file
105
+
106
+
90
107
  def get_img_file_shape(file_path):
91
108
  img = read_img(file_path)
92
109
  return img.shape[:2]
@@ -106,19 +123,29 @@ def validate_image(img, expected_shape=None, expected_dtype=None):
106
123
  raise ShapeError(expected_shape, shape)
107
124
  if expected_dtype and dtype != expected_dtype:
108
125
  raise BitDepthError(expected_dtype, dtype)
126
+ return img
127
+
128
+
129
+ def read_and_validate_img(filename, expected_shape=None, expected_dtype=None):
130
+ return validate_image(read_img(filename), expected_shape, expected_dtype)
109
131
 
110
132
 
111
- def save_plot(filename):
133
+ def save_plot(filename, fig=None):
112
134
  logging.getLogger(__name__).debug(msg=f"save plot file: {filename}")
113
- dir_path = os.path.dirname(filename)
114
- if not dir_path:
115
- dir_path = '.'
116
- if not os.path.isdir(dir_path):
117
- os.makedirs(dir_path)
118
- plt.savefig(filename, dpi=150)
119
- if config.JUPYTER_NOTEBOOK:
120
- plt.show()
121
- plt.close('all')
135
+ save_lock = threading.Lock()
136
+ with save_lock:
137
+ dir_path = os.path.dirname(filename)
138
+ if not dir_path:
139
+ dir_path = '.'
140
+ if not os.path.isdir(dir_path):
141
+ os.makedirs(dir_path)
142
+ if fig is None:
143
+ fig = plt.gcf()
144
+ fig.savefig(filename, dpi=150)
145
+ if config.JUPYTER_NOTEBOOK:
146
+ plt.show()
147
+ plt.close(fig)
148
+ gc.collect()
122
149
 
123
150
 
124
151
  def img_subsample(img, subsample, fast=True):
@@ -7,9 +7,11 @@ import matplotlib.pyplot as plt
7
7
  from scipy.optimize import curve_fit, fsolve
8
8
  import cv2
9
9
  from .. core.colors import color_str
10
+ from .. core.core_utils import setup_matplotlib_mode
10
11
  from .. config.constants import constants
11
12
  from .utils import img_8bit, save_plot, img_subsample
12
13
  from .stack_framework import SubAction
14
+ setup_matplotlib_mode()
13
15
 
14
16
  CLIP_EXP = 10
15
17
 
@@ -53,3 +53,13 @@ def fill_app_menu(app, app_menu):
53
53
  exit_action.setShortcut(quit_short)
54
54
  exit_action.triggered.connect(app.quit)
55
55
  app_menu.addAction(exit_action)
56
+
57
+
58
+ def set_css_style(app):
59
+ css_style = """
60
+ QToolTip {
61
+ color: black;
62
+ border: 1px solid black;
63
+ }
64
+ """
65
+ app.setStyleSheet(css_style)
shinestacker/app/main.py CHANGED
@@ -16,7 +16,8 @@ from shinestacker.config.constants import constants
16
16
  from shinestacker.core.logging import setup_logging
17
17
  from shinestacker.gui.main_window import MainWindow
18
18
  from shinestacker.retouch.image_editor_ui import ImageEditorUI
19
- from shinestacker.app.gui_utils import disable_macos_special_menu_items, fill_app_menu
19
+ from shinestacker.app.gui_utils import (
20
+ disable_macos_special_menu_items, fill_app_menu, set_css_style)
20
21
  from shinestacker.app.help_menu import add_help_action
21
22
  from shinestacker.app.open_frames import open_frames
22
23
 
@@ -233,6 +234,7 @@ expert options are visible by default.
233
234
  app.setWindowIcon(QIcon(icon_path))
234
235
  main_app = MainApp()
235
236
  app.main_app = main_app
237
+ set_css_style(app)
236
238
  main_app.show()
237
239
  main_app.activateWindow()
238
240
  if args['expert']:
@@ -14,7 +14,8 @@ config.init(DISABLE_TQDM=True, DONT_USE_NATIVE_MENU=True)
14
14
  from shinestacker.config.constants import constants
15
15
  from shinestacker.core.logging import setup_logging
16
16
  from shinestacker.gui.main_window import MainWindow
17
- from shinestacker.app.gui_utils import disable_macos_special_menu_items, fill_app_menu
17
+ from shinestacker.app.gui_utils import (
18
+ disable_macos_special_menu_items, fill_app_menu, set_css_style)
18
19
  from shinestacker.app.help_menu import add_help_action
19
20
 
20
21
 
@@ -63,6 +64,7 @@ expert options are visible by default.
63
64
  disable_macos_special_menu_items()
64
65
  icon_path = f"{os.path.dirname(__file__)}/../gui/ico/shinestacker.png"
65
66
  app.setWindowIcon(QIcon(icon_path))
67
+ set_css_style(app)
66
68
  window = ProjectApp()
67
69
  if args['expert']:
68
70
  window.set_expert_options()
@@ -9,7 +9,8 @@ from shinestacker.config.config import config
9
9
  config.init(DISABLE_TQDM=True, DONT_USE_NATIVE_MENU=True)
10
10
  from shinestacker.config.constants import constants
11
11
  from shinestacker.retouch.image_editor_ui import ImageEditorUI
12
- from shinestacker.app.gui_utils import disable_macos_special_menu_items, fill_app_menu
12
+ from shinestacker.app.gui_utils import (
13
+ disable_macos_special_menu_items, fill_app_menu, set_css_style)
13
14
  from shinestacker.app.help_menu import add_help_action
14
15
  from shinestacker.app.open_frames import open_frames
15
16
 
@@ -60,6 +61,7 @@ Multiple directories can be specified separated by ';'.
60
61
  disable_macos_special_menu_items()
61
62
  icon_path = f"{os.path.dirname(__file__)}/../gui/ico/shinestacker.png"
62
63
  app.setWindowIcon(QIcon(icon_path))
64
+ set_css_style(app)
63
65
  editor = RetouchApp()
64
66
  app.editor = editor
65
67
  editor.show()
@@ -63,8 +63,8 @@ class _GuiConstants:
63
63
  DEFAULT_CURSOR_STYLE = 'preview'
64
64
  BRUSH_LINE_WIDTH = 2
65
65
  BRUSH_PREVIEW_LINE_WIDTH = 1.5
66
- ZOOM_IN_FACTOR = 1.25
67
- ZOOM_OUT_FACTOR = 0.80
66
+ ZOOM_IN_FACTOR = 1.10
67
+ ZOOM_OUT_FACTOR = 1 / ZOOM_IN_FACTOR
68
68
 
69
69
  def calculate_gamma(self):
70
70
  if self.BRUSH_SIZES['mid'] <= self.BRUSH_SIZES['min'] or self.BRUSH_SIZES['max'] <= 0: