Files
CADx-4/tests/fake_ceus_run.py
T
iomgaa c582fe426f feat(build): 任务书 02 第四阶段代码——build 装入 CEUS 结果
- build.yaml 加必填字段 ceus_run(CEUS 运行目录或 null)。
- 不为 null 时按视频号找结果;在 failed.csv 里或缺结果目录的整例跳过,记 C15;关键帧图沿用 C14 检查。
- 资料库超声一路加 keyframes/<槽位>.png(去附加数据重编码)和 tic.json(白名单逐字段构造:
  四条曲线的拟合成败与 R²、灌注指标、紫圈曲线是否可用、7 个槽位的定位与视频时间或原因)。
- manifest 的 CEUS 来源写运行号;泄露扫描已覆盖 tic.json,补命中测试。
- 真实构建等配准改完、全量 CEUS 运行之后再做。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 05:55:57 -04:00

119 lines
5.0 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 不该有的字段(视频号、分割缓存键、帧号、拟合参数、失败原因代码、
推注时间、紫圈检出率),用来测 build 只按白名单挑字段。关键帧 PNG 带文本块和 EXIF。
"""
import csv
import json
from pathlib import Path
from typing import Any
from PIL import Image, PngImagePlugin
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,
"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,
results: dict[str, tuple[str, ...]],
failed: list[tuple[str, int, str]],
) -> Path:
"""写一个已完成的运行目录:`results` 是视频号 → 定位到的槽位,`failed` 是 failed.csv 的行。"""
(run_dir / "snapshot").mkdir(parents=True)
(run_dir / "snapshot" / "snapshot.json").write_text("{}", 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