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,
|
series_count integer NOT NULL DEFAULT 0,
|
||||||
annotated_series integer NOT NULL DEFAULT 0,
|
annotated_series integer NOT NULL DEFAULT 0,
|
||||||
unannotated_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,
|
body_parts jsonb NOT NULL DEFAULT '[]'::jsonb,
|
||||||
completed boolean NOT NULL DEFAULT false,
|
completed boolean NOT NULL DEFAULT false,
|
||||||
completed_by text,
|
completed_by text,
|
||||||
@@ -322,6 +323,8 @@ def ensure_study_summary_table() -> None:
|
|||||||
);
|
);
|
||||||
ALTER TABLE public.pacs_dicom_study_summaries
|
ALTER TABLE public.pacs_dicom_study_summaries
|
||||||
ADD COLUMN IF NOT EXISTS body_parts jsonb NOT NULL DEFAULT '[]'::jsonb;
|
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
|
ALTER TABLE public.pacs_dicom_study_summaries
|
||||||
ADD COLUMN IF NOT EXISTS completed boolean NOT NULL DEFAULT false;
|
ADD COLUMN IF NOT EXISTS completed boolean NOT NULL DEFAULT false;
|
||||||
ALTER TABLE public.pacs_dicom_study_summaries
|
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 NULLIF(a.notes, '') IS NOT NULL
|
||||||
OR a.ai_result IS NOT NULL
|
OR a.ai_result IS NOT NULL
|
||||||
)::int AS annotated_series,
|
)::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(
|
COALESCE(
|
||||||
jsonb_agg(DISTINCT part.value) FILTER (WHERE part.value IS NOT NULL),
|
jsonb_agg(DISTINCT part.value) FILTER (WHERE part.value IS NOT NULL),
|
||||||
'[]'::jsonb
|
'[]'::jsonb
|
||||||
@@ -534,7 +550,7 @@ def summary_rows_by_study() -> dict[str, dict[str, Any]]:
|
|||||||
"""
|
"""
|
||||||
SELECT
|
SELECT
|
||||||
ct_number, series_count, annotated_series, unannotated_series,
|
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
|
FROM public.pacs_dicom_study_summaries
|
||||||
""",
|
""",
|
||||||
timeout=8,
|
timeout=8,
|
||||||
@@ -594,6 +610,8 @@ def studies(_: str = Depends(require_auth), q: str = "", limit: int = 200) -> li
|
|||||||
row["series_count"] = total_series
|
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["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"]))
|
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["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"] = bool((summary or {}).get("completed", False))
|
||||||
row["completed_by"] = (summary or {}).get("completed_by", "")
|
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("notes")
|
||||||
or row.get("annotation", {}).get("ai_model")
|
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 {
|
return {
|
||||||
"ct_number": data.get("study", {}).get("ct_number", ""),
|
"ct_number": data.get("study", {}).get("ct_number", ""),
|
||||||
"series_count": total,
|
"series_count": total,
|
||||||
"annotated_series": annotated,
|
"annotated_series": annotated,
|
||||||
"unannotated_series": max(0, total - annotated),
|
"unannotated_series": max(0, total - annotated),
|
||||||
|
"undetermined_series": undetermined,
|
||||||
"body_parts": body_parts,
|
"body_parts": body_parts,
|
||||||
"summary_updated_at": time.strftime("%Y-%m-%d %H:%M:%S"),
|
"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(
|
pg_scalar(
|
||||||
f"""
|
f"""
|
||||||
INSERT INTO public.pacs_dicom_study_summaries (
|
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 (
|
VALUES (
|
||||||
{sql_literal(summary.get('ct_number', ''))},
|
{sql_literal(summary.get('ct_number', ''))},
|
||||||
{int(summary.get('series_count') or 0)},
|
{int(summary.get('series_count') or 0)},
|
||||||
{int(summary.get('annotated_series') or 0)},
|
{int(summary.get('annotated_series') or 0)},
|
||||||
{int(summary.get('unannotated_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,
|
{sql_literal(json.dumps(valid_parts(summary.get('body_parts') or []), ensure_ascii=False))}::jsonb,
|
||||||
now()
|
now()
|
||||||
)
|
)
|
||||||
@@ -1152,6 +1187,7 @@ def save_study_summary(summary: dict[str, Any]) -> None:
|
|||||||
series_count = EXCLUDED.series_count,
|
series_count = EXCLUDED.series_count,
|
||||||
annotated_series = EXCLUDED.annotated_series,
|
annotated_series = EXCLUDED.annotated_series,
|
||||||
unannotated_series = EXCLUDED.unannotated_series,
|
unannotated_series = EXCLUDED.unannotated_series,
|
||||||
|
undetermined_series = EXCLUDED.undetermined_series,
|
||||||
body_parts = EXCLUDED.body_parts,
|
body_parts = EXCLUDED.body_parts,
|
||||||
refreshed_at = now()
|
refreshed_at = now()
|
||||||
""",
|
""",
|
||||||
|
|||||||
@@ -302,9 +302,20 @@ function isSeriesAnnotated(series) {
|
|||||||
return Boolean(annotation.skipped || asList(annotation.body_parts).length || annotation.notes || annotation.ai_model);
|
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() {
|
function refreshStudyAnnotationSummary() {
|
||||||
if (!app.study || !app.series.length) return;
|
if (!app.study || !app.series.length) return;
|
||||||
const annotated = app.series.filter(isSeriesAnnotated).length;
|
const annotated = app.series.filter(isSeriesAnnotated).length;
|
||||||
|
const undetermined = app.series.filter(isSeriesUndetermined).length;
|
||||||
const total = app.series.length;
|
const total = app.series.length;
|
||||||
const bodyParts = BODY_PARTS.filter((part) =>
|
const bodyParts = BODY_PARTS.filter((part) =>
|
||||||
app.series.some((series) => asList(series.annotation?.body_parts).includes(part)),
|
app.series.some((series) => asList(series.annotation?.body_parts).includes(part)),
|
||||||
@@ -312,6 +323,7 @@ function refreshStudyAnnotationSummary() {
|
|||||||
const summary = {
|
const summary = {
|
||||||
annotated_series: annotated,
|
annotated_series: annotated,
|
||||||
unannotated_series: Math.max(0, total - annotated),
|
unannotated_series: Math.max(0, total - annotated),
|
||||||
|
undetermined_series: undetermined,
|
||||||
series_count: total,
|
series_count: total,
|
||||||
body_parts: bodyParts,
|
body_parts: bodyParts,
|
||||||
};
|
};
|
||||||
@@ -458,7 +470,7 @@ function renderStudies() {
|
|||||||
.map((label) => `<em title="${escapeHtml(label)}">${escapeHtml(label)}</em>`)
|
.map((label) => `<em title="${escapeHtml(label)}">${escapeHtml(label)}</em>`)
|
||||||
.join("");
|
.join("");
|
||||||
const studyDateTime = `${fmtDate(study.study_date)} ${fmtTime(study.study_time)}`.trim() || "未记录时间";
|
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 = [
|
const cardTitle = [
|
||||||
`检查号:${study.ct_number}`,
|
`检查号:${study.ct_number}`,
|
||||||
`患者:${name} · ${study.patient_id || "无ID"}`,
|
`患者:${name} · ${study.patient_id || "无ID"}`,
|
||||||
|
|||||||
Reference in New Issue
Block a user