"""两半画面之间的坐标换算:造影那一半局部坐标 + 配准平移 (dx, dy) = 另一半局部坐标。 前两条照搬旧 test_ceus_mapping(去掉锚点配置参数,平移取 (0, 0),断言不变);其余是新写的手算 测试,平移量直接给定(怎么估平移见 test_ceus_registration)。旧文件里依赖锚点、有效区或缩放的 用例不再成立,已删(见任务书 02 执行报告)。 """ import cv2 import numpy as np import pytest from cadx.ceus import ( ViewRect, map_source_mask_to_target, polygon_to_mask, target_polygons_to_source, ) def _largest_external_contour(mask: np.ndarray) -> np.ndarray: """从二值 mask 取面积最大的外轮廓,返回 ``(N, 2)`` 的 ``float32`` 顶点序列。 参数: mask: ``dtype=bool`` 的二值 mask。 返回: 最大外轮廓顶点 ``(x, y)``(``float32``);用于真往返中把 target mask 还原成多边形喂逆向。 """ contours, _ = cv2.findContours( mask.astype(np.uint8), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE ) assert contours, "target mask 应非空,才能提取外轮廓做逆向往返。" largest = max(contours, key=cv2.contourArea) return largest.reshape(-1, 2).astype(np.float32) def _bbox(points: np.ndarray) -> tuple[float, float, float, float]: """返回顶点序列的包围盒 ``(x_min, x_max, y_min, y_max)``(往返形状比对用)。""" return ( float(points[:, 0].min()), float(points[:, 0].max()), float(points[:, 1].min()), float(points[:, 1].max()), ) def test_map_source_mask_to_target_translates_into_target_effective() -> None: """source 视图局部 mask 经映射落入 target 有效区局部坐标,形状为 target_eff 尺寸。""" # source view = 整个左半区 [0,100)×[0,100);有效区同 view(无黑边),平移 (0, 0)。 target_eff = ViewRect(100, 0, 200, 100) source_mask = np.zeros((100, 100), dtype=bool) source_mask[30:50, 20:40] = True # source 局部一个方块 target_mask = map_source_mask_to_target(source_mask, (0, 0), target_eff) assert target_mask.dtype == bool assert target_mask.shape == (target_eff.height, target_eff.width) # 同尺寸有效区 + 无偏移:target 局部方块应与 source 局部方块同位置。 ys, xs = np.where(target_mask) assert int(xs.min()) == 20 and int(xs.max()) == 39 assert int(ys.min()) == 30 and int(ys.max()) == 49 def test_real_roundtrip_same_size_no_offset_restores_polygon() -> None: """真往返(同尺寸有效区、无 offset):多边形→栅格化→正向→提轮廓→逆向,近似还原。 不再假造 target 多边形:从正向 ``map_source_mask_to_target`` 的真实输出 mask 里 用 ``cv2.findContours`` 提最大外轮廓喂逆向,端到端串起正向(mask)→逆向(polygon)。 栅格化 + 轮廓提取带量化误差,容差放宽到 ~2px,但 assert 包围盒近似还原。 """ source_view = ViewRect(0, 0, 120, 100) target_eff = ViewRect(120, 0, 240, 100) # 已知 source 视图局部多边形(远离边界,避免 clip 干扰往返)。 source_polygon = np.array( [[20.0, 30.0], [60.0, 25.0], [70.0, 70.0], [25.0, 65.0]], dtype=np.float32 ) # 正向:source 局部多边形 → 栅格化 mask → 映射到 target 有效区局部 mask。 source_mask = polygon_to_mask(source_polygon, source_view.height, source_view.width) target_mask = map_source_mask_to_target(source_mask, (0, 0), target_eff) assert target_mask.sum() > 0 # 从真实 target mask 提最大外轮廓得 target 多边形,再逆向映回 source 视图局部。 target_polygon = _largest_external_contour(target_mask) [back_to_source] = target_polygons_to_source([target_polygon], (0, 0), source_view) # 形状/包围盒近似还原(栅格化+轮廓量化误差 ≤2px)。 np.testing.assert_allclose(_bbox(back_to_source), _bbox(source_polygon), atol=2.0) @pytest.mark.parametrize( ("offset", "expected_x", "expected_y"), [ # 局部方块 x 300..339、y 50..79。平移 (−37, 4):x 263..302、y 54..83。 ((-37, 4), (263, 302), (54, 83)), # 平移 (25, −6):x 325..364、y 44..73。 ((25, -6), (325, 364), (44, 73)), # 平移 (0, 0):原位不动。 ((0, 0), (300, 339), (50, 79)), ], ) def test_mask_moves_by_offset( offset: tuple[int, int], expected_x: tuple[int, int], expected_y: tuple[int, int] ) -> None: target = ViewRect(640, 0, 1280, 200) source_mask = np.zeros((200, 640), dtype=bool) source_mask[50:80, 300:340] = True target_mask = map_source_mask_to_target(source_mask, offset, target) assert target_mask.shape == (200, 640) ys, xs = np.where(target_mask) assert (int(xs.min()), int(xs.max())) == expected_x assert (int(ys.min()), int(ys.max())) == expected_y def test_mask_crops_what_leaves_target_view() -> None: """平移 (−310, 0):局部 x 300..339 落到 −10..29,出界的 10 列丢掉,不缩放。""" target = ViewRect(0, 0, 640, 200) source_mask = np.zeros((200, 640), dtype=bool) source_mask[10:20, 300:340] = True target_mask = map_source_mask_to_target(source_mask, (-310, 0), target) ys, xs = np.where(target_mask) assert (int(xs.min()), int(xs.max())) == (0, 29) assert (int(ys.min()), int(ys.max())) == (10, 19) def test_polygons_move_back_by_negative_offset() -> None: """平移 (25, 4):目标局部 (40, 30) 挪回 (15, 26);超出造影视图的点裁到边界。""" source = ViewRect(0, 0, 928, 200) polygon = np.array([[40.0, 30.0], [60.0, 30.0], [10.0, 250.0]], dtype=np.float32) [back] = target_polygons_to_source([polygon], (25, 4), source) np.testing.assert_allclose(back, [[15.0, 26.0], [35.0, 26.0], [0.0, 199.0]]) def test_mask_crops_when_target_half_is_narrower() -> None: """宽度为奇数时右半比左半窄 1 像素:平移 (0, 0),不缩放,超出右半的那一列裁掉。""" right = ViewRect(101, 0, 201, 50) source_mask = np.zeros((50, 101), dtype=bool) source_mask[:, 99:101] = True target_mask = map_source_mask_to_target(source_mask, (0, 0), right) assert target_mask.shape == (50, 100) assert np.where(target_mask)[1].tolist() == [99] * 50 def test_real_roundtrip_with_offset_restores_polygon() -> None: """平移 (−37, 4) 下真往返:多边形 → 栅格化 → 正向 → 提轮廓 → 逆向,近似还原(误差 ≤2px)。""" source_view = ViewRect(0, 0, 200, 150) target_view = ViewRect(200, 0, 400, 150) source_polygon = np.array( [[60.0, 30.0], [120.0, 25.0], [130.0, 90.0], [70.0, 85.0]], dtype=np.float32 ) source_mask = polygon_to_mask(source_polygon, source_view.height, source_view.width) target_mask = map_source_mask_to_target(source_mask, (-37, 4), target_view) target_polygon = _largest_external_contour(target_mask) np.testing.assert_allclose( _bbox(target_polygon), np.array(_bbox(source_polygon)) + np.array([-37, -37, 4, 4]), atol=2.0, ) [back] = target_polygons_to_source([target_polygon], (-37, 4), source_view) np.testing.assert_allclose(_bbox(back), _bbox(source_polygon), atol=2.0)