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:
2026-10-16 09:01:44 -04:00
parent e165b32b9d
commit 1de88a5d0a
23 changed files with 561 additions and 202 deletions
+39 -2
View File
@@ -24,14 +24,19 @@ from cadx.ceus import (
CeusConfig,
NoValidContourError,
SegmentationInput,
VideoFile,
crop_frame,
keyframe_images,
locate_contrast_side,
pack_tic_curves,
pack_trajectory,
prepare_segmentation,
quant_summary,
quantify_case,
register_halves,
render_case_overlay,
render_tic_png,
split_views,
tic_plot_markers,
unpack_tic_curves,
unpack_trajectory,
@@ -77,8 +82,9 @@ def test_prepare_segmentation_finds_contrast_half_and_maps_prompts(
target = prepared.contrast.target_view
assert (target.x1, target.x2) == (0, prepared.split_x)
assert abs(prepared.split_x - SPLIT) <= 1
assert len(prepared.target_frames) == FRAMES
assert all(frame.shape == (target.height, target.width, 3) for frame in prepared.target_frames)
target_frames = list(prepared.target_frames())
assert len(target_frames) == FRAMES
assert all(frame.shape == (target.height, target.width, 3) for frame in target_frames)
assert sorted(prepared.prompt_masks) == [p.frame_index for p in prepared.contrast.prompt_frames]
assert 0 in prepared.prompt_masks
xs = np.where(prepared.prompt_masks[0])[1]
@@ -87,6 +93,37 @@ def test_prepare_segmentation_finds_contrast_half_and_maps_prompts(
assert abs(int(xs.max()) - 169) <= 5
def test_streaming_prepare_matches_whole_video_in_memory(
prepared: SegmentationInput, config: CeusConfig
) -> None:
"""逐帧解码的第 1–2 步与「整段帧读进内存再交给各步」逐像素相同(改成流式之前就是后一种)。"""
frames = [frame for _, frame in VideoFile(prepared.video_path).iter_frames()]
split_x, left, right = split_views(frames, config.views)
registration = register_halves(
frames, left, right, config.registration, config.prompt_source, config.pan_source
)
contrast = locate_contrast_side(frames, left, right, config.prompt_source, config.keyframes)
assert prepared.split_x == split_x
assert prepared.registration == registration
assert prepared.contrast.side == contrast.side
assert (prepared.contrast.contrast_view, prepared.contrast.target_view) == (
contrast.contrast_view,
contrast.target_view,
)
assert [p.frame_index for p in prepared.contrast.prompt_frames] == [
p.frame_index for p in contrast.prompt_frames
]
for ours, whole in zip(prepared.contrast.prompt_frames, contrast.prompt_frames, strict=True):
assert np.array_equal(ours.mask, whole.mask)
target_frames = list(prepared.target_frames())
assert len(target_frames) == len(frames)
for ours, frame in zip(target_frames, frames, strict=True):
assert np.array_equal(ours, crop_frame(frame, contrast.target_view))
assert np.array_equal(
prepared.first_contrast_frame, crop_frame(frames[0], contrast.contrast_view)
)
def test_quantify_case_end_to_end(prepared: SegmentationInput, config: CeusConfig) -> None:
result = quantify_case(prepared, _lesion_masks(prepared), _INJECTION_OFFSET_S, config)