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

@@ -6,8 +6,8 @@ from .. config.constants import constants
6
6
  from .. core.colors import color_str
7
7
  from .. core.framework import Job, ActionList
8
8
  from .. core.core_utils import check_path_exists
9
- from .. core.exceptions import ShapeError, BitDepthError, RunStopException
10
- from .utils import read_img, write_img, extension_tif_jpg
9
+ from .. core.exceptions import RunStopException
10
+ from .utils import read_img, write_img, extension_tif_jpg, get_img_metadata, validate_image
11
11
 
12
12
 
13
13
  class StackJob(Job):
@@ -33,26 +33,73 @@ class FramePaths:
33
33
  self.working_path = working_path
34
34
  self.plot_path = plot_path
35
35
  self.input_path = input_path
36
- self.output_path = output_path
37
- self.output_dir = None
36
+ self.output_path = self.name if output_path == '' else output_path
38
37
  self.resample = resample
39
38
  self.reverse_order = reverse_order
40
39
  self.scratch_output_dir = scratch_output_dir
41
- self.input_full_path = None
42
40
  self.enabled = None
43
- self.filenames = None
44
41
  self.base_message = ''
45
-
46
- def folder_filelist(self):
47
- assert False, "this method should be overwritten"
42
+ self._input_full_path = None
43
+ self._output_full_path = None
44
+ self._input_filepaths = None
45
+
46
+ def output_full_path(self):
47
+ if self._output_full_path is None:
48
+ self._output_full_path = os.path.join(self.working_path, self.output_path)
49
+ return self._output_full_path
50
+
51
+ def input_full_path(self):
52
+ if self._input_full_path is None:
53
+ if isinstance(self.input_path, str):
54
+ self._input_full_path = os.path.join(self.working_path, self.input_path)
55
+ check_path_exists(self._input_full_path)
56
+ elif hasattr(self.input_path, "__len__"):
57
+ self._input_full_path = [os.path.join(self.working_path, path)
58
+ for path in self.input_path]
59
+ for path in self._input_full_path:
60
+ check_path_exists(path)
61
+ return self._input_full_path
62
+
63
+ def input_filepaths(self):
64
+ if self._input_filepaths is None:
65
+ if isinstance(self.input_full_path(), str):
66
+ dirs = [self.input_full_path()]
67
+ elif hasattr(self.input_full_path(), "__len__"):
68
+ dirs = self.input_full_path()
69
+ else:
70
+ raise RuntimeError("input_full_path option must contain "
71
+ "a path or an array of paths")
72
+ files = []
73
+ for d in dirs:
74
+ filelist = []
75
+ for _dirpath, _, filenames in os.walk(d):
76
+ filelist = [os.path.join(_dirpath, name)
77
+ for name in filenames if extension_tif_jpg(name)]
78
+ filelist.sort()
79
+ if self.reverse_order:
80
+ filelist.reverse()
81
+ if self.resample > 1:
82
+ filelist = filelist[0::self.resample]
83
+ files += filelist
84
+ if len(files) == 0:
85
+ self.print_message(color_str(f"input folder {d} does not contain any image",
86
+ constants.LOG_COLOR_WARNING),
87
+ level=logging.WARNING)
88
+ self._input_filepaths = files
89
+ return self._input_filepaths
90
+
91
+ def input_filepath(self, index):
92
+ return self.input_filepaths()[index]
93
+
94
+ def num_input_filepaths(self):
95
+ return len(self.input_filepaths())
48
96
 
49
97
  def print_message(self, msg='', level=logging.INFO, end=None, begin='', tqdm=False):
50
98
  assert False, "this method should be overwritten"
51
99
 
52
100
  def set_filelist(self):
53
- self.filenames = self.folder_filelist()
54
- file_folder = self.input_full_path.replace(self.working_path, '').lstrip('/')
55
- self.print_message(color_str(f"{len(self.filenames)} files in folder: {file_folder}",
101
+ file_folder = self.input_full_path().replace(self.working_path, '').lstrip('/')
102
+ self.print_message(color_str(f"{self.num_input_filepaths()} files in folder: {file_folder}",
56
103
  constants.LOG_COLOR_LEVEL_2))
57
104
  self.base_message = color_str(self.name, constants.LOG_COLOR_LEVEL_1, "bold")
58
105
 
@@ -60,20 +107,16 @@ class FramePaths:
60
107
  if self.working_path == '':
61
108
  self.working_path = job.working_path
62
109
  check_path_exists(self.working_path)
63
- if self.output_path == '':
64
- self.output_path = self.name
65
- self.output_dir = self.working_path + \
66
- ('' if self.working_path[-1] == '/' else '/') + \
67
- self.output_path
68
- if not os.path.exists(self.output_dir):
69
- os.makedirs(self.output_dir)
110
+ output_dir = self.output_full_path()
111
+ if not os.path.exists(output_dir):
112
+ os.makedirs(output_dir)
70
113
  else:
71
- list_dir = os.listdir(self.output_dir)
114
+ list_dir = os.listdir(output_dir)
72
115
  if len(list_dir) > 0:
73
116
  if self.scratch_output_dir:
74
117
  if self.enabled:
75
118
  for filename in list_dir:
76
- file_path = os.path.join(self.output_dir, filename)
119
+ file_path = os.path.join(output_dir, filename)
77
120
  if os.path.isfile(file_path):
78
121
  os.remove(file_path)
79
122
  self.print_message(
@@ -100,136 +143,69 @@ class FramePaths:
100
143
  self.input_path = job.paths[-1]
101
144
  job.paths.append(self.output_path)
102
145
 
103
-
104
- class FrameDirectory(FramePaths):
105
- def __init__(self, name, **kwargs):
106
- FramePaths.__init__(self, name, **kwargs)
107
-
108
146
  def folder_list_str(self):
109
- if isinstance(self.input_full_path, list):
147
+ if isinstance(self.input_full_path(), list):
110
148
  file_list = ", ".join(
111
- list(self.input_full_path.replace(self.working_path, '').lstrip('/')))
112
- return "folder" + ('s' if len(self.input_full_path) > 1 else '') + f": {file_list}"
113
- return "folder: " + self.input_full_path.replace(self.working_path, '').lstrip('/')
114
-
115
- def folder_filelist(self):
116
- src_contents = os.walk(self.input_full_path)
117
- _dirpath, _, filenames = next(src_contents)
118
- filelist = [name for name in filenames if extension_tif_jpg(name)]
119
- filelist.sort()
120
- if self.reverse_order:
121
- filelist.reverse()
122
- if self.resample > 1:
123
- filelist = filelist[0::self.resample]
124
- return filelist
149
+ [path.replace(self.working_path, '').lstrip('/')
150
+ for path in self.input_full_path()])
151
+ return "folder" + ('s' if len(self.input_full_path()) > 1 else '') + f": {file_list}"
152
+ return "folder: " + self.input_full_path().replace(self.working_path, '').lstrip('/')
125
153
 
126
- def init(self, job, _working_path=''):
127
- FramePaths.init(self, job)
128
- self.input_full_path = self.working_path + \
129
- ('' if self.working_path[-1] == '/' else '/') + self.input_path
130
- check_path_exists(self.input_full_path)
131
- job.paths.append(self.output_path)
132
-
133
-
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
142
154
 
143
- def folder_list_str(self):
144
- if isinstance(self.input_full_path, list):
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('/')
149
-
150
- def folder_filelist(self):
151
- if isinstance(self.input_full_path, str):
152
- dirs = [self.input_full_path]
153
- paths = [self.input_path]
154
- elif hasattr(self.input_full_path, "__len__"):
155
- dirs = self.input_full_path
156
- paths = self.input_path
157
- else:
158
- raise RuntimeError("input_full_path option must contain a path or an array of paths")
159
- files = []
160
- for d, p in zip(dirs, paths):
161
- filelist = []
162
- for _dirpath, _, filenames in os.walk(d):
163
- filelist = [f"{p}/{name}" for name in filenames if extension_tif_jpg(name)]
164
- if self.reverse_order:
165
- filelist.reverse()
166
- if self.resample > 1:
167
- filelist = filelist[0::self.resample]
168
- files += filelist
169
- if len(files) == 0:
170
- self.print_message(color_str(f"input folder {p} does not contain any image", "red"),
171
- level=logging.WARNING)
172
- return files
173
-
174
- def init(self, job):
175
- FramePaths.init(self, job)
176
- if isinstance(self.input_path, str):
177
- self.input_full_path = self.working_path + \
178
- ('' if self.working_path[-1] == '/' else '/') + \
179
- self.input_path
180
- check_path_exists(self.input_full_path)
181
- elif hasattr(self.input_path, "__len__"):
182
- self.input_full_path = []
183
- for path in self.input_path:
184
- self.input_full_path.append(self.working_path +
185
- ('' if self.working_path[-1] == '/' else '/') +
186
- path)
187
- job.paths.append(self.output_path)
188
-
189
-
190
- class FramesRefActions(ActionList, FrameDirectory):
191
- def __init__(self, name, enabled=True, ref_idx=-1, step_process=False, **kwargs):
192
- FrameDirectory.__init__(self, name, **kwargs)
155
+ class FramesRefActions(ActionList, FramePaths):
156
+ def __init__(self, name, enabled=True, reference_index=0, step_process=False, **kwargs):
157
+ FramePaths.__init__(self, name, **kwargs)
193
158
  ActionList.__init__(self, name, enabled)
194
- self.ref_idx = ref_idx
159
+ self.ref_idx = reference_index
195
160
  self.step_process = step_process
196
- self._idx = None
197
- self._ref_idx = None
198
- self._idx_step = None
161
+ self.current_idx = None
162
+ self.current_ref_idx = None
163
+ self.current_idx_step = None
199
164
 
200
165
  def begin(self):
201
166
  ActionList.begin(self)
202
167
  self.set_filelist()
203
- self.set_counts(len(self.filenames))
204
- if self.ref_idx == -1:
205
- self.ref_idx = len(self.filenames) // 2
168
+ n = self.num_input_filepaths()
169
+ self.set_counts(n)
170
+ if self.ref_idx == 0:
171
+ self.ref_idx = n // 2
172
+ elif self.ref_idx == -1:
173
+ self.ref_idx = n - 1
174
+ else:
175
+ self.ref_idx -= 1
176
+ if not 0 <= self.ref_idx < n:
177
+ msg = f"reference index {self.ref_idx} out of range [1, {n}]"
178
+ self.print_message_r(color_str(msg, constants.LOG_COLOR_LEVEL_2))
179
+ raise IndexError(msg)
206
180
 
207
181
  def end(self):
208
182
  ActionList.end(self)
209
183
 
210
184
  def run_frame(self, _idx, _ref_idx):
211
- pass
185
+ return None
212
186
 
213
187
  def run_step(self):
214
- if self.count == 0:
215
- self._idx = self.ref_idx if self.step_process else 0
216
- self._ref_idx = self.ref_idx
217
- self._idx_step = +1
218
- ll = len(self.filenames)
188
+ if self.current_action_count == 0:
189
+ self.current_idx = self.ref_idx if self.step_process else 0
190
+ self.current_ref_idx = self.ref_idx
191
+ self.current_idx_step = +1
192
+ ll = self.num_input_filepaths()
219
193
  self.print_message_r(
220
- color_str(f"step {self.count + 1}/{ll}: process file: {self.filenames[self._idx]}, "
221
- f"reference: {self.filenames[self._ref_idx]}", constants.LOG_COLOR_LEVEL_2))
194
+ color_str(f"step {self.current_action_count + 1}/{ll}: process file: "
195
+ f"{os.path.basename(self.input_filepath(self.current_idx))}, "
196
+ f"reference: {os.path.basename(self.input_filepath(self.current_ref_idx))}",
197
+ constants.LOG_COLOR_LEVEL_2))
222
198
  self.base_message = color_str(self.name, constants.LOG_COLOR_LEVEL_1, "bold")
223
- self.run_frame(self._idx, self._ref_idx)
224
- if self._idx < ll:
225
- if self.step_process:
226
- self._ref_idx = self._idx
227
- self._idx += self._idx_step
228
- if self._idx == ll:
229
- self._idx = self.ref_idx - 1
199
+ success = self.run_frame(self.current_idx, self.current_ref_idx) is not None
200
+ if self.current_idx < ll:
201
+ if self.step_process and success:
202
+ self.current_ref_idx = self.current_idx
203
+ self.current_idx += self.current_idx_step
204
+ if self.current_idx == ll:
205
+ self.current_idx = self.ref_idx - 1
230
206
  if self.step_process:
231
- self._ref_idx = self.ref_idx
232
- self._idx_step = -1
207
+ self.current_ref_idx = self.ref_idx
208
+ self.current_idx_step = -1
233
209
 
234
210
 
235
211
  class SubAction:
@@ -247,8 +223,7 @@ class CombinedActions(FramesRefActions):
247
223
  def __init__(self, name, actions=[], enabled=True, **kwargs):
248
224
  FramesRefActions.__init__(self, name, enabled, **kwargs)
249
225
  self._actions = actions
250
- self.dtype = None
251
- self.shape = None
226
+ self._metadata = (None, None)
252
227
 
253
228
  def begin(self):
254
229
  FramesRefActions.begin(self)
@@ -257,32 +232,27 @@ class CombinedActions(FramesRefActions):
257
232
  a.begin(self)
258
233
 
259
234
  def img_ref(self, idx):
260
- filename = self.filenames[idx]
261
- img = read_img((self.output_dir
262
- if self.step_process else self.input_full_path) + f"/{filename}")
235
+ input_path = self.input_filepath(idx)
236
+ img = read_img(input_path)
263
237
  if img is None:
264
- raise RuntimeError(f"Invalid file: {self.input_full_path}/{filename}")
265
- self.dtype = img.dtype
266
- self.shape = img.shape
238
+ raise RuntimeError(f"Invalid file: {os.path.basename(input_path)}")
239
+ self._metadata = get_img_metadata(img)
267
240
  return img
268
241
 
269
242
  def run_frame(self, idx, ref_idx):
270
- filename = self.filenames[idx]
243
+ input_path = self.input_filepath(idx)
271
244
  self.sub_message_r(color_str(': read input image', constants.LOG_COLOR_LEVEL_3))
272
- img = read_img(f"{self.input_full_path}/{filename}")
273
- if self.dtype is not None and img.dtype != self.dtype:
274
- raise BitDepthError(self.dtype, img.dtype, )
275
- if self.shape is not None and img.shape != self.shape:
276
- raise ShapeError(self.shape, img.shape)
245
+ img = read_img(input_path)
246
+ validate_image(img, *(self._metadata))
277
247
  if img is None:
278
- raise RuntimeError(f"Invalid file: {self.input_full_path}/{filename}")
248
+ raise RuntimeError(f"Invalid file: {os.path.basename(input_path)}")
279
249
  if len(self._actions) == 0:
280
250
  self.sub_message(color_str(": no actions specified", constants.LOG_COLOR_ALERT),
281
251
  level=logging.WARNING)
282
252
  for a in self._actions:
283
253
  if not a.enabled:
284
254
  self.get_logger().warning(color_str(f"{self.base_message}: sub-action disabled",
285
- 'red'))
255
+ constants.LOG_COLOR_ALERT))
286
256
  else:
287
257
  if self.callback('check_running', self.id, self.name) is False:
288
258
  raise RunStopException(self.name)
@@ -293,14 +263,15 @@ class CombinedActions(FramesRefActions):
293
263
  color_str(": null input received, action skipped",
294
264
  constants.LOG_COLOR_ALERT),
295
265
  level=logging.WARNING)
296
- self.sub_message_r(color_str(': write output image', constants.LOG_COLOR_LEVEL_3))
297
266
  if img is not None:
298
- write_img(self.output_dir + "/" + filename, img)
299
- else:
300
- self.print_message(color_str(
301
- "no output file resulted from processing input file: "
302
- f"{self.input_full_path}/{filename}",
303
- constants.LOG_COLOR_ALERT), level=logging.WARNING)
267
+ self.sub_message_r(color_str(': write output image', constants.LOG_COLOR_LEVEL_3))
268
+ output_path = os.path.join(self.output_full_path(), os.path.basename(input_path))
269
+ write_img(output_path, img)
270
+ return img
271
+ self.print_message(color_str(
272
+ f"no output file resulted from processing input file: {os.path.basename(input_path)}",
273
+ constants.LOG_COLOR_ALERT), level=logging.WARNING)
274
+ return None
304
275
 
305
276
  def end(self):
306
277
  for a in self._actions:
@@ -160,7 +160,7 @@ class Vignetting(SubAction):
160
160
  "light_blue"),
161
161
  level=logging.DEBUG)
162
162
  if self.plot_correction:
163
- plt.figure(figsize=(10, 5))
163
+ plt.figure(figsize=constants.PLT_FIG_SIZE)
164
164
  plt.plot(radii, intensities, label="image mean intensity")
165
165
  plt.plot(radii, sigmoid_model(radii * subsample, *params), label="sigmoid fit")
166
166
  plt.xlabel('radius (pixels)')
@@ -187,12 +187,12 @@ class Vignetting(SubAction):
187
187
 
188
188
  def begin(self, process):
189
189
  self.process = process
190
- self.corrections = [np.full(self.process.counts, None, dtype=float)
190
+ self.corrections = [np.full(self.process.total_action_counts, None, dtype=float)
191
191
  for p in self.percentiles]
192
192
 
193
193
  def end(self):
194
194
  if self.plot_summary:
195
- plt.figure(figsize=(10, 5))
195
+ plt.figure(figsize=constants.PLT_FIG_SIZE)
196
196
  xs = np.arange(1, len(self.corrections[0]) + 1, dtype=int)
197
197
  for i, p in enumerate(self.percentiles):
198
198
  linestyle = 'solid'
@@ -22,6 +22,8 @@ class _Constants:
22
22
 
23
23
  ANSI_ESCAPE = re.compile(r'\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])')
24
24
 
25
+ PLT_FIG_SIZE = (10, 5)
26
+
25
27
  ACTION_JOB = "Job"
26
28
  ACTION_COMBO = "CombinedActions"
27
29
  ACTION_NOISEDETECTION = "NoiseDetection"
@@ -116,7 +118,7 @@ class _Constants:
116
118
  DEFAULT_REFINE_ITERS = 100
117
119
  DEFAULT_ALIGN_CONFIDENCE = 99.9
118
120
  DEFAULT_ALIGN_MAX_ITERS = 2000
119
- DEFAULT_ALIGN_ABORT_ABNORMAL = True
121
+ DEFAULT_ALIGN_ABORT_ABNORMAL = False
120
122
  DEFAULT_BORDER_VALUE = [0] * 4
121
123
  DEFAULT_BORDER_BLUR = 50
122
124
  DEFAULT_ALIGN_SUBSAMPLE = 0
@@ -24,7 +24,7 @@ class TqdmCallbacks:
24
24
 
25
25
  def __init__(self):
26
26
  self.tbar = None
27
- self.counts = -1
27
+ self.total_action_counts = -1
28
28
 
29
29
  @classmethod
30
30
  def instance(cls):
@@ -33,8 +33,8 @@ class TqdmCallbacks:
33
33
  return cls._instance
34
34
 
35
35
  def step_counts(self, name, counts):
36
- self.counts = counts
37
- self.tbar = make_tqdm_bar(name, self.counts)
36
+ self.total_action_counts = counts
37
+ self.tbar = make_tqdm_bar(name, self.total_action_counts)
38
38
 
39
39
  def begin_steps(self, name):
40
40
  pass
@@ -191,12 +191,12 @@ class Job(JobBase):
191
191
  class ActionList(JobBase):
192
192
  def __init__(self, name, enabled=True, **kwargs):
193
193
  JobBase.__init__(self, name, enabled, **kwargs)
194
- self.counts = None
195
- self.count = None
194
+ self.total_action_counts = None
195
+ self.current_action_count = None
196
196
 
197
197
  def set_counts(self, counts):
198
- self.counts = counts
199
- self.callback('step_counts', self.id, self.name, self.counts)
198
+ self.total_action_counts = counts
199
+ self.callback('step_counts', self.id, self.name, self.total_action_counts)
200
200
 
201
201
  def begin(self):
202
202
  self.callback('begin_steps', self.id, self.name)
@@ -205,17 +205,17 @@ class ActionList(JobBase):
205
205
  self.callback('end_steps', self.id, self.name)
206
206
 
207
207
  def __iter__(self):
208
- self.count = 0
208
+ self.current_action_count = 0
209
209
  return self
210
210
 
211
211
  def run_step(self):
212
212
  pass
213
213
 
214
214
  def __next__(self):
215
- if self.count < self.counts:
215
+ if self.current_action_count < self.total_action_counts:
216
216
  self.run_step()
217
- x = self.count
218
- self.count += 1
217
+ x = self.current_action_count
218
+ self.current_action_count += 1
219
219
  return x
220
220
  raise StopIteration
221
221
 
@@ -223,7 +223,7 @@ class ActionList(JobBase):
223
223
  self.print_message(color_str('begin run', constants.LOG_COLOR_LEVEL_2), end='\n')
224
224
  self.begin()
225
225
  for _ in iter(self):
226
- self.callback('after_step', self.id, self.name, self.count)
226
+ self.callback('after_step', self.id, self.name, self.current_action_count)
227
227
  if self.callback('check_running', self.id, self.name) is False:
228
228
  raise RunStopException(self.name)
229
229
  self.end()
@@ -16,12 +16,16 @@ FIELD_ABS_PATH = 'abs_path'
16
16
  FIELD_REL_PATH = 'rel_path'
17
17
  FIELD_FLOAT = 'float'
18
18
  FIELD_INT = 'int'
19
+ FIELD_REF_IDX = 'ref_idx'
19
20
  FIELD_INT_TUPLE = 'int_tuple'
20
21
  FIELD_BOOL = 'bool'
21
22
  FIELD_COMBO = 'combo'
22
23
  FIELD_TYPES = [FIELD_TEXT, FIELD_ABS_PATH, FIELD_REL_PATH, FIELD_FLOAT,
23
24
  FIELD_INT, FIELD_INT_TUPLE, FIELD_BOOL, FIELD_COMBO]
24
25
 
26
+ FIELD_REF_IDX_OPTIONS = ['Median frame', 'First frame', 'Last frame', 'Specify index']
27
+ FIELD_REF_IDX_MAX = 1000
28
+
25
29
 
26
30
  class ActionConfigurator(ABC):
27
31
  def __init__(self, expert, current_wd):
@@ -56,6 +60,8 @@ class FieldBuilder:
56
60
  widget = self.create_float_field(tag, **kwargs)
57
61
  elif field_type == FIELD_INT:
58
62
  widget = self.create_int_field(tag, **kwargs)
63
+ elif field_type == FIELD_REF_IDX:
64
+ widget = self.create_ref_idx_field(tag, **kwargs)
59
65
  elif field_type == FIELD_INT_TUPLE:
60
66
  widget = self.create_int_tuple_field(tag, **kwargs)
61
67
  elif field_type == FIELD_BOOL:
@@ -75,6 +81,8 @@ class FieldBuilder:
75
81
  default_value = kwargs.get('default', 0.0)
76
82
  elif field_type == FIELD_INT:
77
83
  default_value = kwargs.get('default', 0)
84
+ elif field_type == FIELD_REF_IDX:
85
+ default_value = kwargs.get('default', 0)
78
86
  elif field_type == FIELD_INT_TUPLE:
79
87
  default_value = kwargs.get('default', [0] * kwargs.get('size', 1))
80
88
  elif field_type == FIELD_BOOL:
@@ -112,6 +120,9 @@ class FieldBuilder:
112
120
  widget.setChecked(default)
113
121
  elif field['type'] == FIELD_INT:
114
122
  widget.setValue(default)
123
+ elif field['type'] == FIELD_REF_IDX:
124
+ widget.layout().itemAt(2).widget().setValue(default)
125
+ widget.layout().itemAt(0).widget().setCurrentText(FIELD_REF_IDX_OPTIONS[0])
115
126
  elif field['type'] == FIELD_INT_TUPLE:
116
127
  for i in range(field['size']):
117
128
  spinbox = widget.layout().itemAt(1 + i * 2).widget()
@@ -146,6 +157,17 @@ class FieldBuilder:
146
157
  params[tag] = field['widget'].isChecked()
147
158
  elif field['type'] == FIELD_INT:
148
159
  params[tag] = field['widget'].value()
160
+ elif field['type'] == FIELD_REF_IDX:
161
+ wl = field['widget'].layout()
162
+ txt = wl.itemAt(0).widget().currentText()
163
+ if txt == FIELD_REF_IDX_OPTIONS[0]:
164
+ params[tag] = 0
165
+ elif txt == FIELD_REF_IDX_OPTIONS[1]:
166
+ params[tag] = 1
167
+ elif txt == FIELD_REF_IDX_OPTIONS[2]:
168
+ params[tag] = -1
169
+ else:
170
+ params[tag] = wl.itemAt(2).widget().value()
149
171
  elif field['type'] == FIELD_INT_TUPLE:
150
172
  params[tag] = [field['widget'].layout().itemAt(1 + i * 2).widget().value()
151
173
  for i in range(field['size'])]
@@ -327,6 +349,37 @@ class FieldBuilder:
327
349
  spin.setValue(self.action.params.get(tag, default))
328
350
  return spin
329
351
 
352
+ def create_ref_idx_field(self, tag, default=0):
353
+ layout = QHBoxLayout()
354
+ combo = QComboBox()
355
+ combo.addItems(FIELD_REF_IDX_OPTIONS)
356
+ label = QLabel("index [1, ..., N]: ")
357
+ spin = QSpinBox()
358
+ spin.setRange(1, FIELD_REF_IDX_MAX)
359
+ value = self.action.params.get(tag, default)
360
+ if value == 0:
361
+ combo.setCurrentText(FIELD_REF_IDX_OPTIONS[0])
362
+ spin.setValue(1)
363
+ elif value == 1:
364
+ combo.setCurrentText(FIELD_REF_IDX_OPTIONS[1])
365
+ spin.setValue(1)
366
+ elif value == -1:
367
+ combo.setCurrentText(FIELD_REF_IDX_OPTIONS[2])
368
+ spin.setValue(1)
369
+ else:
370
+ combo.setCurrentText(FIELD_REF_IDX_OPTIONS[3])
371
+ spin.setValue(value)
372
+
373
+ def set_enabled():
374
+ spin.setEnabled(combo.currentText() == FIELD_REF_IDX_OPTIONS[-1])
375
+
376
+ combo.currentTextChanged.connect(set_enabled)
377
+ set_enabled()
378
+ layout.addWidget(combo)
379
+ layout.addWidget(label)
380
+ layout.addWidget(spin)
381
+ return create_layout_widget_no_margins(layout)
382
+
330
383
  def create_int_tuple_field(self, tag, size=1,
331
384
  default=[0] * 100, min_val=[0] * 100, max_val=[100] * 100,
332
385
  **kwargs):
@@ -12,7 +12,7 @@ from .base_form_dialog import create_form_layout
12
12
  from . action_config import (
13
13
  FieldBuilder, ActionConfigurator,
14
14
  FIELD_TEXT, FIELD_ABS_PATH, FIELD_REL_PATH, FIELD_FLOAT,
15
- FIELD_INT, FIELD_INT_TUPLE, FIELD_BOOL, FIELD_COMBO
15
+ FIELD_INT, FIELD_INT_TUPLE, FIELD_BOOL, FIELD_COMBO, FIELD_REF_IDX
16
16
  )
17
17
 
18
18
 
@@ -101,9 +101,8 @@ class ActionConfigDialog(QDialog):
101
101
  action_type, DefaultActionConfigurator)(self.expert(), self.current_wd)
102
102
 
103
103
  def accept(self):
104
- self.parent().project_editor.add_undo(self.parent().project().clone())
105
104
  if self.configurator.update_params(self.action.params):
106
- self.parent().mark_as_modified()
105
+ self.parent().mark_as_modified(True, "Modify Configuration")
107
106
  super().accept()
108
107
  else:
109
108
  self.parent().project_editor.pop_undo()
@@ -431,8 +430,8 @@ class CombinedActionsConfigurator(DefaultActionConfigurator):
431
430
  'resample', FIELD_INT, 'Resample frame stack', required=False,
432
431
  default=1, min_val=1, max_val=100)
433
432
  self.add_field(
434
- 'ref_idx', FIELD_INT, 'Reference frame index', required=False,
435
- default=-1, min_val=-1, max_val=1000)
433
+ 'reference_index', FIELD_REF_IDX, 'Reference frame', required=False,
434
+ default=0)
436
435
  self.add_field(
437
436
  'step_process', FIELD_BOOL, 'Step process', required=False,
438
437
  default=True)
@@ -67,13 +67,13 @@ class GuiImageView(QWidget):
67
67
  super().__init__(parent)
68
68
  self.file_path = file_path
69
69
  self.setFixedWidth(gui_constants.GUI_IMG_WIDTH)
70
- self.layout = QVBoxLayout()
71
- self.layout.setContentsMargins(0, 0, 0, 0)
72
- self.layout.setSpacing(0)
70
+ self.main_layout = QVBoxLayout()
71
+ self.main_layout.setContentsMargins(0, 0, 0, 0)
72
+ self.main_layout.setSpacing(0)
73
73
  self.image_label = QLabel()
74
74
  self.image_label.setAlignment(Qt.AlignCenter)
75
- self.layout.addWidget(self.image_label)
76
- self.setLayout(self.layout)
75
+ self.main_layout.addWidget(self.image_label)
76
+ self.setLayout(self.main_layout)
77
77
  pixmap = QPixmap(file_path)
78
78
  if pixmap:
79
79
  scaled_pixmap = pixmap.scaledToWidth(
@@ -105,13 +105,13 @@ class GuiOpenApp(QWidget):
105
105
  self.file_path = file_path
106
106
  self.app = app
107
107
  self.setFixedWidth(gui_constants.GUI_IMG_WIDTH)
108
- self.layout = QVBoxLayout()
109
- self.layout.setContentsMargins(0, 0, 0, 0)
110
- self.layout.setSpacing(0)
108
+ self.main_layout = QVBoxLayout()
109
+ self.main_layout.setContentsMargins(0, 0, 0, 0)
110
+ self.main_layout.setSpacing(0)
111
111
  self.image_label = QLabel()
112
112
  self.image_label.setAlignment(Qt.AlignCenter)
113
- self.layout.addWidget(self.image_label)
114
- self.setLayout(self.layout)
113
+ self.main_layout.addWidget(self.image_label)
114
+ self.setLayout(self.main_layout)
115
115
  pixmap = QPixmap(file_path)
116
116
  if pixmap:
117
117
  scaled_pixmap = pixmap.scaledToWidth(
@@ -328,7 +328,7 @@ class RunWorker(LogWorker):
328
328
  self.status_signal.emit(f"{self.tag} running...", constants.RUN_ONGOING, "", 0)
329
329
  self.html_signal.emit(f'''
330
330
  <div style="margin: 2px 0; font-family: {constants.LOG_FONTS_STR};">
331
- <span style="color: #{ColorPalette.DARK_BLUE.hex()}; font-style: italic; font-weigt: bold;">{self.tag} begins</span>
331
+ <span style="color: #{ColorPalette.DARK_BLUE.hex()}; font-style: italic; font-weight: bold;">{self.tag} begins</span>
332
332
  </div>
333
333
  ''') # noqa
334
334
  status, error_message = self.do_run()