From e788f08037bae546a9396840ae7ca4787bfa4fa8 Mon Sep 17 00:00:00 2001 From: Codex Date: Wed, 27 May 2026 21:22:15 +0800 Subject: [PATCH] Show undetermined study series count --- PACS_DICOM处理/数据处理网页端/app.py | 40 +++++++++++++++++++-- PACS_DICOM处理/数据处理网页端/static/app.js | 14 +++++++- 2 files changed, 51 insertions(+), 3 deletions(-) diff --git a/PACS_DICOM处理/数据处理网页端/app.py b/PACS_DICOM处理/数据处理网页端/app.py index 5628d1d..4bf0480 100644 --- a/PACS_DICOM处理/数据处理网页端/app.py +++ b/PACS_DICOM处理/数据处理网页端/app.py @@ -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() """, diff --git a/PACS_DICOM处理/数据处理网页端/static/app.js b/PACS_DICOM处理/数据处理网页端/static/app.js index 07e793f..da310d9 100644 --- a/PACS_DICOM处理/数据处理网页端/static/app.js +++ b/PACS_DICOM处理/数据处理网页端/static/app.js @@ -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) => `${escapeHtml(label)}`) .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"}`,