Skip to content

Commit 4c70c1f

Browse files
committed
OHIF v3 viewer to display proper segmentation regions after switching to different series and run monailabel
Signed-off-by: Joaquin Anton Guirao <[email protected]>
1 parent b652ca7 commit 4c70c1f

File tree

5 files changed

+216
-52
lines changed

5 files changed

+216
-52
lines changed

plugins/ohifv3/build.sh

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,23 @@
1414
curr_dir="$(pwd)"
1515
my_dir="$(dirname "$(readlink -f "$0")")"
1616

17+
# Load nvm and ensure Node.js 18 is available
18+
export NVM_DIR="$HOME/.nvm"
19+
if [ -s "$NVM_DIR/nvm.sh" ]; then
20+
echo "Loading nvm..."
21+
. "$NVM_DIR/nvm.sh"
22+
nvm use 18 2>/dev/null || nvm install 18
23+
echo "Using Node.js $(node --version)"
24+
else
25+
echo "WARNING: nvm not found. Checking Node.js version..."
26+
NODE_VERSION=$(node --version 2>/dev/null | cut -d'v' -f2 | cut -d'.' -f1)
27+
if [ -z "$NODE_VERSION" ] || [ "$NODE_VERSION" -lt 18 ]; then
28+
echo "ERROR: Node.js >= 18 is required. Current version: $(node --version 2>/dev/null || echo 'not installed')"
29+
echo "Please install Node.js 18 or higher, or install nvm."
30+
exit 1
31+
fi
32+
fi
33+
1734
echo "Installing requirements..."
1835
sh $my_dir/requirements.sh
1936

plugins/ohifv3/extensions/monai-label/src/components/MonaiLabelPanel.tsx

Lines changed: 195 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ export default class MonaiLabelPanel extends Component {
6262
info: { models: [], datasets: [] },
6363
action: {},
6464
options: {},
65+
segmentationSeriesUID: null, // Track which series the segmentation belongs to
6566
};
6667
}
6768

@@ -214,7 +215,7 @@ export default class MonaiLabelPanel extends Component {
214215

215216
// Wait for Above Segmentations to be added/available
216217
setTimeout(() => {
217-
const { viewport } = this.getActiveViewportInfo();
218+
const { viewport, displaySet } = this.getActiveViewportInfo();
218219
for (const segmentIndex of Object.keys(initialSegs)) {
219220
cornerstoneTools.segmentation.config.color.setSegmentIndexColor(
220221
viewport.viewportId,
@@ -223,6 +224,8 @@ export default class MonaiLabelPanel extends Component {
223224
initialSegs[segmentIndex].color
224225
);
225226
}
227+
// Store the series UID for the initial segmentation
228+
this.setState({ segmentationSeriesUID: displaySet?.SeriesInstanceUID });
226229
}, 1000);
227230
}
228231

@@ -268,7 +271,8 @@ export default class MonaiLabelPanel extends Component {
268271
labels,
269272
override = false,
270273
label_class_unknown = false,
271-
sidx = -1
274+
sidx = -1,
275+
inferenceSeriesUID = null
272276
) => {
273277
console.log('UpdateView: ', {
274278
model_id,
@@ -314,63 +318,205 @@ export default class MonaiLabelPanel extends Component {
314318
console.log('Index Remap', labels, modelToSegMapping);
315319
const data = new Uint8Array(ret.image);
316320

317-
const { segmentationService } = this.props.servicesManager.services;
318-
const volumeLoadObject = segmentationService.getLabelmapVolume('1');
321+
const { segmentationService, viewportGridService } = this.props.servicesManager.services;
322+
let volumeLoadObject = segmentationService.getLabelmapVolume('1');
323+
const { displaySet } = this.getActiveViewportInfo();
324+
const currentSeriesUID = displaySet?.SeriesInstanceUID;
325+
326+
// If inferenceSeriesUID is not provided, assume it's for the current series
327+
if (!inferenceSeriesUID) {
328+
inferenceSeriesUID = currentSeriesUID;
329+
}
330+
331+
// Validate inference was run on the current series
332+
if (currentSeriesUID !== inferenceSeriesUID) {
333+
this.notification.show({
334+
title: 'MONAI Label - Series Mismatch',
335+
message: 'Please run inference on the current series',
336+
type: 'error',
337+
duration: 5000,
338+
});
339+
return;
340+
}
341+
342+
// Check if we have a stored series UID for the existing segmentation
343+
const storedSeriesUID = this.state.segmentationSeriesUID;
344+
319345
if (volumeLoadObject) {
320-
// console.log('Volume Object is In Cache....');
321-
let convertedData = data;
322-
for (let i = 0; i < convertedData.length; i++) {
323-
const midx = convertedData[i];
324-
const sidx = modelToSegMapping[midx];
325-
if (midx && sidx) {
326-
convertedData[i] = sidx;
327-
} else if (override && label_class_unknown && labels.length === 1) {
328-
convertedData[i] = midx ? labelNames[labels[0]] : 0;
329-
} else if (labels.length > 0) {
330-
convertedData[i] = 0;
346+
const { voxelManager } = volumeLoadObject;
347+
const existingData = voxelManager?.getCompleteScalarDataArray();
348+
const dimensionsMatch = existingData?.length === data.length;
349+
const seriesMatch = storedSeriesUID === currentSeriesUID;
350+
351+
// If series don't match OR dimensions don't match, this is a different series - need to recreate segmentation
352+
// BUT: if storedSeriesUID is null, this is the first inference, so don't recreate
353+
if (storedSeriesUID !== null && (!seriesMatch || !dimensionsMatch)) {
354+
// Remove the old segmentation
355+
try {
356+
segmentationService.remove('1');
357+
this.setState({ segmentationSeriesUID: null });
358+
} catch (e) {
359+
return;
331360
}
361+
362+
// Create a new segmentation for the current series
363+
if (!this.state.info || !this.state.info.initialSegs) {
364+
return;
365+
}
366+
367+
const segmentations = [
368+
{
369+
segmentationId: '1',
370+
representation: {
371+
type: Enums.SegmentationRepresentations.Labelmap,
372+
},
373+
config: {
374+
label: 'Segmentations',
375+
segments: this.state.info.initialSegs,
376+
},
377+
},
378+
];
379+
380+
this.props.commandsManager.runCommand('loadSegmentationsForViewport', {
381+
segmentations,
382+
});
383+
384+
const responseData = response.data;
385+
setTimeout(() => {
386+
const { viewport } = this.getActiveViewportInfo();
387+
const initialSegs = this.state.info.initialSegs;
388+
389+
for (const segmentIndex of Object.keys(initialSegs)) {
390+
cornerstoneTools.segmentation.config.color.setSegmentIndexColor(
391+
viewport.viewportId,
392+
'1',
393+
initialSegs[segmentIndex].segmentIndex,
394+
initialSegs[segmentIndex].color
395+
);
396+
}
397+
398+
// Recursively call updateView to populate the newly created segmentation
399+
this.updateView(
400+
{ data: responseData },
401+
model_id,
402+
labels,
403+
override,
404+
label_class_unknown,
405+
sidx,
406+
currentSeriesUID
407+
);
408+
}, 1000);
409+
return;
332410
}
333-
334-
if (override === true) {
335-
const { segmentationService } = this.props.servicesManager.services;
336-
const volumeLoadObject = segmentationService.getLabelmapVolume('1');
337-
const { voxelManager } = volumeLoadObject;
338-
const scalarData = voxelManager?.getCompleteScalarDataArray();
339-
340-
// console.log('Current ScalarData: ', scalarData);
341-
const currentSegArray = new Uint8Array(scalarData.length);
342-
currentSegArray.set(scalarData);
343-
344-
// get unique values to determine which organs to update, keep rest
345-
const updateTargets = new Set(convertedData);
346-
const numImageFrames =
347-
this.getActiveViewportInfo().displaySet.numImageFrames;
348-
const sliceLength = scalarData.length / numImageFrames;
349-
const sliceBegin = sliceLength * sidx;
350-
const sliceEnd = sliceBegin + sliceLength;
351-
411+
412+
if (volumeLoadObject) {
413+
// console.log('Volume Object is In Cache....');
414+
let convertedData = data;
352415
for (let i = 0; i < convertedData.length; i++) {
353-
if (sidx >= 0 && (i < sliceBegin || i >= sliceEnd)) {
354-
continue;
416+
const midx = convertedData[i];
417+
const sidx = modelToSegMapping[midx];
418+
if (midx && sidx) {
419+
convertedData[i] = sidx;
420+
} else if (override && label_class_unknown && labels.length === 1) {
421+
convertedData[i] = midx ? labelNames[labels[0]] : 0;
422+
} else if (labels.length > 0) {
423+
convertedData[i] = 0;
355424
}
425+
}
356426

357-
if (
358-
convertedData[i] !== 255 &&
359-
updateTargets.has(currentSegArray[i])
360-
) {
361-
currentSegArray[i] = convertedData[i];
427+
if (override === true) {
428+
const { segmentationService } = this.props.servicesManager.services;
429+
const volumeLoadObject = segmentationService.getLabelmapVolume('1');
430+
const { voxelManager } = volumeLoadObject;
431+
const scalarData = voxelManager?.getCompleteScalarDataArray();
432+
433+
// console.log('Current ScalarData: ', scalarData);
434+
const currentSegArray = new Uint8Array(scalarData.length);
435+
currentSegArray.set(scalarData);
436+
437+
// get unique values to determine which organs to update, keep rest
438+
const updateTargets = new Set(convertedData);
439+
const numImageFrames =
440+
this.getActiveViewportInfo().displaySet.numImageFrames;
441+
const sliceLength = scalarData.length / numImageFrames;
442+
const sliceBegin = sliceLength * sidx;
443+
const sliceEnd = sliceBegin + sliceLength;
444+
445+
for (let i = 0; i < convertedData.length; i++) {
446+
if (sidx >= 0 && (i < sliceBegin || i >= sliceEnd)) {
447+
continue;
448+
}
449+
450+
if (
451+
convertedData[i] !== 255 &&
452+
updateTargets.has(currentSegArray[i])
453+
) {
454+
currentSegArray[i] = convertedData[i];
455+
}
362456
}
457+
convertedData = currentSegArray;
363458
}
364-
convertedData = currentSegArray;
459+
// voxelManager already declared above
460+
voxelManager?.setCompleteScalarDataArray(convertedData);
461+
triggerEvent(eventTarget, Enums.Events.SEGMENTATION_DATA_MODIFIED, {
462+
segmentationId: '1',
463+
});
464+
console.log("updated the segmentation's scalar data");
465+
466+
// Store the series UID for this segmentation
467+
this.setState({ segmentationSeriesUID: currentSeriesUID });
365468
}
366-
const { voxelManager } = volumeLoadObject;
367-
voxelManager?.setCompleteScalarDataArray(convertedData);
368-
triggerEvent(eventTarget, Enums.Events.SEGMENTATION_DATA_MODIFIED, {
369-
segmentationId: '1',
370-
});
371-
console.log("updated the segmentation's scalar data");
372469
} else {
373-
console.log('TODO:: Volume Object is NOT In Cache....');
470+
// Create new segmentation
471+
if (!this.state.info || !this.state.info.initialSegs) {
472+
return;
473+
}
474+
475+
const segmentations = [
476+
{
477+
segmentationId: '1',
478+
representation: {
479+
type: Enums.SegmentationRepresentations.Labelmap,
480+
},
481+
config: {
482+
label: 'Segmentations',
483+
segments: this.state.info.initialSegs,
484+
},
485+
},
486+
];
487+
488+
// Create the segmentation for this viewport
489+
this.props.commandsManager.runCommand('loadSegmentationsForViewport', {
490+
segmentations,
491+
});
492+
493+
// Wait for segmentation to be created, then populate it with inference data
494+
const responseData = response.data;
495+
setTimeout(() => {
496+
const { viewport } = this.getActiveViewportInfo();
497+
const initialSegs = this.state.info.initialSegs;
498+
499+
// Set colors
500+
for (const segmentIndex of Object.keys(initialSegs)) {
501+
cornerstoneTools.segmentation.config.color.setSegmentIndexColor(
502+
viewport.viewportId,
503+
'1',
504+
initialSegs[segmentIndex].segmentIndex,
505+
initialSegs[segmentIndex].color
506+
);
507+
}
508+
509+
// Recursively call updateView to populate the newly created segmentation
510+
this.updateView(
511+
{ data: responseData },
512+
model_id,
513+
labels,
514+
override,
515+
label_class_unknown,
516+
sidx,
517+
currentSeriesUID // Pass the series UID
518+
);
519+
}, 1000);
374520
}
375521
};
376522

plugins/ohifv3/extensions/monai-label/src/components/actions/AutoSegmentation.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ export default class AutoSegmentation extends BaseTab {
122122
duration: 4000,
123123
});
124124

125-
this.props.updateView(response, model, label_names);
125+
this.props.updateView(response, model, label_names, false, false, -1, displaySet.SeriesInstanceUID);
126126
};
127127

128128
render() {

plugins/ohifv3/extensions/monai-label/src/components/actions/ClassPrompts.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ export default class ClassPrompts extends BaseTab {
148148
duration: 4000,
149149
});
150150

151-
this.props.updateView(response, model, label_names, true);
151+
this.props.updateView(response, model, label_names, true, false, -1, displaySet.SeriesInstanceUID);
152152
};
153153

154154
segColorToRgb(s) {

plugins/ohifv3/extensions/monai-label/src/components/actions/PointPrompts.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,8 @@ export default class PointPrompts extends BaseTab {
195195
label_names,
196196
true,
197197
label_class_unknown,
198-
sidx
198+
sidx,
199+
displaySet.SeriesInstanceUID
199200
);
200201
};
201202

0 commit comments

Comments
 (0)