numpyimage 3.5.2__tar.gz → 3.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.
- {numpyimage-3.5.2/numpyimage.egg-info → numpyimage-3.6.0}/PKG-INFO +1 -1
- {numpyimage-3.5.2 → numpyimage-3.6.0}/npimage/graphics.py +14 -7
- {numpyimage-3.5.2 → numpyimage-3.6.0}/npimage/vidio.py +133 -21
- {numpyimage-3.5.2 → numpyimage-3.6.0/numpyimage.egg-info}/PKG-INFO +1 -1
- {numpyimage-3.5.2 → numpyimage-3.6.0}/numpyimage.egg-info/SOURCES.txt +1 -0
- {numpyimage-3.5.2 → numpyimage-3.6.0}/pyproject.toml +1 -1
- numpyimage-3.6.0/tests/test_video_streamer.py +92 -0
- {numpyimage-3.5.2 → numpyimage-3.6.0}/LICENSE +0 -0
- {numpyimage-3.5.2 → numpyimage-3.6.0}/README.md +0 -0
- {numpyimage-3.5.2 → numpyimage-3.6.0}/npimage/__init__.py +0 -0
- {numpyimage-3.5.2 → numpyimage-3.6.0}/npimage/align.py +0 -0
- {numpyimage-3.5.2 → numpyimage-3.6.0}/npimage/imageio.py +0 -0
- {numpyimage-3.5.2 → numpyimage-3.6.0}/npimage/nrrd_utils.py +0 -0
- {numpyimage-3.5.2 → numpyimage-3.6.0}/npimage/operations.py +0 -0
- {numpyimage-3.5.2 → numpyimage-3.6.0}/npimage/utils.py +0 -0
- {numpyimage-3.5.2 → numpyimage-3.6.0}/numpyimage.egg-info/dependency_links.txt +0 -0
- {numpyimage-3.5.2 → numpyimage-3.6.0}/numpyimage.egg-info/requires.txt +0 -0
- {numpyimage-3.5.2 → numpyimage-3.6.0}/numpyimage.egg-info/top_level.txt +0 -0
- {numpyimage-3.5.2 → numpyimage-3.6.0}/setup.cfg +0 -0
- {numpyimage-3.5.2 → numpyimage-3.6.0}/tests/test_heic.py +0 -0
- {numpyimage-3.5.2 → numpyimage-3.6.0}/tests/test_pbm.py +0 -0
- {numpyimage-3.5.2 → numpyimage-3.6.0}/tests/test_video_writers.py +0 -0
|
@@ -637,13 +637,20 @@ def get_voxels_within_distance(distance, center=None, ndims=None,
|
|
|
637
637
|
"""
|
|
638
638
|
Generates a list of voxel coordinates within a given distance of a given
|
|
639
639
|
center point.
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
640
|
+
|
|
641
|
+
Parameters
|
|
642
|
+
----------
|
|
643
|
+
center : tuple
|
|
644
|
+
Voxel coordinate to be used as the center. Defaults to the origin.
|
|
645
|
+
|
|
646
|
+
distance : float
|
|
647
|
+
How far away from the center point to include.
|
|
648
|
+
|
|
649
|
+
metric : string, default 'euclidian'
|
|
650
|
+
How to calculate distance. Options are:
|
|
651
|
+
- euclidian (default)
|
|
652
|
+
- manhattan (sum of coordinates)
|
|
653
|
+
- chebyshev (max of coordinates)
|
|
647
654
|
"""
|
|
648
655
|
assert metric in ['euclidian', 'manhattan', 'chebyshev']
|
|
649
656
|
|
|
@@ -40,9 +40,22 @@ codec_aliases = {
|
|
|
40
40
|
'hev1': 'libx265',
|
|
41
41
|
'h265': 'libx265',
|
|
42
42
|
'H.265': 'libx265',
|
|
43
|
+
'libvpx': 'libvpx',
|
|
44
|
+
'vp8': 'libvpx',
|
|
45
|
+
'libvpx-vp9': 'libvpx-vp9',
|
|
46
|
+
'vp9': 'libvpx-vp9',
|
|
43
47
|
}
|
|
44
48
|
|
|
45
|
-
|
|
49
|
+
# Default codec for each container format
|
|
50
|
+
extension_default_codecs = {
|
|
51
|
+
'mp4': 'libx264',
|
|
52
|
+
'mkv': 'libx264',
|
|
53
|
+
'avi': 'libx264',
|
|
54
|
+
'mov': 'libx264',
|
|
55
|
+
'webm': 'libvpx-vp9',
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
supported_extensions = ['mp4', 'mkv', 'avi', 'mov', 'webm', 'gif']
|
|
46
59
|
|
|
47
60
|
|
|
48
61
|
def _import_av():
|
|
@@ -79,6 +92,30 @@ def load_video(filename,
|
|
|
79
92
|
framerate : float
|
|
80
93
|
The frame rate of the video in frames per second
|
|
81
94
|
"""
|
|
95
|
+
extension = Path(filename).suffix.lower().lstrip('.')
|
|
96
|
+
if extension == 'gif':
|
|
97
|
+
from PIL import Image
|
|
98
|
+
frames = []
|
|
99
|
+
durations = []
|
|
100
|
+
with Image.open(filename) as img:
|
|
101
|
+
while True:
|
|
102
|
+
frames.append(np.array(img.convert('RGB')))
|
|
103
|
+
durations.append(img.info.get('duration', None))
|
|
104
|
+
try:
|
|
105
|
+
img.seek(img.tell() + 1)
|
|
106
|
+
except EOFError:
|
|
107
|
+
break
|
|
108
|
+
data = np.array(frames)
|
|
109
|
+
if return_framerate:
|
|
110
|
+
if None in durations:
|
|
111
|
+
raise ValueError('Cannot return framerate for GIF because one or'
|
|
112
|
+
' more frames are missing duration metadata')
|
|
113
|
+
if len(durations) == 0:
|
|
114
|
+
return data, None
|
|
115
|
+
framerate = 1000 / np.mean(durations)
|
|
116
|
+
return data, float(framerate)
|
|
117
|
+
return data
|
|
118
|
+
|
|
82
119
|
av = _import_av()
|
|
83
120
|
with av.open(filename) as container:
|
|
84
121
|
stream = container.streams.video[0]
|
|
@@ -120,7 +157,7 @@ def load_video(filename,
|
|
|
120
157
|
|
|
121
158
|
def lazy_load_video(filename) -> Iterator[np.ndarray]:
|
|
122
159
|
"""
|
|
123
|
-
Lazily load video frames as numpy arrays using PyAV
|
|
160
|
+
Lazily load video frames as numpy arrays using PyAV (or using PIL for gifs)
|
|
124
161
|
|
|
125
162
|
This iterator yields images in the order they appear in the video.
|
|
126
163
|
If you want reasonably fast random access to arbitrary frames in
|
|
@@ -136,6 +173,19 @@ def lazy_load_video(filename) -> Iterator[np.ndarray]:
|
|
|
136
173
|
frame : np.ndarray
|
|
137
174
|
Video frame as a numpy array, shape (height, width, colors).
|
|
138
175
|
"""
|
|
176
|
+
extension = Path(filename).suffix.lower().lstrip('.')
|
|
177
|
+
if extension == 'gif':
|
|
178
|
+
from PIL import Image
|
|
179
|
+
with Image.open(filename) as img:
|
|
180
|
+
while True:
|
|
181
|
+
img_array = np.array(img.convert('RGB'))
|
|
182
|
+
yield img_array
|
|
183
|
+
try:
|
|
184
|
+
img.seek(img.tell() + 1)
|
|
185
|
+
except EOFError:
|
|
186
|
+
break
|
|
187
|
+
return
|
|
188
|
+
|
|
139
189
|
av = _import_av()
|
|
140
190
|
rotation = _get_rotation_from_metadata(filename)
|
|
141
191
|
with av.open(Path(filename).expanduser()) as container:
|
|
@@ -172,6 +222,10 @@ class VideoStreamer:
|
|
|
172
222
|
If 'auto', the index is cached only if building it takes
|
|
173
223
|
more than 0.5 seconds, which only happens for ~1+ GB videos.
|
|
174
224
|
"""
|
|
225
|
+
extension = Path(filename).suffix.lower().lstrip('.')
|
|
226
|
+
if extension == 'gif':
|
|
227
|
+
raise NotImplementedError('Streaming from gifs not yet implemented. '
|
|
228
|
+
'Use load_video() or lazy_load_video() instead.')
|
|
175
229
|
self.av = _import_av()
|
|
176
230
|
self.verbose = verbose
|
|
177
231
|
self.filename = Path(filename).expanduser()
|
|
@@ -210,10 +264,11 @@ class VideoStreamer:
|
|
|
210
264
|
|
|
211
265
|
with self.av.open(str(self.filename)) as container:
|
|
212
266
|
stream = container.streams.video[0]
|
|
267
|
+
start_pts = stream.start_time if stream.start_time is not None else 0
|
|
213
268
|
# Access as packets to only read metadata (fast), not pixel data (slow)
|
|
214
269
|
for packet in tqdm(container.demux(stream), total=stream.frames,
|
|
215
270
|
desc='Indexing frames', disable=not self.verbose):
|
|
216
|
-
if packet.pts is not None:
|
|
271
|
+
if packet.pts is not None and packet.pts >= start_pts:
|
|
217
272
|
frames_pts.append(packet.pts)
|
|
218
273
|
|
|
219
274
|
# demux gave us packets in the order they appeared in the file
|
|
@@ -256,7 +311,7 @@ class VideoStreamer:
|
|
|
256
311
|
pts_deltas = np.diff(frames_pts) if len(frames_pts) > 1 else None
|
|
257
312
|
if pts_deltas is not None and (pts_deltas == pts_deltas[0]).all():
|
|
258
313
|
# The video is constant framerate
|
|
259
|
-
self.pts_delta = pts_deltas[0]
|
|
314
|
+
self.pts_delta = int(pts_deltas[0])
|
|
260
315
|
self._framerate = 1 / (self.pts_delta * self.time_base)
|
|
261
316
|
if self._framerate.denominator == 1:
|
|
262
317
|
self._framerate = self._framerate.numerator
|
|
@@ -572,15 +627,20 @@ class AVVideoWriter:
|
|
|
572
627
|
compression_speed : str, default 'medium'
|
|
573
628
|
Compression speed preset: 'ultrafast', 'superfast', 'veryfast',
|
|
574
629
|
'faster', 'fast', 'medium', 'slow', 'slower', 'veryslow'.
|
|
575
|
-
codec :
|
|
576
|
-
The video codec to use for encoding.
|
|
577
|
-
|
|
630
|
+
codec : str or None, default None
|
|
631
|
+
The video codec to use for encoding. If None, automatically chosen based
|
|
632
|
+
on the file extension (e.g. libx264 for .mp4, libvpx-vp9 for .webm).
|
|
633
|
+
Accepts aliases like h264, h265, vp8, vp9, etc.
|
|
578
634
|
"""
|
|
579
635
|
def __init__(self, filename, framerate=30, crf=23, compression_speed='medium',
|
|
580
|
-
codec: Literal['libx264', 'libx265'
|
|
636
|
+
codec: Literal['libx264', 'libx265', 'libvpx', 'libvpx-vp9', None] = None,
|
|
581
637
|
overwrite=False):
|
|
582
638
|
self.av = _import_av()
|
|
583
639
|
filename = Path(filename).expanduser()
|
|
640
|
+
extension = filename.suffix.lower().lstrip('.')
|
|
641
|
+
if extension == 'gif':
|
|
642
|
+
raise NotImplementedError('Saving to GIF format not yet implemented. '
|
|
643
|
+
'Use save_video() instead.')
|
|
584
644
|
if filename.exists() and not overwrite:
|
|
585
645
|
raise FileExistsError(f'File {filename} already exists. '
|
|
586
646
|
'Set overwrite=True to overwrite.')
|
|
@@ -588,11 +648,20 @@ class AVVideoWriter:
|
|
|
588
648
|
self._framerate = utils.limit_fraction(framerate)
|
|
589
649
|
self.crf = crf
|
|
590
650
|
self.compression_speed = compression_speed
|
|
591
|
-
|
|
651
|
+
|
|
652
|
+
if codec is None:
|
|
653
|
+
ext = filename.suffix.lower().lstrip('.')
|
|
654
|
+
self.codec = extension_default_codecs.get(ext, 'libx264')
|
|
655
|
+
else:
|
|
656
|
+
self.codec = codec_aliases[codec.lower()]
|
|
657
|
+
|
|
592
658
|
self.container = self.av.open(filename, mode='w')
|
|
593
659
|
self.stream = self.container.add_stream(self.codec, rate=self._framerate)
|
|
594
660
|
self.stream.pix_fmt = 'yuv420p'
|
|
595
|
-
self.
|
|
661
|
+
if self.codec in ('libvpx', 'libvpx-vp9'):
|
|
662
|
+
self.stream.options = {'crf': str(crf), 'b:v': '0'}
|
|
663
|
+
else:
|
|
664
|
+
self.stream.options = {'crf': str(crf), 'preset': compression_speed}
|
|
596
665
|
self._closed = False
|
|
597
666
|
self.stream.width = 0
|
|
598
667
|
self.stream.height = 0
|
|
@@ -676,14 +745,15 @@ class FFmpegVideoWriter:
|
|
|
676
745
|
compression_speed : str, default 'medium'
|
|
677
746
|
Compression speed preset: 'ultrafast', 'superfast', 'veryfast',
|
|
678
747
|
'faster', 'fast', 'medium', 'slow', 'slower', 'veryslow'.
|
|
679
|
-
codec :
|
|
680
|
-
The video codec to use for encoding.
|
|
681
|
-
|
|
748
|
+
codec : str or None, default None
|
|
749
|
+
The video codec to use for encoding. If None, automatically chosen based
|
|
750
|
+
on the file extension (e.g. libx264 for .mp4, libvpx-vp9 for .webm).
|
|
751
|
+
Accepts aliases like h264, h265, vp8, vp9, etc.
|
|
682
752
|
overwrite : bool, default False
|
|
683
753
|
Whether to overwrite the file if it already exists.
|
|
684
754
|
"""
|
|
685
755
|
def __init__(self, filename, framerate=30, crf=23, compression_speed='medium',
|
|
686
|
-
codec: Literal['libx264', 'libx265'
|
|
756
|
+
codec: Literal['libx264', 'libx265', 'libvpx', 'libvpx-vp9', None] = None,
|
|
687
757
|
overwrite=False):
|
|
688
758
|
filename = Path(filename).expanduser()
|
|
689
759
|
if filename.exists() and not overwrite:
|
|
@@ -693,7 +763,12 @@ class FFmpegVideoWriter:
|
|
|
693
763
|
self._framerate = utils.limit_fraction(framerate)
|
|
694
764
|
self.crf = crf
|
|
695
765
|
self.compression_speed = compression_speed
|
|
696
|
-
|
|
766
|
+
|
|
767
|
+
if codec is None:
|
|
768
|
+
ext = filename.suffix.lower().lstrip('.')
|
|
769
|
+
self.codec = extension_default_codecs.get(ext, 'libx264')
|
|
770
|
+
else:
|
|
771
|
+
self.codec = codec_aliases[codec.lower()]
|
|
697
772
|
|
|
698
773
|
# Initialize process state
|
|
699
774
|
self._process = None
|
|
@@ -725,9 +800,13 @@ class FFmpegVideoWriter:
|
|
|
725
800
|
'-c:v', self.codec,
|
|
726
801
|
'-pix_fmt', 'yuv420p', # Output pixel format
|
|
727
802
|
'-crf', str(self.crf),
|
|
728
|
-
'-preset', self.compression_speed,
|
|
729
|
-
self.filename
|
|
730
803
|
]
|
|
804
|
+
if self.codec in ('libvpx', 'libvpx-vp9'):
|
|
805
|
+
# VP8/VP9 need -b:v 0 for constant quality (CRF) mode
|
|
806
|
+
command += ['-b:v', '0']
|
|
807
|
+
else:
|
|
808
|
+
command += ['-preset', self.compression_speed]
|
|
809
|
+
command.append(str(self.filename))
|
|
731
810
|
|
|
732
811
|
# Start FFmpeg process
|
|
733
812
|
self._process = subprocess.Popen(
|
|
@@ -823,7 +902,9 @@ VideoWriter = FFmpegVideoWriter
|
|
|
823
902
|
|
|
824
903
|
def save_video(data, filename, time_axis=0, color_axis=None, overwrite=False,
|
|
825
904
|
dim_order='yx', framerate=30, crf=23, compression_speed='medium',
|
|
826
|
-
progress_bar=True, codec: Literal['libx264', 'libx265'
|
|
905
|
+
progress_bar=True, codec: Literal['libx264', 'libx265', 'libvpx',
|
|
906
|
+
'libvpx-vp9', None] = None
|
|
907
|
+
) -> None:
|
|
827
908
|
"""
|
|
828
909
|
Save a 3D numpy array of greyscale values OR a 4D numpy array of RGB values as a video
|
|
829
910
|
|
|
@@ -869,9 +950,10 @@ def save_video(data, filename, time_axis=0, color_axis=None, overwrite=False,
|
|
|
869
950
|
progress_bar : bool, default True
|
|
870
951
|
If True, display a progress bar.
|
|
871
952
|
|
|
872
|
-
codec :
|
|
873
|
-
The video codec to use for encoding.
|
|
874
|
-
|
|
953
|
+
codec : str or None, default None
|
|
954
|
+
The video codec to use for encoding. If None, automatically chosen based
|
|
955
|
+
on the file extension (e.g. libx264 for .mp4, libvpx-vp9 for .webm).
|
|
956
|
+
Accepts aliases like h264, h265, vp8, vp9, etc.
|
|
875
957
|
"""
|
|
876
958
|
|
|
877
959
|
filename = str(filename)
|
|
@@ -920,6 +1002,36 @@ def save_video(data, filename, time_axis=0, color_axis=None, overwrite=False,
|
|
|
920
1002
|
if pad != [[0, 0], [0, 0], [0, 0]]:
|
|
921
1003
|
data = np.pad(data, pad, mode='edge')
|
|
922
1004
|
|
|
1005
|
+
if extension == 'gif':
|
|
1006
|
+
from PIL import Image
|
|
1007
|
+
pil_images = [Image.fromarray(frame) for frame in data]
|
|
1008
|
+
|
|
1009
|
+
# GIF frame delays must be multiples of 10ms, so GIF doesn't
|
|
1010
|
+
# natively suppport framerates like 30 fps which require an
|
|
1011
|
+
# inter-frame delay of 33.33 ms. To support arbitrary
|
|
1012
|
+
# framerates, we copy the solution used in ffmpeg which is to
|
|
1013
|
+
# use variable inter-frame intervals: 30 ms some frames and 40 ms
|
|
1014
|
+
# others to achieve an average interval of 33.33 ms (for the
|
|
1015
|
+
# example of 30 fps). Implementation of alternating delays:
|
|
1016
|
+
ideal_delay_cs = 100 / framerate
|
|
1017
|
+
lo = int(ideal_delay_cs)
|
|
1018
|
+
hi = lo + 1
|
|
1019
|
+
durations_ms = []
|
|
1020
|
+
accumulated = 0.0
|
|
1021
|
+
for _ in range(len(pil_images)):
|
|
1022
|
+
accumulated += ideal_delay_cs
|
|
1023
|
+
if accumulated >= hi:
|
|
1024
|
+
durations_ms.append(hi * 10)
|
|
1025
|
+
accumulated -= hi
|
|
1026
|
+
else:
|
|
1027
|
+
durations_ms.append(lo * 10)
|
|
1028
|
+
accumulated -= lo
|
|
1029
|
+
|
|
1030
|
+
pil_images[0].save(filename, format='GIF', save_all=True,
|
|
1031
|
+
append_images=pil_images[1:],
|
|
1032
|
+
duration=durations_ms, loop=0)
|
|
1033
|
+
return
|
|
1034
|
+
|
|
923
1035
|
with VideoWriter(filename, framerate=framerate, crf=crf,
|
|
924
1036
|
compression_speed=compression_speed, codec=codec,
|
|
925
1037
|
overwrite=overwrite) as writer:
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Tests for VideoStreamer, particularly handling of HEVC videos with
|
|
4
|
+
negative-PTS priming packets (common in iPhone recordings).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
import subprocess
|
|
9
|
+
import tempfile
|
|
10
|
+
|
|
11
|
+
import numpy as np
|
|
12
|
+
|
|
13
|
+
import npimage
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def test_negative_pts_priming_packet():
|
|
17
|
+
"""
|
|
18
|
+
Test that VideoStreamer correctly handles HEVC videos where the first
|
|
19
|
+
packet has a negative PTS (a "priming" packet used for B-frame prediction
|
|
20
|
+
that doesn't produce a decoded frame).
|
|
21
|
+
|
|
22
|
+
This is common in iPhone HEVC recordings. The bug was that the index was
|
|
23
|
+
built from demuxed packets, which included the priming packet's negative
|
|
24
|
+
PTS. When trying to access frame 0, it would seek to that negative PTS,
|
|
25
|
+
which no decoded frame ever has, causing a VideoSeekError.
|
|
26
|
+
"""
|
|
27
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
28
|
+
video_path = os.path.join(tmpdir, 'test_negative_pts.mp4')
|
|
29
|
+
|
|
30
|
+
# Create a small HEVC video with a negative-PTS priming packet.
|
|
31
|
+
# The -output_ts_offset shifts timestamps so the first keyframe packet
|
|
32
|
+
# gets a negative PTS, mimicking iPhone HEVC behavior.
|
|
33
|
+
result = subprocess.run([
|
|
34
|
+
'ffmpeg', '-y',
|
|
35
|
+
'-f', 'lavfi', '-i', 'color=c=red:size=320x240:rate=30:d=1',
|
|
36
|
+
'-c:v', 'libx265', '-preset', 'ultrafast',
|
|
37
|
+
'-bf', '2', '-x265-params', 'bframes=2',
|
|
38
|
+
'-tag:v', 'hvc1',
|
|
39
|
+
'-output_ts_offset', '-0.033',
|
|
40
|
+
video_path
|
|
41
|
+
], capture_output=True, text=True)
|
|
42
|
+
assert result.returncode == 0, f'ffmpeg failed: {result.stderr}'
|
|
43
|
+
|
|
44
|
+
# Verify the test video actually has the problematic structure:
|
|
45
|
+
# more packets than decoded frames due to the priming packet
|
|
46
|
+
import av
|
|
47
|
+
with av.open(video_path) as container:
|
|
48
|
+
stream = container.streams.video[0]
|
|
49
|
+
pkt_pts = sorted(
|
|
50
|
+
p.pts for p in container.demux(stream) if p.pts is not None
|
|
51
|
+
)
|
|
52
|
+
with av.open(video_path) as container:
|
|
53
|
+
stream = container.streams.video[0]
|
|
54
|
+
frame_pts = [f.pts for f in container.decode(stream)]
|
|
55
|
+
|
|
56
|
+
assert len(pkt_pts) > len(frame_pts), (
|
|
57
|
+
f'Test video should have more packets ({len(pkt_pts)}) than '
|
|
58
|
+
f'decoded frames ({len(frame_pts)}). The test setup may need '
|
|
59
|
+
f'updating if ffmpeg behavior has changed.'
|
|
60
|
+
)
|
|
61
|
+
assert pkt_pts[0] < 0, (
|
|
62
|
+
f'First packet PTS should be negative, got {pkt_pts[0]}'
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
# Now test that VideoStreamer handles this correctly
|
|
66
|
+
vid = npimage.VideoStreamer(video_path)
|
|
67
|
+
|
|
68
|
+
# The number of frames should match decoded frames, not packets
|
|
69
|
+
assert vid.n_frames == len(frame_pts), (
|
|
70
|
+
f'Expected {len(frame_pts)} frames, got {vid.n_frames}'
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
# Accessing frame 0 should work without raising VideoSeekError
|
|
74
|
+
frame = vid[0]
|
|
75
|
+
assert isinstance(frame, np.ndarray)
|
|
76
|
+
assert frame.shape == (240, 320, 3)
|
|
77
|
+
|
|
78
|
+
# Accessing the last frame should also work
|
|
79
|
+
last_frame = vid[vid.n_frames - 1]
|
|
80
|
+
assert isinstance(last_frame, np.ndarray)
|
|
81
|
+
|
|
82
|
+
print(f'PASSED: Loaded {vid.n_frames} frames from video with '
|
|
83
|
+
f'negative-PTS priming packet')
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def main():
|
|
87
|
+
test_negative_pts_priming_packet()
|
|
88
|
+
print('\nAll VideoStreamer tests passed.')
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
if __name__ == '__main__':
|
|
92
|
+
main()
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|