示例成品 · 平台演示,按左边这组点选真跑出来的
我这端没收到可打开的附件,只看到文件名,不能瞎报“第几帧有问题”。
你现在二选一:
1. 重新上传 `04_012_cleaned_v03.mov + 04_012_source_plate.exr`,或给可读网盘链接;
2. 先按下面方法跑出逐帧标注,再把 `mark_*.png + report.csv` 发我,我帮你人工复核。
---
## 1. 先把清理镜头转成逐帧图
```bash
ffmpeg -i 04_012_cleaned_v03.mov -start_number 1 frames/%05d.png
```
源板如果是单张 EXR:
```bash
ffmpeg -i 04_012_source_plate.exr -vf format=rgb24 source_plate.png
```
如果是 EXR 序列:
```bash
ffmpeg -i 04_012_source_plate_%04d.exr -start_number 1 source_%05d.png
```
---
## 2. 自动查两件事
- `JUMP`:时序抖动突变,即相邻帧局部运动方向/速度突然跳变。
- `CLONE_REPEAT`:可疑跳动区域周围还有高相似重复块,疑似克隆纹理。
脚本保存为 `check_jump_clone.py`:
```python
import os, glob, csv
import cv2
import numpy as np
FRAME_DIR = "frames"
OUT_DIR = "marks"
os.makedirs(OUT_DIR, exist_ok=True)
files = sorted(glob.glob(os.path.join(FRAME_DIR, "*.png")))
if len(files) < 2:
raise SystemExit("没有帧,先跑 ffmpeg 转帧")
prev = cv2.imread(files[0])
prev_gray = cv2.cvtColor(prev, cv2.COLOR_BGR2GRAY)
prev_flow = None
rows = []
for idx in range(1, len(files)):
cur = cv2.imread(files[idx])
if cur is None:
continue
gray = cv2.cvtColor(cur, cv2.COLOR_BGR2GRAY)
h, w = gray.shape
flow = cv2.calcOpticalFlowFarneback(
prev_gray, gray, None, 0.5, 3, 15, 3, 5, 1.2, 0
)
if prev_flow is not None:
# 帧间运动变化量:jerk
jerk = np.linalg.norm(flow - prev_flow, axis=2)
jerk = cv2.GaussianBlur(jerk.astype(np.float32), (5, 5), 0)
med = float(np.median(jerk))
thr = max(1.0, med * 2.0) # 调小更敏感,调大更保守
_, mask = cv2.threshold(jerk, thr, 255, cv2.THRESH_BINARY)
mask = cv2.morphologyEx(
mask.astype(np.uint8), cv2.MORPH_CLOSE, np.ones((5, 5), np.uint8)
)
cnts = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
contours = cnts[0] if len(cnts) == 2 else cnts[1]
for c in contours:
x, y, bw, bh = cv2.boundingRect(c)
if bw * bh < 60 or bw < 5 or bh < 5:
continue
score = float(np.mean(jerk[y:y+bh, x:x+bw]))
rows.append([
idx, os.path.basename(files[idx]),
"JUMP", x, y, bw, bh, round(score, 2)
])
cv2.rectangle(cur, (x, y), (x+bw, y+bh), (0, 0, 255), 2)
cv2.putText(
cur, "JUMP", (x, y-5),
cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 255), 1
)
# 只在 JUMP 区域周边查重复纹理块
pad = 12
bx0, by0 = max(0, x-pad), max(0, y-pad)
bx1, by1 = min(w, x+bw+pad), min(h, y+bh+pad)
rw, rh = bx1-bx0, by1-by0
if rw < 16 or rh < 16:
continue
templ = gray[by0:by1, bx0:bx1]
res = cv2.matchTemplate(gray, templ, cv2.TM_CCOEFF_NORMED)
_, max_val, _, max_loc = cv2.minMaxLoc(res)
cx, cy = max_loc
# 排除自己
if bx0 <= cx <= bx0 + rw and by0 <= cy <= by0 + rh:
continue
if max_val > 0.95: # 调高更严格,调低更敏感
rows.append([
idx, os.path.basename(files[idx]),
"CLONE_REPEAT", cx, cy, rw, rh, round(float(max_val), 3)
])
cv2.rectangle(cur, (cx, cy), (cx+rw, cy+rh), (0, 255, 255), 2)
cv2.putText(
cur, "CLONE", (cx, cy-5),
cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 255), 1
)
cv2.imwrite(os.path.join(OUT_DIR, f"mark_{idx:05d}.png
点左边「开工 · 直接出成品」,出一份你自己的版本(文字免费)