numpyimage 2.2.0__tar.gz → 2.4.0__tar.gz
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.
- {numpyimage-2.2.0 → numpyimage-2.4.0}/PKG-INFO +2 -1
- {numpyimage-2.2.0 → numpyimage-2.4.0}/npimage/core.py +149 -19
- {numpyimage-2.2.0 → numpyimage-2.4.0}/npimage/operations.py +13 -6
- {numpyimage-2.2.0 → numpyimage-2.4.0}/numpyimage.egg-info/PKG-INFO +2 -1
- {numpyimage-2.2.0 → numpyimage-2.4.0}/numpyimage.egg-info/SOURCES.txt +1 -3
- {numpyimage-2.2.0 → numpyimage-2.4.0}/numpyimage.egg-info/requires.txt +1 -0
- {numpyimage-2.2.0 → numpyimage-2.4.0}/pyproject.toml +2 -1
- numpyimage-2.4.0/tests/test_heic.py +61 -0
- numpyimage-2.2.0/npimage/align_scratch.py +0 -50
- numpyimage-2.2.0/npimage/filetypes/__init__.py +0 -1
- numpyimage-2.2.0/npimage/filetypes/pbm.py +0 -103
- {numpyimage-2.2.0 → numpyimage-2.4.0}/LICENSE +0 -0
- {numpyimage-2.2.0 → numpyimage-2.4.0}/README.md +0 -0
- {numpyimage-2.2.0 → numpyimage-2.4.0}/npimage/__init__.py +0 -0
- {numpyimage-2.2.0 → numpyimage-2.4.0}/npimage/align.py +0 -0
- {numpyimage-2.2.0 → numpyimage-2.4.0}/npimage/graphics.py +0 -0
- {numpyimage-2.2.0 → numpyimage-2.4.0}/npimage/nrrd_utils.py +0 -0
- {numpyimage-2.2.0 → numpyimage-2.4.0}/npimage/utils.py +0 -0
- {numpyimage-2.2.0 → numpyimage-2.4.0}/numpyimage.egg-info/dependency_links.txt +0 -0
- {numpyimage-2.2.0 → numpyimage-2.4.0}/numpyimage.egg-info/top_level.txt +0 -0
- {numpyimage-2.2.0 → numpyimage-2.4.0}/setup.cfg +0 -0
- {numpyimage-2.2.0 → numpyimage-2.4.0}/tests/test_pbm.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: numpyimage
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.4.0
|
|
4
4
|
Summary: Load, save, & manipulate image files as numpy arrays
|
|
5
5
|
Author-email: Jasper Phelps <jasper.s.phelps@gmail.com>
|
|
6
6
|
License: GNU GENERAL PUBLIC LICENSE
|
|
@@ -690,6 +690,7 @@ Description-Content-Type: text/markdown
|
|
|
690
690
|
License-File: LICENSE
|
|
691
691
|
Requires-Dist: numpy
|
|
692
692
|
Requires-Dist: pillow
|
|
693
|
+
Requires-Dist: pillow-heif
|
|
693
694
|
Requires-Dist: tifffile
|
|
694
695
|
Requires-Dist: pynrrd
|
|
695
696
|
Requires-Dist: matplotlib
|
|
@@ -20,9 +20,21 @@ from . import operations, utils
|
|
|
20
20
|
|
|
21
21
|
supported_extensions = [
|
|
22
22
|
'tif', 'tiff', 'jpg', 'jpeg', 'png', 'pbm',
|
|
23
|
-
'nrrd', 'zarr', 'raw', 'vol', 'ng'
|
|
23
|
+
'nrrd', 'zarr', 'raw', 'vol', 'ng', 'heic'
|
|
24
24
|
]
|
|
25
25
|
|
|
26
|
+
# Flag to track if HEIF opener has been registered
|
|
27
|
+
_heif_opener_registered = False
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _ensure_heif_opener_registered():
|
|
31
|
+
"""Register the HEIF opener if not already registered."""
|
|
32
|
+
global _heif_opener_registered
|
|
33
|
+
if not _heif_opener_registered:
|
|
34
|
+
from pillow_heif import register_heif_opener
|
|
35
|
+
register_heif_opener()
|
|
36
|
+
_heif_opener_registered = True
|
|
37
|
+
|
|
26
38
|
|
|
27
39
|
def load(filename, dim_order='zyx', **kwargs):
|
|
28
40
|
"""
|
|
@@ -33,7 +45,7 @@ def load(filename, dim_order='zyx', **kwargs):
|
|
|
33
45
|
images, or yxc for multi-channel (RGB/RGBA) 2D images.
|
|
34
46
|
Set dim_order='xy' if you want to reverse the order of the axes.
|
|
35
47
|
"""
|
|
36
|
-
filename = str(filename)
|
|
48
|
+
filename = os.path.expanduser(str(filename))
|
|
37
49
|
while filename.endswith('/'):
|
|
38
50
|
filename = filename[:-1]
|
|
39
51
|
if 'format' in kwargs:
|
|
@@ -45,7 +57,7 @@ def load(filename, dim_order='zyx', **kwargs):
|
|
|
45
57
|
f' "{filename}". Please specify the file type via'
|
|
46
58
|
' the `format` argument, e.g. format="tif"')
|
|
47
59
|
if extension not in supported_extensions:
|
|
48
|
-
raise ValueError(f'File format "{
|
|
60
|
+
raise ValueError(f'File format of "{filename}" not supported/recognized.')
|
|
49
61
|
|
|
50
62
|
data = None
|
|
51
63
|
metadata = None
|
|
@@ -54,6 +66,11 @@ def load(filename, dim_order='zyx', **kwargs):
|
|
|
54
66
|
from PIL import Image
|
|
55
67
|
data = np.array(Image.open(filename)) #PIL.Image.open returns zyx order
|
|
56
68
|
|
|
69
|
+
if extension == 'heic':
|
|
70
|
+
_ensure_heif_opener_registered()
|
|
71
|
+
from PIL import Image
|
|
72
|
+
data = np.array(Image.open(filename)) #PIL.Image.open returns zyx order
|
|
73
|
+
|
|
57
74
|
if extension in ['tif', 'tiff']:
|
|
58
75
|
import tifffile
|
|
59
76
|
data = tifffile.imread(filename) #tifffile.imread returns zyx order
|
|
@@ -161,7 +178,7 @@ def save(data,
|
|
|
161
178
|
Currently `pixel_size`, `units`, and `compress` are only recognized
|
|
162
179
|
when saving to .nrrd and .ng files, and ignored otherwise.
|
|
163
180
|
"""
|
|
164
|
-
filename = str(filename)
|
|
181
|
+
filename = os.path.expanduser(str(filename))
|
|
165
182
|
filename = filename.rstrip('/')
|
|
166
183
|
if os.path.exists(filename) and not overwrite:
|
|
167
184
|
raise FileExistsError(f'File {filename} already exists. '
|
|
@@ -195,6 +212,11 @@ def save(data,
|
|
|
195
212
|
from PIL import Image # pip install pillow
|
|
196
213
|
Image.fromarray(data).save(filename)
|
|
197
214
|
|
|
215
|
+
if extension == 'heic':
|
|
216
|
+
_ensure_heif_opener_registered()
|
|
217
|
+
from PIL import Image
|
|
218
|
+
Image.fromarray(data).save(filename)
|
|
219
|
+
|
|
198
220
|
if extension == 'pbm':
|
|
199
221
|
from .filetypes import pbm
|
|
200
222
|
pbm.save(data, filename, comments=metadata)
|
|
@@ -352,10 +374,10 @@ write = save # Function name alias
|
|
|
352
374
|
to_file = save # Function name alias
|
|
353
375
|
|
|
354
376
|
|
|
355
|
-
def save_video(data, filename, time_axis=0,
|
|
356
|
-
framerate=30, crf=23, compression_speed='medium'):
|
|
377
|
+
def save_video(data, filename, time_axis=0, color_axis=None, overwrite=False,
|
|
378
|
+
dim_order='yx', framerate=30, crf=23, compression_speed='medium'):
|
|
357
379
|
"""
|
|
358
|
-
Save a 3D numpy array
|
|
380
|
+
Save a 3D numpy array of greyscale values OR a 4D numpy array of RGB values as a video
|
|
359
381
|
|
|
360
382
|
Follows the PyAV cookbook section on generating video from numpy arrays:
|
|
361
383
|
https://pyav.basswood-io.com/docs/develop/cookbook/numpy.html#generating-video
|
|
@@ -363,7 +385,7 @@ def save_video(data, filename, time_axis=0, overwrite=False, dim_order='yx',
|
|
|
363
385
|
Parameters
|
|
364
386
|
----------
|
|
365
387
|
data : numpy.ndarray or list of filenames
|
|
366
|
-
A 3D numpy array of pixel values.
|
|
388
|
+
A 3D (grayscale) or 4D (RGB) numpy array of pixel values.
|
|
367
389
|
|
|
368
390
|
filename : str
|
|
369
391
|
The filename to save the video to.
|
|
@@ -371,24 +393,44 @@ def save_video(data, filename, time_axis=0, overwrite=False, dim_order='yx',
|
|
|
371
393
|
time_axis : int, default 0
|
|
372
394
|
The axis of the data array that will be played as time in the video.
|
|
373
395
|
|
|
396
|
+
color_axis : int or None, default None
|
|
397
|
+
If not None, specifies the axis of the color channels (e.g., -1 for last axis, 1 for second axis).
|
|
398
|
+
If None, data is assumed to be grayscale.
|
|
399
|
+
|
|
374
400
|
overwrite : bool, default False
|
|
375
401
|
Whether to overwrite the file if it already exists.
|
|
376
402
|
|
|
377
403
|
dim_order : 'yx' (default) or 'xy'
|
|
378
404
|
The order of the spatial dimensions in the input data.
|
|
379
405
|
"""
|
|
380
|
-
if not data.ndim == 3:
|
|
381
|
-
raise ValueError('Input data must be a 3D numpy array.')
|
|
382
406
|
try:
|
|
383
407
|
import av
|
|
384
408
|
except ImportError:
|
|
385
|
-
raise ImportError('To save videos, you must have PyAV installed. '
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
409
|
+
raise ImportError('To save videos, you must have PyAV installed. You can install it with "pip install av".')
|
|
410
|
+
|
|
411
|
+
if color_axis is not None:
|
|
412
|
+
if data.ndim != 4:
|
|
413
|
+
raise ValueError('Input data must be a 4D numpy array for color video.')
|
|
414
|
+
# Move time axis to 0, color axis to -1
|
|
415
|
+
data = np.moveaxis(data, time_axis, 0)
|
|
416
|
+
if color_axis != -1:
|
|
417
|
+
data = np.moveaxis(data, color_axis, -1)
|
|
418
|
+
if 'xy' in dim_order:
|
|
419
|
+
data = data.swapaxes(1, 2)
|
|
420
|
+
n_frames = data.shape[0]
|
|
421
|
+
height, width, channels = data.shape[1:]
|
|
422
|
+
if channels != 3:
|
|
423
|
+
raise ValueError('Color video must have 3 channels (RGB).')
|
|
424
|
+
else:
|
|
425
|
+
if data.ndim != 3:
|
|
426
|
+
raise ValueError('Input data must be a 3D numpy array for grayscale video.')
|
|
427
|
+
data = np.moveaxis(data, time_axis, 0)
|
|
428
|
+
if 'xy' in dim_order:
|
|
429
|
+
data = data.swapaxes(1, 2)
|
|
430
|
+
n_frames = data.shape[0]
|
|
431
|
+
height, width = data.shape[1:]
|
|
391
432
|
|
|
433
|
+
filename = os.path.expanduser(str(filename))
|
|
392
434
|
if filename.split('.')[-1].lower() not in ['mp4', 'mkv', 'avi', 'mov']:
|
|
393
435
|
filename += '.mp4'
|
|
394
436
|
if os.path.exists(filename) and not overwrite:
|
|
@@ -397,9 +439,9 @@ def save_video(data, filename, time_axis=0, overwrite=False, dim_order='yx',
|
|
|
397
439
|
extension = filename.split('.')[-1].lower()
|
|
398
440
|
if extension == 'mp4':
|
|
399
441
|
pad = [[0, 0], [0, 0], [0, 0]]
|
|
400
|
-
if
|
|
442
|
+
if height % 2 != 0:
|
|
401
443
|
pad[1][1] = 1
|
|
402
|
-
if
|
|
444
|
+
if width % 2 != 0:
|
|
403
445
|
pad[2][1] = 1
|
|
404
446
|
if pad != [[0, 0], [0, 0], [0, 0]]:
|
|
405
447
|
data = np.pad(data, pad, mode='edge')
|
|
@@ -412,7 +454,10 @@ def save_video(data, filename, time_axis=0, overwrite=False, dim_order='yx',
|
|
|
412
454
|
stream.width = data.shape[2]
|
|
413
455
|
|
|
414
456
|
for frame_i in range(n_frames):
|
|
415
|
-
|
|
457
|
+
if color_axis is not None:
|
|
458
|
+
frame = av.VideoFrame.from_ndarray(data[frame_i], format='rgb24')
|
|
459
|
+
else:
|
|
460
|
+
frame = av.VideoFrame.from_ndarray(data[frame_i], format='gray')
|
|
416
461
|
for packet in stream.encode(frame):
|
|
417
462
|
container.mux(packet)
|
|
418
463
|
|
|
@@ -535,3 +580,88 @@ def find_channel_axis(data, expected_channel_axis=[0, -1]):
|
|
|
535
580
|
if data.shape[axis] in [2, 3, 4]:
|
|
536
581
|
return axis
|
|
537
582
|
return None
|
|
583
|
+
|
|
584
|
+
|
|
585
|
+
def load_video(filename, force_rgb=False, progress_bar=True):
|
|
586
|
+
"""
|
|
587
|
+
Load all images in a video file as a numpy array.
|
|
588
|
+
|
|
589
|
+
Parameters
|
|
590
|
+
----------
|
|
591
|
+
filename : str
|
|
592
|
+
Path to the video file
|
|
593
|
+
force_rgb : bool, default False
|
|
594
|
+
If True, always return frames as RGB arrays (shape=(H, W, 3))
|
|
595
|
+
If False, return frames in their native format (e.g., grayscale if possible)
|
|
596
|
+
progress_bar : bool, default True
|
|
597
|
+
If True, display a progress bar
|
|
598
|
+
|
|
599
|
+
Returns
|
|
600
|
+
-------
|
|
601
|
+
data : numpy.ndarray
|
|
602
|
+
The video frames as a numpy array, shape (num_frames, height, width[, channels])
|
|
603
|
+
"""
|
|
604
|
+
try:
|
|
605
|
+
import av
|
|
606
|
+
except ImportError:
|
|
607
|
+
raise ImportError('To use load_video, install PyAV: pip install av')
|
|
608
|
+
from tqdm import tqdm
|
|
609
|
+
container = av.open(filename)
|
|
610
|
+
stream = container.streams.video[0]
|
|
611
|
+
num_frames = stream.frames
|
|
612
|
+
if not num_frames or num_frames == 0:
|
|
613
|
+
# If we don't know the number of frames, we can't preallocate, so
|
|
614
|
+
# it's hard to do better than the following approach (which temporarily
|
|
615
|
+
# uses double the amount of RAM than the preallocated approach uses).
|
|
616
|
+
return np.array(list(lazy_load_video(filename, force_rgb=force_rgb)))
|
|
617
|
+
else:
|
|
618
|
+
# Load first image to get shape and dtype
|
|
619
|
+
frame_iter = container.decode(stream)
|
|
620
|
+
first_frame = next(frame_iter)
|
|
621
|
+
if force_rgb:
|
|
622
|
+
first_img = first_frame.to_ndarray(format='rgb24')
|
|
623
|
+
else:
|
|
624
|
+
first_img = first_frame.to_ndarray()
|
|
625
|
+
# Preallocate memory for the entire array
|
|
626
|
+
data = np.empty((num_frames, *first_img.shape), dtype=first_img.dtype)
|
|
627
|
+
# Then fill it up frame by frame
|
|
628
|
+
data[0] = first_img
|
|
629
|
+
for i, frame in tqdm(enumerate(frame_iter, start=1), total=num_frames,
|
|
630
|
+
desc='Loading video', disable=not progress_bar):
|
|
631
|
+
if force_rgb:
|
|
632
|
+
img = frame.to_ndarray(format='rgb24')
|
|
633
|
+
else:
|
|
634
|
+
img = frame.to_ndarray()
|
|
635
|
+
data[i] = img
|
|
636
|
+
return data
|
|
637
|
+
|
|
638
|
+
|
|
639
|
+
def lazy_load_video(filename, force_rgb=False):
|
|
640
|
+
"""
|
|
641
|
+
Lazily load video frames as numpy arrays using PyAV.
|
|
642
|
+
|
|
643
|
+
Parameters
|
|
644
|
+
----------
|
|
645
|
+
filename : str
|
|
646
|
+
Path to the video file.
|
|
647
|
+
force_rgb : bool, default False
|
|
648
|
+
If True, always return frames as RGB arrays (shape=(H, W, 3)).
|
|
649
|
+
If False, return frames in their native format (e.g., grayscale if possible).
|
|
650
|
+
|
|
651
|
+
Yields
|
|
652
|
+
------
|
|
653
|
+
frame : np.ndarray
|
|
654
|
+
Video frame as a numpy array, shape (height, width[, colors]).
|
|
655
|
+
"""
|
|
656
|
+
try:
|
|
657
|
+
import av
|
|
658
|
+
except ImportError:
|
|
659
|
+
raise ImportError('To use lazy_load_video, install PyAV: pip install av')
|
|
660
|
+
container = av.open(filename)
|
|
661
|
+
stream = container.streams.video[0]
|
|
662
|
+
for frame in container.decode(stream):
|
|
663
|
+
if force_rgb:
|
|
664
|
+
img = frame.to_ndarray(format='rgb24')
|
|
665
|
+
else:
|
|
666
|
+
img = frame.to_ndarray()
|
|
667
|
+
yield img
|
|
@@ -19,12 +19,14 @@ import numpy as np
|
|
|
19
19
|
from .utils import iround, eq, isint
|
|
20
20
|
|
|
21
21
|
|
|
22
|
-
def squeeze_dtype(image: np.ndarray):
|
|
22
|
+
def squeeze_dtype(image: np.ndarray, minimum_bits=1):
|
|
23
23
|
"""
|
|
24
24
|
Cast a numpy array to the smallest possible dtype without losing information.
|
|
25
25
|
"""
|
|
26
26
|
if not isinstance(image, np.ndarray):
|
|
27
27
|
raise TypeError('image must be a numpy array')
|
|
28
|
+
if minimum_bits > 64:
|
|
29
|
+
raise ValueError('minimum_bits must be <= 64')
|
|
28
30
|
|
|
29
31
|
im_has_fractions = not np.all(image == np.floor(image))
|
|
30
32
|
if im_has_fractions:
|
|
@@ -35,17 +37,17 @@ def squeeze_dtype(image: np.ndarray):
|
|
|
35
37
|
return image
|
|
36
38
|
elif image.min() < 0:
|
|
37
39
|
candidates = [np.int8, np.int16, np.int32, np.int64]
|
|
38
|
-
elif image.max() <= 1:
|
|
40
|
+
elif image.max() <= 1 and minimum_bits <= 1:
|
|
39
41
|
return image.astype(bool, copy=False)
|
|
40
42
|
else:
|
|
41
43
|
candidates = [np.uint8, np.uint16, np.uint32, np.uint64]
|
|
42
44
|
|
|
43
45
|
image_max = image.max()
|
|
44
|
-
if image_max <= 1:
|
|
46
|
+
if image_max <= 1 and minimum_bits <= 1:
|
|
45
47
|
return image.astype(bool, copy=False)
|
|
46
48
|
for dtype in candidates:
|
|
47
49
|
info = np.iinfo(dtype)
|
|
48
|
-
if image_max <= info.max:
|
|
50
|
+
if image_max <= info.max and info.bits >= minimum_bits:
|
|
49
51
|
return image.astype(dtype, copy=False)
|
|
50
52
|
raise ValueError('Image has values larger than the maximum representable value')
|
|
51
53
|
|
|
@@ -70,7 +72,8 @@ def to_16bit(image: np.ndarray, **kwargs) -> np.ndarray:
|
|
|
70
72
|
|
|
71
73
|
def cast(image: np.ndarray,
|
|
72
74
|
output_dtype: Union[str, np.dtype],
|
|
73
|
-
maximize_contrast=
|
|
75
|
+
maximize_contrast: bool = False,
|
|
76
|
+
round_before_cast_to_int: bool = True,
|
|
74
77
|
bottom_percentile=0.05,
|
|
75
78
|
top_percentile=99.95,
|
|
76
79
|
bottom_value=None,
|
|
@@ -132,11 +135,15 @@ def cast(image: np.ndarray,
|
|
|
132
135
|
raise TypeError(f'Unsupported dtype: {output_dtype}')
|
|
133
136
|
|
|
134
137
|
if not maximize_contrast:
|
|
138
|
+
if (round_before_cast_to_int
|
|
139
|
+
and np.issubdtype(output_dtype, np.integer)
|
|
140
|
+
and not np.issubdtype(image.dtype, np.integer)):
|
|
141
|
+
image = image.round()
|
|
135
142
|
return np.clip(
|
|
136
143
|
image,
|
|
137
144
|
info.min,
|
|
138
145
|
info.max
|
|
139
|
-
).astype(output_dtype)
|
|
146
|
+
).astype(output_dtype, copy=False)
|
|
140
147
|
|
|
141
148
|
if bottom_value is None or top_value is None:
|
|
142
149
|
percentiles = np.percentile(image, [bottom_percentile, top_percentile])
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: numpyimage
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.4.0
|
|
4
4
|
Summary: Load, save, & manipulate image files as numpy arrays
|
|
5
5
|
Author-email: Jasper Phelps <jasper.s.phelps@gmail.com>
|
|
6
6
|
License: GNU GENERAL PUBLIC LICENSE
|
|
@@ -690,6 +690,7 @@ Description-Content-Type: text/markdown
|
|
|
690
690
|
License-File: LICENSE
|
|
691
691
|
Requires-Dist: numpy
|
|
692
692
|
Requires-Dist: pillow
|
|
693
|
+
Requires-Dist: pillow-heif
|
|
693
694
|
Requires-Dist: tifffile
|
|
694
695
|
Requires-Dist: pynrrd
|
|
695
696
|
Requires-Dist: matplotlib
|
|
@@ -3,17 +3,15 @@ README.md
|
|
|
3
3
|
pyproject.toml
|
|
4
4
|
npimage/__init__.py
|
|
5
5
|
npimage/align.py
|
|
6
|
-
npimage/align_scratch.py
|
|
7
6
|
npimage/core.py
|
|
8
7
|
npimage/graphics.py
|
|
9
8
|
npimage/nrrd_utils.py
|
|
10
9
|
npimage/operations.py
|
|
11
10
|
npimage/utils.py
|
|
12
|
-
npimage/filetypes/__init__.py
|
|
13
|
-
npimage/filetypes/pbm.py
|
|
14
11
|
numpyimage.egg-info/PKG-INFO
|
|
15
12
|
numpyimage.egg-info/SOURCES.txt
|
|
16
13
|
numpyimage.egg-info/dependency_links.txt
|
|
17
14
|
numpyimage.egg-info/requires.txt
|
|
18
15
|
numpyimage.egg-info/top_level.txt
|
|
16
|
+
tests/test_heic.py
|
|
19
17
|
tests/test_pbm.py
|
|
@@ -4,7 +4,7 @@ build-backend = 'setuptools.build_meta'
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = 'numpyimage'
|
|
7
|
-
version = '2.
|
|
7
|
+
version = '2.4.0'
|
|
8
8
|
description = 'Load, save, & manipulate image files as numpy arrays'
|
|
9
9
|
readme.file = 'README.md'
|
|
10
10
|
readme.content-type = 'text/markdown'
|
|
@@ -23,6 +23,7 @@ urls = {Repository = 'https://github.com/jasper-tms/npimage'}
|
|
|
23
23
|
dependencies = [
|
|
24
24
|
'numpy',
|
|
25
25
|
'pillow',
|
|
26
|
+
'pillow-heif',
|
|
26
27
|
'tifffile',
|
|
27
28
|
'pynrrd',
|
|
28
29
|
'matplotlib',
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import tempfile
|
|
5
|
+
import numpy as np
|
|
6
|
+
import npimage
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def test_heic_load_save():
|
|
10
|
+
"""Test loading and saving HEIC images."""
|
|
11
|
+
# Create a simple test image
|
|
12
|
+
test_data = np.random.randint(0, 256, (100, 100, 3), dtype=np.uint8)
|
|
13
|
+
|
|
14
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
15
|
+
# Test saving as HEIC
|
|
16
|
+
heic_filename = os.path.join(tmpdir, "test_image.heic")
|
|
17
|
+
|
|
18
|
+
try:
|
|
19
|
+
# Save the test data as HEIC
|
|
20
|
+
npimage.save(test_data, heic_filename)
|
|
21
|
+
|
|
22
|
+
# Verify the file was created
|
|
23
|
+
assert os.path.exists(heic_filename), "HEIC file was not created"
|
|
24
|
+
|
|
25
|
+
# Load the HEIC file back
|
|
26
|
+
loaded_data = npimage.load(heic_filename)
|
|
27
|
+
|
|
28
|
+
# Check that the loaded data has the same shape
|
|
29
|
+
assert loaded_data.shape == test_data.shape, f"Shape mismatch: expected {test_data.shape}, got {loaded_data.shape}"
|
|
30
|
+
|
|
31
|
+
# Check that the data type is correct
|
|
32
|
+
assert loaded_data.dtype == np.uint8, f"Data type mismatch: expected uint8, got {loaded_data.dtype}"
|
|
33
|
+
|
|
34
|
+
print("Test passed: HEIC load/save functionality works correctly.")
|
|
35
|
+
|
|
36
|
+
except Exception as e:
|
|
37
|
+
print(f"Test failed with error: {e}")
|
|
38
|
+
raise
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_heic_extension_support():
|
|
42
|
+
"""Test that HEIC extension is properly recognized."""
|
|
43
|
+
# Test that HEIC is in the supported extensions
|
|
44
|
+
assert 'heic' in npimage.core.supported_extensions, "HEIC extension not in supported extensions"
|
|
45
|
+
|
|
46
|
+
# Test that the extension is properly parsed
|
|
47
|
+
filename = "test_image.heic"
|
|
48
|
+
extension = filename.split('.')[-1].lower()
|
|
49
|
+
assert extension == 'heic', f"Extension parsing failed: expected 'heic', got '{extension}'"
|
|
50
|
+
|
|
51
|
+
print("Test passed: HEIC extension is properly supported.")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
if __name__ == '__main__':
|
|
55
|
+
try:
|
|
56
|
+
test_heic_extension_support()
|
|
57
|
+
test_heic_load_save()
|
|
58
|
+
print("All HEIC tests passed!")
|
|
59
|
+
except Exception as e:
|
|
60
|
+
print(f"HEIC tests failed: {e}")
|
|
61
|
+
raise
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
def align(im1, im2, num_divisions=4):
|
|
5
|
-
"""
|
|
6
|
-
Return a translated version of im1 that is aligned with im2.
|
|
7
|
-
|
|
8
|
-
To align the images, this function divides im1 into a grid of
|
|
9
|
-
num_divisions x num_divisions regions. For each region, we find the
|
|
10
|
-
region in im2 that is most similar to it. After removing outliers
|
|
11
|
-
for which the similarity score is too low, we calculate the average
|
|
12
|
-
translation between the two images. We then shift im1 by this
|
|
13
|
-
average translation.
|
|
14
|
-
"""
|
|
15
|
-
# The code below is entirely GitHub copilot generated and needs to
|
|
16
|
-
# be looked at, finished, tested, etc
|
|
17
|
-
|
|
18
|
-
# Divide the image into a grid of num_divisions x num_divisions regions
|
|
19
|
-
height, width = im1.shape
|
|
20
|
-
region_height = height // num_divisions
|
|
21
|
-
region_width = width // num_divisions
|
|
22
|
-
|
|
23
|
-
# Initialize lists to store the translations for each region
|
|
24
|
-
translations = []
|
|
25
|
-
|
|
26
|
-
# For each region in im1, find the most similar region in im2
|
|
27
|
-
for i in range(num_divisions):
|
|
28
|
-
for j in range(num_divisions):
|
|
29
|
-
# Define the region in im1
|
|
30
|
-
top_left = (i * region_height, j * region_width)
|
|
31
|
-
bottom_right = ((i + 1) * region_height, (j + 1) * region_width)
|
|
32
|
-
region1 = im1[top_left[0]:bottom_right[0], top_left[1]:bottom_right[1]]
|
|
33
|
-
|
|
34
|
-
# Define the search region in im2
|
|
35
|
-
search_bbox = (slice(max(0, top_left[0] - region_height),
|
|
36
|
-
min(height, bottom_right[0] + region_height)),
|
|
37
|
-
slice(max(0, top_left[1] - region_width),
|
|
38
|
-
min(width, bottom_right[1] + region_width)))
|
|
39
|
-
|
|
40
|
-
# Find the most similar region in im2
|
|
41
|
-
top_left2, score = find_landmark(im2, region1, search_bbox=search_bbox)
|
|
42
|
-
|
|
43
|
-
# Add the translation to the list of translations
|
|
44
|
-
translations.append((top_left[0] - top_left2[0], top_left[1] - top_left2[1]))
|
|
45
|
-
|
|
46
|
-
# Remove outliers from the list of translations
|
|
47
|
-
translations = np.array(translations)
|
|
48
|
-
mean_translations = np.mean(translations)
|
|
49
|
-
# Shift im1 by the average translation
|
|
50
|
-
aligned_im1 = np.roll(im1, mean_translations, axis=(0, 1))
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__all__ = ['pbm']
|
|
@@ -1,103 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""
|
|
3
|
-
Read and write PBM files, whose format is specified at
|
|
4
|
-
http://netpbm.sourceforge.net/doc/pbm.html
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
|
-
import numpy as np
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
def load(filename):
|
|
11
|
-
"""
|
|
12
|
-
Load pixel data from a PBM file and return it as a numpy array.
|
|
13
|
-
The returned array has dimensions ordered y then x, as is
|
|
14
|
-
standard in Python. This means data.shape is ordered (height, width)
|
|
15
|
-
"""
|
|
16
|
-
with open(filename, 'rb') as f:
|
|
17
|
-
assert f.readline() == b'P4\n', 'This is not a PBM file'
|
|
18
|
-
line = f.readline()
|
|
19
|
-
while line.startswith(b'#'): # Print and skip comments
|
|
20
|
-
print(line.decode().strip('\n'))
|
|
21
|
-
line = f.readline()
|
|
22
|
-
line = line.decode().strip().split()
|
|
23
|
-
w = int(line[0])
|
|
24
|
-
h = int(line[1])
|
|
25
|
-
|
|
26
|
-
# Calculate the number of bytes per row (padded to the next byte boundary)
|
|
27
|
-
row_bytes = (w + 7) // 8
|
|
28
|
-
data = np.frombuffer(f.read(row_bytes * h), dtype=np.uint8)
|
|
29
|
-
|
|
30
|
-
# Unpack bits and reshape, then slice to remove padding bits
|
|
31
|
-
data = np.unpackbits(data).reshape((h, row_bytes * 8))[:, :w]
|
|
32
|
-
|
|
33
|
-
return data.astype(bool)
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
def save(data, filename, comments=None):
|
|
37
|
-
"""
|
|
38
|
-
Write a numpy array to file in PBM format
|
|
39
|
-
"""
|
|
40
|
-
if not isinstance(data, np.ndarray):
|
|
41
|
-
raise TypeError('data must be a np.ndarray but was {}'.format(type(data)))
|
|
42
|
-
if not isinstance(filename, str):
|
|
43
|
-
raise TypeError('filename must be a str but was {}'.format(type(filename)))
|
|
44
|
-
if comments is not None and not isinstance(comments, str):
|
|
45
|
-
raise TypeError('comments must be a str but was {}'.format(type(comments)))
|
|
46
|
-
with open(filename, 'wb') as f:
|
|
47
|
-
# Header
|
|
48
|
-
f.write(b'P4\n')
|
|
49
|
-
|
|
50
|
-
# Comments
|
|
51
|
-
if comments is not None:
|
|
52
|
-
# Insist on each line starting with a '# '
|
|
53
|
-
comments = '# ' + comments.replace('\n', '\n# ')
|
|
54
|
-
# Remove any double '# ', if we created any
|
|
55
|
-
comments = comments.replace('# # ', '# ')
|
|
56
|
-
while any([comments.endswith(c) for c in ['#', ' ', '\n']]):
|
|
57
|
-
comments = comments[:-1]
|
|
58
|
-
print(comments)
|
|
59
|
-
f.write(comments.encode())
|
|
60
|
-
f.write(b'\n')
|
|
61
|
-
|
|
62
|
-
# Size
|
|
63
|
-
f.write(str(data.shape[1]).encode())
|
|
64
|
-
f.write(b' ')
|
|
65
|
-
f.write(str(data.shape[0]).encode())
|
|
66
|
-
f.write(b'\n')
|
|
67
|
-
|
|
68
|
-
# Data
|
|
69
|
-
# Pad each row to the next byte boundary
|
|
70
|
-
row_bytes = (data.shape[1] + 7) // 8
|
|
71
|
-
padded_data = np.zeros((data.shape[0], row_bytes * 8), dtype=bool)
|
|
72
|
-
padded_data[:, :data.shape[1]] = data
|
|
73
|
-
f.write(np.packbits(padded_data).tobytes())
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
def predict_file_size(data: np.ndarray) -> int:
|
|
77
|
-
"""
|
|
78
|
-
Predict the file size of a PBM file given the image data.
|
|
79
|
-
|
|
80
|
-
Parameters
|
|
81
|
-
----------
|
|
82
|
-
data : np.ndarray
|
|
83
|
-
The image data as a numpy array.
|
|
84
|
-
|
|
85
|
-
Returns
|
|
86
|
-
-------
|
|
87
|
-
int
|
|
88
|
-
The predicted file size in bytes.
|
|
89
|
-
"""
|
|
90
|
-
if not isinstance(data, np.ndarray):
|
|
91
|
-
raise TypeError('data must be a np.ndarray but was {}'.format(type(data)))
|
|
92
|
-
|
|
93
|
-
# Header: 'P4\n' (3 bytes)
|
|
94
|
-
header_size = 3
|
|
95
|
-
|
|
96
|
-
# Dimensions: '<width> <height>\n'
|
|
97
|
-
dimensions_size = len(f"{data.shape[1]} {data.shape[0]}\n")
|
|
98
|
-
|
|
99
|
-
# Data: Each row is padded to the next byte boundary
|
|
100
|
-
row_bytes = (data.shape[1] + 7) // 8
|
|
101
|
-
data_size = row_bytes * data.shape[0]
|
|
102
|
-
|
|
103
|
-
return header_size + dimensions_size + data_size
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|