req custom overlay: add MRI custom overlay using data from additional dicom query

This commit is contained in:
AlfandiMario
2025-11-03 16:01:02 +07:00
parent 1702191d18
commit 62b821df78
2 changed files with 327 additions and 25 deletions

View File

@@ -9,7 +9,7 @@ import { formatPN, formatDICOMDate, formatDICOMTime, formatNumberPrecision } fro
import { StackViewportData, VolumeViewportData } from '../../types/CornerstoneCacheService'; import { StackViewportData, VolumeViewportData } from '../../types/CornerstoneCacheService';
import './CustomizableViewportOverlay.css'; import './CustomizableViewportOverlay.css';
import { studyDataForOverlayItem } from './studyDataForOverlayItem'; import { studyDataForOverlayItem, instanceDataForMriOverlayItem } from './studyDataForOverlayItem';
const EPSILON = 1e-4; const EPSILON = 1e-4;
@@ -58,10 +58,10 @@ const studyDateItem = {
title: 'Study date and time', title: 'Study date and time',
condition: ({ referenceInstance, studyApiData }) => condition: ({ referenceInstance, studyApiData }) =>
referenceInstance?.StudyDate || studyApiData?.studyTime, referenceInstance?.StudyDate || studyApiData?.studyTime,
contentF: ({ referenceInstance, formatters: { formatDate }, studyApiData }) => { contentF: ({ referenceInstance, formatters: { formatDate, formatTime }, studyApiData }) => {
const date = referenceInstance?.StudyDate ? formatDate(referenceInstance.StudyDate) : ''; const date = referenceInstance?.StudyDate ? formatDate(referenceInstance.StudyDate) : '';
const time = studyApiData?.studyTime ? studyApiData.studyTime : ''; const time = studyApiData?.studyTime ? formatTime(studyApiData.studyTime) : '';
return `${date} ${time}`; return `${date} ${time}`.trim();
}, },
}; };
@@ -125,12 +125,184 @@ const accessionNumberItem = {
contentF: ({ studyApiData }) => studyApiData.accessionNumber, contentF: ({ studyApiData }) => studyApiData.accessionNumber,
}; };
// Tambahan untuk Overlay MRI di Top Left
const sliceThicknessItem = {
id: 'SliceThickness',
customizationType: 'ohif.overlayItem',
label: 'Slice Thickness: ',
title: 'Slice Thickness',
condition: ({ mriInstanceData }) => mriInstanceData?.sliceThickness,
contentF: ({ mriInstanceData }) => `${mriInstanceData.sliceThickness} mm`,
};
const sliceLocationItem = {
id: 'SliceLocation',
customizationType: 'ohif.overlayItem',
label: 'Slice Loc: ',
title: 'Slice Location',
condition: ({ mriInstanceData }) => mriInstanceData?.sliceLocation,
contentF: ({ mriInstanceData }) => `${mriInstanceData.sliceLocation} mm`,
};
const sliceSpacingItem = {
id: 'SliceSpacing',
customizationType: 'ohif.overlayItem',
label: 'Slice Spacing: ',
title: 'Slice Spacing',
condition: ({ mriInstanceData }) => mriInstanceData?.spacingBetweenSlices,
contentF: ({ mriInstanceData }) => `${mriInstanceData.spacingBetweenSlices} mm`,
};
const phaseFieldOfViewItem = {
id: 'PhaseFOV',
customizationType: 'ohif.overlayItem',
label: '% Phase FOV: ',
title: '% Phase FOV',
condition: ({ mriInstanceData }) => mriInstanceData?.percentPhaseFieldOfView,
contentF: ({ mriInstanceData }) => `${mriInstanceData.percentPhaseFieldOfView} %`,
};
const acquisitionMatrixItem = {
id: 'AcquisitionMatrix',
customizationType: 'ohif.overlayItem',
label: 'Acq Matrix: ',
title: 'Acquisition Matrix',
condition: ({ mriInstanceData }) => mriInstanceData?.acquisitionMatrix,
contentF: ({ mriInstanceData }) => `${mriInstanceData.acquisitionMatrix.join(' x ')}`,
};
const mriTopLeftItems = {
id: 'cornerstoneOverlayTopLeft',
items: [
sliceThicknessItem,
sliceLocationItem,
sliceSpacingItem,
phaseFieldOfViewItem,
acquisitionMatrixItem,
],
};
// Tambahan bottom left overlay MRI
const scanningSequenceItem = {
id: 'MRIScanningSequence',
customizationType: 'ohif.overlayItem',
label: 'Scanning Seq: ',
title: 'MRI Scanning Sequence',
condition: ({ mriInstanceData }) => mriInstanceData?.scanningSequence,
contentF: ({ mriInstanceData }) => `${mriInstanceData.scanningSequence} `,
};
const repetitionEchoInversionTimesItem = {
id: 'MRIRepetitionEchoInversionTimes',
customizationType: 'ohif.overlayItem',
label: 'TR/TE/TI: ',
title: 'MRI Repetition/Echo/Inversion Times',
condition: ({ mriInstanceData }) =>
mriInstanceData?.repetitionTime || mriInstanceData?.echoTime || mriInstanceData?.inversionTime,
contentF: ({ mriInstanceData }) => {
const tr = mriInstanceData?.repetitionTime || '-';
const te = mriInstanceData?.echoTime || '-';
const ti = mriInstanceData?.inversionTime || '-';
return `${tr}/${te}/${ti} ms`.replace(/\/+/g, ' / ').trim(); // Clean up extra slashes
},
};
const receiverCoilItem = {
id: 'MRIReceiverCoil',
customizationType: 'ohif.overlayItem',
label: 'Receiver Coil: ',
title: 'MRI Receiver Coil',
condition: ({ mriInstanceData }) => mriInstanceData?.receiveCoilName,
contentF: ({ mriInstanceData }) => `${mriInstanceData.receiveCoilName} `,
};
const mriScanModeItem = {
id: 'MRIScanMode',
customizationType: 'ohif.overlayItem',
label: 'Scan Mode: ',
title: 'MRI Scan Mode',
condition: ({ mriInstanceData }) => mriInstanceData?.mrAcquisitionType,
contentF: ({ mriInstanceData }) => `${mriInstanceData.mrAcquisitionType} `,
};
const phaseEncodingDirectionItem = {
id: 'MRIPhaseEncodingDirection',
customizationType: 'ohif.overlayItem',
label: 'Phase Enc Direction: ',
title: 'MRI Phase Encoding Direction',
condition: ({ mriInstanceData }) => mriInstanceData?.phaseEncodingDirection,
contentF: ({ mriInstanceData }) => `${mriInstanceData.phaseEncodingDirection} `,
};
const echoTrainLengthItem = {
id: 'MRIEchoTrainLength',
customizationType: 'ohif.overlayItem',
label: 'ETL: ',
title: 'MRI Echo Train Length',
condition: ({ mriInstanceData }) => mriInstanceData?.echoTrainLength,
contentF: ({ mriInstanceData }) => `${mriInstanceData.echoTrainLength} `,
};
const flipAngleItem = {
id: 'MRIFlipAngle',
customizationType: 'ohif.overlayItem',
label: 'Flip Angle: ',
title: 'MRI Flip Angle',
condition: ({ mriInstanceData }) => mriInstanceData?.flipAngle,
contentF: ({ mriInstanceData }) => `${mriInstanceData.flipAngle} °`,
};
const pixelBandwidthItem = {
id: 'MRIPixelBandwidth',
customizationType: 'ohif.overlayItem',
label: 'Pixel Bandwidth: ',
title: 'MRI Pixel Bandwidth',
condition: ({ mriInstanceData }) => mriInstanceData?.pixelBandwidth,
contentF: ({ mriInstanceData }) => `${mriInstanceData.pixelBandwidth} Hz`,
};
const acquisitionTimeItem = {
id: 'MRIAcquisitionTime',
customizationType: 'ohif.overlayItem',
label: 'Acq Time: ',
title: 'MRI Acquisition Time',
condition: ({ mriInstanceData }) => mriInstanceData?.acquisitionTime,
contentF: ({ referenceInstance, formatters: { formatTime }, mriInstanceData }) => {
const time = mriInstanceData?.acquisitionTime
? formatTime(mriInstanceData.acquisitionTime)
: '';
return `${time}`.trim();
},
};
const parallelImagingItem = {
id: 'MRIParallelImaging',
customizationType: 'ohif.overlayItem',
label: 'Parallel Img: ',
title: 'MRI Parallel Imaging',
condition: ({ mriInstanceData }) => mriInstanceData?.parallelAcquisitionTechnique,
contentF: ({ mriInstanceData }) => `${mriInstanceData.parallelAcquisitionTechnique} `,
};
const mriBottomLeftItems = {
id: 'cornerstoneOverlayBottomLeft',
items: [
scanningSequenceItem,
repetitionEchoInversionTimesItem,
receiverCoilItem,
mriScanModeItem,
phaseEncodingDirectionItem,
echoTrainLengthItem,
flipAngleItem,
pixelBandwidthItem,
acquisitionTimeItem,
parallelImagingItem,
],
};
const topLeftItems = { const topLeftItems = {
id: 'cornerstoneOverlayTopLeft', id: 'cornerstoneOverlayTopLeft',
items: [patientNameItem, mrnItem, sexAndAgeItem], items: [patientNameItem, mrnItem, sexAndAgeItem],
}; };
const topRightItems = { id: 'cornerstoneOverlayTopRight', items: [] }; const topRightItems = {
id: 'cornerstoneOverlayTopRight',
items: [
patientNameItem,
mrnItem,
sexAndAgeItem,
accessionNumberItem,
studyDescriptionItem,
studyDateItem,
],
};
const bottomLeftItems = { const bottomLeftItems = {
id: 'cornerstoneOverlayBottomLeft', id: 'cornerstoneOverlayBottomLeft',
@@ -160,9 +332,9 @@ const bottomRightItems = {
*/ */
const CornerstoneOverlay = { const CornerstoneOverlay = {
id: '@ohif/cornerstoneOverlay', id: '@ohif/cornerstoneOverlay',
topLeftItems, mriTopLeftItems,
topRightItems, topRightItems,
bottomLeftItems, mriBottomLeftItems,
bottomRightItems, bottomRightItems,
}; };
@@ -192,23 +364,6 @@ function CustomizableViewportOverlay({
// on the individual items rather than defining individual items. // on the individual items rather than defining individual items.
const cornerstoneOverlay = customizationService.getCustomization('@ohif/cornerstoneOverlay'); const cornerstoneOverlay = customizationService.getCustomization('@ohif/cornerstoneOverlay');
// Historical usage defined the overlays as separate items due to lack of
// append functionality. This code enables the historical usage, but
// the recommended functionality is to append to the default values in
// cornerstoneOverlay rather than defining individual items.
const topLeftCustomization =
customizationService.getCustomization('cornerstoneOverlayTopLeft') ||
cornerstoneOverlay?.topLeftItems;
const topRightCustomization =
customizationService.getCustomization('cornerstoneOverlayTopRight') ||
cornerstoneOverlay?.topRightItems;
const bottomLeftCustomization =
customizationService.getCustomization('cornerstoneOverlayBottomLeft') ||
cornerstoneOverlay?.bottomLeftItems;
const bottomRightCustomization =
customizationService.getCustomization('cornerstoneOverlayBottomRight') ||
cornerstoneOverlay?.bottomRightItems;
const instanceNumber = useMemo( const instanceNumber = useMemo(
() => () =>
viewportData viewportData
@@ -236,8 +391,41 @@ function CustomizableViewportOverlay({
// FEAT: Edit overlays item [2025-09-04] // FEAT: Edit overlays item [2025-09-04]
const studyInstanceUID = displaySetProps?.referenceInstance?.StudyInstanceUID; const studyInstanceUID = displaySetProps?.referenceInstance?.StudyInstanceUID;
const seriesInstanceUID = displaySetProps?.referenceInstance?.SeriesInstanceUID;
const sopInstanceUID = displaySetProps?.instance?.SOPInstanceUID;
const { studyData: studyApiData, loading, error } = studyDataForOverlayItem(studyInstanceUID); const { studyData: studyApiData, loading, error } = studyDataForOverlayItem(studyInstanceUID);
console.debug('Reference Instance:', displaySetProps?.referenceInstance);
console.debug('Instance:', displaySetProps?.instance);
// Modality untuk menentukan kustomisasi overlay
const modality = displaySetProps?.referenceInstance?.Modality;
const { instanceData: mriInstanceData } = instanceDataForMriOverlayItem(
modality === 'MR' ? studyInstanceUID : null,
modality === 'MR' ? seriesInstanceUID : null,
modality === 'MR' ? sopInstanceUID : null
);
// Historical usage defined the overlays as separate items due to lack of
// append functionality. This code enables the historical usage, but
// the recommended functionality is to append to the default values in
// cornerstoneOverlay rather than defining individual items.
const topLeftCustomization =
customizationService.getCustomization('cornerstoneOverlayTopLeft') ||
(modality === 'MR' ? cornerstoneOverlay?.mriTopLeftItems : cornerstoneOverlay?.topLeftItems);
const topRightCustomization =
customizationService.getCustomization('cornerstoneOverlayTopRight') ||
cornerstoneOverlay?.topRightItems;
const bottomLeftCustomization =
customizationService.getCustomization('cornerstoneOverlayBottomLeft') ||
(modality === 'MR'
? cornerstoneOverlay?.mriBottomLeftItems
: cornerstoneOverlay?.bottomLeftItems);
const bottomRightCustomization =
customizationService.getCustomization('cornerstoneOverlayBottomRight') ||
cornerstoneOverlay?.bottomRightItems;
/** /**
* Updating the VOI when the viewport changes its voi * Updating the VOI when the viewport changes its voi
*/ */
@@ -357,6 +545,7 @@ function CustomizableViewportOverlay({
viewportId, viewportId,
toolGroupService, toolGroupService,
studyApiData, // Pass the API data to the overlay items studyApiData, // Pass the API data to the overlay items
mriInstanceData,
}; };
return ( return (
@@ -370,7 +559,7 @@ function CustomizableViewportOverlay({
</> </>
); );
}, },
[_renderOverlayItem, displaySetProps, studyApiData] [_renderOverlayItem, displaySetProps, studyApiData, mriInstanceData]
); );
return ( return (

View File

@@ -143,3 +143,116 @@ export const studyDataForOverlayItem = (studyInstanceUID: string) => {
return { studyData, loading, error }; return { studyData, loading, error };
}; };
interface MriOverlayInstanceData {
sliceThickness?: string;
sliceLocation?: string;
spacingBetweenSlices?: string;
percentPhaseFieldOfView?: string;
acquisitionMatrix?: string;
scanningSequence?: string;
repetitionTime?: string;
echoTime?: string;
inversionTime?: string;
receiveCoilName?: string;
mrAcquisitionType?: string;
phaseEncodingDirection?: string;
echoTrainLength?: string;
flipAngle?: string;
pixelBandwidth?: string;
acquisitionTime?: string;
parallelAcquisitionTechnique?: string;
}
export const instanceDataForMriOverlayItem = (
studyInstanceUID: string,
seriesInstanceUID: string,
sopInstanceUID: string
) => {
const [instanceData, setInstanceData] = useState<MriOverlayInstanceData | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
useEffect(() => {
if (!studyInstanceUID || !seriesInstanceUID || !sopInstanceUID) return;
const fetchInstanceData = async () => {
setLoading(true);
try {
const qidoRootUrl = getQidoRootUrl();
if (!qidoRootUrl) {
throw new Error('QIDO root URL not configured');
}
// MRI tags: SliceThickness(00180050), SpacingBetweenSlices(00180088),
// PercentPhaseFieldOfView(00180094), AcquisitionMatrix(00181310),
// ScanningSequence(00180020), RepetitionTime(00180080), EchoTime(00180081),
// InversionTime(00180082), ReceiveCoilName(00181250), MRAcquisitionType(00180023),
// InPlanePhaseEncodingDirection(00181312), EchoTrainLength(00180091),
// FlipAngle(00181314), PixelBandwidth(00180095), AcquisitionTime(00080032),
// ParallelAcquisitionTechnique(00181316)
const includeFields = [
'00180050',
'00180088',
'00180094',
'00181310',
'00180020',
'00180080',
'00180081',
'00180082',
'00181250',
'00180023',
'00181312',
'00180091',
'00181314',
'00180095',
'00080032',
'00181316',
].join(',');
const url = `${qidoRootUrl}/studies/${studyInstanceUID}/series/${seriesInstanceUID}/instances/${sopInstanceUID}/metadata`;
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Failed to fetch instance data: ${response.status}`);
}
const data = await response.json();
if (data && data.length > 0) {
const instance = data[0];
const extractedData: MriOverlayInstanceData = {
sliceThickness: instance['00180050']?.Value?.[0],
sliceLocation: instance['00201041']?.Value?.[0],
spacingBetweenSlices: instance['00180088']?.Value?.[0],
percentPhaseFieldOfView: instance['00180094']?.Value?.[0],
acquisitionMatrix: instance['00181310']?.Value,
scanningSequence: instance['00180020']?.Value?.[0],
repetitionTime: instance['00180080']?.Value?.[0],
echoTime: instance['00180081']?.Value?.[0],
inversionTime: instance['00180082']?.Value?.[0],
receiveCoilName: instance['00181250']?.Value?.[0],
mrAcquisitionType: instance['00180023']?.Value?.[0],
phaseEncodingDirection: instance['00181312']?.Value?.[0],
echoTrainLength: instance['00180091']?.Value?.[0],
flipAngle: instance['00181314']?.Value?.[0],
pixelBandwidth: instance['00180095']?.Value?.[0],
acquisitionTime: instance['00080032']?.Value?.[0],
parallelAcquisitionTechnique: instance['00181316']?.Value?.[0],
};
console.debug('Extracted MRI Instance Data:', extractedData);
setInstanceData(extractedData);
}
} catch (err) {
console.error('Error fetching MRI instance data:', err);
setError(err);
} finally {
setLoading(false);
}
};
fetchInstanceData();
}, [studyInstanceUID, seriesInstanceUID, sopInstanceUID]);
return { instanceData, loading, error };
};