From 62b821df789142930bab4a53149d13a286acf93a Mon Sep 17 00:00:00 2001 From: AlfandiMario Date: Mon, 3 Nov 2025 16:01:02 +0700 Subject: [PATCH] req custom overlay: add MRI custom overlay using data from additional dicom query --- .../Overlays/CustomizableViewportOverlay.tsx | 239 ++++++++++++++++-- .../Overlays/studyDataForOverlayItem.ts | 113 +++++++++ 2 files changed, 327 insertions(+), 25 deletions(-) diff --git a/extensions/cornerstone/src/Viewport/Overlays/CustomizableViewportOverlay.tsx b/extensions/cornerstone/src/Viewport/Overlays/CustomizableViewportOverlay.tsx index c2379c0..5434b14 100644 --- a/extensions/cornerstone/src/Viewport/Overlays/CustomizableViewportOverlay.tsx +++ b/extensions/cornerstone/src/Viewport/Overlays/CustomizableViewportOverlay.tsx @@ -9,7 +9,7 @@ import { formatPN, formatDICOMDate, formatDICOMTime, formatNumberPrecision } fro import { StackViewportData, VolumeViewportData } from '../../types/CornerstoneCacheService'; import './CustomizableViewportOverlay.css'; -import { studyDataForOverlayItem } from './studyDataForOverlayItem'; +import { studyDataForOverlayItem, instanceDataForMriOverlayItem } from './studyDataForOverlayItem'; const EPSILON = 1e-4; @@ -58,10 +58,10 @@ const studyDateItem = { title: 'Study date and time', condition: ({ referenceInstance, studyApiData }) => referenceInstance?.StudyDate || studyApiData?.studyTime, - contentF: ({ referenceInstance, formatters: { formatDate }, studyApiData }) => { + contentF: ({ referenceInstance, formatters: { formatDate, formatTime }, studyApiData }) => { const date = referenceInstance?.StudyDate ? formatDate(referenceInstance.StudyDate) : ''; - const time = studyApiData?.studyTime ? studyApiData.studyTime : ''; - return `${date} ${time}`; + const time = studyApiData?.studyTime ? formatTime(studyApiData.studyTime) : ''; + return `${date} ${time}`.trim(); }, }; @@ -125,12 +125,184 @@ const accessionNumberItem = { 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 = { id: 'cornerstoneOverlayTopLeft', items: [patientNameItem, mrnItem, sexAndAgeItem], }; -const topRightItems = { id: 'cornerstoneOverlayTopRight', items: [] }; +const topRightItems = { + id: 'cornerstoneOverlayTopRight', + items: [ + patientNameItem, + mrnItem, + sexAndAgeItem, + accessionNumberItem, + studyDescriptionItem, + studyDateItem, + ], +}; const bottomLeftItems = { id: 'cornerstoneOverlayBottomLeft', @@ -160,9 +332,9 @@ const bottomRightItems = { */ const CornerstoneOverlay = { id: '@ohif/cornerstoneOverlay', - topLeftItems, + mriTopLeftItems, topRightItems, - bottomLeftItems, + mriBottomLeftItems, bottomRightItems, }; @@ -192,23 +364,6 @@ function CustomizableViewportOverlay({ // on the individual items rather than defining individual items. 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( () => viewportData @@ -236,8 +391,41 @@ function CustomizableViewportOverlay({ // FEAT: Edit overlays item [2025-09-04] const studyInstanceUID = displaySetProps?.referenceInstance?.StudyInstanceUID; + const seriesInstanceUID = displaySetProps?.referenceInstance?.SeriesInstanceUID; + const sopInstanceUID = displaySetProps?.instance?.SOPInstanceUID; 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 */ @@ -357,6 +545,7 @@ function CustomizableViewportOverlay({ viewportId, toolGroupService, studyApiData, // Pass the API data to the overlay items + mriInstanceData, }; return ( @@ -370,7 +559,7 @@ function CustomizableViewportOverlay({ ); }, - [_renderOverlayItem, displaySetProps, studyApiData] + [_renderOverlayItem, displaySetProps, studyApiData, mriInstanceData] ); return ( diff --git a/extensions/cornerstone/src/Viewport/Overlays/studyDataForOverlayItem.ts b/extensions/cornerstone/src/Viewport/Overlays/studyDataForOverlayItem.ts index 71f3b87..2586929 100644 --- a/extensions/cornerstone/src/Viewport/Overlays/studyDataForOverlayItem.ts +++ b/extensions/cornerstone/src/Viewport/Overlays/studyDataForOverlayItem.ts @@ -143,3 +143,116 @@ export const studyDataForOverlayItem = (studyInstanceUID: string) => { 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(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 }; +};