Show undetermined study series count
This commit is contained in:
@@ -313,6 +313,7 @@ def ensure_study_summary_table() -> None:
|
||||
series_count integer NOT NULL DEFAULT 0,
|
||||
annotated_series integer NOT NULL DEFAULT 0,
|
||||
unannotated_series integer NOT NULL DEFAULT 0,
|
||||
undetermined_series integer NOT NULL DEFAULT 0,
|
||||
body_parts jsonb NOT NULL DEFAULT '[]'::jsonb,
|
||||
completed boolean NOT NULL DEFAULT false,
|
||||
completed_by text,
|
||||
@@ -322,6 +323,8 @@ def ensure_study_summary_table() -> None:
|
||||
);
|
||||
ALTER TABLE public.pacs_dicom_study_summaries
|
||||
ADD COLUMN IF NOT EXISTS body_parts jsonb NOT NULL DEFAULT '[]'::jsonb;
|
||||
ALTER TABLE public.pacs_dicom_study_summaries
|
||||
ADD COLUMN IF NOT EXISTS undetermined_series integer NOT NULL DEFAULT 0;
|
||||
ALTER TABLE public.pacs_dicom_study_summaries
|
||||
ADD COLUMN IF NOT EXISTS completed boolean NOT NULL DEFAULT false;
|
||||
ALTER TABLE public.pacs_dicom_study_summaries
|
||||
@@ -511,6 +514,19 @@ def annotation_overview_by_study() -> dict[str, dict[str, Any]]:
|
||||
OR NULLIF(a.notes, '') IS NOT NULL
|
||||
OR a.ai_result IS NOT NULL
|
||||
)::int AS annotated_series,
|
||||
count(DISTINCT a.series_instance_uid) FILTER (
|
||||
WHERE a.skipped IS NOT TRUE
|
||||
AND (
|
||||
(
|
||||
a.body_parts ? 'upper_abdomen'
|
||||
AND COALESCE(NULLIF(a.upper_abdomen_phase, ''), 'unknown') = 'unknown'
|
||||
)
|
||||
OR (
|
||||
a.body_parts ? 'chest'
|
||||
AND COALESCE(NULLIF(a.chest_window, ''), 'unknown') = 'unknown'
|
||||
)
|
||||
)
|
||||
)::int AS undetermined_series,
|
||||
COALESCE(
|
||||
jsonb_agg(DISTINCT part.value) FILTER (WHERE part.value IS NOT NULL),
|
||||
'[]'::jsonb
|
||||
@@ -534,7 +550,7 @@ def summary_rows_by_study() -> dict[str, dict[str, Any]]:
|
||||
"""
|
||||
SELECT
|
||||
ct_number, series_count, annotated_series, unannotated_series,
|
||||
body_parts, completed, completed_by, completed_at, completion_updated_at, refreshed_at
|
||||
undetermined_series, body_parts, completed, completed_by, completed_at, completion_updated_at, refreshed_at
|
||||
FROM public.pacs_dicom_study_summaries
|
||||
""",
|
||||
timeout=8,
|
||||
@@ -594,6 +610,8 @@ def studies(_: str = Depends(require_auth), q: str = "", limit: int = 200) -> li
|
||||
row["series_count"] = total_series
|
||||
row["annotated_series"] = min(total_series, int((summary or {}).get("annotated_series") if summary else annotation_summary.get("annotated_series", 0)))
|
||||
row["unannotated_series"] = max(0, total_series - int(row["annotated_series"]))
|
||||
undetermined_source = annotation_summary.get("undetermined_series", (summary or {}).get("undetermined_series", 0))
|
||||
row["undetermined_series"] = min(total_series, int(undetermined_source or 0))
|
||||
row["body_parts"] = valid_parts((summary or {}).get("body_parts") or annotation_summary.get("body_parts") or [])
|
||||
row["completed"] = bool((summary or {}).get("completed", False))
|
||||
row["completed_by"] = (summary or {}).get("completed_by", "")
|
||||
@@ -1122,11 +1140,27 @@ def study_summary_payload(data: dict[str, Any]) -> dict[str, Any]:
|
||||
or row.get("annotation", {}).get("notes")
|
||||
or row.get("annotation", {}).get("ai_model")
|
||||
)
|
||||
undetermined = sum(
|
||||
1
|
||||
for row in series_list
|
||||
if not row.get("annotation", {}).get("skipped")
|
||||
and (
|
||||
(
|
||||
"upper_abdomen" in valid_parts(row.get("annotation", {}).get("body_parts") or [])
|
||||
and row.get("annotation", {}).get("upper_abdomen_phase") == "unknown"
|
||||
)
|
||||
or (
|
||||
"chest" in valid_parts(row.get("annotation", {}).get("body_parts") or [])
|
||||
and row.get("annotation", {}).get("chest_window") == "unknown"
|
||||
)
|
||||
)
|
||||
)
|
||||
return {
|
||||
"ct_number": data.get("study", {}).get("ct_number", ""),
|
||||
"series_count": total,
|
||||
"annotated_series": annotated,
|
||||
"unannotated_series": max(0, total - annotated),
|
||||
"undetermined_series": undetermined,
|
||||
"body_parts": body_parts,
|
||||
"summary_updated_at": time.strftime("%Y-%m-%d %H:%M:%S"),
|
||||
}
|
||||
@@ -1138,13 +1172,14 @@ def save_study_summary(summary: dict[str, Any]) -> None:
|
||||
pg_scalar(
|
||||
f"""
|
||||
INSERT INTO public.pacs_dicom_study_summaries (
|
||||
ct_number, series_count, annotated_series, unannotated_series, body_parts, refreshed_at
|
||||
ct_number, series_count, annotated_series, unannotated_series, undetermined_series, body_parts, refreshed_at
|
||||
)
|
||||
VALUES (
|
||||
{sql_literal(summary.get('ct_number', ''))},
|
||||
{int(summary.get('series_count') or 0)},
|
||||
{int(summary.get('annotated_series') or 0)},
|
||||
{int(summary.get('unannotated_series') or 0)},
|
||||
{int(summary.get('undetermined_series') or 0)},
|
||||
{sql_literal(json.dumps(valid_parts(summary.get('body_parts') or []), ensure_ascii=False))}::jsonb,
|
||||
now()
|
||||
)
|
||||
@@ -1152,6 +1187,7 @@ def save_study_summary(summary: dict[str, Any]) -> None:
|
||||
series_count = EXCLUDED.series_count,
|
||||
annotated_series = EXCLUDED.annotated_series,
|
||||
unannotated_series = EXCLUDED.unannotated_series,
|
||||
undetermined_series = EXCLUDED.undetermined_series,
|
||||
body_parts = EXCLUDED.body_parts,
|
||||
refreshed_at = now()
|
||||
""",
|
||||
|
||||
@@ -302,9 +302,20 @@ function isSeriesAnnotated(series) {
|
||||
return Boolean(annotation.skipped || asList(annotation.body_parts).length || annotation.notes || annotation.ai_model);
|
||||
}
|
||||
|
||||
function isSeriesUndetermined(series) {
|
||||
const annotation = series.annotation || {};
|
||||
if (annotation.skipped) return false;
|
||||
const parts = asList(annotation.body_parts);
|
||||
return (
|
||||
(parts.includes("upper_abdomen") && annotation.upper_abdomen_phase === "unknown") ||
|
||||
(parts.includes("chest") && annotation.chest_window === "unknown")
|
||||
);
|
||||
}
|
||||
|
||||
function refreshStudyAnnotationSummary() {
|
||||
if (!app.study || !app.series.length) return;
|
||||
const annotated = app.series.filter(isSeriesAnnotated).length;
|
||||
const undetermined = app.series.filter(isSeriesUndetermined).length;
|
||||
const total = app.series.length;
|
||||
const bodyParts = BODY_PARTS.filter((part) =>
|
||||
app.series.some((series) => asList(series.annotation?.body_parts).includes(part)),
|
||||
@@ -312,6 +323,7 @@ function refreshStudyAnnotationSummary() {
|
||||
const summary = {
|
||||
annotated_series: annotated,
|
||||
unannotated_series: Math.max(0, total - annotated),
|
||||
undetermined_series: undetermined,
|
||||
series_count: total,
|
||||
body_parts: bodyParts,
|
||||
};
|
||||
@@ -458,7 +470,7 @@ function renderStudies() {
|
||||
.map((label) => `<em title="${escapeHtml(label)}">${escapeHtml(label)}</em>`)
|
||||
.join("");
|
||||
const studyDateTime = `${fmtDate(study.study_date)} ${fmtTime(study.study_time)}`.trim() || "未记录时间";
|
||||
const annotationSummary = `${Number(study.annotated_series || 0)} 个序列已标注,${Number(study.unannotated_series || 0)} 个序列未标注`;
|
||||
const annotationSummary = `${Number(study.annotated_series || 0)} 个序列已标注,${Number(study.undetermined_series || 0)} 个序列待判别`;
|
||||
const cardTitle = [
|
||||
`检查号:${study.ct_number}`,
|
||||
`患者:${name} · ${study.patient_id || "无ID"}`,
|
||||
|
||||
Reference in New Issue
Block a user