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>
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
"""造影画面在哪一半:两半各自选提示帧,恰好一半有才定(任务书 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
|
||||
# 挪到左半(灰阶那一半):整帧 x = 250 − 200 = 50,局部也是 50。
|
||||
mapped = map_source_mask_to_target(prompt.mask, side.contrast_view, side.target_view)
|
||||
xs = np.where(mapped)[1]
|
||||
assert abs(int(xs.min()) - 50) <= 3
|
||||
assert abs(int(xs.max()) - 169) <= 3
|
||||
|
||||
|
||||
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
|
||||
Reference in New Issue
Block a user