try to fix recons

This commit is contained in:
Ross
2026-06-08 11:41:46 +01:00
parent d89df3d0a6
commit e78a69367f
2 changed files with 215 additions and 28 deletions
+37 -15
View File
@@ -336,6 +336,32 @@ def series_reconstruct_task(
src_y = source_for_patient_axis[1]
src_z = source_for_patient_axis[2]
# Build patient-axis basis vectors expressed in source/world space.
# This preserves source world-frame orientation (including obliquity/sign)
# for derived series instead of hardcoding cardinal axes.
patient_x_vec = np.asarray(source_axis_dirs[src_x], dtype=float) * float(sign_for_patient_axis[0])
patient_y_vec = np.asarray(source_axis_dirs[src_y], dtype=float) * float(sign_for_patient_axis[1])
patient_z_vec = np.asarray(source_axis_dirs[src_z], dtype=float) * float(sign_for_patient_axis[2])
def _norm_vec(vec):
nrm = float(np.linalg.norm(vec))
if nrm == 0:
return np.asarray(vec, dtype=float)
return np.asarray(vec, dtype=float) / nrm
patient_x_vec = _norm_vec(patient_x_vec)
patient_y_vec = _norm_vec(patient_y_vec)
patient_z_vec = _norm_vec(patient_z_vec)
def _orientation_pair(col_vec, row_vec, desired_normal):
col = _norm_vec(col_vec)
row = _norm_vec(row_vec)
desired = _norm_vec(desired_normal)
normal = np.cross(col, row)
if float(np.dot(normal, desired)) < 0:
row = -row
return [float(col[0]), float(col[1]), float(col[2]), float(row[0]), float(row[1]), float(row[2])]
spacing_x = source_axis_spacings[src_x]
spacing_y = source_axis_spacings[src_y]
spacing_z = source_axis_spacings[src_z]
@@ -350,7 +376,7 @@ def series_reconstruct_task(
vol_zyx = np.flip(vol_zyx, axis=2)
def source_indices_from_patient_zyx(iz, iy, ix):
src_indices = [0, 0, 0]
src_indices = [0.0, 0.0, 0.0]
z_index = float(iz)
y_index = float(iy)
@@ -378,10 +404,6 @@ def series_reconstruct_task(
)
return [float(pos[0]), float(pos[1]), float(pos[2])]
spacing_x = source_axis_spacings[src_x]
spacing_y = source_axis_spacings[src_y]
spacing_z = source_axis_spacings[src_z]
def _reduce_slab(slab, reduce_axis=0):
if recon_thickness_mode == "max":
out = np.max(slab, axis=reduce_axis)
@@ -440,8 +462,8 @@ def series_reconstruct_task(
"pixel_spacing_out": [float(spacing_y), float(spacing_x)],
"spacing_between_slices_out": float(plane_spacing),
"slice_thickness_out": float(plane_thickness),
"image_orientation_out": [1.0, 0.0, 0.0, 0.0, 1.0, 0.0],
"plane_normal_out": [0.0, 0.0, 1.0],
"image_orientation_out": _orientation_pair(patient_x_vec, patient_y_vec, patient_z_vec),
"plane_normal_out": [float(patient_z_vec[0]), float(patient_z_vec[1]), float(patient_z_vec[2])],
"position_fn": lambda idx, centers=slab_centers_idx: position_from_patient_zyx(centers[idx], 0.0, 0.0),
}
elif plane_norm == "coronal":
@@ -452,13 +474,13 @@ def series_reconstruct_task(
)
plane_builders[plane_norm] = {
"count": int(len(slab_indices)),
"slice_fn": lambda idx, slabs=slab_indices: np.flipud(_reduce_slab(vol_zyx[:, slabs[idx], :], reduce_axis=1)),
"slice_fn": lambda idx, slabs=slab_indices: _reduce_slab(vol_zyx[:, slabs[idx], :], reduce_axis=1),
"pixel_spacing_out": [float(spacing_z), float(spacing_x)],
"spacing_between_slices_out": float(plane_spacing),
"slice_thickness_out": float(plane_thickness),
"image_orientation_out": [1.0, 0.0, 0.0, 0.0, 0.0, -1.0],
"plane_normal_out": [0.0, 1.0, 0.0],
"position_fn": lambda idx, centers=slab_centers_idx: position_from_patient_zyx(float(vol_zyx.shape[0] - 1), centers[idx], 0.0),
"image_orientation_out": _orientation_pair(patient_x_vec, patient_z_vec, patient_y_vec),
"plane_normal_out": [float(patient_y_vec[0]), float(patient_y_vec[1]), float(patient_y_vec[2])],
"position_fn": lambda idx, centers=slab_centers_idx: position_from_patient_zyx(0.0, centers[idx], 0.0),
}
elif plane_norm == "sagittal":
plane_spacing = float(requested_slice_spacing) if requested_slice_spacing is not None else float(spacing_x)
@@ -468,13 +490,13 @@ def series_reconstruct_task(
)
plane_builders[plane_norm] = {
"count": int(len(slab_indices)),
"slice_fn": lambda idx, slabs=slab_indices: np.flipud(_reduce_slab(vol_zyx[:, :, slabs[idx]], reduce_axis=2)),
"slice_fn": lambda idx, slabs=slab_indices: _reduce_slab(vol_zyx[:, :, slabs[idx]], reduce_axis=2),
"pixel_spacing_out": [float(spacing_z), float(spacing_y)],
"spacing_between_slices_out": float(plane_spacing),
"slice_thickness_out": float(plane_thickness),
"image_orientation_out": [0.0, 1.0, 0.0, 0.0, 0.0, -1.0],
"plane_normal_out": [1.0, 0.0, 0.0],
"position_fn": lambda idx, centers=slab_centers_idx: position_from_patient_zyx(float(vol_zyx.shape[0] - 1), 0.0, centers[idx]),
"image_orientation_out": _orientation_pair(patient_y_vec, patient_z_vec, patient_x_vec),
"plane_normal_out": [float(patient_x_vec[0]), float(patient_x_vec[1]), float(patient_x_vec[2])],
"position_fn": lambda idx, centers=slab_centers_idx: position_from_patient_zyx(0.0, 0.0, centers[idx]),
}
if not plane_builders:
@@ -33,7 +33,7 @@
</div>
<div class="alert alert-info recon-advanced-help">
<div><strong>Workflow:</strong> click <em>Apply MPR layout</em>, switch all panes to volume mode if needed, enable crosshairs in the DV3D toolbar, then position the crosshair planes as desired before generating reconstructions.</div>
<div><strong>Workflow:</strong> the page auto-configures a 3-volume MPR layout on load and attempts to enable crosshair-linked navigation. Adjust crosshairs as needed, then generate reconstructions.</div>
</div>
<div id="advanced_recon_root" class="dicom-viewer-root recon-advanced-viewer" data-images='{{ image_ids_json|safe }}'></div>
@@ -72,10 +72,11 @@
<div class="col-12 col-lg-4">
<label class="form-label" for="adv-recon-thickness">Slab thickness (mm)</label>
<input id="adv-recon-thickness" type="number" step="0.1" min="0.1" name="recon_slice_thickness" class="form-control" placeholder="e.g. 2.5">
<input id="adv-recon-thickness" type="number" step="0.1" min="0.1" name="recon_slice_thickness" class="form-control" value="3.0">
<label class="form-label mt-2" for="adv-recon-spacing">Slice spacing (mm)</label>
<input id="adv-recon-spacing" type="number" step="0.1" min="0.1" name="recon_slice_spacing" class="form-control" placeholder="e.g. 1.0">
<input id="adv-recon-spacing" type="number" step="0.1" min="3" name="recon_slice_spacing" class="form-control" value="3.0">
<small class="text-muted d-block mt-1">Spacing is user-controlled and defaults to 3.0 mm minimum to reduce generated stack size.</small>
</div>
<div class="col-12 col-lg-4">
@@ -99,36 +100,176 @@
</div>
</div>
<div class="card border-0 shadow-sm mt-3">
<div class="card-body">
<div class="d-flex flex-wrap justify-content-between align-items-center gap-2 mb-2">
<h6 class="mb-0">Live Reconstruction Plan</h6>
<small id="advanced-plan-status" class="text-muted">Waiting for viewer telemetry...</small>
</div>
<div class="row g-2 small">
<div class="col-12 col-md-4"><strong>Viewport 1:</strong> <span id="advanced-plan-vp0">-</span></div>
<div class="col-12 col-md-4"><strong>Viewport 2:</strong> <span id="advanced-plan-vp1">-</span></div>
<div class="col-12 col-md-4"><strong>Viewport 3:</strong> <span id="advanced-plan-vp2">-</span></div>
</div>
<div class="row g-2 small mt-1">
<div class="col-12 col-md-6"><strong>Slab thickness:</strong> <span id="advanced-plan-thickness">3.0 mm</span></div>
<div class="col-12 col-md-6"><strong>Slice spacing:</strong> <span id="advanced-plan-spacing">3.0 mm</span></div>
</div>
</div>
</div>
<div id="advanced-recon-feedback" class="small mt-3"></div>
</div>
<script src="{% static 'dv3d/index.js' %}" type="module" defer="defer"></script>
<script>
(function () {
var ORIENTATION_LABELS = {
axial: 'Axial',
coronal: 'Coronal',
sagittal: 'Sagittal',
};
function getViewerApi(name) {
return window[name + '_advanced_recon_root'] || window[name + '_advanced_recon'] || window[name + '_root'] || window[name];
}
function buildMprState() {
return {
function parseViewerState(raw) {
if (!raw) {
return null;
}
if (typeof raw === 'string') {
try {
return JSON.parse(raw);
} catch (err) {
return null;
}
}
return raw;
}
function getDisplayOrientation(value) {
if (!value) {
return 'Unknown';
}
return ORIENTATION_LABELS[value] || String(value).charAt(0).toUpperCase() + String(value).slice(1);
}
function ensureMprLayout() {
var exportFn = getViewerApi('exportViewerState');
var importFn = getViewerApi('importViewerState');
if (typeof importFn !== 'function') {
return false;
}
var existing = parseViewerState(typeof exportFn === 'function' ? exportFn() : null) || {};
var stackIdx = Array.isArray(existing.stackIdx) ? existing.stackIdx : [];
var stacks = Array.isArray(existing.stacks) ? existing.stacks : [];
var baseStack = typeof stackIdx[0] === 'number' ? stackIdx[0] : (stacks.length > 0 ? 0 : null);
var nextState = {
grid: { rows: 1, cols: 3 },
modes: ['volume', 'volume', 'volume'],
orientations: ['axial', 'coronal', 'sagittal'],
volumeSliceIndices: [null, null, null],
};
if (stacks.length > 0 && baseStack !== null) {
nextState.stacks = stacks;
nextState.stackIdx = [baseStack, baseStack, baseStack];
}
try {
importFn(nextState);
return true;
} catch (err) {
console.warn('Failed to apply automatic MPR layout', err);
return false;
}
}
function tryEnableCrosshairs() {
var labels = ['Enable Cross Reference Lines', 'Enable Crosshairs'];
var root = document.getElementById('advanced_recon_root');
var scope = root ? root.parentElement || document : document;
var buttons = scope.querySelectorAll('button');
var clicked = false;
buttons.forEach(function (btn) {
if (clicked) {
return;
}
var text = (btn.textContent || '').trim();
if (!text) {
return;
}
for (var i = 0; i < labels.length; i++) {
if (text.indexOf(labels[i]) !== -1) {
btn.click();
clicked = true;
break;
}
}
});
return clicked;
}
function formatViewportPlan(state, idx) {
if (!state) {
return '-';
}
var modes = Array.isArray(state.modes) ? state.modes : [];
var orientations = Array.isArray(state.orientations) ? state.orientations : [];
var slices = Array.isArray(state.volumeSliceIndices) ? state.volumeSliceIndices : [];
var mode = modes[idx] || 'unknown';
var orientation = getDisplayOrientation(orientations[idx]);
var slice = slices[idx];
var sliceText = typeof slice === 'number' ? ('slice ' + slice) : 'slice n/a';
return orientation + ' (' + mode + ', ' + sliceText + ')';
}
function updateLivePlan() {
var exportFn = getViewerApi('exportViewerState');
var thicknessInput = document.getElementById('adv-recon-thickness');
var spacingInput = document.getElementById('adv-recon-spacing');
var state = parseViewerState(typeof exportFn === 'function' ? exportFn() : null);
var statusEl = document.getElementById('advanced-plan-status');
var vp0 = document.getElementById('advanced-plan-vp0');
var vp1 = document.getElementById('advanced-plan-vp1');
var vp2 = document.getElementById('advanced-plan-vp2');
var thicknessEl = document.getElementById('advanced-plan-thickness');
var spacingEl = document.getElementById('advanced-plan-spacing');
if (statusEl) {
statusEl.textContent = state ? 'Viewer telemetry active' : 'Waiting for viewer telemetry...';
}
if (vp0) {
vp0.textContent = formatViewportPlan(state, 0);
}
if (vp1) {
vp1.textContent = formatViewportPlan(state, 1);
}
if (vp2) {
vp2.textContent = formatViewportPlan(state, 2);
}
if (thicknessEl) {
var thicknessValue = thicknessInput && thicknessInput.value ? thicknessInput.value : '3.0';
thicknessEl.textContent = thicknessValue + ' mm';
}
if (spacingEl) {
var spacingValue = spacingInput && spacingInput.value ? spacingInput.value : '3.0';
spacingEl.textContent = spacingValue + ' mm';
}
}
var applyBtn = document.getElementById('apply-mpr-layout');
if (applyBtn) {
applyBtn.addEventListener('click', function () {
var importFn = getViewerApi('importViewerState');
if (typeof importFn === 'function') {
try {
importFn(buildMprState());
} catch (err) {
console.warn('Failed to apply MPR layout', err);
}
}
ensureMprLayout();
setTimeout(updateLivePlan, 400);
});
}
@@ -150,6 +291,30 @@
}
});
}
var thicknessInput = document.getElementById('adv-recon-thickness');
var spacingInput = document.getElementById('adv-recon-spacing');
if (thicknessInput) {
thicknessInput.addEventListener('input', updateLivePlan);
}
if (spacingInput) {
spacingInput.addEventListener('input', updateLivePlan);
}
var autoAttempts = 0;
var autoTimer = setInterval(function () {
autoAttempts += 1;
var mprApplied = ensureMprLayout();
var crosshairEnabled = tryEnableCrosshairs();
updateLivePlan();
if ((mprApplied && crosshairEnabled) || autoAttempts >= 12) {
clearInterval(autoTimer);
}
}, 600);
setInterval(updateLivePlan, 700);
setTimeout(updateLivePlan, 1500);
})();
</script>
{% endblock %}