From c818f0a663f365c7ddd7de8767c3da88aff0964c Mon Sep 17 00:00:00 2001 From: admin <572701190@qq.com> Date: Mon, 18 May 2026 21:47:43 +0800 Subject: [PATCH] =?UTF-8?q?2026-05-18-21-20-13=20=E6=94=B9=E4=B8=BA?= =?UTF-8?q?=E9=80=90=E5=B8=A7=E7=94=9F=E6=88=90=E7=BB=93=E6=9E=9C=E8=A7=86?= =?UTF-8?q?=E9=A2=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/main.py | 43 +++++++++++------------- tests/test_api.py | 7 ++++ 工程分析/实现方案-2026-05-18-21-20-13.md | 17 ++++++++++ 工程分析/测试方案-2026-05-18-21-20-13.md | 30 +++++++++++++++++ 工程分析/经验记录.md | 12 +++++++ 工程分析/需求分析-2026-05-18-21-20-13.md | 17 ++++++++++ 6 files changed, 102 insertions(+), 24 deletions(-) create mode 100644 工程分析/实现方案-2026-05-18-21-20-13.md create mode 100644 工程分析/测试方案-2026-05-18-21-20-13.md create mode 100644 工程分析/需求分析-2026-05-18-21-20-13.md diff --git a/backend/main.py b/backend/main.py index 918dae3..21e2e7c 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, overlay_mask, segment_frame +from backend.segmentation import METHOD_DESCRIPTIONS, compare_frame, segment_frame ROOT = Path(__file__).resolve().parents[1] @@ -213,7 +213,6 @@ 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" @@ -234,9 +233,8 @@ def _process_video( if selected_indices else _selected_frame(frame_index, frame_stride, selected_count, max_frames) ) - video_frame = frame - if should_process and method == "compare": + if method == "compare" and should_process: outputs = compare_frame(frame, previous, sensitivity) for output in outputs: frames.append( @@ -252,34 +250,31 @@ 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: + elif method == "compare": + video_output = segment_frame(frame, "fusion", previous, sensitivity) + else: video_output = segment_frame(frame, method, previous, sensitivity) - frames.append( - _save_frame_outputs( - job_path, - frame_index, - method, - frame, - video_output, - frame_index / source_fps, - frame_index / source_fps, - frame_index, + if should_process: + frames.append( + _save_frame_outputs( + job_path, + frame_index, + method, + frame, + video_output, + frame_index / source_fps, + frame_index / source_fps, + frame_index, + ) ) - ) - active_mask = video_output.mask - video_frame = video_output.overlay + if should_process: 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] fourcc = cv2.VideoWriter_fourcc(*"mp4v") writer = cv2.VideoWriter(str(raw_video_path), fourcc, result_fps, (width, height)) - writer.write(video_frame) + writer.write(video_output.overlay) written_count += 1 previous = frame diff --git a/tests/test_api.py b/tests/test_api.py index 98b72f2..cf3588f 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -80,6 +80,8 @@ def test_segment_video_and_compare_frame(tmp_path: Path): 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.set(cv2.CAP_PROP_POS_FRAMES, second_frame["frame_index"]) + ok_selected, selected_video_frame = result_capture.read() result_capture.release() assert result_frames == 18 assert abs((result_frames / result_fps) - payload["duration"]) < 0.25 @@ -90,6 +92,11 @@ def test_segment_video_and_compare_frame(tmp_path: Path): & (carried_overlay[:, :, 0] < 150) ).sum() assert yellow_pixels > 0 + assert ok_selected + selected_overlay = cv2.imread(str(ROOT / second_frame["overlay_url"].lstrip("/"))) + assert selected_overlay is not None + selected_delta = cv2.absdiff(selected_video_frame, selected_overlay).mean() + assert selected_delta < 20.0 with video_path.open("rb") as handle: compare = client.post( diff --git a/工程分析/实现方案-2026-05-18-21-20-13.md b/工程分析/实现方案-2026-05-18-21-20-13.md new file mode 100644 index 0000000..8943ee3 --- /dev/null +++ b/工程分析/实现方案-2026-05-18-21-20-13.md @@ -0,0 +1,17 @@ +# 实现方案 + +开始时间:2026-05-18-21-20-13 + +## 后端改造 + +1. `_process_video` 每一帧都执行当前方法分割,用当前帧的 overlay 写入右侧结果视频。 +2. `max_frames` 仅控制下方结果卡片保存数量,不再控制结果视频中哪些帧执行分割。 +3. 对 `compare` 方法: + - 抽样关键帧保存多方法结果。 + - 结果视频仍使用该帧或非关键帧的 fusion overlay。 +4. 移除“最近掩膜持续叠加”的逻辑,避免运动帧错位。 + +## 测试增强 + +- 对选中的关键帧,读取结果视频同一帧并与保存的 overlay 图比较,确保视频画面和下方卡片一致。 +- 保留完整视频时长、fps 和多方法对比接口测试。 diff --git a/工程分析/测试方案-2026-05-18-21-20-13.md b/工程分析/测试方案-2026-05-18-21-20-13.md new file mode 100644 index 0000000..ec828f2 --- /dev/null +++ b/工程分析/测试方案-2026-05-18-21-20-13.md @@ -0,0 +1,30 @@ +# 测试方案 + +开始时间:2026-05-18-21-20-13 + +## 自动化测试 + +- `python3 -m compileall backend tests` +- `node --check frontend/app.js` +- `pytest -q` + +## 接口验证 + +- 使用内置样例视频调用 `/api/segment`。 +- 检查结果视频时长仍为 6 秒。 +- 检查第 25、55、70 帧附近有当前帧分割叠加。 + +## 浏览器验证 + +- 加载样例并运行分割。 +- 点击第 25、55、70 帧。 +- 确认右侧视频与下方对应卡片更一致,不再使用旧掩膜造成明显错位。 + +## 执行结果 + +- `python3 -m compileall backend tests`:通过。 +- `node --check frontend/app.js`:通过。 +- `pytest -q`:5 passed。 +- API 样例分割:结果视频仍为 6 秒,关键帧为 `[0, 5, 15, 20, 25, 30, 40, 45, 50, 55, 65, 70]`。 +- 第 25、55、70 帧:右侧视频帧与下方卡片 overlay 的平均像素差分别为 `3.184`、`3.45`、`3.464`,属于 H.264 压缩误差级别。 +- Chrome headless 页面验证:点击第 25、55、70 帧后,右侧时间分别为 `2.0833`、`4.5833`、`5.8333` 秒,并检测到稳定黄色叠加像素。 diff --git a/工程分析/经验记录.md b/工程分析/经验记录.md index 4ca677f..8de6a02 100644 --- a/工程分析/经验记录.md +++ b/工程分析/经验记录.md @@ -163,3 +163,15 @@ B. 产生问题原因:上一版结果视频为了保持 6 秒完整时间轴 C. 解决问题方案:后端在生成完整结果视频时维护最近一次分割掩膜;关键帧写真实叠加图,非关键帧用最近掩膜叠加到当前原始帧后写入,从而在关键帧之间持续显示分割提示。 D. 后续如何避免问题:当主视频播放器用于人工查看结果时,抽帧算法的关键帧结果不能只显示单帧;应使用持续叠加、插值或显式关键帧段落,保证用户拖动时能稳定看到结果。 + +## 2026-05-18-21-20-13 运动帧结果仍显得不匹配 + +### 1. 第 25、55、70 帧叠加结果与原始导丝位置有错位感 + +A. 具体问题:虽然关键帧附近不再空白,但用户观察第 25、55、70 帧时,右侧结果视频仍和左侧原始视频有不贴合的感觉。 + +B. 产生问题原因:上一版使用“最近一次分割掩膜持续叠加”来填充关键帧之间的视频画面。导丝是运动结构,旧掩膜套到后续当前帧上会形成残留或错位,尤其在弯曲和末端位置变化明显的帧上更突出。 + +C. 解决问题方案:后端生成右侧完整结果视频时改为每一帧都运行当前方法分割;`max_frames` 只控制下方结果卡片保存数量,不再控制主结果视频是否分割。关键帧卡片与视频对应帧用像素差测试校验一致性。 + +D. 后续如何避免问题:用于播放的结果视频必须逐帧使用当前帧结果;抽帧限制只能用于结果列表、调参预览或摘要,不应用来偷换主视频的逐帧分割语义。 diff --git a/工程分析/需求分析-2026-05-18-21-20-13.md b/工程分析/需求分析-2026-05-18-21-20-13.md new file mode 100644 index 0000000..bd47e03 --- /dev/null +++ b/工程分析/需求分析-2026-05-18-21-20-13.md @@ -0,0 +1,17 @@ +# 需求分析 + +开始时间:2026-05-18-21-20-13 + +## 用户问题 + +用户反馈:第 25、55、70 帧的右侧结果视频仍然感觉和左侧原始视频不匹配。 + +## 判断 + +上一版为了解决关键帧附近“空白”问题,在完整 6 秒结果视频中复用了最近一次分割掩膜。这个策略能避免纯原图,但会把旧掩膜套到后续运动帧上;当导丝位置变化明显时,结果就会出现视觉错位。 + +## 期望 + +- 右侧结果视频仍保持与原始视频同一时长、同一时间轴。 +- 右侧视频的每一帧都应使用当前帧自身的分割结果,而不是复用旧掩膜。 +- 下方结果卡片仍只保留抽样关键帧,避免界面过多图片。