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.
- shinestacker/_version.py +1 -1
- shinestacker/algorithms/align.py +184 -80
- shinestacker/algorithms/align_auto.py +13 -11
- shinestacker/algorithms/align_parallel.py +41 -16
- shinestacker/algorithms/base_stack_algo.py +1 -1
- shinestacker/algorithms/exif.py +252 -6
- shinestacker/algorithms/multilayer.py +6 -4
- shinestacker/algorithms/noise_detection.py +10 -8
- shinestacker/algorithms/pyramid_tiles.py +1 -1
- shinestacker/algorithms/stack.py +25 -13
- shinestacker/algorithms/stack_framework.py +16 -11
- shinestacker/algorithms/utils.py +18 -2
- shinestacker/algorithms/vignetting.py +16 -3
- shinestacker/app/settings_dialog.py +297 -173
- shinestacker/config/constants.py +10 -6
- shinestacker/config/settings.py +25 -7
- shinestacker/core/exceptions.py +1 -1
- shinestacker/core/framework.py +2 -2
- shinestacker/gui/action_config.py +23 -20
- shinestacker/gui/action_config_dialog.py +38 -21
- shinestacker/gui/folder_file_selection.py +3 -2
- shinestacker/gui/gui_images.py +27 -3
- shinestacker/gui/gui_run.py +2 -2
- shinestacker/gui/new_project.py +23 -12
- shinestacker/gui/project_controller.py +13 -6
- shinestacker/gui/project_editor.py +12 -2
- shinestacker/gui/project_model.py +4 -4
- shinestacker/retouch/exif_data.py +3 -0
- shinestacker/retouch/file_loader.py +3 -3
- shinestacker/retouch/io_gui_handler.py +4 -4
- {shinestacker-1.8.0.dist-info → shinestacker-1.9.0.dist-info}/METADATA +37 -39
- {shinestacker-1.8.0.dist-info → shinestacker-1.9.0.dist-info}/RECORD +36 -36
- {shinestacker-1.8.0.dist-info → shinestacker-1.9.0.dist-info}/WHEEL +0 -0
- {shinestacker-1.8.0.dist-info → shinestacker-1.9.0.dist-info}/entry_points.txt +0 -0
- {shinestacker-1.8.0.dist-info → shinestacker-1.9.0.dist-info}/licenses/LICENSE +0 -0
- {shinestacker-1.8.0.dist-info → shinestacker-1.9.0.dist-info}/top_level.txt +0 -0
shinestacker/algorithms/exif.py
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
146
|
-
|
|
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(
|
|
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
|
-
|
|
113
|
-
if not os.path.exists(
|
|
114
|
-
self.print_message(f"create directory: {
|
|
115
|
-
os.mkdir(
|
|
116
|
-
self.
|
|
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(
|
|
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,
|
|
175
|
-
self.process.
|
|
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":
|
|
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)}")
|
shinestacker/algorithms/stack.py
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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,
|
|
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
|
|
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,
|
|
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
|
-
|
|
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
|
|
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.
|
|
321
|
-
color_str("
|
|
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
|
|
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
|
|
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
|
|
shinestacker/algorithms/utils.py
CHANGED
|
@@ -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
|
|
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,
|
|
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
|
-
|
|
186
|
-
|
|
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(
|