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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: numpyimage
3
- Version: 2.3.0
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.0
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/test_pbm.py
19
- tests/tests.py
16
+ tests/test_heic.py
17
+ tests/test_pbm.py
@@ -1,5 +1,6 @@
1
1
  numpy
2
2
  pillow
3
+ pillow-heif
3
4
  tifffile
4
5
  pynrrd
5
6
  matplotlib
@@ -4,7 +4,7 @@ build-backend = 'setuptools.build_meta'
4
4
 
5
5
  [project]
6
6
  name = 'numpyimage'
7
- version = '2.3.0'
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
@@ -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