Files
CADx-4/tests/test_ceus_registration.py
T
iomgaa 00ebbffb28 feat(ceus): 两半坐标换算改为每段视频配准,加目视核对脚本
- 第 1 步分屏后在两半扇形区内用边缘图归一化互相关估 (dx, dy),再用逐帧多块模板匹配的中位数独立核对;
  两者各分量之差超过门槛(15 像素)这一例第 1 步失败。删掉「两个视图左上角相减」的实现与测试。
- 门槛依据:34 例中走完第 1 步的 29 例,差值中位 2、95 分位 6.2、最大 13;参数与门槛在同一批视频上调出。
- scripts/ceus_align_review.py:只做第 1 步,出扇形区棋盘图与边缘叠加图,混入错开 15–30 像素的
  阴性对照、随机编号;计分时任一对照未被判「没对齐」即作废。
- ceus_quant 读 review.csv:不通过的病例第 1 步失败;表里没有这段视频或平移对不上即停。
- 配准与紫圈颜色参数进分割缓存键。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 06:47:27 -04:00

145 lines
5.3 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.
"""两半配准:每段视频单独估左半 → 右半的平移(任务书 02 第一阶段第 1 条)。
合成分屏画面(`fake_split_scene`):每半 240×200,左半扇形是一个梯形,右半的扇形和纹理是左半平移
(dx, dy) 后的样子,再换一种亮度、加独立噪声。预期值就是造图时设定的平移,不从实现里推。
"""
import numpy as np
import pytest
from cadx.ceus import (
CeusCaseError,
CeusConfig,
RegistrationError,
ViewRect,
register_halves,
)
from fake_split_scene import SplitScene, make_scene, scene_gray, to_bgr
_HEIGHT, _HALF = 200, 240
_LEFT, _RIGHT = ViewRect(0, 0, _HALF, _HEIGHT), ViewRect(_HALF, 0, 2 * _HALF, _HEIGHT)
# 左半局部坐标的梯形扇形:上窄下宽,顶边在顶部信息栏(200 × 0.0834 ≈ 17 行)之下。
_FAN = [(80, 30), (170, 30), (215, 185), (35, 185)]
def _frames(scene: SplitScene, *, green_box: bool) -> list[np.ndarray]:
rng = np.random.default_rng(11)
frames: list[np.ndarray] = []
for index in range(12):
frame = to_bgr(scene_gray(scene, index, rng))
if green_box:
# 绿框只画在左半:配准前要修补掉,否则它的边缘只在一半上有。
frame[90:140, 100] = frame[90:140, 160] = (0, 255, 0)
frame[90, 100:161] = frame[140, 100:161] = (0, 255, 0)
frames.append(frame)
return frames
@pytest.mark.parametrize(
("offset", "green_box"),
[
((-37, 0), False), # 负的水平偏移(真实视频里最常见:造影那一半扇形左边有黑边)
((25, 4), False), # 正的水平偏移、非零竖直偏移
((-12, -6), True), # 两个分量都为负,左半画着绿框
((0, 0), False),
],
)
def test_known_offset_is_recovered_by_both_methods(
ceus_config: CeusConfig, offset: tuple[int, int], green_box: bool
) -> None:
scene = make_scene(
height=_HEIGHT,
half_width=_HALF,
fan_polygon=_FAN,
offset=offset,
interior_shift=None,
texture_std=25.0,
right_gain=0.8,
right_bias=10.0,
seed=5,
)
registration = register_halves(
_frames(scene, green_box=green_box),
_LEFT,
_RIGHT,
ceus_config.registration,
ceus_config.prompt_source,
ceus_config.pan_source,
)
assert registration.correlation == offset
assert registration.template == offset
assert registration.disagreement_px == 0
# 造影在左半时平移照用;造影在右半时(把右半的东西挪到左半)取反。
assert registration.contrast_to_target("left") == offset
assert registration.contrast_to_target("right") == (-offset[0], -offset[1])
# 冲突画面用更宽的扇形:模板块落在扇形中部,纹理平移后真正的匹配位置仍在右半扇形内。
_WIDE_FAN = [(40, 25), (200, 25), (235, 195), (5, 195)]
def _conflicting_scene() -> SplitScene:
"""扇形轮廓按 (15, 0) 平移,里面的纹理按 (−15, 0) 平移:两种方法各信一样。
纹理起伏(标准差 5)远小于扇形内外的亮度差(80),边缘图上占主导的是扇形轮廓,互相关跟着
轮廓走;模板块整块落在扇形里,只看得到纹理,跟着纹理走。
"""
return make_scene(
height=_HEIGHT,
half_width=_HALF,
fan_polygon=_WIDE_FAN,
offset=(15, 0),
interior_shift=(-15, 0),
texture_std=5.0,
right_gain=0.8,
right_bias=10.0,
seed=5,
)
def test_methods_disagree_on_conflicting_scene(ceus_config: CeusConfig) -> None:
"""门槛放到极大时两种方法各自的估计:互相关跟轮廓 (15, 0),模板匹配跟纹理 (−15, 0)。"""
loose = ceus_config.registration.model_copy(update={"max_disagreement_px": 10_000})
registration = register_halves(
_frames(_conflicting_scene(), green_box=False),
_LEFT,
_RIGHT,
loose,
ceus_config.prompt_source,
ceus_config.pan_source,
)
assert registration.correlation == (15, 0)
assert registration.template == (-15, 0)
assert registration.disagreement_px == 30
def test_disagreement_beyond_threshold_fails_case_at_step_1(ceus_config: CeusConfig) -> None:
with pytest.raises(RegistrationError, match="两半配准不一致") as raised:
register_halves(
_frames(_conflicting_scene(), green_box=False),
_LEFT,
_RIGHT,
ceus_config.registration,
ceus_config.prompt_source,
ceus_config.pan_source,
)
assert isinstance(raised.value, CeusCaseError)
assert raised.value.step == 1
# 原因里附两个估计值。
assert "(15, 0)" in raised.value.reason and "(-15, 0)" in raised.value.reason
def test_static_half_has_no_fan_and_fails(ceus_config: CeusConfig) -> None:
"""一半画面完全不动(没有扇形区):第 1 步失败,不猜一个平移。"""
frames = [np.full((_HEIGHT, 2 * _HALF, 3), 60, dtype=np.uint8) for _ in range(12)]
with pytest.raises(RegistrationError, match="找不到扇形区") as raised:
register_halves(
frames,
_LEFT,
_RIGHT,
ceus_config.registration,
ceus_config.prompt_source,
ceus_config.pan_source,
)
assert raised.value.step == 1