try to fix recons
This commit is contained in:
+37
-15
@@ -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 %}
|
||||
|
||||
Reference in New Issue
Block a user