numpyimage 2.3.0__tar.gz → 2.4.1__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.3.0 → numpyimage-2.4.1}/PKG-INFO +2 -1
- {numpyimage-2.3.0 → numpyimage-2.4.1}/npimage/core.py +108 -1
- {numpyimage-2.3.0 → numpyimage-2.4.1}/numpyimage.egg-info/PKG-INFO +2 -1
- {numpyimage-2.3.0 → numpyimage-2.4.1}/numpyimage.egg-info/SOURCES.txt +2 -4
- {numpyimage-2.3.0 → numpyimage-2.4.1}/numpyimage.egg-info/requires.txt +1 -0
- {numpyimage-2.3.0 → numpyimage-2.4.1}/pyproject.toml +2 -1
- numpyimage-2.4.1/tests/test_heic.py +61 -0
- numpyimage-2.3.0/npimage/filetypes/__init__.py +0 -1
- numpyimage-2.3.0/npimage/filetypes/pbm.py +0 -103
- numpyimage-2.3.0/tests/tests.py +0 -39
- {numpyimage-2.3.0 → numpyimage-2.4.1}/LICENSE +0 -0
- {numpyimage-2.3.0 → numpyimage-2.4.1}/README.md +0 -0
- {numpyimage-2.3.0 → numpyimage-2.4.1}/npimage/__init__.py +0 -0
- {numpyimage-2.3.0 → numpyimage-2.4.1}/npimage/align.py +0 -0
- {numpyimage-2.3.0 → numpyimage-2.4.1}/npimage/graphics.py +0 -0
- {numpyimage-2.3.0 → numpyimage-2.4.1}/npimage/nrrd_utils.py +0 -0
- {numpyimage-2.3.0 → numpyimage-2.4.1}/npimage/operations.py +0 -0
- {numpyimage-2.3.0 → numpyimage-2.4.1}/npimage/utils.py +0 -0
- {numpyimage-2.3.0 → numpyimage-2.4.1}/numpyimage.egg-info/dependency_links.txt +0 -0
- {numpyimage-2.3.0 → numpyimage-2.4.1}/numpyimage.egg-info/top_level.txt +0 -0
- {numpyimage-2.3.0 → numpyimage-2.4.1}/setup.cfg +0 -0
- {numpyimage-2.3.0 → numpyimage-2.4.1}/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.1
|
|
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
|
"""
|
|
@@ -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
|
|
@@ -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)
|
|
@@ -558,3 +580,88 @@ def find_channel_axis(data, expected_channel_axis=[0, -1]):
|
|
|
558
580
|
if data.shape[axis] in [2, 3, 4]:
|
|
559
581
|
return axis
|
|
560
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
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: numpyimage
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.4.1
|
|
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
|
|
@@ -8,12 +8,10 @@ npimage/graphics.py
|
|
|
8
8
|
npimage/nrrd_utils.py
|
|
9
9
|
npimage/operations.py
|
|
10
10
|
npimage/utils.py
|
|
11
|
-
npimage/filetypes/__init__.py
|
|
12
|
-
npimage/filetypes/pbm.py
|
|
13
11
|
numpyimage.egg-info/PKG-INFO
|
|
14
12
|
numpyimage.egg-info/SOURCES.txt
|
|
15
13
|
numpyimage.egg-info/dependency_links.txt
|
|
16
14
|
numpyimage.egg-info/requires.txt
|
|
17
15
|
numpyimage.egg-info/top_level.txt
|
|
18
|
-
tests/
|
|
19
|
-
tests/
|
|
16
|
+
tests/test_heic.py
|
|
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.1'
|
|
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 +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
|
numpyimage-2.3.0/tests/tests.py
DELETED
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
|
|
3
|
-
import numpy as np
|
|
4
|
-
import fire
|
|
5
|
-
|
|
6
|
-
import npimage
|
|
7
|
-
import npimage.operations
|
|
8
|
-
|
|
9
|
-
test_im = np.zeros((8, 8), dtype=np.uint8)
|
|
10
|
-
test_im[0:1, 0:2] = 255
|
|
11
|
-
test_im[1:3, 2:4] = 255
|
|
12
|
-
test_im[3:7, 3:7] = 255
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
def mplshow(*args):
|
|
16
|
-
npimage.show(*args, mode='matplotlib')
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
def test_offset():
|
|
20
|
-
mplshow(test_im)
|
|
21
|
-
mplshow(npimage.operations.offset(test_im, (0.5, 0)))
|
|
22
|
-
mplshow(npimage.operations.offset(test_im, (0, 0.5)))
|
|
23
|
-
mplshow(npimage.operations.offset(test_im, (0.5, 0.5)))
|
|
24
|
-
mplshow(npimage.operations.offset(test_im, (1, 1)))
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
def test_paste():
|
|
28
|
-
offsets = [('++', [100, 50]),
|
|
29
|
-
('+-', [100, -50]),
|
|
30
|
-
('-+', [-100, 50]),
|
|
31
|
-
('--', [-100, -50])]
|
|
32
|
-
for offset in offsets:
|
|
33
|
-
im = npimage.load('firefox-logo.png', dim_order='xy')
|
|
34
|
-
npimage.operations.paste(im, im, offset[1])
|
|
35
|
-
npimage.save(im, 'firefox-logo_paste' + offset[0] + '.png', dim_order='xy')
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
if __name__ == '__main__':
|
|
39
|
-
fire.Fire()
|
|
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
|
|
File without changes
|