Show annotated and unannotated series counts
This commit is contained in:
@@ -350,7 +350,7 @@ def studies(_: str = Depends(require_auth), q: str = "", limit: int = 200) -> li
|
|||||||
SELECT
|
SELECT
|
||||||
ct_number, batch_name, target_folder_name, source_patient_name, patient_name_dicom,
|
ct_number, batch_name, target_folder_name, source_patient_name, patient_name_dicom,
|
||||||
patient_id, study_date, study_time, modality, dicom_file_count,
|
patient_id, study_date, study_time, modality, dicom_file_count,
|
||||||
processed_path, needs_ct_number_fix, status
|
series_count, processed_path, needs_ct_number_fix, status
|
||||||
FROM public.{PGTABLE}
|
FROM public.{PGTABLE}
|
||||||
{where}
|
{where}
|
||||||
ORDER BY study_date DESC NULLS LAST, study_time DESC NULLS LAST, ct_number
|
ORDER BY study_date DESC NULLS LAST, study_time DESC NULLS LAST, ct_number
|
||||||
@@ -361,14 +361,20 @@ def studies(_: str = Depends(require_auth), q: str = "", limit: int = 200) -> li
|
|||||||
"""
|
"""
|
||||||
SELECT ct_number, count(*)::int AS annotated_series
|
SELECT ct_number, count(*)::int AS annotated_series
|
||||||
FROM public.pacs_dicom_series_annotations
|
FROM public.pacs_dicom_series_annotations
|
||||||
WHERE skipped IS NOT TRUE AND jsonb_array_length(body_parts) > 0
|
WHERE skipped IS TRUE
|
||||||
|
OR jsonb_array_length(body_parts) > 0
|
||||||
|
OR plain_ct IS TRUE
|
||||||
|
OR NULLIF(notes, '') IS NOT NULL
|
||||||
|
OR ai_result IS NOT NULL
|
||||||
GROUP BY ct_number
|
GROUP BY ct_number
|
||||||
""",
|
""",
|
||||||
timeout=8,
|
timeout=8,
|
||||||
)
|
)
|
||||||
annotation_map = {row["ct_number"]: row["annotated_series"] for row in annotations}
|
annotation_map = {row["ct_number"]: row["annotated_series"] for row in annotations}
|
||||||
for row in rows:
|
for row in rows:
|
||||||
row["annotated_series"] = annotation_map.get(row["ct_number"], 0)
|
total_series = int(row.get("series_count") or 0)
|
||||||
|
row["annotated_series"] = min(total_series, int(annotation_map.get(row["ct_number"], 0)))
|
||||||
|
row["unannotated_series"] = max(0, total_series - int(row["annotated_series"]))
|
||||||
return rows
|
return rows
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -191,6 +191,25 @@ function setSeries(list) {
|
|||||||
renderSeries();
|
renderSeries();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isSeriesAnnotated(series) {
|
||||||
|
const annotation = series.annotation || {};
|
||||||
|
return Boolean(annotation.skipped || annotation.plain_ct || asList(annotation.body_parts).length || annotation.notes || annotation.ai_model);
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshStudyAnnotationSummary() {
|
||||||
|
if (!app.study || !app.series.length) return;
|
||||||
|
const annotated = app.series.filter(isSeriesAnnotated).length;
|
||||||
|
const total = app.series.length;
|
||||||
|
const summary = {
|
||||||
|
annotated_series: annotated,
|
||||||
|
unannotated_series: Math.max(0, total - annotated),
|
||||||
|
series_count: total,
|
||||||
|
};
|
||||||
|
Object.assign(app.study, summary);
|
||||||
|
const study = app.studies.find((item) => item.ct_number === app.study.ct_number);
|
||||||
|
if (study) Object.assign(study, summary);
|
||||||
|
}
|
||||||
|
|
||||||
async function loadStudies() {
|
async function loadStudies() {
|
||||||
const q = encodeURIComponent($("studySearch").value.trim());
|
const q = encodeURIComponent($("studySearch").value.trim());
|
||||||
app.studies = await json(`/api/studies?q=${q}&limit=500`);
|
app.studies = await json(`/api/studies?q=${q}&limit=500`);
|
||||||
@@ -211,7 +230,7 @@ function renderStudies() {
|
|||||||
<strong>${escapeHtml(study.ct_number)}</strong>
|
<strong>${escapeHtml(study.ct_number)}</strong>
|
||||||
<span>${escapeHtml(name)} · ${escapeHtml(study.patient_id || "无ID")}</span>
|
<span>${escapeHtml(name)} · ${escapeHtml(study.patient_id || "无ID")}</span>
|
||||||
<span>${escapeHtml(fmtDate(study.study_date))} ${escapeHtml(fmtTime(study.study_time))} · ${Number(study.dicom_file_count || 0)} 张</span>
|
<span>${escapeHtml(fmtDate(study.study_date))} ${escapeHtml(fmtTime(study.study_time))} · ${Number(study.dicom_file_count || 0)} 张</span>
|
||||||
<span>${Number(study.annotated_series || 0)} 个序列已标注</span>
|
<span>${Number(study.annotated_series || 0)} 个序列已标注,${Number(study.unannotated_series || 0)} 个序列未标注</span>
|
||||||
`;
|
`;
|
||||||
button.onclick = () => selectStudy(study.ct_number);
|
button.onclick = () => selectStudy(study.ct_number);
|
||||||
list.appendChild(button);
|
list.appendChild(button);
|
||||||
@@ -233,6 +252,8 @@ async function selectStudy(ctNumber) {
|
|||||||
const data = await json(`/api/studies/${encodeURIComponent(ctNumber)}/series`);
|
const data = await json(`/api/studies/${encodeURIComponent(ctNumber)}/series`);
|
||||||
app.study = data.study;
|
app.study = data.study;
|
||||||
setSeries(data.series || []);
|
setSeries(data.series || []);
|
||||||
|
refreshStudyAnnotationSummary();
|
||||||
|
renderStudies();
|
||||||
if (app.series.length) selectSeries(app.series[0].series_uid);
|
if (app.series.length) selectSeries(app.series[0].series_uid);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
$("seriesGrid").innerHTML = `<p class="error-line">${escapeHtml(err.message)}</p>`;
|
$("seriesGrid").innerHTML = `<p class="error-line">${escapeHtml(err.message)}</p>`;
|
||||||
@@ -509,7 +530,9 @@ function updateLocalAnnotation(annotation) {
|
|||||||
const index = app.series.findIndex((item) => item.series_uid === app.activeSeries.series_uid);
|
const index = app.series.findIndex((item) => item.series_uid === app.activeSeries.series_uid);
|
||||||
if (index >= 0) app.series[index].annotation = normalized;
|
if (index >= 0) app.series[index].annotation = normalized;
|
||||||
app.series = sortSeries(app.series);
|
app.series = sortSeries(app.series);
|
||||||
|
refreshStudyAnnotationSummary();
|
||||||
renderSeries();
|
renderSeries();
|
||||||
|
renderStudies();
|
||||||
applyAnnotationControls();
|
applyAnnotationControls();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user