feat(review): 核对包改成医生读得懂;资料库超声一路只留给 agent 的参数

- ceus:灌注指标改为对病灶去暗区(roi_perf)和紫圈(roi_pan)两组算,roi_all 指标删除;
  D1–D4 仍要 roi_all、roi_perf 两条都过才定位(负责人 2026-10-17:医生要正常区与病灶对比看)
- build/library:tic.json 只出两组 11 个指标与 7 个槽位的视频时间;医生截图挪进
  ultrasound/keyframes/key.png,UltrasoundRoute 去掉 key_image、keyframes 多一个 key
- review:TIC参数.csv 取代 TIC结果.csv;问题清单按原因对照表写成大白话,对照表外的原因报错;
  程序原文进 snapshot/问题清单_原文.csv;说明.pdf 整份重写为合作者口吻
- 文档:结构.md、医生核对包、装配两个库、CEUS定量

任务书 04。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-17 10:22:15 -04:00
parent 77ee03f9a3
commit 7e7f32a13d
19 changed files with 2111 additions and 634 deletions
+81
View File
@@ -13,6 +13,7 @@
超过 40 秒的视频末尾,未定位。
"""
import dataclasses
import json
import math
@@ -22,10 +23,17 @@ import pytest
from cadx.ceus import (
ANCHOR_SLOTS,
CeusConfig,
LognormalFitResult,
LognormalParameters,
NoValidContourError,
SegmentationInput,
TicResult,
VideoFile,
compute_lognormal_perfusion_metrics,
compute_tic_for_video,
crop_frame,
evaluate_lognormal,
fit_lognormal_curve,
keyframe_images,
locate_contrast_side,
pack_tic_curves,
@@ -156,6 +164,9 @@ def test_quantify_case_end_to_end(prepared: SegmentationInput, config: CeusConfi
json.dumps(summary, allow_nan=False)
assert summary["anchor_frames"]["F1"]["frame_index"] == 150
assert summary["perfusion_metrics"]["roi_perf"] is not None
# 没有紫圈:紫圈那一组整组为 null;病灶整体不算指标。
assert set(summary["perfusion_metrics"]) == {"roi_perf", "roi_pan"}
assert summary["perfusion_metrics"]["roi_pan"] is None
markers = tic_plot_markers(result.anchors)
assert [marker.label.split()[0] for marker in markers][:3] == ["D1", "D2", "D3"]
@@ -179,3 +190,73 @@ def test_mask_shape_mismatch_is_rejected(prepared: SegmentationInput, config: Ce
masks = _lesion_masks(prepared)[:-1]
with pytest.raises(ValueError, match="分割掩膜"):
quantify_case(prepared, masks, _INJECTION_OFFSET_S, config)
def _clean_fit(values: np.ndarray, times: np.ndarray, config: CeusConfig) -> LognormalFitResult:
"""直接调拟合函数拟一条曲线(基线 40、噪声 0.2,前 20 帧当基线段)。"""
return fit_lognormal_curve(
times,
values,
baseline=40.0,
baseline_noise=0.2,
baseline_end_index=20,
duplicate_mask=np.zeros(times.size, dtype=bool),
sample_mask=None,
cfg=config.tic_analysis.fitting,
anchor_cfg=config.tic_analysis.anchor_frames,
)
def test_pan_metrics_computed_and_lesion_gate_unchanged(
prepared: SegmentationInput, config: CeusConfig, monkeypatch: pytest.MonkeyPatch
) -> None:
"""紫圈拟合过、病灶整体拟合没过:紫圈与病灶去暗区各算一组指标,病灶整体不算;
D1–D4 照「病灶两条曲线都过才定位」不定位。
合成视频里没有紫圈,所以把第 6–7 步交出的拟合换掉:紫圈换成一条干净 Lognormal 曲线的拟合
(峰与病灶不同),病灶整体换成一条平线的拟合(没有可信的峰,拟合不过)。
"""
times = np.arange(FRAMES) / FPS
pan_truth = LognormalParameters(
area=600.0, mu=2.0, sigma=0.5, arrival_offset_s=3.0, baseline=40.0
)
pan_fit = _clean_fit(evaluate_lognormal(times, pan_truth), times, config)
flat_fit = _clean_fit(np.full(times.size, 40.0), times, config)
assert pan_fit.success is True and flat_fit.success is False
def patched(*args: object, **kwargs: object) -> TicResult:
tic = compute_tic_for_video(*args, **kwargs) # pyright: ignore[reportArgumentType]
return dataclasses.replace(tic, fits={**tic.fits, "roi_pan": pan_fit, "roi_all": flat_fit})
monkeypatch.setattr("cadx.ceus._pipeline.compute_tic_for_video", patched)
result = quantify_case(prepared, _lesion_masks(prepared), _INJECTION_OFFSET_S, config)
assert result.tic.fits["roi_perf"].success is True
assert result.lesion_fit_ready is False
for slot in ("B0", "D1", "D2", "D3", "D4"):
assert result.anchors[slot].status != "ok"
assert result.anchors[slot].frame_index is None
assert set(result.perfusion) == {"roi_perf", "roi_pan"}
assert result.perfusion["roi_pan"] == compute_lognormal_perfusion_metrics(pan_fit)
summary = quant_summary(result)
assert set(summary["perfusion_metrics"]) == {"roi_perf", "roi_pan"}
expected = compute_lognormal_perfusion_metrics(pan_fit)
assert expected is not None
# 11 项指标的键名与 LognormalPerfusionMetrics 字段的对应,照任务书 04 列的指标逐个手写。
assert summary["perfusion_metrics"]["roi_pan"]["values"] == {
"PE_au": expected.pe_au,
"WiAUC_au_s": expected.wi_auc_au_s,
"RT_s": expected.rt_s,
"mTTl_s": expected.mttl_s,
"TTP_s": expected.ttp_s,
"WiR_au_per_s": expected.wir_au_per_s,
"WiPI_au": expected.wipi_au,
"WoAUC_au_s": expected.wo_auc_au_s,
"WiWoAUC_au_s": expected.wiwo_auc_au_s,
"FT_s": expected.ft_s,
"WoR_au_per_s": expected.wor_au_per_s,
}
# 紫圈的峰与病灶不同:两组指标不是同一组数。
assert summary["perfusion_metrics"]["roi_pan"] != summary["perfusion_metrics"]["roi_perf"]
assert summary["lesion_fit_ready"] is False