feat(build): 资料库装入全量 CEUS 结果;补 CEUS 定量使用说明

- config/build.yaml 的 ceus_run 指向全量运行 runs/ceus/20261016-090325-1de88a5-dirty;
  test_committed_config_is_valid 随之改为断言指向 runs/ceus/ 下跑完的运行目录。
- config/ceus.yaml 配准门槛注释注明全量运行复算的分布相同。
- 新增 research-wiki/guides/CEUS定量.md(已过一轮硕士生阅读);装配两个库.md 补上 CEUS 运行与
  交付包的三条核对、finished.json;结构.md 行数表按当前代码更新。
- WORK_LOG 记任务书 02 完成情况。

来源:任务书 02 第四阶段。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-16 09:49:53 -04:00
parent 1de88a5d0a
commit 2753ea7721
7 changed files with 230 additions and 13 deletions
+4 -2
View File
@@ -4,7 +4,7 @@
## 当前目标
第 3 步 CEUS 定量,按任务书 02 分四个阶段做;第一阶段(底层和分割之前的算法)与 C14 修复已提交,下一个是第二阶段(MedSAM2 与后半段算法)。
第 3 步 CEUS 定量(任务书 02)四个阶段已完成,待负责人验收;下一步见文末。
## 已完成
@@ -19,6 +19,8 @@
- 2026-10-16 任务书 02 第一阶段:`src/cadx/ceus/` 迁入配置(去掉全部默认值)、视频解码(按路径逐帧读)、分屏、绿框、提示帧、两半坐标换算;新写判断造影画面在哪一半(两半各选提示帧,恰好一半有才行,否则 `ContrastSideError`);坐标换算改为两个视图左上角之差,不再用锚点。`config/ceus.yaml` 只有 `paths`、`ceus` 两段。同一提交修了任务书 01 的关键图问题:不能保真重编码的关键图整例跳过,记 C14。`make check` 通过(348 条测试、7 条导入契约)。
- 2026-10-16 任务书 02 第二至四阶段:ceus 其余模块与 segment(MedSAM2,提交 150eb5f)、`scripts/ceus_quant.py`(3524b31)、build 装入 CEUS 结果与 C15(c582fe4);两半坐标换算改为每段视频配准,加阴性对照的目视核对(00ebbff,核对运行 `runs/ceus_align/20261016-064728-00ebbff-dirty`:10 张对照全被判「没对齐」,30 例真实病例两人都判对齐);流式解码降内存、快照开跑即写、build 核对运行与交付包(1de88a5)。全量运行 `runs/ceus/20261016-090325-1de88a5-dirty`:34 例成功 29,第 1 步失败 4(3 例无暗缝、1 例配准差 66),第 2 步失败 1(绿框面积低于下限);常驻内存峰值 4.4GB。真实构建 `20261016-094248-1de88a5-dirty`:40 例进库 29,跳过 C10 6、C15 5,泄露扫描命中 0。工作区不干净(负责人别的会话的改动),要写进论文的运行需在干净工作区重跑。上一次被 OOM 杀掉的 `runs/ceus/20261016-070216-e165b32-dirty.partial` 留着未删。
## 未完成
- 2026-10-16 任务书 01 由执行者完成(提交 bceb01e),监督者验收通过:真实 40 例进库 34、跳过 6(C10 跨表不一致),独立脚本核对资料库内容与跳过名单一致,泄露扫描 0 命中。
@@ -39,7 +41,7 @@
## 下一步
- 任务书 02 第二阶段:MedSAM2 拷代码和权重、segment 模块、其余 11 个 ceus 模块迁移。
- 待负责人定:5 例 CEUS 失败要不要给个别视频手工指定分屏线、或调低绿框面积下限(`待定问题/02`);失败病例是否改为「超声不可评价、仍参与会诊」(`待定问题/03`);旧的 `.partial` 运行目录删不删。
- 待负责人定:化名种子在 git 里,拿到种子可反推进库病例的病例号先后(专家拿不到种子);含「造影提示:」的那份超声描述所在病例被 C10 跳过,故不在复核清单里。
- 向临床方要编码映射表(性别、诊断、症状、既往史、化验档位)。
- 把 `待定问题/04`(6 例跨表不一致)、`待定问题/06`(1 份超声描述中段的「造影提示:」)转给医生;06 定之前的构建不做正式会诊。
+1 -1
View File
@@ -9,4 +9,4 @@ alias_seed: 20261016
decode_codes: false
code_maps: null
# CEUS 定量的运行目录(runs/ceus/<运行号>/),或 null(不装 CEUS 结果)。
ceus_run: null
ceus_run: runs/ceus/20261016-090325-1de88a5-dirty
+2 -1
View File
@@ -71,7 +71,8 @@ ceus:
# 两种方法估计各分量之差的最大值的门槛(像素)。依据:2026-10-16 在交付包检查全部通过的 34 例中
# 走完第 1 步的 29 例上,两种方法差值最小 0、25% 分位 1、中位 2、75% 分位 2、90% 分位 5、
# 95% 分位 6.2、最大 13;15 高于观测最大值,远低于唯一明显错配的 66。参数与门槛在同一批视频上
# 调出,没有独立验证集。负责人 2026-10-16 定
# 调出,没有独立验证集;全量运行 runs/ceus/20261016-090325-1de88a5-dirty 的 29 例复算结果相同。
# 负责人 2026-10-16 定
max_disagreement_px: 15
prompt_source: # 出自旧项目:findings §3.2 绿框检测
+6 -6
View File
@@ -299,19 +299,19 @@ Python 环境用 uv;安装与检查命令见 `../guides/开发环境.md`。
| 模块 | 预期行数 | 实际行数 | 旧项目对应部分的行数 |
| --- | --- | --- | --- |
| ceus | 6000–7500(基本照搬旧算法,去掉只为产品层服务的部分) | 7085 | `ceus_roi/` 除 `service.py` 7330,`video_decoder.py` 135 |
| segment | 150–300 | 233 | `medsam2_local_track.py` 240 |
| ceus | 6000–7500(基本照搬旧算法,去掉只为产品层服务的部分) | 7103 | `ceus_roi/` 除 `service.py` 7330,`video_decoder.py` 135 |
| segment | 150–300 | 300 | `medsam2_local_track.py` 240 |
| delivery | 400–700 | 739 | `clinical.py` 266、`modalities.py` 522、`workbook.py` 99、`delivery.py` 134,共 1021,其中入库与数据库部分不迁 |
| library | 150–300 | 300 | `input_packages.py` 104 |
| library | 150–300 | 354 | `input_packages.py` 104 |
| answers | 100–200 | 201 | — |
| verdicts | 100–200 | — | — |
| build | 300–500 | 307 | `input_readers.py` 329 的装配部分 |
| build | 300–500 | 564(含装入 CEUS 结果的 `_ceus.py`) | `input_readers.py` 329 的装配部分 |
| llm | 200–450 | — | ClinDB `llm/` 415 |
| ledger | 200–300 | — | ClinDB `ledger/` 274 |
| runs | 100–250 | 86(只含快照部分) | — |
| runs | 100–250 | 109(只含快照部分) | — |
| consult | 500–1000(会诊口径定后重估) | — | — |
| evaluate | 300–600(评价口径定后重估) | — | 已删的 `core/eval` 622 |
| scripts/ 每个 | ≤ 300 | `build.py` 37、`scan_library.py` 88、`ceus_quant.py` 393(超出,含目视核对表的读取与核对)、`ceus_align_review.py` 250 | `t1_local_debug.py` 762、`tic_lognormal_replay.py` 627 |
| scripts/ 每个 | ≤ 300 | `build.py` 37、`scan_library.py` 88、`ceus_quant.py` 403(超出,含目视核对表的读取与核对、开跑即写快照)、`ceus_align_review.py` 243 | `t1_local_debug.py` 762、`tic_lognormal_replay.py` 627 |
---
+211
View File
@@ -0,0 +1,211 @@
# CEUS 定量
读者:要跑一次 CEUS 定量、或者要看它的产物和失败清单的人。这一步对交付包里检查全部通过的病例(编号、文件、取值、跨表一致等检查,即「装配两个库」里的 C01–C13,见 `装配两个库.md` 第四节;每例恰好一段视频),逐例读超声造影视频,圈出病灶、画出亮度曲线、拟合、挑关键帧、算灌注指标,结果写进 `runs/ceus/<运行号>/`,再由「装配两个库」装进给超声专家看的那部分资料(「两个库」是给 AI 专家看的资料库和只给评价用的标准答案库;资料库按临床、超声、MR 三路分开,每个专家只看自己那一路)。它在整条流水线里的位置见 `../explanation/结构.md` 第二部分,9 个步骤和关键帧的定义见第四部分,各函数见第八部分;为什么这样设计见 `../任务书/02-CEUS定量.md`。
## 先认识几个词
- **CEUS**(超声造影):往静脉推注(快速注射)造影剂后连续录下的超声视频。超声机录像时屏幕左右各一半:一半是**造影画面**(造影剂到了就变亮),另一半是普通**灰阶画面**(常规黑白 B 超,显示组织结构,不随造影剂变亮)。哪一半是造影画面,各例不一定相同,程序按哪一半有绿框来认。两半中间的分界叫**分屏线**。
- **扇形区**:每一半画面里真正有超声图像的扇形区域,四周是黑边、文字和刻度。
- **绿框**:医生在造影画面上画的病灶框,烧录在视频里,是 CEUS 定量的输入(它在最终轮廓里占多大分量见 `../待定问题/01-医生画的绿框能否作为输入.md`)。
- **紫圈**:医生用紫色轮廓圈的一块正常胰腺,作对照用(个别病例圈的不是胰腺或没有圈,见 `../待定问题/08`)。程序算出的曲线里叫 `roi_pan`。
- **ROI**(感兴趣区):程序最终圈出的病灶区域。**暗区**:病灶里装囊液、不进造影剂、一直发黑的部分。
- **配准**:估出左半画面局部坐标到右半画面局部坐标的平移 (dx, dy)(方向固定是左到右,与哪一半是造影画面无关),让两半里同一个解剖位置对上。程序在造影画面上找绿框,挪到灰阶那一半交给分割模型,分割完再把轮廓挪回造影那一半采亮度,挪的就是这个平移。在灰阶那一半分割,是因为造影画面里病灶边界随造影剂进出时明时暗,灰阶画面上边界稳定(沿用旧代码,任务书 02「先认识几个词」)。配准错了不会报错,只会让亮度采在病灶旁边,所以全量运行之前要做一轮**目视核对**(第二节)。
- **提示帧**:绿框位置或形状明显变化的那几帧,第 2 步挑出,交给分割模型告诉它「病灶在这里」。
- **MedSAM2**:医学图像分割模型,给它几帧提示帧上的病灶框,它把病灶轮廓传播到整段视频的每一帧。要用显卡,是最慢的一步(第 3 步)。怎么装见 `开发环境.md`。
- **TIC**(时间-强度曲线):ROI 平均亮度随时间的变化,造影剂进来变亮、洗出变暗。程序用 Lognormal(对数正态)形状的光滑曲线去拟合它,拟合要过一道**质量门**(拟合优度等门槛,见 `ceus.yaml` 的 `tic_analysis.fitting`)才算成功。病灶有两条曲线(整体、去掉暗区),两条都过了质量门才算**病灶拟合成功**,D1–D4 和灌注指标都要它。
- **灌注指标**:从拟合曲线算出的 11 个数(峰值强度、达峰时间、洗入速率等),口径仿照商用造影定量软件 VueBOX(`结构.md` 第四部分)。
- **关键帧与槽位**:按 TIC 挑出的 7 个时刻的画面,每个时刻叫一个槽位:B0(刚开始变亮之前)、D1(开始变亮)、D2(变亮最快)、D3(最亮)、D4(变暗最快)从拟合曲线上取;F1、F2 是推注后第 20、60 秒。详细定义见 `../explanation/结构.md` 第四部分的关键帧表。
- **推注时间 X**:超声表的「推注开始时间s」,含义是视频第 0 秒时已经推注了多少秒(不是「第几秒开始推注」),交付时已检查是非负整数。F1 取视频第 20 − X 秒,F2 取第 60 − X 秒。
- **第 N 步**:一段视频依次经过的 9 步:1 解码、分屏与两半配准,2 找绿框、选提示帧,3 MedSAM2 分割,4 整理轮廓,5 找暗区,6 TIC 采样,7 拟合,8 关键帧,9 灌注指标(`结构.md` 第四部分的表)。
- **运行号**:格式同构建号,`YYYYMMDD-HHMMSS-<git 短哈希>`,工作区有未提交改动时末尾加 `-dirty`(见 `装配两个库.md` 第三节「构建号」)。要写进论文的运行应在干净的工作区上跑。
## 一、整体顺序
1. 改 `config/ceus.yaml`(一般不用改;字段的定义和出处写在文件注释里)。
2. `ceus_align_review.py render` 出配准核对图(不用显卡)。
3. 派两个子 Agent 各自判读,写 `verdicts_a.csv`、`verdicts_b.csv` 到这次出图的运行目录。
4. `ceus_align_review.py score` 计分,得到 `review.csv`。
5. 把 `ceus.yaml` 的 `paths.align_review` 改成这份 `review.csv` 的路径。
6. 在 tmux 里跑 `ceus_quant.py`,得到 `runs/ceus/<运行号>/`。
7. 把 `config/build.yaml` 的 `ceus_run` 指向这个运行目录,重新构建两个库(`装配两个库.md`)。
全部命令在仓库根目录下敲。两个脚本都只接收配置文件路径(`score` 只接收出图的运行目录),不能在命令行单独改某个参数(`结构.md` 第五部分「配置」)。终端只打印计数,不打印视频号,可以贴进汇报。
## 二、配准的目视核对
为什么要做、为什么用子 Agent 而不是人、为什么混阴性对照,见任务书 02「配准的目视核对」。负责人 2026-10-16 同意由子 Agent 判读、负责人不看。
### 1. 出图
```bash
uv run python scripts/ceus_align_review.py render --config config/ceus.yaml
```
只做第 1 步。对第 1 步成功的每例出两张图,都只截两半扇形区的范围:
- `<编号>_checker.png`(棋盘拼图):左半和按配准平移过来的右半像棋盘一样交替拼接。对齐时,血管、器官轮廓跨过格子交界处是连续的;没对齐时在交界处错开。
- `<编号>_edges.png`(边缘叠加图):左半的边缘画红色,右半平移过来的边缘画绿色,重合处红加绿成黄色。对齐时大部分是黄色;没对齐时红线、绿线成对平行错开。医生画的绿框、紫圈只出现在其中一半,它们的边缘不参与比对。
另随机挑几例,把配准故意错开一段距离,出同样的两张图当**阴性对照**(例数、错开距离区间、随机数种子在 `ceus.yaml` 的 `align_check` 段)。全部图按打乱的三位编号命名,编号和「哪一例、是不是对照、用的 (dx, dy)、视频 SHA-256」的对照写在 `key.csv`。成功例数少于对照例数时脚本报错。
第 1 步就失败的病例不出图,记在这次出图目录的 `failed.csv`(列同第五节);它们在全量运行里同样会在第 1 步失败。
### 2. 判读
派两个互不知情的全新子 Agent,各自看完 `images/` 下的全部图,写一份判读表。prompt 要点:
- 只给 `images/` 目录的路径,让它只读这个目录;不给、不提 `key.csv`,**不提有阴性对照,也不提比例**(提了它就会去猜哪几张是对照)。
- 说明两种图怎么看(上一小节的两条),同一编号的两张图合起来判一次。
- 每个编号只判三档:`对齐`、`没对齐`、`看不清`,外加一句依据(例如「血管在棋盘交界处错开」)。
- 只写编号、判定和依据,不抄图上的任何文字:帧上烧录了拼音姓名、检查号等(`AGENTS.md` 硬性约束 3)。
- 写进运行目录的 `verdicts_a.csv`(第一个)或 `verdicts_b.csv`(第二个),用 Python 的 `csv` 模块写(依据里有逗号也不会错列),UTF-8 **不带 BOM**(计分脚本逐字比对表头,BOM 会让表头对不上而报错),表头恰好是 `编号,判定,依据`,编号写成和文件名一样的三位数字(如 `001`),每个编号恰好一行。
- 本机 `grep`、`find` 用 `/usr/bin/` 下的版本;要用浏览器只能无头模式。
### 3. 计分
```bash
uv run python scripts/ceus_align_review.py score --run-dir runs/ceus_align/<运行号>
```
- 判读表的表头、编号集合、判定取值不合规就报错,改判读表再跑。
- 任一张阴性对照被任一人判成「对齐」或「看不清」,这次核对**作废**:不写 `review.csv`,退出码 1。作废后不放宽标准、不换说法让子 Agent 重判同一批图(任务书 02),在汇报里写明,下一步由负责人定。
- 对照全部判出来了,写 `review.csv`:每个真实病例一行,列是 `video_sha256, dx, dy, passed, verdict_a, verdict_b`。两人都判「对齐」才 `passed` 为 true,其余(含有人判「看不清」)为 false。终端打印通过、不通过、有人判看不清、两人不一致各几例。
- `review.csv` 已存在时报错,不覆盖。
### 4. 接进配置
把 `config/ceus.yaml` 里 `paths.align_review` 改成 `runs/ceus_align/<运行号>/review.csv`(文件里原来是一个占位路径,不改的话全量运行一开始就因找不到文件停下)。
### 什么时候要重做核对
全量运行时,`ceus_quant` 按视频的 SHA-256 在 `review.csv` 里查每例,并核对这次算出的 (dx, dy) 和表里记的相同。下面几种情况会让它整次停下,这时要重新出图、判读、计分:
- 报「配准目视核对表里没有这段视频」:交付包换了视频,或指错了 `review.csv`。
- 报「配准结果 … 与目视核对表里的 … 不同」:改了影响第 1 步的参数(`ceus.yaml` 的 `views`、`registration`、`prompt_source`、`pan_source` 段;后两段是绿框、紫圈的颜色范围,第 2 步找绿框用它们,第 1 步配准时也用它们把绿框紫圈像素先修补掉)或配准代码,核对的已不是这次跑的东西。
## 三、全量运行
要跑一阵子,放进 tmux,日志写到文件:
```bash
mkdir -p work/logs
tmux new -s ceus 'uv run python scripts/ceus_quant.py --config config/ceus.yaml 2>&1 | tee work/logs/ceus_quant-$(date +%Y%m%d-%H%M%S).log'
```
- 跑的是交付包里检查全部通过的病例(C01–C13)。C14 查的是医生截的那张超声关键图(不是程序挑的关键帧)能否保真重编码,它在构建时才查,被它跳过的病例这里照跑,构建时不用就是了。
- 每例打印一行 `[第几例/总数] 完成` 或 `失败在第 N 步`。跑完打印:运行目录、病例总数、成功数、按步分的失败数、关键帧按槽位分的定位数、分割缓存命中数、耗时、显存峰值。
- 不花钱(不调模型 API),要本机显卡(`ceus.yaml` 的 `segment.device`)。分割结果有缓存,第 3 步的输入和参数不变时,再跑一次不再调显卡,只重算 CPU 上的第 4–9 步。
- 只落在一例上的问题(第五节的表)记进 `failed.csv`,接着跑下一例;其余错误整次停下,留下 `.partial` 目录(第四节)。
## 四、产物在哪
### 配准核对 `runs/ceus_align/<运行号>/`
```
images/<编号>_checker.png、<编号>_edges.png
key.csv 编号 → 视频号、是否对照、所用 (dx, dy)、视频 SHA-256(不给判读者)
failed.csv 第 1 步就失败的病例
snapshot/ snapshot.json(git 状态、开始时间等)、ceus.yaml 原文、finished.json(结束时间)
verdicts_a.csv、verdicts_b.csv 判读(第二节第 2 步写进来)
review.csv 计分结果(第二节第 3 步写出,作废时没有)
```
### 全量运行 `runs/ceus/<运行号>/`
```
failed.csv
snapshot/
snapshot.json git 状态、uv.lock 和各配置文件的 SHA-256、开始时间(开跑时就写)
ceus.yaml 这次所用配置的原文(开跑时复制,这次运行读的就是这份副本)
align_review.csv 这次所用的配准核对表原文(同上)
segment.json 权重文件 SHA-256、MedSAM2 提交号、设备
finished.json 结束时间、显存峰值、分割缓存命中数(跑完才写;没有它说明没跑完)
<视频号>/ 每个成功的病例一个目录,失败的没有
summary.json 视频 SHA-256、推注时间、配准结果、四条曲线的拟合、灌注指标、紫圈检出率、7 个槽位的定位
trajectory.npz 逐帧轮廓(两半的 ROI、暗区、背景圆)
tic_curves.npz 逐帧的曲线数值
tic.png 曲线图(第七节)
overlay.mp4 带轮廓的回放视频(第六节)
keyframes/<槽位>.png 定位到的关键帧整幅原始画面(左右两半都在),未定位的槽位没有文件
```
`summary.json` 里与人工检查最相关的几处:`registration`(第 2 步定出的哪一半是造影画面,第 1 步两种配准方法即互相关、模板匹配各自的估计和差值);`fits.<曲线>.success`、`failure_reason`(拟合过没过质量门、为什么);`pan_detected_fraction`(多少比例的帧认出了紫圈);`anchor_frames.<槽位>.status`、`reason`(第七节)。四条曲线是 `roi_all`(病灶整体)、`roi_perf`(病灶去掉暗区)、`roi_pan`(紫圈)、`background_circle`(程序在病灶外自动挑的一小块背景圆)。
### 分割缓存 `work/segment_cache/<键>/masks.npz`
第 3 步的逐帧病灶掩码。键是「视频 SHA-256 + 第 1–3 步的参数 + 权重 SHA-256 + MedSAM2 提交号」的哈希,键相同就直接读缓存。`work/` 整个可以删掉,代价是下次重新调显卡。
### `.partial` 是什么
三处都是先写进 `<名字>.partial/`,写完再改名:
- `runs/ceus/<运行号>.partial/`、`runs/ceus_align/<运行号>.partial/`:那次运行中途停下了。不会被自动删,留着查原因;查完删不删先问负责人(运行产物只追加,`AGENTS.md`「两档标准」)。构建拒绝 `.partial` 结尾的 `ceus_run`。
- `work/segment_cache/<键>.partial/`:那一例分割写到一半时停下了。它会让下次跑到这一例时报错,**直接删掉**它再跑(`work/` 可以删掉重算)。
目标目录已存在时报错、不覆盖;运行号带到秒,正常不会重名。
## 五、failed.csv 怎么看
三列:`video_id`(视频号)、`step`(失败在第几步)、`reason`(原因,原文是代码里的报错消息)。UTF-8 不带 BOM,用 Excel 直接打开中文可能乱码,用编辑器或导入时选 UTF-8。只有终端打印的计数可以贴进汇报;`video_id` 是病例标识,不进文档、提交说明和 Linear。
| 第几步 | `reason` 里的关键词 | 意思和常见原因 |
| --- | --- | --- |
| 1 | `cv2 打不开视频`、`非法 fps`、`非法元数据`、`时间戳`、`解码帧数 … 与容器记录的帧数 … 不一致` | 视频解码失败:文件损坏、编码不常见,或容器里记的帧数、时间戳不可信。用播放器打开看能不能放完 |
| 1 | `未找到稳定暗缝`、`落在搜索 band … 外`、`宽度退化`、`左右宽度比` | 分屏线找不到:画面中部没有一条明显暗缝(例如不是左右分屏录的、单画面录像),或找到的线把画面切得一边太窄。搜索范围在 `ceus.yaml` 的 `views` 段 |
| 1 | `找不到扇形区` | 某一半画面没有随时间变化的区域(例如这一半是静止的) |
| 1 | `可用的模板块只有 … 块` | 左半扇形太小或太暗,模板匹配凑不够块 |
| 1 | `两半配准不一致` | 互相关和模板匹配两种方法估出的平移相差超过 `registration.max_disagreement_px`,不知道该信哪个 |
| 1 | `配准目视核对未通过` | 配准算出来了,但核对时两个子 Agent 没有都判「对齐」(含有人判「看不清」) |
| 2 | `左右两半都找到绿框` | 两半各自都选出了提示帧,定不出哪一半是造影画面;可能灰阶那一半也有颜色接近绿框的东西 |
| 2 | `左右两半都没有找到绿框` | 视频里没有绿框,或颜色、面积没达到 `prompt_source` 段的门槛 |
| 4 | `未产出任何有效 ROI 轮廓` | 分割在每一帧都没出像样的掩码 |
几点说明:
- 病灶拟合没过质量门**不算失败**:这一例照常有结果目录,D1–D4 记为未定位,灌注指标写 null,原因写在 `summary.json`;F1、F2 照常按推注时间取。个别槽位未定位也不算失败。
- 不进 `failed.csv`、而是整次停下的:配准核对表对不上(第二节末)、MedSAM2 报错(如显存不够、权重找不到)、重新解码时读不到关键帧、配置写错。报错信息在终端和日志里,留下 `.partial`。
- 失败的病例构建时按 C15 整例跳过(`装配两个库.md` 第四节);要不要改成「超声不可评价、仍参与会诊」等 `../待定问题/03-病例排除标准.md` 定。
## 六、overlay.mp4 怎么看
整段视频的回放,左右两半都在,程序画的线叠在原始画面上(医生烧录的绿框、紫圈也照常可见):
| 颜色 | 画在哪一半 | 是什么 |
| --- | --- | --- |
| 黄色轮廓 | 造影画面 | 病灶 ROI,**亮度就在这里面采**(TIC 来自它) |
| 绿色轮廓 | 灰阶画面 | 分割那一半的 ROI(MedSAM2 在这一半上分割) |
| 橙红色轮廓 | 造影画面 | 暗区(`roi_perf` 采样时去掉的部分) |
| 青色圆 | 造影画面 | 背景圆(`background_circle`) |
看什么:
- **黄色轮廓是否始终落在病灶上**:从开头到造影剂洗出,轮廓贴着病灶边界走,探头移动时跟着病灶动,不跳到旁边的组织或血管上。前后两三秒的轻微漂移可以接受,整段偏在一边就是有问题。
- **黄、绿两个轮廓圈的是不是同一块解剖结构**:两半里病灶的形状、位置应对得上。对不上说明配准错了,即使目视核对通过了也要报告。
- **橙红色暗区是否在病灶内部、而且确实一直发黑**:暗区盖住了明显变亮的部分,`roi_perf` 的曲线就不可信。
- **青色圆是否在病灶外的组织里**,不落在黑边、文字或病灶上。
## 七、tic.png 怎么看
横轴是视频时间(秒),纵轴是灰度强度。
- **曲线**:蓝色 `roi_all`(病灶整体)、橙色 `roi_perf`(病灶去暗区)、紫色 `roi_pan`(紫圈)。细的带点线是逐帧原始数值,粗实线是过了质量门的 Lognormal 拟合。病灶曲线拟合没过就不画粗线;紫圈拟合没过时画成虚线并标 `QC rejected`,便于复核。背景圆的曲线不画。
- **竖虚线**:D1(红)、D2(绿)、D3(紫)、D4(蓝),标签形如 `D1 model`(model 指取自拟合模型),画在从 `roi_perf` 拟合曲线算出的时刻上,病灶拟合没成功时不画;算不出时刻的不画。标签后带 `[状态]` 的,表示时刻算出来了,但这个槽位没定位到帧(状态含义见本节末)。B0、F1、F2 不画在图上,看 `summary.json`。
看什么:
- 粗线是否贴着原始点走,尤其是上升段和峰。贴不住而又过了质量门,记下来报告。
- D1 是否在曲线刚开始抬头处,D2 在上升最陡处,D3 在峰顶,D4 在下降最陡处。
- 没有粗线、也没有竖线:病灶拟合没过质量门,原因看 `summary.json` 的 `fits.roi_all.failure_reason`、`fits.roi_perf.failure_reason`。
- 曲线在开头就已经很高(没有变亮之前的平台),说明录像开始时造影剂已经到了。这时 `summary.json` 的 `early_truncation_flag` 为 true(`roi_all`、`roi_perf`、`roi_pan` 中有一条凑不出足够长的增强前基线,即造影剂到达前那段平稳的亮度),B0 往往未定位。
`summary.json` 里每个槽位的 `status`:`ok`(定位到了);`anchor_undefined`(这个时刻本身算不出来,例如病灶拟合没过质量门、曲线升不过到达门槛即判定「造影剂到了」的亮度门槛(`anchor_frames.arrival`)、开头没有稳定基线时的 B0);`not_covered`(时刻落在视频有效帧之外,例如推注后 20 秒早于视频开头,或峰后录得不够长、D4 落在录像结束之后);`no_frame_in_window`(时刻在视频内,但容差窗口里没有可用帧);`no_ordered_frame`(窗口里有帧,但都被时间更早的槽位占用了,再取会打乱先后顺序)。`reason` 一栏写具体原因。容差窗口在 `ceus.yaml` 的 `tic_analysis.anchor_frames` 段。
## 八、装进资料库
把 `config/build.yaml` 的 `ceus_run` 改成 `runs/ceus/<运行号>`,再按 `装配两个库.md` 第二节构建并跑泄露扫描。构建只认跑完的运行目录(不是 `.partial`,有 `failed.csv` 和 `snapshot/snapshot.json`),并核对它确实跑在这份交付包上:快照 `ceus.yaml` 的 `paths.delivery_dir` 与 `build.yaml` 的是同一路径,每个成功病例 `summary.json` 里的视频 SHA-256 和推注时间与交付包一致,任一条不符整次停下。旧代码跑出、`summary.json` 里没有 `video_sha256` 的运行目录不能用,要重跑;资料库里装哪些字段、哪些图,见 `结构.md` 第四部分和 `装配两个库.md` 第三节。
## 九、注意
- `runs/`、`work/`、`data/` 都不进 git。`key.csv`、`failed.csv`、各病例目录名里有视频号(视频号里带病例号),帧上烧录了姓名拼音和检查号;文档、提交说明、Linear 和汇报里只写计数、运行号和文件路径的结构,不写视频号。
- 把图发给子 Agent 之外的外部模型服务,先问负责人(`AGENTS.md` 硬性约束 3)。
- 要写进论文的数字,追到具体的 `runs/ceus/<运行号>/`(`AGENTS.md` 硬性约束 5)。
+3 -2
View File
@@ -47,7 +47,7 @@
### ceus_run
指向一次**跑完的** CEUS 运行目录。目录不存在、名字以 `.partial` 结尾(那次运行没跑完)、缺 `failed.csv` 或 `snapshot/snapshot.json`,整次构建在建目录之前停下。目录合规时,按视频号找每例的结果,找不到或 CEUS 在这一例上失败的按 C15 跳过(第四节)。
指向一次**跑完的** CEUS 运行目录。目录不存在、名字以 `.partial` 结尾(那次运行没跑完)、缺 `failed.csv` 或 `snapshot/snapshot.json`,或快照里 `ceus.yaml` 的 `paths.delivery_dir` 与本文件的 `delivery_dir` 不是同一路径(那次运行跑的不是这份交付包),整次构建在建目录之前停下。目录合规时,按视频号找每例的结果,找不到或 CEUS 在这一例上失败的按 C15 跳过(第四节)。找到的结果,`summary.json` 里记的视频 SHA-256 和推注时间要与交付包一致;不一致说明视频或推注时间在那次运行之后改过,整次构建停下(这是整次运行与交付包对不上,不是单例问题),要重跑 CEUS 定量。
### 映射表的格式
@@ -131,7 +131,8 @@ review.csv
| 文件 | 内容 |
| --- | --- |
| `manifest.json` | 构建号、跳过清单的说明句(`skipped_note`)、本目录其余全部文件(含 `snapshot/` 下的)的 SHA-256 |
| `snapshot/snapshot.json` | 构建号、完整 git 提交号、工作区是否干净、`git diff HEAD` 的 SHA-256、Python 版本、`uv.lock` 的 SHA-256、开始和结束时间、各配置文件的 SHA-256 |
| `snapshot/snapshot.json` | 构建号、完整 git 提交号、工作区是否干净、`git diff HEAD` 的 SHA-256、Python 版本、`uv.lock` 的 SHA-256、开始时间、各配置文件的 SHA-256(开跑时写) |
| `snapshot/finished.json` | 结束时间(跑完才写) |
| `snapshot/build.yaml` 等 | 这次所用配置的原文副本。配置文件以后改了,这次构建用的是什么仍然查得到 |
| `aliases.csv` | 化名、三位病例号、视频号(`U` 加病例号)。会诊产物只用化名,评价靠这张表接回真实病例;CEUS 定量的结果按视频号接回 |
| `diagnoses.csv` | 化名、病例号、临床表、超声表、MR 表各自的诊断编码、诊断名(不解码时留空)。三张表各存一份,用哪一份算分由评价决定(进库病例已过 C10,三份相同;分开存是为了保留各表原值,评价口径不必在装配时定) |
+3 -1
View File
@@ -169,7 +169,9 @@ def test_committed_config_is_valid() -> None:
config = load_build_config(REPO_ROOT / "config" / "build.yaml", REPO_ROOT)
assert config.decode_codes is False
assert config.code_maps is None
assert config.ceus_run is None
assert config.ceus_run is not None
assert config.ceus_run.parent == REPO_ROOT / "runs" / "ceus"
assert not config.ceus_run.name.endswith(".partial")
def test_config_duplicate_key(tmp_path: Path) -> None: