shinestacker 0.5.0__py3-none-any.whl → 1.0.1__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 (57) hide show
  1. shinestacker/_version.py +1 -1
  2. shinestacker/algorithms/align.py +4 -12
  3. shinestacker/algorithms/balance.py +11 -9
  4. shinestacker/algorithms/depth_map.py +0 -30
  5. shinestacker/algorithms/utils.py +10 -0
  6. shinestacker/algorithms/vignetting.py +116 -70
  7. shinestacker/app/about_dialog.py +37 -16
  8. shinestacker/app/gui_utils.py +1 -1
  9. shinestacker/app/help_menu.py +1 -1
  10. shinestacker/app/main.py +2 -2
  11. shinestacker/app/project.py +2 -2
  12. shinestacker/config/constants.py +4 -1
  13. shinestacker/config/gui_constants.py +3 -4
  14. shinestacker/gui/action_config.py +5 -561
  15. shinestacker/gui/action_config_dialog.py +567 -0
  16. shinestacker/gui/base_form_dialog.py +18 -0
  17. shinestacker/gui/colors.py +5 -6
  18. shinestacker/gui/gui_logging.py +0 -1
  19. shinestacker/gui/gui_run.py +54 -106
  20. shinestacker/gui/ico/shinestacker.icns +0 -0
  21. shinestacker/gui/ico/shinestacker.ico +0 -0
  22. shinestacker/gui/ico/shinestacker.png +0 -0
  23. shinestacker/gui/ico/shinestacker.svg +60 -0
  24. shinestacker/gui/main_window.py +275 -371
  25. shinestacker/gui/menu_manager.py +236 -0
  26. shinestacker/gui/new_project.py +75 -20
  27. shinestacker/gui/{actions_window.py → project_controller.py} +166 -79
  28. shinestacker/gui/project_converter.py +6 -6
  29. shinestacker/gui/project_editor.py +248 -165
  30. shinestacker/gui/project_model.py +2 -7
  31. shinestacker/gui/tab_widget.py +81 -0
  32. shinestacker/gui/time_progress_bar.py +95 -0
  33. shinestacker/retouch/base_filter.py +173 -40
  34. shinestacker/retouch/brush_preview.py +0 -10
  35. shinestacker/retouch/brush_tool.py +2 -5
  36. shinestacker/retouch/denoise_filter.py +5 -44
  37. shinestacker/retouch/exif_data.py +10 -13
  38. shinestacker/retouch/file_loader.py +1 -1
  39. shinestacker/retouch/filter_manager.py +1 -4
  40. shinestacker/retouch/image_editor_ui.py +318 -40
  41. shinestacker/retouch/image_viewer.py +34 -11
  42. shinestacker/retouch/io_gui_handler.py +34 -30
  43. shinestacker/retouch/layer_collection.py +2 -0
  44. shinestacker/retouch/shortcuts_help.py +12 -0
  45. shinestacker/retouch/unsharp_mask_filter.py +10 -10
  46. shinestacker/retouch/vignetting_filter.py +69 -0
  47. shinestacker/retouch/white_balance_filter.py +46 -14
  48. {shinestacker-0.5.0.dist-info → shinestacker-1.0.1.dist-info}/METADATA +8 -2
  49. shinestacker-1.0.1.dist-info/RECORD +91 -0
  50. shinestacker/app/app_config.py +0 -22
  51. shinestacker/retouch/image_editor.py +0 -197
  52. shinestacker/retouch/image_filters.py +0 -69
  53. shinestacker-0.5.0.dist-info/RECORD +0 -87
  54. {shinestacker-0.5.0.dist-info → shinestacker-1.0.1.dist-info}/WHEEL +0 -0
  55. {shinestacker-0.5.0.dist-info → shinestacker-1.0.1.dist-info}/entry_points.txt +0 -0
  56. {shinestacker-0.5.0.dist-info → shinestacker-1.0.1.dist-info}/licenses/LICENSE +0 -0
  57. {shinestacker-0.5.0.dist-info → shinestacker-1.0.1.dist-info}/top_level.txt +0 -0
shinestacker/_version.py CHANGED
@@ -1 +1 @@
1
- __version__ = '0.5.0'
1
+ __version__ = '1.0.1'
@@ -6,8 +6,7 @@ import cv2
6
6
  from .. config.constants import constants
7
7
  from .. core.exceptions import AlignmentError, InvalidOptionError
8
8
  from .. core.colors import color_str
9
- from .utils import img_8bit, img_bw_8bit, save_plot
10
- from .utils import get_img_metadata, validate_image
9
+ from .utils import img_8bit, img_bw_8bit, save_plot, get_img_metadata, validate_image, img_subsample
11
10
  from .stack_framework import SubAction
12
11
 
13
12
  _DEFAULT_FEATURE_CONFIG = {
@@ -166,19 +165,12 @@ def align_images(img_1, img_0, feature_config=None, matching_config=None, alignm
166
165
  if callbacks and 'message' in callbacks:
167
166
  callbacks['message']()
168
167
  subsample = alignment_config['subsample']
168
+ fast_subsampling = alignment_config['fast_subsampling']
169
169
  min_good_matches = alignment_config['min_good_matches']
170
170
  while True:
171
171
  if subsample > 1:
172
- if alignment_config['fast_subsampling']:
173
- img_0_sub, img_1_sub = \
174
- img_0[::subsample, ::subsample], img_1[::subsample, ::subsample]
175
- else:
176
- img_0_sub = cv2.resize(img_0, (0, 0),
177
- fx=1 / subsample, fy=1 / subsample,
178
- interpolation=cv2.INTER_AREA)
179
- img_1_sub = cv2.resize(img_1, (0, 0),
180
- fx=1 / subsample, fy=1 / subsample,
181
- interpolation=cv2.INTER_AREA)
172
+ img_0_sub = img_subsample(img_0, subsample, fast_subsampling)
173
+ img_1_sub = img_subsample(img_1, subsample, fast_subsampling)
182
174
  else:
183
175
  img_0_sub, img_1_sub = img_0, img_1
184
176
  kp_0, kp_1, good_matches = detect_and_compute(img_0_sub, img_1_sub,
@@ -7,7 +7,7 @@ from scipy.interpolate import interp1d
7
7
  from .. config.constants import constants
8
8
  from .. core.exceptions import InvalidOptionError
9
9
  from .. core.colors import color_str
10
- from .utils import read_img, save_plot
10
+ from .utils import read_img, save_plot, img_subsample
11
11
  from .stack_framework import SubAction
12
12
 
13
13
 
@@ -122,13 +122,15 @@ class LinearMap(CorrectionMap):
122
122
 
123
123
  class Correction:
124
124
  def __init__(self, channels, mask_size=0, intensity_interval=None,
125
- subsample=-1, corr_map=constants.DEFAULT_CORR_MAP,
125
+ subsample=-1, fast_subsampling=constants.DEFAULT_BALANCE_FAST_SUBSAMPLING,
126
+ corr_map=constants.DEFAULT_CORR_MAP,
126
127
  plot_histograms=False, plot_summary=False):
127
128
  self.mask_size = mask_size
128
129
  self.intensity_interval = intensity_interval
129
130
  self.plot_histograms = plot_histograms
130
131
  self.plot_summary = plot_summary
131
132
  self.subsample = constants.DEFAULT_BALANCE_SUBSAMPLE if subsample == -1 else subsample
133
+ self.fast_subsampling = fast_subsampling
132
134
  self.corr_map = corr_map
133
135
  self.channels = channels
134
136
  self.dtype = None
@@ -154,17 +156,18 @@ class Correction:
154
156
  self.corrections = np.ones((size, self.channels))
155
157
 
156
158
  def calc_hist_1ch(self, image):
157
- img_subsample = image if self.subsample == 1 else image[::self.subsample, ::self.subsample]
159
+ img_sub = image if self.subsample == 1 \
160
+ else img_subsample(image, self.subsample, self.fast_subsampling)
158
161
  if self.mask_size == 0:
159
- image_sel = img_subsample
162
+ image_sel = img_sub
160
163
  else:
161
- height, width = img_subsample.shape[:2]
164
+ height, width = img_sub.shape[:2]
162
165
  xv, yv = np.meshgrid(
163
166
  np.linspace(0, width - 1, width),
164
167
  np.linspace(0, height - 1, height)
165
168
  )
166
169
  mask_radius = min(width, height) * self.mask_size / 2
167
- image_sel = img_subsample[
170
+ image_sel = img_sub[
168
171
  (xv - width / 2) ** 2 + (yv - height / 2) ** 2 <= mask_radius ** 2
169
172
  ]
170
173
  hist, _bins = np.histogram(
@@ -305,9 +308,6 @@ class Ch2Correction(Correction):
305
308
  def preprocess(self, image):
306
309
  assert False, 'abstract method'
307
310
 
308
- def get_labels(self):
309
- assert False, 'abstract method'
310
-
311
311
  def get_hist(self, image, idx):
312
312
  hist = [self.calc_hist_1ch(chan) for chan in cv2.split(image)]
313
313
  if self.plot_histograms:
@@ -370,6 +370,8 @@ class BalanceFrames(SubAction):
370
370
  self.shape = None
371
371
  corr_map = kwargs.get('corr_map', constants.DEFAULT_CORR_MAP)
372
372
  subsample = kwargs.get('subsample', constants.DEFAULT_BALANCE_SUBSAMPLE)
373
+ self.fast_subsampling = kwargs.get(
374
+ 'fast_subsampling', constants.DEFAULT_BALANCE_FAST_SUBSAMPLING)
373
375
  channel = kwargs.pop('channel', constants.DEFAULT_CHANNEL)
374
376
  kwargs['subsample'] = (
375
377
  1 if corr_map == constants.BALANCE_MATCH_HIST
@@ -61,36 +61,6 @@ class DepthMapStack(BaseStackAlgo):
61
61
  raise InvalidOptionError("map_type", self.map_type, details=f" valid values are "
62
62
  f"{constants.DM_MAP_AVERAGE} and {constants.DM_MAP_MAX}.")
63
63
 
64
- def pyramid_blend(self, images, weights):
65
- blended = None
66
- for i in range(images.shape[0]):
67
- img = images[i].astype(self.float_type)
68
- weight = weights[i]
69
- gp_img = [img]
70
- gp_weight = [weight]
71
- for _ in range(self.levels - 1):
72
- gp_img.append(cv2.pyrDown(gp_img[-1]))
73
- gp_weight.append(cv2.pyrDown(gp_weight[-1]))
74
- lp_img = [gp_img[-1]]
75
- for j in range(self.levels - 1, 0, -1):
76
- size = (gp_img[j - 1].shape[1], gp_img[j - 1].shape[0])
77
- expanded = cv2.pyrUp(gp_img[j], dstsize=size)
78
- lp_img.append(gp_img[j - 1] - expanded)
79
- current_blend = []
80
- for j in range(self.levels):
81
- w = gp_weight[self.levels - 1 - j][..., np.newaxis]
82
- current_blend.append(lp_img[j] * w)
83
- if blended is None:
84
- blended = current_blend
85
- else:
86
- for j in range(self.levels):
87
- blended[j] += current_blend[j]
88
- result = blended[0]
89
- for j in range(1, self.levels):
90
- size = (blended[j].shape[1], blended[j].shape[0])
91
- result = cv2.pyrUp(result, dstsize=size) + blended[j]
92
- return result
93
-
94
64
  def focus_stack(self, filenames):
95
65
  gray_images = []
96
66
  metadata = None
@@ -74,3 +74,13 @@ def save_plot(filename):
74
74
  if config.JUPYTER_NOTEBOOK:
75
75
  plt.show()
76
76
  plt.close('all')
77
+
78
+
79
+ def img_subsample(img, subsample, fast=True):
80
+ if fast:
81
+ img_sub = img[::subsample, ::subsample]
82
+ else:
83
+ img_sub = cv2.resize(img, (0, 0),
84
+ fx=1 / subsample, fy=1 / subsample,
85
+ interpolation=cv2.INTER_AREA)
86
+ return img_sub
@@ -1,4 +1,5 @@
1
- # pylint: disable=C0114, C0115, C0116, R0902, E1101, W0718, W0640
1
+ # pylint: disable=C0114, C0115, C0116, R0902, E1101, W0718, W0640, R0913, R0917, R0914
2
+ import traceback
2
3
  import logging
3
4
  import numpy as np
4
5
  import matplotlib.pyplot as plt
@@ -6,22 +7,108 @@ from scipy.optimize import curve_fit, fsolve
6
7
  import cv2
7
8
  from .. core.colors import color_str
8
9
  from .. config.constants import constants
9
- from .utils import img_8bit, save_plot
10
+ from .utils import img_8bit, save_plot, img_subsample
10
11
  from .stack_framework import SubAction
11
12
 
12
13
  CLIP_EXP = 10
13
14
 
14
15
 
16
+ def sigmoid_model(r, i0, k, r0):
17
+ return i0 / (1.0 +
18
+ np.exp(np.minimum(CLIP_EXP,
19
+ np.exp(np.clip(k * (r - r0),
20
+ -CLIP_EXP, CLIP_EXP)))))
21
+
22
+
23
+ def radial_mean_intensity(image, r_steps):
24
+ if len(image.shape) > 2:
25
+ raise ValueError("The image must be grayscale")
26
+ h, w = image.shape
27
+ w_2, h_2 = w / 2, h / 2
28
+ r_max = np.sqrt((w / 2)**2 + (h / 2)**2)
29
+ radii = np.linspace(0, r_max, r_steps + 1)
30
+ mean_intensities = np.zeros(r_steps)
31
+ y, x = np.ogrid[:h, :w]
32
+ dist_from_center = np.sqrt((x - w_2)**2 + (y - h_2)**2)
33
+ for i in range(r_steps):
34
+ mask = (dist_from_center >= radii[i]) & (dist_from_center < radii[i + 1])
35
+ if np.any(mask):
36
+ mean_intensities[i] = np.mean(image[mask])
37
+ else:
38
+ mean_intensities[i] = np.nan
39
+ return (radii[1:] + radii[:-1]) / 2, mean_intensities
40
+
41
+
42
+ def fit_sigmoid(radii, intensities):
43
+ valid_mask = ~np.isnan(intensities)
44
+ i_valid, r_valid = intensities[valid_mask], radii[valid_mask]
45
+ r_max = radii.max()
46
+ res = curve_fit(sigmoid_model, r_valid, i_valid,
47
+ p0=[2 * np.max(i_valid), 10 / r_max, 0.8 * r_max],
48
+ bounds=([0, 0, 0], ['inf', 'inf', 'inf']))[0]
49
+ return res
50
+
51
+
52
+ def img_subsampled(image, subsample=constants.DEFAULT_VIGN_SUBSAMPLE,
53
+ fast_subsampling=constants.DEFAULT_VIGN_FAST_SUBSAMPLING):
54
+ image_bw = cv2.cvtColor(img_8bit(image), cv2.COLOR_BGR2GRAY)
55
+ return image_bw if subsample == 1 else img_subsample(image_bw, subsample, fast_subsampling)
56
+
57
+
58
+ def compute_fit_parameters(
59
+ image, r_steps, radii=None, intensities=None,
60
+ subsample=constants.DEFAULT_VIGN_SUBSAMPLE,
61
+ fast_subsampling=constants.DEFAULT_VIGN_FAST_SUBSAMPLING):
62
+ image_sub = img_subsampled(image, subsample, fast_subsampling)
63
+ if radii is None and intensities is None:
64
+ radii, intensities = radial_mean_intensity(image_sub, r_steps)
65
+ params = fit_sigmoid(radii, intensities)
66
+ params[1] /= subsample # k
67
+ params[2] *= subsample # r0
68
+ return params
69
+
70
+
71
+ def correct_vignetting(
72
+ image, max_correction=constants.DEFAULT_MAX_CORRECTION,
73
+ black_threshold=constants.DEFAULT_BLACK_THRESHOLD,
74
+ r_steps=constants.DEFAULT_R_STEPS, params=None, v0=None,
75
+ subsample=constants.DEFAULT_VIGN_SUBSAMPLE,
76
+ fast_subsampling=constants.DEFAULT_VIGN_FAST_SUBSAMPLING):
77
+ if params is None:
78
+ if r_steps is None:
79
+ raise RuntimeError("Either r_steps or pars must not be None")
80
+ params = compute_fit_parameters(
81
+ image, r_steps, subsample=subsample, fast_subsampling=fast_subsampling)
82
+ if v0 is None:
83
+ v0 = sigmoid_model(0, *params)
84
+ h, w = image.shape[:2]
85
+ y, x = np.ogrid[:h, :w]
86
+ r = np.sqrt((x - w / 2)**2 + (y - h / 2)**2)
87
+ vignette = np.clip(sigmoid_model(r, *params) / v0, 1e-6, 1)
88
+ if max_correction < 1:
89
+ vignette = (1.0 - max_correction) + vignette * max_correction
90
+ threshold = black_threshold if image.dtype == np.uint8 else black_threshold * 256
91
+ if len(image.shape) == 3:
92
+ vignette = vignette[:, :, np.newaxis]
93
+ vignette[np.min(image, axis=2) < threshold, :] = 1
94
+ else:
95
+ vignette[image < black_threshold] = 1
96
+ return np.clip(image / vignette, 0, 255
97
+ if image.dtype == np.uint8 else 65535).astype(image.dtype)
98
+
99
+
15
100
  class Vignetting(SubAction):
16
101
  def __init__(self, enabled=True, percentiles=(0.05, 0.1, 0.25, 0.5, 0.75, 0.9, 0.95), **kwargs):
17
102
  super().__init__(enabled)
18
103
  self.r_steps = kwargs.get('r_steps', constants.DEFAULT_R_STEPS)
19
104
  self.black_threshold = kwargs.get('black_threshold', constants.DEFAULT_BLACK_THRESHOLD)
20
- self.apply_correction = kwargs.get('apply_correction', True)
21
105
  self.plot_correction = kwargs.get('plot_correction', False)
22
106
  self.plot_summary = kwargs.get('plot_summary', False)
23
107
  self.max_correction = kwargs.get('max_correction', constants.DEFAULT_MAX_CORRECTION)
24
108
  self.percentiles = np.sort(percentiles)
109
+ self.subsample = kwargs.get('subsample', constants.DEFAULT_VIGN_SUBSAMPLE)
110
+ self.fast_subsampling = kwargs.get(
111
+ 'fast_subsampling', constants.DEFAULT_VIGN_FAST_SUBSAMPLING)
25
112
  self.w_2 = None
26
113
  self.h_2 = None
27
114
  self.v0 = None
@@ -29,76 +116,35 @@ class Vignetting(SubAction):
29
116
  self.process = None
30
117
  self.corrections = None
31
118
 
32
- def radial_mean_intensity(self, image):
33
- if len(image.shape) > 2:
34
- raise ValueError("The image must be grayscale")
35
- h, w = image.shape
119
+ def run_frame(self, idx, _ref_idx, img_0):
120
+ self.process.sub_message_r(color_str(": compute vignetting", "cyan"))
121
+ h, w = img_0.shape[:2]
36
122
  self.w_2, self.h_2 = w / 2, h / 2
37
123
  self.r_max = np.sqrt((w / 2)**2 + (h / 2)**2)
38
- radii = np.linspace(0, self.r_max, self.r_steps + 1)
39
- mean_intensities = np.zeros(self.r_steps)
40
- y, x = np.ogrid[:h, :w]
41
- dist_from_center = np.sqrt((x - self.w_2)**2 + (y - self.h_2)**2)
42
- for i in range(self.r_steps):
43
- mask = (dist_from_center >= radii[i]) & (dist_from_center < radii[i + 1])
44
- if np.any(mask):
45
- mean_intensities[i] = np.mean(image[mask])
46
- else:
47
- mean_intensities[i] = np.nan
48
- return (radii[1:] + radii[:-1]) / 2, mean_intensities
49
-
50
- @staticmethod
51
- def sigmoid(r, i0, k, r0):
52
- return i0 / (1.0 +
53
- np.exp(np.minimum(CLIP_EXP,
54
- np.exp(np.clip(k * (r - r0),
55
- -CLIP_EXP, CLIP_EXP)))))
56
-
57
- def fit_sigmoid(self, radii, intensities):
58
- valid_mask = ~np.isnan(intensities)
59
- i_valid, r_valid = intensities[valid_mask], radii[valid_mask]
124
+ image_sub = img_subsampled(img_0, self.subsample, self.fast_subsampling)
125
+ radii, intensities = radial_mean_intensity(image_sub, self.r_steps)
60
126
  try:
61
- res = curve_fit(Vignetting.sigmoid, r_valid, i_valid,
62
- p0=[np.max(i_valid), 0.01, np.median(r_valid)],
63
- bounds=([0, 0, 0], ['inf', 'inf', 'inf']))[0]
64
- except Exception:
127
+ params = compute_fit_parameters(
128
+ img_0, self.r_steps, radii, intensities, self.subsample, self.fast_subsampling)
129
+ except Exception as e:
130
+ traceback.print_tb(e.__traceback__)
65
131
  self.process.sub_message(
66
- color_str(": could not find vignetting model", "red"),
67
- level=logging.WARNING)
68
- res = None
69
- return res
70
-
71
- def correct_vignetting(self, image, params):
72
- h, w = image.shape[:2]
73
- y, x = np.ogrid[:h, :w]
74
- r = np.sqrt((x - w / 2)**2 + (y - h / 2)**2)
75
- vignette = np.clip(Vignetting.sigmoid(r, *params) / self.v0, 1e-6, 1)
76
- if self.max_correction < 1:
77
- vignette = (1.0 - self.max_correction) + vignette * self.max_correction
78
- if len(image.shape) == 3:
79
- vignette = vignette[:, :, np.newaxis]
80
- vignette[np.min(image, axis=2) < self.black_threshold, :] = 1
81
- else:
82
- vignette[image < self.black_threshold] = 1
83
- return np.clip(image / vignette, 0, 255
84
- if image.dtype == np.uint8 else 65535).astype(image.dtype)
85
-
86
- def run_frame(self, idx, _ref_idx, img_0):
87
- self.process.sub_message_r(": compute vignetting")
88
- img = cv2.cvtColor(img_8bit(img_0), cv2.COLOR_BGR2GRAY)
89
- radii, intensities = self.radial_mean_intensity(img)
90
- pars = self.fit_sigmoid(radii, intensities)
91
- if pars is None:
132
+ color_str(": could not find vignetting model", "red"), level=logging.WARNING)
133
+ params = None
134
+ if params is None:
92
135
  return img_0
93
- self.v0 = Vignetting.sigmoid(0, *pars)
94
- i0_fit, k_fit, r0_fit = pars
95
- self.process.sub_message(
96
- f": vignetting model parameters: i0={i0_fit:.4f}, k={k_fit:.4f}, r0={r0_fit:.4f}",
97
- level=logging.DEBUG)
136
+ self.v0 = sigmoid_model(0, *params)
137
+ i0_fit, k_fit, r0_fit = params
138
+ self.process.sub_message(color_str(": vignetting model parameters: ", "cyan") +
139
+ color_str(f"i0={i0_fit / 2:.4f}, "
140
+ f"k={k_fit * self.r_max:.4f}, "
141
+ f"r0={r0_fit / self.r_max:.4f}",
142
+ "light_blue"),
143
+ level=logging.DEBUG)
98
144
  if self.plot_correction:
99
145
  plt.figure(figsize=(10, 5))
100
146
  plt.plot(radii, intensities, label="image mean intensity")
101
- plt.plot(radii, Vignetting.sigmoid(radii, *pars), label="sigmoid fit")
147
+ plt.plot(radii, sigmoid_model(radii * self.subsample, *params), label="sigmoid fit")
102
148
  plt.xlabel('radius (pixels)')
103
149
  plt.ylabel('mean intensity')
104
150
  plt.legend()
@@ -114,12 +160,12 @@ class Vignetting(SubAction):
114
160
  'save_plot', self.process.id,
115
161
  f"{self.process.name}: intensity\nframe {idx_str}", plot_path)
116
162
  for i, p in enumerate(self.percentiles):
117
- self.corrections[i][idx] = fsolve(lambda x: Vignetting.sigmoid(x, *pars) /
163
+ self.corrections[i][idx] = fsolve(lambda x: sigmoid_model(x, *params) /
118
164
  self.v0 - p, r0_fit)[0]
119
- if self.apply_correction:
120
- self.process.sub_message_r(": correct vignetting")
121
- return self.correct_vignetting(img_0, pars)
122
- return img_0
165
+ self.process.sub_message_r(color_str(": correct vignetting", "cyan"))
166
+ return correct_vignetting(
167
+ img_0, self.max_correction, self.black_threshold, None, params, self.v0,
168
+ self.subsample, self.fast_subsampling)
123
169
 
124
170
  def begin(self, process):
125
171
  self.process = process
@@ -1,13 +1,41 @@
1
- # pylint: disable=C0114, C0116, E0611, W0718
1
+ # pylint: disable=C0114, C0115, C0116, E0611, W0718, R0903
2
2
  import json
3
3
  from urllib.request import urlopen, Request
4
4
  from urllib.error import URLError
5
- from PySide6.QtWidgets import QMessageBox
5
+ from PySide6.QtWidgets import QDialog, QVBoxLayout, QHBoxLayout, QPushButton, QLabel
6
6
  from PySide6.QtCore import Qt
7
7
  from .. import __version__
8
+ from .. retouch.icon_container import icon_container
8
9
  from .. config.constants import constants
9
10
 
10
11
 
12
+ class AboutDialog(QDialog):
13
+ def __init__(self, parent=None, about_text=""):
14
+ super().__init__(parent)
15
+ self.setWindowTitle("About")
16
+ self.resize(400, 300)
17
+ layout = QVBoxLayout(self)
18
+ layout.setAlignment(Qt.AlignTop)
19
+ icon_widget = icon_container()
20
+ icon_layout = QHBoxLayout()
21
+ icon_layout.addStretch()
22
+ icon_layout.addWidget(icon_widget)
23
+ icon_layout.addStretch()
24
+ layout.addLayout(icon_layout)
25
+ about_label = QLabel(about_text)
26
+ about_label.setWordWrap(True)
27
+ about_label.setAlignment(Qt.AlignLeft)
28
+ layout.addWidget(about_label)
29
+ button_layout = QHBoxLayout()
30
+ button_layout.addStretch()
31
+ button = QPushButton("OK")
32
+ button.setFixedWidth(100)
33
+ button.clicked.connect(self.accept)
34
+ button_layout.addWidget(button)
35
+ button_layout.addStretch()
36
+ layout.addLayout(button_layout)
37
+
38
+
11
39
  def compare_versions(current, latest):
12
40
  def parse_version(v):
13
41
  v = v.lstrip('v')
@@ -49,7 +77,7 @@ def get_latest_version():
49
77
  return None
50
78
 
51
79
 
52
- def show_about_dialog():
80
+ def show_about_dialog(parent):
53
81
  version_clean = __version__.split("+", maxsplit=1)[0]
54
82
  latest_version = None
55
83
  try:
@@ -57,7 +85,6 @@ def show_about_dialog():
57
85
  except Exception:
58
86
  pass
59
87
  update_text = ""
60
- # pyling: disable=XXX
61
88
  if latest_version:
62
89
  latest_clean = latest_version.lstrip('v')
63
90
  if compare_versions(version_clean, latest_clean) < 0:
@@ -68,17 +95,17 @@ def show_about_dialog():
68
95
  </p>
69
96
  """ # noqa E501
70
97
  else:
71
- update_text = f"""
98
+ update_text = """
72
99
  <p style="color: green; font-weight: bold;">
73
- You are using the lastet version: {latest_version}.
100
+ You are using the lastet version.
74
101
  </p>
75
102
  """
76
103
  about_text = f"""
77
104
  <h3>{constants.APP_TITLE}</h3>
78
105
  <h4>version: v{version_clean}</h4>
79
106
  {update_text}
80
- <p style='font-weight: normal;'>App and framework to combine multiple images
81
- into a single focused image.</p>
107
+ <p style='font-weight: normal;'>Focus stackign applications and framework.<br>
108
+ Combine multiple frames into a single focused image.</p>
82
109
  <p>Author: Luca Lista<br/>
83
110
  Email: <a href="mailto:luka.lista@gmail.com">luka.lista@gmail.com</a></p>
84
111
  <ul>
@@ -86,11 +113,5 @@ def show_about_dialog():
86
113
  <li><a href="https://github.com/lucalista/shinestacker">GitHub project repository</a></li>
87
114
  </ul>
88
115
  """
89
- # pyling: enable=XXX
90
- msg = QMessageBox()
91
- msg.setWindowTitle(f"About {constants.APP_STRING}")
92
- msg.setIcon(QMessageBox.Icon.Information)
93
- msg.setTextFormat(Qt.TextFormat.RichText)
94
- msg.setText(about_text)
95
- msg.setIcon(QMessageBox.Icon.NoIcon)
96
- msg.exec_()
116
+ dialog = AboutDialog(parent, about_text)
117
+ dialog.exec()
@@ -42,7 +42,7 @@ def disable_macos_special_menu_items():
42
42
 
43
43
  def fill_app_menu(app, app_menu):
44
44
  about_action = QAction(f"About {constants.APP_STRING}", app)
45
- about_action.triggered.connect(show_about_dialog)
45
+ about_action.triggered.connect(lambda: show_about_dialog(app))
46
46
  app_menu.addAction(about_action)
47
47
  app_menu.addSeparator()
48
48
  if config.DONT_USE_NATIVE_MENU:
@@ -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/docs/gui.md")
17
+ webbrowser.open("https://shinestacker.readthedocs.io/en/latest/")
shinestacker/app/main.py CHANGED
@@ -141,7 +141,7 @@ expert options are visible by default.
141
141
  filename = filenames[0]
142
142
  extension = filename.split('.')[-1]
143
143
  if len(filenames) == 1 and extension == 'fsp':
144
- main_app.project_window.open_project(filename)
144
+ main_app.project_window.project_controller.open_project(filename)
145
145
  main_app.project_window.setFocus()
146
146
  else:
147
147
  main_app.switch_to_retouch()
@@ -152,7 +152,7 @@ expert options are visible by default.
152
152
  main_app.switch_to_retouch()
153
153
  else:
154
154
  main_app.switch_to_project()
155
- QTimer.singleShot(100, main_app.project_window.new_project)
155
+ QTimer.singleShot(100, main_app.project_window.project_controller.new_project)
156
156
  QTimer.singleShot(100, main_app.setFocus)
157
157
  sys.exit(app.exec())
158
158
 
@@ -70,9 +70,9 @@ expert options are visible by default.
70
70
  window.show()
71
71
  filename = args['filename']
72
72
  if filename:
73
- QTimer.singleShot(100, lambda: window.open_project(filename))
73
+ QTimer.singleShot(100, lambda: window.project_controller.open_project(filename))
74
74
  else:
75
- QTimer.singleShot(100, window.new_project)
75
+ QTimer.singleShot(100, window.project_controller.new_project)
76
76
  sys.exit(app.exec())
77
77
 
78
78
 
@@ -121,13 +121,16 @@ class _Constants:
121
121
  VALID_BALANCE_CHANNELS = [BALANCE_LUMI, BALANCE_RGB, BALANCE_HSV, BALANCE_HLS]
122
122
 
123
123
  DEFAULT_BALANCE_SUBSAMPLE = 8
124
+ DEFAULT_BALANCE_FAST_SUBSAMPLING = False
124
125
  DEFAULT_CORR_MAP = BALANCE_LINEAR
125
126
  DEFAULT_CHANNEL = BALANCE_LUMI
126
127
  DEFAULT_INTENSITY_INTERVAL = {'min': 0, 'max': -1}
127
128
 
128
129
  DEFAULT_R_STEPS = 100
129
- DEFAULT_BLACK_THRESHOLD = 1
130
+ DEFAULT_BLACK_THRESHOLD = 1.0
130
131
  DEFAULT_MAX_CORRECTION = 1
132
+ DEFAULT_VIGN_SUBSAMPLE = 8
133
+ DEFAULT_VIGN_FAST_SUBSAMPLING = False
131
134
 
132
135
  FLOAT_32 = 'float-32'
133
136
  FLOAT_64 = 'float-64'
@@ -6,8 +6,9 @@ class _GuiConstants:
6
6
  GUI_IMG_WIDTH = 250 # px
7
7
  DISABLED_TAG = "" # " <disabled>"
8
8
 
9
- MIN_ZOOMED_IMG_WIDTH = 400
10
- MAX_ZOOMED_IMG_PX_SIZE = 50
9
+ MIN_ZOOMED_IMG_WIDTH = 600
10
+ MIN_ZOOMED_IMG_HEIGHT = 600
11
+ MAX_ZOOMED_IMG_PX_SIZE = 40
11
12
  MAX_UNDO_SIZE = 65535
12
13
 
13
14
  NEW_PROJECT_NOISE_DETECTION = False
@@ -41,8 +42,6 @@ class _GuiConstants:
41
42
  THUMB_MASTER_HI_COLOR = '#0000FF'
42
43
  THUMB_MASTER_LO_COLOR = 'transparent'
43
44
 
44
- MAX_UNDO_STEPS = 50
45
-
46
45
  BRUSH_SIZE_SLIDER_MAX = 1000
47
46
 
48
47
  UI_SIZES = {