numpyimage 2.4.2__tar.gz → 2.5.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.4.2
3
+ Version: 2.5.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
@@ -695,6 +695,9 @@ Requires-Dist: tifffile
695
695
  Requires-Dist: pynrrd
696
696
  Requires-Dist: matplotlib
697
697
  Requires-Dist: opencv-python-headless
698
+ Provides-Extra: all
699
+ Requires-Dist: tqdm; extra == "all"
700
+ Requires-Dist: av; extra == "all"
698
701
  Dynamic: license-file
699
702
 
700
703
  # npimage
@@ -9,7 +9,7 @@ Function list:
9
9
  - show(np_array) -> Displays a numpy array of pixel values as an image
10
10
  """
11
11
 
12
- from typing import Literal
12
+ from typing import Literal, Union, Tuple, Iterator
13
13
  import os
14
14
  import glob
15
15
  from builtins import open as builtin_open
@@ -24,7 +24,7 @@ supported_extensions = [
24
24
  ]
25
25
 
26
26
 
27
- def load(filename, dim_order='zyx', **kwargs):
27
+ def load(filename, dim_order='zyx', **kwargs) -> Union[np.ndarray, Tuple[np.ndarray, dict]]:
28
28
  """
29
29
  Open a 2D or 3D image file and return its pixel values as a numpy array.
30
30
 
@@ -52,16 +52,16 @@ def load(filename, dim_order='zyx', **kwargs):
52
52
 
53
53
  if extension in ['jpg', 'jpeg', 'png']:
54
54
  from PIL import Image
55
- data = np.array(Image.open(filename)) #PIL.Image.open returns zyx order
55
+ data = np.array(Image.open(filename)) # PIL.Image.open returns zyx order
56
56
 
57
57
  if extension == 'heic':
58
58
  _ensure_heif_opener_registered()
59
59
  from PIL import Image
60
- data = np.array(Image.open(filename)) #PIL.Image.open returns zyx order
60
+ data = np.array(Image.open(filename)) # PIL.Image.open returns zyx order
61
61
 
62
62
  if extension in ['tif', 'tiff']:
63
63
  import tifffile
64
- data = tifffile.imread(filename) #tifffile.imread returns zyx order
64
+ data = tifffile.imread(filename) # tifffile.imread returns zyx order
65
65
 
66
66
  if extension == 'pbm':
67
67
  from .filetypes import pbm
@@ -151,7 +151,7 @@ def save(data,
151
151
  units=None,
152
152
  compress=None,
153
153
  compression_level=3,
154
- metadata=None):
154
+ metadata=None) -> None:
155
155
  """
156
156
  Save a numpy array to file with a file type specified by the
157
157
  filename extension.
@@ -357,12 +357,13 @@ def save(data,
357
357
 
358
358
  vol[:] = data
359
359
 
360
-
361
360
  write = save # Function name alias
362
361
  to_file = save # Function name alias
363
362
 
364
363
 
365
- def load_video(filename, force_rgb=False, progress_bar=True):
364
+ def load_video(filename,
365
+ return_framerate=False,
366
+ progress_bar=True) -> Union[np.ndarray, Tuple[np.ndarray, float]]:
366
367
  """
367
368
  Load all images in a video file as a numpy array.
368
369
 
@@ -370,53 +371,55 @@ def load_video(filename, force_rgb=False, progress_bar=True):
370
371
  ----------
371
372
  filename : str
372
373
  Path to the video file
373
- force_rgb : bool, default False
374
- If True, always return frames as RGB arrays (shape=(H, W, 3))
375
- If False, return frames in their native format (e.g., grayscale if possible)
374
+ return_framerate : bool, default False
375
+ If True, return the frame rate of the video
376
376
  progress_bar : bool, default True
377
377
  If True, display a progress bar
378
378
 
379
379
  Returns
380
380
  -------
381
381
  data : numpy.ndarray
382
- The video frames as a numpy array, shape (num_frames, height, width[, channels])
382
+ The video frames as a numpy array, shape (num_frames, height, width, colors)
383
383
  """
384
384
  try:
385
385
  import av
386
+ from tqdm import tqdm
386
387
  except ImportError:
387
- raise ImportError('To use load_video, install PyAV: pip install av')
388
- from tqdm import tqdm
388
+ raise ImportError('Missing optional dependency for video processing,'
389
+ ' run `pip install av tqdm`')
390
+
389
391
  container = av.open(filename)
390
392
  stream = container.streams.video[0]
391
393
  num_frames = stream.frames
392
394
  if not num_frames or num_frames == 0:
393
395
  # If we don't know the number of frames, we can't preallocate, so
394
- # it's hard to do better than the following approach (which temporarily
395
- # uses double the amount of RAM than the preallocated approach uses).
396
- return np.array(list(lazy_load_video(filename, force_rgb=force_rgb)))
396
+ # it's hard to do better than the following approach which temporarily
397
+ # uses double the amount of RAM compared to the preallocated approach.
398
+ data = np.array(list(lazy_load_video(filename)))
399
+ if return_framerate:
400
+ return data, float(stream.average_rate)
401
+ else:
402
+ return data
397
403
  else:
398
404
  # Load first image to get shape and dtype
399
405
  frame_iter = container.decode(stream)
400
406
  first_frame = next(frame_iter)
401
- if force_rgb:
402
- first_img = first_frame.to_ndarray(format='rgb24')
403
- else:
404
- first_img = first_frame.to_ndarray()
407
+ first_img = first_frame.to_ndarray(format='rgb24')
405
408
  # Preallocate memory for the entire array
406
409
  data = np.empty((num_frames, *first_img.shape), dtype=first_img.dtype)
407
410
  # Then fill it up frame by frame
408
411
  data[0] = first_img
409
412
  for i, frame in tqdm(enumerate(frame_iter, start=1), total=num_frames,
410
413
  desc='Loading video', disable=not progress_bar):
411
- if force_rgb:
412
- img = frame.to_ndarray(format='rgb24')
413
- else:
414
- img = frame.to_ndarray()
414
+ img = frame.to_ndarray(format='rgb24')
415
415
  data[i] = img
416
- return data
416
+ if return_framerate:
417
+ return data, float(stream.average_rate)
418
+ else:
419
+ return data
417
420
 
418
421
 
419
- def lazy_load_video(filename, force_rgb=False):
422
+ def lazy_load_video(filename) -> Iterator[np.ndarray]:
420
423
  """
421
424
  Lazily load video frames as numpy arrays using PyAV.
422
425
 
@@ -424,31 +427,115 @@ def lazy_load_video(filename, force_rgb=False):
424
427
  ----------
425
428
  filename : str
426
429
  Path to the video file.
427
- force_rgb : bool, default False
428
- If True, always return frames as RGB arrays (shape=(H, W, 3)).
429
- If False, return frames in their native format (e.g., grayscale if possible).
430
430
 
431
431
  Yields
432
432
  ------
433
433
  frame : np.ndarray
434
- Video frame as a numpy array, shape (height, width[, colors]).
434
+ Video frame as a numpy array, shape (height, width, colors).
435
435
  """
436
436
  try:
437
437
  import av
438
438
  except ImportError:
439
- raise ImportError('To use lazy_load_video, install PyAV: pip install av')
439
+ raise ImportError('Missing optional dependency for video processing,'
440
+ ' run `pip install av tqdm`')
440
441
  container = av.open(filename)
441
442
  stream = container.streams.video[0]
442
443
  for frame in container.decode(stream):
443
- if force_rgb:
444
- img = frame.to_ndarray(format='rgb24')
445
- else:
446
- img = frame.to_ndarray()
444
+ img = frame.to_ndarray(format='rgb24')
447
445
  yield img
448
446
 
449
447
 
448
+ codec_aliases = {
449
+ "libx264": "libx264",
450
+ "avc1": "libx264",
451
+ "h264": "libx264",
452
+ "H.264": "libx264",
453
+ "libx265": "libx265",
454
+ "hevc": "libx265",
455
+ "hvc1": "libx265",
456
+ "hev1": "libx265",
457
+ "h265": "libx265",
458
+ "H.265": "libx265",
459
+ }
460
+
461
+
462
+ def video_writer(filename, framerate=30, crf=23, compression_speed='medium',
463
+ codec: Literal['libx264', 'libx265'] = 'libx264') -> 'VideoWriter':
464
+ """
465
+ Create a video writer object for saving frames to a video file.
466
+
467
+ Parameters
468
+ ----------
469
+ filename : str
470
+ The filename to save the video to.
471
+ framerate : int, default 30
472
+ The frame rate of the video.
473
+ crf : int, default 23
474
+ Constant Rate Factor for encoding quality (lower is better quality).
475
+ compression_speed : str, default 'medium'
476
+ Compression speed preset: 'ultrafast', 'superfast', 'veryfast',
477
+ 'faster', 'fast', 'medium', 'slow', 'slower', 'veryslow'.
478
+ codec : Literal['libx264', 'libx265'], default 'libx264'
479
+ The video codec to use for encoding. Can be any of a number of aliases for
480
+ these two codecs, including avc1/h264 vs hevc/hvc1/hev1/h265.
481
+
482
+ Returns
483
+ -------
484
+ VideoWriter
485
+ The video writer object supporting context manager protocol.
486
+ """
487
+ try:
488
+ import av
489
+ except ImportError:
490
+ raise ImportError('Missing optional dependency for video processing,'
491
+ ' run `pip install av tqdm`')
492
+
493
+ class VideoWriter:
494
+ def __init__(self, filename, framerate, crf, compression_speed, codec):
495
+ self.filename = filename
496
+ self.framerate = framerate
497
+ self.crf = crf
498
+ self.compression_speed = compression_speed
499
+ self.codec = codec_aliases[codec]
500
+ self.container = av.open(filename, mode='w')
501
+ self.stream = self.container.add_stream(codec, rate=framerate)
502
+ self.stream.pix_fmt = 'yuv420p'
503
+ self.stream.options = {'crf': str(crf), 'preset': compression_speed}
504
+ self._closed = False
505
+ self.stream.width = 0
506
+ self.stream.height = 0
507
+
508
+ def write(self, frame):
509
+ if not isinstance(frame, av.VideoFrame):
510
+ if frame.ndim == 3 and frame.shape[-1] == 3:
511
+ frame = av.VideoFrame.from_ndarray(frame, format='rgb24')
512
+ elif frame.ndim == 2:
513
+ frame = av.VideoFrame.from_ndarray(frame, format='gray')
514
+ else:
515
+ raise ValueError(f'Frame must be (H, W) or (H, W, 3) but was {frame.shape}')
516
+ if self.stream.width == 0:
517
+ self.stream.width = frame.width
518
+ if self.stream.height == 0:
519
+ self.stream.height = frame.height
520
+ for packet in self.stream.encode(frame):
521
+ self.container.mux(packet)
522
+
523
+ def __enter__(self):
524
+ return self
525
+
526
+ def __exit__(self, exc_type, exc_val, exc_tb):
527
+ # Flush stream
528
+ for packet in self.stream.encode():
529
+ self.container.mux(packet)
530
+ self.container.close()
531
+ self._closed = True
532
+
533
+ return VideoWriter(filename, framerate, crf, compression_speed, codec)
534
+
535
+
450
536
  def save_video(data, filename, time_axis=0, color_axis=None, overwrite=False,
451
- dim_order='yx', framerate=30, crf=23, compression_speed='medium'):
537
+ dim_order='yx', framerate=30, crf=23, compression_speed='medium',
538
+ progress_bar=True, codec: Literal['libx264', 'libx265'] = 'libx264') -> None:
452
539
  """
453
540
  Save a 3D numpy array of greyscale values OR a 4D numpy array of RGB values as a video
454
541
 
@@ -467,23 +554,57 @@ def save_video(data, filename, time_axis=0, color_axis=None, overwrite=False,
467
554
  The axis of the data array that will be played as time in the video.
468
555
 
469
556
  color_axis : int or None, default None
470
- If not None, specifies the axis of the color channels (e.g., -1 for last axis, 1 for second axis).
471
- If None, data is assumed to be grayscale.
557
+ If not None, specifies the axis of the color channels (e.g., -1 for last axis,
558
+ 1 for second axis).
559
+ If None, data must be 3D (greyscale) or 4D with one length 3 axis (RGB).
472
560
 
473
561
  overwrite : bool, default False
474
562
  Whether to overwrite the file if it already exists.
475
563
 
476
564
  dim_order : 'yx' (default) or 'xy'
477
565
  The order of the spatial dimensions in the input data.
566
+
567
+ framerate : int, default 30
568
+ The frame rate of the video.
569
+
570
+ crf : int, default 23
571
+ Constant Rate Factor that specifies amount of lossiness allowed in compression.
572
+ Lower values produce better quality, larger videos. crf=17 has no human-visible
573
+ compression artifacts. File size approximately doubles/halves each time you
574
+ add/subtract 6 from crf, so crf=17 produces files about twice as large as
575
+ the default crf=23.
576
+
577
+ compression_speed : str, default 'medium'
578
+ Compression speed preset: 'ultrafast', 'superfast', 'veryfast',
579
+ 'faster', 'fast', 'medium', 'slow', 'slower', 'veryslow'.
580
+
581
+ progress_bar : bool, default True
582
+ If True, display a progress bar.
583
+
584
+ codec : Literal['libx264', 'libx265'], default 'libx264'
585
+ The video codec to use for encoding. Can be any of a number of aliases for
586
+ these two codecs, including avc1/h264 vs hevc/hvc1/hev1/h265.
478
587
  """
479
588
  try:
480
- import av
589
+ from tqdm import tqdm
481
590
  except ImportError:
482
- raise ImportError('To save videos, you must have PyAV installed. You can install it with "pip install av".')
591
+ raise ImportError('Missing optional dependency for video processing,'
592
+ ' run `pip install av tqdm`')
593
+
594
+ filename = os.path.expanduser(str(filename))
595
+ if filename.split('.')[-1].lower() not in ['mp4', 'mkv', 'avi', 'mov']:
596
+ filename += '.mp4'
597
+ if os.path.exists(filename) and not overwrite:
598
+ raise FileExistsError(f'File {filename} already exists. '
599
+ 'Set overwrite=True to overwrite.')
483
600
 
601
+ if color_axis is None and data.ndim == 4:
602
+ color_axis = find_channel_axis(data, possible_channel_lengths=3)
603
+ if color_axis is None:
604
+ raise ValueError('4D input data must have an RGB (length 3) axis.')
484
605
  if color_axis is not None:
485
606
  if data.ndim != 4:
486
- raise ValueError('Input data must be a 4D numpy array for color video.')
607
+ raise ValueError('Input data must be 4D when color_axis is specified.')
487
608
  # Move time axis to 0, color axis to -1
488
609
  data = np.moveaxis(data, time_axis, 0)
489
610
  if color_axis != -1:
@@ -493,22 +614,16 @@ def save_video(data, filename, time_axis=0, color_axis=None, overwrite=False,
493
614
  n_frames = data.shape[0]
494
615
  height, width, channels = data.shape[1:]
495
616
  if channels != 3:
496
- raise ValueError('Color video must have 3 channels (RGB).')
617
+ raise ValueError(f'Color video must have 3 channels (RGB) but had {channels}.')
497
618
  else:
498
619
  if data.ndim != 3:
499
- raise ValueError('Input data must be a 3D numpy array for grayscale video.')
620
+ raise ValueError('Input data must be 3D when color_axis is not specified.')
500
621
  data = np.moveaxis(data, time_axis, 0)
501
622
  if 'xy' in dim_order:
502
623
  data = data.swapaxes(1, 2)
503
624
  n_frames = data.shape[0]
504
625
  height, width = data.shape[1:]
505
626
 
506
- filename = os.path.expanduser(str(filename))
507
- if filename.split('.')[-1].lower() not in ['mp4', 'mkv', 'avi', 'mov']:
508
- filename += '.mp4'
509
- if os.path.exists(filename) and not overwrite:
510
- raise FileExistsError(f'File {filename} already exists. '
511
- 'Set overwrite=True to overwrite.')
512
627
  extension = filename.split('.')[-1].lower()
513
628
  if extension == 'mp4':
514
629
  pad = [[0, 0], [0, 0], [0, 0]]
@@ -519,25 +634,11 @@ def save_video(data, filename, time_axis=0, color_axis=None, overwrite=False,
519
634
  if pad != [[0, 0], [0, 0], [0, 0]]:
520
635
  data = np.pad(data, pad, mode='edge')
521
636
 
522
- container = av.open(filename, mode='w')
523
- stream = container.add_stream('libx264', rate=framerate)
524
- stream.pix_fmt = 'yuv420p'
525
- stream.options = {'crf': str(crf), 'preset': compression_speed}
526
- stream.height = data.shape[1]
527
- stream.width = data.shape[2]
528
-
529
- for frame_i in range(n_frames):
530
- if color_axis is not None:
531
- frame = av.VideoFrame.from_ndarray(data[frame_i], format='rgb24')
532
- else:
533
- frame = av.VideoFrame.from_ndarray(data[frame_i], format='gray')
534
- for packet in stream.encode(frame):
535
- container.mux(packet)
536
-
537
- for packet in stream.encode():
538
- container.mux(packet)
539
-
540
- container.close()
637
+ with video_writer(filename, framerate=framerate, crf=crf,
638
+ compression_speed=compression_speed, codec=codec) as writer:
639
+ for frame_i in tqdm(range(n_frames), total=n_frames,
640
+ desc='Saving video', disable=not progress_bar):
641
+ writer.write(data[frame_i])
541
642
 
542
643
 
543
644
  def show(data,
@@ -546,7 +647,7 @@ def show(data,
546
647
  mode: Literal['PIL', 'mpl'] = 'PIL',
547
648
  convert_to_8bit=True,
548
649
  channel_axis='guess',
549
- **kwargs):
650
+ **kwargs) -> None:
550
651
  """
551
652
  Display a numpy array of pixel values as an image. Supported types:
552
653
  1-channel (grayscale) : data.shape must be (y, x)
@@ -610,7 +711,9 @@ def show(data,
610
711
  imshow = show # Function name alias
611
712
 
612
713
 
613
- def find_channel_axis(data, expected_channel_axis=[0, -1]):
714
+ def find_channel_axis(data,
715
+ possible_channel_axes=[-1, 0],
716
+ possible_channel_lengths=[2, 3, 4]) -> Union[int, None]:
614
717
  """
615
718
  If the given numpy array has a shape suggesting that it has a
616
719
  channel (color) axis (that is, any axis with length 2 (2-color),
@@ -621,23 +724,28 @@ def find_channel_axis(data, expected_channel_axis=[0, -1]):
621
724
  data : numpy.ndarray
622
725
  The numpy array to check for a channel axis.
623
726
 
624
- expected_channel_axis : int or list of int, default [0, -1]
727
+ possible_channel_axes : int or list of int, default [0, -1]
625
728
  If None, any axis having length 2, 3, or 4 will be considered
626
729
  a channel axis.
627
730
  If an int, only that axis index will be checked.
628
731
  If a list of ints, all axes with those indices will be checked.
629
- The default value of [0, -1] checks the first and last axes, which is
732
+ The default value of [-1, 0] checks the last and first axes, which is
630
733
  almost always where a channel axis will be found.
631
734
 
735
+ possible_channel_lengths : int or list of int, default [2, 3, 4]
736
+ If an int, only that length will be considered a channel axis.
737
+ If a list of ints, an axis with any of those lengths will be considered
738
+ a channel axis.
739
+
632
740
  Returns
633
741
  -------
634
742
  int or None
635
743
  The index of the channel axis, or None if no channel axis was found.
636
744
 
637
- If expected_channel_axis is given, the returned value will be one of
638
- the expected_channel_axis values, or None if no channel axis was found.
745
+ If possible_channel_axes is given, the returned value will be one of
746
+ the possible_channel_axes values, or None if no channel axis was found.
639
747
 
640
- If expected_channel_axis is None, the returned value will be between
748
+ If possible_channel_axes is None, the returned value will be between
641
749
  0 and data.ndim - 1, inclusive, or None if no channel axis was found.
642
750
 
643
751
  Note that returning 0 means the channel axis was found and is the first
@@ -645,25 +753,24 @@ def find_channel_axis(data, expected_channel_axis=[0, -1]):
645
753
  because 0 will evaluate to False even though the data has a channel axis.
646
754
  Instead write `if find_channel_axis(data) is not None:`
647
755
  """
648
- if isinstance(expected_channel_axis, int):
649
- expected_channel_axis = [expected_channel_axis]
650
- if expected_channel_axis is None:
651
- expected_channel_axis = range(data.ndim)
652
- for axis in expected_channel_axis:
653
- if data.shape[axis] in [2, 3, 4]:
756
+ if isinstance(possible_channel_axes, int):
757
+ possible_channel_axes = [possible_channel_axes]
758
+ if possible_channel_axes is None:
759
+ possible_channel_axes = range(data.ndim)
760
+ if isinstance(possible_channel_lengths, int):
761
+ possible_channel_lengths = [possible_channel_lengths]
762
+ for axis in possible_channel_axes:
763
+ if data.shape[axis] in possible_channel_lengths:
654
764
  return axis
655
765
  return None
656
766
 
657
767
 
658
768
  # Flag to track if HEIF opener has been registered
659
769
  _heif_opener_registered = False
660
-
661
-
662
- def _ensure_heif_opener_registered():
770
+ def _ensure_heif_opener_registered() -> None:
663
771
  """Register the HEIF opener if not already registered."""
664
772
  global _heif_opener_registered
665
773
  if not _heif_opener_registered:
666
774
  from pillow_heif import register_heif_opener
667
775
  register_heif_opener()
668
776
  _heif_opener_registered = True
669
-
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: numpyimage
3
- Version: 2.4.2
3
+ Version: 2.5.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
@@ -695,6 +695,9 @@ Requires-Dist: tifffile
695
695
  Requires-Dist: pynrrd
696
696
  Requires-Dist: matplotlib
697
697
  Requires-Dist: opencv-python-headless
698
+ Provides-Extra: all
699
+ Requires-Dist: tqdm; extra == "all"
700
+ Requires-Dist: av; extra == "all"
698
701
  Dynamic: license-file
699
702
 
700
703
  # npimage
@@ -14,4 +14,5 @@ numpyimage.egg-info/dependency_links.txt
14
14
  numpyimage.egg-info/requires.txt
15
15
  numpyimage.egg-info/top_level.txt
16
16
  tests/test_heic.py
17
- tests/test_pbm.py
17
+ tests/test_pbm.py
18
+ tests/test_video.py
@@ -5,3 +5,7 @@ tifffile
5
5
  pynrrd
6
6
  matplotlib
7
7
  opencv-python-headless
8
+
9
+ [all]
10
+ tqdm
11
+ av
@@ -4,7 +4,7 @@ build-backend = 'setuptools.build_meta'
4
4
 
5
5
  [project]
6
6
  name = 'numpyimage'
7
- version = '2.4.2'
7
+ version = '2.5.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'
@@ -30,5 +30,11 @@ dependencies = [
30
30
  'opencv-python-headless',
31
31
  ]
32
32
 
33
+ [project.optional-dependencies]
34
+ all = [
35
+ "tqdm",
36
+ "av"
37
+ ]
38
+
33
39
  [tool.setuptools.packages.find]
34
40
  include = ['npimage']
@@ -0,0 +1,31 @@
1
+ #! /usr/bin/env python3
2
+
3
+ import numpy as np
4
+ import npimage
5
+ from pathlib import Path
6
+
7
+
8
+ def test_save_and_load_video(tmp_path=Path.cwd()):
9
+ # Create synthetic video data
10
+ data = np.full((120, 128, 256, 3), 255, dtype=np.uint8)
11
+ for i in range(120):
12
+ # Zero out the green (channel 1) channel along a moving line
13
+ npimage.drawline(
14
+ data[i],
15
+ pt1=(10 + i / 4, 10 + i, 1),
16
+ pt2=(90 + i / 4, 128 + i, 1),
17
+ value=0,
18
+ thickness=(3, 3, 1) # Thickness 1 along the color axis
19
+ )
20
+ video_path = tmp_path / 'test_video.mp4'
21
+ npimage.save_video(data, str(video_path), crf=15, overwrite=True)
22
+ loaded = npimage.load_video(str(video_path), progress_bar=False)
23
+ # Check shape
24
+ assert loaded.shape == data.shape
25
+ # Allow for some difference due to lossy encoding/decoding
26
+ assert np.allclose(loaded.mean(axis=-1), data.mean(axis=-1), atol=64)
27
+ print('test_save_and_load_video PASSED')
28
+
29
+
30
+ if __name__ == '__main__':
31
+ test_save_and_load_video()
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes