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>
This commit is contained in:
2026-10-16 06:47:27 -04:00
parent c582fe426f
commit 00ebbffb28
20 changed files with 1500 additions and 277 deletions
+57
View File
@@ -0,0 +1,57 @@
"""测试用合成 CEUS 视频:400×200、10 fps、40 秒的分屏造影视频(test_ceus_pipeline、test_ceus_quant、
test_ceus_align_review 共用)。
两半各有一块位置相同、带纹理、随时间轻微起伏的扇形区(`fake_split_scene`,配准平移 (0, 0))。
造影画面在右半:病灶是整帧 `LESION` 方块,亮度按 Lognormal 团注曲线 `BOLUS` 变化,内部有一块一直
发黑的囊 `CYST`;`green_box` 为真时画医生的绿框(病灶的空心描边)。
"""
from pathlib import Path
import cv2
import numpy as np
from cadx.ceus import LognormalParameters, evaluate_lognormal
from fake_split_scene import make_scene, scene_gray, to_bgr
WIDTH, HEIGHT, SPLIT = 400, 200, 200
FPS, FRAMES = 10.0, 400
LESION = (250, 60, 369, 159) # 整帧坐标 x1, y1, x2, y2(含端点)
CYST = (290, 95, 320, 125)
BOLUS = LognormalParameters(area=1300.0, mu=2.4, sigma=0.45, arrival_offset_s=4.0, baseline=40.0)
def write_video(path: Path, *, green_box: bool) -> None:
rng = np.random.default_rng(3)
intensity = evaluate_lognormal(np.arange(FRAMES) / FPS, BOLUS)
# 两半是同一幅带纹理的扇形,平移 (0, 0):配准应估出 (0, 0),绿框原位挪到左半。
scene = make_scene(
height=HEIGHT,
half_width=SPLIT,
fan_polygon=[(30, 22), (170, 22), (197, 192), (3, 192)],
offset=(0, 0),
interior_shift=None,
texture_std=20.0,
# 造影那一半调暗(扇形内平均约 32,与改动前的 30 相当):mp4 压缩把绿框描边的颜色渗到
# 相邻像素,背景亮时渗出的像素更容易落进绿框的 HSV 范围,绿框会被认大。
right_gain=0.15,
right_bias=0.0,
seed=7,
)
x1, y1, x2, y2 = LESION
# 病灶里保留组织纹理(减去它在病灶方块内的均值),病灶平均亮度仍等于团注曲线。
detail = scene.right[y1 : y2 + 1, x1 - SPLIT : x2 - SPLIT + 1]
detail = detail - detail.mean()
writer = cv2.VideoWriter(str(path), cv2.VideoWriter.fourcc(*"mp4v"), FPS, (WIDTH, HEIGHT))
for index in range(FRAMES):
gray = scene_gray(scene, index, rng)
gray[:, SPLIT] = 0 # 分屏缝
lesion = intensity[index] + detail + rng.normal(0.0, 6.0, detail.shape)
gray[y1 : y2 + 1, x1 : x2 + 1] = lesion
cx1, cy1, cx2, cy2 = CYST
gray[cy1 : cy2 + 1, cx1 : cx2 + 1] = 10
frame = np.ascontiguousarray(to_bgr(gray))
if green_box:
cv2.rectangle(frame, (x1, y1), (x2, y2), (0, 255, 0), thickness=3)
writer.write(frame)
writer.release()