# 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-`,工作区有未提交改动时末尾加 `-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)。