diff --git a/backend/main.py b/backend/main.py index 42050dd..918dae3 100644 --- a/backend/main.py +++ b/backend/main.py @@ -12,7 +12,7 @@ from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import FileResponse, Response from fastapi.staticfiles import StaticFiles -from backend.segmentation import METHOD_DESCRIPTIONS, compare_frame, segment_frame +from backend.segmentation import METHOD_DESCRIPTIONS, compare_frame, overlay_mask, segment_frame ROOT = Path(__file__).resolve().parents[1] @@ -213,6 +213,7 @@ def _process_video( frame_index = 0 selected_count = 0 written_count = 0 + active_mask = None writer = None raw_video_path = job_path / f"{method}_overlay.raw.mp4" video_path = job_path / f"{method}_overlay.mp4" @@ -251,6 +252,7 @@ def _process_video( ) ) video_output = next(item for item in outputs if item.method == "fusion") + active_mask = video_output.mask video_frame = video_output.overlay selected_count += 1 elif should_process: @@ -267,8 +269,11 @@ def _process_video( frame_index, ) ) + active_mask = video_output.mask video_frame = video_output.overlay selected_count += 1 + elif active_mask is not None: + video_frame = overlay_mask(frame, active_mask) if writer is None: height, width = frame.shape[:2] diff --git a/tests/test_api.py b/tests/test_api.py index caff579..98b72f2 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -78,9 +78,18 @@ def test_segment_video_and_compare_frame(tmp_path: Path): assert result_capture.isOpened() result_frames = int(result_capture.get(cv2.CAP_PROP_FRAME_COUNT) or 0) result_fps = float(result_capture.get(cv2.CAP_PROP_FPS) or 0) + result_capture.set(cv2.CAP_PROP_POS_FRAMES, 1) + ok, carried_overlay = result_capture.read() result_capture.release() assert result_frames == 18 assert abs((result_frames / result_fps) - payload["duration"]) < 0.25 + assert ok + yellow_pixels = ( + (carried_overlay[:, :, 1] > 140) + & (carried_overlay[:, :, 2] > 140) + & (carried_overlay[:, :, 0] < 150) + ).sum() + assert yellow_pixels > 0 with video_path.open("rb") as handle: compare = client.post( diff --git a/工程分析/实现方案-2026-05-18-20-35-32.md b/工程分析/实现方案-2026-05-18-20-35-32.md new file mode 100644 index 0000000..00a7c94 --- /dev/null +++ b/工程分析/实现方案-2026-05-18-20-35-32.md @@ -0,0 +1,19 @@ +# 实现方案 + +开始时间:2026-05-18-20-35-32 + +## 后端 + +1. 在 `_process_video` 中维护最近一次成功分割得到的 `active_mask`。 +2. 命中抽帧关键帧时: + - 执行分割。 + - 保存该帧原图、掩膜、叠加图。 + - 更新 `active_mask`。 + - 结果视频写入该关键帧叠加图。 +3. 未命中抽帧关键帧时: + - 如果已有 `active_mask`,将最近掩膜叠加到当前原始帧上写入结果视频。 + - 如果还没有 `active_mask`,写入原始帧。 + +## 测试增强 + +- 在视频 API 测试中读取结果视频的非关键帧,检查其仍包含黄色叠加像素,避免结果视频在关键帧之间回到纯原图。 diff --git a/工程分析/测试方案-2026-05-18-20-35-32.md b/工程分析/测试方案-2026-05-18-20-35-32.md new file mode 100644 index 0000000..00f1c74 --- /dev/null +++ b/工程分析/测试方案-2026-05-18-20-35-32.md @@ -0,0 +1,30 @@ +# 测试方案 + +开始时间:2026-05-18-20-35-32 + +## 自动化测试 + +- `python3 -m compileall backend tests` +- `node --check frontend/app.js` +- `pytest -q` + +## 接口验证 + +- 使用内置样例视频调用 `/api/segment`。 +- 检查第 40 帧的卡片叠加图存在黄色分割像素。 +- 检查输出结果视频第 40 帧附近存在黄色叠加像素。 + +## 浏览器验证 + +- 打开页面、加载样例、运行分割。 +- 点击下方“帧 40”。 +- 确认右侧视频不再显示纯原图,而是带有分割叠加。 + +## 执行结果 + +- `python3 -m compileall backend tests`:通过。 +- `node --check frontend/app.js`:通过。 +- `pytest -q`:5 passed。 +- API 样例分割:返回 12 个结果关键帧,包含第 40 帧。 +- 后端视频像素检查:结果视频第 39、40、41 帧分别检测到 4742、5851、5710 个黄色叠加像素。 +- Chrome headless 页面验证:点击“帧 40”后,右侧视频 `currentTime=3.3333`、`duration=6`,画面检测到 5824 个黄色叠加像素。 diff --git a/工程分析/经验记录.md b/工程分析/经验记录.md index 65a6660..4ca677f 100644 --- a/工程分析/经验记录.md +++ b/工程分析/经验记录.md @@ -151,3 +151,15 @@ B. 产生问题原因:后端把已抽取并分割的结果帧直接按固定 8 C. 解决问题方案:后端改为按原视频 `source_fps` 写完整结果视频;抽中的帧写入分割叠加画面,未抽中的帧写入原始画面;`result_fps`、`result_duration`、`result_time` 与源视频时间轴保持一致。前端双视频 seek 同步改为同一时间点同步。 D. 后续如何避免问题:任何面向并排对照的视频结果,都应优先保持与源视频相同时间轴;抽帧结果可以作为下方帧卡片展示,但主视频播放器不应只由抽帧结果压缩拼接。 + +## 2026-05-18-20-35-32 结果视频关键帧附近空白 + +### 1. 点击第 40 帧后右侧结果视频看起来没有叠加 + +A. 具体问题:下方结果列表中第 40 帧存在分割结果,但点击后右侧完整结果视频画面可能显示为纯原图,用户感知为“空白”。 + +B. 产生问题原因:上一版结果视频为了保持 6 秒完整时间轴,只在被抽中的关键帧写入一帧叠加图,其他帧写原始图。浏览器 seek 到关键帧附近时可能显示相邻未抽中帧,因此叠加结果只闪一帧,难以稳定看到。 + +C. 解决问题方案:后端在生成完整结果视频时维护最近一次分割掩膜;关键帧写真实叠加图,非关键帧用最近掩膜叠加到当前原始帧后写入,从而在关键帧之间持续显示分割提示。 + +D. 后续如何避免问题:当主视频播放器用于人工查看结果时,抽帧算法的关键帧结果不能只显示单帧;应使用持续叠加、插值或显式关键帧段落,保证用户拖动时能稳定看到结果。 diff --git a/工程分析/需求分析-2026-05-18-20-35-32.md b/工程分析/需求分析-2026-05-18-20-35-32.md new file mode 100644 index 0000000..be2bdec --- /dev/null +++ b/工程分析/需求分析-2026-05-18-20-35-32.md @@ -0,0 +1,17 @@ +# 需求分析 + +开始时间:2026-05-18-20-35-32 + +## 用户问题 + +用户反馈:当前选择到第 40 帧时,右侧结果视频看起来是空白或没有分割叠加。 + +## 判断 + +第 40 帧的结果卡片和后端保存的 `frame_0040_overlay.png` 本身包含分割叠加,不是算法完全未输出。问题出在右侧完整时长结果视频:上一轮为了让右侧视频保持 6 秒,只在被抽中的关键帧写入叠加画面,未抽中的帧写入原始画面。浏览器 seek 到第 40 帧附近时,可能显示相邻未抽中帧,于是看起来像空白。 + +## 期望 + +- 结果视频保持与原始视频相同 6 秒时间轴。 +- 在关键帧附近拖动或点击时,不应闪回纯原图。 +- 下方帧卡片仍展示真实被分割的关键帧。