fix: is_blank_frame双条件检测 + CSV列错位修复 + 自检逻辑 + 单元测试

This commit is contained in:
simonkoson
2026-06-12 18:48:29 +08:00
parent d14bcc2778
commit 513ccb34d4
2 changed files with 108 additions and 10 deletions
+43 -9
View File
@@ -176,28 +176,35 @@ def extract_audio(
# ======================================================================== # ========================================================================
def is_blank_frame(image_path: Path, debug: bool = False) -> Tuple[bool, float]: def is_blank_frame(image_path: Path, debug: bool = False) -> Tuple[bool, float, int]:
""" """
判断是否为空白帧(留气口黑画面) 判断是否为空白帧(留气口黑画面)
算法:转灰度,统计亮度 > 200(接近白)的像素数量 双条件检测(两条件必须同时满足才算"有字幕"):
如果占比 < 0.5%,判定为空白帧 - 条件 A: max_brightness >= 240 (有接近纯白的像素)
- 条件 B: white_pixel_ratio >= 0.005 (白像素占比 >= 0.5%)
任一条件不满足 → is_blank=True
适用于:黑底白字场景(军事科技栏目) 适用于:黑底白字场景(军事科技栏目)
返回: (is_blank, white_ratio) 返回: (is_blank, white_pixel_ratio, max_brightness)
""" """
img = Image.open(image_path).convert("L") img = Image.open(image_path).convert("L")
arr = np.array(img) arr = np.array(img)
white_ratio = float(np.mean(arr > BLANK_FRAME_BRIGHTNESS_THRESHOLD)) max_brightness = int(np.max(arr))
is_blank = white_ratio < BLANK_FRAME_WHITE_PIXEL_RATIO # 阈值固定用 240,与条件 A 一致
white_pixel_ratio = float(np.mean(arr > BLANK_FRAME_BRIGHTNESS_THRESHOLD))
# 双条件:同时满足才算有字幕
has_subtitle = (max_brightness >= BLANK_FRAME_BRIGHTNESS_THRESHOLD) and (white_pixel_ratio >= BLANK_FRAME_WHITE_PIXEL_RATIO)
is_blank = not has_subtitle
if debug: if debug:
frame_idx = image_path.stem frame_idx = image_path.stem
print(f"[debug] {frame_idx}, white_ratio={white_ratio:.6f}, is_blank={is_blank}") print(f"[debug] {frame_idx}, max_brightness={max_brightness}, white_pixel_ratio={white_pixel_ratio:.6f}, is_blank={is_blank}")
max_brightness = int(np.max(arr)) return is_blank, white_pixel_ratio, max_brightness
return is_blank, white_ratio, max_brightness
# ======================================================================== # ========================================================================
@@ -638,6 +645,33 @@ def split_video(
print(f"[video_split] dry-run keyframes.json 写入: {keyframes_path}") print(f"[video_split] dry-run keyframes.json 写入: {keyframes_path}")
print("[video_split] 请检查 frames/ 目录下的关键帧图片,确认裁切框位置正确") print("[video_split] 请检查 frames/ 目录下的关键帧图片,确认裁切框位置正确")
# ---- 自检:随机抽 3 张最终关键帧 PNG,验证像素数据与 CSV 一致 ----
keyframe_files = sorted(frames_dir.glob("frame_*.png"))
import random
sample_frames = random.sample(keyframe_files, min(3, len(keyframe_files)))
for frame_file in sample_frames:
is_blank_check, white_ratio_check, max_brightness_check = is_blank_frame(frame_file, debug=False)
frame_idx_str = frame_file.stem # e.g. "frame_0001"
# CSV 里 frame_index = 1 对应 frame_0001
frame_idx_in_csv = int(frame_idx_str.split("_")[1])
if frame_idx_in_csv in frame_analysis:
csv_max_bright = None
csv_white_ratio = None
# 从 CSV 原始行找对应 frame_index 的数据
with open(debug_csv_path, "r", encoding="utf-8") as csvf:
reader = csv.DictReader(csvf)
for row in reader:
if int(row["frame_index"]) == frame_idx_in_csv:
csv_max_bright = int(row["max_brightness"])
csv_white_ratio = float(row["white_pixel_ratio"])
break
if csv_max_bright is not None:
if abs(csv_max_bright - max_brightness_check) > 1 or abs(csv_white_ratio - white_ratio_check) > 0.0001:
print(f"[WARNING] 自检失败: {frame_file.name} 像素数据与 CSV 不一致! CSV: max_brightness={csv_max_bright}, white_ratio={csv_white_ratio:.6f}; 实际重读: max_brightness={max_brightness_check}, white_ratio={white_ratio_check:.6f}")
raise RuntimeError(f"自检失败: {frame_file.name} 像素数据与 CSV 记录不一致,已中止")
else:
print(f"[自检] {frame_file.name}: max_brightness={max_brightness_check}, white_ratio={white_ratio_check:.6f}")
return { return {
"keyframes_path": str(keyframes_path), "keyframes_path": str(keyframes_path),
"audio_path": str(audio_path), "audio_path": str(audio_path),
+65 -1
View File
@@ -17,7 +17,7 @@ from doco.src.video_split import (
hamming_distance, hamming_distance,
format_timestamp, format_timestamp,
build_b_manuscript, build_b_manuscript,
compute_phash, is_blank_frame,
) )
@@ -139,3 +139,67 @@ class TestKeyframesJson:
assert loaded["phash_threshold"] == 8 assert loaded["phash_threshold"] == 8
assert len(loaded["keyframes"]) == 1 assert len(loaded["keyframes"]) == 1
assert loaded["keyframes"][0]["frame_index"] == 0 assert loaded["keyframes"][0]["frame_index"] == 0
class TestIsBlankFrame:
"""is_blank_frame 双条件检测单元测试"""
def test_pure_black_frame(self, tmp_path):
"""全 0 纯黑图:max_brightness=0,white_ratio=0,is_blank=True"""
from PIL import Image
img_path = tmp_path / "black.png"
img = Image.new("L", (100, 100), 0) # 全黑
img.save(img_path)
is_blank, white_ratio, max_brightness = is_blank_frame(img_path)
assert is_blank is True
assert max_brightness == 0
assert white_ratio == 0.0
def test_subtitle_frame(self, tmp_path):
"""有少量白像素(亮度255,占~1%)的图:is_blank=False"""
from PIL import Image
import numpy as np
img_path = tmp_path / "subtitle.png"
arr = np.zeros((100, 100), dtype=np.uint8)
# 1% 白像素(亮度255),其余黑(亮度0)
arr[:100, :10] = 255
img = Image.fromarray(arr, mode="L")
img.save(img_path)
is_blank, white_ratio, max_brightness = is_blank_frame(img_path)
assert is_blank is False
assert max_brightness == 255
assert 0.005 <= white_ratio <= 0.02 # ~1%
def test_max_brightness_ok_but_ratio_too_low(self, tmp_path):
"""max_brightness>=240 但 white_ratio<0.005 → is_blank=True"""
from PIL import Image
import numpy as np
img_path = tmp_path / "few_pixels.png"
arr = np.zeros((100, 100), dtype=np.uint8)
# 只有 0.1% 白像素(不够 0.5%),但 max_brightness=255
arr[:10, :1] = 255
img = Image.fromarray(arr, mode="L")
img.save(img_path)
is_blank, white_ratio, max_brightness = is_blank_frame(img_path)
assert is_blank is True
assert max_brightness == 255
assert white_ratio < 0.005
def test_ratio_ok_but_max_brightness_too_low(self, tmp_path):
"""white_ratio>=0.005 但 max_brightness<240 → is_blank=True"""
from PIL import Image
import numpy as np
img_path = tmp_path / "dim_pixels.png"
arr = np.zeros((100, 100), dtype=np.uint8)
# 1% 像素亮度=220(不够240阈值),其余黑
arr[:100, :10] = 220
img = Image.fromarray(arr, mode="L")
img.save(img_path)
is_blank, white_ratio, max_brightness = is_blank_frame(img_path)
assert is_blank is True
assert max_brightness == 220
assert white_ratio >= 0.005