Files
CADx-4/tests/test_segment.py
T
iomgaa 150eb5f6ae feat(ceus,segment): 任务书 02 第二阶段——分割和后半段算法
- ceus 迁入 polygons、dark_region、reference_region、tic_fitting、tic、anchor_frames、
  perfusion_metrics、trajectory、tic_curves、tic_plot、overlay,旧测试照搬(改 import 与配置读取)。
- 对外两段接口:prepare_segmentation(第 1–2 步)与 quantify_case(第 4–9 步)。
- 第 3 条:删掉 D1 取医生标注时刻的模式,D1 只从拟合曲线算。
- 第 4 条:F1/F2 取视频第 20−X、60−X 秒(X 为视频第 0 秒时已推注的秒数),落在视频外记未定位。
- 绿框融合权重、紫圈颜色阈值照搬;关键帧容差窗口的出处写进 ceus.yaml 注释。
- segment:同一进程调用 MedSAM2,设置写进 ceus.yaml 的 segment 段,全部必填;gpu 测试真跑通过。
- 依赖加 scipy、matplotlib、medsam2(可编辑安装,不编译 CUDA 扩展);uv 只解析 linux x86_64。
- 导入契约:只有 segment 引用 torch、sam2;segment 不引用其他 cadx 模块。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 05:07:02 -04:00

96 lines
3.2 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.
"""segment:分割设置的校验,以及在合成小视频上真跑一次 MedSAM2(`gpu`)。"""
from pathlib import Path
from typing import Any, cast
import cv2
import numpy as np
import numpy.typing as npt
import pytest
import yaml
from pydantic import ValidationError
from cadx.segment import SegmentConfig, medsam2_commit, segment_video
REPO_ROOT = Path(__file__).resolve().parents[1]
CEUS_YAML = REPO_ROOT / "config" / "ceus.yaml"
def _segment_section() -> dict[str, Any]:
document = cast(dict[str, Any], yaml.safe_load(CEUS_YAML.read_text(encoding="utf-8")))
return cast(dict[str, Any], document["segment"])
def _absolute(section: dict[str, Any]) -> dict[str, Any]:
"""路径字段换成仓库根目录下的绝对路径(脚本也这样做)。"""
return {
**section,
"medsam2_dir": str(REPO_ROOT / section["medsam2_dir"]),
"checkpoint": str(REPO_ROOT / section["checkpoint"]),
}
def test_config_yaml_section_validates() -> None:
cfg = SegmentConfig.model_validate(_segment_section())
assert cfg.sam2_config == "configs/sam2.1_hiera_t512.yaml"
@pytest.mark.parametrize(
"field",
[
"medsam2_dir",
"checkpoint",
"sam2_config",
"device",
"offload_video_to_cpu",
"offload_state_to_cpu",
],
)
def test_config_missing_field_raises(field: str) -> None:
section = _segment_section()
del section[field]
with pytest.raises(ValidationError, match=field):
SegmentConfig.model_validate(section)
def test_config_extra_field_raises() -> None:
with pytest.raises(ValidationError, match="object_id"):
SegmentConfig.model_validate({**_segment_section(), "object_id": 1})
def test_config_bool_not_coerced() -> None:
with pytest.raises(ValidationError, match="offload_video_to_cpu"):
SegmentConfig.model_validate({**_segment_section(), "offload_video_to_cpu": "false"})
def _moving_disc_video(
frame_count: int, height: int, width: int, radius: int
) -> tuple[list[npt.NDArray[np.uint8]], list[npt.NDArray[np.bool_]]]:
"""暗背景上一个亮圆从左往右匀速移动;交出逐帧 BGR 图像和每帧圆的真值掩码。"""
frames: list[npt.NDArray[np.uint8]] = []
truths: list[npt.NDArray[np.bool_]] = []
yy, xx = np.mgrid[0:height, 0:width]
for index in range(frame_count):
cx = radius + 10 + index * 2
cy = height // 2
frame = np.full((height, width, 3), 30, dtype=np.uint8)
cv2.circle(frame, (cx, cy), radius, (220, 220, 220), thickness=-1)
frames.append(frame)
truths.append((xx - cx) ** 2 + (yy - cy) ** 2 <= radius**2)
return frames, truths
@pytest.mark.gpu
def test_segment_video_on_synthetic_clip() -> None:
cfg = SegmentConfig.model_validate(_absolute(_segment_section()))
frame_count, height, width = 40, 120, 200
frames, truths = _moving_disc_video(frame_count, height, width, radius=18)
# 两个提示帧,最早的不在第 0 帧,前向和反向传播都要走到。
prompt_masks = {5: truths[5], 25: truths[25]}
masks = segment_video(frames, prompt_masks, cfg)
assert masks.shape == (frame_count, height, width)
assert masks.dtype == np.bool_
assert len(medsam2_commit(cfg.medsam2_dir)) == 40