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
@@ -0,0 +1,567 @@
1
+ # pylint: disable=C0114, C0115, C0116, E0611, R0913, R0917, R0915, R0912
2
+ # pylint: disable=E0606, W0718, R1702, W0102, W0221
3
+ import traceback
4
+ from typing import Dict, Any
5
+ from PySide6.QtWidgets import (QWidget, QPushButton, QHBoxLayout, QLabel,
6
+ QMessageBox, QStackedWidget, QFormLayout)
7
+ from PySide6.QtCore import Qt, QTimer
8
+ from .. config.constants import constants
9
+ from .. algorithms.align import validate_align_config
10
+ from .project_model import ActionConfig
11
+ from .base_form_dialog import BaseFormDialog
12
+ from . action_config import (
13
+ FieldBuilder, ActionConfigurator,
14
+ FIELD_TEXT, FIELD_ABS_PATH, FIELD_REL_PATH, FIELD_FLOAT,
15
+ FIELD_INT, FIELD_INT_TUPLE, FIELD_BOOL, FIELD_COMBO
16
+ )
17
+
18
+
19
+ class ActionConfigDialog(BaseFormDialog):
20
+ def __init__(self, action: ActionConfig, current_wd, parent=None):
21
+ super().__init__(f"Configure {action.type_name}", parent)
22
+ self.current_wd = current_wd
23
+ self.action = action
24
+ self.configurator = self.get_configurator(action.type_name)
25
+ self.configurator.create_form(self.layout, action)
26
+ button_box = QHBoxLayout()
27
+ ok_button = QPushButton("OK")
28
+ ok_button.setFocus()
29
+ cancel_button = QPushButton("Cancel")
30
+ reset_button = QPushButton("Reset")
31
+ button_box.addWidget(ok_button)
32
+ button_box.addWidget(cancel_button)
33
+ button_box.addWidget(reset_button)
34
+ reset_button.clicked.connect(self.reset_to_defaults)
35
+ self.add_row_to_layout(button_box)
36
+ ok_button.clicked.connect(self.accept)
37
+ cancel_button.clicked.connect(self.reject)
38
+
39
+ def get_configurator(self, action_type: str) -> ActionConfigurator:
40
+ configurators = {
41
+ constants.ACTION_JOB: JobConfigurator,
42
+ constants.ACTION_COMBO: CombinedActionsConfigurator,
43
+ constants.ACTION_NOISEDETECTION: NoiseDetectionConfigurator,
44
+ constants.ACTION_FOCUSSTACK: FocusStackConfigurator,
45
+ constants.ACTION_FOCUSSTACKBUNCH: FocusStackBunchConfigurator,
46
+ constants.ACTION_MULTILAYER: MultiLayerConfigurator,
47
+ constants.ACTION_MASKNOISE: MaskNoiseConfigurator,
48
+ constants.ACTION_VIGNETTING: VignettingConfigurator,
49
+ constants.ACTION_ALIGNFRAMES: AlignFramesConfigurator,
50
+ constants.ACTION_BALANCEFRAMES: BalanceFramesConfigurator,
51
+ }
52
+ return configurators.get(
53
+ action_type, DefaultActionConfigurator)(self.expert(), self.current_wd)
54
+
55
+ def accept(self):
56
+ self.parent().project_editor.add_undo(self.parent().project().clone())
57
+ if self.configurator.update_params(self.action.params):
58
+ self.parent().mark_as_modified()
59
+ super().accept()
60
+ else:
61
+ self.parent().project_editor.pop_undo()
62
+
63
+ def reset_to_defaults(self):
64
+ builder = self.configurator.get_builder()
65
+ if builder:
66
+ builder.reset_to_defaults()
67
+
68
+ def expert(self):
69
+ return self.parent().expert_options
70
+
71
+
72
+ class NoNameActionConfigurator(ActionConfigurator):
73
+ def __init__(self, expert, current_wd):
74
+ super().__init__(expert, current_wd)
75
+ self.builder = None
76
+
77
+ def get_builder(self):
78
+ return self.builder
79
+
80
+ def update_params(self, params: Dict[str, Any]) -> bool:
81
+ return self.builder.update_params(params)
82
+
83
+ def add_bold_label(self, label):
84
+ label = QLabel(label)
85
+ label.setStyleSheet("font-weight: bold")
86
+ self.builder.layout.addRow(label)
87
+
88
+
89
+ class DefaultActionConfigurator(NoNameActionConfigurator):
90
+ def create_form(self, layout, action, tag='Action'):
91
+ self.builder = FieldBuilder(layout, action, self.current_wd)
92
+ self.builder.add_field('name', FIELD_TEXT, f'{tag} name', required=True)
93
+
94
+
95
+ class JobConfigurator(DefaultActionConfigurator):
96
+ def create_form(self, layout, action):
97
+ super().create_form(layout, action, "Job")
98
+ self.builder.add_field('working_path', FIELD_ABS_PATH, 'Working path', required=True)
99
+ self.builder.add_field('input_path', FIELD_REL_PATH, 'Input path', required=False,
100
+ must_exist=True, placeholder='relative to working path')
101
+
102
+
103
+ class NoiseDetectionConfigurator(DefaultActionConfigurator):
104
+ def create_form(self, layout, action):
105
+ super().create_form(layout, action)
106
+ self.builder.add_field('working_path', FIELD_ABS_PATH, 'Working path', required=True,
107
+ placeholder='inherit from job')
108
+ self.builder.add_field('input_path', FIELD_REL_PATH,
109
+ f'Input path (separate by {constants.PATH_SEPARATOR})',
110
+ required=False, multiple_entries=True,
111
+ placeholder='relative to working path')
112
+ self.builder.add_field('max_frames', FIELD_INT, 'Max. num. of frames', required=False,
113
+ default=-1, min_val=-1, max_val=1000)
114
+ self.builder.add_field('channel_thresholds', FIELD_INT_TUPLE, 'Noise threshold',
115
+ required=False, size=3,
116
+ default=constants.DEFAULT_CHANNEL_THRESHOLDS,
117
+ labels=constants.RGB_LABELS, min_val=[1] * 3,
118
+ max_val=[1000] * 3)
119
+ if self.expert:
120
+ self.builder.add_field('blur_size', FIELD_INT, 'Blur size (px)', required=False,
121
+ default=constants.DEFAULT_BLUR_SIZE, min_val=1, max_val=50)
122
+ self.builder.add_field('file_name', FIELD_TEXT, 'File name', required=False,
123
+ default=constants.DEFAULT_NOISE_MAP_FILENAME,
124
+ placeholder=constants.DEFAULT_NOISE_MAP_FILENAME)
125
+ self.add_bold_label("Miscellanea:")
126
+ self.builder.add_field('plot_histograms', FIELD_BOOL, 'Plot histograms', required=False,
127
+ default=False)
128
+ self.builder.add_field('plot_path', FIELD_REL_PATH, 'Plots path', required=False,
129
+ default=constants.DEFAULT_PLOTS_PATH,
130
+ placeholder='relative to working path')
131
+ self.builder.add_field('plot_range', FIELD_INT_TUPLE, 'Plot range', required=False,
132
+ size=2, default=constants.DEFAULT_NOISE_PLOT_RANGE,
133
+ labels=['min', 'max'], min_val=[0] * 2, max_val=[1000] * 2)
134
+
135
+
136
+ class FocusStackBaseConfigurator(DefaultActionConfigurator):
137
+ ENERGY_OPTIONS = ['Laplacian', 'Sobel']
138
+ MAP_TYPE_OPTIONS = ['Average', 'Maximum']
139
+ FLOAT_OPTIONS = ['float 32 bits', 'float 64 bits']
140
+
141
+ def create_form(self, layout, action):
142
+ super().create_form(layout, action)
143
+ if self.expert:
144
+ self.builder.add_field('working_path', FIELD_ABS_PATH, 'Working path', required=False)
145
+ self.builder.add_field('input_path', FIELD_REL_PATH, 'Input path', required=False,
146
+ placeholder='relative to working path')
147
+ self.builder.add_field('output_path', FIELD_REL_PATH, 'Output path', required=False,
148
+ placeholder='relative to working path')
149
+ self.builder.add_field('scratch_output_dir', FIELD_BOOL, 'Scratch output dir.',
150
+ required=False, default=True)
151
+
152
+ def common_fields(self, layout):
153
+ self.builder.add_field('denoise_amount', FIELD_FLOAT, 'Denoise', required=False,
154
+ default=0, min_val=0, max_val=10)
155
+ self.add_bold_label("Stacking algorithm:")
156
+ combo = self.builder.add_field('stacker', FIELD_COMBO, 'Stacking algorithm', required=True,
157
+ options=constants.STACK_ALGO_OPTIONS,
158
+ default=constants.STACK_ALGO_DEFAULT)
159
+ q_pyramid, q_depthmap = QWidget(), QWidget()
160
+ for q in [q_pyramid, q_depthmap]:
161
+ layout = QFormLayout()
162
+ layout.setFieldGrowthPolicy(QFormLayout.AllNonFixedFieldsGrow)
163
+ layout.setRowWrapPolicy(QFormLayout.DontWrapRows)
164
+ layout.setFormAlignment(Qt.AlignLeft | Qt.AlignTop)
165
+ layout.setLabelAlignment(Qt.AlignLeft)
166
+ q.setLayout(layout)
167
+ stacked = QStackedWidget()
168
+ stacked.addWidget(q_pyramid)
169
+ stacked.addWidget(q_depthmap)
170
+
171
+ def change():
172
+ text = combo.currentText()
173
+ if text == 'Pyramid':
174
+ stacked.setCurrentWidget(q_pyramid)
175
+ elif text == 'Depth map':
176
+ stacked.setCurrentWidget(q_depthmap)
177
+ change()
178
+ if self.expert:
179
+ self.builder.add_field('pyramid_min_size', FIELD_INT, 'Minimum size (px)',
180
+ required=False, add_to_layout=q_pyramid.layout(),
181
+ default=constants.DEFAULT_PY_MIN_SIZE, min_val=2, max_val=256)
182
+ self.builder.add_field('pyramid_kernel_size', FIELD_INT, 'Kernel size (px)',
183
+ required=False, add_to_layout=q_pyramid.layout(),
184
+ default=constants.DEFAULT_PY_KERNEL_SIZE, min_val=3, max_val=21)
185
+ self.builder.add_field('pyramid_gen_kernel', FIELD_FLOAT, 'Gen. kernel',
186
+ required=False, add_to_layout=q_pyramid.layout(),
187
+ default=constants.DEFAULT_PY_GEN_KERNEL,
188
+ min_val=0.0, max_val=2.0)
189
+ self.builder.add_field('pyramid_float_type', FIELD_COMBO, 'Precision', required=False,
190
+ add_to_layout=q_pyramid.layout(),
191
+ options=self.FLOAT_OPTIONS, values=constants.VALID_FLOATS,
192
+ default=dict(zip(constants.VALID_FLOATS,
193
+ self.FLOAT_OPTIONS))[constants.DEFAULT_PY_FLOAT])
194
+ self.builder.add_field('depthmap_energy', FIELD_COMBO, 'Energy', required=False,
195
+ add_to_layout=q_depthmap.layout(),
196
+ options=self.ENERGY_OPTIONS, values=constants.VALID_DM_ENERGY,
197
+ default=dict(zip(constants.VALID_DM_ENERGY,
198
+ self.ENERGY_OPTIONS))[constants.DEFAULT_DM_ENERGY])
199
+ self.builder.add_field('map_type', FIELD_COMBO, 'Map type', required=False,
200
+ add_to_layout=q_depthmap.layout(),
201
+ options=self.MAP_TYPE_OPTIONS, values=constants.VALID_DM_MAP,
202
+ default=dict(zip(constants.VALID_DM_MAP,
203
+ self.MAP_TYPE_OPTIONS))[constants.DEFAULT_DM_MAP])
204
+ if self.expert:
205
+ self.builder.add_field('depthmap_kernel_size', FIELD_INT, 'Kernel size (px)',
206
+ required=False, add_to_layout=q_depthmap.layout(),
207
+ default=constants.DEFAULT_DM_KERNEL_SIZE, min_val=3, max_val=21)
208
+ self.builder.add_field('depthmap_blur_size', FIELD_INT, 'Blurl size (px)',
209
+ required=False, add_to_layout=q_depthmap.layout(),
210
+ default=constants.DEFAULT_DM_BLUR_SIZE, min_val=1, max_val=21)
211
+ self.builder.add_field('depthmap_smooth_size', FIELD_INT, 'Smooth size (px)',
212
+ required=False, add_to_layout=q_depthmap.layout(),
213
+ default=constants.DEFAULT_DM_SMOOTH_SIZE, min_val=0, max_val=256)
214
+ self.builder.add_field('depthmap_temperature', FIELD_FLOAT, 'Temperature',
215
+ required=False,
216
+ add_to_layout=q_depthmap.layout(),
217
+ default=constants.DEFAULT_DM_TEMPERATURE,
218
+ min_val=0, max_val=1, step=0.05)
219
+ self.builder.add_field('depthmap_levels', FIELD_INT, 'Levels', required=False,
220
+ add_to_layout=q_depthmap.layout(),
221
+ default=constants.DEFAULT_DM_LEVELS, min_val=2, max_val=6)
222
+ self.builder.add_field('depthmap_float_type', FIELD_COMBO, 'Precision', required=False,
223
+ add_to_layout=q_depthmap.layout(), options=self.FLOAT_OPTIONS,
224
+ values=constants.VALID_FLOATS,
225
+ default=dict(zip(constants.VALID_FLOATS,
226
+ self.FLOAT_OPTIONS))[constants.DEFAULT_DM_FLOAT])
227
+ self.builder.layout.addRow(stacked)
228
+ combo.currentIndexChanged.connect(change)
229
+
230
+
231
+ class FocusStackConfigurator(FocusStackBaseConfigurator):
232
+ def create_form(self, layout, action):
233
+ super().create_form(layout, action)
234
+ if self.expert:
235
+ self.builder.add_field('exif_path', FIELD_REL_PATH, 'Exif data path', required=False,
236
+ placeholder='relative to working path')
237
+ self.builder.add_field('prefix', FIELD_TEXT, 'Ouptut filename prefix', required=False,
238
+ default=constants.DEFAULT_STACK_PREFIX,
239
+ placeholder=constants.DEFAULT_STACK_PREFIX)
240
+ self.builder.add_field('plot_stack', FIELD_BOOL, 'Plot stack', required=False,
241
+ default=constants.DEFAULT_PLOT_STACK)
242
+ super().common_fields(layout)
243
+
244
+
245
+ class FocusStackBunchConfigurator(FocusStackBaseConfigurator):
246
+ def create_form(self, layout, action):
247
+ super().create_form(layout, action)
248
+ self.builder.add_field('frames', FIELD_INT, 'Frames', required=False,
249
+ default=constants.DEFAULT_FRAMES, min_val=1, max_val=100)
250
+ self.builder.add_field('overlap', FIELD_INT, 'Overlapping frames', required=False,
251
+ default=constants.DEFAULT_OVERLAP, min_val=0, max_val=100)
252
+ self.builder.add_field('plot_stack', FIELD_BOOL, 'Plot stack', required=False,
253
+ default=constants.DEFAULT_PLOT_STACK_BUNCH)
254
+ super().common_fields(layout)
255
+
256
+
257
+ class MultiLayerConfigurator(DefaultActionConfigurator):
258
+ def create_form(self, layout, action):
259
+ super().create_form(layout, action)
260
+ if self.expert:
261
+ self.builder.add_field('working_path', FIELD_ABS_PATH, 'Working path', required=False)
262
+ self.builder.add_field('input_path', FIELD_REL_PATH,
263
+ f'Input path (separate by {constants.PATH_SEPARATOR})',
264
+ required=False, multiple_entries=True,
265
+ placeholder='relative to working path')
266
+ if self.expert:
267
+ self.builder.add_field('output_path', FIELD_REL_PATH, 'Output path', required=False,
268
+ placeholder='relative to working path')
269
+ self.builder.add_field('exif_path', FIELD_REL_PATH, 'Exif data path', required=False,
270
+ placeholder='relative to working path')
271
+ self.builder.add_field('scratch_output_dir', FIELD_BOOL, 'Scratch output dir.',
272
+ required=False, default=True)
273
+ self.builder.add_field('reverse_order', FIELD_BOOL, 'Reverse file order',
274
+ required=False,
275
+ default=constants.DEFAULT_MULTILAYER_FILE_REVERSE_ORDER)
276
+
277
+
278
+ class CombinedActionsConfigurator(DefaultActionConfigurator):
279
+ def create_form(self, layout, action):
280
+ super().create_form(layout, action)
281
+ if self.expert:
282
+ self.builder.add_field('working_path', FIELD_ABS_PATH, 'Working path', required=False)
283
+ self.builder.add_field('input_path', FIELD_REL_PATH, 'Input path', required=False,
284
+ must_exist=True, placeholder='relative to working path')
285
+ self.builder.add_field('output_path', FIELD_REL_PATH, 'Output path', required=False,
286
+ placeholder='relative to working path')
287
+ self.builder.add_field('scratch_output_dir', FIELD_BOOL, 'Scratch output dir.',
288
+ required=False, default=True)
289
+ if self.expert:
290
+ self.builder.add_field('plot_path', FIELD_REL_PATH, 'Plots path', required=False,
291
+ default="plots", placeholder='relative to working path')
292
+ self.builder.add_field('resample', FIELD_INT, 'Resample frame stack', required=False,
293
+ default=1, min_val=1, max_val=100)
294
+ self.builder.add_field('ref_idx', FIELD_INT, 'Reference frame index', required=False,
295
+ default=-1, min_val=-1, max_val=1000)
296
+ self.builder.add_field('step_process', FIELD_BOOL, 'Step process', required=False,
297
+ default=True)
298
+
299
+
300
+ class MaskNoiseConfigurator(DefaultActionConfigurator):
301
+ def create_form(self, layout, action):
302
+ super().create_form(layout, action)
303
+ self.builder.add_field('noise_mask', FIELD_REL_PATH, 'Noise mask file', required=False,
304
+ path_type='file', must_exist=True,
305
+ default=constants.DEFAULT_NOISE_MAP_FILENAME,
306
+ placeholder=constants.DEFAULT_NOISE_MAP_FILENAME)
307
+ if self.expert:
308
+ self.builder.add_field('kernel_size', FIELD_INT, 'Kernel size', required=False,
309
+ default=constants.DEFAULT_MN_KERNEL_SIZE, min_va=1, max_val=10)
310
+ self.builder.add_field('method', FIELD_COMBO, 'Interpolation method', required=False,
311
+ options=['Mean', 'Median'], default='Mean')
312
+
313
+
314
+ class VignettingConfigurator(DefaultActionConfigurator):
315
+ def create_form(self, layout, action):
316
+ super().create_form(layout, action)
317
+ if self.expert:
318
+ self.builder.add_field('r_steps', FIELD_INT, 'Radial steps', required=False,
319
+ default=constants.DEFAULT_R_STEPS, min_val=1, max_val=1000)
320
+ self.builder.add_field('black_threshold', FIELD_INT, 'Black intensity threshold',
321
+ required=False, default=constants.DEFAULT_BLACK_THRESHOLD,
322
+ min_val=0, max_val=1000)
323
+ self.builder.add_field('subsample', FIELD_INT, 'Subsample factor', required=False,
324
+ default=constants.DEFAULT_VIGN_SUBSAMPLE, min_val=1, max_val=256)
325
+ self.builder.add_field('fast_subsampling', FIELD_BOOL, 'Fast subsampling',
326
+ required=False, default=constants.DEFAULT_VIGN_FAST_SUBSAMPLING)
327
+ self.builder.add_field('max_correction', FIELD_FLOAT, 'Max. correction', required=False,
328
+ default=constants.DEFAULT_MAX_CORRECTION,
329
+ min_val=0, max_val=1, step=0.05)
330
+ self.add_bold_label("Miscellanea:")
331
+ self.builder.add_field('plot_correction', FIELD_BOOL, 'Plot correction', required=False,
332
+ default=False)
333
+ self.builder.add_field('plot_summary', FIELD_BOOL, 'Plot summary', required=False,
334
+ default=False)
335
+
336
+
337
+ class AlignFramesConfigurator(DefaultActionConfigurator):
338
+ BORDER_MODE_OPTIONS = ['Constant', 'Replicate', 'Replicate and blur']
339
+ TRANSFORM_OPTIONS = ['Rigid', 'Homography']
340
+ METHOD_OPTIONS = ['Random Sample Consensus (RANSAC)', 'Least Median (LMEDS)']
341
+ MATCHING_METHOD_OPTIONS = ['K-nearest neighbors', 'Hamming distance']
342
+
343
+ def __init__(self, expert, current_wd):
344
+ super().__init__(expert, current_wd)
345
+ self.matching_method_field = None
346
+ self.info_label = None
347
+ self.detector_field = None
348
+ self.descriptor_field = None
349
+ self.matching_method_field = None
350
+
351
+ def show_info(self, message, timeout=3000):
352
+ self.info_label.setText(message)
353
+ self.info_label.setVisible(True)
354
+ timer = QTimer(self.info_label)
355
+ timer.setSingleShot(True)
356
+ timer.timeout.connect(self.info_label.hide)
357
+ timer.start(timeout)
358
+
359
+ def change_match_config(self):
360
+ detector = self.detector_field.currentText()
361
+ descriptor = self.descriptor_field.currentText()
362
+ match_method = dict(
363
+ zip(self.MATCHING_METHOD_OPTIONS,
364
+ constants.VALID_MATCHING_METHODS))[self.matching_method_field.currentText()]
365
+ try:
366
+ validate_align_config(detector, descriptor, match_method)
367
+ except Exception as e:
368
+ self.show_info(str(e))
369
+ if descriptor == constants.DETECTOR_SIFT and \
370
+ match_method == constants.MATCHING_NORM_HAMMING:
371
+ self.matching_method_field.setCurrentText(self.MATCHING_METHOD_OPTIONS[0])
372
+ if detector == constants.DETECTOR_ORB and descriptor == constants.DESCRIPTOR_AKAZE and \
373
+ match_method == constants.MATCHING_NORM_HAMMING:
374
+ self.matching_method_field.setCurrentText(constants.MATCHING_NORM_HAMMING)
375
+ if detector == constants.DETECTOR_BRISK and descriptor == constants.DESCRIPTOR_AKAZE:
376
+ self.descriptor_field.setCurrentText('BRISK')
377
+ if detector == constants.DETECTOR_SURF and descriptor == constants.DESCRIPTOR_AKAZE:
378
+ self.descriptor_field.setCurrentText('SIFT')
379
+ if detector == constants.DETECTOR_SIFT and descriptor != constants.DESCRIPTOR_SIFT:
380
+ self.descriptor_field.setCurrentText('SIFT')
381
+ if detector in constants.NOKNN_METHODS['detectors'] and \
382
+ descriptor in constants.NOKNN_METHODS['descriptors']:
383
+ if match_method == constants.MATCHING_KNN:
384
+ self.matching_method_field.setCurrentText(self.MATCHING_METHOD_OPTIONS[1])
385
+
386
+ def create_form(self, layout, action):
387
+ super().create_form(layout, action)
388
+ self.detector_field = None
389
+ self.descriptor_field = None
390
+ self.matching_method_field = None
391
+ if self.expert:
392
+ self.add_bold_label("Feature identification:")
393
+
394
+ self.info_label = QLabel()
395
+ self.info_label.setStyleSheet("color: orange; font-style: italic;")
396
+ self.info_label.setVisible(False)
397
+ layout.addRow(self.info_label)
398
+
399
+ self.detector_field = self.builder.add_field(
400
+ 'detector', FIELD_COMBO, 'Detector', required=False,
401
+ options=constants.VALID_DETECTORS, default=constants.DEFAULT_DETECTOR)
402
+ self.descriptor_field = self.builder.add_field(
403
+ 'descriptor', FIELD_COMBO, 'Descriptor', required=False,
404
+ options=constants.VALID_DESCRIPTORS, default=constants.DEFAULT_DESCRIPTOR)
405
+
406
+ self.add_bold_label("Feature matching:")
407
+ self.matching_method_field = self.builder.add_field(
408
+ 'match_method', FIELD_COMBO, 'Match method', required=False,
409
+ options=self.MATCHING_METHOD_OPTIONS, values=constants.VALID_MATCHING_METHODS,
410
+ default=constants.DEFAULT_MATCHING_METHOD)
411
+ self.detector_field.setToolTip(
412
+ "SIFT: Requires SIFT descriptor and K-NN matching\n"
413
+ "ORB/AKAZE: Work best with Hamming distance"
414
+ )
415
+
416
+ self.descriptor_field.setToolTip(
417
+ "SIFT: Requires K-NN matching\n"
418
+ "ORB/AKAZE: Require Hamming distance with ORB/AKAZE detectors"
419
+ )
420
+
421
+ self.matching_method_field.setToolTip(
422
+ "Automatically selected based on detector/descriptor combination"
423
+ )
424
+
425
+ self.detector_field.currentIndexChanged.connect(self.change_match_config)
426
+ self.descriptor_field.currentIndexChanged.connect(self.change_match_config)
427
+ self.matching_method_field.currentIndexChanged.connect(self.change_match_config)
428
+ self.builder.add_field('flann_idx_kdtree', FIELD_INT, 'Flann idx kdtree',
429
+ required=False,
430
+ default=constants.DEFAULT_FLANN_IDX_KDTREE,
431
+ min_val=0, max_val=10)
432
+ self.builder.add_field('flann_trees', FIELD_INT, 'Flann trees', required=False,
433
+ default=constants.DEFAULT_FLANN_TREES,
434
+ min_val=0, max_val=10)
435
+ self.builder.add_field('flann_checks', FIELD_INT, 'Flann checks', required=False,
436
+ default=constants.DEFAULT_FLANN_CHECKS,
437
+ min_val=0, max_val=1000)
438
+ self.builder.add_field('threshold', FIELD_FLOAT, 'Threshold', required=False,
439
+ default=constants.DEFAULT_ALIGN_THRESHOLD,
440
+ min_val=0, max_val=1, step=0.05)
441
+
442
+ self.add_bold_label("Transform:")
443
+ transform = self.builder.add_field(
444
+ 'transform', FIELD_COMBO, 'Transform', required=False,
445
+ options=self.TRANSFORM_OPTIONS, values=constants.VALID_TRANSFORMS,
446
+ default=constants.DEFAULT_TRANSFORM)
447
+ method = self.builder.add_field(
448
+ 'align_method', FIELD_COMBO, 'Align method', required=False,
449
+ options=self.METHOD_OPTIONS, values=constants.VALID_ALIGN_METHODS,
450
+ default=constants.DEFAULT_ALIGN_METHOD)
451
+ rans_threshold = self.builder.add_field(
452
+ 'rans_threshold', FIELD_FLOAT, 'RANSAC threshold (px)', required=False,
453
+ default=constants.DEFAULT_RANS_THRESHOLD, min_val=0, max_val=20, step=0.1)
454
+ self.builder.add_field(
455
+ 'min_good_matches', FIELD_INT, "Min. good matches", required=False,
456
+ default=constants.DEFAULT_ALIGN_MIN_GOOD_MATCHES, min_val=0, max_val=500)
457
+
458
+ def change_method():
459
+ text = method.currentText()
460
+ if text == self.METHOD_OPTIONS[0]:
461
+ rans_threshold.setEnabled(True)
462
+ elif text == self.METHOD_OPTIONS[1]:
463
+ rans_threshold.setEnabled(False)
464
+ method.currentIndexChanged.connect(change_method)
465
+ change_method()
466
+ self.builder.add_field('align_confidence', FIELD_FLOAT, 'Confidence (%)',
467
+ required=False, decimals=1,
468
+ default=constants.DEFAULT_ALIGN_CONFIDENCE,
469
+ min_val=70.0, max_val=100.0, step=0.1)
470
+
471
+ refine_iters = self.builder.add_field(
472
+ 'refine_iters', FIELD_INT, 'Refinement iterations (Rigid)', required=False,
473
+ default=constants.DEFAULT_REFINE_ITERS, min_val=0, max_val=1000)
474
+ max_iters = self.builder.add_field(
475
+ 'max_iters', FIELD_INT, 'Max. iterations (Homography)', required=False,
476
+ default=constants.DEFAULT_ALIGN_MAX_ITERS, min_val=0, max_val=5000)
477
+
478
+ def change_transform():
479
+ text = transform.currentText()
480
+ if text == self.TRANSFORM_OPTIONS[0]:
481
+ refine_iters.setEnabled(True)
482
+ max_iters.setEnabled(False)
483
+ elif text == self.TRANSFORM_OPTIONS[1]:
484
+ refine_iters.setEnabled(False)
485
+ max_iters.setEnabled(True)
486
+ transform.currentIndexChanged.connect(change_transform)
487
+ change_transform()
488
+ subsample = self.builder.add_field(
489
+ 'subsample', FIELD_INT, 'Subsample factor', required=False,
490
+ default=constants.DEFAULT_ALIGN_SUBSAMPLE, min_val=1, max_val=256)
491
+ fast_subsampling = self.builder.add_field(
492
+ 'fast_subsampling', FIELD_BOOL, 'Fast subsampling', required=False,
493
+ default=constants.DEFAULT_ALIGN_FAST_SUBSAMPLING)
494
+
495
+ def change_subsample():
496
+ fast_subsampling.setEnabled(subsample.value() > 1)
497
+ subsample.valueChanged.connect(change_subsample)
498
+ change_subsample()
499
+ self.add_bold_label("Border:")
500
+ self.builder.add_field('border_mode', FIELD_COMBO, 'Border mode', required=False,
501
+ options=self.BORDER_MODE_OPTIONS,
502
+ values=constants.VALID_BORDER_MODES,
503
+ default=constants.DEFAULT_BORDER_MODE)
504
+ self.builder.add_field('border_value', FIELD_INT_TUPLE,
505
+ 'Border value (if constant)', required=False, size=4,
506
+ default=constants.DEFAULT_BORDER_VALUE,
507
+ labels=constants.RGBA_LABELS,
508
+ min_val=constants.DEFAULT_BORDER_VALUE, max_val=[255] * 4)
509
+ self.builder.add_field('border_blur', FIELD_FLOAT, 'Border blur', required=False,
510
+ default=constants.DEFAULT_BORDER_BLUR,
511
+ min_val=0, max_val=1000, step=1)
512
+ self.add_bold_label("Miscellanea:")
513
+ self.builder.add_field('plot_summary', FIELD_BOOL, 'Plot summary',
514
+ required=False, default=False)
515
+ self.builder.add_field('plot_matches', FIELD_BOOL, 'Plot matches',
516
+ required=False, default=False)
517
+
518
+ def update_params(self, params: Dict[str, Any]) -> bool:
519
+ if self.detector_field and self.descriptor_field and self.matching_method_field:
520
+ try:
521
+ detector = self.detector_field.currentText()
522
+ descriptor = self.descriptor_field.currentText()
523
+ match_method = dict(
524
+ zip(self.MATCHING_METHOD_OPTIONS,
525
+ constants.VALID_MATCHING_METHODS))[
526
+ self.matching_method_field.currentText()]
527
+ validate_align_config(detector, descriptor, match_method)
528
+ return super().update_params(params)
529
+ except Exception as e:
530
+ traceback.print_tb(e.__traceback__)
531
+ QMessageBox.warning(None, "Error", f"{str(e)}")
532
+ return False
533
+ return super().update_params(params)
534
+
535
+
536
+ class BalanceFramesConfigurator(DefaultActionConfigurator):
537
+ CORRECTION_MAP_OPTIONS = ['Linear', 'Gamma', 'Match histograms']
538
+ CHANNEL_OPTIONS = ['Luminosity', 'RGB', 'HSV', 'HLS']
539
+
540
+ def create_form(self, layout, action):
541
+ super().create_form(layout, action)
542
+ if self.expert:
543
+ self.builder.add_field('mask_size', FIELD_FLOAT, 'Mask size', required=False,
544
+ default=0, min_val=0, max_val=5, step=0.1)
545
+ self.builder.add_field('intensity_interval', FIELD_INT_TUPLE, 'Intensity range',
546
+ required=False, size=2,
547
+ default=[v for k, v in
548
+ constants.DEFAULT_INTENSITY_INTERVAL.items()],
549
+ labels=['min', 'max'], min_val=[-1] * 2, max_val=[65536] * 2)
550
+ self.builder.add_field('subsample', FIELD_INT, 'Subsample factor', required=False,
551
+ default=constants.DEFAULT_BALANCE_SUBSAMPLE,
552
+ min_val=1, max_val=256)
553
+ self.builder.add_field('fast_subsampling', FIELD_BOOL, 'Fast subsampling',
554
+ required=False,
555
+ default=constants.DEFAULT_BALANCE_FAST_SUBSAMPLING)
556
+ self.builder.add_field('corr_map', FIELD_COMBO, 'Correction map', required=False,
557
+ options=self.CORRECTION_MAP_OPTIONS, values=constants.VALID_BALANCE,
558
+ default='Linear')
559
+ self.builder.add_field('channel', FIELD_COMBO, 'Channel', required=False,
560
+ options=self.CHANNEL_OPTIONS,
561
+ values=constants.VALID_BALANCE_CHANNELS,
562
+ default='Luminosity')
563
+ self.add_bold_label("Miscellanea:")
564
+ self.builder.add_field('plot_summary', FIELD_BOOL, 'Plot summary',
565
+ required=False, default=False)
566
+ self.builder.add_field('plot_histograms', FIELD_BOOL, 'Plot histograms',
567
+ required=False, default=False)
@@ -0,0 +1,18 @@
1
+ # pylint: disable=C0114, C0115, C0116, R0903, E0611
2
+ from PySide6.QtWidgets import QFormLayout, QDialog
3
+ from PySide6.QtCore import Qt
4
+
5
+
6
+ class BaseFormDialog(QDialog):
7
+ def __init__(self, title, parent=None):
8
+ super().__init__(parent)
9
+ self.setWindowTitle(title)
10
+ self.resize(500, self.height())
11
+ self.layout = QFormLayout(self)
12
+ self.layout.setFieldGrowthPolicy(QFormLayout.AllNonFixedFieldsGrow)
13
+ self.layout.setRowWrapPolicy(QFormLayout.DontWrapRows)
14
+ self.layout.setFormAlignment(Qt.AlignLeft | Qt.AlignTop)
15
+ self.layout.setLabelAlignment(Qt.AlignLeft)
16
+
17
+ def add_row_to_layout(self, item):
18
+ self.layout.addRow(item)
@@ -1,7 +1,4 @@
1
1
  # pylint: disable=C0114, C0115, C0116, R0903, E0611
2
- from PySide6.QtGui import QColor
3
-
4
-
5
2
  class ColorEntry:
6
3
  def __init__(self, r, g, b):
7
4
  self.r = r
@@ -14,9 +11,6 @@ class ColorEntry:
14
11
  def hex(self):
15
12
  return f"{self.r:02x}{self.g:02x}{self.b:02x}"
16
13
 
17
- def q_color(self):
18
- return QColor(self.r, self.g, self.b)
19
-
20
14
 
21
15
  class ColorPalette:
22
16
  BLACK = ColorEntry(0, 0, 0)
@@ -56,3 +50,8 @@ BLUE_COMBO_STYLE = f"""
56
50
  color: #{ColorPalette.MEDIUM_BLUE.hex()};
57
51
  }}
58
52
  """
53
+
54
+ ACTION_RUNNING_COLOR = ColorPalette.MEDIUM_BLUE
55
+ ACTION_COMPLETED_COLOR = ColorPalette.MEDIUM_GREEN
56
+ ACTION_STOPPED_COLOR = ColorPalette.MEDIUM_RED
57
+ ACTION_FAILED_COLOR = ColorPalette.MEDIUM_RED
@@ -57,7 +57,6 @@ class SimpleHtmlFormatter(logging.Formatter):
57
57
 
58
58
  def __init__(self, fmt=None, datefmt=None, style='%'):
59
59
  super().__init__()
60
- self._fmt = fmt or "[%(levelname).3s] %(message)s"
61
60
  self.datefmt = datefmt or "%H:%M:%S"
62
61
 
63
62
  def format(self, record):