From c17b9d8db005a9d1dd3056dad5b51a64a6fcf7c0 Mon Sep 17 00:00:00 2001 From: jinhohwang-meta Date: Thu, 8 May 2025 09:13:19 -0700 Subject: [PATCH 01/14] add opencv benchmark --- benchmarks/decoders/benchmark_decoders.py | 2 + .../decoders/benchmark_decoders_library.py | 68 +++++++++++++++++++ 2 files changed, 70 insertions(+) diff --git a/benchmarks/decoders/benchmark_decoders.py b/benchmarks/decoders/benchmark_decoders.py index c01fd6fa..908785ba 100644 --- a/benchmarks/decoders/benchmark_decoders.py +++ b/benchmarks/decoders/benchmark_decoders.py @@ -28,6 +28,7 @@ TorchCodecPublic, TorchCodecPublicNonBatch, TorchVision, + OpenCVDecoder, ) @@ -61,6 +62,7 @@ class DecoderKind: {"backend": "video_reader"}, ), "torchaudio": DecoderKind("TorchAudio", TorchAudioDecoder), + "opencv": DecoderKind("OpenCV", OpenCVDecoder), } diff --git a/benchmarks/decoders/benchmark_decoders_library.py b/benchmarks/decoders/benchmark_decoders_library.py index 9b25c6fe..907ea2ba 100644 --- a/benchmarks/decoders/benchmark_decoders_library.py +++ b/benchmarks/decoders/benchmark_decoders_library.py @@ -145,6 +145,74 @@ def decode_and_resize(self, video_file, pts_list, height, width, device): ] return frames +class OpenCVDecoder(AbstractDecoder): + def __init__(self): + import cv2.videoio_registry as vr + + self._print_each_iteration_time = False + api_pref = None + for backend in vr.getStreamBufferedBackends(): + if not vr.hasBackend(backend): + continue + if not vr.isBackendBuiltIn(backend): + _, abi, api = vr.getStreamBufferedBackendPluginVersion(backend) + if (abi < 1 or (abi == 1 and api < 2)): + continue + api_pref = backend + break + self._backend = api_pref + + def decode_frames(self, video_file, pts_list): + import cv2 + + cap = cv2.VideoCapture(video_file, self._backend, []) + if not cap.isOpened(): + raise ValueError("Could not open video stream") + + fps = cap.get(cv2.CAP_PROP_FPS) + frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) + approx_frame_numbers = [int(pts * fps) for pts in pts_list] + + current_frame = 0 + frames = [] + while True: + ok = cap.grab() + if not ok: + break + if current_frame in approx_frame_numbers: # only decompress needed + ret, frame = cap.retrieve() + if ret: + frames.append(frame) + + if len(frames) == len(approx_frame_numbers): + break + current_frame += 1 + cap.release() + return frames + + def decode_first_n_frames(self, video_file, n): + import cv2 + + cap = cv2.VideoCapture(video_file, self._backend, []) + if not cap.isOpened(): + raise ValueError("Could not open video stream") + + frames = [] + for i in range(n): + ok = cap.grab() + if not ok: + break + ret, frame = cap.retrieve() + if ret: + frames.append(frame) + cap.release() + return frames + + def decode_and_resize(self, video_file, pts_list, height, width, device): + import cv2 + frames = [cv2.resize(frame, (width, height)) for frame in self.decode_frames(video_file, pts_list)] + return frames + class TorchCodecCore(AbstractDecoder): def __init__(self, num_threads=None, color_conversion_library=None, device="cpu"): From 478136fa359e72231060ed05facc810ada0aa757 Mon Sep 17 00:00:00 2001 From: jinhohwang-meta Date: Tue, 13 May 2025 07:51:37 -0700 Subject: [PATCH 02/14] reflect comments --- benchmarks/decoders/benchmark_decoders_library.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/benchmarks/decoders/benchmark_decoders_library.py b/benchmarks/decoders/benchmark_decoders_library.py index 907ea2ba..7fe46eab 100644 --- a/benchmarks/decoders/benchmark_decoders_library.py +++ b/benchmarks/decoders/benchmark_decoders_library.py @@ -151,6 +151,7 @@ def __init__(self): self._print_each_iteration_time = False api_pref = None + # Check backend abi/api for compatibility for backend in vr.getStreamBufferedBackends(): if not vr.hasBackend(backend): continue @@ -170,24 +171,24 @@ def decode_frames(self, video_file, pts_list): raise ValueError("Could not open video stream") fps = cap.get(cv2.CAP_PROP_FPS) - frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) - approx_frame_numbers = [int(pts * fps) for pts in pts_list] + approx_frame_indices = [int(pts * fps) for pts in pts_list] current_frame = 0 frames = [] while True: ok = cap.grab() if not ok: - break - if current_frame in approx_frame_numbers: # only decompress needed + raise ValueError("Could not grab video frame") + if current_frame in approx_frame_indices: # only decompress needed ret, frame = cap.retrieve() if ret: frames.append(frame) - if len(frames) == len(approx_frame_numbers): + if len(frames) == len(approx_frame_indices): break current_frame += 1 cap.release() + assert len(frames) == len(approx_frame_indices) return frames def decode_first_n_frames(self, video_file, n): @@ -201,11 +202,12 @@ def decode_first_n_frames(self, video_file, n): for i in range(n): ok = cap.grab() if not ok: - break + raise ValueError("Could not grab video frame") ret, frame = cap.retrieve() if ret: frames.append(frame) cap.release() + assert len(frames) == n return frames def decode_and_resize(self, video_file, pts_list, height, width, device): From 70f57270b9136e554dd4c16d58c8ce895983db1a Mon Sep 17 00:00:00 2001 From: Daniel Flores Date: Tue, 3 Jun 2025 17:27:50 -0700 Subject: [PATCH 03/14] Remove backend checking from OpenCV benchmark --- benchmarks.png | Bin 0 -> 41355 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 benchmarks.png diff --git a/benchmarks.png b/benchmarks.png new file mode 100644 index 0000000000000000000000000000000000000000..a05719185e801fdfe7222573e8782797e212b462 GIT binary patch literal 41355 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 Jc$@Z~_&+r>KcN5s literal 0 HcmV?d00001 From d85192c45cdb15dc95eecba4d8257d8c284db0ff Mon Sep 17 00:00:00 2001 From: Daniel Flores Date: Tue, 3 Jun 2025 17:27:50 -0700 Subject: [PATCH 04/14] Remove backend checking from OpenCV benchmark From d5e74d7cab5f80f7fe0c056857ccf62f5bb83281 Mon Sep 17 00:00:00 2001 From: Daniel Flores Date: Wed, 4 Jun 2025 07:10:28 -0700 Subject: [PATCH 05/14] Set ffmpeg as default backend for OpenCV --- benchmarks/decoders/benchmark_decoders.py | 6 ++-- .../decoders/benchmark_decoders_library.py | 30 ++++++++----------- 2 files changed, 17 insertions(+), 19 deletions(-) diff --git a/benchmarks/decoders/benchmark_decoders.py b/benchmarks/decoders/benchmark_decoders.py index 908785ba..57669d12 100644 --- a/benchmarks/decoders/benchmark_decoders.py +++ b/benchmarks/decoders/benchmark_decoders.py @@ -18,6 +18,7 @@ AbstractDecoder, DecordAccurate, DecordAccurateBatch, + OpenCVDecoder, plot_data, run_benchmarks, TorchAudioDecoder, @@ -28,7 +29,6 @@ TorchCodecPublic, TorchCodecPublicNonBatch, TorchVision, - OpenCVDecoder, ) @@ -62,7 +62,9 @@ class DecoderKind: {"backend": "video_reader"}, ), "torchaudio": DecoderKind("TorchAudio", TorchAudioDecoder), - "opencv": DecoderKind("OpenCV", OpenCVDecoder), + "opencv": DecoderKind( + "OpenCV[backend=FFMPEG]", OpenCVDecoder, {"backend": "FFMPEG"} + ), } diff --git a/benchmarks/decoders/benchmark_decoders_library.py b/benchmarks/decoders/benchmark_decoders_library.py index 7fe46eab..e49d3424 100644 --- a/benchmarks/decoders/benchmark_decoders_library.py +++ b/benchmarks/decoders/benchmark_decoders_library.py @@ -145,28 +145,20 @@ def decode_and_resize(self, video_file, pts_list, height, width, device): ] return frames + class OpenCVDecoder(AbstractDecoder): - def __init__(self): - import cv2.videoio_registry as vr + def __init__(self, backend): + import cv2 + + self._available_backends = {"FFMPEG": cv2.CAP_FFMPEG} + self._backend = self._available_backends.get(backend) self._print_each_iteration_time = False - api_pref = None - # Check backend abi/api for compatibility - for backend in vr.getStreamBufferedBackends(): - if not vr.hasBackend(backend): - continue - if not vr.isBackendBuiltIn(backend): - _, abi, api = vr.getStreamBufferedBackendPluginVersion(backend) - if (abi < 1 or (abi == 1 and api < 2)): - continue - api_pref = backend - break - self._backend = api_pref def decode_frames(self, video_file, pts_list): import cv2 - cap = cv2.VideoCapture(video_file, self._backend, []) + cap = cv2.VideoCapture(video_file, self._backend) if not cap.isOpened(): raise ValueError("Could not open video stream") @@ -194,7 +186,7 @@ def decode_frames(self, video_file, pts_list): def decode_first_n_frames(self, video_file, n): import cv2 - cap = cv2.VideoCapture(video_file, self._backend, []) + cap = cv2.VideoCapture(video_file, self._backend) if not cap.isOpened(): raise ValueError("Could not open video stream") @@ -212,7 +204,11 @@ def decode_first_n_frames(self, video_file, n): def decode_and_resize(self, video_file, pts_list, height, width, device): import cv2 - frames = [cv2.resize(frame, (width, height)) for frame in self.decode_frames(video_file, pts_list)] + + frames = [ + cv2.resize(frame, (width, height)) + for frame in self.decode_frames(video_file, pts_list) + ] return frames From 24b67ec9092ffdda8f605acda2c6d08036a9489f Mon Sep 17 00:00:00 2001 From: Daniel Flores Date: Wed, 4 Jun 2025 07:10:28 -0700 Subject: [PATCH 06/14] Remove benchmarks.png --- benchmarks.png | Bin 41355 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 benchmarks.png diff --git a/benchmarks.png b/benchmarks.png deleted file mode 100644 index a05719185e801fdfe7222573e8782797e212b462..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 41355 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 Jc$@Z~_&+r>KcN5s From 04019e7d2d7298e3b4d9a96b278206fe3d4b2260 Mon Sep 17 00:00:00 2001 From: Daniel Flores Date: Fri, 6 Jun 2025 09:36:34 -0700 Subject: [PATCH 07/14] Remove duplicate imports, return tensor --- .../decoders/benchmark_decoders_library.py | 26 ++++++++++++------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/benchmarks/decoders/benchmark_decoders_library.py b/benchmarks/decoders/benchmark_decoders_library.py index e49d3424..f09de61c 100644 --- a/benchmarks/decoders/benchmark_decoders_library.py +++ b/benchmarks/decoders/benchmark_decoders_library.py @@ -150,19 +150,19 @@ class OpenCVDecoder(AbstractDecoder): def __init__(self, backend): import cv2 + self.cv2 = cv2 + self._available_backends = {"FFMPEG": cv2.CAP_FFMPEG} self._backend = self._available_backends.get(backend) self._print_each_iteration_time = False def decode_frames(self, video_file, pts_list): - import cv2 - - cap = cv2.VideoCapture(video_file, self._backend) + cap = self.cv2.VideoCapture(video_file, self._backend) if not cap.isOpened(): raise ValueError("Could not open video stream") - fps = cap.get(cv2.CAP_PROP_FPS) + fps = cap.get(self.cv2.CAP_PROP_FPS) approx_frame_indices = [int(pts * fps) for pts in pts_list] current_frame = 0 @@ -174,6 +174,11 @@ def decode_frames(self, video_file, pts_list): if current_frame in approx_frame_indices: # only decompress needed ret, frame = cap.retrieve() if ret: + # OpenCV uses BGR, change to RGB + frame = self.cv2.cvtColor(frame, self.cv2.COLOR_BGR2RGB) + # Update to C, H, W + frame = np.transpose(frame, (2, 0, 1)) + frame = torch.from_numpy(frame) frames.append(frame) if len(frames) == len(approx_frame_indices): @@ -184,9 +189,7 @@ def decode_frames(self, video_file, pts_list): return frames def decode_first_n_frames(self, video_file, n): - import cv2 - - cap = cv2.VideoCapture(video_file, self._backend) + cap = self.cv2.VideoCapture(video_file, self._backend) if not cap.isOpened(): raise ValueError("Could not open video stream") @@ -197,16 +200,21 @@ def decode_first_n_frames(self, video_file, n): raise ValueError("Could not grab video frame") ret, frame = cap.retrieve() if ret: + # OpenCV uses BGR, change to RGB + frame = self.cv2.cvtColor(frame, self.cv2.COLOR_BGR2RGB) + # Update to C, H, W + frame = np.transpose(frame, (2, 0, 1)) + frame = torch.from_numpy(frame) frames.append(frame) cap.release() assert len(frames) == n return frames def decode_and_resize(self, video_file, pts_list, height, width, device): - import cv2 + # OpenCV doesn't apply antialias, while other `decode_and_resize()` implementations apply antialias by default. frames = [ - cv2.resize(frame, (width, height)) + self.cv2.resize(frame, (width, height)) for frame in self.decode_frames(video_file, pts_list) ] return frames From baa79f4c9f667065664d5c1fd4ceda8bc8aedc2c Mon Sep 17 00:00:00 2001 From: Daniel Flores Date: Tue, 10 Jun 2025 10:07:03 -0700 Subject: [PATCH 08/14] Add stream_index option to TorchCodecPublic decoder --- benchmarks/decoders/benchmark_decoders_library.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/benchmarks/decoders/benchmark_decoders_library.py b/benchmarks/decoders/benchmark_decoders_library.py index f09de61c..b5d7ed08 100644 --- a/benchmarks/decoders/benchmark_decoders_library.py +++ b/benchmarks/decoders/benchmark_decoders_library.py @@ -367,10 +367,17 @@ def decode_first_n_frames(self, video_file, n): class TorchCodecPublic(AbstractDecoder): - def __init__(self, num_ffmpeg_threads=None, device="cpu", seek_mode="exact"): + def __init__( + self, + num_ffmpeg_threads=None, + device="cpu", + seek_mode="exact", + stream_index: int | None = None, + ): self._num_ffmpeg_threads = num_ffmpeg_threads self._device = device self._seek_mode = seek_mode + self._stream_index = int(stream_index) if stream_index else None from torchvision.transforms import v2 as transforms_v2 @@ -385,6 +392,7 @@ def decode_frames(self, video_file, pts_list): num_ffmpeg_threads=num_ffmpeg_threads, device=self._device, seek_mode=self._seek_mode, + stream_index=self._stream_index, ) return decoder.get_frames_played_at(pts_list) @@ -397,6 +405,7 @@ def decode_first_n_frames(self, video_file, n): num_ffmpeg_threads=num_ffmpeg_threads, device=self._device, seek_mode=self._seek_mode, + stream_index=self._stream_index, ) frames = [] count = 0 From 3c3668eac435cc8e993a23a536d2f2004a280fcd Mon Sep 17 00:00:00 2001 From: Daniel Flores Date: Tue, 10 Jun 2025 11:39:01 -0700 Subject: [PATCH 09/14] sort before plotting --- benchmarks/decoders/benchmark_decoders_library.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/benchmarks/decoders/benchmark_decoders_library.py b/benchmarks/decoders/benchmark_decoders_library.py index b5d7ed08..7dc96c00 100644 --- a/benchmarks/decoders/benchmark_decoders_library.py +++ b/benchmarks/decoders/benchmark_decoders_library.py @@ -425,6 +425,7 @@ def decode_and_resize(self, video_file, pts_list, height, width, device): num_ffmpeg_threads=num_ffmpeg_threads, device=self._device, seek_mode=self._seek_mode, + stream_index=self._stream_index, ) frames = decoder.get_frames_played_at(pts_list) frames = self.transforms_v2.functional.resize(frames.data, (height, width)) @@ -828,7 +829,7 @@ def run_benchmarks( # are using different random pts values across videos. random_pts_list = (torch.rand(num_samples) * duration).tolist() - for decoder_name, decoder in decoder_dict.items(): + for decoder_name, decoder in sorted(decoder_dict.items(), key=lambda x: x[0]): print(f"video={video_file_path}, decoder={decoder_name}") if dataloader_parameters: From 2e23cfa9eccb2670d0f228df6753673c7615f4a7 Mon Sep 17 00:00:00 2001 From: Daniel Flores Date: Tue, 10 Jun 2025 13:04:43 -0700 Subject: [PATCH 10/14] Add stream_index to TorchAudioDecoder --- .../decoders/benchmark_decoders_library.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/benchmarks/decoders/benchmark_decoders_library.py b/benchmarks/decoders/benchmark_decoders_library.py index 7dc96c00..92812811 100644 --- a/benchmarks/decoders/benchmark_decoders_library.py +++ b/benchmarks/decoders/benchmark_decoders_library.py @@ -372,7 +372,7 @@ def __init__( num_ffmpeg_threads=None, device="cpu", seek_mode="exact", - stream_index: int | None = None, + stream_index=None, ): self._num_ffmpeg_threads = num_ffmpeg_threads self._device = device @@ -536,7 +536,7 @@ def decode_first_n_frames(self, video_file, n): class TorchAudioDecoder(AbstractDecoder): - def __init__(self): + def __init__(self, stream_index=None): import torchaudio # noqa: F401 self.torchaudio = torchaudio @@ -544,11 +544,14 @@ def __init__(self): from torchvision.transforms import v2 as transforms_v2 self.transforms_v2 = transforms_v2 + self._stream_index = int(stream_index) if stream_index else None def decode_frames(self, video_file, pts_list): stream_reader = self.torchaudio.io.StreamReader(src=video_file) stream_reader.add_basic_video_stream( - frames_per_chunk=1, decoder_option={"threads": "0"} + frames_per_chunk=1, + decoder_option={"threads": "0"}, + stream_index=self._stream_index, ) frames = [] for pts in pts_list: @@ -561,7 +564,9 @@ def decode_frames(self, video_file, pts_list): def decode_first_n_frames(self, video_file, n): stream_reader = self.torchaudio.io.StreamReader(src=video_file) stream_reader.add_basic_video_stream( - frames_per_chunk=1, decoder_option={"threads": "0"} + frames_per_chunk=1, + decoder_option={"threads": "0"}, + stream_index=self._stream_index, ) frames = [] frame_cnt = 0 @@ -576,7 +581,9 @@ def decode_first_n_frames(self, video_file, n): def decode_and_resize(self, video_file, pts_list, height, width, device): stream_reader = self.torchaudio.io.StreamReader(src=video_file) stream_reader.add_basic_video_stream( - frames_per_chunk=1, decoder_option={"threads": "1"} + frames_per_chunk=1, + decoder_option={"threads": "1"}, + stream_index=self._stream_index, ) frames = [] for pts in pts_list: From b97e7bfb7947c92788b69e95af31aed2a6c5cfd4 Mon Sep 17 00:00:00 2001 From: Daniel Flores Date: Wed, 11 Jun 2025 08:21:41 -0700 Subject: [PATCH 11/14] Annotate types for ints passed as str/None, extract opencv conversion to function --- .../decoders/benchmark_decoders_library.py | 55 +++++++++++++------ 1 file changed, 38 insertions(+), 17 deletions(-) diff --git a/benchmarks/decoders/benchmark_decoders_library.py b/benchmarks/decoders/benchmark_decoders_library.py index 92812811..1e458bb7 100644 --- a/benchmarks/decoders/benchmark_decoders_library.py +++ b/benchmarks/decoders/benchmark_decoders_library.py @@ -174,11 +174,7 @@ def decode_frames(self, video_file, pts_list): if current_frame in approx_frame_indices: # only decompress needed ret, frame = cap.retrieve() if ret: - # OpenCV uses BGR, change to RGB - frame = self.cv2.cvtColor(frame, self.cv2.COLOR_BGR2RGB) - # Update to C, H, W - frame = np.transpose(frame, (2, 0, 1)) - frame = torch.from_numpy(frame) + frame = self.convert_frame_to_rgb_tensor(frame) frames.append(frame) if len(frames) == len(approx_frame_indices): @@ -200,11 +196,7 @@ def decode_first_n_frames(self, video_file, n): raise ValueError("Could not grab video frame") ret, frame = cap.retrieve() if ret: - # OpenCV uses BGR, change to RGB - frame = self.cv2.cvtColor(frame, self.cv2.COLOR_BGR2RGB) - # Update to C, H, W - frame = np.transpose(frame, (2, 0, 1)) - frame = torch.from_numpy(frame) + frame = self.convert_frame_to_rgb_tensor(frame) frames.append(frame) cap.release() assert len(frames) == n @@ -219,9 +211,23 @@ def decode_and_resize(self, video_file, pts_list, height, width, device): ] return frames + def convert_frame_to_rgb_tensor(self, frame): + # OpenCV uses BGR, change to RGB + frame = self.cv2.cvtColor(frame, self.cv2.COLOR_BGR2RGB) + # Update to C, H, W + frame = np.transpose(frame, (2, 0, 1)) + # Convert to tensor + frame = torch.from_numpy(frame) + return frame + class TorchCodecCore(AbstractDecoder): - def __init__(self, num_threads=None, color_conversion_library=None, device="cpu"): + def __init__( + self, + num_threads: str | None = None, + color_conversion_library=None, + device="cpu", + ): self._num_threads = int(num_threads) if num_threads else None self._color_conversion_library = color_conversion_library self._device = device @@ -259,7 +265,12 @@ def decode_first_n_frames(self, video_file, n): class TorchCodecCoreNonBatch(AbstractDecoder): - def __init__(self, num_threads=None, color_conversion_library=None, device="cpu"): + def __init__( + self, + num_threads: str | None = None, + color_conversion_library=None, + device="cpu", + ): self._num_threads = num_threads self._color_conversion_library = color_conversion_library self._device = device @@ -328,7 +339,12 @@ def decode_and_resize(self, video_file, pts_list, height, width, device): class TorchCodecCoreBatch(AbstractDecoder): - def __init__(self, num_threads=None, color_conversion_library=None, device="cpu"): + def __init__( + self, + num_threads: str | None = None, + color_conversion_library=None, + device="cpu", + ): self._print_each_iteration_time = False self._num_threads = int(num_threads) if num_threads else None self._color_conversion_library = color_conversion_library @@ -369,10 +385,10 @@ def decode_first_n_frames(self, video_file, n): class TorchCodecPublic(AbstractDecoder): def __init__( self, - num_ffmpeg_threads=None, + num_ffmpeg_threads: str | None = None, device="cpu", seek_mode="exact", - stream_index=None, + stream_index: str | None = None, ): self._num_ffmpeg_threads = num_ffmpeg_threads self._device = device @@ -433,7 +449,12 @@ def decode_and_resize(self, video_file, pts_list, height, width, device): class TorchCodecPublicNonBatch(AbstractDecoder): - def __init__(self, num_ffmpeg_threads=None, device="cpu", seek_mode="approximate"): + def __init__( + self, + num_ffmpeg_threads: str | None = None, + device="cpu", + seek_mode="approximate", + ): self._num_ffmpeg_threads = num_ffmpeg_threads self._device = device self._seek_mode = seek_mode @@ -536,7 +557,7 @@ def decode_first_n_frames(self, video_file, n): class TorchAudioDecoder(AbstractDecoder): - def __init__(self, stream_index=None): + def __init__(self, stream_index: str | None = None): import torchaudio # noqa: F401 self.torchaudio = torchaudio From 9717fe7b10867d614db83728d185d760b01afd3c Mon Sep 17 00:00:00 2001 From: Daniel Flores Date: Wed, 11 Jun 2025 08:36:41 -0700 Subject: [PATCH 12/14] Remove OpenCV decode_and_resize function --- benchmarks/decoders/benchmark_decoders_library.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/benchmarks/decoders/benchmark_decoders_library.py b/benchmarks/decoders/benchmark_decoders_library.py index 1e458bb7..b4d42419 100644 --- a/benchmarks/decoders/benchmark_decoders_library.py +++ b/benchmarks/decoders/benchmark_decoders_library.py @@ -202,15 +202,6 @@ def decode_first_n_frames(self, video_file, n): assert len(frames) == n return frames - def decode_and_resize(self, video_file, pts_list, height, width, device): - - # OpenCV doesn't apply antialias, while other `decode_and_resize()` implementations apply antialias by default. - frames = [ - self.cv2.resize(frame, (width, height)) - for frame in self.decode_frames(video_file, pts_list) - ] - return frames - def convert_frame_to_rgb_tensor(self, frame): # OpenCV uses BGR, change to RGB frame = self.cv2.cvtColor(frame, self.cv2.COLOR_BGR2RGB) From 0674655c13f710e92b4ed28ca48bc6d8fd87be2c Mon Sep 17 00:00:00 2001 From: Daniel Flores Date: Wed, 11 Jun 2025 08:54:28 -0700 Subject: [PATCH 13/14] Raise error in opencv decode_and_resize function --- benchmarks/decoders/benchmark_decoders_library.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/benchmarks/decoders/benchmark_decoders_library.py b/benchmarks/decoders/benchmark_decoders_library.py index b4d42419..cf1b233d 100644 --- a/benchmarks/decoders/benchmark_decoders_library.py +++ b/benchmarks/decoders/benchmark_decoders_library.py @@ -202,6 +202,11 @@ def decode_first_n_frames(self, video_file, n): assert len(frames) == n return frames + def decode_and_resize(self, *args, **kwargs): + raise ValueError( + "OpenCV doesn't apply antialias while pytorch does by default, this is potentially an unfair comparison" + ) + def convert_frame_to_rgb_tensor(self, frame): # OpenCV uses BGR, change to RGB frame = self.cv2.cvtColor(frame, self.cv2.COLOR_BGR2RGB) From 96b8e18eabf6df31f64e9ddfe2752e2b1c342669 Mon Sep 17 00:00:00 2001 From: Daniel Flores Date: Wed, 11 Jun 2025 08:59:38 -0700 Subject: [PATCH 14/14] Add comment explaining sorting --- benchmarks/decoders/benchmark_decoders_library.py | 1 + 1 file changed, 1 insertion(+) diff --git a/benchmarks/decoders/benchmark_decoders_library.py b/benchmarks/decoders/benchmark_decoders_library.py index cf1b233d..9be26330 100644 --- a/benchmarks/decoders/benchmark_decoders_library.py +++ b/benchmarks/decoders/benchmark_decoders_library.py @@ -853,6 +853,7 @@ def run_benchmarks( # are using different random pts values across videos. random_pts_list = (torch.rand(num_samples) * duration).tolist() + # The decoder items are sorted to perform and display the benchmarks in a consistent order. for decoder_name, decoder in sorted(decoder_dict.items(), key=lambda x: x[0]): print(f"video={video_file_path}, decoder={decoder_name}")