Fix smart_detect: correct pos index, remove O(n²) loop, clean up
Docker Build and Push / build-and-push (cpu, latest) (push) Has been skipped
Docker Build and Push / build-and-push (cuda, 11.8) (push) Has been skipped
Docker Build and Push / check-secrets (push) Successful in 5s
Docker Build and Push / build-and-push (cuda, 12.6) (push) Has been skipped
Docker Build and Push / build-and-push (cuda, 12.8) (push) Has been skipped
Docker Build and Push / build-and-push (directml, latest) (push) Has been skipped
Build Windows CPU / build (push) Has been cancelled
Build Windows CUDA 11.8 / build (push) Has been cancelled
Build Windows CUDA 12.6 / build (push) Has been cancelled
Build Windows CUDA 12.8 / build (push) Has been cancelled
Build Windows DirectML / build (push) Has been cancelled

- actual_pos_s[] tracks real timestamps per frame (was using positions[i]
  which breaks when frames fail to read)
- Replace O(n²) np.array_equal logo frame matching with simple bool list
- Remove unused pos_set variable
- Fix docstring still mentioning '8s tail' after removing it
- All variables consistent: logo_gray_frames replaces logo_frames_gray

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
liuchen
2026-06-27 16:16:48 +08:00
co-authored by Claude Haiku 4.5
parent 5cdc727207
commit 8b583a4b0e
+31 -55
View File
@@ -15,92 +15,68 @@ def detect_static_regions(video_path: str, sample_frames: int = 30, threshold: f
max_scan_seconds: float = None):
"""
饱和度签名 + 跨帧对比法检测水印/Logo。
采样策略:每秒采一帧(顺序读取,不随机 seek)+ 超长视频后段每8秒一帧。
分析时缩放到 1/4 分辨率提速,结果坐标还原到原始尺寸。
采样策略:全程每秒一帧,分析时缩放到 1/4 分辨率提速,坐标还原回原始尺寸。
"""
cap = cv2.VideoCapture(video_path)
total = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
fps = cap.get(cv2.CAP_PROP_FPS) or 30.0
w = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
h = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
orig_frame_w, orig_frame_h = w, h # 保存原始分辨率,用于最终坐标还原
orig_frame_w = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
orig_frame_h = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
duration_s = total / fps
# ── 采样策略:全程每秒采一帧 ─────────────────────────────────────────
# 全程每秒采一帧
step = max(1, int(fps))
positions = list(range(0, total, step))
# ── 顺序读取,缩放到 1/4 分辨率加速分析 ────────────────────────────────
SW, SH = max(1, w // 4), max(1, h // 4) # 分析用小尺寸
pos_set = set(positions)
bgr_frames_small = [] # 缩放后的小帧(用于分析)
bgr_frames_orig = [] # 原始尺寸帧(用于 SAM2,只保留少数几帧)
# 分析用 1/4 分辨率加速
SW, SH = max(1, orig_frame_w // 4), max(1, orig_frame_h // 4)
bgr_frames = [] # 小尺寸帧,用于分析
gray_frames = []
keep_orig_interval = max(1, len(positions) // 8) # 每隔若干帧保留一张原始尺寸
cap.set(cv2.CAP_PROP_POS_FRAMES, 0)
frame_idx = 0
pos_iter = iter(sorted(positions))
next_pos = next(pos_iter, None)
while next_pos is not None:
if frame_idx < next_pos:
# 快速跳帧:只有在需要的位置才真正读
cap.set(cv2.CAP_PROP_POS_FRAMES, next_pos)
frame_idx = next_pos
actual_pos_s = [] # 每帧对应的时间戳(秒)
for pos in positions:
cap.set(cv2.CAP_PROP_POS_FRAMES, pos)
ret, frame = cap.read()
if not ret:
next_pos = next(pos_iter, None)
continue
small = cv2.resize(frame, (SW, SH), interpolation=cv2.INTER_AREA)
bgr_frames_small.append(small)
bgr_frames.append(small)
gray_frames.append(cv2.cvtColor(small, cv2.COLOR_BGR2GRAY).astype(np.float32))
if len(bgr_frames_orig) == 0 or len(bgr_frames_small) % keep_orig_interval == 0:
bgr_frames_orig.append(frame)
frame_idx += 1
next_pos = next(pos_iter, None)
actual_pos_s.append(pos / fps)
cap.release()
# 后续分析全部用小尺寸帧
bgr_frames = bgr_frames_small
w, h = SW, SH # 分析坐标空间切换到小尺寸
print(f"[SAM2-detect] 采样 {len(bgr_frames)} 帧(全程每秒一帧,共{duration_s:.1f}s,分析尺寸{SW}x{SH})", flush=True)
if len(bgr_frames) < 4:
print(f"[SAM2-detect] 帧数不足4帧,返回空", flush=True)
return []
# ── 阶段1:通过高饱和度识别含logo帧 ──────────────────────────────────
# 水印/logo通常色彩比视频自然内容更鲜艳(高饱和度)
# 在帧的上半部分(logo常出现区域)检测高饱和度像素数
top_h = h // 5 # 检查帧顶部 20%
logo_sat_thresh = 200 # 饱和度 > 200 认为是logo颜色
logo_min_pixels = top_h * w * 0.003 # 至少 0.3% 的上方区域为logo颜色
# ── 阶段1:高饱和度识别含logo帧 ─────────────────────────────────────
top_h = SH // 5
logo_sat_thresh = 200
logo_min_pixels = top_h * SW * 0.003
logo_frames_gray = []
for i, (bgr, gray) in enumerate(zip(bgr_frames, gray_frames)):
is_logo = []
for i, bgr in enumerate(bgr_frames):
hsv = cv2.cvtColor(bgr, cv2.COLOR_BGR2HSV)
high_sat = int(np.sum(hsv[:top_h, :, 1] > logo_sat_thresh))
pos_s = positions[i] / fps
if high_sat >= logo_min_pixels:
logo_frames_gray.append(gray)
print(f"[SAM2-detect] ✓ {pos_s:.1f}s: 高饱和={high_sat}px (含logo)", flush=True)
else:
print(f"[SAM2-detect] ✗ {pos_s:.1f}s: 高饱和={high_sat}px (无logo)", flush=True)
has_logo = high_sat >= logo_min_pixels
is_logo.append(has_logo)
mark = '✓' if has_logo else '✗'
print(f"[SAM2-detect] {mark} {actual_pos_s[i]:.1f}s: 高饱和={high_sat}px", flush=True)
print(f"[SAM2-detect] logo含帧: {len(logo_frames_gray)}/{len(bgr_frames)}", flush=True)
logo_bgr_frames = [bgr_frames[i] for i, v in enumerate(is_logo) if v]
nlogo_bgr_frames = [bgr_frames[i] for i, v in enumerate(is_logo) if not v]
logo_gray_frames = [gray_frames[i] for i, v in enumerate(is_logo) if v]
logo_bgr_frames = [bgr_frames[i] for i, g in enumerate(gray_frames)
if any(np.array_equal(g, l) for l in logo_frames_gray)]
nlogo_bgr_frames = [bgr_frames[i] for i, g in enumerate(gray_frames)
if not any(np.array_equal(g, l) for l in logo_frames_gray)]
print(f"[SAM2-detect] logo含帧: {len(logo_bgr_frames)}/{len(bgr_frames)}", flush=True)
if len(logo_frames_gray) < 2:
# 分析时使用小尺寸坐标空间
w, h = SW, SH
if len(logo_gray_frames) < 2:
print(f"[SAM2-detect] logo帧不足,回退到时序std法", flush=True)
stack = np.stack(gray_frames, axis=0)
std_map = np.std(stack, axis=0)