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