Files
CADx-4/tests/test_ceus_split_screen.py
T
iomgaa 9a916c65a5 feat(ceus): 任务书 02 第一阶段——底层和分割之前的算法;修复 C14
任务书 01 遗留修复(C14):
- library 新增 check_key_image(path):关键图能逐像素保真重编码时返回 None,否则返回原因;
  _write_clean_png 的判断移过去,自身保留断言性质报错(走到这里说明 build 漏了检查)。
- build 写库前对每例调用,返回原因的整例跳过,记 C14「关键图能原样重编码」,涉及值写原因。
- 原来三条「关键图格式不对整次构建停下」的测试改为整例跳过,另加非 PNG、library 漏检报错两条。
- 结构.md 第八部分、guides/装配两个库.md 相应几行改掉。

第一阶段(src/cadx/ceus/):
- 照搬旧 geometry、video_decoder、research_config、split_screen、green_prompt、prompt_frames、
  mapping,去掉产品层,注释改中文。
- 配置类去掉全部默认值,strict、extra=forbid、frozen;新建 config/ceus.yaml(paths、ceus 两段),
  数值照抄旧 yaml,逐项注明出处。删去 source_view/target_view、effective_region、mapping 锚点;
  旧代码写死的分屏抽帧数 25 和搜索区间 (0.35, 0.65) 改成配置,值不变。
- 视频解码改为按路径用 cv2 逐帧读。
- 坐标换算改为两个视图矩形左上角之差,不再用按某种宽度写死的锚点。
- 造影画面在哪一半:两半各自选提示帧,恰好一半有才定,否则抛 ContrastSideError(CeusCaseError
  子类,带步号和原因,供第三阶段记 failed.csv)。
- 旧测试照搬(改 import 与配置读取方式),因上述改动不再成立的删去;新增两处修改的手算测试。
- import-linter 加「ceus 不引用其他 cadx 模块」。依赖加 numpy、opencv-python-headless。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 03:46:56 -04:00

131 lines
5.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""B2 split_screen 分屏检测单元测试(旧文件的黑边剔除部分随裁黑边功能删去)。
钉死两件事的关键性质:
- ``detect_split_x``:合成左右两半(中间一列黑缝)→ 切分线落在中缝 ±2px;
全黑 / 无缝 / 切分偏出中部合理范围 → ``SplitDetectionError``(不兜底)。
- ``view_rect``:horizontal_split 用检测到的 ``split_x`` 而非 ``W//2``;
vertical_split / full / manual_crop 分支与参考一致;非法组合 ``RuntimeError``。
测试用真实数据的二次构造(合成左右分屏帧),边缘场景靠实现 raise 保证。
"""
import numpy as np
import pytest
from cadx.ceus import (
SplitDetectionError,
ViewRect,
detect_split_x,
view_rect,
)
from conftest import load_ceus_section
# 旧 detect_split_x 的参数默认值;新代码不设默认值,从 config/ceus.yaml 读。
_LO, _HI = load_ceus_section()["views"]["split_search_band"]
BAND = (float(_LO), float(_HI))
def _split_frame(
height: int,
width: int,
split_x: int,
*,
left_gray: int = 180,
right_gray: int = 120,
) -> np.ndarray:
"""合成一帧左右分屏 BGR 图:左右两半各为常量灰度,``split_x`` 处一列纯黑缝。"""
frame = np.zeros((height, width, 3), dtype=np.uint8)
frame[:, :split_x] = left_gray
frame[:, split_x + 1 :] = right_gray
frame[:, split_x] = 0
return frame
# Phase 1: detect_split_x —— 中缝定位与防御
def test_detect_split_x_finds_centered_seam() -> None:
frames = [_split_frame(120, 200, 100) for _ in range(5)]
detected = detect_split_x(frames, search_band=BAND)
assert abs(detected - 100) <= 2
def test_detect_split_x_finds_offcenter_seam_within_band() -> None:
# 缝在 0.42*W 处,仍落在 band=(0.35,0.65) 内、左右宽度比合法。
frames = [_split_frame(120, 200, 84) for _ in range(5)]
detected = detect_split_x(frames, search_band=BAND)
assert abs(detected - 84) <= 2
def test_detect_split_x_all_black_raises() -> None:
frames = [np.zeros((120, 200, 3), dtype=np.uint8) for _ in range(3)]
with pytest.raises(SplitDetectionError):
detect_split_x(frames, search_band=BAND)
def test_detect_split_x_no_seam_raises() -> None:
# 整幅均匀灰度、无暗缝:中部 band 内列均值平坦,找不到稳定低谷。
frames = [np.full((120, 200, 3), 150, dtype=np.uint8) for _ in range(3)]
with pytest.raises(SplitDetectionError):
detect_split_x(frames, search_band=BAND)
def test_detect_split_x_empty_frames_raises() -> None:
with pytest.raises(SplitDetectionError):
detect_split_x([], search_band=BAND)
def test_detect_split_x_reversed_band_raises() -> None:
# 逆序 band(lo>=hi)非法:要求 0<=lo<hi<=1,应直接报错而非兜底。
frames = [_split_frame(120, 200, 100) for _ in range(3)]
with pytest.raises(SplitDetectionError):
detect_split_x(frames, search_band=(0.6, 0.4))
def test_detect_split_x_out_of_range_band_raises() -> None:
# 越界 band(hi>1)非法。
frames = [_split_frame(120, 200, 100) for _ in range(3)]
with pytest.raises(SplitDetectionError):
detect_split_x(frames, search_band=(0.35, 1.5))
def test_detect_split_x_width_ratio_violation_raises() -> None:
# 把 band 放宽到几乎整宽,缝置于极偏位置(仍在 band 内),使左右宽度比越界。
# split_x=20、W=200 → 左 20 / 右 180,比值 0.11 < 0.5 → SplitDetectionError。
frames = [_split_frame(120, 200, 20) for _ in range(3)]
with pytest.raises(SplitDetectionError):
detect_split_x(frames, search_band=(0.05, 0.95))
# Phase 2: view_rect —— horizontal_split 用 split_x,而非 W//2
def test_view_rect_horizontal_split_uses_split_x() -> None:
width, height, split_x = 200, 120, 84
left = view_rect({"layout": "horizontal_split"}, "left", width, height, split_x=split_x)
right = view_rect({"layout": "horizontal_split"}, "right", width, height, split_x=split_x)
assert left == ViewRect(0, 0, split_x, height)
assert right == ViewRect(split_x, 0, width, height)
# 与朴素 W//2 不同,证明用了检测值。
assert split_x != width // 2
def test_view_rect_vertical_split() -> None:
width, height = 200, 120
top = view_rect({"layout": "vertical_split"}, "top", width, height, split_x=None)
bottom = view_rect({"layout": "vertical_split"}, "bottom", width, height, split_x=None)
assert top == ViewRect(0, 0, width, height // 2)
assert bottom == ViewRect(0, height // 2, width, height)
def test_view_rect_full() -> None:
rect = view_rect({"layout": "full"}, "left", 200, 120, split_x=None)
assert rect == ViewRect(0, 0, 200, 120)
def test_view_rect_manual_crop() -> None:
cfg = {"layout": "manual_crop", "crops": {"left": [0.0, 0.0, 0.5, 1.0]}}
rect = view_rect(cfg, "left", 200, 120, split_x=None)
assert rect == ViewRect(0, 0, 100, 120)
def test_view_rect_unsupported_pair_raises() -> None:
with pytest.raises(RuntimeError):
view_rect({"layout": "horizontal_split"}, "top", 200, 120, split_x=100)