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:
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user