Files
iomgaa 7e7f32a13d feat(review): 核对包改成医生读得懂;资料库超声一路只留给 agent 的参数
- ceus:灌注指标改为对病灶去暗区(roi_perf)和紫圈(roi_pan)两组算,roi_all 指标删除;
  D1–D4 仍要 roi_all、roi_perf 两条都过才定位(负责人 2026-10-17:医生要正常区与病灶对比看)
- build/library:tic.json 只出两组 11 个指标与 7 个槽位的视频时间;医生截图挪进
  ultrasound/keyframes/key.png,UltrasoundRoute 去掉 key_image、keyframes 多一个 key
- review:TIC参数.csv 取代 TIC结果.csv;问题清单按原因对照表写成大白话,对照表外的原因报错;
  程序原文进 snapshot/问题清单_原文.csv;说明.pdf 整份重写为合作者口吻
- 文档:结构.md、医生核对包、装配两个库、CEUS定量

任务书 04。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 10:22:15 -04:00

22 KiB
Raw Permalink Blame History

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)才算成功。病灶有两条曲线(整体、去掉暗区),两条都过了质量门才算病灶拟合成功,B0、D1–D4 要它。
  • 灌注指标:从拟合曲线算出的 11 个数(峰值强度、达峰时间、洗入速率等),口径仿照商用造影定量软件 VueBOX(结构.md 第四部分)。对病灶去暗区(roi_perf)和紫圈(roi_pan)两条曲线各算一组,哪条的拟合过了质量门就算哪条,没过的那组为 null。
  • 关键帧与槽位:按 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. 出图

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. 计分

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,日志写到文件:

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 轮廓 分割在每一帧都没出像样的掩码

几点说明:

  • 病灶拟合没过质量门不算失败:这一例照常有结果目录,B0、D1–D4 记为未定位,拟合没过的那条曲线(roi_perf、roi_pan)的灌注指标写 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)。