Skip to content
Merged
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
109 changes: 109 additions & 0 deletions vllm/multimodal/video.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
import math
import warnings
from abc import abstractmethod
from io import BytesIO
from typing import TYPE_CHECKING, Any, cast
Expand Down Expand Up @@ -747,3 +748,111 @@ def load_bytes(
**kwargs,
)
return out


@VIDEO_LOADER_REGISTRY.register("opencv_dynamic_openpangu")
Comment thread
Isotr0py marked this conversation as resolved.
Outdated
class OpenCVDynamicOpenPanguVideoBackend(OpenCVVideoBackend):
@classmethod
def load_bytes(
cls,
data: bytes,
num_frames: int = 32,
fps: int = 1,
max_duration: int = 300,
frame_recovery: bool = False,
**kwargs,
) -> tuple[npt.NDArray, dict[str, Any]]:
"""
Load video frames with dynamic sampling based on duration.
Assume that total_num_frames = 10 and fps = 1.
The timestamp of frame 0 is 0.0.
The timestamp of frame 1 is 1.0.…
The timestamp of frame 9 (the last frame) should be 9.0, that is,
(total_frames_num – 1) / original_fps.

Args:
data: Raw video bytes
num_frames: Not used in dynamic backend
fps: Target FPS for sampling (default: 2)

Returns:
Tuple of (frames_array, metadata_dict)
"""
import cv2

backend = cls().get_cv2_video_api()
cap = cv2.VideoCapture(BytesIO(data), backend, [])
if not cap.isOpened():
raise ValueError("Could not open video stream")

total_frames_num = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
original_fps = float(cap.get(cv2.CAP_PROP_FPS))
# The timestamp of the rightmost frame, cannot be used to calculate frame 0.
if total_frames_num >= 1 and original_fps > 0:
total_duration = (total_frames_num - 1) / original_fps
else:
total_duration = 0

# `fps` is the FPS parameter passed in for sampling,
# -1 indicates that sampling can be performed directly without FPS limitation.
if fps > 0:
# Num_frames is the maximum number of frames to sample.
# If fewer frames are sampled at this sample_fps, the update duration will be longer. # noqa: E501
if num_frames >= int(total_duration * fps) + 1:
num_frames = int(total_duration * fps) + 1
# Under the new maximum frame rate, the video duration of the rightmost frame, # noqa: E501
# cannot be calculated for frame 0.
total_duration = min(total_duration, (num_frames - 1) / fps)
elif fps != -1:
raise ValueError(
f"requires dataset fps is -1 or greater than 0 but got {fps}"
)

sample_frame_timestamps = np.linspace(
0, total_duration, num_frames, dtype=float
)
frames_indices = [
min(total_frames_num - 1, round(t * original_fps))
for t in sample_frame_timestamps
]

width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
frames = np.empty((len(frames_indices), height, width, 3), dtype=np.uint8)

i = 0
for frame_idx in frames_indices:
cap.set(cv2.CAP_PROP_POS_FRAMES, frame_idx)
ret, frame = cap.read()
if ret:
frames[i] = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
i += 1
else:
# when get a bad frame,continuous finding a next good frame
next_idx = frame_idx + 1
while next_idx < total_frames_num:
ret, next_frame = cap.read()
if ret:
frames[i] = cv2.cvtColor(next_frame, cv2.COLOR_BGR2RGB)
i += 1
break
next_idx += 1

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The while loop for finding a good frame uses cap.read(), which advances the video stream's internal pointer. However, it doesn't explicitly set the frame position for each attempt within the loop. If frames_indices contains consecutive or close indices (e.g., [10, 11]) and reading frame 10 fails, the recovery logic might read frame 11 as a substitute. Then, in the next iteration of the outer loop for frame_idx = 11, cap.set(..., 11) followed by cap.read() will actually read frame 12, skipping the intended frame 11. This leads to incorrect frame sampling. You should use cap.set() inside the recovery loop to ensure the correct frame is read.

            else:
                # when get a bad frame,continuous finding a next good frame
                next_idx = frame_idx + 1
                while next_idx < total_frames_num:
                    cap.set(cv2.CAP_PROP_POS_FRAMES, next_idx)
                    ret, next_frame = cap.read()
                    if ret:
                        frames[i] = cv2.cvtColor(next_frame, cv2.COLOR_BGR2RGB)
                        i += 1
                        break
                    next_idx += 1

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried frames_indices = [10, 11]. After one cap.set and two cap.read operations, the second cap.set operation did not skip the original frame.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can use cls._read_frames and cls._read_frames_with_recovery here to avoid duplicate implementation.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Modified already.


if i != len(frames_indices):
warnings.warn(
f"Expected reading {len(frames_indices)} frames,"
f"but only loaded {i} frames from video.",
UserWarning,
stacklevel=2,
)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
warnings.warn(
f"Expected reading {len(frames_indices)} frames,"
f"but only loaded {i} frames from video.",
UserWarning,
stacklevel=2,
)
logger.warning(
f"Expected reading {len(frames_indices)} frames,"
f"but only loaded {i} frames from video.",
)

Use logger.warning for warning message

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Modified already.


# Use transformers transformers.video_utils.VideoMetadata format
metadata = {
"total_num_frames": total_frames_num,
"fps": original_fps,
"duration": total_duration,
"video_backend": "opencv_dynamic_openpangu",
"frames_indices": frames_indices,
"do_sample_frames": False,
}
return frames, metadata