"""造影画面在哪一半:两半各自选提示帧,恰好一半有才定(任务书 02 第一阶段第 2 条)。 合成 400×200 的分屏视频,分屏线在 x=200;绿框是 120×100 的实心框(外接框面积 12000, 大于 config/ceus.yaml 的 min_area 10000),画在顶部文字带之下。预期值按画框位置手算。 """ import cv2 import numpy as np import pytest from cadx.ceus import ( CeusCaseError, CeusConfig, ContrastSideError, ViewRect, locate_contrast_side, map_source_mask_to_target, split_views, ) from cadx.ceus._geometry import Frame _WIDTH, _HEIGHT, _SPLIT = 400, 200, 200 _GREEN = (0, 255, 0) def _frames(green_boxes_x1: list[int]) -> list[Frame]: """10 帧分屏画面;每个 x1 画一个整帧坐标 [x1, x1+119] × [60, 159] 的绿框。""" frame = np.zeros((_HEIGHT, _WIDTH, 3), dtype=np.uint8) frame[:, :_SPLIT] = 120 frame[:, _SPLIT + 1 :] = 90 for x1 in green_boxes_x1: cv2.rectangle(frame, (x1, 60), (x1 + 119, 159), _GREEN, thickness=-1) return [frame.copy() for _ in range(10)] def test_green_box_in_left_half(ceus_config: CeusConfig) -> None: frames = _frames([40]) _, left, right = split_views(frames, ceus_config.views) assert (left, right) == (ViewRect(0, 0, 200, 200), ViewRect(200, 0, 400, 200)) side = locate_contrast_side( frames, left, right, ceus_config.prompt_source, ceus_config.keyframes ) assert side.side == "left" assert side.contrast_view == left assert side.target_view == right # 10 帧绿框不变:只有第一帧是提示帧。 assert [p.frame_index for p in side.prompt_frames] == [0] x1, y1, _, _ = side.prompt_frames[0].bbox_xyxy assert abs(x1 - 40) <= 3 and abs(y1 - 60) <= 3 def test_green_box_in_right_half(ceus_config: CeusConfig) -> None: frames = _frames([250]) _, left, right = split_views(frames, ceus_config.views) side = locate_contrast_side( frames, left, right, ceus_config.prompt_source, ceus_config.keyframes ) assert side.side == "right" assert side.contrast_view == right assert side.target_view == left # 整帧 x=250 在右半局部是 250 − 200 = 50。 prompt = side.prompt_frames[0] assert abs(prompt.bbox_xyxy[0] - 50) <= 3 # 挪到左半(灰阶那一半),两半扇形对齐、配准平移为 (0, 0):局部 x 仍是 50。 mapped = map_source_mask_to_target(prompt.mask, (0, 0), side.target_view) xs = np.where(mapped)[1] assert abs(int(xs.min()) - 50) <= 3 assert abs(int(xs.max()) - 169) <= 3 def test_frames_are_iterated_once(ceus_config: CeusConfig) -> None: """帧只遍历一遍:传逐帧交出的生成器,结果与传列表相同。""" frames = _frames([250]) _, left, right = split_views(frames, ceus_config.views) from_list = locate_contrast_side( frames, left, right, ceus_config.prompt_source, ceus_config.keyframes ) from_generator = locate_contrast_side( (frame for frame in frames), left, right, ceus_config.prompt_source, ceus_config.keyframes ) assert from_generator.side == from_list.side == "right" assert [p.frame_index for p in from_generator.prompt_frames] == [0] assert np.array_equal(from_generator.prompt_frames[0].mask, from_list.prompt_frames[0].mask) def test_green_box_in_both_halves_fails_case(ceus_config: CeusConfig) -> None: frames = _frames([40, 250]) _, left, right = split_views(frames, ceus_config.views) with pytest.raises(ContrastSideError, match="两半都找到绿框") as raised: locate_contrast_side(frames, left, right, ceus_config.prompt_source, ceus_config.keyframes) assert isinstance(raised.value, CeusCaseError) assert raised.value.step == 2 def test_no_green_box_fails_case(ceus_config: CeusConfig) -> None: frames = _frames([]) _, left, right = split_views(frames, ceus_config.views) with pytest.raises(ContrastSideError, match="两半都没有找到绿框") as raised: locate_contrast_side(frames, left, right, ceus_config.prompt_source, ceus_config.keyframes) assert raised.value.step == 2