main
py 152 lines 4.55 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4 import cv2
5 import numpy as np
6 import numpy.typing as npt
7 from PIL import Image
8
9
10 def random_image(rng: np.random.RandomState, min_wh: int, max_wh: int):
11 w, h = rng.randint(min_wh, max_wh, size=(2,))
12 arr = rng.randint(0, 255, size=(w, h, 3), dtype=np.uint8)
13 return Image.fromarray(arr)
14
15
16 def random_video(
17 rng: np.random.RandomState,
18 min_frames: int,
19 max_frames: int,
20 min_wh: int,
21 max_wh: int,
22 ):
23 num_frames = rng.randint(min_frames, max_frames)
24 w, h = rng.randint(min_wh, max_wh, size=(2,))
25 return rng.randint(0, 255, size=(num_frames, w, h, 3), dtype=np.uint8)
26
27
28 def random_audio(
29 rng: np.random.RandomState,
30 min_len: int,
31 max_len: int,
32 sr: int,
33 ):
34 audio_len = rng.randint(min_len, max_len)
35 return rng.rand(audio_len), sr
36
37
38 def create_video_from_image(
39 image_path: str,
40 video_path: str,
41 num_frames: int = 10,
42 fps: float = 1.0,
43 is_color: bool = True,
44 fourcc: str = "mp4v",
45 ):
46 image = cv2.imread(image_path)
47 if not is_color:
48 # Convert to grayscale if is_color is False
49 image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
50 height, width = image.shape
51 else:
52 height, width, _ = image.shape
53
54 video_writer = cv2.VideoWriter(
55 video_path,
56 cv2.VideoWriter_fourcc(*fourcc),
57 fps,
58 (width, height),
59 isColor=is_color,
60 )
61
62 for _ in range(num_frames):
63 video_writer.write(image)
64
65 video_writer.release()
66 return video_path
67
68
69 def create_long_gop_video(
70 num_frames: int = 50,
71 fps: int = 30,
72 width: int = 64,
73 height: int = 64,
74 ) -> bytes:
75 """Encode an H.264 clip with one keyframe and green-channel = frame index.
76
77 The marker lets a test recover which frame the decoder actually returned,
78 independent of any metadata label.
79 """
80 import io
81
82 import av
83
84 buf = io.BytesIO()
85 with av.open(buf, mode="w", format="mp4") as container:
86 stream = container.add_stream("h264", rate=fps)
87 stream.width = width
88 stream.height = height
89 stream.pix_fmt = "yuv420p"
90 stream.codec_context.gop_size = num_frames
91 stream.codec_context.max_b_frames = 0
92 stream.codec_context.options = {
93 "x264-params": (f"scenecut=0:keyint={num_frames}:min-keyint={num_frames}")
94 }
95 for i in range(num_frames):
96 img = np.zeros((height, width, 3), dtype=np.uint8)
97 img[:, :, 1] = i % 256
98 frame = av.VideoFrame.from_ndarray(img, format="rgb24")
99 for packet in stream.encode(frame):
100 container.mux(packet)
101 for packet in stream.encode():
102 container.mux(packet)
103 return buf.getvalue()
104
105
106 def create_edit_list_trimmed_video(
107 num_frames: int = 90,
108 trim_start_frame: int = 60,
109 fps: int = 30,
110 ) -> tuple[bytes, int]:
111 """Stream-copy-cut a long-GOP clip so an mp4 edit list hides the lead-in.
112
113 Remuxes the tail of a single-keyframe clip without re-encoding, rebasing
114 timestamps at ``trim_start_frame``: the lead-in packets are still needed
115 to decode, so they stay in the file at negative pts and the muxer records
116 an edit list. The header sample count stays ``num_frames`` while only the
117 trailing frames are presentable — the shape produced by lossless trims
118 (e.g. ``ffmpeg -ss ... -c copy``). Returns (video_bytes, visible_frames).
119 """
120 import io
121
122 import av
123
124 src = create_long_gop_video(num_frames=num_frames, fps=fps)
125 buf = io.BytesIO()
126 with (
127 av.open(io.BytesIO(src)) as source,
128 av.open(buf, mode="w", format="mp4") as out,
129 ):
130 in_stream = source.streams.video[0]
131 out_stream = out.add_stream_from_template(in_stream)
132 start_pts = round(trim_start_frame / fps / in_stream.time_base)
133 for packet in source.demux(in_stream):
134 if packet.pts is None or packet.dts is None:
135 continue
136 packet.pts -= start_pts
137 packet.dts -= start_pts
138 packet.stream = out_stream
139 out.mux(packet)
140 return buf.getvalue(), num_frames - trim_start_frame
141
142
143 def cosine_similarity(A: npt.NDArray, B: npt.NDArray, axis: int = -1) -> npt.NDArray:
144 """Compute cosine similarity between two vectors."""
145 return np.sum(A * B, axis=axis) / (
146 np.linalg.norm(A, axis=axis) * np.linalg.norm(B, axis=axis)
147 )
148
149
150 def normalize_image(image: npt.NDArray) -> npt.NDArray:
151 """Normalize image to [0, 1] range."""
152 return image.astype(np.float32) / 255.0