shinestacker 1.8.0__py3-none-any.whl → 1.9.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of shinestacker might be problematic. Click here for more details.

Files changed (36) hide show
  1. shinestacker/_version.py +1 -1
  2. shinestacker/algorithms/align.py +184 -80
  3. shinestacker/algorithms/align_auto.py +13 -11
  4. shinestacker/algorithms/align_parallel.py +41 -16
  5. shinestacker/algorithms/base_stack_algo.py +1 -1
  6. shinestacker/algorithms/exif.py +252 -6
  7. shinestacker/algorithms/multilayer.py +6 -4
  8. shinestacker/algorithms/noise_detection.py +10 -8
  9. shinestacker/algorithms/pyramid_tiles.py +1 -1
  10. shinestacker/algorithms/stack.py +25 -13
  11. shinestacker/algorithms/stack_framework.py +16 -11
  12. shinestacker/algorithms/utils.py +18 -2
  13. shinestacker/algorithms/vignetting.py +16 -3
  14. shinestacker/app/settings_dialog.py +297 -173
  15. shinestacker/config/constants.py +10 -6
  16. shinestacker/config/settings.py +25 -7
  17. shinestacker/core/exceptions.py +1 -1
  18. shinestacker/core/framework.py +2 -2
  19. shinestacker/gui/action_config.py +23 -20
  20. shinestacker/gui/action_config_dialog.py +38 -21
  21. shinestacker/gui/folder_file_selection.py +3 -2
  22. shinestacker/gui/gui_images.py +27 -3
  23. shinestacker/gui/gui_run.py +2 -2
  24. shinestacker/gui/new_project.py +23 -12
  25. shinestacker/gui/project_controller.py +13 -6
  26. shinestacker/gui/project_editor.py +12 -2
  27. shinestacker/gui/project_model.py +4 -4
  28. shinestacker/retouch/exif_data.py +3 -0
  29. shinestacker/retouch/file_loader.py +3 -3
  30. shinestacker/retouch/io_gui_handler.py +4 -4
  31. {shinestacker-1.8.0.dist-info → shinestacker-1.9.0.dist-info}/METADATA +37 -39
  32. {shinestacker-1.8.0.dist-info → shinestacker-1.9.0.dist-info}/RECORD +36 -36
  33. {shinestacker-1.8.0.dist-info → shinestacker-1.9.0.dist-info}/WHEEL +0 -0
  34. {shinestacker-1.8.0.dist-info → shinestacker-1.9.0.dist-info}/entry_points.txt +0 -0
  35. {shinestacker-1.8.0.dist-info → shinestacker-1.9.0.dist-info}/licenses/LICENSE +0 -0
  36. {shinestacker-1.8.0.dist-info → shinestacker-1.9.0.dist-info}/top_level.txt +0 -0
@@ -1,12 +1,14 @@
1
- # pylint: disable=C0114, C0116, W0718, R0911, R0912, E1101
1
+ # pylint: disable=C0114, C0116, W0718, R0911, R0912, E1101, R0915, R1702, R0914, R0917, R0913
2
2
  import os
3
3
  import re
4
4
  import io
5
5
  import logging
6
+ import traceback
6
7
  import cv2
7
8
  import numpy as np
8
9
  from PIL import Image
9
10
  from PIL.TiffImagePlugin import IFDRational
11
+ from PIL.PngImagePlugin import PngInfo
10
12
  from PIL.ExifTags import TAGS
11
13
  import tifffile
12
14
  from .. config.constants import constants
@@ -58,9 +60,33 @@ def get_exif(exif_filename):
58
60
  if data is not None:
59
61
  exif_data[XMLPACKET] = data
60
62
  return exif_data
63
+ if extension_png(exif_filename):
64
+ exif_data = get_exif_from_png(image)
65
+ return exif_data if exif_data else image.getexif()
61
66
  return image.getexif()
62
67
 
63
68
 
69
+ def get_exif_from_png(image):
70
+ exif_data = {}
71
+ try:
72
+ exif_from_image = image.getexif()
73
+ if exif_from_image:
74
+ exif_data.update(dict(exif_from_image))
75
+ except Exception:
76
+ pass
77
+ try:
78
+ if hasattr(image, 'text') and image.text:
79
+ for key, value in image.text.items():
80
+ exif_data[f"PNG_{key}"] = value
81
+ if hasattr(image, 'info') and image.info:
82
+ for key, value in image.info.items():
83
+ if key not in ['dpi', 'gamma']:
84
+ exif_data[f"PNG_{key}"] = value
85
+ except Exception:
86
+ pass
87
+ return exif_data
88
+
89
+
64
90
  def exif_extra_tags_for_tif(exif):
65
91
  logger = logging.getLogger(__name__)
66
92
  res_x, res_y = exif.get(RESOLUTIONX), exif.get(RESOLUTIONY)
@@ -153,7 +179,222 @@ def add_exif_data_to_jpg_file(exif, in_filenama, out_filename, verbose=False):
153
179
  return exif
154
180
 
155
181
 
156
- def write_image_with_exif_data(exif, image, out_filename, verbose=False):
182
+ def create_xmp_from_exif(exif_data):
183
+ xmp_elements = []
184
+ if exif_data:
185
+ for tag_id, value in exif_data.items():
186
+ if isinstance(tag_id, int):
187
+ if tag_id == 270 and value: # ImageDescription
188
+ desc = value
189
+ if isinstance(desc, bytes):
190
+ desc = desc.decode('utf-8', errors='ignore')
191
+ xmp_elements.append(
192
+ f'<dc:description><rdf:Alt><rdf:li xml:lang="x-default">{desc}</rdf:li>'
193
+ '</rdf:Alt></dc:description>')
194
+ elif tag_id == 315 and value: # Artist
195
+ artist = value
196
+ if isinstance(artist, bytes):
197
+ artist = artist.decode('utf-8', errors='ignore')
198
+ xmp_elements.append(
199
+ f'<dc:creator><rdf:Seq><rdf:li>{artist}</rdf:li>'
200
+ '</rdf:Seq></dc:creator>')
201
+ elif tag_id == 33432 and value: # Copyright
202
+ copyright_tag = value
203
+ if isinstance(copyright_tag, bytes):
204
+ copyright_tag = copyright_tag.decode('utf-8', errors='ignore')
205
+ xmp_elements.append(
206
+ f'<dc:rights><rdf:Alt><rdf:li xml:lang="x-default">{copyright_tag}</rdf:li>'
207
+ '</rdf:Alt></dc:rights>')
208
+ elif tag_id == 271 and value: # Make
209
+ make = value
210
+ if isinstance(make, bytes):
211
+ make = make.decode('utf-8', errors='ignore')
212
+ xmp_elements.append(f'<tiff:Make>{make}</tiff:Make>')
213
+ elif tag_id == 272 and value: # Model
214
+ model = value
215
+ if isinstance(model, bytes):
216
+ model = model.decode('utf-8', errors='ignore')
217
+ xmp_elements.append(f'<tiff:Model>{model}</tiff:Model>')
218
+ elif tag_id == 306 and value: # DateTime
219
+ datetime_val = value
220
+ if isinstance(datetime_val, bytes):
221
+ datetime_val = datetime_val.decode('utf-8', errors='ignore')
222
+ if ':' in datetime_val:
223
+ datetime_val = datetime_val.replace(':', '-', 2).replace(' ', 'T')
224
+ xmp_elements.append(f'<xmp:CreateDate>{datetime_val}</xmp:CreateDate>')
225
+ elif tag_id == 305 and value: # Software
226
+ software = value
227
+ if isinstance(software, bytes):
228
+ software = software.decode('utf-8', errors='ignore')
229
+ xmp_elements.append(f'<xmp:CreatorTool>{software}</xmp:CreatorTool>')
230
+ if xmp_elements:
231
+ xmp_content = '\n '.join(xmp_elements)
232
+ xmp_template = f"""<?xpacket begin='' id='W5M0MpCehiHzreSzNTczkc9d'?>
233
+ <x:xmpmeta xmlns:x='adobe:ns:meta/'
234
+ x:xmptk='Adobe XMP Core 5.6-c140 79.160451, 2017/05/06-01:08:21'>
235
+ <rdf:RDF xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns#'>
236
+ <rdf:Description rdf:about=''
237
+ xmlns:dc='http://purl.org/dc/elements/1.1/'
238
+ xmlns:xmp='http://ns.adobe.com/xap/1.0/'
239
+ xmlns:tiff='http://ns.adobe.com/tiff/1.0/'
240
+ xmlns:exif='http://ns.adobe.com/exif/1.0/'>
241
+ {xmp_content}
242
+ </rdf:Description>
243
+ </rdf:RDF>
244
+ </x:xmpmeta>
245
+ <?xpacket end='w'?>"""
246
+ return xmp_template
247
+ return """<?xpacket begin='' id='W5M0MpCehiHzreSzNTczkc9d'?>
248
+ <x:xmpmeta xmlns:x='adobe:ns:meta/'
249
+ x:xmptk='Adobe XMP Core 5.6-c140 79.160451, 2017/05/06-01:08:21'>
250
+ <rdf:RDF xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns#'>
251
+ <rdf:Description rdf:about=''/>
252
+ </rdf:RDF>
253
+ </x:xmpmeta>
254
+ <?xpacket end='w'?>"""
255
+
256
+
257
+ def write_image_with_exif_data_png(exif, image, out_filename, verbose=False, color_order='auto'):
258
+ logger = logging.getLogger(__name__)
259
+ if isinstance(image, np.ndarray) and image.dtype == np.uint16:
260
+ if verbose:
261
+ logger.warning(msg="EXIF data not supported for 16-bit PNG format")
262
+ write_img(out_filename, image)
263
+ return
264
+ pil_image = _convert_to_pil_image(image, color_order, verbose, logger)
265
+ pnginfo, icc_profile = _prepare_png_metadata(exif, verbose, logger)
266
+ _save_png_with_metadata(pil_image, out_filename, pnginfo, icc_profile, verbose, logger)
267
+
268
+
269
+ def _convert_to_pil_image(image, color_order, verbose, logger):
270
+ if isinstance(image, np.ndarray):
271
+ if len(image.shape) == 3 and image.shape[2] == 3:
272
+ if color_order in ['auto', 'bgr']:
273
+ image_rgb = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
274
+ if verbose:
275
+ logger.info(msg="Converted BGR to RGB for PIL")
276
+ return Image.fromarray(image_rgb)
277
+ return Image.fromarray(image)
278
+ return image
279
+
280
+
281
+ def _prepare_png_metadata(exif, verbose, logger):
282
+ pnginfo = PngInfo()
283
+ icc_profile = None
284
+ xmp_data = _extract_xmp_data(exif, verbose, logger)
285
+ if xmp_data:
286
+ pnginfo.add_text("XML:com.adobe.xmp", xmp_data)
287
+ if verbose:
288
+ logger.info(msg="Added XMP data to PNG info")
289
+ _add_exif_tags_to_pnginfo(exif, pnginfo, verbose, logger)
290
+ icc_profile = _extract_icc_profile(exif, verbose, logger)
291
+ return pnginfo, icc_profile
292
+
293
+
294
+ def _extract_xmp_data(exif, verbose, logger):
295
+ for key, value in exif.items():
296
+ if isinstance(key, str) and ('xmp' in key.lower() or 'xml' in key.lower()):
297
+ if isinstance(value, bytes):
298
+ try:
299
+ xmp_data = value.decode('utf-8', errors='ignore')
300
+ if verbose:
301
+ logger.info(msg=f"Found existing XMP data in source: {key}")
302
+ return xmp_data
303
+ except Exception:
304
+ continue
305
+ elif isinstance(value, str):
306
+ if verbose:
307
+ logger.info(msg=f"Found existing XMP data in source: {key}")
308
+ return value
309
+ if verbose:
310
+ logger.info("Generated new XMP data from EXIF")
311
+ return create_xmp_from_exif(exif)
312
+
313
+
314
+ def _add_exif_tags_to_pnginfo(exif, pnginfo, verbose, logger):
315
+ for tag_id, value in exif.items():
316
+ if value is None:
317
+ continue
318
+ if isinstance(tag_id, int):
319
+ _add_exif_tag(pnginfo, tag_id, value, verbose, logger)
320
+ elif isinstance(tag_id, str) and not tag_id.lower().startswith(('xmp', 'xml')):
321
+ _add_png_text_tag(pnginfo, tag_id, value, verbose, logger)
322
+
323
+
324
+ def _add_exif_tag(pnginfo, tag_id, value, verbose, logger):
325
+ try:
326
+ tag_name = TAGS.get(tag_id, f"Unknown_{tag_id}")
327
+ if isinstance(value, bytes) and len(value) > 1000:
328
+ return
329
+ if isinstance(value, (int, float, str)):
330
+ pnginfo.add_text(tag_name, str(value))
331
+ elif isinstance(value, bytes):
332
+ try:
333
+ decoded_value = value.decode('utf-8', errors='replace')
334
+ pnginfo.add_text(tag_name, decoded_value)
335
+ except Exception:
336
+ pass
337
+ elif hasattr(value, 'numerator'): # IFDRational
338
+ rational_str = f"{value.numerator}/{value.denominator}"
339
+ pnginfo.add_text(tag_name, rational_str)
340
+ else:
341
+ pnginfo.add_text(tag_name, str(value))
342
+ except Exception as e:
343
+ if verbose:
344
+ logger.warning(f"Could not store EXIF tag {tag_id}: {e}")
345
+
346
+
347
+ def _add_png_text_tag(pnginfo, key, value, verbose, logger):
348
+ try:
349
+ clean_key = key[4:] if key.startswith('PNG_') else key
350
+ if 'icc' in clean_key.lower() or 'profile' in clean_key.lower():
351
+ return
352
+ if isinstance(value, bytes):
353
+ try:
354
+ decoded_value = value.decode('utf-8', errors='replace')
355
+ pnginfo.add_text(clean_key, decoded_value)
356
+ except Exception:
357
+ truncated_value = str(value)[:100] + "..."
358
+ pnginfo.add_text(clean_key, truncated_value)
359
+ else:
360
+ pnginfo.add_text(clean_key, str(value))
361
+ except Exception as e:
362
+ if verbose:
363
+ logger.warning(msg=f"Could not store PNG metadata {key}: {e}")
364
+
365
+
366
+ def _extract_icc_profile(exif, verbose, logger):
367
+ for key, value in exif.items():
368
+ if (isinstance(key, str) and
369
+ isinstance(value, bytes) and
370
+ ('icc' in key.lower() or 'profile' in key.lower())):
371
+ if verbose:
372
+ logger.info(f"Found ICC profile: {key}")
373
+ return value
374
+ return None
375
+
376
+
377
+ def _save_png_with_metadata(pil_image, out_filename, pnginfo, icc_profile, verbose, logger):
378
+ try:
379
+ save_args = {'format': 'PNG', 'pnginfo': pnginfo}
380
+ if icc_profile:
381
+ save_args['icc_profile'] = icc_profile
382
+ if verbose:
383
+ logger.info(msg="Saved PNG with ICC profile and metadata")
384
+ else:
385
+ if verbose:
386
+ logger.info(msg="Saved PNG without ICC profile but with metadata")
387
+ pil_image.save(out_filename, **save_args)
388
+ if verbose:
389
+ logger.info(msg=f"Successfully wrote PNG with metadata: {out_filename}")
390
+ except Exception as e:
391
+ if verbose:
392
+ logger.error(msg=f"Failed to write PNG with metadata: {e}")
393
+ logger.error(traceback.format_exc())
394
+ pil_image.save(out_filename, format='PNG')
395
+
396
+
397
+ def write_image_with_exif_data(exif, image, out_filename, verbose=False, color_order='auto'):
157
398
  if exif is None:
158
399
  write_img(out_filename, image)
159
400
  return None
@@ -168,7 +409,7 @@ def write_image_with_exif_data(exif, image, out_filename, verbose=False):
168
409
  tifffile.imwrite(out_filename, image, metadata=metadata, compression='adobe_deflate',
169
410
  extratags=extra_tags, **exif_tags)
170
411
  elif extension_png(out_filename):
171
- image.save(out_filename, 'PNG', exif=exif, quality=100)
412
+ write_image_with_exif_data_png(exif, image, out_filename, verbose, color_order=color_order)
172
413
  return exif
173
414
 
174
415
 
@@ -181,8 +422,10 @@ def save_exif_data(exif, in_filename, out_filename=None, verbose=False):
181
422
  print_exif(exif)
182
423
  if extension_tif(in_filename):
183
424
  image_new = tifffile.imread(in_filename)
184
- else:
425
+ elif extension_jpg(in_filename):
185
426
  image_new = Image.open(in_filename)
427
+ elif extension_png(in_filename):
428
+ image_new = cv2.imread(in_filename, cv2.IMREAD_UNCHANGED)
186
429
  if extension_jpg(in_filename):
187
430
  add_exif_data_to_jpg_file(exif, in_filename, out_filename, verbose)
188
431
  elif extension_tif(in_filename):
@@ -191,7 +434,7 @@ def save_exif_data(exif, in_filename, out_filename=None, verbose=False):
191
434
  tifffile.imwrite(out_filename, image_new, metadata=metadata, compression='adobe_deflate',
192
435
  extratags=extra_tags, **exif_tags)
193
436
  elif extension_png(in_filename):
194
- image_new.save(out_filename, 'PNG', exif=exif, quality=100)
437
+ write_image_with_exif_data_png(exif, image_new, out_filename, verbose)
195
438
  return exif
196
439
 
197
440
 
@@ -237,4 +480,7 @@ def print_exif(exif, hide_xml=True):
237
480
  for tag, (tag_id, data) in exif_data.items():
238
481
  if isinstance(data, IFDRational):
239
482
  data = f"{data.numerator}/{data.denominator}"
240
- logger.info(msg=f"{tag:25} [#{tag_id:5d}]: {data}")
483
+ if isinstance(tag_id, int):
484
+ logger.info(msg=f"{tag:25} [#{tag_id:5d}]: {data}")
485
+ else:
486
+ logger.info(msg=f"{tag:25} [ {tag_id:20} ]: {str(data)[:100]}...")
@@ -13,7 +13,7 @@ from .. config.constants import constants
13
13
  from .. config.config import config
14
14
  from .. core.colors import color_str
15
15
  from .. core.framework import TaskBase
16
- from .utils import EXTENSIONS_TIF, EXTENSIONS_JPG, EXTENSIONS_PNG
16
+ from .utils import EXTENSIONS_TIF, EXTENSIONS_JPG, EXTENSIONS_PNG, EXTENSIONS_SUPPORTED
17
17
  from .stack_framework import ImageSequenceManager
18
18
  from .exif import exif_extra_tags_for_tif, get_exif
19
19
 
@@ -142,14 +142,16 @@ def write_multilayer_tiff_from_images(image_dict, output_file, exif_path='', cal
142
142
  elif os.path.isdir(exif_path):
143
143
  _dirpath, _, fnames = next(os.walk(exif_path))
144
144
  fnames = [name for name in fnames
145
- if os.path.splitext(name)[-1][1:].lower() in constants.EXTENSIONS]
146
- extra_tags, exif_tags = exif_extra_tags_for_tif(get_exif(exif_path + '/' + fnames[0]))
145
+ if os.path.splitext(name)[-1][1:].lower() in EXTENSIONS_SUPPORTED]
146
+ file_path = os.path.join(exif_path, fnames[0])
147
+ extra_tags, exif_tags = exif_extra_tags_for_tif(get_exif(file_path))
148
+ extra_tags = [tag for tag in extra_tags if isinstance(tag[0], int)]
147
149
  tiff_tags['extratags'] += extra_tags
148
150
  tiff_tags = {**tiff_tags, **exif_tags}
149
151
  if callbacks:
150
152
  callback = callbacks.get('write_msg', None)
151
153
  if callback:
152
- callback(output_file.split('/')[-1])
154
+ callback(os.path.basename(output_file))
153
155
  compression = 'adobe_deflate'
154
156
  overlayed_images = overlay(
155
157
  *((np.concatenate((image, np.expand_dims(transp, axis=-1)),
@@ -109,13 +109,14 @@ class NoiseDetection(TaskBase, ImageSequenceManager):
109
109
  {'rgb': 'black', 'r': 'red', 'g': 'green', 'b': 'blue'}[ch])
110
110
  msg.append(hpx)
111
111
  self.print_message(color_str("hot pixels: " + ", ".join(msg), constants.LOG_COLOR_LEVEL_2))
112
- path = "/".join(self.file_name.split("/")[:-1])
113
- if not os.path.exists(f"{self.working_path}/{path}"):
114
- self.print_message(f"create directory: {path}")
115
- os.mkdir(f"{self.working_path}/{path}")
116
- self.print_message(color_str(f"writing hot pixels map file: {self.file_name}",
112
+ output_full_path = os.path.join(self.working_path, self.output_path)
113
+ if not os.path.exists(output_full_path):
114
+ self.print_message(f"create directory: {self.output_path}")
115
+ os.mkdir(output_full_path)
116
+ file_path = os.path.join(self.output_path, self.file_name)
117
+ self.print_message(color_str(f"writing hot pixels map file: {file_path}",
117
118
  constants.LOG_COLOR_LEVEL_2))
118
- cv2.imwrite(f"{self.working_path}/{self.file_name}", hot_rgb)
119
+ cv2.imwrite(os.path.join(output_full_path, self.file_name), hot_rgb)
119
120
  plot_range = self.plot_range
120
121
  min_th, max_th = min(self.channel_thresholds), max(self.channel_thresholds)
121
122
  if min_th < plot_range[0]:
@@ -171,8 +172,9 @@ class MaskNoise(SubAction):
171
172
  else:
172
173
  raise ImageLoadError(path, "file not found.")
173
174
 
174
- def run_frame(self, _idx, _ref_idx, image):
175
- self.process.sub_message_r(color_str(': mask noisy pixels', constants.LOG_COLOR_LEVEL_3))
175
+ def run_frame(self, idx, _ref_idx, image):
176
+ self.process.print_message(color_str(
177
+ f'{self.process.frame_str(idx)}: mask noisy pixels', constants.LOG_COLOR_LEVEL_3))
176
178
  shape = image.shape[:2]
177
179
  if shape != self.expected_shape:
178
180
  raise ShapeError(self.expected_shape, shape)
@@ -222,7 +222,7 @@ class PyramidTilesStack(PyramidBase):
222
222
  all_level_counts[img_index] = level_count
223
223
  completed_count += 1
224
224
  self.print_message(
225
- f": processing completed, {self.image_str(completed_count - 1)}")
225
+ f": preprocessing completed, {self.image_str(completed_count - 1)}")
226
226
  except Exception as e:
227
227
  self.print_message(
228
228
  f"Error processing {self.image_str(i)}: {str(e)}")
@@ -1,11 +1,13 @@
1
1
  # pylint: disable=C0114, C0115, C0116, R0913, R0917
2
2
  import os
3
3
  import traceback
4
+ import logging
5
+ import numpy as np
4
6
  from .. config.constants import constants
5
7
  from .. core.framework import TaskBase
6
8
  from .. core.colors import color_str
7
9
  from .. core.exceptions import InvalidOptionError
8
- from .utils import write_img, extension_tif_jpg
10
+ from .utils import write_img, extension_supported
9
11
  from .stack_framework import ImageSequenceManager, SequentialTask
10
12
  from .exif import copy_exif_from_file_to_file
11
13
  from .denoise import denoise
@@ -35,18 +37,28 @@ class FocusStackBase(TaskBase, ImageSequenceManager):
35
37
  stacked_img = denoise(stacked_img, self.denoise_amount, self.denoise_amount)
36
38
  write_img(out_filename, stacked_img)
37
39
  if self.exif_path != '':
38
- self.sub_message_r(color_str(': copy exif data', constants.LOG_COLOR_LEVEL_3))
39
- if not os.path.exists(self.exif_path):
40
- raise RuntimeError(f"Path {self.exif_path} does not exist.")
41
- try:
42
- _dirpath, _, fnames = next(os.walk(self.exif_path))
43
- fnames = [name for name in fnames if extension_tif_jpg(name)]
44
- exif_filename = os.path.join(self.exif_path, fnames[0])
45
- copy_exif_from_file_to_file(exif_filename, out_filename)
46
- self.sub_message_r(' ' * 60)
47
- except Exception as e:
48
- traceback.print_tb(e.__traceback__)
49
- raise RuntimeError("Can't copy EXIF data") from e
40
+ if stacked_img.dtype == np.uint16 and \
41
+ os.path.splitext(out_filename)[-1].lower() == '.png':
42
+ self.sub_message_r(color_str(': exif not supported for 16-bit PNG format',
43
+ constants.LOG_COLOR_WARNING),
44
+ level=logging.WARNING)
45
+ else:
46
+ self.sub_message_r(color_str(': copy exif data', constants.LOG_COLOR_LEVEL_3))
47
+ if not os.path.exists(self.exif_path):
48
+ raise RuntimeError(f"path {self.exif_path} does not exist.")
49
+ try:
50
+ _dirpath, _, fnames = next(os.walk(self.exif_path))
51
+ fnames = [name for name in fnames if extension_supported(name)]
52
+ if len(fnames) == 0:
53
+ raise RuntimeError(f"path {self.exif_path} does not contain image files.")
54
+ exif_filename = os.path.join(self.exif_path, fnames[0])
55
+ copy_exif_from_file_to_file(exif_filename, out_filename)
56
+ self.sub_message_r(' ' * 60)
57
+ except Exception as e:
58
+ traceback.print_tb(e.__traceback__)
59
+ self.sub_message_r(color_str(f': failed to copy EXIF data: {str(e)}',
60
+ constants.LOG_COLOR_WARNING),
61
+ level=logging.WARNING)
50
62
  if self.plot_stack:
51
63
  idx_str = f"{self.frame_count + 1:04d}" if self.frame_count >= 0 else ''
52
64
  name = f"{self.name}: {self.stack_algo.name()}"
@@ -7,7 +7,7 @@ from .. core.colors import color_str
7
7
  from .. core.framework import Job, SequentialTask
8
8
  from .. core.core_utils import check_path_exists
9
9
  from .. core.exceptions import RunStopException
10
- from .utils import read_img, write_img, extension_tif_jpg, get_img_metadata, validate_image
10
+ from .utils import read_img, write_img, extension_supported, get_img_metadata, validate_image
11
11
 
12
12
 
13
13
  class StackJob(Job):
@@ -95,7 +95,7 @@ class ImageSequenceManager:
95
95
  filelist = []
96
96
  for _dirpath, _, filenames in os.walk(d):
97
97
  filelist = [os.path.join(_dirpath, name)
98
- for name in filenames if extension_tif_jpg(name)]
98
+ for name in filenames if extension_supported(name)]
99
99
  filelist.sort()
100
100
  if self.reverse_order:
101
101
  filelist.reverse()
@@ -191,7 +191,8 @@ class ImageSequenceManager:
191
191
 
192
192
 
193
193
  class ReferenceFrameTask(SequentialTask, ImageSequenceManager):
194
- def __init__(self, name, enabled=True, reference_index=0, step_process=False, **kwargs):
194
+ def __init__(self, name, enabled=True, reference_index=0,
195
+ step_process=constants.DEFAULT_COMBINED_ACTIONS_STEP_PROCESS, **kwargs):
195
196
  ImageSequenceManager.__init__(self, name, **kwargs)
196
197
  SequentialTask.__init__(self, name, enabled)
197
198
  self.ref_idx = reference_index
@@ -276,7 +277,8 @@ class SubAction:
276
277
 
277
278
  class CombinedActions(ReferenceFrameTask):
278
279
  def __init__(self, name, actions=[], enabled=True, **kwargs):
279
- ReferenceFrameTask.__init__(self, name, enabled, **kwargs)
280
+ step_process = kwargs.pop('step_process', constants.DEFAULT_COMBINED_ACTIONS_STEP_PROCESS)
281
+ ReferenceFrameTask.__init__(self, name, enabled, step_process=step_process, **kwargs)
280
282
  self._actions = actions
281
283
  self._metadata = (None, None)
282
284
 
@@ -294,11 +296,15 @@ class CombinedActions(ReferenceFrameTask):
294
296
  self._metadata = get_img_metadata(img)
295
297
  return img
296
298
 
299
+ def frame_str(self, idx=-1):
300
+ if self.run_sequential():
301
+ idx = self.current_action_count
302
+ return f"frame {idx + 1}/{self.total_action_counts}"
303
+
297
304
  def run_frame(self, idx, ref_idx):
298
305
  input_path = self.input_filepath(idx)
299
306
  self.print_message(
300
- color_str(f'read input image '
301
- f'{idx + 1}/{self.total_action_counts}, '
307
+ color_str(f'read input {self.frame_str(idx)}, '
302
308
  f'{os.path.basename(input_path)}', constants.LOG_COLOR_LEVEL_3))
303
309
  img = read_img(input_path)
304
310
  validate_image(img, *(self._metadata))
@@ -317,20 +323,19 @@ class CombinedActions(ReferenceFrameTask):
317
323
  if img is not None:
318
324
  img = a.run_frame(idx, ref_idx, img)
319
325
  else:
320
- self.sub_message(
321
- color_str(": null input received, action skipped",
326
+ self.print_message(
327
+ color_str("null input received, action skipped",
322
328
  constants.LOG_COLOR_ALERT),
323
329
  level=logging.WARNING)
324
330
  if img is not None:
325
331
  output_path = os.path.join(self.output_full_path(), os.path.basename(input_path))
326
332
  self.print_message(
327
- color_str(f'write output image '
328
- f'{idx + 1}/{self.total_action_counts}, '
333
+ color_str(f'write output {self.frame_str(idx)}, '
329
334
  f'{os.path.basename(output_path)}', constants.LOG_COLOR_LEVEL_3))
330
335
  write_img(output_path, img)
331
336
  return img
332
337
  self.print_message(color_str(
333
- f"no output file resulted from processing input file: {os.path.basename(input_path)}",
338
+ f"no output resulted from processing input file: {os.path.basename(input_path)}",
334
339
  constants.LOG_COLOR_ALERT), level=logging.WARNING)
335
340
  return None
336
341
 
@@ -18,6 +18,15 @@ EXTENSIONS_TIF = ['tif', 'tiff']
18
18
  EXTENSIONS_JPG = ['jpg', 'jpeg']
19
19
  EXTENSIONS_PNG = ['png']
20
20
  EXTENSIONS_PDF = ['pdf']
21
+ EXTENSIONS_SUPPORTED = EXTENSIONS_TIF + EXTENSIONS_JPG + EXTENSIONS_PNG
22
+ EXTENSIONS_GUI_STR = " ".join([f"*.{ext}" for ext in EXTENSIONS_SUPPORTED])
23
+ EXTENSION_GUI_TIF = " ".join([f"*.{ext}" for ext in EXTENSIONS_TIF])
24
+ EXTENSION_GUI_JPG = " ".join([f"*.{ext}" for ext in EXTENSIONS_JPG])
25
+ EXTENSION_GUI_PNG = " ".join([f"*.{ext}" for ext in EXTENSIONS_PNG])
26
+ EXTENSIONS_GUI_SAVE_STR = f"TIFF Files ({EXTENSION_GUI_TIF});;" \
27
+ f"JPEG Files ({EXTENSION_GUI_JPG});;" \
28
+ f"PNG Files ({EXTENSION_GUI_PNG});;" \
29
+ "All Files (*)"
21
30
 
22
31
 
23
32
  def extension_in(path, exts):
@@ -56,6 +65,10 @@ def extension_jpg_tif_png(path):
56
65
  return extension_in(path, EXTENSIONS_JPG + EXTENSIONS_TIF + EXTENSIONS_PNG)
57
66
 
58
67
 
68
+ def extension_supported(path):
69
+ return extension_in(path, EXTENSIONS_SUPPORTED)
70
+
71
+
59
72
  def read_img(file_path):
60
73
  if not os.path.isfile(file_path):
61
74
  raise RuntimeError("File does not exist: " + file_path)
@@ -73,7 +86,10 @@ def write_img(file_path, img):
73
86
  elif extension_tif(file_path):
74
87
  cv2.imwrite(file_path, img, [int(cv2.IMWRITE_TIFF_COMPRESSION), 1])
75
88
  elif extension_png(file_path):
76
- cv2.imwrite(file_path, img)
89
+ cv2.imwrite(file_path, img, [
90
+ int(cv2.IMWRITE_PNG_COMPRESSION), 9,
91
+ int(cv2.IMWRITE_PNG_STRATEGY), cv2.IMWRITE_PNG_STRATEGY_HUFFMAN_ONLY
92
+ ])
77
93
 
78
94
 
79
95
  def img_8bit(img):
@@ -96,7 +112,7 @@ def img_bw(img):
96
112
  def get_first_image_file(filenames):
97
113
  first_img_file = None
98
114
  for filename in filenames:
99
- if os.path.isfile(filename) and extension_tif_jpg(filename):
115
+ if os.path.isfile(filename) and extension_supported(filename):
100
116
  first_img_file = filename
101
117
  break
102
118
  if first_img_file is None:
@@ -4,7 +4,7 @@ import traceback
4
4
  import logging
5
5
  import numpy as np
6
6
  import matplotlib.pyplot as plt
7
- from scipy.optimize import curve_fit, fsolve
7
+ from scipy.optimize import curve_fit, bisect
8
8
  import cv2
9
9
  from .. core.colors import color_str
10
10
  from .. core.core_utils import setup_matplotlib_mode
@@ -181,9 +181,22 @@ class Vignetting(SubAction):
181
181
  self.process.callback(
182
182
  constants.CALLBACK_SAVE_PLOT, self.process.id,
183
183
  f"{self.process.name}: intensity\nframe {idx_str}", plot_path)
184
+
184
185
  for i, p in enumerate(self.percentiles):
185
- self.corrections[i][idx] = fsolve(lambda x: sigmoid_model(x, *params) /
186
- self.v0 - p, r0_fit)[0]
186
+ s1 = sigmoid_model(0, *params) / self.v0
187
+ s2 = sigmoid_model(self.r_max, *params) / self.v0
188
+ if s1 > p > s2:
189
+ try:
190
+ c = bisect(lambda x: sigmoid_model(x, *params) / self.v0 - p, 0, self.r_max)
191
+ except Exception as e:
192
+ traceback.print_tb(e.__traceback__)
193
+ self.process.sub_message(color_str(f": {str(e).lower()}", "yellow"),
194
+ level=logging.WARNING)
195
+ elif s1 <= p:
196
+ c = 0
197
+ else:
198
+ c = self.r_max
199
+ self.corrections[i][idx] = c
187
200
  self.process.print_message(
188
201
  color_str(f"{self.process.idx_tot_str(idx)}: correct vignetting", "cyan"))
189
202
  return correct_vignetting(