shinestacker 0.3.2__py3-none-any.whl → 0.3.4__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 (71) hide show
  1. shinestacker/__init__.py +2 -1
  2. shinestacker/_version.py +1 -1
  3. shinestacker/algorithms/__init__.py +3 -2
  4. shinestacker/algorithms/align.py +102 -64
  5. shinestacker/algorithms/balance.py +89 -42
  6. shinestacker/algorithms/base_stack_algo.py +42 -0
  7. shinestacker/algorithms/core_utils.py +6 -6
  8. shinestacker/algorithms/denoise.py +4 -1
  9. shinestacker/algorithms/depth_map.py +28 -39
  10. shinestacker/algorithms/exif.py +43 -38
  11. shinestacker/algorithms/multilayer.py +48 -28
  12. shinestacker/algorithms/noise_detection.py +34 -23
  13. shinestacker/algorithms/pyramid.py +42 -42
  14. shinestacker/algorithms/sharpen.py +1 -0
  15. shinestacker/algorithms/stack.py +42 -41
  16. shinestacker/algorithms/stack_framework.py +111 -65
  17. shinestacker/algorithms/utils.py +12 -11
  18. shinestacker/algorithms/vignetting.py +48 -22
  19. shinestacker/algorithms/white_balance.py +1 -0
  20. shinestacker/app/about_dialog.py +6 -2
  21. shinestacker/app/app_config.py +1 -0
  22. shinestacker/app/gui_utils.py +20 -0
  23. shinestacker/app/help_menu.py +1 -0
  24. shinestacker/app/main.py +9 -18
  25. shinestacker/app/open_frames.py +5 -4
  26. shinestacker/app/project.py +5 -16
  27. shinestacker/app/retouch.py +5 -17
  28. shinestacker/core/colors.py +4 -4
  29. shinestacker/core/core_utils.py +1 -1
  30. shinestacker/core/exceptions.py +2 -1
  31. shinestacker/core/framework.py +46 -33
  32. shinestacker/core/logging.py +9 -10
  33. shinestacker/gui/action_config.py +253 -197
  34. shinestacker/gui/actions_window.py +32 -28
  35. shinestacker/gui/colors.py +1 -0
  36. shinestacker/gui/gui_images.py +7 -3
  37. shinestacker/gui/gui_logging.py +3 -2
  38. shinestacker/gui/gui_run.py +53 -38
  39. shinestacker/gui/main_window.py +69 -25
  40. shinestacker/gui/new_project.py +35 -2
  41. shinestacker/gui/project_converter.py +21 -20
  42. shinestacker/gui/project_editor.py +45 -52
  43. shinestacker/gui/project_model.py +15 -23
  44. shinestacker/retouch/{filter_base.py → base_filter.py} +7 -4
  45. shinestacker/retouch/brush.py +1 -0
  46. shinestacker/retouch/brush_gradient.py +17 -3
  47. shinestacker/retouch/brush_preview.py +14 -10
  48. shinestacker/retouch/brush_tool.py +28 -19
  49. shinestacker/retouch/denoise_filter.py +3 -2
  50. shinestacker/retouch/display_manager.py +11 -5
  51. shinestacker/retouch/exif_data.py +1 -0
  52. shinestacker/retouch/file_loader.py +13 -9
  53. shinestacker/retouch/filter_manager.py +1 -0
  54. shinestacker/retouch/image_editor.py +14 -48
  55. shinestacker/retouch/image_editor_ui.py +10 -5
  56. shinestacker/retouch/image_filters.py +4 -2
  57. shinestacker/retouch/image_viewer.py +33 -31
  58. shinestacker/retouch/io_gui_handler.py +25 -13
  59. shinestacker/retouch/io_manager.py +3 -2
  60. shinestacker/retouch/layer_collection.py +79 -23
  61. shinestacker/retouch/shortcuts_help.py +1 -0
  62. shinestacker/retouch/undo_manager.py +7 -0
  63. shinestacker/retouch/unsharp_mask_filter.py +3 -2
  64. shinestacker/retouch/white_balance_filter.py +11 -6
  65. {shinestacker-0.3.2.dist-info → shinestacker-0.3.4.dist-info}/METADATA +10 -4
  66. shinestacker-0.3.4.dist-info/RECORD +86 -0
  67. shinestacker-0.3.2.dist-info/RECORD +0 -85
  68. {shinestacker-0.3.2.dist-info → shinestacker-0.3.4.dist-info}/WHEEL +0 -0
  69. {shinestacker-0.3.2.dist-info → shinestacker-0.3.4.dist-info}/entry_points.txt +0 -0
  70. {shinestacker-0.3.2.dist-info → shinestacker-0.3.4.dist-info}/licenses/LICENSE +0 -0
  71. {shinestacker-0.3.2.dist-info → shinestacker-0.3.4.dist-info}/top_level.txt +0 -0
@@ -1,3 +1,5 @@
1
+ # pylint: disable=C0114, C0115, C0116, W0102, R0902, R0903
2
+ # pylint: disable=R0917, R0913, R1702, R0912, E1111, E1121, W0613
1
3
  import logging
2
4
  import os
3
5
  from .. config.constants import constants
@@ -23,21 +25,34 @@ class StackJob(Job):
23
25
 
24
26
 
25
27
  class FramePaths:
26
- def __init__(self, name, input_path='', output_path='', working_path='', plot_path=constants.DEFAULT_PLOTS_PATH,
27
- scratch_output_dir=True, resample=1, reverse_order=constants.DEFAULT_FILE_REVERSE_ORDER, **kwargs):
28
+ def __init__(self, name, input_path='', output_path='', working_path='',
29
+ plot_path=constants.DEFAULT_PLOTS_PATH,
30
+ scratch_output_dir=True, resample=1,
31
+ reverse_order=constants.DEFAULT_FILE_REVERSE_ORDER, **_kwargs):
28
32
  self.name = name
29
33
  self.working_path = working_path
30
34
  self.plot_path = plot_path
31
35
  self.input_path = input_path
32
36
  self.output_path = output_path
37
+ self.output_dir = None
33
38
  self.resample = resample
34
39
  self.reverse_order = reverse_order
35
40
  self.scratch_output_dir = scratch_output_dir
41
+ self.input_full_path = None
42
+ self.enabled = None
43
+ self.filenames = None
44
+
45
+ def folder_filelist(self):
46
+ assert False, "this method should be overwritten"
47
+
48
+ def print_message(self, msg='', level=logging.INFO, end=None, begin='', tqdm=False):
49
+ assert False, "this method should be overwritten"
36
50
 
37
51
  def set_filelist(self):
38
- self.filenames = self.folder_filelist(self.input_full_path)
52
+ self.filenames = self.folder_filelist()
39
53
  file_list = self.input_full_path.replace(self.working_path, '').lstrip('/')
40
- self.print_message(color_str(": {} files ".format(len(self.filenames)) + "in folder: " + file_list, 'blue'))
54
+ self.print_message(color_str(f": {len(self.filenames)} files in folder: {file_list}",
55
+ 'blue'))
41
56
  self.print_message(color_str("focus stacking", 'blue'))
42
57
 
43
58
  def init(self, job):
@@ -46,7 +61,9 @@ class FramePaths:
46
61
  check_path_exists(self.working_path)
47
62
  if self.output_path == '':
48
63
  self.output_path = self.name
49
- self.output_dir = self.working_path + ('' if self.working_path[-1] == '/' else '/') + self.output_path
64
+ self.output_dir = self.working_path + \
65
+ ('' if self.working_path[-1] == '/' else '/') + \
66
+ self.output_path
50
67
  if not os.path.exists(self.output_dir):
51
68
  os.makedirs(self.output_dir)
52
69
  else:
@@ -58,19 +75,27 @@ class FramePaths:
58
75
  file_path = os.path.join(self.output_dir, filename)
59
76
  if os.path.isfile(file_path):
60
77
  os.remove(file_path)
61
- self.print_message(color_str(f": output directory {self.output_path} content erased", 'yellow'))
78
+ self.print_message(
79
+ color_str(f": output directory {self.output_path} content erased",
80
+ 'yellow'))
62
81
  else:
63
- self.print_message(color_str(f": module disabled, output directory {self.output_path} not scratched", 'yellow'))
82
+ self.print_message(
83
+ color_str(f": module disabled, output directory {self.output_path}"
84
+ " not scratched", 'yellow'))
64
85
  else:
65
- self.print_message(color_str(f": output directory {self.output_path} not empty, "
66
- "files may be overwritten or merged with existing ones.", 'yellow'), level=logging.WARNING) # noqa
86
+ self.print_message(
87
+ color_str(
88
+ f": output directory {self.output_path} not empty, "
89
+ "files may be overwritten or merged with existing ones.", 'yellow'
90
+ ), level=logging.WARNING)
67
91
  if self.plot_path == '':
68
- self.plot_path = self.working_path + ('' if self.working_path[-1] == '/' else '/') + self.plot_path
92
+ self.plot_path = self.working_path + \
93
+ ('' if self.working_path[-1] == '/' else '/') + self.plot_path
69
94
  if not os.path.exists(self.plot_path):
70
95
  os.makedirs(self.plot_path)
71
96
  if self.input_path == '':
72
97
  if len(job.paths) == 0:
73
- raise Exception(f"Job {job.name} does not have any configured path")
98
+ raise RuntimeError(f"Job {job.name} does not have any configured path")
74
99
  self.input_path = job.paths[-1]
75
100
  job.paths.append(self.output_path)
76
101
 
@@ -81,15 +106,16 @@ class FrameDirectory(FramePaths):
81
106
 
82
107
  def folder_list_str(self):
83
108
  if isinstance(self.input_full_path, list):
84
- file_list = ", ".join([i for i in self.input_full_path.replace(self.working_path, '').lstrip('/')])
85
- return "folder{}: ".format('s' if len(self.input_full_path) > 1 else '') + file_list
86
- else:
87
- return "folder: " + self.input_full_path.replace(self.working_path, '').lstrip('/')
109
+ file_list = ", ".join(
110
+ list(self.input_full_path.replace(self.working_path, '').lstrip('/')))
111
+ return "folder" + ('s' if len(self.input_full_path) > 1 else '') + f": {file_list}"
112
+ return "folder: " + self.input_full_path.replace(self.working_path, '').lstrip('/')
88
113
 
89
- def folder_filelist(self, path):
114
+ def folder_filelist(self):
90
115
  src_contents = os.walk(self.input_full_path)
91
- dirpath, _, filenames = next(src_contents)
92
- filelist = [name for name in filenames if os.path.splitext(name)[-1][1:].lower() in constants.EXTENSIONS]
116
+ _dirpath, _, filenames = next(src_contents)
117
+ filelist = [name for name in filenames
118
+ if os.path.splitext(name)[-1][1:].lower() in constants.EXTENSIONS]
93
119
  filelist.sort()
94
120
  if self.reverse_order:
95
121
  filelist.reverse()
@@ -97,24 +123,29 @@ class FrameDirectory(FramePaths):
97
123
  filelist = filelist[0::self.resample]
98
124
  return filelist
99
125
 
100
- def init(self, job):
126
+ def init(self, job, _working_path=''):
101
127
  FramePaths.init(self, job)
102
- self.input_full_path = self.working_path + ('' if self.working_path[-1] == '/' else '/') + self.input_path
128
+ self.input_full_path = self.working_path + \
129
+ ('' if self.working_path[-1] == '/' else '/') + self.input_path
103
130
  check_path_exists(self.input_full_path)
104
131
  job.paths.append(self.output_path)
105
132
 
106
133
 
107
- class FrameMultiDirectory:
108
- def __init__(self, name, input_path='', output_path='', working_path='', plot_path=constants.DEFAULT_PLOTS_PATH,
109
- scratch_output_dir=True, resample=1, reverse_order=constants.DEFAULT_FILE_REVERSE_ORDER, **kwargs):
110
- FramePaths.__init__(self, name, input_path, output_path, working_path, plot_path, scratch_output_dir, resample, reverse_order, **kwargs)
134
+ class FrameMultiDirectory(FramePaths):
135
+ def __init__(self, name, input_path='', output_path='', working_path='',
136
+ plot_path=constants.DEFAULT_PLOTS_PATH,
137
+ scratch_output_dir=True, resample=1,
138
+ reverse_order=constants.DEFAULT_FILE_REVERSE_ORDER, **_kwargs):
139
+ FramePaths.__init__(self, name, input_path, output_path, working_path, plot_path,
140
+ scratch_output_dir, resample, reverse_order)
141
+ self.input_full_path = None
111
142
 
112
143
  def folder_list_str(self):
113
144
  if isinstance(self.input_full_path, list):
114
- file_list = ", ".join([d.replace(self.working_path, '').lstrip('/') for d in self.input_full_path])
115
- return "folder{}: ".format('s' if len(self.input_full_path) > 1 else '') + file_list
116
- else:
117
- return "folder: " + self.input_full_path.replace(self.working_path, '').lstrip('/')
145
+ file_list = ", ".join([d.replace(self.working_path, '').lstrip('/')
146
+ for d in self.input_full_path])
147
+ return "folder" + ('s' if len(self.input_full_path) > 1 else '') + f": {file_list}"
148
+ return "folder: " + self.input_full_path.replace(self.working_path, '').lstrip('/')
118
149
 
119
150
  def folder_filelist(self):
120
151
  if isinstance(self.input_full_path, str):
@@ -124,39 +155,49 @@ class FrameMultiDirectory:
124
155
  dirs = self.input_full_path
125
156
  paths = self.input_path
126
157
  else:
127
- raise Exception("input_full_path option must contain a path or an array of paths")
158
+ raise RuntimeError("input_full_path option must contain a path or an array of paths")
128
159
  files = []
129
160
  for d, p in zip(dirs, paths):
130
161
  filelist = []
131
- for dirpath, _, filenames in os.walk(d):
132
- filelist = [p + "/" + name for name in filenames if os.path.splitext(name)[-1][1:].lower() in constants.EXTENSIONS]
162
+ for _dirpath, _, filenames in os.walk(d):
163
+ filelist = [p + "/" + name
164
+ for name in filenames
165
+ if os.path.splitext(name)[-1][1:].lower() in constants.EXTENSIONS]
133
166
  if self.reverse_order:
134
167
  filelist.reverse()
135
168
  if self.resample > 1:
136
169
  filelist = filelist[0::self.resample]
137
170
  files += filelist
138
171
  if len(files) == 0:
139
- self.print_message(color_str(f"input folder {p} does not contain any image", "red"), level=logging.WARNING)
172
+ self.print_message(color_str(f"input folder {p} does not contain any image", "red"),
173
+ level=logging.WARNING)
140
174
  return files
141
175
 
142
176
  def init(self, job):
143
177
  FramePaths.init(self, job)
144
178
  if isinstance(self.input_path, str):
145
- self.input_full_path = self.working_path + ('' if self.working_path[-1] == '/' else '/') + self.input_path
179
+ self.input_full_path = self.working_path + \
180
+ ('' if self.working_path[-1] == '/' else '/') + \
181
+ self.input_path
146
182
  check_path_exists(self.input_full_path)
147
183
  elif hasattr(self.input_path, "__len__"):
148
184
  self.input_full_path = []
149
185
  for path in self.input_path:
150
- self.input_full_path.append(self.working_path + ('' if self.working_path[-1] == '/' else '/') + path)
186
+ self.input_full_path.append(self.working_path +
187
+ ('' if self.working_path[-1] == '/' else '/') +
188
+ path)
151
189
  job.paths.append(self.output_path)
152
190
 
153
191
 
154
- class FramesRefActions(FrameDirectory, ActionList):
192
+ class FramesRefActions(ActionList, FrameDirectory):
155
193
  def __init__(self, name, enabled=True, ref_idx=-1, step_process=False, **kwargs):
156
194
  FrameDirectory.__init__(self, name, **kwargs)
157
195
  ActionList.__init__(self, name, enabled)
158
196
  self.ref_idx = ref_idx
159
197
  self.step_process = step_process
198
+ self._idx = None
199
+ self._ref_idx = None
200
+ self._idx_step = None
160
201
 
161
202
  def begin(self):
162
203
  ActionList.begin(self)
@@ -168,28 +209,28 @@ class FramesRefActions(FrameDirectory, ActionList):
168
209
  def end(self):
169
210
  ActionList.end(self)
170
211
 
171
- def run_frame(self, idx, ref_idx):
172
- assert False, 'abstract method'
212
+ def run_frame(self, _idx, _ref_idx):
213
+ pass
173
214
 
174
215
  def run_step(self):
175
216
  if self.count == 1:
176
- self.__idx = self.ref_idx if self.step_process else 0
177
- self.__ref_idx = self.ref_idx
178
- self.__idx_step = +1
217
+ self._idx = self.ref_idx if self.step_process else 0
218
+ self._ref_idx = self.ref_idx
219
+ self._idx_step = +1
179
220
  ll = len(self.filenames)
180
221
  self.print_message_r(
181
- color_str("step {}/{}: process file: {}, reference: {}".format(self.count, ll, self.filenames[self.__idx],
182
- self.filenames[self.__ref_idx]), "blue"))
183
- self.run_frame(self.__idx, self.__ref_idx)
184
- if self.__idx < ll:
222
+ color_str(f"step {self.count}/{ll}: process file: {self.filenames[self._idx]}, "
223
+ f"reference: {self.filenames[self._ref_idx]}", "blue"))
224
+ self.run_frame(self._idx, self._ref_idx)
225
+ if self._idx < ll:
185
226
  if self.step_process:
186
- self.__ref_idx = self.__idx
187
- self.__idx += self.__idx_step
188
- if self.__idx == ll:
189
- self.__idx = self.ref_idx - 1
227
+ self._ref_idx = self._idx
228
+ self._idx += self._idx_step
229
+ if self._idx == ll:
230
+ self._idx = self.ref_idx - 1
190
231
  if self.step_process:
191
- self.__ref_idx = self.ref_idx
192
- self.__idx_step = -1
232
+ self._ref_idx = self.ref_idx
233
+ self._idx_step = -1
193
234
 
194
235
 
195
236
  class SubAction:
@@ -200,38 +241,42 @@ class SubAction:
200
241
  class CombinedActions(FramesRefActions):
201
242
  def __init__(self, name, actions=[], enabled=True, **kwargs):
202
243
  FramesRefActions.__init__(self, name, enabled, **kwargs)
203
- self.__actions = actions
244
+ self._actions = actions
245
+ self.dtype = None
246
+ self.shape = None
204
247
 
205
248
  def begin(self):
206
249
  FramesRefActions.begin(self)
207
- for a in self.__actions:
250
+ for a in self._actions:
208
251
  if a.enabled:
209
252
  a.begin(self)
210
253
 
211
254
  def img_ref(self, idx):
212
255
  filename = self.filenames[idx]
213
- img = read_img((self.output_dir if self.step_process else self.input_full_path) + "/" + filename)
256
+ img = read_img((self.output_dir
257
+ if self.step_process else self.input_full_path) + f"/{filename}")
214
258
  self.dtype = img.dtype
215
259
  self.shape = img.shape
216
260
  if img is None:
217
- raise Exception("Invalid file: " + self.input_full_path + "/" + filename)
261
+ raise RuntimeError(f"Invalid file: {self.input_full_path}/{filename}")
218
262
  return img
219
263
 
220
264
  def run_frame(self, idx, ref_idx):
221
265
  filename = self.filenames[idx]
222
266
  self.sub_message_r(': read input image')
223
- img = read_img(self.input_full_path + "/" + filename)
224
- if hasattr(self, 'dtype') and img.dtype != self.dtype:
225
- raise BitDepthError(img.dtype, self.dtype)
226
- if hasattr(self, 'shape') and img.shape != self.shape:
227
- raise ShapeError(img.shape, self.shape)
267
+ img = read_img(f"{self.input_full_path}/{filename}")
268
+ if self.dtype is not None and img.dtype != self.dtype:
269
+ raise BitDepthError(self.dtype, img.dtype, )
270
+ if self.shape is not None and img.shape != self.shape:
271
+ raise ShapeError(self.shape, img.shape)
228
272
  if img is None:
229
- raise Exception("Invalid file: " + self.input_full_path + "/" + filename)
230
- if len(self.__actions) == 0:
273
+ raise RuntimeError(f"Invalid file: {self.input_full_path}/{filename}")
274
+ if len(self._actions) == 0:
231
275
  self.sub_message(color_str(": no actions specified.", "red"), level=logging.WARNING)
232
- for a in self.__actions:
276
+ for a in self._actions:
233
277
  if not a.enabled:
234
- self.get_logger().warning(color_str(self.base_message + ": sub-action disabled", 'red'))
278
+ self.get_logger().warning(color_str(f"{self.base_message}: sub-action disabled",
279
+ 'red'))
235
280
  else:
236
281
  if self.callback('check_running', self.id, self.name) is False:
237
282
  raise RunStopException(self.name)
@@ -240,9 +285,10 @@ class CombinedActions(FramesRefActions):
240
285
  if img is not None:
241
286
  write_img(self.output_dir + "/" + filename, img)
242
287
  else:
243
- self.print_message("No output file resulted from processing input file: " + self.input_full_path + "/" + filename, level=logging.WARNING)
288
+ self.print_message("No output file resulted from processing input file: "
289
+ f"{self.input_full_path}/{filename}", level=logging.WARNING)
244
290
 
245
291
  def end(self):
246
- for a in self.__actions:
292
+ for a in self._actions:
247
293
  if a.enabled:
248
294
  a.end()
@@ -1,7 +1,8 @@
1
- import cv2
1
+ # pylint: disable=C0114, C0116, E1101
2
2
  import os
3
- import numpy as np
4
3
  import logging
4
+ import numpy as np
5
+ import cv2
5
6
  import matplotlib.pyplot as plt
6
7
  from .. config.config import config
7
8
  from .. core.exceptions import ShapeError, BitDepthError
@@ -9,20 +10,21 @@ from .. core.exceptions import ShapeError, BitDepthError
9
10
 
10
11
  def read_img(file_path):
11
12
  if not os.path.isfile(file_path):
12
- raise Exception("File does not exist: " + file_path)
13
+ raise RuntimeError("File does not exist: " + file_path)
13
14
  ext = file_path.split(".")[-1]
14
- if ext == 'jpeg' or ext == 'jpg':
15
+ img = None
16
+ if ext in ['jpeg', 'jpg']:
15
17
  img = cv2.imread(file_path)
16
- elif ext == 'tiff' or ext == 'tif' or ext == 'png':
18
+ elif ext in ['tiff', 'tif', 'png']:
17
19
  img = cv2.imread(file_path, cv2.IMREAD_UNCHANGED)
18
20
  return img
19
21
 
20
22
 
21
23
  def write_img(file_path, img):
22
24
  ext = file_path.split(".")[-1]
23
- if ext == 'jpeg' or ext == 'jpg':
25
+ if ext in ['jpeg', 'jpg']:
24
26
  cv2.imwrite(file_path, img, [int(cv2.IMWRITE_JPEG_QUALITY), 100])
25
- elif ext == 'tiff' or ext == 'tif':
27
+ elif ext in ['tiff', 'tif']:
26
28
  cv2.imwrite(file_path, img, [int(cv2.IMWRITE_TIFF_COMPRESSION), 1])
27
29
  elif ext == 'png':
28
30
  cv2.imwrite(file_path, img)
@@ -36,10 +38,9 @@ def img_bw_8bit(img):
36
38
  img = img_8bit(img)
37
39
  if len(img.shape) == 3:
38
40
  return cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
39
- elif len(img.shape) == 2:
41
+ if len(img.shape) == 2:
40
42
  return img
41
- else:
42
- raise ValueError(f"Unsupported image format: {img.shape}")
43
+ raise ValueError(f"Unsupported image format: {img.shape}")
43
44
 
44
45
 
45
46
  def img_bw(img):
@@ -63,7 +64,7 @@ def validate_image(img, expected_shape=None, expected_dtype=None):
63
64
 
64
65
 
65
66
  def save_plot(filename):
66
- logging.getLogger(__name__).debug("save plot file: " + filename)
67
+ logging.getLogger(__name__).debug(msg=f"save plot file: {filename}")
67
68
  dir_path = os.path.dirname(filename)
68
69
  if not dir_path:
69
70
  dir_path = '.'
@@ -1,8 +1,9 @@
1
- import cv2
1
+ # pylint: disable=C0114, C0115, C0116, R0902, E1101, W0718, W0640
2
2
  import logging
3
3
  import numpy as np
4
4
  import matplotlib.pyplot as plt
5
5
  from scipy.optimize import curve_fit, fsolve
6
+ import cv2
6
7
  from .. core.colors import color_str
7
8
  from .. config.constants import constants
8
9
  from .utils import img_8bit, save_plot
@@ -21,6 +22,12 @@ class Vignetting(SubAction):
21
22
  self.plot_summary = kwargs.get('plot_summary', False)
22
23
  self.max_correction = kwargs.get('max_correction', constants.DEFAULT_MAX_CORRECTION)
23
24
  self.percentiles = np.sort(percentiles)
25
+ self.w_2 = None
26
+ self.h_2 = None
27
+ self.v0 = None
28
+ self.r_max = None
29
+ self.process = None
30
+ self.corrections = None
24
31
 
25
32
  def radial_mean_intensity(self, image):
26
33
  if len(image.shape) > 2:
@@ -40,8 +47,12 @@ class Vignetting(SubAction):
40
47
  mean_intensities[i] = np.nan
41
48
  return (radii[1:] + radii[:-1]) / 2, mean_intensities
42
49
 
50
+ @staticmethod
43
51
  def sigmoid(r, i0, k, r0):
44
- return i0 / (1.0 + np.exp(np.minimum(CLIP_EXP, np.exp(np.clip(k * (r - r0), -CLIP_EXP, CLIP_EXP)))))
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)))))
45
56
 
46
57
  def fit_sigmoid(self, radii, intensities):
47
58
  valid_mask = ~np.isnan(intensities)
@@ -50,7 +61,9 @@ class Vignetting(SubAction):
50
61
  res = curve_fit(Vignetting.sigmoid, r_valid, i_valid,
51
62
  p0=[np.max(i_valid), 0.01, np.median(r_valid)])[0]
52
63
  except Exception:
53
- self.process.sub_message(color_str(": could not find vignetting model", "red"), level=logging.WARNING)
64
+ self.process.sub_message(
65
+ color_str(": could not find vignetting model", "red"),
66
+ level=logging.WARNING)
54
67
  res = None
55
68
  return res
56
69
 
@@ -66,9 +79,10 @@ class Vignetting(SubAction):
66
79
  vignette[np.min(image, axis=2) < self.black_threshold, :] = 1
67
80
  else:
68
81
  vignette[image < self.black_threshold] = 1
69
- return np.clip(image / vignette, 0, 255 if image.dtype == np.uint8 else 65535).astype(image.dtype)
82
+ return np.clip(image / vignette, 0, 255
83
+ if image.dtype == np.uint8 else 65535).astype(image.dtype)
70
84
 
71
- def run_frame(self, idx, ref_idx, img_0):
85
+ def run_frame(self, idx, _ref_idx, img_0):
72
86
  self.process.sub_message_r(color_str(": compute vignetting", "light_blue"))
73
87
  img = cv2.cvtColor(img_8bit(img_0), cv2.COLOR_BGR2GRAY)
74
88
  radii, intensities = self.radial_mean_intensity(img)
@@ -77,8 +91,9 @@ class Vignetting(SubAction):
77
91
  return img_0
78
92
  self.v0 = Vignetting.sigmoid(0, *pars)
79
93
  i0_fit, k_fit, r0_fit = pars
80
- self.process.sub_message(f": fit parameters: i0={i0_fit:.4f}, k={k_fit:.4f}, r0={r0_fit:.4f}",
81
- level=logging.DEBUG)
94
+ self.process.sub_message(
95
+ f": fit parameters: i0={i0_fit:.4f}, k={k_fit:.4f}, r0={r0_fit:.4f}",
96
+ level=logging.DEBUG)
82
97
  if self.plot_correction:
83
98
  plt.figure(figsize=(10, 5))
84
99
  plt.plot(radii, intensities, label="image mean intensity")
@@ -88,22 +103,27 @@ class Vignetting(SubAction):
88
103
  plt.legend()
89
104
  plt.xlim(radii[0], radii[-1])
90
105
  plt.ylim(0)
91
- idx_str = "{:04d}".format(idx)
92
- plot_path = f"{self.process.working_path}/{self.process.plot_path}/{self.process.name}-radial-intensity-{idx_str}.pdf"
106
+ idx_str = f"{idx:04d}"
107
+ plot_path = f"{self.process.working_path}/" \
108
+ f"{self.process.plot_path}/{self.process.name}-" \
109
+ f"radial-intensity-{idx_str}.pdf"
93
110
  save_plot(plot_path)
94
111
  plt.close('all')
95
- self.process.callback('save_plot', self.process.id, f"{self.process.name}: intensity\nframe {idx_str}", plot_path)
112
+ self.process.callback(
113
+ 'save_plot', self.process.id,
114
+ f"{self.process.name}: intensity\nframe {idx_str}", plot_path)
96
115
  for i, p in enumerate(self.percentiles):
97
- self.corrections[i][idx] = fsolve(lambda x: Vignetting.sigmoid(x, *pars) / self.v0 - p, r0_fit)[0]
116
+ self.corrections[i][idx] = fsolve(lambda x: Vignetting.sigmoid(x, *pars) /
117
+ self.v0 - p, r0_fit)[0]
98
118
  if self.apply_correction:
99
119
  self.process.sub_message_r(color_str(": correct vignetting", "light_blue"))
100
120
  return self.correct_vignetting(img_0, pars)
101
- else:
102
- return img_0
121
+ return img_0
103
122
 
104
123
  def begin(self, process):
105
124
  self.process = process
106
- self.corrections = [np.full(self.process.counts, None, dtype=float) for p in self.percentiles]
125
+ self.corrections = [np.full(self.process.counts, None, dtype=float)
126
+ for p in self.percentiles]
107
127
 
108
128
  def end(self):
109
129
  if self.plot_summary:
@@ -113,7 +133,7 @@ class Vignetting(SubAction):
113
133
  linestyle = 'solid'
114
134
  if p == 0.5:
115
135
  linestyle = '-.'
116
- elif i == 0 or i == len(self.percentiles) - 1:
136
+ elif i in (0, len(self.percentiles) - 1):
117
137
  linestyle = 'dotted'
118
138
  plt.plot(xs, self.corrections[i], label=f"{p:.0%} correction",
119
139
  linestyle=linestyle, color="blue")
@@ -121,17 +141,23 @@ class Vignetting(SubAction):
121
141
  iis = np.where(self.percentiles == 0.5)
122
142
  if len(iis) > 0:
123
143
  i = iis[0][0]
124
- if i >= 1 and i < len(self.percentiles) - 1:
125
- plt.fill_between(xs, self.corrections[i - 1], self.corrections[i + 1], color="#0000ff20")
126
- plt.plot(xs[[0, -1]], [self.r_max] * 2, linestyle="--", label="max. radius", color="darkred")
127
- plt.plot(xs[[0, -1]], [self.w_2] * 2, linestyle="--", label="half width", color="limegreen")
128
- plt.plot(xs[[0, -1]], [self.h_2] * 2, linestyle="--", label="half height", color="darkgreen")
144
+ if 1 <= i < len(self.percentiles) - 1:
145
+ plt.fill_between(xs, self.corrections[i - 1], self.corrections[i + 1],
146
+ color="#0000ff20")
147
+ plt.plot(xs[[0, -1]], [self.r_max] * 2,
148
+ linestyle="--", label="max. radius", color="darkred")
149
+ plt.plot(xs[[0, -1]], [self.w_2] * 2,
150
+ linestyle="--", label="half width", color="limegreen")
151
+ plt.plot(xs[[0, -1]], [self.h_2] * 2,
152
+ linestyle="--", label="half height", color="darkgreen")
129
153
  plt.xlabel('frame')
130
154
  plt.ylabel('distance from center (pixels)')
131
155
  plt.legend(ncols=2)
132
156
  plt.xlim(xs[0], xs[-1])
133
157
  plt.ylim(0, self.r_max * 1.05)
134
- plot_path = self.process.working_path + "/" + self.process.plot_path + "/" + self.process.name + "-r0.pdf"
158
+ plot_path = f"{self.process.working_path}/{self.process.plot_path}/" \
159
+ f"{self.process.name}-r0.pdf"
135
160
  save_plot(plot_path)
136
161
  plt.close('all')
137
- self.process.callback('save_plot', self.process.id, f"{self.process.name}: vignetting", plot_path)
162
+ self.process.callback('save_plot', self.process.id,
163
+ f"{self.process.name}: vignetting", plot_path)
@@ -1,3 +1,4 @@
1
+ # pylint: disable=C0114, C0116
1
2
  import numpy as np
2
3
 
3
4
 
@@ -1,3 +1,4 @@
1
+ # pylint: disable=C0114, C0116, E0611
1
2
  from PySide6.QtWidgets import QMessageBox
2
3
  from PySide6.QtCore import Qt
3
4
  from .. import __version__
@@ -5,7 +6,7 @@ from .. config.constants import constants
5
6
 
6
7
 
7
8
  def show_about_dialog():
8
- version_clean = __version__.split("+")[0]
9
+ version_clean = __version__.split("+", maxsplit=1)[0]
9
10
  about_text = f"""
10
11
  <h3>{constants.APP_TITLE}</h3>
11
12
  <h4>version: v{version_clean}</h4>
@@ -13,7 +14,10 @@ def show_about_dialog():
13
14
  into a single focused image.</p>
14
15
  <p>Author: Luca Lista<br/>
15
16
  Email: <a href="mailto:luka.lista@gmail.com">luka.lista@gmail.com</a></p>
16
- <p><a href="https://github.com/lucalista/shinestacker">GitHub homepage</a></p>
17
+ <ul>
18
+ <li><a href="https://shinestacker.wordpress.com/">Website on Wordpress</a></li>
19
+ <li><a href="https://github.com/lucalista/shinestacker">GitHub project repository</a></li>
20
+ </ul>
17
21
  """
18
22
  msg = QMessageBox()
19
23
  msg.setWindowTitle(f"About {constants.APP_STRING}")
@@ -1,3 +1,4 @@
1
+ # pylint: disable=C0114, C0115, C0116, C0103, W0201
1
2
  class _AppConfig:
2
3
  _initialized = False
3
4
  _instance = None
@@ -1,6 +1,11 @@
1
+ # pylint: disable=C0114, C0116, E0611
1
2
  import os
2
3
  import sys
3
4
  from PySide6.QtCore import QCoreApplication, QProcess
5
+ from PySide6.QtGui import QAction
6
+ from shinestacker.config.constants import constants
7
+ from shinestacker.config.config import config
8
+ from shinestacker.app.about_dialog import show_about_dialog
4
9
 
5
10
 
6
11
  def disable_macos_special_menu_items():
@@ -33,3 +38,18 @@ def disable_macos_special_menu_items():
33
38
  if user:
34
39
  QProcess.startDetached("pkill", ["-u", user, "-f", "cfprefsd"])
35
40
  QProcess.startDetached("pkill", ["-u", user, "-f", "SystemUIServer"])
41
+
42
+
43
+ def fill_app_menu(app, app_menu):
44
+ about_action = QAction(f"About {constants.APP_STRING}", app)
45
+ about_action.triggered.connect(show_about_dialog)
46
+ app_menu.addAction(about_action)
47
+ app_menu.addSeparator()
48
+ if config.DONT_USE_NATIVE_MENU:
49
+ quit_txt, quit_short = "&Quit", "Ctrl+Q"
50
+ else:
51
+ quit_txt, quit_short = "Shut dw&wn", "Ctrl+Q"
52
+ exit_action = QAction(quit_txt, app)
53
+ exit_action.setShortcut(quit_short)
54
+ exit_action.triggered.connect(app.quit)
55
+ app_menu.addAction(exit_action)
@@ -1,3 +1,4 @@
1
+ # pylint: disable=C0114, C0116, E0611
1
2
  import webbrowser
2
3
  from PySide6.QtWidgets import QMenu
3
4
  from PySide6.QtGui import QAction