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