Showing
2 changed files
with
96 additions
and
53 deletions
| @@ -122,7 +122,7 @@ class Video2Frame: | @@ -122,7 +122,7 @@ class Video2Frame: | ||
| 122 | if not cap.isOpened(): | 122 | if not cap.isOpened(): |
| 123 | raise ValueError(f"无法打开视频片段: {clip_path}") | 123 | raise ValueError(f"无法打开视频片段: {clip_path}") |
| 124 | 124 | ||
| 125 | - total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) | 125 | + # total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) |
| 126 | video_fps = cap.get(cv2.CAP_PROP_FPS) | 126 | video_fps = cap.get(cv2.CAP_PROP_FPS) |
| 127 | if video_fps <= 0: | 127 | if video_fps <= 0: |
| 128 | video_fps = 25.0 | 128 | video_fps = 25.0 |
| @@ -132,10 +132,9 @@ class Video2Frame: | @@ -132,10 +132,9 @@ class Video2Frame: | ||
| 132 | if frame_interval < 1: | 132 | if frame_interval < 1: |
| 133 | frame_interval = 1 | 133 | frame_interval = 1 |
| 134 | 134 | ||
| 135 | - frame_paths = [] | 135 | + frames = [] |
| 136 | saved_count = 0 | 136 | saved_count = 0 |
| 137 | frame_count = 0 | 137 | frame_count = 0 |
| 138 | - | ||
| 139 | while True: | 138 | while True: |
| 140 | ret, frame = cap.read() | 139 | ret, frame = cap.read() |
| 141 | if not ret: | 140 | if not ret: |
| @@ -145,27 +144,26 @@ class Video2Frame: | @@ -145,27 +144,26 @@ class Video2Frame: | ||
| 145 | if roi is not None: | 144 | if roi is not None: |
| 146 | x, y, w, h = roi | 145 | x, y, w, h = roi |
| 147 | frame = frame[y:y + h, x:x + w] | 146 | frame = frame[y:y + h, x:x + w] |
| 148 | - | ||
| 149 | - if max_px_area is not None: | ||
| 150 | - h, w = frame.shape[:2] | ||
| 151 | - area = h * w | ||
| 152 | - if area > max_px_area: | ||
| 153 | - scale = (max_px_area / area) ** 0.5 | ||
| 154 | - new_w = int(w * scale) | ||
| 155 | - new_h = int(h * scale) | ||
| 156 | - frame = cv2.resize( | ||
| 157 | - frame, (new_w, new_h), interpolation=cv2.INTER_AREA | ||
| 158 | - ) | ||
| 159 | - | 147 | + frame = self.reisize_frame(frame, max_px_area) |
| 148 | + frames.append(frame) | ||
| 160 | frame_path = frame_output_dir / f"frame_{saved_count:06d}.jpg" | 149 | frame_path = frame_output_dir / f"frame_{saved_count:06d}.jpg" |
| 161 | cv2.imwrite(str(frame_path), frame) | 150 | cv2.imwrite(str(frame_path), frame) |
| 162 | - frame_paths.append(str(frame_path)) | ||
| 163 | saved_count += 1 | 151 | saved_count += 1 |
| 164 | - | ||
| 165 | frame_count += 1 | 152 | frame_count += 1 |
| 166 | - | ||
| 167 | cap.release() | 153 | cap.release() |
| 168 | - return frame_paths | 154 | + return frames |
| 155 | + | ||
| 156 | + def reisize_frame(self, frame, max_px_area): | ||
| 157 | + if max_px_area is None: | ||
| 158 | + return frame | ||
| 159 | + h, w = frame.shape[:2] | ||
| 160 | + area = h * w | ||
| 161 | + if area > max_px_area: | ||
| 162 | + scale = (max_px_area / area) ** 0.5 | ||
| 163 | + new_w = int(w * scale) | ||
| 164 | + new_h = int(h * scale) | ||
| 165 | + frame = cv2.resize(frame, (new_w, new_h), interpolation=cv2.INTER_AREA) | ||
| 166 | + return frame | ||
| 169 | 167 | ||
| 170 | @staticmethod | 168 | @staticmethod |
| 171 | def _find_frame_idx(utc_list, target_utc, find_last_not_greater=True): | 169 | def _find_frame_idx(utc_list, target_utc, find_last_not_greater=True): |
| @@ -241,28 +239,34 @@ def frames2content(frames): | @@ -241,28 +239,34 @@ def frames2content(frames): | ||
| 241 | 将帧列表(图片路径列表)转为 LLM content 列表。 | 239 | 将帧列表(图片路径列表)转为 LLM content 列表。 |
| 242 | 格式与 llm_video_content.py 中的 contents 返回的 content 一致。 | 240 | 格式与 llm_video_content.py 中的 contents 返回的 content 一致。 |
| 243 | """ | 241 | """ |
| 244 | - content = [] | 242 | + contents = [] |
| 245 | video_prompt = ( | 243 | video_prompt = ( |
| 246 | f"以下是从视频中按时间顺序提取的 {len(frames)} 帧画面," | 244 | f"以下是从视频中按时间顺序提取的 {len(frames)} 帧画面," |
| 247 | f"请将它们视为一个连续的视频进行分析。" | 245 | f"请将它们视为一个连续的视频进行分析。" |
| 248 | ) | 246 | ) |
| 249 | - content.append({"type": "text", "text": video_prompt}) | 247 | + contents.append({"type": "text", "text": video_prompt}) |
| 250 | 248 | ||
| 251 | - for frame_path in frames: | ||
| 252 | - with open(frame_path, "rb") as f: | ||
| 253 | - img_bytes = f.read() | ||
| 254 | - b64_str = base64.b64encode(img_bytes).decode("utf-8") | ||
| 255 | - content.append({ | 249 | + for frame in frames: |
| 250 | + _, buffer = cv2.imencode('.jpg', frame) | ||
| 251 | + b64_str = base64.b64encode(buffer).decode('utf-8') | ||
| 252 | + | ||
| 253 | + # with open(frame_path, "rb") as f: | ||
| 254 | + # img_bytes = f.read() | ||
| 255 | + # b64_str = base64.b64encode(img_bytes).decode("utf-8") | ||
| 256 | + | ||
| 257 | + contents.append({ | ||
| 256 | "type": "image_url", | 258 | "type": "image_url", |
| 257 | "image_url": { | 259 | "image_url": { |
| 258 | "url": f"data:image/jpeg;base64,{b64_str}" | 260 | "url": f"data:image/jpeg;base64,{b64_str}" |
| 259 | } | 261 | } |
| 260 | }) | 262 | }) |
| 261 | 263 | ||
| 262 | - return content | 264 | + return contents |
| 263 | 265 | ||
| 264 | 266 | ||
| 265 | if __name__ == "__main__": | 267 | if __name__ == "__main__": |
| 268 | + # pass | ||
| 269 | + | ||
| 266 | import argparse | 270 | import argparse |
| 267 | import sys | 271 | import sys |
| 268 | import tempfile | 272 | import tempfile |
| @@ -275,7 +279,7 @@ if __name__ == "__main__": | @@ -275,7 +279,7 @@ if __name__ == "__main__": | ||
| 275 | ) | 279 | ) |
| 276 | parser.add_argument( | 280 | parser.add_argument( |
| 277 | "--cache-dir", | 281 | "--cache-dir", |
| 278 | - default=None, | 282 | + default=r"C:\Users\lzw\AppData\Local\Temp\video2frame_test_mn26tcy_my", |
| 279 | help="缓存目录(默认自动创建临时目录)", | 283 | help="缓存目录(默认自动创建临时目录)", |
| 280 | ) | 284 | ) |
| 281 | parser.add_argument( | 285 | parser.add_argument( |
| @@ -293,7 +297,7 @@ if __name__ == "__main__": | @@ -293,7 +297,7 @@ if __name__ == "__main__": | ||
| 293 | parser.add_argument( | 297 | parser.add_argument( |
| 294 | "--fps", | 298 | "--fps", |
| 295 | type=float, | 299 | type=float, |
| 296 | - default=1.0, | 300 | + default=2.0, |
| 297 | help="抽帧帧率(默认 1.0)", | 301 | help="抽帧帧率(默认 1.0)", |
| 298 | ) | 302 | ) |
| 299 | args = parser.parse_args() | 303 | args = parser.parse_args() |
| @@ -314,11 +318,13 @@ if __name__ == "__main__": | @@ -314,11 +318,13 @@ if __name__ == "__main__": | ||
| 314 | start_utc=args.start, | 318 | start_utc=args.start, |
| 315 | end_utc=args.end, | 319 | end_utc=args.end, |
| 316 | fps=args.fps, | 320 | fps=args.fps, |
| 321 | + roi=None, | ||
| 322 | + max_px_area=200_000, | ||
| 317 | ) | 323 | ) |
| 318 | print(f" 成功提取 {len(frames)} 帧") | 324 | print(f" 成功提取 {len(frames)} 帧") |
| 319 | if frames: | 325 | if frames: |
| 320 | - print(f" 首帧: {frames[0]}") | ||
| 321 | - print(f" 末帧: {frames[-1]}") | 326 | + print(f" 首帧: {frames[0].shape}") |
| 327 | + print(f" 末帧: {frames[-1].shape}") | ||
| 322 | except Exception as e: | 328 | except Exception as e: |
| 323 | print(f" 失败: {e}") | 329 | print(f" 失败: {e}") |
| 324 | import traceback | 330 | import traceback |
| @@ -340,27 +346,46 @@ if __name__ == "__main__": | @@ -340,27 +346,46 @@ if __name__ == "__main__": | ||
| 340 | if len(content) > 3: | 346 | if len(content) > 3: |
| 341 | print(f" ... 还有 {len(content) - 3} 个元素") | 347 | print(f" ... 还有 {len(content) - 3} 个元素") |
| 342 | 348 | ||
| 343 | - # TEST 3: ROI + max_px_area | ||
| 344 | - print("\n[TEST 3] to_frames with roi + max_px_area") | ||
| 345 | - try: | ||
| 346 | - end_crop = min(args.start + 5, args.end) | ||
| 347 | - frames_cropped = v2f.to_frames( | ||
| 348 | - url=args.url, | ||
| 349 | - start_utc=args.start, | ||
| 350 | - end_utc=end_crop, | ||
| 351 | - fps=1.0, | ||
| 352 | - roi=None, | ||
| 353 | - max_px_area=200_000, | ||
| 354 | - ) | ||
| 355 | - print(f" 成功提取 {len(frames_cropped)} 帧(带 ROI 裁剪和面积缩放)") | ||
| 356 | - except Exception as e: | ||
| 357 | - print(f" 跳过/失败: {e}") | 349 | + print("\n[TEST 3] FootballReplayVideoEvent 进球识别(可选)") |
| 358 | 350 | ||
| 359 | - # TEST 4: SEI UTC 模式说明(仅演示,需真实含 SEI 的流才能运行) | ||
| 360 | - print("\n[TEST 4] SEI UTC 绝对时间戳模式(示例代码,需替换为含 SEI 的流)") | ||
| 361 | - print( | ||
| 362 | - " # 示例:假设视频第一帧 UTC 为 1715600000,提取 10 秒片段\n" | ||
| 363 | - " # frames = v2f.to_frames(url, start_utc=1715600000, end_utc=1715600010, fps=1)" | 351 | + try: |
| 352 | + from ..util.football_replay_video_event_by_llm import FootballReplayVideoEvent | ||
| 353 | + except ImportError: | ||
| 354 | + import sys | ||
| 355 | + from pathlib import Path | ||
| 356 | + sys.path.insert(0, str(Path(__file__).parent.parent)) | ||
| 357 | + from util.football_replay_video_event_by_llm import FootballReplayVideoEvent | ||
| 358 | + | ||
| 359 | + fbrv = FootballReplayVideoEvent( | ||
| 360 | + base_url="http://192.168.1.59:11434", | ||
| 361 | + model="qwen3.6:35b-a3b-q8_0", | ||
| 362 | + temperature=0.7, | ||
| 364 | ) | 363 | ) |
| 365 | - | ||
| 366 | - print("\n[INFO] 所有测试完成") | 364 | + event_json = fbrv.video_event_for_contents(content, None) |
| 365 | + print(f" 识别结果: {event_json}") | ||
| 366 | + | ||
| 367 | + | ||
| 368 | + # # TEST 3: ROI + max_px_area | ||
| 369 | + # print("\n[TEST 3] to_frames with roi + max_px_area") | ||
| 370 | + # try: | ||
| 371 | + # end_crop = min(args.start + 5, args.end) | ||
| 372 | + # frames_cropped = v2f.to_frames( | ||
| 373 | + # url=args.url, | ||
| 374 | + # start_utc=args.start, | ||
| 375 | + # end_utc=end_crop, | ||
| 376 | + # fps=1.0, | ||
| 377 | + # roi=None, | ||
| 378 | + # max_px_area=200_000, | ||
| 379 | + # ) | ||
| 380 | + # print(f" 成功提取 {len(frames_cropped)} 帧(带 ROI 裁剪和面积缩放)") | ||
| 381 | + # except Exception as e: | ||
| 382 | + # print(f" 跳过/失败: {e}") | ||
| 383 | + # | ||
| 384 | + # # TEST 4: SEI UTC 模式说明(仅演示,需真实含 SEI 的流才能运行) | ||
| 385 | + # print("\n[TEST 4] SEI UTC 绝对时间戳模式(示例代码,需替换为含 SEI 的流)") | ||
| 386 | + # print( | ||
| 387 | + # " # 示例:假设视频第一帧 UTC 为 1715600000,提取 10 秒片段\n" | ||
| 388 | + # " # frames = v2f.to_frames(url, start_utc=1715600000, end_utc=1715600010, fps=1)" | ||
| 389 | + # ) | ||
| 390 | + # | ||
| 391 | + # print("\n[INFO] 所有测试完成") |
| @@ -96,3 +96,21 @@ class FootballReplayVideoEvent: | @@ -96,3 +96,21 @@ class FootballReplayVideoEvent: | ||
| 96 | with open(cache_path, 'w', encoding='utf-8') as f: | 96 | with open(cache_path, 'w', encoding='utf-8') as f: |
| 97 | f.write(result) | 97 | f.write(result) |
| 98 | return result_json | 98 | return result_json |
| 99 | + | ||
| 100 | + def video_event_for_contents(self, contents: list, asr_text: str = '无', cache_path=None): | ||
| 101 | + if cache_path is not None and os.path.exists(cache_path): | ||
| 102 | + with open(cache_path, 'r', encoding='utf-8') as f: | ||
| 103 | + return json.loads(f.read()) | ||
| 104 | + system_message = SystemMessage(content=req_prompt) | ||
| 105 | + video_message = HumanMessage(content=contents) | ||
| 106 | + asr_message = HumanMessage(content=f"解说内容:{asr_text}") | ||
| 107 | + result = self.model.invoke([system_message, video_message, asr_message]).content | ||
| 108 | + try: | ||
| 109 | + result_json = json.loads(result) | ||
| 110 | + except json.JSONDecodeError: | ||
| 111 | + result_json = json.loads(result.replace("```json", "").replace("```", "")) | ||
| 112 | + if cache_path is not None: | ||
| 113 | + os.makedirs(Path(cache_path).parent, exist_ok=True) | ||
| 114 | + with open(cache_path, 'w', encoding='utf-8') as f: | ||
| 115 | + f.write(result) | ||
| 116 | + return result_json |
-
Please register or login to post a comment