Skip to content

Commit bddaa7c

Browse files
authored
move HDR tests to VideoDecoder (#1450)
1 parent 69c7c3a commit bddaa7c

10 files changed

Lines changed: 174 additions & 176 deletions

src/torchcodec/_core/_decoder_utils.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,7 @@ def create_video_decoder(
166166
device_variant: str = "default",
167167
transforms: Sequence[DecoderTransform | nn.Module] | None = None,
168168
custom_frame_mappings: tuple[Tensor, Tensor, Tensor] | None = None,
169+
output_dtype: str = "uint8",
169170
) -> tuple[Tensor, int, VideoStreamMetadata]:
170171

171172
decoder = create_decoder(source=source, seek_mode=seek_mode)
@@ -191,6 +192,7 @@ def create_video_decoder(
191192
device_variant=device_variant,
192193
transform_specs=transform_specs,
193194
custom_frame_mappings=custom_frame_mappings,
195+
output_dtype=output_dtype,
194196
)
195197

196198
return (decoder, stream_index, metadata)

src/torchcodec/_core/ops.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -84,8 +84,8 @@ def add_video_stream(
8484
custom_frame_mappings: (
8585
tuple[torch.Tensor, torch.Tensor, torch.Tensor] | None
8686
) = None,
87-
# TODO_HDR: should default be None or uint8??
88-
output_dtype: str | None = None,
87+
# TODO_HDR
88+
output_dtype: str = "uint8",
8989
) -> None:
9090
custom_frame_mappings_pts: torch.Tensor | None = None
9191
custom_frame_mappings_keyframe_indices: torch.Tensor | None = None
@@ -430,7 +430,7 @@ def _add_video_stream_abstract(
430430
custom_frame_mappings_duration: torch.Tensor | None = None,
431431
custom_frame_mappings_keyframe_indices: torch.Tensor | None = None,
432432
color_conversion_library: str | None = None,
433-
output_dtype: str | None = None,
433+
output_dtype: str = "uint8",
434434
) -> None:
435435
return
436436

@@ -448,7 +448,7 @@ def add_video_stream_abstract(
448448
custom_frame_mappings_pts: torch.Tensor | None = None,
449449
custom_frame_mappings_duration: torch.Tensor | None = None,
450450
custom_frame_mappings_keyframe_indices: torch.Tensor | None = None,
451-
output_dtype: str | None = None,
451+
output_dtype: str = "uint8",
452452
) -> None:
453453
return
454454

src/torchcodec/decoders/_video_decoder.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ class VideoDecoder:
125125
:class:`~torchcodec.transforms.DecoderTransform` and
126126
:class:`~torchvision.transforms.v2.Transform`
127127
objects. Read more about this parameter in :ref:`sphx_glr_generated_examples_decoding_transforms.py`.
128+
output_dtype: TODO_HDR: add docs for output_dtype
128129
custom_frame_mappings (str, bytes, or file-like object, optional):
129130
Mapping of frames to their metadata, typically generated via ffprobe.
130131
This enables accurate frame seeking without requiring a full video scan.
@@ -168,6 +169,8 @@ def __init__(
168169
device: str | torch_device | None = None,
169170
seek_mode: Literal["exact", "approximate"] = "exact",
170171
transforms: Sequence[DecoderTransform | nn.Module] | None = None,
172+
# TODO_HDR
173+
output_dtype: torch.dtype | Literal["auto"] = torch.uint8,
171174
custom_frame_mappings: (
172175
str | bytes | io.RawIOBase | io.BufferedReader | None
173176
) = None,
@@ -205,6 +208,15 @@ def __init__(
205208
if num_ffmpeg_threads is None:
206209
raise ValueError(f"{num_ffmpeg_threads = } should be an int.")
207210

211+
_DTYPE_TO_STR = {torch.uint8: "uint8", torch.float32: "float32"}
212+
if output_dtype != "auto":
213+
if output_dtype not in _DTYPE_TO_STR:
214+
raise ValueError(
215+
f"Invalid output_dtype ({output_dtype}). "
216+
f"Supported values are torch.uint8, torch.float32, and 'auto'."
217+
)
218+
output_dtype = _DTYPE_TO_STR[output_dtype]
219+
208220
device_variant = _get_cuda_backend()
209221
if device is None:
210222
device = str(torch.get_default_device())
@@ -224,6 +236,7 @@ def __init__(
224236
device_variant=device_variant,
225237
transforms=transforms,
226238
custom_frame_mappings=custom_frame_mappings_data,
239+
output_dtype=output_dtype,
227240
)
228241

229242
assert self.metadata.begin_stream_seconds is not None # mypy.

test/generate_reference_resources.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ def generate_nasa_13013_references_by_index():
151151
# Note: The naming scheme used here must match the naming scheme used to load
152152
# tensors in ./utils.py.
153153
streams = [0, 3]
154-
frames = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 15, 20, 25, 30, 35, 386, 387, 388, 389]
154+
frames = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 15, 20, 25, 30, 35, 386, 387, 388, 389]
155155
for stream in streams:
156156
for frame in frames:
157157
generate_frame_by_index(NASA_VIDEO, frame_index=frame, stream_index=stream)
@@ -216,9 +216,18 @@ def generate_av1_video_references():
216216

217217
def generate_hdr_references_rgb48():
218218
frames = [0, 5, 10]
219-
for video in (NASA_VIDEO_HDR, TEST_SRC_2_720P_HDR, TEST_SRC_2_12BIT_HDR):
219+
for video in (
220+
NASA_VIDEO,
221+
NASA_VIDEO_HDR,
222+
TEST_SRC_2_720P_HDR,
223+
TEST_SRC_2_12BIT_HDR,
224+
):
220225
for frame in frames:
221-
generate_frame_by_index_rgb48(video, frame_index=frame, stream_index=0)
226+
generate_frame_by_index_rgb48(
227+
video,
228+
frame_index=frame,
229+
stream_index=video.default_stream_index,
230+
)
222231

223232

224233
def generate_hdr_references_rgb24():
761 KB
Binary file not shown.
761 KB
Binary file not shown.
382 KB
Binary file not shown.
761 KB
Binary file not shown.

test/test_decoders.py

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,11 +42,13 @@
4242
H265_10BITS,
4343
H265_VIDEO,
4444
in_fbcode,
45+
IS_WINDOWS,
4546
make_video_decoder,
4647
NASA_AUDIO,
4748
NASA_AUDIO_MP3,
4849
NASA_AUDIO_MP3_44100,
4950
NASA_VIDEO,
51+
NASA_VIDEO_HDR,
5052
NASA_VIDEO_ROTATED,
5153
needs_cuda,
5254
needs_ffmpeg_cli,
@@ -57,8 +59,10 @@
5759
SINE_MONO_S32_44100,
5860
SINE_MONO_S32_8000,
5961
TEST_NON_ZERO_START,
62+
TEST_SRC_2_12BIT_HDR,
6063
TEST_SRC_2_720P,
6164
TEST_SRC_2_720P_H265,
65+
TEST_SRC_2_720P_HDR,
6266
TEST_SRC_2_720P_MPEG4,
6367
TEST_SRC_2_720P_VP8,
6468
TEST_SRC_2_720P_VP9,
@@ -2360,6 +2364,145 @@ def test_beta_backend_still_supported_for_bc(self):
23602364
dec[0]
23612365
assert dec.cpu_fallback._backend == "CUDA"
23622366

2367+
@staticmethod
2368+
def _assert_float32_frame_matches_rgb48_ref(frame_data, asset, frame_index):
2369+
frame_as_uint16 = (frame_data * 65535).round().to(torch.uint16)
2370+
ref = asset.get_frame_data_by_index_rgb48(frame_index)
2371+
torch.testing.assert_close(frame_as_uint16, ref, rtol=0, atol=0)
2372+
2373+
@pytest.mark.xfail(
2374+
IS_WINDOWS and ffmpeg_major_version < 5,
2375+
reason="swscale YUV->RGB differs on Windows + FFmpeg 4",
2376+
)
2377+
@pytest.mark.parametrize(
2378+
"asset",
2379+
(NASA_VIDEO, NASA_VIDEO_HDR, TEST_SRC_2_720P_HDR, TEST_SRC_2_12BIT_HDR),
2380+
)
2381+
def test_output_dtype_uint8(self, asset):
2382+
decoder = VideoDecoder(asset.path, output_dtype=torch.uint8)
2383+
frame_indices = [0, 5, 10]
2384+
for frame_index in frame_indices:
2385+
frame = decoder[frame_index]
2386+
assert frame.dtype == torch.uint8
2387+
assert_frames_equal(frame.data, asset.get_frame_data_by_index(frame_index))
2388+
2389+
@pytest.mark.xfail(
2390+
IS_WINDOWS and ffmpeg_major_version < 5,
2391+
reason="swscale YUV->RGB48 differs on Windows + FFmpeg 4",
2392+
)
2393+
@pytest.mark.parametrize(
2394+
"asset",
2395+
(NASA_VIDEO, NASA_VIDEO_HDR, TEST_SRC_2_720P_HDR, TEST_SRC_2_12BIT_HDR),
2396+
)
2397+
def test_output_dtype_float32(self, asset):
2398+
decoder = VideoDecoder(asset.path, output_dtype=torch.float32)
2399+
frame_indices = [0, 5, 10]
2400+
for frame_index in frame_indices:
2401+
frame = decoder[frame_index]
2402+
assert frame.dtype == torch.float32
2403+
2404+
self._assert_float32_frame_matches_rgb48_ref(frame.data, asset, frame_index)
2405+
2406+
@pytest.mark.xfail(
2407+
IS_WINDOWS and ffmpeg_major_version < 5,
2408+
reason="swscale YUV->RGB48 differs on Windows + FFmpeg 4",
2409+
)
2410+
@pytest.mark.parametrize(
2411+
"asset, is_hdr",
2412+
(
2413+
(NASA_VIDEO, False),
2414+
(NASA_VIDEO_HDR, True),
2415+
(TEST_SRC_2_720P_HDR, True),
2416+
(TEST_SRC_2_12BIT_HDR, True),
2417+
),
2418+
)
2419+
def test_output_dtype_auto(self, asset, is_hdr):
2420+
decoder = VideoDecoder(asset.path, output_dtype="auto")
2421+
frame_indices = [0, 5, 10]
2422+
for frame_index in frame_indices:
2423+
frame = decoder[frame_index]
2424+
2425+
if is_hdr:
2426+
assert frame.dtype == torch.float32
2427+
frame_as_uint16 = (frame.data * 65535).round().to(torch.uint16)
2428+
ref = asset.get_frame_data_by_index_rgb48(frame_index)
2429+
torch.testing.assert_close(frame_as_uint16, ref, rtol=0, atol=0)
2430+
else:
2431+
assert frame.dtype == torch.uint8
2432+
ref = asset.get_frame_data_by_index(frame_index)
2433+
assert_frames_equal(frame.data, ref)
2434+
2435+
@pytest.mark.xfail(
2436+
IS_WINDOWS and ffmpeg_major_version < 5,
2437+
reason="swscale YUV->RGB48 differs on Windows + FFmpeg 4",
2438+
)
2439+
@pytest.mark.parametrize(
2440+
"asset",
2441+
(NASA_VIDEO, NASA_VIDEO_HDR, TEST_SRC_2_720P_HDR, TEST_SRC_2_12BIT_HDR),
2442+
)
2443+
def test_output_dtype_float32_batch_apis(self, asset):
2444+
decoder = VideoDecoder(asset.path, output_dtype=torch.float32)
2445+
indices = [0, 5, 10]
2446+
2447+
# get_frame_at
2448+
self._assert_float32_frame_matches_rgb48_ref(
2449+
decoder.get_frame_at(0).data, asset, 0
2450+
)
2451+
2452+
# get_frames_at
2453+
frames = decoder.get_frames_at(indices)
2454+
for i, idx in enumerate(indices):
2455+
self._assert_float32_frame_matches_rgb48_ref(frames.data[i], asset, idx)
2456+
2457+
# get_frames_in_range
2458+
frames_range = decoder.get_frames_in_range(start=5, stop=11)
2459+
self._assert_float32_frame_matches_rgb48_ref(frames_range.data[0], asset, 5)
2460+
self._assert_float32_frame_matches_rgb48_ref(frames_range.data[5], asset, 10)
2461+
2462+
@pytest.mark.xfail(
2463+
IS_WINDOWS and ffmpeg_major_version < 5,
2464+
reason="swscale YUV->RGB48 differs on Windows + FFmpeg 4",
2465+
)
2466+
@pytest.mark.parametrize(
2467+
"asset",
2468+
(NASA_VIDEO, NASA_VIDEO_HDR, TEST_SRC_2_720P_HDR, TEST_SRC_2_12BIT_HDR),
2469+
)
2470+
def test_output_dtype_float32_pts_apis(self, asset):
2471+
decoder = VideoDecoder(asset.path, output_dtype=torch.float32)
2472+
indices = [0, 5, 10]
2473+
2474+
pts_seconds_ref = [decoder.get_frame_at(i).pts_seconds for i in indices]
2475+
2476+
# get_frame_played_at
2477+
for pts, idx in zip(pts_seconds_ref, indices):
2478+
frame = decoder.get_frame_played_at(pts)
2479+
self._assert_float32_frame_matches_rgb48_ref(frame.data, asset, idx)
2480+
2481+
# get_frames_played_in_range (full range)
2482+
frames = decoder.get_frames_played_in_range(
2483+
start_seconds=0,
2484+
stop_seconds=pts_seconds_ref[-1] + 1e-4,
2485+
)
2486+
for idx in indices:
2487+
self._assert_float32_frame_matches_rgb48_ref(frames.data[idx], asset, idx)
2488+
2489+
# get_frames_played_in_range (single-frame ranges)
2490+
for pts, idx in zip(pts_seconds_ref, indices):
2491+
frames = decoder.get_frames_played_in_range(
2492+
start_seconds=pts, stop_seconds=pts + 1e-4
2493+
)
2494+
self._assert_float32_frame_matches_rgb48_ref(frames.data[0], asset, idx)
2495+
2496+
# get_frames_played_at
2497+
frames = decoder.get_frames_played_at(pts_seconds_ref)
2498+
for i, idx in enumerate(indices):
2499+
self._assert_float32_frame_matches_rgb48_ref(frames.data[i], asset, idx)
2500+
2501+
@pytest.mark.parametrize("bad_dtype", (torch.float64, torch.int32, "not_a_dtype"))
2502+
def test_output_dtype_invalid(self, bad_dtype):
2503+
with pytest.raises(ValueError, match="Invalid output_dtype"):
2504+
VideoDecoder(NASA_VIDEO.path, output_dtype=bad_dtype)
2505+
23632506

23642507
class TestAudioDecoder:
23652508
@pytest.mark.parametrize(

0 commit comments

Comments
 (0)