numpyimage 2.4.3__tar.gz → 2.6.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.3
3
+ Version: 2.6.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,57 @@ 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
- for i, frame in tqdm(enumerate(frame_iter, start=1), total=num_frames,
412
+ for i, frame in tqdm(enumerate(frame_iter), 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
+ if i == 0:
415
+ continue
416
+ img = frame.to_ndarray(format='rgb24')
415
417
  data[i] = img
416
- return data
418
+ if return_framerate:
419
+ return data, float(stream.average_rate)
420
+ else:
421
+ return data
417
422
 
418
423
 
419
- def lazy_load_video(filename, force_rgb=False):
424
+ def lazy_load_video(filename) -> Iterator[np.ndarray]:
420
425
  """
421
426
  Lazily load video frames as numpy arrays using PyAV.
422
427
 
@@ -424,31 +429,112 @@ def lazy_load_video(filename, force_rgb=False):
424
429
  ----------
425
430
  filename : str
426
431
  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
432
 
431
433
  Yields
432
434
  ------
433
435
  frame : np.ndarray
434
- Video frame as a numpy array, shape (height, width[, colors]).
436
+ Video frame as a numpy array, shape (height, width, colors).
435
437
  """
436
438
  try:
437
439
  import av
438
440
  except ImportError:
439
- raise ImportError('To use lazy_load_video, install PyAV: pip install av')
441
+ raise ImportError('Missing optional dependency for video processing,'
442
+ ' run `pip install av tqdm`')
440
443
  container = av.open(filename)
441
444
  stream = container.streams.video[0]
442
445
  for frame in container.decode(stream):
443
- if force_rgb:
444
- img = frame.to_ndarray(format='rgb24')
445
- else:
446
- img = frame.to_ndarray()
446
+ img = frame.to_ndarray(format='rgb24')
447
447
  yield img
448
448
 
449
449
 
450
+ codec_aliases = {
451
+ "libx264": "libx264",
452
+ "avc1": "libx264",
453
+ "h264": "libx264",
454
+ "H.264": "libx264",
455
+ "libx265": "libx265",
456
+ "hevc": "libx265",
457
+ "hvc1": "libx265",
458
+ "hev1": "libx265",
459
+ "h265": "libx265",
460
+ "H.265": "libx265",
461
+ }
462
+
463
+
464
+ class VideoWriter:
465
+ """
466
+ Create a video writer object for saving frames to a video file.
467
+
468
+ Parameters
469
+ ----------
470
+ filename : str
471
+ The filename to save the video to.
472
+ framerate : int, default 30
473
+ The frame rate of the video.
474
+ crf : int, default 23
475
+ Constant Rate Factor for encoding quality (lower is better quality).
476
+ compression_speed : str, default 'medium'
477
+ Compression speed preset: 'ultrafast', 'superfast', 'veryfast',
478
+ 'faster', 'fast', 'medium', 'slow', 'slower', 'veryslow'.
479
+ codec : Literal['libx264', 'libx265'], default 'libx264'
480
+ The video codec to use for encoding. Can be any of a number of aliases for
481
+ these two codecs, including avc1/h264 vs hevc/hvc1/hev1/h265.
482
+ """
483
+ def __init__(self, filename, framerate=30, crf=23, compression_speed='medium',
484
+ codec: Literal['libx264', 'libx265'] = 'libx264',
485
+ overwrite=False):
486
+ try:
487
+ import av
488
+ except ImportError:
489
+ raise ImportError('Missing optional dependency for video processing,'
490
+ ' run `pip install av tqdm`')
491
+ self.av = av
492
+ filename = os.path.expanduser(str(filename))
493
+ if os.path.exists(filename) and not overwrite:
494
+ raise FileExistsError(f'File {filename} already exists. '
495
+ 'Set overwrite=True to overwrite.')
496
+ self.filename = filename
497
+ self.framerate = framerate
498
+ self.crf = crf
499
+ self.compression_speed = compression_speed
500
+ self.codec = codec_aliases[codec.lower()]
501
+ self.container = av.open(filename, mode='w')
502
+ self.stream = self.container.add_stream(self.codec, rate=framerate)
503
+ self.stream.pix_fmt = 'yuv420p'
504
+ self.stream.options = {'crf': str(crf), 'preset': compression_speed}
505
+ self._closed = False
506
+ self.stream.width = 0
507
+ self.stream.height = 0
508
+
509
+ def write(self, frame):
510
+ if not isinstance(frame, self.av.VideoFrame):
511
+ if frame.ndim == 3 and frame.shape[-1] == 3:
512
+ frame = self.av.VideoFrame.from_ndarray(frame, format='rgb24')
513
+ elif frame.ndim == 2:
514
+ frame = self.av.VideoFrame.from_ndarray(frame, format='gray')
515
+ else:
516
+ raise ValueError(f'Frame must be (H, W) or (H, W, 3) but was {frame.shape}')
517
+ if self.stream.width == 0:
518
+ self.stream.width = frame.width
519
+ if self.stream.height == 0:
520
+ self.stream.height = frame.height
521
+ for packet in self.stream.encode(frame):
522
+ self.container.mux(packet)
523
+
524
+ def __enter__(self):
525
+ return self
526
+
527
+ def __exit__(self, exc_type, exc_val, exc_tb):
528
+ # Flush stream
529
+ for packet in self.stream.encode():
530
+ self.container.mux(packet)
531
+ self.container.close()
532
+ self._closed = True
533
+
534
+
450
535
  def save_video(data, filename, time_axis=0, color_axis=None, overwrite=False,
451
- dim_order='yx', framerate=30, crf=23, compression_speed='medium'):
536
+ dim_order='yx', framerate=30, crf=23, compression_speed='medium',
537
+ progress_bar=True, codec: Literal['libx264', 'libx265'] = 'libx264') -> None:
452
538
  """
453
539
  Save a 3D numpy array of greyscale values OR a 4D numpy array of RGB values as a video
454
540
 
@@ -467,23 +553,57 @@ def save_video(data, filename, time_axis=0, color_axis=None, overwrite=False,
467
553
  The axis of the data array that will be played as time in the video.
468
554
 
469
555
  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.
556
+ If not None, specifies the axis of the color channels (e.g., -1 for last axis,
557
+ 1 for second axis).
558
+ If None, data must be 3D (greyscale) or 4D with one length 3 axis (RGB).
472
559
 
473
560
  overwrite : bool, default False
474
561
  Whether to overwrite the file if it already exists.
475
562
 
476
563
  dim_order : 'yx' (default) or 'xy'
477
564
  The order of the spatial dimensions in the input data.
565
+
566
+ framerate : int, default 30
567
+ The frame rate of the video.
568
+
569
+ crf : int, default 23
570
+ Constant Rate Factor that specifies amount of lossiness allowed in compression.
571
+ Lower values produce better quality, larger videos. crf=17 has no human-visible
572
+ compression artifacts. File size approximately doubles/halves each time you
573
+ add/subtract 6 from crf, so crf=17 produces files about twice as large as
574
+ the default crf=23.
575
+
576
+ compression_speed : str, default 'medium'
577
+ Compression speed preset: 'ultrafast', 'superfast', 'veryfast',
578
+ 'faster', 'fast', 'medium', 'slow', 'slower', 'veryslow'.
579
+
580
+ progress_bar : bool, default True
581
+ If True, display a progress bar.
582
+
583
+ codec : Literal['libx264', 'libx265'], default 'libx264'
584
+ The video codec to use for encoding. Can be any of a number of aliases for
585
+ these two codecs, including avc1/h264 vs hevc/hvc1/hev1/h265.
478
586
  """
479
587
  try:
480
- import av
588
+ from tqdm import tqdm
481
589
  except ImportError:
482
- raise ImportError('To save videos, you must have PyAV installed. You can install it with "pip install av".')
590
+ raise ImportError('Missing optional dependency for video processing,'
591
+ ' run `pip install av tqdm`')
592
+
593
+ filename = os.path.expanduser(str(filename))
594
+ if filename.split('.')[-1].lower() not in ['mp4', 'mkv', 'avi', 'mov']:
595
+ filename += '.mp4'
596
+ if os.path.exists(filename) and not overwrite:
597
+ raise FileExistsError(f'File {filename} already exists. '
598
+ 'Set overwrite=True to overwrite.')
483
599
 
600
+ if color_axis is None and data.ndim == 4:
601
+ color_axis = find_channel_axis(data, possible_channel_lengths=3)
602
+ if color_axis is None:
603
+ raise ValueError('4D input data must have an RGB (length 3) axis.')
484
604
  if color_axis is not None:
485
605
  if data.ndim != 4:
486
- raise ValueError('Input data must be a 4D numpy array for color video.')
606
+ raise ValueError('Input data must be 4D when color_axis is specified.')
487
607
  # Move time axis to 0, color axis to -1
488
608
  data = np.moveaxis(data, time_axis, 0)
489
609
  if color_axis != -1:
@@ -493,22 +613,16 @@ def save_video(data, filename, time_axis=0, color_axis=None, overwrite=False,
493
613
  n_frames = data.shape[0]
494
614
  height, width, channels = data.shape[1:]
495
615
  if channels != 3:
496
- raise ValueError('Color video must have 3 channels (RGB).')
616
+ raise ValueError(f'Color video must have 3 channels (RGB) but had {channels}.')
497
617
  else:
498
618
  if data.ndim != 3:
499
- raise ValueError('Input data must be a 3D numpy array for grayscale video.')
619
+ raise ValueError('Input data must be 3D when color_axis is not specified.')
500
620
  data = np.moveaxis(data, time_axis, 0)
501
621
  if 'xy' in dim_order:
502
622
  data = data.swapaxes(1, 2)
503
623
  n_frames = data.shape[0]
504
624
  height, width = data.shape[1:]
505
625
 
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
626
  extension = filename.split('.')[-1].lower()
513
627
  if extension == 'mp4':
514
628
  pad = [[0, 0], [0, 0], [0, 0]]
@@ -519,25 +633,12 @@ def save_video(data, filename, time_axis=0, color_axis=None, overwrite=False,
519
633
  if pad != [[0, 0], [0, 0], [0, 0]]:
520
634
  data = np.pad(data, pad, mode='edge')
521
635
 
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()
636
+ with VideoWriter(filename, framerate=framerate, crf=crf,
637
+ compression_speed=compression_speed, codec=codec,
638
+ overwrite=overwrite) 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.3
3
+ Version: 2.6.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.3'
7
+ version = '2.6.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