2026-05-18-20-35-32 修复结果视频关键帧空白

This commit is contained in:
2026-05-18 20:39:52 +08:00
parent 5264c5c7fc
commit 69444ced72
6 changed files with 93 additions and 1 deletions

View File

@@ -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]

View File

@@ -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(

View File

@@ -0,0 +1,19 @@
# 实现方案
开始时间2026-05-18-20-35-32
## 后端
1.`_process_video` 中维护最近一次成功分割得到的 `active_mask`
2. 命中抽帧关键帧时:
- 执行分割。
- 保存该帧原图、掩膜、叠加图。
- 更新 `active_mask`
- 结果视频写入该关键帧叠加图。
3. 未命中抽帧关键帧时:
- 如果已有 `active_mask`,将最近掩膜叠加到当前原始帧上写入结果视频。
- 如果还没有 `active_mask`,写入原始帧。
## 测试增强
- 在视频 API 测试中读取结果视频的非关键帧,检查其仍包含黄色叠加像素,避免结果视频在关键帧之间回到纯原图。

View File

@@ -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 个黄色叠加像素。

View File

@@ -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. 后续如何避免问题:当主视频播放器用于人工查看结果时,抽帧算法的关键帧结果不能只显示单帧;应使用持续叠加、插值或显式关键帧段落,保证用户拖动时能稳定看到结果。

View File

@@ -0,0 +1,17 @@
# 需求分析
开始时间2026-05-18-20-35-32
## 用户问题
用户反馈:当前选择到第 40 帧时,右侧结果视频看起来是空白或没有分割叠加。
## 判断
第 40 帧的结果卡片和后端保存的 `frame_0040_overlay.png` 本身包含分割叠加,不是算法完全未输出。问题出在右侧完整时长结果视频:上一轮为了让右侧视频保持 6 秒,只在被抽中的关键帧写入叠加画面,未抽中的帧写入原始画面。浏览器 seek 到第 40 帧附近时,可能显示相邻未抽中帧,于是看起来像空白。
## 期望
- 结果视频保持与原始视频相同 6 秒时间轴。
- 在关键帧附近拖动或点击时,不应闪回纯原图。
- 下方帧卡片仍展示真实被分割的关键帧。