diff --git a/DICOM_and_UPP配准/app.py b/DICOM_and_UPP配准/app.py
index 6a4150f..91dec31 100644
--- a/DICOM_and_UPP配准/app.py
+++ b/DICOM_and_UPP配准/app.py
@@ -468,6 +468,7 @@ def cases(
q: str = "",
status_filter: str = Query(default="", alias="status"),
body_part: str = "",
+ algorithm_model: str = "",
limit: int = Query(default=120, ge=1, le=400),
_: str = Depends(require_auth),
) -> list[dict[str, Any]]:
@@ -492,6 +493,8 @@ def cases(
clauses.append(f"registration_status = {sql_literal(status_filter)}")
if body_part in {"head_neck", "chest", "upper_abdomen", "lower_abdomen", "pelvis"}:
clauses.append(f"body_part_keys ? {sql_literal(body_part)}")
+ if algorithm_model.strip():
+ clauses.append(f"algorithm_model ILIKE {sql_literal('%' + algorithm_model.strip().replace('%', '').replace('_', '') + '%')}")
where_sql = "WHERE " + " AND ".join(clauses) if clauses else ""
return pg_json_rows(
f"""
diff --git a/DICOM_and_UPP配准/static/app.js b/DICOM_and_UPP配准/static/app.js
index 9bddb0b..b176082 100644
--- a/DICOM_and_UPP配准/static/app.js
+++ b/DICOM_and_UPP配准/static/app.js
@@ -46,6 +46,7 @@ const state = {
links: { pacs_viewer_url: "http://127.0.0.1:8107", relation_viewer_url: "http://127.0.0.1:8108" },
statusFilter: "",
partFilter: "",
+ modelFilter: "",
search: "",
caseCollapsed: false,
activeToolTab: "series",
@@ -60,11 +61,15 @@ const state = {
pose: { ...DEFAULT_POSE },
windowMode: "default",
fusionMode: "fusion",
+ fusionDetail: "low",
+ modelDetail: "standard",
+ mappingMode: "result",
sliceIndex: 0,
sliceRangeStart: 0,
sliceRangeEnd: 0,
dicomPreviewTimer: 0,
autoResult: null,
+ lastAutoPose: null,
dirty: false,
dirtyReason: "",
saving: false,
@@ -77,6 +82,8 @@ const state = {
scene: null,
controls: null,
dicomGroup: null,
+ dicomPlane: null,
+ dicomPoints: null,
modelGroup: null,
rawModelGroup: null,
modelMeshes: [],
@@ -247,6 +254,7 @@ async function loadCases(selectFirst = true) {
if (state.search) params.set("q", state.search);
if (state.statusFilter) params.set("status", state.statusFilter);
if (state.partFilter) params.set("body_part", state.partFilter);
+ if (state.modelFilter) params.set("algorithm_model", state.modelFilter);
const rows = await api(`/api/cases?${params.toString()}`);
state.cases = rows;
renderCases();
@@ -382,12 +390,6 @@ function clearDetail() {
$("activeTitle").textContent = "未选择 CT";
$("activeMeta").textContent = "从左侧选择一个完整关联检查";
$("activeTags").innerHTML = "";
- $("patientSummary").textContent = "未选择 CT";
- $("summaryCt").textContent = "-";
- $("summaryPatientId").textContent = "-";
- $("summaryStudyTime").textContent = "-";
- $("summaryBodyPart").textContent = "-";
- $("matchedSeries").innerHTML = "";
$("seriesList").innerHTML = `
未选择 CT
`;
$("stlList").innerHTML = `未选择 STL
`;
$("modelRail").innerHTML = "";
@@ -407,7 +409,7 @@ async function selectCase(ctNumber, algorithmModel = "未指定模型") {
&& sameCtNumber(state.activeCase.ct_number, ctNumber)
&& (state.algorithmModel || "未指定模型") !== normalizedAlgorithm;
const previousPose = { ...state.pose };
- if (!switchingModelOnly && !(await confirmChange())) return;
+ if (!(await confirmChange())) return;
setTopLoading(true);
setFusionLoading(true, "正在读取 CT 关联数据", 6);
try {
@@ -426,7 +428,10 @@ async function selectCase(ctNumber, algorithmModel = "未指定模型") {
const savedTransform = registration.transform || {};
const keepPreviousPose = switchingModelOnly;
state.pose = keepPreviousPose ? { ...DEFAULT_POSE, ...previousPose } : { ...DEFAULT_POSE, ...savedTransform };
+ state.fusionDetail = registration.model_reference?.fusion_detail || state.fusionDetail || "low";
+ state.modelDetail = registration.model_reference?.model_detail || state.modelDetail || "standard";
state.autoResult = null;
+ state.lastAutoPose = null;
state.sliceIndex = 0;
$("notes").value = registration.notes || "";
@@ -447,6 +452,7 @@ async function selectCase(ctNumber, algorithmModel = "未指定模型") {
renderSeries();
renderStl();
renderPoseControls();
+ syncDisplayControls();
resetDirty();
await loadFusion();
} catch (error) {
@@ -503,26 +509,25 @@ function currentSeries() {
function renderActiveHeader() {
const row = state.activeCase;
if (!row) return;
+ const selected = currentSeries();
+ const bodyTags = bodyPartTags(row);
+ const selectedLabels = selected?.annotation_labels?.length ? selected.annotation_labels.filter((label) => !String(label).includes("略过") && !String(label).includes("不采用")) : [];
$("activeTitle").textContent = `${row.ct_number} · ${row.algorithm_model || "未指定模型"}`;
- $("activeMeta").textContent = `${row.patient_name || row.upp_patient_name || "-"} · ${fmtDateTime(row.study_date, row.study_time) || "检查时间未知"} · ${Number(row.series_count || 0)} 个 DICOM 序列 · STL ${Number(row.stl_file_count || 0)} 个`;
+ $("activeMeta").textContent = `${row.patient_name || row.upp_patient_name || "-"} · ${row.patient_id || "-"} · ${fmtDateTime(row.study_date, row.study_time) || "检查时间未知"} · ${bodyTags.join("、") || row.study_description || "部位未知"} · ${Number(row.series_count || 0)} 个 DICOM 序列 · STL ${Number(row.stl_file_count || 0)} 个`;
const tags = [
- `${statusText(state.registrationStatus)}`,
- ...bodyPartTags(row).map((tag) => `${escapeHtml(tag)}`),
+ `${statusText(state.registrationStatus)}`,
+ `${escapeHtml(state.algorithmModel || row.algorithm_model || "未指定模型")}`,
+ ...(selected ? [`${escapeHtml(selected.description || "当前序列")}`] : []),
+ ...selectedLabels.map((tag) => `${escapeHtml(tag)}`),
+ ...bodyTags.map((tag) => `${escapeHtml(tag)}`),
];
$("activeTags").innerHTML = tags.join("");
$("statusBtn").textContent = state.registrationStatus === "registered" ? "标为未配准" : "标为已配准";
$("statusBtn").classList.toggle("unregistered", state.registrationStatus !== "registered");
- $("patientSummary").textContent = `${row.patient_name || row.upp_patient_name || "-"} ${row.patient_sex || ""}`.trim();
- $("summaryCt").textContent = row.ct_number || "-";
- $("summaryPatientId").textContent = row.patient_id || "-";
- $("summaryStudyTime").textContent = fmtDateTime(row.study_date, row.study_time) || "-";
- $("summaryBodyPart").textContent = bodyPartTags(row).join("、") || row.study_description || "-";
}
function renderSeries() {
$("seriesCount").textContent = state.series.length;
- const activeSeries = currentSeries();
- $("matchedSeries").innerHTML = activeSeries ? seriesCardHtml(activeSeries, true) : `尚未选择配准序列
`;
$("seriesList").innerHTML = state.series.length
? sortedSeriesForDisplay(state.series)
.map((item) => seriesCardHtml(item, item.series_uid === state.selectedSeriesUid))
@@ -541,6 +546,7 @@ function seriesCardHtml(item, active = false) {
const skipped = isSkippedSeries(item);
return `