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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: numpyimage
3
- Version: 2.2.0
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 "{extension}" not supported/recognized.')
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, overwrite=False, dim_order='yx',
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 as a video, with a specified axis as the time axis.
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
- 'You can install it with "pip install av".')
387
- data = np.moveaxis(data, time_axis, 0)
388
- if 'xy' in dim_order:
389
- data = data.swapaxes(1, 2)
390
- n_frames = data.shape[0]
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 data.shape[1] % 2 != 0:
442
+ if height % 2 != 0:
401
443
  pad[1][1] = 1
402
- if data.shape[2] % 2 != 0:
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
- frame = av.VideoFrame.from_ndarray(data[frame_i], format='gray')
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=True,
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.2.0
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
@@ -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.2.0'
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