Skip to content

Commit 1ebfad0

Browse files
authored
HDR-correct dtype handling for the Blocks API (#1625)
1 parent a8c084f commit 1ebfad0

20 files changed

Lines changed: 385 additions & 153 deletions

src/torchcodec/_core/BetaCudaDeviceInterface.cpp

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1208,8 +1208,6 @@ void BetaCudaDeviceInterface::convert_av_frame_to_frame_output(
12081208
producer_stream = get_current_cuda_stream(device_.index());
12091209
}
12101210

1211-
// TODO_API_BREAKDOWN P1: we don't suppor output_dtype so some of that is not
1212-
// execrcized.
12131211
auto convert_frame = [&](std::optional<torch::stable::Tensor> pre_alloc)
12141212
-> torch::stable::Tensor {
12151213
return convert_yuv_frame_to_rgb(

src/torchcodec/_core/ColorConverter.cpp

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,28 @@
1515

1616
namespace facebook::torchcodec {
1717

18-
ColorConverter::ColorConverter(const StableDevice& device) {
18+
ColorConverter::ColorConverter(
19+
const StableDevice& device,
20+
OutputDtypeConfig output_dtype_config)
21+
: device_(device), output_dtype_config_(output_dtype_config) {
1922
device_interface_ = create_device_interface(device);
2023
STD_TORCH_CHECK(
2124
device_interface_ != nullptr,
2225
"Failed to create device interface. This should never happen, please report.");
26+
}
27+
28+
void ColorConverter::maybe_initialize_interface(OutputDtype output_dtype) {
29+
// Interface initialization is done per-frame, not in the constructor: with
30+
// AUTO, the desired output dtype is only known once we see a frame, and it
31+
// can differ from one frame to the next.
32+
if (initialized_output_dtype_.has_value() &&
33+
*initialized_output_dtype_ == output_dtype) {
34+
return;
35+
}
2336

2437
VideoStreamOptions options;
25-
options.output_dtype = OutputDtype::UINT8; // dtype not exposed yet
26-
options.device = device;
38+
options.output_dtype = output_dtype;
39+
options.device = device_;
2740

2841
// TODO_API_BREAKDOWN P1 It seems unnatural that the color-converter needs its
2942
// own device_interface_, but at the same time the color-conversion *must* be
@@ -32,13 +45,18 @@ ColorConverter::ColorConverter(const StableDevice& device) {
3245
std::vector<std::unique_ptr<Transform>> no_transforms;
3346
device_interface_->initialize_color_conversion(
3447
options, no_transforms, /*resized_output_dims=*/std::nullopt);
48+
initialized_output_dtype_ = output_dtype;
3549
}
3650

3751
torch::stable::Tensor ColorConverter::convert(const AVFrame& av_frame) {
52+
OutputDtype output_dtype = resolve_output_dtype(
53+
output_dtype_config_, static_cast<AVPixelFormat>(av_frame.format));
54+
maybe_initialize_interface(output_dtype);
55+
3856
FrameOutput frame_output;
3957
device_interface_->convert_av_frame_to_frame_output(
4058
av_frame, frame_output, std::nullopt);
41-
return frame_output.data;
59+
return convert_to_output_dtype(frame_output.data, output_dtype);
4260
}
4361

4462
} // namespace facebook::torchcodec

src/torchcodec/_core/ColorConverter.h

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,23 +7,31 @@
77
#pragma once
88

99
#include <memory>
10+
#include <optional>
1011
#include <string_view>
1112

1213
#include "DeviceInterface.h"
1314
#include "FFMPEGCommon.h"
1415
#include "StableABICompat.h"
16+
#include "StreamOptions.h"
1517

1618
namespace facebook::torchcodec {
1719

1820
class FORCE_PUBLIC_VISIBILITY ColorConverter {
1921
public:
2022
explicit ColorConverter(
21-
const StableDevice& device = StableDevice(kStableCPU));
23+
const StableDevice& device = StableDevice(kStableCPU),
24+
OutputDtypeConfig output_dtype_config = OutputDtypeConfig::UINT8);
2225

2326
torch::stable::Tensor convert(const AVFrame& av_frame);
2427

2528
private:
29+
void maybe_initialize_interface(OutputDtype output_dtype);
30+
2631
std::unique_ptr<DeviceInterface> device_interface_;
32+
StableDevice device_;
33+
OutputDtypeConfig output_dtype_config_;
34+
std::optional<OutputDtype> initialized_output_dtype_;
2735
};
2836

2937
} // namespace facebook::torchcodec

src/torchcodec/_core/FFMPEGCommon.cpp

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,25 @@ bool is_nvdec_16bit_surface(int format) {
5555
}
5656
#endif
5757

58+
OutputDtype resolve_output_dtype(
59+
OutputDtypeConfig output_dtype_config,
60+
AVPixelFormat pix_fmt) {
61+
switch (output_dtype_config) {
62+
case OutputDtypeConfig::UINT8:
63+
return OutputDtype::UINT8;
64+
case OutputDtypeConfig::FLOAT32:
65+
return OutputDtype::FLOAT32;
66+
case OutputDtypeConfig::AUTO: {
67+
// TODO_HDR: This is basically our heuristic that defines how we identify
68+
// HDR videos, we might want to refine it.
69+
const AVPixFmtDescriptor* desc = av_pix_fmt_desc_get(pix_fmt);
70+
return (desc != nullptr && desc->comp[0].depth > 8) ? OutputDtype::FLOAT32
71+
: OutputDtype::UINT8;
72+
}
73+
}
74+
return OutputDtype::UINT8;
75+
}
76+
5877
AutoAVPacket::AutoAVPacket() : av_packet_(av_packet_alloc()) {
5978
STD_TORCH_CHECK(av_packet_ != nullptr, "Couldn't allocate avPacket.");
6079
}

src/torchcodec/_core/FFMPEGCommon.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
#include <string>
1313
#include <vector>
1414

15+
#include "StreamOptions.h"
16+
1517
extern "C" {
1618
#include <libavcodec/avcodec.h>
1719
#include <libavcodec/bsf.h>
@@ -71,6 +73,10 @@ namespace facebook::torchcodec {
7173
AVPixelFormat nvdec_pix_fmt(bool is_p016_surface, int bit_depth);
7274
bool is_nvdec_16bit_surface(int format);
7375

76+
OutputDtype resolve_output_dtype(
77+
OutputDtypeConfig output_dtype_config,
78+
AVPixelFormat pix_fmt);
79+
7480
// FFMPEG uses special delete functions for some structures. These template
7581
// functions are used to pass into unique_ptr as custom deleters so we can
7682
// wrap FFMPEG structs with unique_ptrs for ease of use.

src/torchcodec/_core/Frame.cpp

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,4 +53,23 @@ torch::stable::Tensor allocate_empty_hwc_tensor(
5353
}
5454
}
5555

56+
torch::stable::Tensor convert_to_output_dtype(
57+
const torch::stable::Tensor& tensor,
58+
OutputDtype output_dtype) {
59+
bool is_uint16 = tensor.scalar_type() == kStableUInt16;
60+
61+
if (output_dtype == OutputDtype::FLOAT32) {
62+
double max_val = is_uint16 ? 65535.0 : 255.0;
63+
return stable_div(torch::stable::to(tensor, kStableFloat32), max_val);
64+
}
65+
66+
if (!is_uint16) {
67+
return tensor;
68+
}
69+
// uint16 -> uint8. 257 is 65535 / 255.
70+
return torch::stable::to(
71+
stable_div(torch::stable::to(tensor, kStableFloat32), 257.0),
72+
kStableUInt8);
73+
}
74+
5675
} // namespace facebook::torchcodec

src/torchcodec/_core/Frame.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,4 +69,8 @@ torch::stable::Tensor allocate_empty_hwc_tensor(
6969
OutputDtype output_dtype,
7070
std::optional<int> num_frames = std::nullopt);
7171

72+
torch::stable::Tensor convert_to_output_dtype(
73+
const torch::stable::Tensor& tensor,
74+
OutputDtype output_dtype);
75+
7276
} // namespace facebook::torchcodec

src/torchcodec/_core/PacketDecoder.cpp

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -71,11 +71,17 @@ PacketDecoder::PacketDecoder(
7171
stream, av_codec, device_interface_.get(), ffmpeg_thread_count);
7272
device_interface_->initialize(codec_context_);
7373

74+
const AVPixFmtDescriptor* stream_desc =
75+
av_pix_fmt_desc_get(codec_context_->pix_fmt);
76+
int stream_bit_depth = stream_desc ? stream_desc->comp[0].depth : 8;
77+
7478
VideoStreamOptions options;
75-
// TODO_API_BREAKDOWN P1: Need to design and figure out behavior of Blocks
76-
// with HDR data.
77-
options.output_dtype = OutputDtype::UINT8; // dtype not exposed yet
7879
options.device = device;
80+
// This is ugly: what we actually mean is "let the device interface decode
81+
// into the native surface", which matters for NVDEC.
82+
// TODO_API_BREAKDOWN P2: Find a cleaner way to express this?
83+
options.output_dtype =
84+
stream_bit_depth > 8 ? OutputDtype::FLOAT32 : OutputDtype::UINT8;
7985

8086
device_interface_->initialize_video_decoding(
8187
stream, demuxer.format_context(), options);
@@ -135,6 +141,8 @@ FramePlanes frame_to_planes(
135141
result.colorspace = colorspace_name ? colorspace_name : "unknown";
136142
result.color_range = color_range_name ? color_range_name : "unknown";
137143

144+
result.bit_depth = desc->comp[0].depth;
145+
138146
for (int c = 0; c < desc->nb_components; ++c) {
139147
const AVComponentDescriptor& comp = desc->comp[c];
140148
int64_t bytes_per_sample = (comp.depth > 8) ? 2 : 1;

src/torchcodec/_core/PacketDecoder.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ struct FramePlanes {
7373
std::string pix_fmt;
7474
std::string colorspace;
7575
std::string color_range;
76+
int64_t bit_depth = 8;
7677
};
7778

7879
FORCE_PUBLIC_VISIBILITY FramePlanes frame_to_planes(

src/torchcodec/_core/SingleStreamDecoder.cpp

Lines changed: 15 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -559,26 +559,9 @@ void SingleStreamDecoder::add_video_stream(
559559
active_stream_index_, custom_frame_mappings.value());
560560
}
561561

562-
// Resolve the user-facing OutputDtypeConfig (which may be AUTO) into an
563-
// OutputDtype that downstream code can use directly.
564-
// TODO_HDR: This is basically our heuristic that defines how we identify HDR
565-
// videos, we might want to refine it.
566-
switch (stream_info.video_stream_options.output_dtype_config) {
567-
case OutputDtypeConfig::UINT8:
568-
stream_info.video_stream_options.output_dtype = OutputDtype::UINT8;
569-
break;
570-
case OutputDtypeConfig::FLOAT32:
571-
stream_info.video_stream_options.output_dtype = OutputDtype::FLOAT32;
572-
break;
573-
case OutputDtypeConfig::AUTO: {
574-
const AVPixFmtDescriptor* desc = av_pix_fmt_desc_get(
575-
static_cast<AVPixelFormat>(stream_info.stream->codecpar->format));
576-
stream_info.video_stream_options.output_dtype =
577-
(desc != nullptr && desc->comp[0].depth > 8) ? OutputDtype::FLOAT32
578-
: OutputDtype::UINT8;
579-
break;
580-
}
581-
}
562+
stream_info.video_stream_options.output_dtype = resolve_output_dtype(
563+
stream_info.video_stream_options.output_dtype_config,
564+
static_cast<AVPixelFormat>(stream_info.stream->codecpar->format));
582565

583566
// Set preRotationDims_ for the active stream. These are the raw encoded
584567
// dimensions from FFmpeg, used as a fallback for tensor pre-allocation when
@@ -678,7 +661,7 @@ void SingleStreamDecoder::add_audio_stream(
678661
FrameOutput SingleStreamDecoder::get_next_frame() {
679662
auto output = get_next_frame_internal();
680663
if (stream_infos_[active_stream_index_].av_media_type == AVMEDIA_TYPE_VIDEO) {
681-
output.data = maybe_permute_and_convert_to_float32(output.data);
664+
output.data = maybe_permute_and_convert_dtype(output.data);
682665
}
683666
return output;
684667
}
@@ -695,7 +678,7 @@ FrameOutput SingleStreamDecoder::get_next_frame_internal(
695678

696679
FrameOutput SingleStreamDecoder::get_frame_at_index(int64_t frame_index) {
697680
auto frame_output = get_frame_at_index_internal(frame_index);
698-
frame_output.data = maybe_permute_and_convert_to_float32(frame_output.data);
681+
frame_output.data = maybe_permute_and_convert_dtype(frame_output.data);
699682
return frame_output;
700683
}
701684

@@ -805,7 +788,7 @@ FrameBatchOutput SingleStreamDecoder::get_frames_at_indices(
805788
previous_index_in_video = index_in_video;
806789
}
807790
frame_batch_output.data =
808-
maybe_permute_and_convert_to_float32(frame_batch_output.data);
791+
maybe_permute_and_convert_dtype(frame_batch_output.data);
809792
return frame_batch_output;
810793
}
811794

@@ -855,7 +838,7 @@ FrameBatchOutput SingleStreamDecoder::get_frames_in_range(
855838
frame_batch_output_duration_seconds[f] = frame_output.duration_seconds;
856839
}
857840
frame_batch_output.data =
858-
maybe_permute_and_convert_to_float32(frame_batch_output.data);
841+
maybe_permute_and_convert_dtype(frame_batch_output.data);
859842
return frame_batch_output;
860843
}
861844

@@ -897,7 +880,7 @@ FrameOutput SingleStreamDecoder::get_frame_played_at(double seconds) {
897880

898881
// Convert the frame to tensor.
899882
FrameOutput frame_output = convert_av_frame_to_frame_output(*av_frame);
900-
frame_output.data = maybe_permute_and_convert_to_float32(frame_output.data);
883+
frame_output.data = maybe_permute_and_convert_dtype(frame_output.data);
901884
return frame_output;
902885
}
903886

@@ -987,7 +970,7 @@ FrameBatchOutput SingleStreamDecoder::get_frames_played_in_range(
987970
device_interface_->get_pre_allocation_dtype(
988971
video_stream_options.output_dtype));
989972
frame_batch_output.data =
990-
maybe_permute_and_convert_to_float32(frame_batch_output.data);
973+
maybe_permute_and_convert_dtype(frame_batch_output.data);
991974
return frame_batch_output;
992975
}
993976

@@ -1061,7 +1044,7 @@ FrameBatchOutput SingleStreamDecoder::get_frames_played_in_range(
10611044
}
10621045

10631046
frame_batch_output.data =
1064-
maybe_permute_and_convert_to_float32(frame_batch_output.data);
1047+
maybe_permute_and_convert_dtype(frame_batch_output.data);
10651048
return frame_batch_output;
10661049
} else {
10671050
// Note that we look at nextPts for a frame, and not its pts or duration.
@@ -1098,7 +1081,7 @@ FrameBatchOutput SingleStreamDecoder::get_frames_played_in_range(
10981081
frame_batch_output_duration_seconds[f] = frame_output.duration_seconds;
10991082
}
11001083
frame_batch_output.data =
1101-
maybe_permute_and_convert_to_float32(frame_batch_output.data);
1084+
maybe_permute_and_convert_dtype(frame_batch_output.data);
11021085

11031086
return frame_batch_output;
11041087
}
@@ -1692,7 +1675,7 @@ FrameOutput SingleStreamDecoder::convert_av_frame_to_frame_output(
16921675
// OUTPUT ALLOCATION AND SHAPE CONVERSION
16931676
// --------------------------------------------------------------------------
16941677

1695-
torch::stable::Tensor SingleStreamDecoder::maybe_permute_and_convert_to_float32(
1678+
torch::stable::Tensor SingleStreamDecoder::maybe_permute_and_convert_dtype(
16961679
torch::stable::Tensor& hwc_tensor) {
16971680
// Permute HWC to CHW if needed. Returns a view of the input tensor, the
16981681
// leading batch-dimension [N] is optional i.e. the input tensor can be 3D or
@@ -1718,19 +1701,9 @@ torch::stable::Tensor SingleStreamDecoder::maybe_permute_and_convert_to_float32(
17181701
}
17191702
}
17201703

1721-
// Convert to float32 and normalize to [0, 1] if needed.
1722-
OutputDtype output_dtype =
1723-
stream_infos_[active_stream_index_].video_stream_options.output_dtype;
1724-
if (output_dtype != OutputDtype::FLOAT32) {
1725-
return tensor;
1726-
}
1727-
bool is_uint16 =
1728-
tensor.scalar_type() == torch::headeronly::ScalarType::UInt16;
1729-
double max_val = static_cast<double>(
1730-
is_uint16 ? std::numeric_limits<uint16_t>::max()
1731-
: std::numeric_limits<uint8_t>::max());
1732-
auto as_float = torch::stable::to(tensor, kStableFloat32);
1733-
return stable_div(as_float, max_val);
1704+
return convert_to_output_dtype(
1705+
tensor,
1706+
stream_infos_[active_stream_index_].video_stream_options.output_dtype);
17341707
}
17351708

17361709
// --------------------------------------------------------------------------

0 commit comments

Comments
 (0)