Files
CADx-4/tests/fake_split_scene.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

93 lines
3.6 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.
"""测试用合成分屏画面:左右两半各有一块带纹理的扇形区,右半内容是左半平移 `(dx, dy)` 后的样子。
模仿真实录像里配准要用到的几样东西:扇形区里的组织纹理随时间轻微起伏(扇形外的界面是静止的),
两半的成像亮度不同(右半扇形内做一次线性变换 `gain × 灰度 + bias`),各帧噪声独立。
- `right_local(x, y) = left_local(x − dx, y − dy)`:右半局部坐标 = 左半局部坐标 + (dx, dy);
- `interior_shift` 不为 None 时,右半扇形的轮廓仍按 `offset` 平移,里面的纹理却按 `interior_shift`
平移,用来造「扇形轮廓和组织纹理给出不同平移」的画面。
"""
from dataclasses import dataclass
import cv2
import numpy as np
import numpy.typing as npt
type Gray = npt.NDArray[np.float32]
OUTSIDE = 20.0 # 扇形外静止界面的灰度
BASE = 100.0 # 扇形内的平均灰度
@dataclass(frozen=True)
class SplitScene:
"""一幅分屏画面的静止部分:两半的灰度内容和扇形掩膜(各自局部坐标)。"""
left: Gray
right: Gray
left_fan: npt.NDArray[np.bool_]
right_fan: npt.NDArray[np.bool_]
def _fan(shape: tuple[int, int], polygon: list[tuple[int, int]], shift: tuple[int, int]) -> Gray:
points = np.asarray(polygon, dtype=np.int32) + np.asarray(shift, dtype=np.int32)[None, :]
mask = np.zeros(shape, dtype=np.uint8)
cv2.fillConvexPoly(mask, points, 1)
return mask.astype(np.float32)
def make_scene(
*,
height: int,
half_width: int,
fan_polygon: list[tuple[int, int]],
offset: tuple[int, int],
interior_shift: tuple[int, int] | None,
texture_std: float,
right_gain: float,
right_bias: float,
seed: int,
) -> SplitScene:
"""左半扇形取 `fan_polygon`(左半局部坐标),右半按 `offset` 平移;纹理是平滑过的随机场。"""
rng = np.random.default_rng(seed)
margin = 64
canvas = cv2.GaussianBlur(
rng.normal(0.0, 1.0, (height + 2 * margin, half_width + 2 * margin)).astype(np.float32),
(0, 0),
4.0,
)
canvas *= texture_std / float(canvas.std())
shape = (height, half_width)
left_texture = canvas[margin : margin + height, margin : margin + half_width]
tdx, tdy = offset if interior_shift is None else interior_shift
right_texture = canvas[
margin - tdy : margin - tdy + height, margin - tdx : margin - tdx + half_width
]
left_fan = _fan(shape, fan_polygon, (0, 0))
right_fan = _fan(shape, fan_polygon, offset)
left = OUTSIDE + left_fan * (BASE - OUTSIDE + left_texture)
# 右半是另一种成像:同一组织,亮度做一次线性变换。
right = OUTSIDE + right_fan * (right_gain * (BASE - OUTSIDE + right_texture) + right_bias)
return SplitScene(
left=left.astype(np.float32),
right=right.astype(np.float32),
left_fan=left_fan > 0,
right_fan=right_fan > 0,
)
def scene_gray(scene: SplitScene, index: int, rng: np.random.Generator) -> Gray:
"""第 `index` 帧的整帧灰度(左右拼接):扇形内加缓慢起伏和独立噪声,扇形外不动。"""
wave = 10.0 * np.sin(2 * np.pi * index / 7.0)
halves: list[Gray] = []
for content, fan in ((scene.left, scene.left_fan), (scene.right, scene.right_fan)):
noise = rng.normal(0.0, 3.0, content.shape).astype(np.float32)
halves.append(content + fan * (wave + noise))
return np.hstack(halves)
def to_bgr(gray: Gray) -> npt.NDArray[np.uint8]:
"""灰度裁到 0–255 转成三通道 uint8。"""
return np.repeat(np.clip(gray, 0, 255).astype(np.uint8)[:, :, None], 3, axis=2)