00ebbffb28
- 第 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>
93 lines
3.6 KiB
Python
93 lines
3.6 KiB
Python
"""测试用合成分屏画面:左右两半各有一块带纹理的扇形区,右半内容是左半平移 `(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)
|