fix: is_blank_frame双条件检测 + CSV列错位修复 + 自检逻辑 + 单元测试
This commit is contained in:
@@ -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")
|
||||
arr = np.array(img)
|
||||
white_ratio = float(np.mean(arr > BLANK_FRAME_BRIGHTNESS_THRESHOLD))
|
||||
is_blank = white_ratio < BLANK_FRAME_WHITE_PIXEL_RATIO
|
||||
max_brightness = int(np.max(arr))
|
||||
# 阈值固定用 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:
|
||||
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_ratio, max_brightness
|
||||
return is_blank, white_pixel_ratio, max_brightness
|
||||
|
||||
|
||||
# ========================================================================
|
||||
@@ -638,6 +645,33 @@ def split_video(
|
||||
print(f"[video_split] dry-run keyframes.json 写入: {keyframes_path}")
|
||||
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 {
|
||||
"keyframes_path": str(keyframes_path),
|
||||
"audio_path": str(audio_path),
|
||||
|
||||
@@ -17,7 +17,7 @@ from doco.src.video_split import (
|
||||
hamming_distance,
|
||||
format_timestamp,
|
||||
build_b_manuscript,
|
||||
compute_phash,
|
||||
is_blank_frame,
|
||||
)
|
||||
|
||||
|
||||
@@ -139,3 +139,67 @@ class TestKeyframesJson:
|
||||
assert loaded["phash_threshold"] == 8
|
||||
assert len(loaded["keyframes"]) == 1
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user