Improve auto pose refinement controls
This commit is contained in:
@@ -99,6 +99,8 @@ const state = {
|
|||||||
dicomPreviewTimer: 0,
|
dicomPreviewTimer: 0,
|
||||||
autoResult: null,
|
autoResult: null,
|
||||||
lastAutoPose: null,
|
lastAutoPose: null,
|
||||||
|
autoDeltaBaseline: null,
|
||||||
|
autoFineRunning: false,
|
||||||
dirty: false,
|
dirty: false,
|
||||||
dirtyReason: "",
|
dirtyReason: "",
|
||||||
savedPoseSignature: "",
|
savedPoseSignature: "",
|
||||||
@@ -223,6 +225,7 @@ function markDirty(reason = "pose") {
|
|||||||
state.dirty = true;
|
state.dirty = true;
|
||||||
state.dirtyReason = reason;
|
state.dirtyReason = reason;
|
||||||
setSaveState("未保存", "warn");
|
setSaveState("未保存", "warn");
|
||||||
|
if (reason === "pose") updateAutoPoseResult();
|
||||||
}
|
}
|
||||||
|
|
||||||
function poseSignature(pose = state.pose) {
|
function poseSignature(pose = state.pose) {
|
||||||
@@ -243,6 +246,7 @@ function resetDirty() {
|
|||||||
state.dirtyReason = "";
|
state.dirtyReason = "";
|
||||||
state.savedPoseSignature = poseSignature(state.pose);
|
state.savedPoseSignature = poseSignature(state.pose);
|
||||||
setSaveState("已保存", "ok");
|
setSaveState("已保存", "ok");
|
||||||
|
resetAutoDeltaBaseline();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function api(path, options = {}) {
|
async function api(path, options = {}) {
|
||||||
@@ -530,8 +534,7 @@ function modelPoseMatrix() {
|
|||||||
return state.modelGroup.matrix.clone();
|
return state.modelGroup.matrix.clone();
|
||||||
}
|
}
|
||||||
|
|
||||||
function visibleModelBox() {
|
function visibleModelBox(records = visibleModelRecords()) {
|
||||||
const records = visibleModelRecords();
|
|
||||||
if (!records.length) return null;
|
if (!records.length) return null;
|
||||||
const box = new THREE.Box3();
|
const box = new THREE.Box3();
|
||||||
const matrix = modelPoseMatrix();
|
const matrix = modelPoseMatrix();
|
||||||
@@ -543,6 +546,14 @@ function visibleModelBox() {
|
|||||||
return box;
|
return box;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function autoReferenceRecords(settings = null) {
|
||||||
|
const visibleRecords = visibleModelRecords();
|
||||||
|
const referenceIds = new Set((settings?.referenceIds || []).map((id) => Number(id)));
|
||||||
|
if (!referenceIds.size) return visibleRecords;
|
||||||
|
const referenceRecords = visibleRecords.filter((record) => referenceIds.has(Number(record.file?.id)));
|
||||||
|
return referenceRecords.length ? referenceRecords : visibleRecords;
|
||||||
|
}
|
||||||
|
|
||||||
function bodyPartTags(row) {
|
function bodyPartTags(row) {
|
||||||
const labels = Array.isArray(row?.body_part_labels) ? row.body_part_labels : [];
|
const labels = Array.isArray(row?.body_part_labels) ? row.body_part_labels : [];
|
||||||
const keys = Array.isArray(row?.body_part_keys) ? row.body_part_keys : [];
|
const keys = Array.isArray(row?.body_part_keys) ? row.body_part_keys : [];
|
||||||
@@ -637,6 +648,7 @@ async function selectCase(ctNumber, algorithmModel = "未指定模型") {
|
|||||||
state.modelDetail = registration.model_reference?.model_detail || state.modelDetail || "ultra";
|
state.modelDetail = registration.model_reference?.model_detail || state.modelDetail || "ultra";
|
||||||
state.autoResult = null;
|
state.autoResult = null;
|
||||||
state.lastAutoPose = null;
|
state.lastAutoPose = null;
|
||||||
|
state.autoDeltaBaseline = null;
|
||||||
state.sliceIndex = 0;
|
state.sliceIndex = 0;
|
||||||
$("notes").value = registration.notes || "";
|
$("notes").value = registration.notes || "";
|
||||||
|
|
||||||
@@ -2460,6 +2472,9 @@ function readAutoSettings() {
|
|||||||
const value = Number($(id)?.value);
|
const value = Number($(id)?.value);
|
||||||
return Number.isFinite(value) ? value : fallback;
|
return Number.isFinite(value) ? value : fallback;
|
||||||
};
|
};
|
||||||
|
const referenceIds = [...document.querySelectorAll("#autoBoneOptions input:checked")]
|
||||||
|
.map((input) => Number(input.value))
|
||||||
|
.filter(Number.isFinite);
|
||||||
return {
|
return {
|
||||||
adjustable: {
|
adjustable: {
|
||||||
translateX: $("autoX")?.checked,
|
translateX: $("autoX")?.checked,
|
||||||
@@ -2467,6 +2482,7 @@ function readAutoSettings() {
|
|||||||
translateZ: $("autoZ")?.checked,
|
translateZ: $("autoZ")?.checked,
|
||||||
scale: $("autoScale")?.checked,
|
scale: $("autoScale")?.checked,
|
||||||
},
|
},
|
||||||
|
referenceIds,
|
||||||
sampleSlices: Math.max(3, Math.min(60, Math.round(numericValue("autoSampleSlicesNumber", 9)))),
|
sampleSlices: Math.max(3, Math.min(60, Math.round(numericValue("autoSampleSlicesNumber", 9)))),
|
||||||
boneReward: numericValue("autoBoneReward", 1),
|
boneReward: numericValue("autoBoneReward", 1),
|
||||||
outsidePenalty: numericValue("autoOutsidePenalty", 0.1),
|
outsidePenalty: numericValue("autoOutsidePenalty", 0.1),
|
||||||
@@ -2479,6 +2495,9 @@ function readAutoSettings() {
|
|||||||
|
|
||||||
function poseDelta(from, to) {
|
function poseDelta(from, to) {
|
||||||
return {
|
return {
|
||||||
|
rotateX: Number(((to.rotateX || 0) - (from.rotateX || 0)).toFixed(3)),
|
||||||
|
rotateY: Number(((to.rotateY || 0) - (from.rotateY || 0)).toFixed(3)),
|
||||||
|
rotateZ: Number(((to.rotateZ || 0) - (from.rotateZ || 0)).toFixed(3)),
|
||||||
translateX: Number(((to.translateX || 0) - (from.translateX || 0)).toFixed(3)),
|
translateX: Number(((to.translateX || 0) - (from.translateX || 0)).toFixed(3)),
|
||||||
translateY: Number(((to.translateY || 0) - (from.translateY || 0)).toFixed(3)),
|
translateY: Number(((to.translateY || 0) - (from.translateY || 0)).toFixed(3)),
|
||||||
translateZ: Number(((to.translateZ || 0) - (from.translateZ || 0)).toFixed(3)),
|
translateZ: Number(((to.translateZ || 0) - (from.translateZ || 0)).toFixed(3)),
|
||||||
@@ -2502,11 +2521,14 @@ function autoPoseResultItem(label, deltaText, currentText, changed = true) {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateAutoPoseResult(before = state.lastAutoPose?.before || state.pose, after = state.pose) {
|
function updateAutoPoseResult(before = state.autoDeltaBaseline || state.lastAutoPose?.before || state.pose, after = state.pose) {
|
||||||
const delta = poseDelta(before, after);
|
const delta = poseDelta(before, after);
|
||||||
const el = $("autoPoseResult");
|
const el = $("autoPoseResult");
|
||||||
if (!el) return;
|
if (!el) return;
|
||||||
el.innerHTML = [
|
el.innerHTML = [
|
||||||
|
autoPoseResultItem("旋转 X", `Δ ${signedValue(delta.rotateX, 1)}°`, `当前 ${Number(after.rotateX || 0).toFixed(1)}°`, Math.abs(delta.rotateX) > 0.0005),
|
||||||
|
autoPoseResultItem("旋转 Y", `Δ ${signedValue(delta.rotateY, 1)}°`, `当前 ${Number(after.rotateY || 0).toFixed(1)}°`, Math.abs(delta.rotateY) > 0.0005),
|
||||||
|
autoPoseResultItem("旋转 Z", `Δ ${signedValue(delta.rotateZ, 1)}°`, `当前 ${Number(after.rotateZ || 0).toFixed(1)}°`, Math.abs(delta.rotateZ) > 0.0005),
|
||||||
autoPoseResultItem("平移 X", `Δ ${signedValue(delta.translateX)}`, `当前 ${Number(after.translateX || 0).toFixed(3)}`, Math.abs(delta.translateX) > 0.0005),
|
autoPoseResultItem("平移 X", `Δ ${signedValue(delta.translateX)}`, `当前 ${Number(after.translateX || 0).toFixed(3)}`, Math.abs(delta.translateX) > 0.0005),
|
||||||
autoPoseResultItem("平移 Y", `Δ ${signedValue(delta.translateY)}`, `当前 ${Number(after.translateY || 0).toFixed(3)}`, Math.abs(delta.translateY) > 0.0005),
|
autoPoseResultItem("平移 Y", `Δ ${signedValue(delta.translateY)}`, `当前 ${Number(after.translateY || 0).toFixed(3)}`, Math.abs(delta.translateY) > 0.0005),
|
||||||
autoPoseResultItem("平移 Z", `Δ ${signedValue(delta.translateZ)}`, `当前 ${Number(after.translateZ || 0).toFixed(3)}`, Math.abs(delta.translateZ) > 0.0005),
|
autoPoseResultItem("平移 Z", `Δ ${signedValue(delta.translateZ)}`, `当前 ${Number(after.translateZ || 0).toFixed(3)}`, Math.abs(delta.translateZ) > 0.0005),
|
||||||
@@ -2514,6 +2536,31 @@ function updateAutoPoseResult(before = state.lastAutoPose?.before || state.pose,
|
|||||||
].join("");
|
].join("");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resetAutoDeltaBaseline() {
|
||||||
|
state.autoDeltaBaseline = { ...state.pose };
|
||||||
|
updateAutoPoseResult(state.pose, state.pose);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setAutoProgress(visible, value = 0, text = "") {
|
||||||
|
const wrap = $("autoProgress");
|
||||||
|
if (!wrap) return;
|
||||||
|
wrap.classList.toggle("hidden", !visible);
|
||||||
|
const safeValue = Math.max(0, Math.min(100, Number(value) || 0));
|
||||||
|
$("autoProgressFill").style.width = `${safeValue}%`;
|
||||||
|
$("autoProgressText").textContent = text || `${Math.round(safeValue)}%`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setAutoControlsBusy(busy) {
|
||||||
|
["autoCoarseBtn", "autoFineBtn", "saveIterateBtn"].forEach((id) => {
|
||||||
|
const button = $(id);
|
||||||
|
if (button) button.disabled = busy;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function waitForUiFrame() {
|
||||||
|
return new Promise((resolve) => window.requestAnimationFrame(() => resolve()));
|
||||||
|
}
|
||||||
|
|
||||||
function restoreAutoPose() {
|
function restoreAutoPose() {
|
||||||
if (!state.lastAutoPose?.before) {
|
if (!state.lastAutoPose?.before) {
|
||||||
$("autoResult").textContent = "暂无可退回的自动调整位姿。";
|
$("autoResult").textContent = "暂无可退回的自动调整位姿。";
|
||||||
@@ -2523,7 +2570,7 @@ function restoreAutoPose() {
|
|||||||
renderPoseControls();
|
renderPoseControls();
|
||||||
applyPose();
|
applyPose();
|
||||||
fitCamera();
|
fitCamera();
|
||||||
updateAutoPoseResult(state.lastAutoPose.after, state.pose);
|
resetAutoDeltaBaseline();
|
||||||
$("autoResult").textContent = "已退回到自动调整前的位姿。";
|
$("autoResult").textContent = "已退回到自动调整前的位姿。";
|
||||||
markDirty();
|
markDirty();
|
||||||
}
|
}
|
||||||
@@ -2531,9 +2578,11 @@ function restoreAutoPose() {
|
|||||||
function candidateScore(pose, settings = null) {
|
function candidateScore(pose, settings = null) {
|
||||||
if (!state.dicomSceneBox || !state.modelGroup || !visibleModelRecords().length) return -Infinity;
|
if (!state.dicomSceneBox || !state.modelGroup || !visibleModelRecords().length) return -Infinity;
|
||||||
const config = settings || readAutoSettings();
|
const config = settings || readAutoSettings();
|
||||||
|
const records = autoReferenceRecords(config);
|
||||||
|
if (!records.length) return -Infinity;
|
||||||
const current = { ...state.pose };
|
const current = { ...state.pose };
|
||||||
applyPose(pose);
|
applyPose(pose);
|
||||||
const modelBox = visibleModelBox();
|
const modelBox = visibleModelBox(records);
|
||||||
applyPose(current);
|
applyPose(current);
|
||||||
if (!modelBox) return -Infinity;
|
if (!modelBox) return -Infinity;
|
||||||
const overlap = modelBox.clone().intersect(state.dicomSceneBox);
|
const overlap = modelBox.clone().intersect(state.dicomSceneBox);
|
||||||
@@ -2556,7 +2605,8 @@ function candidateScore(pose, settings = null) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function estimateCoarsePose(settings) {
|
function estimateCoarsePose(settings) {
|
||||||
const modelBox = visibleModelBox();
|
const records = autoReferenceRecords(settings);
|
||||||
|
const modelBox = visibleModelBox(records);
|
||||||
if (!modelBox || !state.dicomSceneBox) return null;
|
if (!modelBox || !state.dicomSceneBox) return null;
|
||||||
const adjustable = settings.adjustable || {};
|
const adjustable = settings.adjustable || {};
|
||||||
const dicomSize = new THREE.Vector3();
|
const dicomSize = new THREE.Vector3();
|
||||||
@@ -2577,7 +2627,7 @@ function estimateCoarsePose(settings) {
|
|||||||
}
|
}
|
||||||
const previous = { ...state.pose };
|
const previous = { ...state.pose };
|
||||||
applyPose(candidate);
|
applyPose(candidate);
|
||||||
const nextBox = visibleModelBox();
|
const nextBox = visibleModelBox(records);
|
||||||
applyPose(previous);
|
applyPose(previous);
|
||||||
if (!nextBox) return candidate;
|
if (!nextBox) return candidate;
|
||||||
const modelCenter = new THREE.Vector3();
|
const modelCenter = new THREE.Vector3();
|
||||||
@@ -2594,6 +2644,8 @@ function runAutoCoarse() {
|
|||||||
$("autoResult").textContent = "请先选择 DICOM 序列和至少一个 STL。";
|
$("autoResult").textContent = "请先选择 DICOM 序列和至少一个 STL。";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (state.autoFineRunning) return;
|
||||||
|
setAutoProgress(false);
|
||||||
const before = { ...state.pose };
|
const before = { ...state.pose };
|
||||||
const settings = readAutoSettings();
|
const settings = readAutoSettings();
|
||||||
const estimated = estimateCoarsePose(settings);
|
const estimated = estimateCoarsePose(settings);
|
||||||
@@ -2608,6 +2660,7 @@ function runAutoCoarse() {
|
|||||||
fitCamera();
|
fitCamera();
|
||||||
state.autoResult = { score: candidateScore(state.pose, settings), pose: { ...state.pose }, evaluated: 1, settings };
|
state.autoResult = { score: candidateScore(state.pose, settings), pose: { ...state.pose }, evaluated: 1, settings };
|
||||||
state.lastAutoPose = { before, after: { ...state.pose } };
|
state.lastAutoPose = { before, after: { ...state.pose } };
|
||||||
|
state.autoDeltaBaseline = { ...before };
|
||||||
setAutoState("粗配准完成", "ok");
|
setAutoState("粗配准完成", "ok");
|
||||||
const delta = poseDelta(before, state.pose);
|
const delta = poseDelta(before, state.pose);
|
||||||
$("autoResult").textContent = `已按 DICOM/STL 盒体估算中心与缩放:ΔX ${signedValue(delta.translateX)},ΔY ${signedValue(delta.translateY)},ΔZ ${signedValue(delta.translateZ)},缩放 ×${delta.scale.toFixed(3)};score ${state.autoResult.score.toFixed(4)}。`;
|
$("autoResult").textContent = `已按 DICOM/STL 盒体估算中心与缩放:ΔX ${signedValue(delta.translateX)},ΔY ${signedValue(delta.translateY)},ΔZ ${signedValue(delta.translateZ)},缩放 ×${delta.scale.toFixed(3)};score ${state.autoResult.score.toFixed(4)}。`;
|
||||||
@@ -2615,49 +2668,81 @@ function runAutoCoarse() {
|
|||||||
markDirty();
|
markDirty();
|
||||||
}
|
}
|
||||||
|
|
||||||
function runAutoFine() {
|
async function runAutoFine() {
|
||||||
if (!state.activeCase || !currentSeries() || !visibleModelRecords().length) {
|
if (!state.activeCase || !currentSeries() || !visibleModelRecords().length) {
|
||||||
$("autoResult").textContent = "请先选择 DICOM 序列和至少一个 STL。";
|
$("autoResult").textContent = "请先选择 DICOM 序列和至少一个 STL。";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setAutoState("运行中", "warn");
|
if (state.autoFineRunning) return;
|
||||||
$("autoResult").textContent = "正在进行几何重叠微调...";
|
|
||||||
const before = { ...state.pose };
|
const before = { ...state.pose };
|
||||||
const settings = readAutoSettings();
|
const settings = readAutoSettings();
|
||||||
const adjustable = settings.adjustable;
|
const adjustable = settings.adjustable;
|
||||||
let best = { pose: { ...state.pose }, score: candidateScore(state.pose, settings), mode: "初始位姿" };
|
state.autoFineRunning = true;
|
||||||
let evaluated = 1;
|
setAutoControlsBusy(true);
|
||||||
const rounds = Array.from({ length: settings.iterations }, (_, index) => {
|
setAutoState("运行中", "warn");
|
||||||
const t = Math.max(0.18, 1 - index / Math.max(settings.iterations, 1));
|
setAutoProgress(true, 2, "2%");
|
||||||
return { move: 0.34 * t, scale: 0.12 * t };
|
if (state.windowMode !== "bone") setWindowMode("bone", true);
|
||||||
});
|
$("autoResult").textContent = "已切换骨窗显示,正在按参考 STL 与 DICOM 三维盒体重叠做微调...";
|
||||||
for (const round of rounds) {
|
await waitForUiFrame();
|
||||||
const candidates = [];
|
try {
|
||||||
if (adjustable.translateX) candidates.push(["translateX", round.move], ["translateX", -round.move]);
|
let best = { pose: { ...state.pose }, score: candidateScore(state.pose, settings), mode: "初始位姿" };
|
||||||
if (adjustable.translateY) candidates.push(["translateY", round.move], ["translateY", -round.move]);
|
let evaluated = 1;
|
||||||
if (adjustable.translateZ) candidates.push(["translateZ", round.move], ["translateZ", -round.move]);
|
const coarsePose = estimateCoarsePose(settings);
|
||||||
if (adjustable.scale) candidates.push(["scale", round.scale], ["scale", -round.scale]);
|
if (coarsePose) {
|
||||||
for (const [key, delta] of candidates.slice(0, settings.candidates)) {
|
const coarseScore = candidateScore(coarsePose, settings);
|
||||||
const pose = { ...best.pose, [key]: key === "scale" ? Math.max(0.2, Math.min(3, best.pose.scale + delta)) : best.pose[key] + delta };
|
|
||||||
const score = candidateScore(pose, settings);
|
|
||||||
evaluated += 1;
|
evaluated += 1;
|
||||||
if (score > best.score) best = { pose, score, mode: `${key} ${delta > 0 ? "+" : "-"}` };
|
if (coarseScore > best.score) best = { pose: coarsePose, score: coarseScore, mode: "粗配准种子" };
|
||||||
}
|
}
|
||||||
|
const rounds = Array.from({ length: settings.iterations }, (_, index) => {
|
||||||
|
const t = Math.max(0.18, 1 - index / Math.max(settings.iterations, 1));
|
||||||
|
return { move: 0.34 * t, scale: 0.12 * t };
|
||||||
|
});
|
||||||
|
for (const [index, round] of rounds.entries()) {
|
||||||
|
const candidates = [];
|
||||||
|
if (adjustable.translateX) candidates.push(["translateX", round.move], ["translateX", -round.move]);
|
||||||
|
if (adjustable.translateY) candidates.push(["translateY", round.move], ["translateY", -round.move]);
|
||||||
|
if (adjustable.translateZ) candidates.push(["translateZ", round.move], ["translateZ", -round.move]);
|
||||||
|
if (adjustable.scale) candidates.push(["scale", round.scale], ["scale", -round.scale]);
|
||||||
|
for (const [key, delta] of candidates.slice(0, settings.candidates)) {
|
||||||
|
const baseValue = Number(best.pose[key] ?? DEFAULT_POSE[key]);
|
||||||
|
const nextValue = key === "scale" ? Math.max(0.2, Math.min(3, baseValue + delta)) : baseValue + delta;
|
||||||
|
const pose = { ...best.pose, [key]: Number(nextValue.toFixed(key === "scale" ? 3 : 4)) };
|
||||||
|
const score = candidateScore(pose, settings);
|
||||||
|
evaluated += 1;
|
||||||
|
if (score > best.score) best = { pose, score, mode: `${key} ${delta > 0 ? "+" : "-"}` };
|
||||||
|
}
|
||||||
|
const progress = 8 + ((index + 1) / Math.max(rounds.length, 1)) * 88;
|
||||||
|
setAutoProgress(true, progress, `${Math.round(progress)}%`);
|
||||||
|
await waitForUiFrame();
|
||||||
|
}
|
||||||
|
state.pose = { ...best.pose };
|
||||||
|
state.autoResult = { ...best, evaluated, settings: { ...settings, scoring: "reference-stl-box-overlap", window: "bone-display" } };
|
||||||
|
state.lastAutoPose = { before, after: { ...state.pose } };
|
||||||
|
state.autoDeltaBaseline = { ...before };
|
||||||
|
renderPoseControls();
|
||||||
|
applyPose();
|
||||||
|
fitCamera();
|
||||||
|
const changed = poseSignature(best.pose) !== poseSignature(before);
|
||||||
|
setAutoState(changed ? "微调完成" : "微调无变化", changed ? "ok" : "warn");
|
||||||
|
const scoreText = Number.isFinite(best.score) ? best.score.toFixed(4) : "不可用";
|
||||||
|
$("autoResult").textContent = changed
|
||||||
|
? `最佳候选:${best.mode};score ${scoreText};评估 ${evaluated} 个候选;骨窗显示 + 参考 STL 盒体评分。`
|
||||||
|
: `已完成评估但最佳仍为初始位姿;score ${scoreText};评估 ${evaluated} 个候选。当前微调使用骨窗显示和参考 STL 盒体评分,若图像上仍错位,需要先粗配准或手动调整旋转后再微调。`;
|
||||||
|
updateAutoPoseResult(before, state.pose);
|
||||||
|
setAutoProgress(true, 100, "100%");
|
||||||
|
markDirty();
|
||||||
|
} catch (error) {
|
||||||
|
setAutoState("微调失败", "error");
|
||||||
|
$("autoResult").textContent = error.message || "自动微调失败。";
|
||||||
|
setAutoProgress(false);
|
||||||
|
} finally {
|
||||||
|
state.autoFineRunning = false;
|
||||||
|
setAutoControlsBusy(false);
|
||||||
}
|
}
|
||||||
state.pose = { ...best.pose };
|
|
||||||
state.autoResult = { ...best, evaluated, settings };
|
|
||||||
state.lastAutoPose = { before, after: { ...state.pose } };
|
|
||||||
renderPoseControls();
|
|
||||||
applyPose();
|
|
||||||
fitCamera();
|
|
||||||
setAutoState("微调完成", "ok");
|
|
||||||
$("autoResult").textContent = `最佳候选:${best.mode};score ${best.score.toFixed(4)};评估 ${evaluated} 个候选;采样 ${settings.sampleSlices} 张。`;
|
|
||||||
updateAutoPoseResult(before, state.pose);
|
|
||||||
markDirty();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveRegistration(nextStatus = null) {
|
async function saveRegistration(nextStatus = null) {
|
||||||
if (!state.activeCase || !currentSeries() || state.saving) return;
|
if (!state.activeCase || !currentSeries() || state.saving) return false;
|
||||||
state.saving = true;
|
state.saving = true;
|
||||||
$("saveBtn").disabled = true;
|
$("saveBtn").disabled = true;
|
||||||
$("statusBtn").disabled = true;
|
$("statusBtn").disabled = true;
|
||||||
@@ -2722,8 +2807,10 @@ async function saveRegistration(nextStatus = null) {
|
|||||||
renderCases();
|
renderCases();
|
||||||
renderSeries();
|
renderSeries();
|
||||||
renderActiveHeader();
|
renderActiveHeader();
|
||||||
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setSaveState(error.message, "error");
|
setSaveState(error.message, "error");
|
||||||
|
return false;
|
||||||
} finally {
|
} finally {
|
||||||
state.saving = false;
|
state.saving = false;
|
||||||
$("saveBtn").disabled = false;
|
$("saveBtn").disabled = false;
|
||||||
@@ -2822,8 +2909,10 @@ function wireEvents() {
|
|||||||
$("autoFineBtn").addEventListener("click", runAutoFine);
|
$("autoFineBtn").addEventListener("click", runAutoFine);
|
||||||
$("savePoseBtn").addEventListener("click", () => saveRegistration());
|
$("savePoseBtn").addEventListener("click", () => saveRegistration());
|
||||||
$("saveIterateBtn").addEventListener("click", async () => {
|
$("saveIterateBtn").addEventListener("click", async () => {
|
||||||
await saveRegistration();
|
const saved = await saveRegistration();
|
||||||
runAutoFine();
|
if (!saved) return;
|
||||||
|
await runAutoFine();
|
||||||
|
resetAutoDeltaBaseline();
|
||||||
});
|
});
|
||||||
$("restorePoseBtn").addEventListener("click", restoreAutoPose);
|
$("restorePoseBtn").addEventListener("click", restoreAutoPose);
|
||||||
const syncSampleSlices = (source) => {
|
const syncSampleSlices = (source) => {
|
||||||
|
|||||||
@@ -236,23 +236,30 @@
|
|||||||
<label>迭代轮次<input id="autoIterations" type="number" min="1" max="100" value="30" /></label>
|
<label>迭代轮次<input id="autoIterations" type="number" min="1" max="100" value="30" /></label>
|
||||||
<label>每轮候选<input id="autoCandidates" type="number" min="6" max="96" value="36" /></label>
|
<label>每轮候选<input id="autoCandidates" type="number" min="6" max="96" value="36" /></label>
|
||||||
</div>
|
</div>
|
||||||
<div class="pose-actions modal-actions">
|
<div class="pose-actions modal-actions auto-run-actions">
|
||||||
<button id="autoCoarseBtn" type="button">自动粗配准</button>
|
<button id="autoCoarseBtn" type="button">自动粗配准</button>
|
||||||
<button id="autoFineBtn" type="button">自动微调</button>
|
<button id="autoFineBtn" type="button">自动微调</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="pose-actions modal-actions save-pose-actions">
|
||||||
|
<button id="savePoseBtn" type="button">保存位姿</button>
|
||||||
|
<button id="saveIterateBtn" type="button">保存并迭代</button>
|
||||||
|
<button id="restorePoseBtn" type="button">退回位姿</button>
|
||||||
|
</div>
|
||||||
<div id="autoPoseResult" class="auto-pose-result">
|
<div id="autoPoseResult" class="auto-pose-result">
|
||||||
|
<span>旋转 X 0</span>
|
||||||
|
<span>旋转 Y 0</span>
|
||||||
|
<span>旋转 Z 0</span>
|
||||||
<span>平移 X 0</span>
|
<span>平移 X 0</span>
|
||||||
<span>平移 Y 0</span>
|
<span>平移 Y 0</span>
|
||||||
<span>平移 Z 0</span>
|
<span>平移 Z 0</span>
|
||||||
<span>缩放 1</span>
|
<span>缩放 1</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="pose-actions modal-actions">
|
|
||||||
<button id="savePoseBtn" type="button">保存位姿</button>
|
|
||||||
<button id="saveIterateBtn" type="button">保存并迭代</button>
|
|
||||||
<button id="restorePoseBtn" type="button">退回位姿</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div id="autoResult" class="auto-result">选择 DICOM 序列和 STL 后可运行。</div>
|
<div id="autoResult" class="auto-result">选择 DICOM 序列和 STL 后可运行。</div>
|
||||||
|
<div id="autoProgress" class="auto-progress hidden">
|
||||||
|
<div><i id="autoProgressFill"></i></div>
|
||||||
|
<span id="autoProgressText">0%</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<textarea id="notes" class="notes" rows="2" placeholder="配准备注"></textarea>
|
<textarea id="notes" class="notes" rows="2" placeholder="配准备注"></textarea>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -1872,6 +1872,42 @@ input[type="range"] {
|
|||||||
padding: 8px;
|
padding: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.auto-progress {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr) 42px;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
min-height: 28px;
|
||||||
|
border: 1px solid rgba(59, 130, 246, 0.3);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: rgba(7, 10, 15, 0.72);
|
||||||
|
padding: 7px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auto-progress div {
|
||||||
|
height: 7px;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 99px;
|
||||||
|
background: rgba(148, 163, 184, 0.18);
|
||||||
|
}
|
||||||
|
|
||||||
|
.auto-progress i {
|
||||||
|
display: block;
|
||||||
|
width: 0%;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: inherit;
|
||||||
|
background: linear-gradient(90deg, #22d3ee, #3b82f6);
|
||||||
|
transition: width 160ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auto-progress span {
|
||||||
|
color: #c7d2fe;
|
||||||
|
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 950;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
.compact-title {
|
.compact-title {
|
||||||
min-height: 28px;
|
min-height: 28px;
|
||||||
margin-top: 4px;
|
margin-top: 4px;
|
||||||
@@ -2076,7 +2112,7 @@ input[type="range"] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.tool-pane .auto-pose-result {
|
.tool-pane .auto-pose-result {
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
}
|
}
|
||||||
|
|
||||||
.compact-empty {
|
.compact-empty {
|
||||||
@@ -2084,7 +2120,7 @@ input[type="range"] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.auto-pose-result span {
|
.auto-pose-result span {
|
||||||
min-height: 52px;
|
min-height: 50px;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-rows: auto auto auto;
|
grid-template-rows: auto auto auto;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
Reference in New Issue
Block a user