diff --git a/src/utils/extractPositioningFromDataset.js b/src/utils/extractPositioningFromDataset.js new file mode 100644 index 00000000..f5bfc02a --- /dev/null +++ b/src/utils/extractPositioningFromDataset.js @@ -0,0 +1,343 @@ +import getNumberValues from "./getNumberValues"; +import isNMReconstructable from "./isNMReconstructable"; + +/** + * Get a subpart of Image Type dicom tag defined by index + * @param {*} dataSet + * @param {*} index 0 based index of the subtype + */ +function getImageTypeSubItemFromDataset(dataSet, index) { + const imageType = dataSet.string("x00080008"); + + if (imageType) { + const subTypes = imageType.split("\\"); + + if (subTypes.length > index) { + return subTypes[index]; + } + } + + return undefined; +} +// DICOM 标准中针对 Enhanced CT/MR/PET 等“增强型多帧”(Enhanced Multi-frame)影像, 强制要求为每一帧显式记录真实的独立空间坐标 。 + +// - 它们的数据通常存放在: Per-frame Functional Groups Sequence (5200,9230) -> 对应的 Frame Item -> Plane Position Sequence (0020,9113) -> ImagePositionPatient (0020,0032) 。 +function getSharedFunctionalGroupsDataSet(dataSet) { + const seq = dataSet?.elements?.x52009229; + if (!seq?.items?.length) { + return; + } + return seq.items[0].dataSet; +} + +function getPerFrameFunctionalGroupsDataSet(dataSet, frameIndex) { + const seq = dataSet?.elements?.x52009230; + if (!seq?.items?.length) { + return; + } + if ( + !Number.isInteger(frameIndex) || + frameIndex < 0 || + frameIndex >= seq.items.length + ) { + return; + } + return seq.items[frameIndex].dataSet; +} + +function getOrientationFromPlaneOrientationSequence(dataSet) { + const seq = dataSet?.elements?.x00209116; + if (!seq?.items?.length) { + return; + } + return getNumberValues(seq.items[0].dataSet, "x00200037", 6); +} + +function getPositionFromPlanePositionSequence(dataSet) { + const seq = dataSet?.elements?.x00209113; + if (!seq?.items?.length) { + return; + } + return getNumberValues(seq.items[0].dataSet, "x00200032", 3); +} + +function getPixelSpacingFromPixelMeasuresSequence(dataSet) { + const seq = dataSet?.elements?.x00289110; + if (!seq?.items?.length) { + return; + } + return getNumberValues(seq.items[0].dataSet, "x00280030", 2); +} + +function getSliceThicknessFromPixelMeasuresSequence(dataSet) { + const seq = dataSet?.elements?.x00289110; + if (!seq?.items?.length) { + return; + } + if (!seq.items[0]?.dataSet?.elements?.x00180050) { + return; + } + return seq.items[0].dataSet.floatString("x00180050"); +} + +/** + * Extracts the orientation from NM multiframe dataset, if image type + * equal to RECON TOMO or RECON GATED TOMO + * @param {*} dataSet + * @returns + */ +function extractOrientationFromNMMultiframeDataset(dataSet) { + let imageOrientationPatient; + const modality = dataSet.string("x00080060"); + + if (modality?.includes("NM")) { + const imageSubType = getImageTypeSubItemFromDataset(dataSet, 2); + + if (imageSubType && isNMReconstructable(imageSubType)) { + if (dataSet.elements.x00540022) { + imageOrientationPatient = getNumberValues( + dataSet.elements.x00540022.items[0].dataSet, + "x00200037", + 6 + ); + } + } + } + + return imageOrientationPatient; +} + +/** + * Extracts the position from NM multiframe dataset, if image type + * equal to RECON TOMO or RECON GATED TOMO + * @param {*} dataSet + * @returns + */ +function extractPositionFromNMMultiframeDataset(dataSet) { + let imagePositionPatient; + const modality = dataSet.string("x00080060"); + + if (modality?.includes("NM")) { + const imageSubType = getImageTypeSubItemFromDataset(dataSet, 2); + + if (imageSubType && isNMReconstructable(imageSubType)) { + if (dataSet.elements.x00540022) { + imagePositionPatient = getNumberValues( + dataSet.elements.x00540022.items[0].dataSet, + "x00200032", + 3 + ); + } + } + } + + return imagePositionPatient; +} + +function isMultiFrame(dataSet) { + const numberOfFrames = + dataSet.string("x00280008") || dataSet.uint16("x00280008"); + return numberOfFrames !== undefined && Number(numberOfFrames) > 1; +} + +/** + * Extract orientation information from a dataset. It tries to get the orientation + * from the Detector Information Sequence (for NM images) if image type equal + * to RECON TOMO or RECON GATED TOMO + * @param {*} dataSet + * @returns + */ +function extractOrientationFromDataset(dataSet) { + // let imageOrientationPatient; + + // if (isMultiFrame(dataSet)) { + // const perFrameDataSet = getPerFrameFunctionalGroupsDataSet( + // dataSet, + // frameIndex + // ); + // imageOrientationPatient = + // getOrientationFromPlaneOrientationSequence(perFrameDataSet); + + // if (!imageOrientationPatient) { + // const sharedDataSet = getSharedFunctionalGroupsDataSet(dataSet); + // imageOrientationPatient = + // getOrientationFromPlaneOrientationSequence(sharedDataSet); + // } + // } + + // if (!imageOrientationPatient) { + // imageOrientationPatient = getNumberValues(dataSet, "x00200037", 6); + // } + let imageOrientationPatient = getNumberValues(dataSet, "x00200037", 6) + // Trying to get the orientation from the Plane Orientation Sequence + if (!imageOrientationPatient && dataSet.elements.x00209116) { + imageOrientationPatient = getNumberValues( + dataSet.elements.x00209116.items[0].dataSet, + "x00200037", + 6 + ); + } + + // If orientation not valid to this point, trying to get the orientation + // from the Detector Information Sequence (for NM images) with image type + // equal to RECON TOMO or RECON GATED TOMO + + if (!imageOrientationPatient) { + imageOrientationPatient = + extractOrientationFromNMMultiframeDataset(dataSet); + } + + return imageOrientationPatient; +} + +/** + * Extract position information from a dataset. It tries to get the position + * from the Detector Information Sequence (for NM images) if image type equal + * to RECON TOMO or RECON GATED TOMO + * @param {*} dataSet + * @returns + */ +function extractPositionFromDataset(dataSet) { + // let imagePositionPatient; + + // if (isMultiFrame(dataSet)) { + // const perFrameDataSet = getPerFrameFunctionalGroupsDataSet( + // dataSet, + // frameIndex + // ); + // imagePositionPatient = + // getPositionFromPlanePositionSequence(perFrameDataSet); + + // if (!imagePositionPatient) { + // const sharedDataSet = getSharedFunctionalGroupsDataSet(dataSet); + // imagePositionPatient = + // getPositionFromPlanePositionSequence(sharedDataSet); + // } + // } + + // if (!imagePositionPatient) { + // imagePositionPatient = getNumberValues(dataSet, "x00200032", 3); + // } + let imagePositionPatient = getNumberValues(dataSet, 'x00200032', 3); + // Trying to get the position from the Plane Position Sequence + if (!imagePositionPatient && dataSet.elements.x00209113) { + imagePositionPatient = getNumberValues( + dataSet.elements.x00209113.items[0].dataSet, + "x00200032", + 3 + ); + } + + // If position not valid to this point, trying to get the position + // from the Detector Information Sequence (for NM images) + if (!imagePositionPatient) { + imagePositionPatient = extractPositionFromNMMultiframeDataset(dataSet); + } + + return imagePositionPatient; +} + +/** + * Extract the pixelSpacing information. If exists, extracts this information + * from Pixel Measures Sequence + * @param {*} dataSet + * @returns + */ +function extractSpacingFromDataset(dataSet) { + // let pixelSpacing = getNumberValues(dataSet, 'x00280030', 2); + + // // If pixelSpacing not valid to this point, trying to get the spacing + // // from the Pixel Measures Sequence + // if (!pixelSpacing && dataSet.elements.x00289110) { + // pixelSpacing = getNumberValues( + // dataSet.elements.x00289110.items[0].dataSet, + // 'x00280030', + // 2 + // ); + // } + + // return pixelSpacing; + + let rowPixelSpacing = null; + let columnPixelSpacing = null; + // let pixelSpacing; + + // if (isMultiFrame(dataSet)) { + // const perFrameDataSet = getPerFrameFunctionalGroupsDataSet( + // dataSet, + // frameIndex + // ); + // const sharedDataSet = getSharedFunctionalGroupsDataSet(dataSet); + // pixelSpacing = + // getPixelSpacingFromPixelMeasuresSequence(perFrameDataSet) || + // getPixelSpacingFromPixelMeasuresSequence(sharedDataSet); + // } + + // if (!pixelSpacing) { + // pixelSpacing = + // getNumberValues(dataSet, "x00280030", 2) || + // getPixelSpacingFromPixelMeasuresSequence(dataSet); + // } + let pixelSpacing = getNumberValues(dataSet, 'x00280030', 2); + const imagePixelSpacing = getNumberValues(dataSet, "x00181164", 2); + const estimatedRadiographicMagnificationFactor = getNumberValues( + dataSet, + "x00181114", + 2 + ); + if (pixelSpacing) { + rowPixelSpacing = pixelSpacing[0]; + columnPixelSpacing = pixelSpacing[1]; + } else if (imagePixelSpacing && estimatedRadiographicMagnificationFactor) { + rowPixelSpacing = + imagePixelSpacing[0] / estimatedRadiographicMagnificationFactor[0]; + columnPixelSpacing = + imagePixelSpacing[1] / estimatedRadiographicMagnificationFactor[1]; + } else if (imagePixelSpacing && !estimatedRadiographicMagnificationFactor) { + rowPixelSpacing = imagePixelSpacing[0]; + columnPixelSpacing = imagePixelSpacing[1]; + } + return rowPixelSpacing !== null + ? [rowPixelSpacing, columnPixelSpacing] + : undefined; +} + +/** + * Extract the sliceThickness information. If exists, extracts this information + * from Pixel Measures Sequence + * @param {*} dataSet + * @returns + */ +function extractSliceThicknessFromDataset(dataSet) { + let sliceThickness; + if (dataSet.elements.x00180050) { + sliceThickness = dataSet.floatString('x00180050'); + } + else if (dataSet.elements.x00289110 && + dataSet.elements.x00289110.items.length && + dataSet.elements.x00289110.items[0].dataSet.elements.x00180050) { + sliceThickness = + dataSet.elements.x00289110.items[0].dataSet.floatString('x00180050'); + } + + // if (sliceThickness === undefined && isMultiFrame(dataSet)) { + // const perFrameDataSet = getPerFrameFunctionalGroupsDataSet( + // dataSet, + // frameIndex + // ); + // const sharedDataSet = getSharedFunctionalGroupsDataSet(dataSet); + // sliceThickness = + // getSliceThicknessFromPixelMeasuresSequence(perFrameDataSet) || + // getSliceThicknessFromPixelMeasuresSequence(sharedDataSet); + // } + + return sliceThickness; +} + +export { + getImageTypeSubItemFromDataset, + extractOrientationFromDataset, + extractPositionFromDataset, + extractSpacingFromDataset, + extractSliceThicknessFromDataset, +}; diff --git a/src/utils/getNumberValues.js b/src/utils/getNumberValues.js new file mode 100644 index 00000000..7fcaad24 --- /dev/null +++ b/src/utils/getNumberValues.js @@ -0,0 +1,24 @@ +function getNumberValues( + dataSet, + tag, + minimumLength +){ + const values = []; + const valueAsString = dataSet.string(tag); + + if (!valueAsString) { + return; + } + const split = valueAsString.split('\\'); + + if (minimumLength && split.length < minimumLength) { + return; + } + for (let i = 0; i < split.length; i++) { + values.push(parseFloat(split[i])); + } + + return values; +} + +export default getNumberValues; diff --git a/src/utils/isNMReconstructable.js b/src/utils/isNMReconstructable.js new file mode 100644 index 00000000..4a0909d2 --- /dev/null +++ b/src/utils/isNMReconstructable.js @@ -0,0 +1,3 @@ +export default function isNMReconstructable(imageSubType) { + return imageSubType === 'RECON TOMO' || imageSubType === 'RECON GATED TOMO'; +} diff --git a/src/utils/metaDataProvider.js b/src/utils/metaDataProvider.js index b840ddc0..d9c38b06 100644 --- a/src/utils/metaDataProvider.js +++ b/src/utils/metaDataProvider.js @@ -1,4 +1,11 @@ import * as cornerstoneWADOImageLoader from 'cornerstone-wado-image-loader' +import { + getImageTypeSubItemFromDataset, + extractOrientationFromDataset, + extractPositionFromDataset, + extractSpacingFromDataset, + extractSliceThicknessFromDataset, +} from './extractPositioningFromDataset'; function parseImageId(imageId) { // build a url by parsing out the url scheme and frame index from the imageId const firstColonIndex = imageId.indexOf(':'); @@ -141,6 +148,19 @@ function populatePaletteColorLut(dataSet, imagePixelModule) { imagePixelModule.bluePaletteColorLookupTableDescriptor ); } + +function getSpacingBetweenSlices(dataSet) { + if (dataSet?.elements?.x00180088) { + return dataSet.floatString('x00180088'); + } + const pixelMeasuresSequence = dataSet?.elements?.x00289110; + if ( + pixelMeasuresSequence?.items?.length && + pixelMeasuresSequence.items[0]?.dataSet?.elements?.x00180088 + ) { + return pixelMeasuresSequence.items[0].dataSet.floatString('x00180088'); + } +} function metaDataProvider(type, imageId) { const parsedImageId = parseImageId(imageId); const dataSet = cornerstoneWADOImageLoader.wadouri.dataSetCacheManager.get(parsedImageId.url); @@ -149,10 +169,47 @@ function metaDataProvider(type, imageId) { return; } if (type === 'imagePlaneModule') { - const imageOrientationPatient = getNumberValues(dataSet, 'x00200037', 6); - const imagePositionPatient = getNumberValues(dataSet, 'x00200032', 3); - const pixelSpacing = getNumberValues(dataSet, 'x00280030', 2); + // const imageOrientationPatient = getNumberValues(dataSet, 'x00200037', 6); + // const imagePositionPatient = getNumberValues(dataSet, 'x00200032', 3); + // const pixelSpacing = getNumberValues(dataSet, 'x00280030', 2); const imagePixelSpacing = getNumberValues(dataSet, 'x00181164', 2); + const frameIndex = parsedImageId.frame !== undefined ? parsedImageId.frame - 1 : undefined; + const imageOrientationPatient = extractOrientationFromDataset(dataSet, frameIndex); + let imagePositionPatient = extractPositionFromDataset(dataSet, frameIndex); + const pixelSpacing = extractSpacingFromDataset(dataSet, frameIndex); + const sliceThickness = extractSliceThicknessFromDataset(dataSet, frameIndex); + + const modality = dataSet.string("x00080060"); + if (modality && modality.includes('NM') && parsedImageId.frame !== undefined && parsedImageId.frame > 1) { + const spacingBetweenSlices = getSpacingBetweenSlices(dataSet); + const step = spacingBetweenSlices !== undefined ? spacingBetweenSlices : sliceThickness; + if (imageOrientationPatient && imagePositionPatient && step !== undefined && frameIndex !== undefined) { + const rowCosines = [ + parseFloat(imageOrientationPatient[0]), + parseFloat(imageOrientationPatient[1]), + parseFloat(imageOrientationPatient[2]), + ]; + const columnCosines = [ + parseFloat(imageOrientationPatient[3]), + parseFloat(imageOrientationPatient[4]), + parseFloat(imageOrientationPatient[5]), + ]; + const normal = [ + rowCosines[1] * columnCosines[2] - rowCosines[2] * columnCosines[1], + rowCosines[2] * columnCosines[0] - rowCosines[0] * columnCosines[2], + rowCosines[0] * columnCosines[1] - rowCosines[1] * columnCosines[0], + ]; + + const offset = frameIndex * parseFloat(step); + + imagePositionPatient = [ + parseFloat(imagePositionPatient[0]) + normal[0] * offset, + parseFloat(imagePositionPatient[1]) + normal[1] * offset, + parseFloat(imagePositionPatient[2]) + normal[2] * offset, + ]; + } + } + const estimatedRadiographicMagnificationFactor = getNumberValues(dataSet, 'x00181114', 2); let columnPixelSpacing = null; @@ -194,7 +251,7 @@ function metaDataProvider(type, imageId) { rowCosines, columnCosines, imagePositionPatient, - sliceThickness: dataSet.floatString('x00180050'), + sliceThickness, sliceLocation: dataSet.floatString('x00201041'), pixelSpacing, rowPixelSpacing, @@ -224,6 +281,11 @@ function metaDataProvider(type, imageId) { } return imagePixelModule; } + // if (type === "multiframeModule") { + // return { + // NumberOfFrames: dataSet.uint16("x00280008") || dataSet.string("x00280008") + // }; + // } // if (type === 'imagePixelModule') { // return { // samplesPerPixel: dataSet.uint16('x00280002'), @@ -249,4 +311,4 @@ function metaDataProvider(type, imageId) { // } // } } -export default metaDataProvider; \ No newline at end of file +export default metaDataProvider; diff --git a/src/views/trials/trials-panel/reading/dicoms/components/Fusion/js/imagePixelSpacingMetaDataProvider.js b/src/views/trials/trials-panel/reading/dicoms/components/Fusion/js/imagePixelSpacingMetaDataProvider.js index 6a2f283d..64a75482 100644 --- a/src/views/trials/trials-panel/reading/dicoms/components/Fusion/js/imagePixelSpacingMetaDataProvider.js +++ b/src/views/trials/trials-panel/reading/dicoms/components/Fusion/js/imagePixelSpacingMetaDataProvider.js @@ -1,10 +1,31 @@ -import cornerstoneDICOMImageLoader from '@cornerstonejs/dicom-image-loader' +import cornerstoneDICOMImageLoader from "@cornerstonejs/dicom-image-loader"; +import { + getImageTypeSubItemFromDataset, + extractOrientationFromDataset, + extractPositionFromDataset, + extractSpacingFromDataset, + extractSliceThicknessFromDataset, +} from "@/utils/extractPositioningFromDataset"; +import getNumberValues from "@/utils/getNumberValues"; + +function getSpacingBetweenSlices(dataSet) { + if (dataSet?.elements?.x00180088) { + return dataSet.floatString("x00180088"); + } + const pixelMeasuresSequence = dataSet?.elements?.x00289110; + if ( + pixelMeasuresSequence?.items?.length && + pixelMeasuresSequence.items[0]?.dataSet?.elements?.x00180088 + ) { + return pixelMeasuresSequence.items[0].dataSet.floatString("x00180088"); + } +} function parseImageId(imageId) { // build a url by parsing out the url scheme and frame index from the imageId - const firstColonIndex = imageId.indexOf(':'); + const firstColonIndex = imageId.indexOf(":"); let url = imageId.substring(firstColonIndex + 1); - const frameIndex = url.indexOf('frame='); + const frameIndex = url.indexOf("frame="); let frame; @@ -21,127 +42,112 @@ function parseImageId(imageId) { frame, }; } -function getNumberValues(dataSet, tag, minimumLength) { - const values = []; - const valueAsString = dataSet.string(tag); - if (!valueAsString) { +function metaDataProvider(type, imageId) { + const parsedImageId = parseImageId(imageId); + const dataSet = cornerstoneDICOMImageLoader.wadouri.dataSetCacheManager.get( + parsedImageId.url + ); + if (!dataSet) { return; } - const split = valueAsString.split('\\'); + if (type === "imagePlaneModule") { + const frameIndex = parsedImageId.frame !== undefined ? parsedImageId.frame - 1 : undefined; + // const imageOrientationPatient = extractOrientationFromDataset(dataSet, frameIndex); + // let imagePositionPatient = extractPositionFromDataset(dataSet, frameIndex); + // const pixelSpacing = extractSpacingFromDataset(dataSet, frameIndex); + // const sliceThickness = extractSliceThicknessFromDataset(dataSet, frameIndex); + const imageOrientationPatient = extractOrientationFromDataset(dataSet); + let imagePositionPatient = extractPositionFromDataset(dataSet); + const pixelSpacing = extractSpacingFromDataset(dataSet); + const sliceThickness = extractSliceThicknessFromDataset(dataSet); + + const modality = dataSet.string("x00080060"); + if ( + modality && + modality.includes("NM") && + parsedImageId.frame !== undefined && + parsedImageId.frame > 1 + ) { + // 坐标堆叠 (位置相同) :NM 多帧 DICOM 的空间位置 ( imagePositionPatient ) 通常只存在于 Detector Information Sequence 序列的第一个 Item 中。原来的代码在提取位置时,所有帧都被赋予了第一帧的位置。因为所有帧都挤在一个坐标上,3D 引擎无法根据厚度和法向量空间展开它们,导致间距 (Spacing) 为 0 + // 当识别到 NM 多帧图像且属于多帧切片时,通过 RowCosines 与 ColumnCosines 的叉乘计算出切片的 法向量(Normal) ,并结合 帧索引 (frameIndex) 以及 Spacing Between Slices (0018,0088)(层间距) 或者 Slice Thickness (0018,0050)(层厚) ,自动推算补全后续每一帧的 imagePositionPatient 真实三维坐标 + // + if ( + imageOrientationPatient && + imagePositionPatient && + sliceThickness !== undefined + ) { + const spacingBetweenSlices = getSpacingBetweenSlices(dataSet); + const step = + spacingBetweenSlices !== undefined ? spacingBetweenSlices : sliceThickness; + const rowCosines = [ + parseFloat(imageOrientationPatient[0]), + parseFloat(imageOrientationPatient[1]), + parseFloat(imageOrientationPatient[2]), + ]; + const columnCosines = [ + parseFloat(imageOrientationPatient[3]), + parseFloat(imageOrientationPatient[4]), + parseFloat(imageOrientationPatient[5]), + ]; + const normal = [ + rowCosines[1] * columnCosines[2] - rowCosines[2] * columnCosines[1], + rowCosines[2] * columnCosines[0] - rowCosines[0] * columnCosines[2], + rowCosines[0] * columnCosines[1] - rowCosines[1] * columnCosines[0], + ]; - if (minimumLength && split.length < minimumLength) { - return; - } - for (let i = 0; i < split.length; i++) { - values.push(parseFloat(split[i])); - } + const offset = frameIndex * parseFloat(step); - return values; -} -function getImageTypeSubItemFromDataset(dataSet, index) { - const imageType = dataSet.string('x00080008'); - if (imageType) { - const subTypes = imageType.split('\\'); - if (subTypes.length > index) { - return subTypes[index]; + imagePositionPatient = [ + parseFloat(imagePositionPatient[0]) + normal[0] * offset, + parseFloat(imagePositionPatient[1]) + normal[1] * offset, + parseFloat(imagePositionPatient[2]) + normal[2] * offset, + ]; } - } - return undefined; -} -function isNMReconstructable(imageSubType) { - return imageSubType === 'RECON TOMO' || imageSubType === 'RECON GATED TOMO'; -} -function extractOrientationFromNMMultiframeDataset(dataSet) { - let imageOrientationPatient; - const modality = dataSet.string('x00080060'); - if (modality?.includes('NM')) { - const imageSubType = getImageTypeSubItemFromDataset(dataSet, 2); - if (imageSubType && isNMReconstructable(imageSubType)) { - if (dataSet.elements.x00540022) { - imageOrientationPatient = getNumberValues(dataSet.elements.x00540022.items[0].dataSet, 'x00200037', 6); - } - } - } - return imageOrientationPatient; -} -function extractOrientationFromDataset(dataSet) { - let imageOrientationPatient = getNumberValues(dataSet, 'x00200037', 6); - if (!imageOrientationPatient && dataSet.elements.x00209116) { - imageOrientationPatient = getNumberValues(dataSet.elements.x00209116.items[0].dataSet, 'x00200037', 6); - } - if (!imageOrientationPatient) { - imageOrientationPatient = - extractOrientationFromNMMultiframeDataset(dataSet); - } - return imageOrientationPatient; -} -function extractPositionFromNMMultiframeDataset(dataSet) { - let imagePositionPatient; - const modality = dataSet.string('x00080060'); - if (modality?.includes('NM')) { - const imageSubType = getImageTypeSubItemFromDataset(dataSet, 2); - if (imageSubType && isNMReconstructable(imageSubType)) { - if (dataSet.elements.x00540022) { - imagePositionPatient = getNumberValues(dataSet.elements.x00540022.items[0].dataSet, 'x00200032', 3); - } - } - } - return imagePositionPatient; -} -function extractPositionFromDataset(dataSet) { - let imagePositionPatient = getNumberValues(dataSet, 'x00200032', 3); - if (!imagePositionPatient && dataSet.elements.x00209113) { - imagePositionPatient = getNumberValues(dataSet.elements.x00209113.items[0].dataSet, 'x00200032', 3); - } - if (!imagePositionPatient) { - imagePositionPatient = extractPositionFromNMMultiframeDataset(dataSet); - } - return imagePositionPatient; -} -function extractSliceThicknessFromDataset(dataSet) { - let sliceThickness; - if (dataSet.elements.x00180050) { - sliceThickness = dataSet.floatString('x00180050'); - } - else if (dataSet.elements.x00289110 && - dataSet.elements.x00289110.items.length && - dataSet.elements.x00289110.items[0].dataSet.elements.x00180050) { - sliceThickness = - dataSet.elements.x00289110.items[0].dataSet.floatString('x00180050'); - } - return sliceThickness; -} -function extractSpacingFromDataset(dataSet) { - let rowPixelSpacing = null - let columnPixelSpacing = null - let pixelSpacing = getNumberValues(dataSet, 'x00280030', 2); - const imagePixelSpacing = getNumberValues(dataSet, 'x00181164', 2); - const estimatedRadiographicMagnificationFactor = getNumberValues(dataSet, 'x00181114', 2); - if (pixelSpacing) { - rowPixelSpacing = pixelSpacing[0] - columnPixelSpacing = pixelSpacing[1] + } + + // const imageOrientationPatient = getNumberValues(dataSet, 'x00200037', 6); + // const imagePositionPatient = getNumberValues(dataSet, 'x00200032', 3); + // const pixelSpacing = extractSpacingFromDataset(dataSet); + const imagePixelSpacing = getNumberValues(dataSet, "x00181164", 2); + + // let rowCosines = null; + + // let columnCosines = null; + + // if (imageOrientationPatient) { + // rowCosines = [ + // parseFloat(imageOrientationPatient[0]), + // parseFloat(imageOrientationPatient[1]), + // parseFloat(imageOrientationPatient[2]), + // ]; + // columnCosines = [ + // parseFloat(imageOrientationPatient[3]), + // parseFloat(imageOrientationPatient[4]), + // parseFloat(imageOrientationPatient[5]), + // ]; + // } + const estimatedRadiographicMagnificationFactor = getNumberValues( + dataSet, + "x00181114", + 2 + ); + let columnPixelSpacing = null; + + let rowPixelSpacing = null; + + if (pixelSpacing) { + rowPixelSpacing = pixelSpacing[0]; + columnPixelSpacing = pixelSpacing[1]; } else if (imagePixelSpacing && estimatedRadiographicMagnificationFactor) { - rowPixelSpacing = imagePixelSpacing[0] / estimatedRadiographicMagnificationFactor[0]; - columnPixelSpacing = imagePixelSpacing[1] / estimatedRadiographicMagnificationFactor[1]; + rowPixelSpacing = + imagePixelSpacing[0] / estimatedRadiographicMagnificationFactor[0]; + columnPixelSpacing = + imagePixelSpacing[1] / estimatedRadiographicMagnificationFactor[1]; } else if (imagePixelSpacing && !estimatedRadiographicMagnificationFactor) { rowPixelSpacing = imagePixelSpacing[0]; columnPixelSpacing = imagePixelSpacing[1]; } - return rowPixelSpacing !== null ? [rowPixelSpacing, columnPixelSpacing] : undefined -} -function metaDataProvider(type, imageId) { - const parsedImageId = parseImageId(imageId); - const dataSet = cornerstoneDICOMImageLoader.wadouri.dataSetCacheManager.get(parsedImageId.url) - - - if (!dataSet) { - return; - } - if (type === 'imagePlaneModule') { - const imageOrientationPatient = getNumberValues(dataSet, 'x00200037', 6); - const imagePositionPatient = getNumberValues(dataSet, 'x00200032', 3); - const pixelSpacing = extractSpacingFromDataset(dataSet); let rowCosines = null; @@ -160,34 +166,35 @@ function metaDataProvider(type, imageId) { ]; } return { - frameOfReferenceUID: dataSet.string('x00200052'), - rows: dataSet.uint16('x00280010'), - columns: dataSet.uint16('x00280011'), + frameOfReferenceUID: dataSet.string("x00200052"), + rows: dataSet.uint16("x00280010"), + columns: dataSet.uint16("x00280011"), imageOrientationPatient, rowCosines, columnCosines, imagePositionPatient, - sliceThickness: dataSet.floatString('x00180050'), - sliceLocation: dataSet.floatString('x00201041'), - pixelSpacing: pixelSpacing, - rowPixelSpacing: pixelSpacing ? pixelSpacing[0] : null, - columnPixelSpacing: pixelSpacing ? pixelSpacing[1] : null, + sliceThickness, + sliceLocation: dataSet.floatString("x00201041"), + pixelSpacing, + rowPixelSpacing, + columnPixelSpacing, }; } - if (type === 'nmMultiframeGeometryModule') { - const modality = dataSet.string('x00080060'); - const imageSubType = getImageTypeSubItemFromDataset(dataSet, 2); - return { - modality, - imageType: dataSet.string('x00080008'), - imageSubType, - imageOrientationPatient: extractOrientationFromDataset(dataSet), - imagePositionPatient: extractPositionFromDataset(dataSet), - sliceThickness: extractSliceThicknessFromDataset(dataSet), - pixelSpacing: extractSpacingFromDataset(dataSet), - numberOfFrames: dataSet.uint16('x00280008'), - isNMReconstructable: isNMReconstructable(imageSubType) && modality.includes('NM'), - }; + if (type === "nmMultiframeGeometryModule") { + const modality = dataSet.string("x00080060"); + const imageSubType = getImageTypeSubItemFromDataset(dataSet, 2); + return { + modality, + imageType: dataSet.string("x00080008"), + imageSubType, + imageOrientationPatient: extractOrientationFromDataset(dataSet), + imagePositionPatient: extractPositionFromDataset(dataSet), + sliceThickness: extractSliceThicknessFromDataset(dataSet), + pixelSpacing: extractSpacingFromDataset(dataSet), + numberOfFrames: dataSet.uint16("x00280008"), + isNMReconstructable: + isNMReconstructable(imageSubType) && modality.includes("NM"), + }; } } -export default metaDataProvider; \ No newline at end of file +export default metaDataProvider; diff --git a/src/views/trials/trials-panel/reading/dicoms3D/components/PetCtViewport.vue b/src/views/trials/trials-panel/reading/dicoms3D/components/PetCtViewport.vue index a2262c30..c9ce9b8b 100644 --- a/src/views/trials/trials-panel/reading/dicoms3D/components/PetCtViewport.vue +++ b/src/views/trials/trials-panel/reading/dicoms3D/components/PetCtViewport.vue @@ -17,7 +17,7 @@ {{ `${series.TaskInfo.SubjectCode} ${series.TaskInfo.TaskBlindName} ` }}
Series: #{{ series.SeriesNumber }}
-
Image: #{{ `${series.SliceIndex + 1}/${series.Stack.length}` }}
+
Image: #{{ `${series.SliceIndex + 1}/${imageInfo.total || series.Stack.length}` }}
{{ series.Modality }}
{{ ctSeries.Modality }} / {{ series.Modality }}
MIP
@@ -103,7 +103,7 @@ import { setMipTransferFunctionForVolumeActor, setPetTransferFunctionForVolumeActor } from './helpers/index.js' -const { BlendModes, OrientationAxis } = Enums; +const { BlendModes, OrientationAxis } = Enums; const { getColormap } = csUtils.colormap; import { vec3, mat4 } from 'gl-matrix' export default { @@ -149,7 +149,8 @@ export default { size: null, location: null, sliceThickness: null, - wwwc: null + wwwc: null, + total: null }, digitPlaces: 2, orientationMarkers: [], @@ -224,7 +225,9 @@ export default { if (this.series) { this.series.SliceIndex = detail.imageIndex } - this.sliderInfo.height = detail.imageIndex * 100 / detail.numberOfSlices + this.imageInfo.total = detail.numberOfSlices + const maxIndex = detail.numberOfSlices > 1 ? detail.numberOfSlices - 1 : 1 + this.sliderInfo.height = detail.imageIndex * 100 / maxIndex const renderingEngine = getRenderingEngine(this.renderingEngineId) const viewport = renderingEngine.getViewport(this.viewportId) const zoom = viewport.getZoom() @@ -414,9 +417,6 @@ export default { const renderingEngine = getRenderingEngine(this.renderingEngineId) const voiRange = { lower: 0, upper: v } this.currentVoiUpper = v - if (this.isFusion) { - console.log('voiChange', v) - } const viewport = renderingEngine.getViewport(this.viewportId) if (!viewport) return @@ -476,12 +476,21 @@ export default { const renderingEngine = getRenderingEngine(this.renderingEngineId) const viewport = renderingEngine.getViewport(this.viewportId) if (!viewport) return - let volumeId = this.isFusion ? this.ptVolumeId : this.volumeId - viewport.setProperties( - { colormap: { opacity: Number(opacity) } }, - volumeId - ) + if (this.isFusion) { + const ctVolumeId = this.ctSeries?.SeriesInstanceUid + const ptVolumeId = this.ptVolumeId + const topVolumeId = this.fusionCtOnTop ? ctVolumeId : ptVolumeId + const bottomVolumeId = this.fusionCtOnTop ? ptVolumeId : ctVolumeId + const topOpacity = Number(opacity) + const bottomOpacity = 1 - topOpacity + if (bottomVolumeId) { + viewport.setProperties({ colormap: { opacity: bottomOpacity } }, bottomVolumeId) + } + if (topVolumeId) { + viewport.setProperties({ colormap: { opacity: topOpacity } }, topVolumeId) + } + } viewport.render() }, applyFusionOpacity() { @@ -512,11 +521,14 @@ export default { }, async createImageIdsAndCacheMetaData(obj) { this.loading = true - await createImageIdsAndCacheMetaData({ - modality: obj.Modality, - imageIds: obj.ImageIds - }) - this.loading = false + try { + return await createImageIdsAndCacheMetaData({ + modality: obj.Modality, + imageIds: obj.ImageIds + }) + } finally { + this.loading = false + } }, getFusionVolumes() { const ctVolumeId = this.ctSeries?.SeriesInstanceUid @@ -531,8 +543,8 @@ export default { setCtTransferFunctionForVolumeActor({ ...r, volumeId: ctVolumeId }) if (this.fusionCtOnTop) { this.topFusionVolumeActor = r.volumeActor - this.applyFusionOpacity() } + this.applyFusionOpacity() console.log("融合ct渲染成功") } } @@ -543,8 +555,8 @@ export default { setPetColorMapTransferFunctionForVolumeActor({ ...r, volumeId: ptFusionVolumeId }) if (!this.fusionCtOnTop) { this.topFusionVolumeActor = r.volumeActor - this.applyFusionOpacity() } + this.applyFusionOpacity() console.log("融合pet渲染成功") } } @@ -709,8 +721,10 @@ export default { clickSlider(e) { const height = e.offsetY * 100 / this.$refs['sliderBox'].clientHeight this.sliderInfo.height = height - let sliceIdx = Math.trunc(this.series.Stack.length * height / 100) - sliceIdx = sliceIdx >= this.series.Stack.length ? this.series.Stack.length - 1 : sliceIdx < 0 ? 0 : sliceIdx + const totalSlices = this.imageInfo.total || this.series.Stack?.length || 1 + const maxIndex = totalSlices > 1 ? totalSlices - 1 : 0 + let sliceIdx = Math.round(maxIndex * height / 100) + sliceIdx = sliceIdx > maxIndex ? maxIndex : sliceIdx < 0 ? 0 : sliceIdx const renderingEngine = getRenderingEngine(this.renderingEngineId) const viewport = renderingEngine.getViewport( this.viewportId @@ -725,7 +739,7 @@ export default { }, sliderMousedown(e) { const boxHeight = this.$refs['sliderBox'].clientHeight - this.sliderInfo.oldB = parseInt(e.srcElement.style.top) * boxHeight / 100 + this.sliderInfo.oldB = this.sliderInfo.height * boxHeight / 100 this.sliderInfo.oldM = e.clientY this.sliderInfo.isMove = true e.stopImmediatePropagation() @@ -739,8 +753,10 @@ export default { if (delta < 0) return if (delta > boxHeight) return const height = delta * 100 / boxHeight - let sliceIdx = Math.trunc(this.series.Stack.length * height / 100) - sliceIdx = sliceIdx >= this.series.Stack.length ? this.series.Stack.length - 1 : sliceIdx < 0 ? 0 : sliceIdx + const totalSlices = this.imageInfo.total || this.series.Stack?.length || 1 + const maxIndex = totalSlices > 1 ? totalSlices - 1 : 0 + let sliceIdx = Math.round(maxIndex * height / 100) + sliceIdx = sliceIdx > maxIndex ? maxIndex : sliceIdx < 0 ? 0 : sliceIdx this.sliderInfo.height = height const renderingEngine = getRenderingEngine(this.renderingEngineId) const viewport = renderingEngine.getViewport( @@ -891,7 +907,9 @@ export default { .opacity-slider { -webkit-appearance: slider-vertical; - writing-mode: bt-lr; + appearance: slider-vertical; + writing-mode: vertical-lr; + direction: rtl; width: 10px; height: 150px; padding: 0; diff --git a/src/views/trials/trials-panel/reading/dicoms3D/components/ReadPage.vue b/src/views/trials/trials-panel/reading/dicoms3D/components/ReadPage.vue index eff2e12c..1c6bc950 100644 --- a/src/views/trials/trials-panel/reading/dicoms3D/components/ReadPage.vue +++ b/src/views/trials/trials-panel/reading/dicoms3D/components/ReadPage.vue @@ -3696,8 +3696,8 @@ export default { if (cache.getVolume(volumeId)) { volume = cache.getVolume(volumeId) } else { - await this.$refs[`${this.viewportKey}-0`][0].createImageIdsAndCacheMetaData(serie) - let imageIds = this.sortImageIdsByImagePositionPatient(serie.ImageIds) + let imageIds = await this.$refs[`${this.viewportKey}-0`][0].createImageIdsAndCacheMetaData(serie) + // imageIds = this.sortImageIdsByImagePositionPatient(imageIds) volume = await volumeLoader.createAndCacheVolume(volumeId, { imageIds: imageIds }) volume.load() }