Merge branch 'main' of https://gitea.frp.extimaging.com/XCKJ/irc_web
continuous-integration/drone/push Build is passing Details

# Conflicts:
#	src/views/trials/trials-panel/reading/dicoms3D/components/PetCtViewport.vue
uat_us
wangxiaoshuang 2026-03-27 15:47:24 +08:00
commit 09211c8841
7 changed files with 626 additions and 169 deletions

View File

@ -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,
};

View File

@ -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;

View File

@ -0,0 +1,3 @@
export default function isNMReconstructable(imageSubType) {
return imageSubType === 'RECON TOMO' || imageSubType === 'RECON GATED TOMO';
}

View File

@ -1,4 +1,11 @@
import * as cornerstoneWADOImageLoader from 'cornerstone-wado-image-loader' import * as cornerstoneWADOImageLoader from 'cornerstone-wado-image-loader'
import {
getImageTypeSubItemFromDataset,
extractOrientationFromDataset,
extractPositionFromDataset,
extractSpacingFromDataset,
extractSliceThicknessFromDataset,
} from './extractPositioningFromDataset';
function parseImageId(imageId) { function parseImageId(imageId) {
// build a url by parsing out the url scheme and frame index from the imageId // build a url by parsing out the url scheme and frame index from the imageId
const firstColonIndex = imageId.indexOf(':'); const firstColonIndex = imageId.indexOf(':');
@ -141,6 +148,19 @@ function populatePaletteColorLut(dataSet, imagePixelModule) {
imagePixelModule.bluePaletteColorLookupTableDescriptor 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) { function metaDataProvider(type, imageId) {
const parsedImageId = parseImageId(imageId); const parsedImageId = parseImageId(imageId);
const dataSet = cornerstoneWADOImageLoader.wadouri.dataSetCacheManager.get(parsedImageId.url); const dataSet = cornerstoneWADOImageLoader.wadouri.dataSetCacheManager.get(parsedImageId.url);
@ -149,10 +169,47 @@ function metaDataProvider(type, imageId) {
return; return;
} }
if (type === 'imagePlaneModule') { if (type === 'imagePlaneModule') {
const imageOrientationPatient = getNumberValues(dataSet, 'x00200037', 6); // const imageOrientationPatient = getNumberValues(dataSet, 'x00200037', 6);
const imagePositionPatient = getNumberValues(dataSet, 'x00200032', 3); // const imagePositionPatient = getNumberValues(dataSet, 'x00200032', 3);
const pixelSpacing = getNumberValues(dataSet, 'x00280030', 2); // const pixelSpacing = getNumberValues(dataSet, 'x00280030', 2);
const imagePixelSpacing = getNumberValues(dataSet, 'x00181164', 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); const estimatedRadiographicMagnificationFactor = getNumberValues(dataSet, 'x00181114', 2);
let columnPixelSpacing = null; let columnPixelSpacing = null;
@ -194,7 +251,7 @@ function metaDataProvider(type, imageId) {
rowCosines, rowCosines,
columnCosines, columnCosines,
imagePositionPatient, imagePositionPatient,
sliceThickness: dataSet.floatString('x00180050'), sliceThickness,
sliceLocation: dataSet.floatString('x00201041'), sliceLocation: dataSet.floatString('x00201041'),
pixelSpacing, pixelSpacing,
rowPixelSpacing, rowPixelSpacing,
@ -224,6 +281,11 @@ function metaDataProvider(type, imageId) {
} }
return imagePixelModule; return imagePixelModule;
} }
// if (type === "multiframeModule") {
// return {
// NumberOfFrames: dataSet.uint16("x00280008") || dataSet.string("x00280008")
// };
// }
// if (type === 'imagePixelModule') { // if (type === 'imagePixelModule') {
// return { // return {
// samplesPerPixel: dataSet.uint16('x00280002'), // samplesPerPixel: dataSet.uint16('x00280002'),
@ -249,4 +311,4 @@ function metaDataProvider(type, imageId) {
// } // }
// } // }
} }
export default metaDataProvider; export default metaDataProvider;

View File

@ -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) { function parseImageId(imageId) {
// build a url by parsing out the url scheme and frame index from the 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); let url = imageId.substring(firstColonIndex + 1);
const frameIndex = url.indexOf('frame='); const frameIndex = url.indexOf("frame=");
let frame; let frame;
@ -21,127 +42,112 @@ function parseImageId(imageId) {
frame, 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; 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) { const offset = frameIndex * parseFloat(step);
return;
}
for (let i = 0; i < split.length; i++) {
values.push(parseFloat(split[i]));
}
return values; imagePositionPatient = [
} parseFloat(imagePositionPatient[0]) + normal[0] * offset,
function getImageTypeSubItemFromDataset(dataSet, index) { parseFloat(imagePositionPatient[1]) + normal[1] * offset,
const imageType = dataSet.string('x00080008'); parseFloat(imagePositionPatient[2]) + normal[2] * offset,
if (imageType) { ];
const subTypes = imageType.split('\\');
if (subTypes.length > index) {
return subTypes[index];
} }
} }
return undefined;
} // const imageOrientationPatient = getNumberValues(dataSet, 'x00200037', 6);
function isNMReconstructable(imageSubType) { // const imagePositionPatient = getNumberValues(dataSet, 'x00200032', 3);
return imageSubType === 'RECON TOMO' || imageSubType === 'RECON GATED TOMO'; // const pixelSpacing = extractSpacingFromDataset(dataSet);
} const imagePixelSpacing = getNumberValues(dataSet, "x00181164", 2);
function extractOrientationFromNMMultiframeDataset(dataSet) {
let imageOrientationPatient; // let rowCosines = null;
const modality = dataSet.string('x00080060');
if (modality?.includes('NM')) { // let columnCosines = null;
const imageSubType = getImageTypeSubItemFromDataset(dataSet, 2);
if (imageSubType && isNMReconstructable(imageSubType)) { // if (imageOrientationPatient) {
if (dataSet.elements.x00540022) { // rowCosines = [
imageOrientationPatient = getNumberValues(dataSet.elements.x00540022.items[0].dataSet, 'x00200037', 6); // parseFloat(imageOrientationPatient[0]),
} // parseFloat(imageOrientationPatient[1]),
} // parseFloat(imageOrientationPatient[2]),
} // ];
return imageOrientationPatient; // columnCosines = [
} // parseFloat(imageOrientationPatient[3]),
function extractOrientationFromDataset(dataSet) { // parseFloat(imageOrientationPatient[4]),
let imageOrientationPatient = getNumberValues(dataSet, 'x00200037', 6); // parseFloat(imageOrientationPatient[5]),
if (!imageOrientationPatient && dataSet.elements.x00209116) { // ];
imageOrientationPatient = getNumberValues(dataSet.elements.x00209116.items[0].dataSet, 'x00200037', 6); // }
} const estimatedRadiographicMagnificationFactor = getNumberValues(
if (!imageOrientationPatient) { dataSet,
imageOrientationPatient = "x00181114",
extractOrientationFromNMMultiframeDataset(dataSet); 2
} );
return imageOrientationPatient; let columnPixelSpacing = null;
}
function extractPositionFromNMMultiframeDataset(dataSet) { let rowPixelSpacing = null;
let imagePositionPatient;
const modality = dataSet.string('x00080060'); if (pixelSpacing) {
if (modality?.includes('NM')) { rowPixelSpacing = pixelSpacing[0];
const imageSubType = getImageTypeSubItemFromDataset(dataSet, 2); columnPixelSpacing = pixelSpacing[1];
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]
} else if (imagePixelSpacing && estimatedRadiographicMagnificationFactor) { } else if (imagePixelSpacing && estimatedRadiographicMagnificationFactor) {
rowPixelSpacing = imagePixelSpacing[0] / estimatedRadiographicMagnificationFactor[0]; rowPixelSpacing =
columnPixelSpacing = imagePixelSpacing[1] / estimatedRadiographicMagnificationFactor[1]; imagePixelSpacing[0] / estimatedRadiographicMagnificationFactor[0];
columnPixelSpacing =
imagePixelSpacing[1] / estimatedRadiographicMagnificationFactor[1];
} else if (imagePixelSpacing && !estimatedRadiographicMagnificationFactor) { } else if (imagePixelSpacing && !estimatedRadiographicMagnificationFactor) {
rowPixelSpacing = imagePixelSpacing[0]; rowPixelSpacing = imagePixelSpacing[0];
columnPixelSpacing = imagePixelSpacing[1]; 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; let rowCosines = null;
@ -160,34 +166,35 @@ function metaDataProvider(type, imageId) {
]; ];
} }
return { return {
frameOfReferenceUID: dataSet.string('x00200052'), frameOfReferenceUID: dataSet.string("x00200052"),
rows: dataSet.uint16('x00280010'), rows: dataSet.uint16("x00280010"),
columns: dataSet.uint16('x00280011'), columns: dataSet.uint16("x00280011"),
imageOrientationPatient, imageOrientationPatient,
rowCosines, rowCosines,
columnCosines, columnCosines,
imagePositionPatient, imagePositionPatient,
sliceThickness: dataSet.floatString('x00180050'), sliceThickness,
sliceLocation: dataSet.floatString('x00201041'), sliceLocation: dataSet.floatString("x00201041"),
pixelSpacing: pixelSpacing, pixelSpacing,
rowPixelSpacing: pixelSpacing ? pixelSpacing[0] : null, rowPixelSpacing,
columnPixelSpacing: pixelSpacing ? pixelSpacing[1] : null, columnPixelSpacing,
}; };
} }
if (type === 'nmMultiframeGeometryModule') { if (type === "nmMultiframeGeometryModule") {
const modality = dataSet.string('x00080060'); const modality = dataSet.string("x00080060");
const imageSubType = getImageTypeSubItemFromDataset(dataSet, 2); const imageSubType = getImageTypeSubItemFromDataset(dataSet, 2);
return { return {
modality, modality,
imageType: dataSet.string('x00080008'), imageType: dataSet.string("x00080008"),
imageSubType, imageSubType,
imageOrientationPatient: extractOrientationFromDataset(dataSet), imageOrientationPatient: extractOrientationFromDataset(dataSet),
imagePositionPatient: extractPositionFromDataset(dataSet), imagePositionPatient: extractPositionFromDataset(dataSet),
sliceThickness: extractSliceThicknessFromDataset(dataSet), sliceThickness: extractSliceThicknessFromDataset(dataSet),
pixelSpacing: extractSpacingFromDataset(dataSet), pixelSpacing: extractSpacingFromDataset(dataSet),
numberOfFrames: dataSet.uint16('x00280008'), numberOfFrames: dataSet.uint16("x00280008"),
isNMReconstructable: isNMReconstructable(imageSubType) && modality.includes('NM'), isNMReconstructable:
}; isNMReconstructable(imageSubType) && modality.includes("NM"),
};
} }
} }
export default metaDataProvider; export default metaDataProvider;

View File

@ -18,7 +18,7 @@
{{ `${series.TaskInfo.SubjectCode} ${series.TaskInfo.TaskBlindName} ` }} {{ `${series.TaskInfo.SubjectCode} ${series.TaskInfo.TaskBlindName} ` }}
</h2> </h2>
<div v-if="!isMip && !isFusion">Series: #{{ series.SeriesNumber }}</div> <div v-if="!isMip && !isFusion">Series: #{{ series.SeriesNumber }}</div>
<div v-if="series.Stack && !isMip">Image: #{{ `${series.SliceIndex + 1}/${series.Stack.length}` }}</div> <div v-if="series.Stack && !isMip">Image: #{{ `${series.SliceIndex + 1}/${imageInfo.total || series.Stack.length}` }}</div>
<div v-if="!isMip && !isFusion">{{ series.Modality }}</div> <div v-if="!isMip && !isFusion">{{ series.Modality }}</div>
<div v-if="isFusion">{{ ctSeries.Modality }} / {{ series.Modality }}</div> <div v-if="isFusion">{{ ctSeries.Modality }} / {{ series.Modality }}</div>
<div v-if="isMip">MIP</div> <div v-if="isMip">MIP</div>
@ -104,7 +104,7 @@ import {
setMipTransferFunctionForVolumeActor, setMipTransferFunctionForVolumeActor,
setPetTransferFunctionForVolumeActor setPetTransferFunctionForVolumeActor
} from './helpers/index.js' } from './helpers/index.js'
const { BlendModes, OrientationAxis } = Enums; const { BlendModes, OrientationAxis } = Enums;
const { getColormap } = csUtils.colormap; const { getColormap } = csUtils.colormap;
import { vec3, mat4 } from 'gl-matrix' import { vec3, mat4 } from 'gl-matrix'
export default { export default {
@ -150,7 +150,8 @@ export default {
size: null, size: null,
location: null, location: null,
sliceThickness: null, sliceThickness: null,
wwwc: null wwwc: null,
total: null
}, },
digitPlaces: 2, digitPlaces: 2,
orientationMarkers: [], orientationMarkers: [],
@ -225,7 +226,9 @@ export default {
if (this.series) { if (this.series) {
this.series.SliceIndex = detail.imageIndex 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 renderingEngine = getRenderingEngine(this.renderingEngineId)
const viewport = renderingEngine.getViewport(this.viewportId) const viewport = renderingEngine.getViewport(this.viewportId)
const zoom = viewport.getZoom() const zoom = viewport.getZoom()
@ -415,10 +418,7 @@ export default {
const renderingEngine = getRenderingEngine(this.renderingEngineId) const renderingEngine = getRenderingEngine(this.renderingEngineId)
const voiRange = { lower: 0, upper: v } const voiRange = { lower: 0, upper: v }
this.currentVoiUpper = v this.currentVoiUpper = v
if (this.isFusion) {
console.log('voiChange', v)
}
const viewport = renderingEngine.getViewport(this.viewportId) const viewport = renderingEngine.getViewport(this.viewportId)
if (!viewport) return if (!viewport) return
let volumeId = this.isFusion ? this.ptVolumeId : this.volumeId let volumeId = this.isFusion ? this.ptVolumeId : this.volumeId
@ -477,12 +477,21 @@ export default {
const renderingEngine = getRenderingEngine(this.renderingEngineId) const renderingEngine = getRenderingEngine(this.renderingEngineId)
const viewport = renderingEngine.getViewport(this.viewportId) const viewport = renderingEngine.getViewport(this.viewportId)
if (!viewport) return if (!viewport) return
let volumeId = this.isFusion ? this.ptVolumeId : this.volumeId
viewport.setProperties( if (this.isFusion) {
{ colormap: { opacity: Number(opacity) } }, const ctVolumeId = this.ctSeries?.SeriesInstanceUid
volumeId 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() viewport.render()
}, },
applyFusionOpacity() { applyFusionOpacity() {
@ -513,11 +522,14 @@ export default {
}, },
async createImageIdsAndCacheMetaData(obj) { async createImageIdsAndCacheMetaData(obj) {
this.loading = true this.loading = true
await createImageIdsAndCacheMetaData({ try {
modality: obj.Modality, return await createImageIdsAndCacheMetaData({
imageIds: obj.ImageIds modality: obj.Modality,
}) imageIds: obj.ImageIds
this.loading = false })
} finally {
this.loading = false
}
}, },
getFusionVolumes() { getFusionVolumes() {
const ctVolumeId = this.ctSeries?.SeriesInstanceUid const ctVolumeId = this.ctSeries?.SeriesInstanceUid
@ -532,8 +544,8 @@ export default {
setCtTransferFunctionForVolumeActor({ ...r, volumeId: ctVolumeId }) setCtTransferFunctionForVolumeActor({ ...r, volumeId: ctVolumeId })
if (this.fusionCtOnTop) { if (this.fusionCtOnTop) {
this.topFusionVolumeActor = r.volumeActor this.topFusionVolumeActor = r.volumeActor
this.applyFusionOpacity()
} }
this.applyFusionOpacity()
console.log("融合ct渲染成功") console.log("融合ct渲染成功")
} }
} }
@ -544,8 +556,8 @@ export default {
setPetColorMapTransferFunctionForVolumeActor({ ...r, volumeId: ptFusionVolumeId }) setPetColorMapTransferFunctionForVolumeActor({ ...r, volumeId: ptFusionVolumeId })
if (!this.fusionCtOnTop) { if (!this.fusionCtOnTop) {
this.topFusionVolumeActor = r.volumeActor this.topFusionVolumeActor = r.volumeActor
this.applyFusionOpacity()
} }
this.applyFusionOpacity()
console.log("融合pet渲染成功") console.log("融合pet渲染成功")
} }
} }
@ -745,8 +757,10 @@ export default {
clickSlider(e) { clickSlider(e) {
const height = e.offsetY * 100 / this.$refs['sliderBox'].clientHeight const height = e.offsetY * 100 / this.$refs['sliderBox'].clientHeight
this.sliderInfo.height = height this.sliderInfo.height = height
let sliceIdx = Math.trunc(this.series.Stack.length * height / 100) const totalSlices = this.imageInfo.total || this.series.Stack?.length || 1
sliceIdx = sliceIdx >= this.series.Stack.length ? this.series.Stack.length - 1 : sliceIdx < 0 ? 0 : sliceIdx 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 renderingEngine = getRenderingEngine(this.renderingEngineId)
const viewport = renderingEngine.getViewport( const viewport = renderingEngine.getViewport(
this.viewportId this.viewportId
@ -761,7 +775,7 @@ export default {
}, },
sliderMousedown(e) { sliderMousedown(e) {
const boxHeight = this.$refs['sliderBox'].clientHeight 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.oldM = e.clientY
this.sliderInfo.isMove = true this.sliderInfo.isMove = true
e.stopImmediatePropagation() e.stopImmediatePropagation()
@ -775,8 +789,10 @@ export default {
if (delta < 0) return if (delta < 0) return
if (delta > boxHeight) return if (delta > boxHeight) return
const height = delta * 100 / boxHeight const height = delta * 100 / boxHeight
let sliceIdx = Math.trunc(this.series.Stack.length * height / 100) const totalSlices = this.imageInfo.total || this.series.Stack?.length || 1
sliceIdx = sliceIdx >= this.series.Stack.length ? this.series.Stack.length - 1 : sliceIdx < 0 ? 0 : sliceIdx 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 this.sliderInfo.height = height
const renderingEngine = getRenderingEngine(this.renderingEngineId) const renderingEngine = getRenderingEngine(this.renderingEngineId)
const viewport = renderingEngine.getViewport( const viewport = renderingEngine.getViewport(
@ -927,7 +943,9 @@ export default {
.opacity-slider { .opacity-slider {
-webkit-appearance: slider-vertical; -webkit-appearance: slider-vertical;
writing-mode: bt-lr; appearance: slider-vertical;
writing-mode: vertical-lr;
direction: rtl;
width: 10px; width: 10px;
height: 150px; height: 150px;
padding: 0; padding: 0;

View File

@ -3710,8 +3710,8 @@ export default {
if (cache.getVolume(volumeId)) { if (cache.getVolume(volumeId)) {
volume = cache.getVolume(volumeId) volume = cache.getVolume(volumeId)
} else { } else {
await this.$refs[`${this.viewportKey}-0`][0].createImageIdsAndCacheMetaData(serie) let imageIds = await this.$refs[`${this.viewportKey}-0`][0].createImageIdsAndCacheMetaData(serie)
let imageIds = this.sortImageIdsByImagePositionPatient(serie.ImageIds) // imageIds = this.sortImageIdsByImagePositionPatient(imageIds)
volume = await volumeLoader.createAndCacheVolume(volumeId, { imageIds: imageIds }) volume = await volumeLoader.createAndCacheVolume(volumeId, { imageIds: imageIds })
volume.load() volume.load()
} }