fix(ceus): 降低长视频内存峰值,快照开跑即写,build 核对 CEUS 运行与交付包
- 内存:prepare_segmentation 改为流式解码,只留分屏和配准的抽样帧,绿框逐帧在两半上找; 分割那一半由 SegmentationInput.target_frames() 到用时再解码,segment_video 边读边写 jpg; MedSAM2 init_state 的读帧换成直接在存放设备上建张量(不改 MedSAM2 源码,结果逐位相同); 传播结果写进预分配数组。帧数最多的一段视频常驻内存峰值 11.2 GiB → 2.3 GiB,分辨率最大的 一段原代码被 OOM 杀掉、现为 4.2 GiB;两段的掩码和全部产物与改前逐字节相同,33 段视频的 第 1–2 步结果与改前一致,29 例配准与 review.csv 一致。 - 快照:ceus_quant 开跑时写 snapshot/(git、ceus.yaml、review.csv、uv.lock、segment.json), 配置和核对表从快照副本读;收尾由新函数 runs.finish_snapshot 只新建 finished.json。 build 与 ceus_align_review 同样改用 finish_snapshot 记结束时间。 - build:summary.json 加 video_sha256;装 CEUS 结果时核对快照里的 delivery_dir、逐例核对 视频 SHA-256 和推注时间,不同就整次停下;新字段做类型检查,不进 tic.json。 来源:主会话任务(问题 1–3 及补充的 delivery_dir 比对)。 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -165,6 +165,7 @@ library/<构建号>/
|
||||
|
||||
- `keyframes/<槽位>.png`:下文关键帧表里定位到的那几张(槽位即 B0、D1 等),和 `key.png` 一样去掉附加数据重新编码;每例最多 8 张图(7 张关键帧加 `key.png`)。
|
||||
- `tic.json`:从这一例的 `summary.json` 里逐字段挑出,此外什么都不写:四条曲线(`roi_all` 病灶整体、`roi_perf` 病灶去暗区、`roi_pan` 紫圈即医生圈的一块正常胰腺、`background_circle` 背景圆)各自的拟合是否成功(`success`)和拟合优度 R²(`r2`);病灶两条曲线的灌注指标(`perfusion_metrics`,拟合没过质量门为 null);紫圈曲线是否可用(`pan_curve_usable`);7 个槽位各自是否定位到(`located`)、定位到的视频时间(`video_time_s`,秒)、没定位的原因(`reason`)。视频号、文件路径、配置、帧号、拟合参数都不写。挑哪些字段只在 `src/cadx/build/_ceus.py` 里列。
|
||||
- 核对这次 CEUS 运行是在这份交付包上跑的:运行快照里 `ceus.yaml` 的 `paths.delivery_dir` 与 `build.yaml` 的 `delivery_dir` 规范化后不是同一路径;或逐例比,交付包里这段视频文件的 SHA-256 与 `summary.json` 的 `video_sha256` 不同,或交付包的推注时间与 `summary.json` 的 `injection_offset_s` 不同,整次停下报错。这两个字段不进 `tic.json`。
|
||||
- CEUS 在这一例上失败(在 `failed.csv` 里)或运行目录里没有这一例的结果,这一例整例跳过,记 C15。失败的病例要不要改成「超声不可评价、仍参与会诊」等 `../待定问题/03-病例排除标准.md` 定。
|
||||
|
||||
每一路能看见什么:
|
||||
@@ -371,15 +372,16 @@ Python 环境用 uv;安装与检查命令见 `../guides/开发环境.md`。
|
||||
- `read_git_state(repo_root)`:读出完整和短提交号、工作区是否干净(含未跟踪文件)、`git diff HEAD` 输出的 SHA-256。
|
||||
- `GitState`:上面这四项。
|
||||
- `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` 哈希、起止时间、各配置的哈希)。
|
||||
- `write_snapshot(snapshot_dir, build_id, git, started_at, uv_lock, configs)`:开跑时调。新建快照目录,复制配置原文并写 `snapshot.json`(构建号、git 状态、Python 版本、`uv.lock` 哈希、开始时间、各配置的哈希)。运行要跑很久时(CEUS 定量约一小时)在开跑时写,期间改了配置文件,快照里仍是这次用的那份。
|
||||
- `finish_snapshot(snapshot_dir, finished_at, record)`:收尾时调。在快照目录里新建 `finished.json`:结束时间加 `record`(运行结束才知道的值,如显存峰值、缓存命中数);已存在就报错,不改开跑时写的文件。
|
||||
|
||||
### ceus(`src/cadx/ceus/`)
|
||||
|
||||
对外两段接口,中间由 `scripts/ceus_quant.py` 调 segment 做第 3 步:
|
||||
|
||||
- `prepare_segmentation(video_path, cfg)`:第 1–2 步,交出 `SegmentationInput`;视频打不开或读出的不合规(fps、尺寸、时间戳、帧数)抛 `VideoDecodeError`、找不到可信分屏线抛 `SplitDetectionError`、两半配准失败(找不到扇形区、可用模板匹配太少,或两种方法估出的平移相差超过 `registration.max_disagreement_px`)抛 `RegistrationError`(都是第 1 步),两半都有或都没有绿框抛 `ContrastSideError`(第 2 步)。
|
||||
- `prepare_segmentation(video_path, cfg)`:第 1–2 步,交出 `SegmentationInput`(逐帧解码几遍,不把整段视频留在内存里,只留分割那一半的画面);视频打不开或读出的不合规(fps、尺寸、时间戳、帧数)抛 `VideoDecodeError`、找不到可信分屏线抛 `SplitDetectionError`、两半配准失败(找不到扇形区、可用模板匹配太少,或两种方法估出的平移相差超过 `registration.max_disagreement_px`)抛 `RegistrationError`(都是第 1 步),两半都有或都没有绿框抛 `ContrastSideError`(第 2 步)。
|
||||
- `register_halves(frames, left_view, right_view, cfg.registration, cfg.prompt_source, cfg.pan_source)`:第 1 步的两半配准,交出 `HalfRegistration`:左半局部坐标 → 右半局部坐标的平移,`correlation`(两半扇形区边缘图的归一化互相关,采用它)、`template`(左半扇形内多块模板逐帧匹配后取中位数,只用来核对)、各次匹配明细、两者各分量之差的最大值;`contrast_to_target(side)` 换成造影那一半 → 另一半的平移。
|
||||
- `SegmentationInput`:视频的 fps、帧数、宽高、逐帧时间戳,分屏线,`registration`(`HalfRegistration`),`offset`(造影那一半局部坐标 + `offset` = `target_view` 局部坐标),`contrast`(`ContrastSide`:哪一半是造影画面、造影视图和分割那一半视图 `target_view`、提示帧),`target_frames`(分割那一半的逐帧 BGR 图像),`prompt_masks`(提示帧帧号 → 绿框按 `offset` 挪到 `target_view` 局部坐标的掩膜),造影那一半的第一帧。
|
||||
- `SegmentationInput`:视频的 fps、帧数、宽高、逐帧时间戳,分屏线,`registration`(`HalfRegistration`),`offset`(造影那一半局部坐标 + `offset` = `target_view` 局部坐标),`contrast`(`ContrastSide`:哪一半是造影画面、造影视图和分割那一半视图 `target_view`、提示帧),`target_frames()`(重新解码,逐帧交出分割那一半的 BGR 图像;不存整段),`prompt_masks`(提示帧帧号 → 绿框按 `offset` 挪到 `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` 或未定位的类别)、未定位的原因、锚点时刻。
|
||||
@@ -392,7 +394,7 @@ Python 环境用 uv;安装与检查命令见 `../guides/开发环境.md`。
|
||||
|
||||
### segment(`src/cadx/segment/`)
|
||||
|
||||
- `segment_video(frames, prompt_masks, cfg)`:把提示帧的病灶掩码用 MedSAM2 传播到每一帧;`frames` 是分割那一半的逐帧 BGR 图像,`prompt_masks` 是提示帧帧号 → 同尺寸布尔掩码,交出 `(帧数, 高, 宽)` 布尔数组(即 `quantify_case` 的 `target_masks`)。
|
||||
- `segment_video(frames, prompt_masks, cfg)`:把提示帧的病灶掩码用 MedSAM2 传播到每一帧;`frames` 是分割那一半的逐帧 BGR 图像(只遍历一遍,边读边写成 jpg,可以是逐帧解码的生成器),`prompt_masks` 是提示帧帧号 → 同尺寸布尔掩码,交出 `(帧数, 高, 宽)` 布尔数组(即 `quantify_case` 的 `target_masks`)。MedSAM2 的 `init_state` 读帧时换成本模块的读法,帧张量直接建在显卡上,不先在 CPU 上建整段(结果逐位相同)。
|
||||
- `SegmentConfig`:`config/ceus.yaml` 里 `segment` 段的字段定义(MedSAM2 代码目录、权重、模型配置名、设备、两个 offload 开关;pydantic,字段必填、不许多写、读入后不可改)。
|
||||
- `medsam2_commit(medsam2_dir)`:读 MedSAM2 代码目录 `.git` 的 HEAD 提交号,给快照和分割缓存键用。
|
||||
- `cuda_peak_memory_bytes(device)`:本进程在该设备上分配过的显存峰值(字节),给运行快照用;非 CUDA 设备为 None,本进程没初始化过 CUDA(分割全读了缓存)为 0。放在 segment 里,是因为 `cadx` 里只有它引用 `torch`。
|
||||
|
||||
Reference in New Issue
Block a user