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_y = source_for_patient_axis[1]
|
||||||
src_z = source_for_patient_axis[2]
|
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_x = source_axis_spacings[src_x]
|
||||||
spacing_y = source_axis_spacings[src_y]
|
spacing_y = source_axis_spacings[src_y]
|
||||||
spacing_z = source_axis_spacings[src_z]
|
spacing_z = source_axis_spacings[src_z]
|
||||||
@@ -350,7 +376,7 @@ def series_reconstruct_task(
|
|||||||
vol_zyx = np.flip(vol_zyx, axis=2)
|
vol_zyx = np.flip(vol_zyx, axis=2)
|
||||||
|
|
||||||
def source_indices_from_patient_zyx(iz, iy, ix):
|
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)
|
z_index = float(iz)
|
||||||
y_index = float(iy)
|
y_index = float(iy)
|
||||||
@@ -378,10 +404,6 @@ def series_reconstruct_task(
|
|||||||
)
|
)
|
||||||
return [float(pos[0]), float(pos[1]), float(pos[2])]
|
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):
|
def _reduce_slab(slab, reduce_axis=0):
|
||||||
if recon_thickness_mode == "max":
|
if recon_thickness_mode == "max":
|
||||||
out = np.max(slab, axis=reduce_axis)
|
out = np.max(slab, axis=reduce_axis)
|
||||||
@@ -440,8 +462,8 @@ def series_reconstruct_task(
|
|||||||
"pixel_spacing_out": [float(spacing_y), float(spacing_x)],
|
"pixel_spacing_out": [float(spacing_y), float(spacing_x)],
|
||||||
"spacing_between_slices_out": float(plane_spacing),
|
"spacing_between_slices_out": float(plane_spacing),
|
||||||
"slice_thickness_out": float(plane_thickness),
|
"slice_thickness_out": float(plane_thickness),
|
||||||
"image_orientation_out": [1.0, 0.0, 0.0, 0.0, 1.0, 0.0],
|
"image_orientation_out": _orientation_pair(patient_x_vec, patient_y_vec, patient_z_vec),
|
||||||
"plane_normal_out": [0.0, 0.0, 1.0],
|
"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),
|
"position_fn": lambda idx, centers=slab_centers_idx: position_from_patient_zyx(centers[idx], 0.0, 0.0),
|
||||||
}
|
}
|
||||||
elif plane_norm == "coronal":
|
elif plane_norm == "coronal":
|
||||||
@@ -452,13 +474,13 @@ def series_reconstruct_task(
|
|||||||
)
|
)
|
||||||
plane_builders[plane_norm] = {
|
plane_builders[plane_norm] = {
|
||||||
"count": int(len(slab_indices)),
|
"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)],
|
"pixel_spacing_out": [float(spacing_z), float(spacing_x)],
|
||||||
"spacing_between_slices_out": float(plane_spacing),
|
"spacing_between_slices_out": float(plane_spacing),
|
||||||
"slice_thickness_out": float(plane_thickness),
|
"slice_thickness_out": float(plane_thickness),
|
||||||
"image_orientation_out": [1.0, 0.0, 0.0, 0.0, 0.0, -1.0],
|
"image_orientation_out": _orientation_pair(patient_x_vec, patient_z_vec, patient_y_vec),
|
||||||
"plane_normal_out": [0.0, 1.0, 0.0],
|
"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(float(vol_zyx.shape[0] - 1), centers[idx], 0.0),
|
"position_fn": lambda idx, centers=slab_centers_idx: position_from_patient_zyx(0.0, centers[idx], 0.0),
|
||||||
}
|
}
|
||||||
elif plane_norm == "sagittal":
|
elif plane_norm == "sagittal":
|
||||||
plane_spacing = float(requested_slice_spacing) if requested_slice_spacing is not None else float(spacing_x)
|
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] = {
|
plane_builders[plane_norm] = {
|
||||||
"count": int(len(slab_indices)),
|
"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)],
|
"pixel_spacing_out": [float(spacing_z), float(spacing_y)],
|
||||||
"spacing_between_slices_out": float(plane_spacing),
|
"spacing_between_slices_out": float(plane_spacing),
|
||||||
"slice_thickness_out": float(plane_thickness),
|
"slice_thickness_out": float(plane_thickness),
|
||||||
"image_orientation_out": [0.0, 1.0, 0.0, 0.0, 0.0, -1.0],
|
"image_orientation_out": _orientation_pair(patient_y_vec, patient_z_vec, patient_x_vec),
|
||||||
"plane_normal_out": [1.0, 0.0, 0.0],
|
"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(float(vol_zyx.shape[0] - 1), 0.0, centers[idx]),
|
"position_fn": lambda idx, centers=slab_centers_idx: position_from_patient_zyx(0.0, 0.0, centers[idx]),
|
||||||
}
|
}
|
||||||
|
|
||||||
if not plane_builders:
|
if not plane_builders:
|
||||||
|
|||||||
@@ -33,7 +33,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="alert alert-info recon-advanced-help">
|
<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>
|
||||||
|
|
||||||
<div id="advanced_recon_root" class="dicom-viewer-root recon-advanced-viewer" data-images='{{ image_ids_json|safe }}'></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">
|
<div class="col-12 col-lg-4">
|
||||||
<label class="form-label" for="adv-recon-thickness">Slab thickness (mm)</label>
|
<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>
|
<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>
|
||||||
|
|
||||||
<div class="col-12 col-lg-4">
|
<div class="col-12 col-lg-4">
|
||||||
@@ -99,36 +100,176 @@
|
|||||||
</div>
|
</div>
|
||||||
</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 id="advanced-recon-feedback" class="small mt-3"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="{% static 'dv3d/index.js' %}" type="module" defer="defer"></script>
|
<script src="{% static 'dv3d/index.js' %}" type="module" defer="defer"></script>
|
||||||
<script>
|
<script>
|
||||||
(function () {
|
(function () {
|
||||||
|
var ORIENTATION_LABELS = {
|
||||||
|
axial: 'Axial',
|
||||||
|
coronal: 'Coronal',
|
||||||
|
sagittal: 'Sagittal',
|
||||||
|
};
|
||||||
|
|
||||||
function getViewerApi(name) {
|
function getViewerApi(name) {
|
||||||
return window[name + '_advanced_recon_root'] || window[name + '_advanced_recon'] || window[name + '_root'] || window[name];
|
return window[name + '_advanced_recon_root'] || window[name + '_advanced_recon'] || window[name + '_root'] || window[name];
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildMprState() {
|
function parseViewerState(raw) {
|
||||||
return {
|
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 },
|
grid: { rows: 1, cols: 3 },
|
||||||
modes: ['volume', 'volume', 'volume'],
|
modes: ['volume', 'volume', 'volume'],
|
||||||
orientations: ['axial', 'coronal', 'sagittal'],
|
orientations: ['axial', 'coronal', 'sagittal'],
|
||||||
volumeSliceIndices: [null, null, null],
|
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');
|
var applyBtn = document.getElementById('apply-mpr-layout');
|
||||||
if (applyBtn) {
|
if (applyBtn) {
|
||||||
applyBtn.addEventListener('click', function () {
|
applyBtn.addEventListener('click', function () {
|
||||||
var importFn = getViewerApi('importViewerState');
|
ensureMprLayout();
|
||||||
if (typeof importFn === 'function') {
|
setTimeout(updateLivePlan, 400);
|
||||||
try {
|
|
||||||
importFn(buildMprState());
|
|
||||||
} catch (err) {
|
|
||||||
console.warn('Failed to apply MPR layout', err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
Reference in New Issue
Block a user