Files
CADx-4/tests/fake_ceus_run.py
T
iomgaa 1de88a5d0a 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>
2026-10-16 09:01:44 -04:00

128 lines
5.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""测试用假 CEUS 运行目录:布局照 `scripts/ceus_quant.py` 写出的(failed.csv、snapshot/、
`<视频号>/summary.json`、`<视频号>/keyframes/<槽位>.png`),数值全部编造,不调显卡。
summary.json 故意带上 tic.json 不该有的字段(视频号、视频 SHA-256、分割缓存键、帧号、拟合参数、
失败原因代码、推注时间、紫圈检出率),用来测 build 只按白名单挑字段。视频 SHA-256 和推注时间照
`fake_delivery` 的假交付包(视频内容 `VIDEO_BYTES`、推注 5 秒)。关键帧 PNG 带文本块和 EXIF。
"""
import csv
import hashlib
import json
from pathlib import Path
from typing import Any
from PIL import Image, PngImagePlugin
from fake_delivery import VIDEO_BYTES
SLOTS = ("B0", "D1", "D2", "D3", "D4", "F1", "F2")
CURVES = ("roi_all", "roi_perf", "roi_pan", "background_circle")
UNLOCATED_REASON = "拟合曲线上没有 D4 时刻(编造的原因)"
def _perfusion(scale: float) -> dict[str, Any]:
names = ("PE_au", "WiAUC_au_s", "RT_s", "mTTl_s", "TTP_s", "WiR_au_per_s", "WiPI_au")
more = ("WoAUC_au_s", "WiWoAUC_au_s", "FT_s", "WoR_au_per_s")
return {
"signal_domain": "db_display",
"time_origin": "video_start",
"area_curve": "fitted_minus_baseline",
"values": {name: scale * (i + 1) for i, name in enumerate((*names, *more))},
"supporting_times_s": {"TI": 3.0, "mTT": 9.5, "TO": None},
}
def _fit(success: bool, r2: float | None) -> dict[str, Any]:
return {
"success": success,
"failure_reason": None if success else "credible_peak_not_found",
"curve_model": "lognormal",
"baseline_source": "observed",
"valid_samples": 120,
"duplicate_samples": 3,
"parameters": {
"area": 1.0,
"mu": 2.0,
"sigma": 0.5,
"arrival_offset_s": 4.0,
"baseline": 9,
},
"quality": {"r2": r2, "nrmse": 0.1, "residual_noise_ratio": 1.2, "multi_peak_flag": False},
"model_arrival_time_video_s": 4.0,
"model_landmark_times_video_s": {"D2": 6.0, "D3": 9.0, "D4": 15.0},
"model_landmark_intensities": {"D2": 1.0, "D3": 2.0, "D4": 1.5},
}
def summary(video_id: str, located: tuple[str, ...]) -> dict[str, Any]:
"""一例的 summary.json:`located` 里的槽位定位到(视频时间=槽位序号×2.5 秒),余下未定位。"""
anchors = {
slot: {
"frame_index": index * 25 if slot in located else None,
"frame_time_video_s": index * 2.5 if slot in located else None,
"time_since_injection_s": index * 2.5 + 5.0 if slot in located else None,
"status": "ok" if slot in located else "anchor_undefined",
"reason": None if slot in located else UNLOCATED_REASON,
"anchor_time_video_s": index * 2.5,
"time_error_s": 0.0 if slot in located else None,
"source_curve": "roi_perf",
}
for index, slot in enumerate(SLOTS)
}
return {
"video_id": video_id,
"video_sha256": hashlib.sha256(VIDEO_BYTES).hexdigest(),
"segment_cache_key": "f" * 64,
"injection_offset_s": 5.0,
"reference_valid": False,
"pan_detected_fraction": 0.4,
"early_truncation_flag": False,
"lesion_fit_ready": True,
"fits": {
"roi_all": _fit(True, 0.93),
"roi_perf": _fit(True, 0.91),
"roi_pan": _fit(False, None),
"background_circle": _fit(False, 0.2),
},
"perfusion_metrics": {"roi_all": _perfusion(1.0), "roi_perf": None},
"anchor_frames": anchors,
"available_dynamic_slots": [s for s in ("B0", "D1", "D2", "D3", "D4") if s in located],
}
def keyframe_png(path: Path, shade: int) -> None:
"""一张 8 位 RGB 关键帧,带文本块和 EXIF(写进资料库前要去掉)。"""
pixels = bytes(v for i in range(12) for v in (shade, i * 20, 200 - i * 10))
image = Image.frombytes("RGB", (4, 3), pixels)
info = PngImagePlugin.PngInfo()
info.add_text("Source", "U999_roi2.mp4")
exif = Image.Exif()
exif[0x010E] = "U999" # ImageDescription
image.save(path, format="PNG", pnginfo=info, exif=exif.tobytes())
def write_run(
run_dir: Path,
delivery_dir: Path,
results: dict[str, tuple[str, ...]],
failed: list[tuple[str, int, str]],
) -> Path:
"""写一个已完成的运行目录:`delivery_dir` 记进快照里的 ceus.yaml,`results` 是视频号 → 定位到的
槽位,`failed` 是 failed.csv 的行。"""
(run_dir / "snapshot").mkdir(parents=True)
(run_dir / "snapshot" / "snapshot.json").write_text("{}", encoding="utf-8")
ceus_yaml = f"paths:\n delivery_dir: {json.dumps(str(delivery_dir), ensure_ascii=False)}\n"
(run_dir / "snapshot" / "ceus.yaml").write_text(ceus_yaml, encoding="utf-8")
with (run_dir / "failed.csv").open("w", encoding="utf-8", newline="") as handle:
writer = csv.writer(handle)
writer.writerow(("video_id", "step", "reason"))
writer.writerows(failed)
for video_id, located in results.items():
(run_dir / video_id / "keyframes").mkdir(parents=True)
content = json.dumps(summary(video_id, located), ensure_ascii=False, indent=2)
(run_dir / video_id / "summary.json").write_text(content, encoding="utf-8")
for shade, slot in enumerate(located):
keyframe_png(run_dir / video_id / "keyframes" / f"{slot}.png", shade * 30)
return run_dir