irc_web/src/utils/metaDataProvider.js

524 lines
18 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

import * as cornerstoneWADOImageLoader from "cornerstone-wado-image-loader";
import store from "@/store";
import { getPTClinicalDataForInstance } from "@/utils/ptClinicalDataCache";
import {
getImageTypeSubItemFromDataset,
extractOrientationFromDataset,
extractPositionFromDataset,
extractSpacingFromDataset,
extractSliceThicknessFromDataset,
} from "./extractPositioningFromDataset";
function toNumber(val) {
if (val === undefined || val === null || val === "") return null;
const n = typeof val === "number" ? val : parseFloat(val);
return Number.isFinite(n) ? n : null;
}
function normalizeDicomTime(value) {
if (value === undefined || value === null || value === "") return null;
const num = toNumber(value);
if (num != null) {
return String(Math.floor(num)).padStart(6, "0");
}
if (typeof value !== "string") return null;
const trimmed = value.trim();
if (!trimmed) return null;
const plain = trimmed.replace(/:/g, "");
const parts = plain.split(".");
const main = parts[0];
const fractional = parts[1];
if (!/^\d+$/.test(main)) {
return trimmed;
}
const normalizedMain = main.padStart(6, "0");
return fractional ? `${normalizedMain}.${fractional}` : normalizedMain;
}
function getPTClinicalOverrideFromImageId(imageId) {
try {
const qIndex = imageId.indexOf("?");
if (qIndex === -1) return null;
const params = new URLSearchParams(imageId.slice(qIndex + 1));
const visitTaskId = params.get("visitTaskId");
const idx = params.get("idx");
// 场景1trials/dicomsimageId 带 visitTaskId + idx优先使用列表中的最新 study 值
if (visitTaskId && idx) {
const parts = idx.split("|");
const studyIndex = toNumber(parts[0]);
if (Number.isInteger(studyIndex) && studyIndex >= 0) {
const visitTaskList = store.state.reading.visitTaskList;
if (Array.isArray(visitTaskList)) {
const visitTaskInfo = visitTaskList.find(
(v) => v && String(v.VisitTaskId) === visitTaskId
);
const study = visitTaskInfo?.StudyList?.[studyIndex];
if (
study &&
!(
study.PatientWeight == null &&
study.RadionuclideTotalDose == null &&
study.RadionuclideHalfLife == null &&
study.RadiopharmaceuticalStartTime == null &&
study.AcquisitionTime == null &&
study.PatientSex == null
)
) {
return study;
}
}
}
}
// 场景2src/components/DicomimageId 仅带 instanceId回退到实例缓存
const instanceId = params.get("instanceId");
if (instanceId) {
const cached = getPTClinicalDataForInstance(instanceId);
if (cached) return cached;
}
return null;
} catch (e) {
return null;
}
}
function parseImageId(imageId) {
// 兼容 frame 参数不在最后的情况:只移除 frame 参数本身,保留 instanceId/visitTaskId/idx 等其余 query
const firstColonIndex = imageId.indexOf(":");
const scheme = imageId.substr(0, firstColonIndex);
const urlPart = imageId.substring(firstColonIndex + 1);
let url = urlPart;
let frame;
const qIndex = urlPart.indexOf("?");
if (qIndex !== -1) {
const base = urlPart.slice(0, qIndex);
const query = urlPart.slice(qIndex + 1);
const parts = query.split("&").filter(Boolean);
const preservedParts = [];
parts.forEach((part) => {
const eqIndex = part.indexOf("=");
const key = eqIndex === -1 ? part : part.slice(0, eqIndex);
const value = eqIndex === -1 ? "" : part.slice(eqIndex + 1);
if (key === "frame") {
const n = value !== "" ? parseInt(value, 10) : NaN;
if (Number.isFinite(n)) {
frame = n;
}
return;
}
// 保留原始 query 片段,避免 URLSearchParams 把 | 编码成 %7C 导致 dataSetCache key 对不上
preservedParts.push(part);
});
url = preservedParts.length ? `${base}?${preservedParts.join("&")}` : base;
}
return { scheme, url, frame };
}
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;
}
function getLutDescriptor(dataSet, tag) {
if (!dataSet.elements[tag] || dataSet.elements[tag].length !== 6) {
return;
}
return [
dataSet.uint16(tag, 0),
dataSet.uint16(tag, 1),
dataSet.uint16(tag, 2),
];
}
function getLutData(lutDataSet, tag, lutDescriptor) {
const lut = [];
const lutData = lutDataSet.elements[tag];
for (let i = 0; i < lutDescriptor[0]; i++) {
// Output range is always unsigned
if (lutDescriptor[2] === 16) {
lut[i] = lutDataSet.uint16(tag, i);
} else {
lut[i] = lutDataSet.byteArray[i + lutData.dataOffset];
}
}
return lut;
}
function populateSmallestLargestPixelValues(dataSet, imagePixelModule) {
const pixelRepresentation = dataSet.uint16("x00280103");
if (pixelRepresentation === 0) {
imagePixelModule.smallestPixelValue = dataSet.uint16("x00280106");
imagePixelModule.largestPixelValue = dataSet.uint16("x00280107");
} else {
imagePixelModule.smallestPixelValue = dataSet.int16("x00280106");
imagePixelModule.largestPixelValue = dataSet.int16("x00280107");
}
imagePixelModule.largestPixelValue =
imagePixelModule.largestPixelValue === 0
? undefined
: imagePixelModule.largestPixelValue;
}
function populatePaletteColorLut(dataSet, imagePixelModule) {
imagePixelModule.redPaletteColorLookupTableDescriptor = getLutDescriptor(
dataSet,
"x00281101"
);
imagePixelModule.greenPaletteColorLookupTableDescriptor = getLutDescriptor(
dataSet,
"x00281102"
);
imagePixelModule.bluePaletteColorLookupTableDescriptor = getLutDescriptor(
dataSet,
"x00281103"
);
// The first Palette Color Lookup Table Descriptor value is the number of entries in the lookup table.
// When the number of table entries is equal to 2ˆ16 then this value shall be 0.
// See http://dicom.nema.org/MEDICAL/DICOM/current/output/chtml/part03/sect_C.7.6.3.html#sect_C.7.6.3.1.5
if (imagePixelModule.redPaletteColorLookupTableDescriptor[0] === 0) {
imagePixelModule.redPaletteColorLookupTableDescriptor[0] = 65536;
imagePixelModule.greenPaletteColorLookupTableDescriptor[0] = 65536;
imagePixelModule.bluePaletteColorLookupTableDescriptor[0] = 65536;
}
// The third Palette Color Lookup Table Descriptor value specifies the number of bits for each entry in the Lookup Table Data.
// It shall take the value of 8 or 16.
// The LUT Data shall be stored in a format equivalent to 8 bits allocated when the number of bits for each entry is 8, and 16 bits allocated when the number of bits for each entry is 16, where in both cases the high bit is equal to bits allocated-1.
// The third value shall be identical for each of the Red, Green and Blue Palette Color Lookup Table Descriptors.
//
// Note: Some implementations have encoded 8 bit entries with 16 bits allocated, padding the high bits;
// this can be detected by comparing the number of entries specified in the LUT Descriptor with the actual value length of the LUT Data entry.
// The value length in bytes should equal the number of entries if bits allocated is 8, and be twice as long if bits allocated is 16.
const numLutEntries =
imagePixelModule.redPaletteColorLookupTableDescriptor[0];
const lutData = dataSet.elements.x00281201;
const lutBitsAllocated = lutData.length === numLutEntries ? 8 : 16;
// If the descriptors do not appear to have the correct values, correct them
if (
imagePixelModule.redPaletteColorLookupTableDescriptor[2] !==
lutBitsAllocated
) {
imagePixelModule.redPaletteColorLookupTableDescriptor[2] = lutBitsAllocated;
imagePixelModule.greenPaletteColorLookupTableDescriptor[2] =
lutBitsAllocated;
imagePixelModule.bluePaletteColorLookupTableDescriptor[2] =
lutBitsAllocated;
}
imagePixelModule.redPaletteColorLookupTableData = getLutData(
dataSet,
"x00281201",
imagePixelModule.redPaletteColorLookupTableDescriptor
);
imagePixelModule.greenPaletteColorLookupTableData = getLutData(
dataSet,
"x00281202",
imagePixelModule.greenPaletteColorLookupTableDescriptor
);
imagePixelModule.bluePaletteColorLookupTableData = getLutData(
dataSet,
"x00281203",
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
);
if (!dataSet) {
return;
}
const ptOverride = getPTClinicalOverrideFromImageId(imageId);
if (type === "generalSeriesModule") {
const { dicomParser } = cornerstoneWADOImageLoader.external || {};
const overrideTime = normalizeDicomTime(ptOverride?.AcquisitionTime);
const seriesTimeRaw =
overrideTime ||
dataSet.string("x00080031") ||
dataSet.string("x00080032") ||
"";
const acquisitionTimeRaw =
overrideTime ||
dataSet.string("x00080032") ||
dataSet.string("x00080031") ||
"";
return {
modality: dataSet.string("x00080060"),
seriesInstanceUID: dataSet.string("x0020000e"),
seriesNumber: dataSet.intString("x00200011"),
studyInstanceUID: dataSet.string("x0020000d"),
seriesDate: dicomParser.parseDA(dataSet.string("x00080021")),
// 2D SUV 计算读取的是 seriesTime这里同步使用用户录入采集时间
// seriesTime: dicomParser.parseTM(String(seriesTimeRaw)),
seriesTime: dicomParser.parseTM(dataSet.string('x00080031') || ''),
acquisitionDate: dicomParser.parseDA(dataSet.string("x00080022") || ""),
acquisitionTime: dicomParser.parseTM(String(acquisitionTimeRaw)),
};
}
if (type === "patientStudyModule") {
const weightOverride = ptOverride?.PatientWeight;
const weight =
weightOverride
? toNumber(weightOverride)
: dataSet.floatString("x00101030");
return {
patientAge: dataSet.intString("x00101010"),
patientSize: dataSet.floatString("x00101020"),
patientWeight: weight,
};
}
if (type === "petIsotopeModule") {
const { dicomParser } = cornerstoneWADOImageLoader.external || {};
const radiopharmaceuticalInfo = dataSet.elements.x00540016;
if (radiopharmaceuticalInfo === undefined) {
return;
}
const firstRadiopharmaceuticalInfoDataSet =
radiopharmaceuticalInfo.items[0].dataSet;
const startTimeRaw =
firstRadiopharmaceuticalInfoDataSet.string("x00181072") || "";
const totalDoseRaw =
firstRadiopharmaceuticalInfoDataSet.floatString("x00181074");
const halfLifeRaw =
firstRadiopharmaceuticalInfoDataSet.floatString("x00181075");
let startTimeValue = normalizeDicomTime(
ptOverride?.RadiopharmaceuticalStartTime
);
if (!startTimeValue && startTimeRaw) {
startTimeValue = normalizeDicomTime(startTimeRaw) || startTimeRaw;
}
const radiopharmaceuticalStartTime = dicomParser.parseTM(
String(startTimeValue)
);
const overrideTotalDose =
ptOverride?.RadionuclideTotalDose != null &&
ptOverride.RadionuclideTotalDose !== ""
? toNumber(ptOverride.RadionuclideTotalDose)
: null;
const overrideHalfLife =
ptOverride?.RadionuclideHalfLife != null &&
ptOverride.RadionuclideHalfLife !== ""
? toNumber(ptOverride.RadionuclideHalfLife)
: null;
const radionuclideTotalDose =
overrideTotalDose != null ? overrideTotalDose : toNumber(totalDoseRaw);
const radionuclideHalfLife =
overrideHalfLife != null ? overrideHalfLife : toNumber(halfLifeRaw);
return {
radiopharmaceuticalInfo: {
radiopharmaceuticalStartTime,
radionuclideTotalDose,
radionuclideHalfLife,
},
};
}
if (type === "imagePlaneModule") {
// 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;
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];
} else if (imagePixelSpacing && !estimatedRadiographicMagnificationFactor) {
rowPixelSpacing = imagePixelSpacing[0];
columnPixelSpacing = imagePixelSpacing[1];
}
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]),
];
}
return {
frameOfReferenceUID: dataSet.string("x00200052"),
rows: dataSet.uint16("x00280010"),
columns: dataSet.uint16("x00280011"),
imageOrientationPatient,
rowCosines,
columnCosines,
imagePositionPatient,
sliceThickness,
sliceLocation: dataSet.floatString("x00201041"),
pixelSpacing,
rowPixelSpacing,
columnPixelSpacing,
};
}
if (type === "imagePixelModule") {
const imagePixelModule = {
samplesPerPixel: dataSet.uint16("x00280002"),
photometricInterpretation: dataSet.string("x00280004"),
rows: dataSet.uint16("x00280010"),
columns: dataSet.uint16("x00280011"),
bitsAllocated: dataSet.uint16("x00280100"),
bitsStored: dataSet.uint16("x00280101"),
highBit: dataSet.uint16("x00280102"),
pixelRepresentation: dataSet.uint16("x00280103"),
planarConfiguration: dataSet.uint16("x00280006"),
pixelAspectRatio: dataSet.string("x00280034"),
};
populateSmallestLargestPixelValues(dataSet, imagePixelModule);
if (
imagePixelModule.photometricInterpretation === "PALETTE COLOR" &&
dataSet.elements.x00281101
) {
populatePaletteColorLut(dataSet, imagePixelModule);
}
return imagePixelModule;
}
// if (type === "multiframeModule") {
// return {
// NumberOfFrames: dataSet.uint16("x00280008") || dataSet.string("x00280008")
// };
// }
// if (type === 'imagePixelModule') {
// return {
// samplesPerPixel: dataSet.uint16('x00280002'),
// photometricInterpretation: dataSet.string('x00280004'),
// rows: dataSet.uint16('x00280010'),
// columns: dataSet.uint16('x00280011'),
// bitsAllocated: dataSet.uint16('x00280100'),
// bitsStored: dataSet.uint16('x00280101'),
// highBit: dataSet.uint16('x00280102'),
// pixelRepresentation: dataSet.uint16('x00280103'),
// planarConfiguration: dataSet.uint16('x00280006'),
// pixelAspectRatio: dataSet.uint16('x00280034'),
// smallestPixelValue: null,
// largestPixelValue: null,
// // smallestPixelValue: dataSet.uint16('x00280106'),
// // largestPixelValue: dataSet.uint16('x00280107'),
// redPaletteColorLookupTableDescriptor: dataSet.string('x00281101'),
// greenPaletteColorLookupTableDescriptor: dataSet.string('x00281102'),
// bluePaletteColorLookupTableDescriptor: dataSet.string('x00281103'),
// redPaletteColorLookupTableData: dataSet.string('x00281201'),
// greenPaletteColorLookupTableData: dataSet.string('x00281202'),
// bluePaletteColorLookupTableData: dataSet.string('x00281203')
// }
// }
}
export default metaDataProvider;