Align registration status and crisp slice masks

This commit is contained in:
Codex
2026-05-30 15:40:51 +08:00
parent 30f73937aa
commit 2a14099534
4 changed files with 33 additions and 42 deletions

View File

@@ -1813,7 +1813,7 @@ function paintMaskPixel(maskData, width, height, x, y, rgb, alpha) {
}
function solidStrokeRadius(width, height) {
return Math.max(2.2, Math.min(5.5, Math.max(width, height) * 0.006));
return Math.max(1, Math.min(2.4, Math.max(width, height) * 0.002));
}
function fillSegmentCapsulesIntoMask(maskData, width, height, segments, rgb, alpha, radius) {
@@ -1972,12 +1972,13 @@ function drawFallbackClosedRegion(context, width, height, segments, color, opaci
function fillSegmentsAsSolidMask(context, width, height, segments, color, opacity = 0.72) {
if (!segments.length) return 0;
const rgb = parseCssColor(color);
const alpha = Math.round(clamp(opacity, 0.1, 1) * 190);
const alpha = Math.round(clamp(opacity, 0.1, 1) * 220);
const maskCanvas = document.createElement("canvas");
maskCanvas.width = width;
maskCanvas.height = height;
const maskContext = maskCanvas.getContext("2d");
if (!maskContext) return 0;
maskContext.imageSmoothingEnabled = false;
const maskData = maskContext.createImageData(width, height);
const radius = solidStrokeRadius(width, height);
const groups = groupPlaneSegmentsByConnectivity(segments, radius * 1.15);
@@ -2018,40 +2019,12 @@ function fillSegmentsAsSolidMask(context, width, height, segments, color, opacit
filledPixels += groupPixels;
});
filledPixels += closeSmallMaskGaps(maskData, width, height, rgb, alpha, 3);
filledPixels += closeSmallMaskGaps(maskData, width, height, rgb, alpha, 1);
filledPixels += fillInternalMaskHoles(maskData, width, height, rgb, alpha);
maskContext.putImageData(maskData, 0, 0);
context.imageSmoothingEnabled = false;
context.drawImage(maskCanvas, 0, 0);
if (filledPixels === 0) {
context.save();
context.globalAlpha = clamp(opacity, 0.1, 1) * 0.42;
context.strokeStyle = color;
context.lineWidth = Math.max(0.8, Math.max(width, height) * 0.0012);
context.lineCap = "round";
context.lineJoin = "round";
context.beginPath();
segments.forEach((segment) => {
context.moveTo(segment.a.x, segment.a.y);
context.lineTo(segment.b.x, segment.b.y);
});
context.stroke();
context.restore();
}
context.save();
context.globalAlpha = clamp(opacity, 0.1, 1) * 0.65;
context.strokeStyle = color;
context.lineWidth = Math.max(0.8, Math.max(width, height) * 0.0012);
context.lineCap = "round";
context.lineJoin = "round";
context.beginPath();
segments.forEach((segment) => {
context.moveTo(segment.a.x, segment.a.y);
context.lineTo(segment.b.x, segment.b.y);
});
context.stroke();
context.restore();
return filledPixels;
}
@@ -2079,6 +2052,7 @@ async function drawMappingView(version = state.mappingVersion) {
if (canvas.width !== width) canvas.width = width;
if (canvas.height !== height) canvas.height = height;
const context = canvas.getContext("2d");
context.imageSmoothingEnabled = false;
context.clearRect(0, 0, width, height);
const targetZ = sliceToSceneZ(state.sliceIndex, volumeScene);
@@ -2101,7 +2075,7 @@ async function drawMappingView(version = state.mappingVersion) {
const position = record.mesh.geometry.attributes.position;
if (!position) continue;
const triangleCount = Math.floor(position.count / 3);
const step = Math.max(1, Math.ceil(triangleCount / 260000));
const step = 1;
const segments = [];
for (let tri = 0; tri < triangleCount; tri += step) {
tempA.fromBufferAttribute(position, tri * 3).applyMatrix4(matrix);
@@ -3166,8 +3140,8 @@ function wireEvents() {
$("autoSettingsPanel").scrollIntoView({ block: "nearest", behavior: "smooth" });
});
$("suggestBoneBtn").addEventListener("click", suggestBoneSelection);
$("autoCoarseBtn").addEventListener("click", runAutoCoarse);
$("autoFineBtn").addEventListener("click", runAutoFine);
$("autoCoarseBtn")?.addEventListener("click", runAutoCoarse);
$("autoFineBtn")?.addEventListener("click", runAutoFine);
$("savePoseBtn").addEventListener("click", () => saveRegistration());
$("saveIterateBtn").addEventListener("click", saveAndIteratePose);
$("restorePoseBtn").addEventListener("click", restoreAutoPose);

View File

@@ -236,9 +236,8 @@
<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>
</div>
<div class="pose-actions modal-actions auto-run-actions">
<button id="autoCoarseBtn" type="button">自动粗配准</button>
<button id="autoFineBtn" type="button">自动微调</button>
<div class="registration-operation-hint">
下方为配准操作区:保存当前位姿、保存并迭代自动微调,或退回上次自动调整前位姿。
</div>
<div class="pose-actions modal-actions save-pose-actions">
<button id="savePoseBtn" type="button">保存位姿</button>

View File

@@ -2039,6 +2039,18 @@ input[type="range"] {
margin: 12px 0 0;
}
.registration-operation-hint {
border: 1px solid rgba(25, 214, 195, 0.26);
border-radius: 8px;
background: rgba(20, 184, 166, 0.08);
color: #a7f3d0;
font-size: 11px;
font-weight: 900;
line-height: 1.45;
margin-top: 12px;
padding: 9px 10px;
}
.auto-config-grid {
display: grid;
grid-template-columns: minmax(240px, 0.85fr) minmax(300px, 1.15fr);

View File

@@ -529,10 +529,16 @@ def relation_cte() -> str:
r AS (
SELECT
upper(ct_number) AS ct_key,
count(*)::int AS registration_count,
count(DISTINCT series_instance_uid) FILTER (WHERE series_instance_uid <> '')::int AS registered_series,
count(*) FILTER (WHERE locked)::int AS locked_count,
max(updated_at) AS registration_updated_at
count(*) FILTER (WHERE COALESCE(registration_status, 'unregistered') = 'registered')::int AS registration_count,
count(DISTINCT series_instance_uid) FILTER (
WHERE COALESCE(registration_status, 'unregistered') = 'registered'
AND series_instance_uid <> ''
)::int AS registered_series,
count(*) FILTER (
WHERE COALESCE(registration_status, 'unregistered') = 'registered'
AND COALESCE(locked, false)
)::int AS locked_count,
max(updated_at) FILTER (WHERE COALESCE(registration_status, 'unregistered') = 'registered') AS registration_updated_at
FROM public.{REGISTRATION_TABLE_SQL}
GROUP BY upper(ct_number)
),