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>
This commit is contained in:
@@ -172,7 +172,7 @@ library/<构建号>/
|
||||
| MR 报告「所见」段 | 看不见 | 看不见 | 看得见 |
|
||||
| 病理诊断、报告「印象 / 结论」段、医生标注工作簿、真实病例号、Excel 行序、姓名 | 看不见 | 看不见 | 看不见 |
|
||||
|
||||
性别、年龄只给临床一路:医院在两份报告工作簿里也复制了这两列,但影像两路先看到它们会被人口学先验带偏(旧项目 design 0026 决定三)。
|
||||
性别、年龄在资料库里只放临床一路:医院在两份报告工作簿里也复制了这两列,但影像两路的第一轮判断必须只凭影像证据,先看到它们会被人口学先验带偏(旧项目 design 0026 决定三)。这张表管的是资料库里每一路放什么。会诊时,影像两路在第一轮判断之后,需要时可以向临床专家提问,临床专家的回答可以含性别、年龄;这属于会诊流程,细节由会诊任务书定(负责人 2026-10-16)。
|
||||
|
||||
病例用化名,目录和清单按化名排序:交付文件名里带病例号(如 `U001_roi2.mp4`),行序可能和诊断有关;化名是按种子随机抽的 4 位小写字母加数字,按它排序后的顺序与交付顺序无关。图片写入时去掉文件元数据。构建完不再修改;要改就做一次新构建。
|
||||
|
||||
@@ -263,9 +263,10 @@ X 是超声报告工作簿的「推注开始时间s」,含义是视频第 0
|
||||
|
||||
文件格式:数组用 npz(读取时 `allow_pickle=False`,pickle 读取时会执行任意代码),每例的标量和摘要用 json,跨病例的表用 csv,图用 png。缺失值写 null,不写 NaN 或 0:0 是合法的测量值,NaN 在 json 里不合法。
|
||||
|
||||
导入边界用 import-linter 检查,写进 `make check`。契约在 `pyproject.toml`,检查范围是 `src/cadx`,**不含 `scripts/` 和 `tests/`**。下面只列契约实际强制的(共 7 条),没有的保护不写:
|
||||
导入边界用 import-linter 检查,写进 `make check`。契约在 `pyproject.toml`,检查范围是 `src/cadx`,**不含 `scripts/` 和 `tests/`**。下面只列契约实际强制的(共 8 条),没有的保护不写:
|
||||
|
||||
- `cadx` 不引用 `torch`、`sam2`。
|
||||
- `cadx` 里只有 segment 引用 `torch`、`sam2`(契约逐个列出其余已落地的模块,新模块落地时加进去)。
|
||||
- segment 不引用其他 `cadx` 模块。
|
||||
- delivery、library、answers、runs 互不引用。
|
||||
- delivery、library、answers、runs 不引用其他 `cadx` 模块(build 和规划中的各模块)。
|
||||
- ceus 不引用其他 `cadx` 模块(含 segment:分割结果由 `scripts/ceus_quant.py` 传进来)。
|
||||
@@ -273,7 +274,7 @@ X 是超声报告工作簿的「推注开始时间s」,含义是视频第 0
|
||||
- 只有 delivery 引用 `openpyxl`(交付包的 Excel 只有它碰)。
|
||||
- 只有 library 引用 `PIL`(写资料库的图片时去掉元数据)。
|
||||
|
||||
第三部分依赖表的其余部分和下面几条外部库独占,随模块落地逐条写成契约(import-linter 只能检查已经存在的模块):只有 segment 引用 `torch`、`sam2`(落地时把上面第一条改成放行 segment);只有 llm 引用 `polygateway`;只有 ledger 引用 `sqlite3`。
|
||||
第三部分依赖表的其余部分和下面几条外部库独占,随模块落地逐条写成契约(import-linter 只能检查已经存在的模块):只有 llm 引用 `polygateway`;只有 ledger 引用 `sqlite3`。
|
||||
|
||||
契约管不到的,不要当成机器守住了:
|
||||
|
||||
@@ -292,7 +293,7 @@ Python 环境用 uv;安装与检查命令见 `../guides/开发环境.md`。
|
||||
| 模块 | 预期行数 | 实际行数 | 旧项目对应部分的行数 |
|
||||
| --- | --- | --- | --- |
|
||||
| ceus | 6000–7500(基本照搬旧算法,去掉只为产品层服务的部分) | — | `ceus_roi/` 除 `service.py` 7330,`video_decoder.py` 135 |
|
||||
| segment | 150–300 | — | `medsam2_local_track.py` 240 |
|
||||
| segment | 150–300 | 220 | `medsam2_local_track.py` 240 |
|
||||
| delivery | 400–700 | 733 | `clinical.py` 266、`modalities.py` 522、`workbook.py` 99、`delivery.py` 134,共 1021,其中入库与数据库部分不迁 |
|
||||
| library | 150–300 | 300 | `input_packages.py` 104 |
|
||||
| answers | 100–200 | 201 | — |
|
||||
@@ -365,15 +366,26 @@ Python 环境用 uv;安装与检查命令见 `../guides/开发环境.md`。
|
||||
- `make_build_id(now, git)`:构建号 `YYYYMMDD-HHMMSS-<git 短哈希>`,工作区不干净时末尾加 `-dirty`。
|
||||
- `write_snapshot(snapshot_dir, build_id, git, started_at, finished_at, uv_lock, configs)`:新建快照目录,复制配置原文并写 `snapshot.json`(构建号、git 状态、Python 版本、`uv.lock` 哈希、起止时间、各配置的哈希)。
|
||||
|
||||
### ceus(`src/cadx/ceus/`,目前只有第 1、2 步和坐标换算)
|
||||
### ceus(`src/cadx/ceus/`)
|
||||
|
||||
对外两段接口,中间由 `scripts/ceus_quant.py` 调 segment 做第 3 步:
|
||||
|
||||
- `prepare_segmentation(video_path, cfg)`:第 1–2 步,交出 `SegmentationInput`;找不到可信分屏线抛 `SplitDetectionError`(第 1 步),两半都有或都没有绿框抛 `ContrastSideError`(第 2 步)。
|
||||
- `SegmentationInput`:视频的 fps、帧数、宽高、逐帧时间戳,分屏线,`contrast`(`ContrastSide`:哪一半是造影画面、造影视图和分割那一半视图 `target_view`、提示帧),`target_frames`(分割那一半的逐帧 BGR 图像),`prompt_masks`(提示帧帧号 → 绿框挪到 `target_view` 局部坐标的掩膜),造影那一半的第一帧。
|
||||
- `quantify_case(prepared, target_masks, injection_offset_s, cfg)`:第 4–9 步。`target_masks` 是分割在 `target_view` 局部坐标上的 `(帧数, 高, 宽)` 布尔数组,`injection_offset_s` 是推注时间 X;分割一帧有效轮廓都没有抛 `NoValidContourError`(第 4 步)。拟合没过质量门不抛错。
|
||||
- `QuantResult`:`trajectory`(`Trajectory`)、`tic`(`TicResult`)、`curves`(`TicCurves`)、推注时间、病灶两条曲线拟合是否都过质量门、`anchors`(7 个槽位的 `AnchorFrameLocation`)、`perfusion`(`roi_all`、`roi_perf` 各自的 `LognormalPerfusionMetrics`,拟合没过为 None)、紫圈检出率。
|
||||
- `AnchorFrameLocation`:一个关键帧槽位的帧号、视频时间、推注后时刻、状态(`ok` 或未定位的类别)、未定位的原因、锚点时刻。
|
||||
- 写产物用:`pack_trajectory(trajectory)`(trajectory.npz 字节)、`pack_tic_curves(curves)`(tic_curves.npz 字节)、`render_tic_png(tic, markers=tic_plot_markers(anchors))`(tic.png,标出 D1–D4)、`render_case_overlay(prepared, result)`(overlay.mp4 字节)、`keyframe_images(prepared, anchors)`(定位到的关键帧整幅原图,槽位 → 图像)、`quant_summary(result)`(拟合、灌注指标、紫圈检出率、7 个关键帧定位,可直接写成 JSON);读回用 `unpack_trajectory`、`unpack_tic_curves`。
|
||||
- `CeusConfig` 及各段的配置类:`config/ceus.yaml` 里 `ceus` 段的字段定义(pydantic,字段必填、不许多写、读入后不可改);`paths` 段由 `scripts/ceus_quant.py` 定义。
|
||||
- `CeusCaseError`:只落在这一例上的失败,带失败的步号和原因;`SplitDetectionError`、`ContrastSideError`、`NoValidContourError` 是它的子类。脚本据此把这一例记进 `failed.csv`、接着跑下一例。
|
||||
- `VideoFile(path)`:按路径用 cv2 读视频:`probe()` 给 fps、帧数、宽高;`iter_frames()` 每次从第 0 帧逐帧读;`frame_times_s()` 给逐帧显示时间戳。
|
||||
- `split_views(frames, views_cfg)`:第 1 步,在均匀抽的帧上找分屏线,交出分屏线和左右两半的 `ViewRect`;找不到可信的分屏线抛 `SplitDetectionError`。
|
||||
- `locate_contrast_side(frames, left_view, right_view, prompt_cfg, keyframes_cfg)`:第 2 步,两半各自找绿框、选提示帧,恰好一半有就定为造影画面,交出 `ContrastSide`(哪一半、造影视图、另一半视图、提示帧);两半都有或都没有抛 `ContrastSideError`。
|
||||
- `view_offset`、`map_source_mask_to_target`、`target_polygons_to_source`:两半之间的坐标换算,平移量是两个视图左上角之差。
|
||||
- `CeusCaseError`:只落在这一例上的失败,带失败的步号和原因;`SplitDetectionError`、`ContrastSideError` 是它的子类。脚本据此把这一例记进 `failed.csv`、接着跑下一例。
|
||||
- 其余导出(`ViewRect` 与几何函数、`extract_color_prompt`、`select_prompt_frames` 等)是各步的组成部分,供第二阶段的模块和测试用。
|
||||
- 其余导出(几何函数、各步的算法函数与结果类型,如 `split_views`、`locate_contrast_side`、`masks_to_smoothed_polygons`、`extract_dark_region_polygons`、`select_reference_region`、`compute_tic_for_video`、`fit_lognormal_curve`、`locate_anchor_frames`、`compute_lognormal_perfusion_metrics`、`render_overlay`)是两段接口的组成部分,供测试用。
|
||||
|
||||
### segment(`src/cadx/segment/`)
|
||||
|
||||
- `segment_video(frames, prompt_masks, cfg)`:把提示帧的病灶掩码用 MedSAM2 传播到每一帧;`frames` 是分割那一半的逐帧 BGR 图像,`prompt_masks` 是提示帧帧号 → 同尺寸布尔掩码,交出 `(帧数, 高, 宽)` 布尔数组(即 `quantify_case` 的 `target_masks`)。
|
||||
- `SegmentConfig`:`config/ceus.yaml` 里 `segment` 段的字段定义(MedSAM2 代码目录、权重、模型配置名、设备、两个 offload 开关;pydantic,字段必填、不许多写、读入后不可改)。
|
||||
- `medsam2_commit(medsam2_dir)`:读 MedSAM2 代码目录 `.git` 的 HEAD 提交号,给快照和分割缓存键用。
|
||||
|
||||
### build(`src/cadx/build/`)
|
||||
|
||||
|
||||
@@ -32,6 +32,55 @@ uv run pre-commit install # 每次提交前跑 gitleaks、本地材料拦截
|
||||
|---|---|
|
||||
| `data/` | 远程 `wangwenhan@202.38.247.58:CADx-4-CAD/data/` 下的 `交付包_20260908_40例`、`1.2-TEST`,用 rsync 同步 |
|
||||
| `reference/CADx-4-CAD/` | 旧项目:先 rsync 工作区(排除 `data/`),再 `git fetch` 远程 `CADx-4` 仓库的全部分支恢复历史 |
|
||||
| `reference/MedSAM2/` | MedSAM2 代码(连同 `.git`),见下一节 |
|
||||
| `models/` | MedSAM2 权重 `MedSAM2_latest.pt`,见下一节 |
|
||||
|
||||
远程服务器走 `202.38.247.58`,不走 Tailscale 地址 `100.83.164.94`:同一台机器,后者慢几百倍。
|
||||
| `models/` | MedSAM2 权重,CEUS 迁移的任务书落地后补充来源 |
|
||||
|
||||
## 五、MedSAM2(CEUS 分割用)
|
||||
|
||||
MedSAM2 是医学图像分割模型(GitHub `bowang-lab/MedSAM2`),CEUS 定量的第 3 步用它把几帧的病灶提示传播到整段视频。它以可编辑方式装进项目环境,代码放在 `reference/MedSAM2/`,`src/cadx/segment` 在同一进程里 `import sam2` 调用。位置写在 `config/ceus.yaml` 的 `segment` 段。
|
||||
|
||||
**1. 拷代码和权重**(两种情况都要做;代码和权重都不进 git):
|
||||
|
||||
```bash
|
||||
# 代码:连同 .git 一起拷(提交号从这个 .git 读),不要 checkpoints/ 和 data/,约 25MB
|
||||
rsync -a --exclude checkpoints/ --exclude data/ \
|
||||
wangwenhan@202.38.247.58:/home/wangwenhan/MedSAM2/ reference/MedSAM2/
|
||||
# 权重:只要这一个文件,约 156MB
|
||||
mkdir -p models
|
||||
rsync -a wangwenhan@202.38.247.58:/home/wangwenhan/MedSAM2/checkpoints/MedSAM2_latest.pt models/
|
||||
```
|
||||
|
||||
远程的代码是 `bowang-lab/MedSAM2` 的 332f30d 提交,只改了 `download.sh`,不影响代码。
|
||||
|
||||
**2. 装进环境**,分两种情况:
|
||||
|
||||
- **仓库里已有 `uv.lock` 且已含 `medsam2`**(`pyproject.toml` 的依赖里有 `medsam2`,平常就是这种):拷完代码后 `make sync`(即 `uv sync`)就会按锁文件装上 torch 和 MedSAM2。
|
||||
- **第一次把 MedSAM2 加进项目**(`pyproject.toml` 里还没有 `medsam2`):
|
||||
|
||||
```bash
|
||||
SAM2_BUILD_CUDA=0 uv add --editable reference/MedSAM2
|
||||
```
|
||||
|
||||
`SAM2_BUILD_CUDA=0` 表示不编译 MedSAM2 的 CUDA 扩展(旧项目也没用它)。命令会改 `pyproject.toml` 和 `uv.lock`。
|
||||
|
||||
`uv sync` 时同样要设 `SAM2_BUILD_CUDA=0`(`SAM2_BUILD_CUDA=0 uv sync`),否则重新构建 `medsam2` 时会尝试编译 CUDA 扩展。
|
||||
|
||||
**代理太慢时**:torch 的轮子有几个 GB,走代理可能很慢。可以临时绕开代理直连国内镜像:
|
||||
|
||||
```bash
|
||||
env -u http_proxy -u https_proxy -u HTTP_PROXY -u HTTPS_PROXY \
|
||||
UV_DEFAULT_INDEX=https://mirrors.aliyun.com/pypi/simple/ \
|
||||
SAM2_BUILD_CUDA=0 uv sync # 第一次加进项目时把 uv sync 换成 uv add --editable reference/MedSAM2
|
||||
```
|
||||
|
||||
镜像地址只在这一条命令里临时用,不写进 `pyproject.toml`;`uv add` 会把镜像地址写进 `uv.lock` 里各个包的来源,装完要把这些地址换回 `https://pypi.org/simple`(以及对应的 `files.pythonhosted.org` 下载地址),再跑一次 `uv lock --check` 确认锁文件和 `pyproject.toml` 一致。
|
||||
|
||||
**3. 确认 torch 看得到显卡**:
|
||||
|
||||
```bash
|
||||
uv run python -c "import torch; print(torch.__version__, torch.cuda.is_available(), torch.cuda.get_device_name(0))"
|
||||
```
|
||||
|
||||
应打印 torch 版本、`True` 和显卡名。最后跑 `uv run pytest -m gpu -v`:在一段合成小视频上真跑一次 MedSAM2。运行时会出现几条警告(注意力内核回退;`sam2/_C.so` 里的连通域后处理报 `inputs must be a CUDA tensor` 后被跳过),MedSAM2 自己说明这不影响主要结果,测试仍应 `passed`。
|
||||
|
||||
Reference in New Issue
Block a user