- 任务书 02:先修 C14(关键图问题改为跳过单例);四阶段迁移 ceus、segment、 ceus_quant、build 装入 8 张图中的 7 张关键帧与 tic.json;过一轮「硕士生阅读」 - 负责人 2026-10-16:MedSAM2 从远程拷贝最快;任务书一份;紫圈阈值先照搬并记待定问题; CEUS 整例失败跳过、拟合不过只缺 D1–D4,两点认可 - 待定问题 07(超声图上烧录的性别年龄)、08(紫圈颜色阈值是占位值) - 结构.md:CEUS 运行产物进 runs/ceus/(会进资料库与论文,需可追溯),work/ 只放分割缓存 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
23 KiB
任务书 02:CEUS 定量
把旧项目的 CEUS(超声造影视频)定量算法迁到本仓库,在本机显卡上跑完全部病例。对每段视频,程序要圈出病灶、画出亮度随时间变化的曲线、挑出关键帧、算出灌注指标,最后由 build 把每例的 7 张关键帧和指标装进资料库的超声一路。资料库里原有医生截的关键结构图,加上这 7 张,一共 8 张图。
对应 research-wiki/explanation/结构.md 第七部分第 3 步。前提:任务书 01 已完成(提交 bceb01e)。
一份任务书分四个阶段,每个阶段做完单独提交,四个阶段都完成后一起交报告(负责人 2026-10-16:「任务书一份」)。
来源:
- 负责人 2026-10-16 定:CEUS 代码「直接一个个迁移」,不在本机重跑旧代码作对比基线(design 0002 决定 4)。
- 负责人 2026-10-16 定:「推注开始时间s」是视频第 0 秒时已经推注了多少秒;40 例没有医生标的造影剂到达时间,关键帧全部由程序从曲线上算或按推注时间取(design 0002 决定 5、6)。
- 负责人 2026-10-16 定:MedSAM2 用最快的方式装(从远程服务器拷代码和权重);紫圈的颜色阈值先照搬旧值,记为待定问题(
待定问题/08);先把关键帧和指标放进资料库,「一个患者的资料库中有 8 帧」。 - 其余做法(四处已知问题怎么改、中间文件的布局、复用的键、对外接口)是监督者 2026-10-16 提出的,负责人确认任务书时一并认可。
先认识几个词
- CEUS、造影画面:往静脉推注造影剂后连续录下的超声视频。超声机录像时屏幕左右各一半:一半是造影画面(造影剂到了就变亮),另一半是普通灰阶超声画面。哪一半是造影画面,各例不一定相同。
- 绿框:医生在造影画面上画的病灶框,烧录在视频里。
- 紫圈:医生用紫色轮廓圈的一块正常胰腺(003、010、012 例圈的是肝,033 例没有),用作对照(
待定问题/08)。 - 提示帧:绿框位置或形状明显变化的那几帧。程序把这几帧的绿框交给分割模型,告诉它「病灶在这里」。
- MedSAM2:医学图像分割模型(GitHub
bowang-lab/MedSAM2),给它几帧的提示,它能把病灶轮廓传播到整段视频的每一帧。要用显卡,是整个流程里最慢的一步。 - ROI:最终圈出的病灶区域。暗区:病灶里装囊液、不进造影剂、一直发黑的部分。
- TIC(时间-强度曲线):ROI 平均亮度随时间的变化,先变亮再变暗。用 Lognormal(对数正态)形状的光滑曲线去拟合它,后面的时刻和指标都在拟合出的光滑曲线上算。
- 关键帧:B0(刚开始变亮之前)、D1(开始变亮)、D2(变亮最快)、D3(最亮)、D4(变暗最快)这 5 张从拟合曲线上取;F1、F2 是推注后第 20、60 秒。定义见
结构.md第四部分「CEUS 中间文件」。 - 灌注指标:从拟合曲线算出的 11 个数(峰值强度、达峰时间、洗入速率等),口径仿照商用造影定量软件 VueBOX。
work/:中间文件目录,不进 git,可以删掉重算。- 两套编号:本任务书的「第一阶段」到「第四阶段」是你交付的四个阶段;「第 1 步」到「第 9 步」是一段视频在 CEUS 定量里依次经过的 9 步(
结构.md第四部分有表):1 解码与分屏,2 找绿框、选提示帧,3 MedSAM2 分割,4 整理轮廓(平滑、与绿框融合),5 找暗区,6 TIC 采样,7 拟合,8 关键帧,9 灌注指标。 - 在哪一半上分割:沿用旧代码。绿框画在造影那一半;造影画面里病灶边界随造影剂进出时明时暗,所以旧代码把绿框挪到灰阶那一半,让 MedSAM2 在灰阶画面上分割,再把轮廓挪回造影那一半,在造影画面上采亮度。
- 监督者:和负责人讨论、写任务书并验收的会话;执行者是拿着这份任务书写代码的你。
开工前先读
AGENTS.md:全部,尤其是「不设默认值」「罕见情况怎么办」「错误怎么处理」「测试」。research-wiki/explanation/结构.md:第二部分(框架图)、第三部分(ceus、segment 的职责和「几条边界」)、第四部分(「CEUS 定量的产物」的 9 步和关键帧表)、第五部分(「配置」、文件格式、导入契约)、第八部分(任务书 01 已有的接口)。research-wiki/design/0002-四段流水线与两个库.md:决定 4、5、6。research-wiki/待定问题/01(绿框在最终轮廓里占多少)、02(参数何时冻结)、07(图上的信息栏)、08(紫圈阈值)。这一步都不处理,只要知道它们存在、不要顺手改。research-wiki/任务书/01-装配两个库.md和src/cadx/现有代码。- 旧代码(只读,路径相对
reference/CADx-4-CAD/):app/domain/ceus_roi/除service.py外的 17 个模块,约 7300 行,都不依赖数据库或 HTTP。app/providers/video_decoder.py(视频解码)。- 实际跑出 40 例结果的三个脚本:
debugs/t1_local_debug.py做第 1–6 步,用子进程调debugs/medsam2_local_track.py做分割;debugs/tic_lognormal_replay.py读轨迹,做 TIC、拟合、关键帧和指标。迁移时以这条路径的行为为准,service.py不搬。 config/ceus_roi_default.yaml:108 个参数。tests/unit/下 19 个算法测试文件(约 303 个测试,全用合成数据)。
旧测试怎么接新配置:配置去掉默认值以后,旧测试里靠默认值构造配置的地方,改成由一个夹具读入 config/ceus.yaml,需要不同取值的测试用 model_copy(update=...) 改那一项;断言和预期值不改。
旧脚本里的编排逻辑放哪:两个 debug 脚本里按顺序调用各模块的那部分逻辑,收进 ceus 对外的两段接口(第二阶段「ceus 对外交出什么」);scripts/ceus_quant.py 只管读配置、找病例、读写文件、分割缓存和逐例循环。
病人资料不进任何文字:报告、提交说明、测试、日志里只写结构和计数,不写病例号、视频号、诊断和报告原文。
先修任务书 01 的一处
任务书 01 的执行者报告:关键图格式不对(不是 PNG、16 位、带透明)时,library 抛 ValueError,整次构建停下。这是只落在一个病例上的问题,按 AGENTS.md「错误怎么处理」应该跳过这一例。
要做:
- library 提供
check_key_image(path) -> str | None:能原样重编码时返回 None,不能时返回原因。现在_write_clean_png里的那几条判断移到这里。 - build 在写资料库之前对每例调它,返回原因的,这一例跳过,记为 C14「关键图能原样重编码」,涉及值写原因。
_write_clean_png里的判断保留为断言性质的报错(走到这里说明 build 漏了检查)。- 补测试:一例关键图带透明,这一例被跳过、记 C14、其余照常进库。
结构.md第八部分相应几行改掉。
和第一阶段一起提交,提交说明里写明。
第一阶段:底层和分割之前的算法
迁移以下模块到 src/cadx/ceus/(文件名加 _ 前缀,对外只经 __init__.py):geometry、视频解码(旧 video_decoder.py)、配置(旧 research_config.py)、split_screen、green_prompt、prompt_frames、mapping。
怎么迁
- 逐个模块照旧代码搬,去掉产品层的东西(数据库、settings、日志框架、
app.*的引用),注释和 docstring 改成中文、只写现在做什么和为什么。 - 对应的旧测试一起搬,改 import,断言和预期值不改。旧测试要是因为下面「四处要改的地方」而不再成立,删掉或改写那一条,在报告里逐条列出。
- 视频解码:旧代码先把整段视频读成字节、写临时文件,再整段读进内存。改成直接按路径用 cv2 打开、逐帧读。行为(读出的帧和时间戳)不变。
test_video_decoder原来要一段真实样例视频,改成在测试里用 cv2 现造一段几十帧的小视频。
配置
- 旧
research_config.py的 22 个 pydantic 配置类搬过来,去掉全部默认值(旧 schema 里约 134 处),每个字段必填;strict=True、extra="forbid"、frozen=True,规则同结构.md第五部分「配置」。 - 新建
config/ceus.yaml:数值逐个照抄旧ceus_roi_default.yaml。每个参数的注释写明出处:旧 yaml 注释里引的出处(临床口径契约、旧 findings、旧 design)照抄并注明「出自旧项目」;没有出处的写「旧项目工程侧定的,无出处」。不改任何数值(待定问题/02)。 ceus.yaml分三段:paths(交付包位置、runs/和work/位置)、segment(分割设置,第二阶段加)、ceus(算法参数)。字段定义各归其主:ceus段归 ceus,segment段归 segment,最外层和paths由scripts/ceus_quant.py定义,它读整份文件、把各段交给各自的模型校验。第一阶段先只有paths和ceus两段。- 下面「四处要改的地方」用不到的旧字段(映射锚点、
source_view、只给service.py用的裁黑边设置、D1 取医生时刻的那个模式开关)直接删掉,不留着不用。
四处要改的地方
这四处在旧代码里是错的,或者在 40 例上会出问题,按下面改,各配手算预期值的测试。
- 左右两半之间的坐标换算。 程序要把造影画面上的绿框挪到另一半画面的同一位置。两半大小相同、只差一个水平平移,旧代码却把换算用的锚点按某一种视频宽度(1856 像素)写死成像素坐标(旧 yaml 第 45–49 行、旧
mapping.py:91),40 例有 5 种宽度,其余宽度会挪偏一两百像素。改成:平移量 = 目标那一半视图的左上角 − 造影那一半视图的左上角,直接用第 1 步切分出来的两个视图矩形算。测试:至少 3 种宽度,手算平移量。 - 造影画面在哪一半。 旧代码配置写死造影在左半(旧 yaml 第 15–16 行),013 例造影在右半,找不到绿框。改成:两半各自按旧代码的办法选提示帧(
collect_prompt_observations+select_prompt_frames,参数不变),选出至少一帧就算「这一半有绿框」;恰好一半有,这一半就是造影画面;两半都找到或都找不到,这一例失败,记原因。测试:合成视频,绿框分别在左、在右、两边都有、两边都没有。 - D1 不用医生的时刻。 旧关键帧代码有一个模式让 D1 直接取医生标注的造影剂到达时间(旧
anchor_frames.py:203–206)。这条路删掉,只保留从拟合曲线算 D1(design 0002 决定 6)。 - 推注时间的换算。 X 是超声报告表的「推注开始时间s」,含义是视频第 0 秒时已经推注了 X 秒。F1 取视频第 20 − X 秒,F2 取第 60 − X 秒,取帧的容差窗口照旧
anchor_frames段对 F1、F2 的设置;落在视频之外(小于 0 或超过视频末尾),这一帧记为未定位,写明原因,不拿别的帧补(结构.md第四部分)。旧代码按「推注发生在视频第 X 秒」处理(旧anchor_frames.py:204的符号),要改。测试手算:X = 7 时 F1 在第 13 秒、F2 在第 53 秒;X = 25 时 F1 在第 −5 秒,未定位。
另有两处旧 debug 路径和 service.py 不一样,按 debug 路径,因为 40 例的结果是它跑出来的:不裁画面黑边;找分屏线只用均匀抽的 25 帧。
收尾点
上面的模块和测试都搬完,make check 通过,提交。
第二阶段:分割和后半段算法
MedSAM2
- 从远程服务器拷代码和权重(ssh/rsync 一律走
wangwenhan@202.38.247.58,见AGENTS.md「本机环境的坑」):- 代码:远程
/home/wangwenhan/MedSAM2/,排除checkpoints/和data/(约 25MB),连同.git一起拷到本机reference/MedSAM2/,提交号从这个.git读出,不写死。它是 GitHubbowang-lab/MedSAM2的 332f30d 提交,远程只改了download.sh,不影响代码。 - 权重:只拷
checkpoints/MedSAM2_latest.pt(约 156MB)到本机models/MedSAM2_latest.pt。
- 代码:远程
- 用
SAM2_BUILD_CUDA=0 uv add --editable reference/MedSAM2装进项目环境(不编译 CUDA 扩展;旧项目也没用它)。装完确认 torch 能看到本机显卡。guides/开发环境.md写明这几步,下一个人在新机器上照着就能装好。 - segment 模块(
src/cadx/segment/):从旧medsam2_local_track.py改写,在同一进程里调用,不再开子进程和 conda 环境。对外一个函数:输入逐帧图像(只是要分割的那一半画面)、若干提示帧的病灶掩码、分割设置,输出每一帧的病灶掩码。它不认识分屏和绿框(结构.md第三部分)。MedSAM2 要求帧以 jpg 文件放在一个目录里,临时目录由 segment 自己建、自己删。 - 分割设置(权重路径、模型配置路径、设备、旧脚本里的两个 offload 开关)写进
ceus.yaml的segment段,字段定义在 segment 里,全部必填。权重路径不写死在代码里。 - 导入契约:
cadx里只有 segment 能引用torch、sam2(改pyproject.toml里现有那条)。
其余模块
迁移 polygons、dark_region、reference_region、tic_fitting、tic、anchor_frames、perfusion_metrics、trajectory、tic_curves、tic_plot、overlay,旧测试一起搬,做法同第一阶段。
- 绿框在最终轮廓里的融合权重(旧
roi_shape段)照搬,写在ceus.yaml里(待定问题/01)。 - 紫圈的颜色阈值(旧
pan_source段)照搬(待定问题/08)。 - 关键帧各时刻的容差窗口,逐个核对出处,写进
ceus.yaml的注释(结构.md第四部分)。
ceus 对外交出什么
ceus 不能引用 segment(结构.md 第三部分依赖表),所以分两段对外:
- 第一段:输入视频路径和参数,做第 1–2 步,交出分屏结果、哪一半是造影画面、要交给分割的那一半的逐帧图像,以及提示帧的掩码。
- 第二段:输入第一段的结果、分割得到的逐帧掩码、推注时间 X 和参数,做第 4–9 步,交出轨迹、TIC 曲线、拟合、7 个关键帧的定位结果、灌注指标。
两段中间由 scripts/ceus_quant.py 调 segment。具体类型由执行者定,写进 结构.md 第八部分。
收尾点
全部模块和测试搬完,make check 通过。另写一条标 gpu 的测试:在一段合成小视频上真的调一次 MedSAM2,输出掩码的帧数和尺寸对。跑通后提交。
第三阶段:scripts/ceus_quant.py,跑全部病例
跑哪些病例
用 delivery 读交付包,跑检查全部通过的病例(目前 34 例)。C14(关键图)在 build 时才查,被它跳过的病例 CEUS 照跑,build 时不用就是了。医生改完数据重新交付后,再跑一次。
delivery 的 CaseRecord 目前没有视频路径和推注时间,加上这两项(C04、C08 已经检查过它们)。
输出
CEUS 的结果会进资料库,最终可能写进论文,按 AGENTS.md 硬性约束 5 要能追到一次真跑的原始记录,所以每次运行放在 runs/ceus/ 下、只追加不删;只有能重算的分割缓存放 work/。
runs/
ceus/<运行号>/
snapshot/ 快照:git 状态、ceus.yaml 原文、权重文件的 SHA-256、MedSAM2 的提交号
failed.csv 失败的病例:视频号、失败在第几步、原因
<视频号>/
trajectory.npz 逐帧轮廓(格式同旧 trajectory.py)
tic_curves.npz 曲线(格式同旧 tic_curves.py)
summary.json 拟合结果、灌注指标、紫圈检出率、7 个关键帧的定位(帧号、视频时间、状态、未定位的原因)
tic.png 曲线图,标出 D1–D4
overlay.mp4 带轮廓的回放视频,供人目视检查
keyframes/<槽位>.png 定位到的关键帧,B0、D1……F2,未定位的槽位没有文件
work/
segment_cache/<键>/masks.npz
- 运行号的格式同构建号(用 runs 的
make_build_id)。运行目录先写成.partial,跑完再改名,已存在就报错,规则同任务书 01。 - 关键帧图是该帧的整幅原始画面(左右两半都在),不裁剪。给超声专家时裁不裁、怎么遮信息栏,留给会诊任务书(
待定问题/07)。 - 只有分割结果跨运行复用(
结构.md第四部分)。键是下面几样拼起来的 SHA-256:视频文件的 SHA-256、第 1–3 步用到的全部算法参数(规范化成 JSON;不含设备和两个 offload 开关,它们不改变结果)、权重文件的 SHA-256、MedSAM2 的提交号(从reference/MedSAM2/.git读)。键相同就直接读缓存,不调显卡。缓存目录也是先写.partial再改名。 - 某一例在第 1–6 步失败(包括第一阶段第 2 条的「两半都有或都没有绿框」、选不出提示帧),这一例记进
failed.csv、接着跑下一例,和交付资料的问题同一个规矩(AGENTS.md「错误怎么处理」)。拟合没过质量门不算失败:D1–D4 记为未定位、灌注指标写 null,原因写进summary.json,F1、F2 照常按推注时间取。个别关键帧槽位未定位也不算失败。 - 终端只打印计数:总例数、成功数、按步分的失败数、关键帧按槽位分的定位数、耗时。不打印视频号。
scripts/ceus_quant.py 只接收 --config config/ceus.yaml。全部病例要跑一阵子,放进 tmux,日志写到文件。
收尾点
在本机跑完全部病例,提交代码(runs/、work/ 不进 git)。
第四阶段:build 装入 CEUS 结果
build.yaml加一个必填字段ceus_run:某次 CEUS 运行目录的路径,或者 null(不装 CEUS 结果,就是现在的情况)。- 不为 null 时,build 按视频号找到每例的 CEUS 结果。这一例在
failed.csv里或者找不到结果目录,这一例跳过,记为 C15「CEUS 定量结果齐全」,涉及值写失败阶段和原因。CEUS 失败的病例要不要改成「超声不可评价、仍参与会诊」,等待定问题/03定,这一步按跳过处理。 - 资料库超声一路加:
ultrasound/keyframes/<槽位>.png:定位到的关键帧,经 library 去掉附加数据重新编码(同key.png)。加上原有的key.png,每例最多 8 张图。ultrasound/tic.json:从summary.json里只挑这几样,此外什么都不写:旧tic.py四条曲线(病灶整体、病灶去暗区、紫圈、背景圆)各自的拟合是否成功和拟合优度;灌注指标(同旧 replaysummary.json的perfusion_metrics段);紫圈曲线是否可用;7 个槽位各自定位到没有,定位到的写视频时间(秒),未定位的写原因。视频号、文件路径、配置、帧号都不写。
- 资料库
manifest.json的 CEUS 来源字段写 CEUS 运行号。 - 泄露扫描的禁词表加上视频文件名;扫描范围加上
tic.json。 - 用
ceus_run指向第三阶段的那次运行,重新构建一次真实资料库,跑scripts/scan_library.py。
收尾点
测试通过、真实构建和扫描跑完,提交。
测试
预期值照本任务书文字和旧测试写,不从新实现里推。
- 第一阶段的四处修改:各条手算测试,见上文。
- 旧测试:19 个文件中的 18 个(
test_tic_lognormal_replay不搬,它测的是旧 replay 脚本读标注工作簿的逻辑,第 3、4 条修改后不再适用)。conftest 里设产品层环境变量的那个夹具不搬。 - segment:一条
gpu测试(第二阶段收尾点)。 - 分割缓存:同样的输入第二次运行不调 segment(用 pytest 的 monkeypatch 把脚本里用到的 segment 函数换成计数的假函数,不为测试加参数);改一个第 1–3 步的参数,键就变。
ceus_quant:合成的两例小视频,一例正常、一例两半都没有绿框,后者进failed.csv,前者有完整输出。- build:C15 跳过;
tic.json只有上面列的几样;keyframes 的附加数据被去掉。
规模
| 模块 | 预期行数 |
|---|---|
| ceus | 6000–7500(照搬旧算法,去掉产品层) |
| segment | 150–300 |
scripts/ceus_quant.py |
≤ 300 |
| delivery、library、build 的增量 | 各 ≤ 150 |
超过区间上限的两倍就停下检查。完成后更新:结构.md 第六部分实际行数、第八部分 ceus 和 segment 的对外接口;guides/开发环境.md(MedSAM2 怎么装);新写 guides/CEUS定量.md(怎么跑、产物在哪、怎么看 failed.csv 和 overlay.mp4,过一轮「硕士生阅读」);WORK_LOG.md。
审阅
- 这一步大部分是「其余部分」那一档(
AGENTS.md「两档标准」),写完派一个全新上下文的子 Agent 审阅:只给它本任务书、结构.md第三到第五部分和 diff,让它重点看「四处要改的地方」是否改对、迁移有没有漏掉旧代码里的分支、关键帧和指标有没有算错。 - 第四阶段改的是资料库(决定专家能看见什么),属于账本级,另派一个全新上下文的子 Agent 单独审这部分,找:CEUS 结果里有没有带进视频号、文件名、医生的判断;CEUS 失败的病例有没有进库。
- Codex 可用时再加一轮对抗审查(
/codex:rescue --fresh --wait)。 - 意见的处理同任务书 01。子 Agent 的 prompt 里写明本机
grep、find用/usr/bin/下的版本。
报告
四个阶段都完成后,在回复里交一份报告。已验证的事实、推断、未验证的部分分开写。
- 四个提交号;各模块行数。
make check的完整输出;gpu测试的输出。- 旧测试:搬了多少条,删改了哪些、为什么。
- 真实运行:运行号、跑了几例、成功几例、按步分的失败数、关键帧按槽位分的定位数、总耗时和显存峰值。
- 紫圈检出率:每例一个数(有多少比例的帧认出了紫圈),只报分布(最小、四分位、最大),以及低于 50% 的有几例。给
待定问题/08用。 - 请负责人目视检查的样例:挑 3 例
overlay.mp4和tic.png的路径(成功、关键帧缺槽位、紫圈检出率最低的各一例),只给路径不描述内容。 - 重新构建的资料库:构建号、进库数、C15 跳过数、扫描结果。
- 审阅意见和处理。
- 和本任务书、
结构.md、AGENTS.md不一致的地方,以及为什么。 - 规则不清楚的地方:哪里犹豫、哪里靠猜。没有就写「没有」。