Align DICOM UPP registration uniqueness

This commit is contained in:
Codex
2026-05-28 18:41:49 +08:00
parent cb18aabb4d
commit 1679ddb03a
4 changed files with 166 additions and 19 deletions

View File

@@ -12,6 +12,7 @@ const state = {
stlFiles: [],
selectedStlIds: new Set(),
selectedFamilies: new Set(),
selectedAlgorithmModel: "",
mode: "file",
registrations: [],
pose: defaultPose(),
@@ -335,15 +336,46 @@ async function selectCase(ctKey) {
function chooseDefaultStl() {
state.selectedStlIds.clear();
state.selectedFamilies.clear();
if (!state.stlFiles.length) return;
const families = Array.from(new Set(state.stlFiles.map((item) => item.family).filter(Boolean)));
if (!state.stlFiles.length) {
state.selectedAlgorithmModel = "";
return;
}
const models = algorithmModels();
const nextModel = models.includes(state.selectedAlgorithmModel) ? state.selectedAlgorithmModel : models[0] || "";
selectDefaultStlForModel(nextModel);
}
function algorithmModels() {
return Array.from(new Set(state.stlFiles.map((item) => item.algorithm_model || "未指定模型").filter(Boolean))).sort();
}
function currentAlgorithmModel() {
const selected = selectedStlFiles();
const selectedModel = selected.find((item) => item.algorithm_model)?.algorithm_model;
if (selectedModel) return selectedModel;
if (state.selectedAlgorithmModel) return state.selectedAlgorithmModel;
return algorithmModels()[0] || state.activeCase?.algorithm_model || "未指定模型";
}
function stlFilesForSelectedModel() {
const model = currentAlgorithmModel();
return model ? state.stlFiles.filter((item) => (item.algorithm_model || "未指定模型") === model) : state.stlFiles;
}
function selectDefaultStlForModel(model) {
state.selectedStlIds.clear();
state.selectedFamilies.clear();
state.selectedAlgorithmModel = model || algorithmModels()[0] || "";
const candidates = stlFilesForSelectedModel();
if (!candidates.length) return;
const families = Array.from(new Set(candidates.map((item) => item.family).filter(Boolean)));
const preferred = ["liver", "portal_vein", "liver_artery", "liver_vein", "bile_duct", "pancreas", "spleen"];
const selectedFamily = preferred.find((family) => families.includes(family)) || families[0];
if (selectedFamily) {
state.selectedFamilies.add(selectedFamily);
state.stlFiles.filter((item) => item.family === selectedFamily).forEach((item) => state.selectedStlIds.add(Number(item.id)));
candidates.filter((item) => item.family === selectedFamily).forEach((item) => state.selectedStlIds.add(Number(item.id)));
} else {
state.selectedStlIds.add(Number(state.stlFiles[0].id));
state.selectedStlIds.add(Number(candidates[0].id));
}
}
@@ -423,24 +455,40 @@ function renderStl() {
$("stlCount").textContent = String(state.stlFiles.length);
$("modeFile").classList.toggle("active", state.mode === "file");
$("modeFamily").classList.toggle("active", state.mode === "family");
const families = Array.from(new Set(state.stlFiles.map((item) => item.family).filter(Boolean)));
const models = algorithmModels();
if (models.length && !models.includes(state.selectedAlgorithmModel)) {
state.selectedAlgorithmModel = models[0];
}
$("modelRail").innerHTML = models
.map((model) => `<button class="chip ${model === currentAlgorithmModel() ? "active" : ""}" data-model="${escapeHtml(model)}" type="button">${escapeHtml(model)}</button>`)
.join("");
const visibleFiles = stlFilesForSelectedModel();
const families = Array.from(new Set(visibleFiles.map((item) => item.family).filter(Boolean)));
$("familyRail").innerHTML = families
.map((family) => `<button class="chip ${state.selectedFamilies.has(family) ? "active" : ""}" data-family="${escapeHtml(family)}" type="button">${escapeHtml(family)}</button>`)
.join("");
document.querySelectorAll("[data-model]").forEach((button) => {
button.addEventListener("click", async () => {
selectDefaultStlForModel(button.dataset.model);
markUnsaved();
renderStl();
await reloadStlVisuals();
});
});
document.querySelectorAll("[data-family]").forEach((button) => {
button.addEventListener("click", async () => {
const family = button.dataset.family;
if (state.selectedFamilies.has(family)) state.selectedFamilies.delete(family);
else state.selectedFamilies.add(family);
state.selectedStlIds.clear();
state.stlFiles.filter((item) => state.selectedFamilies.has(item.family)).forEach((item) => state.selectedStlIds.add(Number(item.id)));
visibleFiles.filter((item) => state.selectedFamilies.has(item.family)).forEach((item) => state.selectedStlIds.add(Number(item.id)));
markUnsaved();
renderStl();
await reloadStlVisuals();
});
});
$("stlList").innerHTML = state.stlFiles.length
? state.stlFiles
$("stlList").innerHTML = visibleFiles.length
? visibleFiles
.map((row) => {
const checked = state.selectedStlIds.has(Number(row.id));
return `
@@ -533,9 +581,11 @@ async function reloadStlVisuals() {
function applyLatestRegistration() {
const seriesUid = state.selectedSeries?.series_uid || "";
const selectedLabel = currentStlLabel();
const exact = state.registrations.find((item) => item.series_instance_uid === seriesUid && item.stl_family === selectedLabel);
const latest = exact || state.registrations.find((item) => item.series_instance_uid === seriesUid) || null;
const model = currentAlgorithmModel();
const latest =
state.registrations.find((item) => item.algorithm_model === model) ||
state.registrations.find((item) => item.series_instance_uid === seriesUid) ||
null;
if (latest?.transform) {
state.pose = { ...defaultPose(), ...latest.transform };
$("registrationNotes").value = latest.notes || "";
@@ -557,11 +607,16 @@ async function saveRegistration(locked = false, forceUnlock = false) {
alert("请至少选择一个 STL 构件");
return;
}
const selectedModels = Array.from(new Set(selected.map((item) => item.algorithm_model || "未指定模型")));
if (selectedModels.length > 1) {
alert("同一次配准只能选择同一个算法模型的 STL");
return;
}
const payload = {
ct_number: state.activeCase.pacs_ct_number || state.activeCase.ct_key,
series_instance_uid: state.selectedSeries.series_uid,
series_description: state.selectedSeries.description || "",
algorithm_model: state.activeCase.algorithm_model || selected[0]?.algorithm_model || "未指定模型",
algorithm_model: selectedModels[0] || currentAlgorithmModel(),
stl_family: currentStlLabel(),
view_mode: state.mode,
selected_stl_files: selected.map((item) => ({
@@ -584,7 +639,7 @@ async function saveRegistration(locked = false, forceUnlock = false) {
slice_thickness: state.selectedSeries.slice_thickness,
spacing_between_slices: state.selectedSeries.spacing_between_slices,
},
model_reference: { selected_count: selected.length, families: Array.from(state.selectedFamilies) },
model_reference: { algorithm_model: selectedModels[0] || currentAlgorithmModel(), selected_count: selected.length, families: Array.from(state.selectedFamilies) },
segmentation: { preview: state.maskVisible, source: "visible_stl_preview" },
notes: $("registrationNotes").value.trim(),
locked,