PT临床数据更改
continuous-integration/drone/push Build is running Details

uat_us
caiyiling 2026-04-24 14:30:36 +08:00
parent f7dd68f7f3
commit ab372a9c29
12 changed files with 517 additions and 394 deletions

View File

@ -78,16 +78,18 @@
import * as cornerstone from 'cornerstone-core' import * as cornerstone from 'cornerstone-core'
import * as cornerstoneMath from 'cornerstone-math' import * as cornerstoneMath from 'cornerstone-math'
import * as cornerstoneTools from 'cornerstone-tools' import * as cornerstoneTools from 'cornerstone-tools'
import metaDataProvider from '@/utils/metaDataProvider'
const scroll = cornerstoneTools.import('util/scrollToIndex') const scroll = cornerstoneTools.import('util/scrollToIndex')
import Hammer from 'hammerjs' import Hammer from 'hammerjs'
import getOrientationString from '@/views/trials/trials-panel/reading/dicoms/tools/OrientationMarkers/getOrientationString' import getOrientationString from '@/views/trials/trials-panel/reading/dicoms/tools/OrientationMarkers/getOrientationString'
import invertOrientationString from '@/views/trials/trials-panel/reading/dicoms/tools/OrientationMarkers/invertOrientationString' import invertOrientationString from '@/views/trials/trials-panel/reading/dicoms/tools/OrientationMarkers/invertOrientationString'
import calculateSUV from '@/views/trials/trials-panel/reading/dicoms/tools/calculateSUV' const calculateSUV = cornerstoneTools.import('util/calculateSUV')
// import requestPoolManager from '@/utils/request-pool' // import requestPoolManager from '@/utils/request-pool'
import ScaleOverlayTool from '@/views/trials/trials-panel/reading/dicoms/tools/ScaleOverlay/ScaleOverlayTool' import ScaleOverlayTool from '@/views/trials/trials-panel/reading/dicoms/tools/ScaleOverlay/ScaleOverlayTool'
import Note_RectangleRoiTool from '@/views/trials/trials-panel/reading/dicoms/tools/RectangleRoi/Note_RectangleRoiTool' import Note_RectangleRoiTool from '@/views/trials/trials-panel/reading/dicoms/tools/RectangleRoi/Note_RectangleRoiTool'
let isMetaDataProviderAdded = false
cornerstoneTools.external.cornerstone = cornerstone cornerstoneTools.external.cornerstone = cornerstone
cornerstoneTools.external.Hammer = Hammer cornerstoneTools.external.Hammer = Hammer
cornerstoneTools.external.cornerstoneMath = cornerstoneMath cornerstoneTools.external.cornerstoneMath = cornerstoneMath
@ -217,6 +219,11 @@ export default {
// this.stack.instanceId = instanceId // this.stack.instanceId = instanceId
this.toolState.clipPlaying = false this.toolState.clipPlaying = false
const element = this.$refs.canvas const element = this.$refs.canvas
if (!isMetaDataProviderAdded) {
// metaDataProvider SUV 退 DICOM
cornerstone.metaData.addProvider(metaDataProvider, 100000)
isMetaDataProviderAdded = true
}
cornerstone.enable(element) cornerstone.enable(element)
cornerstoneTools.stopClip(this.canvas) cornerstoneTools.stopClip(this.canvas)
this.toolState.clipPlaying = false this.toolState.clipPlaying = false
@ -384,7 +391,7 @@ export default {
this.dicomInfo.age = data.string('x00101010') this.dicomInfo.age = data.string('x00101010')
this.dicomInfo.sex = data.string('x00100040') this.dicomInfo.sex = data.string('x00100040')
this.dicomInfo.acc = data.string('x00080050') // this.dicomInfo.acc = data.string('x00080050') //
this.dicomInfo.modality = data.string('x00080060') this.dicomInfo.modality = (data.string('x00080060') || '').trim()
this.dicomInfo.time = this.formatDicomDateTime( this.dicomInfo.time = this.formatDicomDateTime(
data.string('x00080020'), data.string('x00080020'),
data.string('x00080030') data.string('x00080030')

View File

@ -363,7 +363,7 @@ import {
getPatientInfo, getPatientInfo,
editPatientInfo editPatientInfo
} from '@/api/trials' } from '@/api/trials'
import { setPTClinicalDataForInstance } from '@/utils/ptClinicalDataCache' import { setPTClinicalDataForInstance, clearPTClinicalDataCache } from '@/utils/ptClinicalDataCache'
export default { export default {
name: 'DicomsViewer', name: 'DicomsViewer',
components: { components: {
@ -476,6 +476,9 @@ export default {
this.type = this.$route.query.type this.type = this.$route.query.type
this.isEdit = parseInt(this.$route.query.showDelete) this.isEdit = parseInt(this.$route.query.showDelete)
}, },
beforeDestroy() {
clearPTClinicalDataCache()
},
methods: { methods: {
anonymousImage() { anonymousImage() {
@ -554,7 +557,17 @@ export default {
this.currentDicomCanvas.toolState.clipPlaying = false this.currentDicomCanvas.toolState.clipPlaying = false
this.$nextTick(() => { this.$nextTick(() => {
this.series = Object.assign({}, dicomSeries) this.series = Object.assign({}, dicomSeries)
this.seriesList = [this.series]
this.currentDicomCanvas.loadImageStack(this.series) this.currentDicomCanvas.loadImageStack(this.series)
if (
this.formData.PatientWeight != null ||
this.formData.RadionuclideTotalDose != null ||
this.formData.RadionuclideHalfLife != null ||
this.formData.RadiopharmaceuticalStartTime != null ||
this.formData.AcquisitionTime != null
) {
this.cachePtClinicalDataToInstances()
}
}) })
}, },
loadOtherImageStack(seriesList) { loadOtherImageStack(seriesList) {
@ -568,6 +581,15 @@ export default {
this.$refs[`dicomCanvas${canvasIndex}`].loadImageStack(series) this.$refs[`dicomCanvas${canvasIndex}`].loadImageStack(series)
} }
}) })
if (
this.formData.PatientWeight != null ||
this.formData.RadionuclideTotalDose != null ||
this.formData.RadionuclideHalfLife != null ||
this.formData.RadiopharmaceuticalStartTime != null ||
this.formData.AcquisitionTime != null
) {
this.cachePtClinicalDataToInstances()
}
}) })
}, },
activateDicomCanvas(index) { activateDicomCanvas(index) {

View File

@ -1,115 +1,123 @@
import * as cornerstoneWADOImageLoader from 'cornerstone-wado-image-loader' import * as cornerstoneWADOImageLoader from "cornerstone-wado-image-loader";
import store from '@/store' import store from "@/store";
import { getPTClinicalDataForInstance } from '@/utils/ptClinicalDataCache' import { getPTClinicalDataForInstance } from "@/utils/ptClinicalDataCache";
import { import {
getImageTypeSubItemFromDataset, getImageTypeSubItemFromDataset,
extractOrientationFromDataset, extractOrientationFromDataset,
extractPositionFromDataset, extractPositionFromDataset,
extractSpacingFromDataset, extractSpacingFromDataset,
extractSliceThicknessFromDataset, extractSliceThicknessFromDataset,
} from './extractPositioningFromDataset'; } from "./extractPositioningFromDataset";
function toNumber(val) { function toNumber(val) {
if (val === undefined || val === null || val === '') return null; if (val === undefined || val === null || val === "") return null;
const n = typeof val === 'number' ? val : parseFloat(val); const n = typeof val === "number" ? val : parseFloat(val);
return Number.isFinite(n) ? n : null; return Number.isFinite(n) ? n : null;
} }
function parseDicomTimeToObject(value) { function normalizeDicomTime(value) {
if (value === undefined || value === null || value === '') return null; if (value === undefined || value === null || value === "") return null;
const raw = String(value).trim(); const num = toNumber(value);
if (!raw) return null; if (num != null) {
const cleaned = raw.replace(/[^\d.]/g, ''); return String(Math.floor(num)).padStart(6, "0");
if (!cleaned) return null;
const [baseRaw, fracRaw] = cleaned.split('.');
const base = `${baseRaw || ''}`.padStart(6, '0').slice(-6);
const hours = toNumber(base.slice(0, 2)) ?? 0;
const minutes = toNumber(base.slice(2, 4)) ?? 0;
const seconds = toNumber(base.slice(4, 6)) ?? 0;
const fractionalSeconds = `${fracRaw || ''}`.padEnd(6, '0').slice(0, 6);
if (!Number.isFinite(hours) || !Number.isFinite(minutes) || !Number.isFinite(seconds)) return null;
return { hours, minutes, seconds, fractionalSeconds };
}
function getFirstSequenceItemDataSet(dataSet, tag) {
const el = dataSet?.elements?.[tag];
const item = el?.items?.[0];
return item?.dataSet;
}
function getIntString(dataSet, tag) {
if (!dataSet) return undefined;
if (typeof dataSet.intString === 'function') {
return dataSet.intString(tag);
} }
const raw = dataSet.string(tag); if (typeof value !== "string") return null;
const n = raw != null ? parseInt(raw, 10) : NaN; const trimmed = value.trim();
return Number.isFinite(n) ? n : undefined; 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) { function getPTClinicalOverrideFromImageId(imageId) {
try { try {
const qIndex0 = imageId.indexOf('?'); const qIndex = imageId.indexOf("?");
if (qIndex0 !== -1) {
const params0 = new URLSearchParams(imageId.slice(qIndex0 + 1));
const instanceId0 = params0.get('instanceId');
if (instanceId0) {
const cached = getPTClinicalDataForInstance(instanceId0);
if (cached) return cached;
}
}
const qIndex = imageId.indexOf('?');
if (qIndex === -1) return null; if (qIndex === -1) return null;
const params = new URLSearchParams(imageId.slice(qIndex + 1)); const params = new URLSearchParams(imageId.slice(qIndex + 1));
const visitTaskId = params.get('visitTaskId'); const visitTaskId = params.get("visitTaskId");
const idx = params.get('idx'); const idx = params.get("idx");
if (!visitTaskId || !idx) return null;
const parts = idx.split('|'); // 场景1trials/dicomsimageId 带 visitTaskId + idx优先使用列表中的最新 study 值
if (visitTaskId && idx) {
const parts = idx.split("|");
const studyIndex = toNumber(parts[0]); const studyIndex = toNumber(parts[0]);
if (!Number.isInteger(studyIndex) || studyIndex < 0) return null; if (Number.isInteger(studyIndex) && studyIndex >= 0) {
const visitTaskList = store.state.reading.visitTaskList;
const visitTaskList = store?.state?.reading?.visitTaskList; if (Array.isArray(visitTaskList)) {
if (!Array.isArray(visitTaskList)) return null; const visitTaskInfo = visitTaskList.find(
const visitTaskInfo = visitTaskList.find(v => v && v.VisitTaskId === visitTaskId); (v) => v && String(v.VisitTaskId) === visitTaskId
);
const study = visitTaskInfo?.StudyList?.[studyIndex]; const study = visitTaskInfo?.StudyList?.[studyIndex];
if (!study) return null;
if ( if (
study &&
!(
study.PatientWeight == null && study.PatientWeight == null &&
study.RadionuclideTotalDose == null && study.RadionuclideTotalDose == null &&
study.RadionuclideHalfLife == null && study.RadionuclideHalfLife == null &&
study.RadiopharmaceuticalStartTime == null && study.RadiopharmaceuticalStartTime == null &&
study.AcquisitionTime == null && study.AcquisitionTime == null &&
study.PatientSex == null study.PatientSex == null
)
) { ) {
return null;
}
return study; 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) { } catch (e) {
return null; return null;
} }
} }
function parseImageId(imageId) { function parseImageId(imageId) {
// build a url by parsing out the url scheme and frame index from the imageId // 兼容 frame 参数不在最后的情况:只移除 frame 参数本身,保留 instanceId/visitTaskId/idx 等其余 query
const firstColonIndex = imageId.indexOf(':'); const firstColonIndex = imageId.indexOf(":");
const scheme = imageId.substr(0, firstColonIndex);
let url = imageId.substring(firstColonIndex + 1); const urlPart = imageId.substring(firstColonIndex + 1);
const frameIndex = url.indexOf('frame='); let url = urlPart;
let frame; 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 = [];
if (frameIndex !== -1) { parts.forEach((part) => {
const frameStr = url.substr(frameIndex + 6); const eqIndex = part.indexOf("=");
const key = eqIndex === -1 ? part : part.slice(0, eqIndex);
const value = eqIndex === -1 ? "" : part.slice(eqIndex + 1);
frame = parseInt(frameStr, 10); if (key === "frame") {
url = url.substr(0, frameIndex - 1); const n = value !== "" ? parseInt(value, 10) : NaN;
if (Number.isFinite(n)) {
frame = n;
}
return;
} }
return { // 保留原始 query 片段,避免 URLSearchParams 把 | 编码成 %7C 导致 dataSetCache key 对不上
scheme: imageId.substr(0, firstColonIndex), preservedParts.push(part);
url, });
frame,
}; url = preservedParts.length ? `${base}?${preservedParts.join("&")}` : base;
}
return { scheme, url, frame };
} }
function getNumberValues(dataSet, tag, minimumLength) { function getNumberValues(dataSet, tag, minimumLength) {
const values = []; const values = [];
@ -118,7 +126,7 @@ function getNumberValues(dataSet, tag, minimumLength) {
if (!valueAsString) { if (!valueAsString) {
return; return;
} }
const split = valueAsString.split('\\'); const split = valueAsString.split("\\");
if (minimumLength && split.length < minimumLength) { if (minimumLength && split.length < minimumLength) {
return; return;
@ -157,28 +165,31 @@ function getLutData(lutDataSet, tag, lutDescriptor) {
return lut; return lut;
} }
function populateSmallestLargestPixelValues(dataSet, imagePixelModule) { function populateSmallestLargestPixelValues(dataSet, imagePixelModule) {
const pixelRepresentation = dataSet.uint16('x00280103'); const pixelRepresentation = dataSet.uint16("x00280103");
if (pixelRepresentation === 0) { if (pixelRepresentation === 0) {
imagePixelModule.smallestPixelValue = dataSet.uint16('x00280106'); imagePixelModule.smallestPixelValue = dataSet.uint16("x00280106");
imagePixelModule.largestPixelValue = dataSet.uint16('x00280107'); imagePixelModule.largestPixelValue = dataSet.uint16("x00280107");
} else { } else {
imagePixelModule.smallestPixelValue = dataSet.int16('x00280106'); imagePixelModule.smallestPixelValue = dataSet.int16("x00280106");
imagePixelModule.largestPixelValue = dataSet.int16('x00280107'); imagePixelModule.largestPixelValue = dataSet.int16("x00280107");
} }
imagePixelModule.largestPixelValue = imagePixelModule.largestPixelValue === 0 ? undefined : imagePixelModule.largestPixelValue; imagePixelModule.largestPixelValue =
imagePixelModule.largestPixelValue === 0
? undefined
: imagePixelModule.largestPixelValue;
} }
function populatePaletteColorLut(dataSet, imagePixelModule) { function populatePaletteColorLut(dataSet, imagePixelModule) {
imagePixelModule.redPaletteColorLookupTableDescriptor = getLutDescriptor( imagePixelModule.redPaletteColorLookupTableDescriptor = getLutDescriptor(
dataSet, dataSet,
'x00281101' "x00281101"
); );
imagePixelModule.greenPaletteColorLookupTableDescriptor = getLutDescriptor( imagePixelModule.greenPaletteColorLookupTableDescriptor = getLutDescriptor(
dataSet, dataSet,
'x00281102' "x00281102"
); );
imagePixelModule.bluePaletteColorLookupTableDescriptor = getLutDescriptor( imagePixelModule.bluePaletteColorLookupTableDescriptor = getLutDescriptor(
dataSet, dataSet,
'x00281103' "x00281103"
); );
// The first Palette Color Lookup Table Descriptor value is the number of entries in the lookup table. // The first Palette Color Lookup Table Descriptor value is the number of entries in the lookup table.
@ -217,115 +228,165 @@ function populatePaletteColorLut(dataSet, imagePixelModule) {
imagePixelModule.redPaletteColorLookupTableData = getLutData( imagePixelModule.redPaletteColorLookupTableData = getLutData(
dataSet, dataSet,
'x00281201', "x00281201",
imagePixelModule.redPaletteColorLookupTableDescriptor imagePixelModule.redPaletteColorLookupTableDescriptor
); );
imagePixelModule.greenPaletteColorLookupTableData = getLutData( imagePixelModule.greenPaletteColorLookupTableData = getLutData(
dataSet, dataSet,
'x00281202', "x00281202",
imagePixelModule.greenPaletteColorLookupTableDescriptor imagePixelModule.greenPaletteColorLookupTableDescriptor
); );
imagePixelModule.bluePaletteColorLookupTableData = getLutData( imagePixelModule.bluePaletteColorLookupTableData = getLutData(
dataSet, dataSet,
'x00281203', "x00281203",
imagePixelModule.bluePaletteColorLookupTableDescriptor imagePixelModule.bluePaletteColorLookupTableDescriptor
); );
} }
function getSpacingBetweenSlices(dataSet) { function getSpacingBetweenSlices(dataSet) {
if (dataSet?.elements?.x00180088) { if (dataSet?.elements?.x00180088) {
return dataSet.floatString('x00180088'); return dataSet.floatString("x00180088");
} }
const pixelMeasuresSequence = dataSet?.elements?.x00289110; const pixelMeasuresSequence = dataSet?.elements?.x00289110;
if ( if (
pixelMeasuresSequence?.items?.length && pixelMeasuresSequence?.items?.length &&
pixelMeasuresSequence.items[0]?.dataSet?.elements?.x00180088 pixelMeasuresSequence.items[0]?.dataSet?.elements?.x00180088
) { ) {
return pixelMeasuresSequence.items[0].dataSet.floatString('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
);
if (!dataSet) { if (!dataSet) {
return; return;
} }
const ptOverride = getPTClinicalOverrideFromImageId(imageId); const ptOverride = getPTClinicalOverrideFromImageId(imageId);
if (type === 'generalSeriesModule') { if (type === "generalSeriesModule") {
// 参照 cornerstoneWADOImageLoader 的 module 结构返回,避免调用方拿不到预期字段 const { dicomParser } = cornerstoneWADOImageLoader.external || {};
const dicomParser = cornerstoneWADOImageLoader?.external?.dicomParser; const overrideTime = normalizeDicomTime(ptOverride?.AcquisitionTime);
const modality = dataSet.string('x00080060'); const seriesTimeRaw =
const seriesDateRaw = dataSet.string('x00080021') || ''; overrideTime ||
const seriesTimeRaw = dataSet.string('x00080031') || ''; dataSet.string("x00080031") ||
const acquisitionDateRaw = dataSet.string('x00080022') || ''; dataSet.string("x00080032") ||
const acquisitionTimeRaw = dataSet.string('x00080032') || ''; "";
const seriesTimeValue = ptOverride?.AcquisitionTime ?? seriesTimeRaw; const acquisitionTimeRaw =
overrideTime ||
dataSet.string("x00080032") ||
dataSet.string("x00080031") ||
"";
return { return {
modality, modality: dataSet.string("x00080060"),
seriesInstanceUID: dataSet.string('x0020000e'), seriesInstanceUID: dataSet.string("x0020000e"),
seriesNumber: getIntString(dataSet, 'x00200011'), seriesNumber: dataSet.intString("x00200011"),
studyInstanceUID: dataSet.string('x0020000d'), studyInstanceUID: dataSet.string("x0020000d"),
seriesDate: dicomParser?.parseDA ? dicomParser.parseDA(seriesDateRaw) : seriesDateRaw, seriesDate: dicomParser.parseDA(dataSet.string("x00080021")),
seriesTime: dicomParser?.parseTM ? dicomParser.parseTM(seriesTimeValue || '') : parseDicomTimeToObject(seriesTimeValue), // 2D SUV 计算读取的是 seriesTime这里同步使用用户录入采集时间
acquisitionDate: dicomParser?.parseDA ? dicomParser.parseDA(acquisitionDateRaw) : acquisitionDateRaw, // seriesTime: dicomParser.parseTM(String(seriesTimeRaw)),
acquisitionTime: dicomParser?.parseTM ? dicomParser.parseTM(acquisitionTimeRaw || '') : parseDicomTimeToObject(acquisitionTimeRaw) seriesTime: dicomParser.parseTM(dataSet.string('x00080031') || ''),
acquisitionDate: dicomParser.parseDA(dataSet.string("x00080022") || ""),
acquisitionTime: dicomParser.parseTM(String(acquisitionTimeRaw)),
}; };
} }
if (type === 'patientStudyModule') { if (type === "patientStudyModule") {
// 参照 cornerstoneWADOImageLoader 的 module 结构返回 const weightOverride = ptOverride?.PatientWeight;
const patientWeightRaw = dataSet.floatString('x00101030') || dataSet.string('x00101030'); const weight =
// const patientSexRaw = dataSet.string('x00100040'); weightOverride
const patientSizeRaw = dataSet.floatString('x00101020') || dataSet.string('x00101020'); ? toNumber(weightOverride)
const patientAge = getIntString(dataSet, 'x00101010'); : dataSet.floatString("x00101030");
const patientWeight = toNumber(ptOverride?.PatientWeight ?? patientWeightRaw);
// const patientSex = ptOverride?.PatientSex ?? patientSexRaw;
const patientSize = toNumber(patientSizeRaw);
return { return {
patientAge, patientAge: dataSet.intString("x00101010"),
patientWeight, patientSize: dataSet.floatString("x00101020"),
// patientSex, patientWeight: weight,
patientSize
}; };
} }
if (type === 'petIsotopeModule') {
// 统一 SUV 口径:优先使用接口/人工录入的 PT 临床数据,缺失时回退读取 DICOM Tag if (type === "petIsotopeModule") {
// 同时保持与 cornerstoneWADOImageLoader 返回结构一致radiopharmaceuticalInfo.* const { dicomParser } = cornerstoneWADOImageLoader.external || {};
const dicomParser = cornerstoneWADOImageLoader?.external?.dicomParser; const radiopharmaceuticalInfo = dataSet.elements.x00540016;
const radioPharmItem = getFirstSequenceItemDataSet(dataSet, 'x00540016');
const startTimeRaw = radioPharmItem?.string?.('x00181072') || dataSet.string('x00181072'); if (radiopharmaceuticalInfo === undefined) {
const totalDoseRaw = radioPharmItem?.floatString?.('x00181074') || radioPharmItem?.string?.('x00181074') || dataSet.floatString('x00181074') || dataSet.string('x00181074'); return;
const halfLifeRaw = radioPharmItem?.floatString?.('x00181075') || radioPharmItem?.string?.('x00181075') || dataSet.floatString('x00181075') || dataSet.string('x00181075'); }
const startTimeValue = ptOverride?.RadiopharmaceuticalStartTime ?? startTimeRaw; const firstRadiopharmaceuticalInfoDataSet =
const radiopharmaceuticalStartTime = dicomParser?.parseTM radiopharmaceuticalInfo.items[0].dataSet;
? dicomParser.parseTM(startTimeValue || '')
: parseDicomTimeToObject(startTimeValue); const startTimeRaw =
const radionuclideTotalDose = toNumber(ptOverride?.RadionuclideTotalDose ?? totalDoseRaw); firstRadiopharmaceuticalInfoDataSet.string("x00181072") || "";
const radionuclideHalfLife = toNumber(ptOverride?.RadionuclideHalfLife ?? halfLifeRaw); 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 { return {
radiopharmaceuticalInfo: { radiopharmaceuticalInfo: {
radiopharmaceuticalStartTime, radiopharmaceuticalStartTime,
radionuclideTotalDose, radionuclideTotalDose,
radionuclideHalfLife radionuclideHalfLife,
} },
}; };
} }
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 frameIndex =
const imageOrientationPatient = extractOrientationFromDataset(dataSet, frameIndex); parsedImageId.frame !== undefined ? parsedImageId.frame - 1 : undefined;
const imageOrientationPatient = extractOrientationFromDataset(
dataSet,
frameIndex
);
let imagePositionPatient = extractPositionFromDataset(dataSet, frameIndex); let imagePositionPatient = extractPositionFromDataset(dataSet, frameIndex);
const pixelSpacing = extractSpacingFromDataset(dataSet, frameIndex); const pixelSpacing = extractSpacingFromDataset(dataSet, frameIndex);
const sliceThickness = extractSliceThicknessFromDataset(dataSet, frameIndex); const sliceThickness = extractSliceThicknessFromDataset(
dataSet,
frameIndex
);
const modality = dataSet.string("x00080060"); const modality = dataSet.string("x00080060");
if (modality && modality.includes('NM') && parsedImageId.frame !== undefined && parsedImageId.frame > 1) { if (
modality &&
modality.includes("NM") &&
parsedImageId.frame !== undefined &&
parsedImageId.frame > 1
) {
const spacingBetweenSlices = getSpacingBetweenSlices(dataSet); const spacingBetweenSlices = getSpacingBetweenSlices(dataSet);
const step = spacingBetweenSlices !== undefined ? spacingBetweenSlices : sliceThickness; const step =
if (imageOrientationPatient && imagePositionPatient && step !== undefined && frameIndex !== undefined) { spacingBetweenSlices !== undefined
? spacingBetweenSlices
: sliceThickness;
if (
imageOrientationPatient &&
imagePositionPatient &&
step !== undefined &&
frameIndex !== undefined
) {
const rowCosines = [ const rowCosines = [
parseFloat(imageOrientationPatient[0]), parseFloat(imageOrientationPatient[0]),
parseFloat(imageOrientationPatient[1]), parseFloat(imageOrientationPatient[1]),
@ -352,7 +413,11 @@ function metaDataProvider(type, imageId) {
} }
} }
const estimatedRadiographicMagnificationFactor = getNumberValues(dataSet, 'x00181114', 2); const estimatedRadiographicMagnificationFactor = getNumberValues(
dataSet,
"x00181114",
2
);
let columnPixelSpacing = null; let columnPixelSpacing = null;
let rowPixelSpacing = null; let rowPixelSpacing = null;
@ -361,8 +426,10 @@ function metaDataProvider(type, imageId) {
rowPixelSpacing = pixelSpacing[0]; rowPixelSpacing = pixelSpacing[0];
columnPixelSpacing = pixelSpacing[1]; 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];
@ -386,37 +453,37 @@ 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, sliceThickness,
sliceLocation: dataSet.floatString('x00201041'), sliceLocation: dataSet.floatString("x00201041"),
pixelSpacing, pixelSpacing,
rowPixelSpacing, rowPixelSpacing,
columnPixelSpacing, columnPixelSpacing,
}; };
} }
if (type === 'imagePixelModule') { if (type === "imagePixelModule") {
const imagePixelModule = { const imagePixelModule = {
samplesPerPixel: dataSet.uint16('x00280002'), samplesPerPixel: dataSet.uint16("x00280002"),
photometricInterpretation: dataSet.string('x00280004'), photometricInterpretation: dataSet.string("x00280004"),
rows: dataSet.uint16('x00280010'), rows: dataSet.uint16("x00280010"),
columns: dataSet.uint16('x00280011'), columns: dataSet.uint16("x00280011"),
bitsAllocated: dataSet.uint16('x00280100'), bitsAllocated: dataSet.uint16("x00280100"),
bitsStored: dataSet.uint16('x00280101'), bitsStored: dataSet.uint16("x00280101"),
highBit: dataSet.uint16('x00280102'), highBit: dataSet.uint16("x00280102"),
pixelRepresentation: dataSet.uint16('x00280103'), pixelRepresentation: dataSet.uint16("x00280103"),
planarConfiguration: dataSet.uint16('x00280006'), planarConfiguration: dataSet.uint16("x00280006"),
pixelAspectRatio: dataSet.string('x00280034'), pixelAspectRatio: dataSet.string("x00280034"),
}; };
populateSmallestLargestPixelValues(dataSet, imagePixelModule); populateSmallestLargestPixelValues(dataSet, imagePixelModule);
if ( if (
imagePixelModule.photometricInterpretation === 'PALETTE COLOR' && imagePixelModule.photometricInterpretation === "PALETTE COLOR" &&
dataSet.elements.x00281101 dataSet.elements.x00281101
) { ) {
populatePaletteColorLut(dataSet, imagePixelModule); populatePaletteColorLut(dataSet, imagePixelModule);

View File

@ -11,7 +11,7 @@ export function setPTClinicalDataForInstance(instanceId, clinicalData) {
const key = normalizeId(instanceId) const key = normalizeId(instanceId)
if (!key) return if (!key) return
if (!clinicalData || typeof clinicalData !== 'object') return if (!clinicalData || typeof clinicalData !== 'object') return
instanceIdToClinicalData.set(key, clinicalData) instanceIdToClinicalData.set(key, { ...clinicalData })
} }
export function getPTClinicalDataForInstance(instanceId) { export function getPTClinicalDataForInstance(instanceId) {
@ -20,7 +20,12 @@ export function getPTClinicalDataForInstance(instanceId) {
return instanceIdToClinicalData.get(key) || null return instanceIdToClinicalData.get(key) || null
} }
export function deletePTClinicalDataForInstance(instanceId) {
const key = normalizeId(instanceId)
if (!key) return
instanceIdToClinicalData.delete(key)
}
export function clearPTClinicalDataCache() { export function clearPTClinicalDataCache() {
instanceIdToClinicalData.clear() instanceIdToClinicalData.clear()
} }

View File

@ -313,6 +313,10 @@ export default {
isExistsClinicalData: { isExistsClinicalData: {
type: Boolean, type: Boolean,
required: true required: true
},
imageToolType: {
type: Number,
required: true
} }
}, },
data() { data() {
@ -551,7 +555,7 @@ export default {
// window.opener.postMessage('refreshTaskList', window.location) // window.opener.postMessage('refreshTaskList', window.location)
// //
this.adInfo.ReadingTaskState = 2 this.adInfo.ReadingTaskState = 2
const res = await getAutoCutNextTask() const res = await getAutoCutNextTask({imageToolType: this.imageToolType})
var isAutoTask = res.Result.AutoCutNextTask var isAutoTask = res.Result.AutoCutNextTask
if (isAutoTask) { if (isAutoTask) {
// store.dispatch('reading/resetVisitTasks') // store.dispatch('reading/resetVisitTasks')

View File

@ -1134,7 +1134,8 @@ export default {
// resolve() // resolve()
// }) // })
this.loading = true this.loading = true
cornerstone.metaData.addProvider(metaDataProvider, 1); cornerstone.metaData.removeProvider(metaDataProvider)
cornerstone.metaData.addProvider(metaDataProvider, 100000)
cornerstone.loadAndCacheImage(this.stack.imageIds[this.stack.currentImageIdIndex]) cornerstone.loadAndCacheImage(this.stack.imageIds[this.stack.currentImageIdIndex])
.then(async image => { .then(async image => {
if (this.stack.imageIds.indexOf(image.imageId) !== -1) { if (this.stack.imageIds.indexOf(image.imageId) !== -1) {

View File

@ -2,20 +2,37 @@ import { metaData } from '@cornerstonejs/core'
// import { InstanceMetadata } from '@cornerstonejs/calculate-suv' // import { InstanceMetadata } from '@cornerstonejs/calculate-suv'
import cornerstoneDICOMImageLoader from '@cornerstonejs/dicom-image-loader' import cornerstoneDICOMImageLoader from '@cornerstonejs/dicom-image-loader'
import { getPTClinicalDataForInstance } from '@/utils/ptClinicalDataCache' import { getPTClinicalDataForInstance } from '@/utils/ptClinicalDataCache'
function parseImageId(imageId) { 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(':')
const urlPart = imageId.substring(firstColonIndex + 1)
let url = imageId.substring(firstColonIndex + 1) let url = urlPart
const frameIndex = url.indexOf('frame=')
let frame let frame
const qIndex = urlPart.indexOf('?')
if (frameIndex !== -1) { if (qIndex !== -1) {
const frameStr = url.substr(frameIndex + 6) const base = urlPart.slice(0, qIndex)
const query = urlPart.slice(qIndex + 1)
const parts = query.split('&').filter(Boolean)
const preservedParts = []
frame = parseInt(frameStr, 10) parts.forEach((part) => {
url = url.substr(0, frameIndex - 1) 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
}
preservedParts.push(part)
})
url = preservedParts.length ? `${base}?${preservedParts.join('&')}` : base
} }
return { return {
@ -24,6 +41,7 @@ function parseImageId(imageId) {
frame frame
} }
} }
function getMetaData(type, imageId) { function getMetaData(type, imageId) {
// const { dicomParser } = cornerstoneDICOMImageLoader.external // const { dicomParser } = cornerstoneDICOMImageLoader.external
const parsedImageId = parseImageId(imageId) const parsedImageId = parseImageId(imageId)
@ -33,6 +51,7 @@ function getMetaData(type, imageId) {
if (!dataSet) { if (!dataSet) {
return return
} }
if (type === 'petImageModule') { if (type === 'petImageModule') {
// 1340137.4196974 240000 // 1340137.4196974 240000
// console.log(dataSet.string('x00541300'), dataSet.string('x00181242')) // console.log(dataSet.string('x00541300'), dataSet.string('x00181242'))
@ -42,194 +61,69 @@ function getMetaData(type, imageId) {
} }
} }
} }
export default function getPTImageIdInstanceMetadata(imageId) { export default function getPTImageIdInstanceMetadata(imageId) {
const instanceId = getInstanceIdFromImageId(imageId) const instanceId = getInstanceIdFromImageId(imageId)
// 统一 SUV 口径:若接口存在 PT 临床数据,则优先覆盖 DICOM 元数据
const ptClinicalData = instanceId ? getPTClinicalDataForInstance(instanceId) : null const ptClinicalData = instanceId ? getPTClinicalDataForInstance(instanceId) : null
const petSequenceModule = metaData.get('petIsotopeModule', imageId) const petSequenceModule = metaData.get('petIsotopeModule', imageId)
const generalSeriesModule = metaData.get('generalSeriesModule', imageId) || {}
const patientStudyModule = metaData.get('patientStudyModule', imageId) || {}
const ptSeriesModule = metaData.get('petSeriesModule', imageId) || {}
const ptImageModule = getMetaData('petImageModule', imageId) || {}
const radiopharmaceuticalInfo = petSequenceModule?.radiopharmaceuticalInfo
const generalSeriesModule = metaData.get('generalSeriesModule', imageId) if (!radiopharmaceuticalInfo) {
const patientStudyModule = metaData.get('patientStudyModule', imageId)
const ptSeriesModule = metaData.get('petSeriesModule', imageId)
// const ptImageModule = metaData.get('petImageModule', imageId)
const ptImageModule = getMetaData('petImageModule', imageId)
if (!petSequenceModule) {
throw new Error('petSequenceModule metadata is required') throw new Error('petSequenceModule metadata is required')
} }
const radiopharmaceuticalInfo = petSequenceModule.radiopharmaceuticalInfo const patientWeight = toNumber(
ptClinicalData?.PatientWeight,
const { seriesDate, seriesTime, acquisitionDate, acquisitionTime } = patientStudyModule.patientWeight
generalSeriesModule )
var { patientWeight } = patientStudyModule const totalDose = toNumber(
if (ptClinicalData && ptClinicalData.PatientWeight != null && ptClinicalData.PatientWeight !== '') { ptClinicalData?.RadionuclideTotalDose,
patientWeight = parseFloat(ptClinicalData.PatientWeight) radiopharmaceuticalInfo.radionuclideTotalDose
} )
// console.log('更改前:', patientWeight) const halfLife = toNumber(
// patientWeight = patientWeight * 10 ptClinicalData?.RadionuclideHalfLife,
// console.log('更改后:', patientWeight) radiopharmaceuticalInfo.radionuclideHalfLife
const { correctedImage, units, decayCorrection } = ptSeriesModule )
const totalDose = ptClinicalData && ptClinicalData.RadionuclideTotalDose != null && ptClinicalData.RadionuclideTotalDose !== '' const startTime = firstValue(
? parseFloat(ptClinicalData.RadionuclideTotalDose) normalizeDicomTime(ptClinicalData?.RadiopharmaceuticalStartTime),
: radiopharmaceuticalInfo.radionuclideTotalDose toDicomTimeString(radiopharmaceuticalInfo.radiopharmaceuticalStartTime)
const halfLife = ptClinicalData && ptClinicalData.RadionuclideHalfLife != null && ptClinicalData.RadionuclideHalfLife !== '' )
? parseFloat(ptClinicalData.RadionuclideHalfLife) const acquisitionTime = firstValue(
: radiopharmaceuticalInfo.radionuclideHalfLife normalizeDicomTime(ptClinicalData?.AcquisitionTime),
const startTimeOverride = ptClinicalData ? normalizeDicomTime(ptClinicalData.RadiopharmaceuticalStartTime) : null toDicomTimeString(generalSeriesModule.acquisitionTime)
const acquisitionTimeOverride = ptClinicalData ? normalizeDicomTime(ptClinicalData.AcquisitionTime) : null )
if (
seriesDate === undefined ||
seriesTime === undefined ||
patientWeight === undefined ||
acquisitionDate === undefined ||
(acquisitionTimeOverride || acquisitionTime) === undefined ||
correctedImage === undefined ||
units === undefined ||
decayCorrection === undefined ||
totalDose === undefined ||
halfLife === undefined ||
(radiopharmaceuticalInfo.radiopharmaceuticalStartDateTime === undefined &&
seriesDate === undefined &&
(startTimeOverride || radiopharmaceuticalInfo.radiopharmaceuticalStartTime) === undefined)
//
) {
throw new Error('required metadata are missing')
}
const instanceMetadata = { const instanceMetadata = {
CorrectedImage: correctedImage, CorrectedImage: ptSeriesModule.correctedImage,
Units: units, Units: ptSeriesModule.units,
RadionuclideHalfLife: halfLife, RadionuclideHalfLife: halfLife,
RadionuclideTotalDose: totalDose, RadionuclideTotalDose: totalDose,
DecayCorrection: decayCorrection, DecayCorrection: ptSeriesModule.decayCorrection,
PatientWeight: patientWeight, PatientWeight: patientWeight,
SeriesDate: seriesDate, SeriesDate: toDicomDateString(generalSeriesModule.seriesDate),
SeriesTime: seriesTime, SeriesTime: toDicomTimeString(generalSeriesModule.seriesTime),
AcquisitionDate: acquisitionDate, AcquisitionDate: toDicomDateString(generalSeriesModule.acquisitionDate),
AcquisitionTime: acquisitionTimeOverride || acquisitionTime AcquisitionTime: acquisitionTime
} }
if ( assignIfPresent(
radiopharmaceuticalInfo.radiopharmaceuticalStartDateTime && instanceMetadata,
radiopharmaceuticalInfo.radiopharmaceuticalStartDateTime !== undefined && 'RadiopharmaceuticalStartDateTime',
typeof radiopharmaceuticalInfo.radiopharmaceuticalStartDateTime === 'string' toDicomDateString(radiopharmaceuticalInfo.radiopharmaceuticalStartDateTime)
) {
instanceMetadata.RadiopharmaceuticalStartDateTime =
radiopharmaceuticalInfo.radiopharmaceuticalStartDateTime
}
if (
radiopharmaceuticalInfo.radiopharmaceuticalStartDateTime &&
radiopharmaceuticalInfo.radiopharmaceuticalStartDateTime !== undefined &&
typeof radiopharmaceuticalInfo.radiopharmaceuticalStartDateTime !== 'string'
) {
const dateString = convertInterfaceDateToString(
radiopharmaceuticalInfo.radiopharmaceuticalStartDateTime
) )
instanceMetadata.RadiopharmaceuticalStartDateTime = dateString assignIfPresent(instanceMetadata, 'RadiopharmaceuticalStartTime', startTime)
} assignIfPresent(instanceMetadata, 'FrameReferenceTime', ptImageModule.frameReferenceTime)
assignIfPresent(instanceMetadata, 'ActualFrameDuration', ptImageModule.actualFrameDuration)
if ( assignIfPresent(
instanceMetadata.AcquisitionDate && instanceMetadata,
instanceMetadata.AcquisitionDate !== undefined && 'PatientSex',
typeof instanceMetadata.AcquisitionDate !== 'string' firstValue(ptClinicalData?.PatientSex, patientStudyModule.patientSex)
) {
const dateString = convertInterfaceDateToString(
instanceMetadata.AcquisitionDate
) )
instanceMetadata.AcquisitionDate = dateString assignIfPresent(instanceMetadata, 'PatientSize', patientStudyModule.patientSize)
}
if (
instanceMetadata.SeriesDate &&
instanceMetadata.SeriesDate !== undefined &&
typeof instanceMetadata.SeriesDate !== 'string'
) {
const dateString = convertInterfaceDateToString(
instanceMetadata.SeriesDate
)
instanceMetadata.SeriesDate = dateString
}
if (
radiopharmaceuticalInfo.radiopharmaceuticalStartTime &&
radiopharmaceuticalInfo.radiopharmaceuticalStartTime !== undefined &&
typeof radiopharmaceuticalInfo.radiopharmaceuticalStartTime === 'string'
) {
instanceMetadata.RadiopharmaceuticalStartTime =
radiopharmaceuticalInfo.radiopharmaceuticalStartTime
}
if (
radiopharmaceuticalInfo.radiopharmaceuticalStartTime &&
radiopharmaceuticalInfo.radiopharmaceuticalStartTime !== undefined &&
typeof radiopharmaceuticalInfo.radiopharmaceuticalStartTime !== 'string'
) {
const timeString = convertInterfaceTimeToString(
radiopharmaceuticalInfo.radiopharmaceuticalStartTime
)
instanceMetadata.RadiopharmaceuticalStartTime = timeString
}
if (startTimeOverride) {
instanceMetadata.RadiopharmaceuticalStartTime = startTimeOverride
}
if (
instanceMetadata.AcquisitionTime &&
instanceMetadata.AcquisitionTime !== undefined &&
typeof instanceMetadata.AcquisitionTime !== 'string'
) {
const timeString = convertInterfaceTimeToString(
instanceMetadata.AcquisitionTime
)
instanceMetadata.AcquisitionTime = timeString
}
if (
instanceMetadata.SeriesTime &&
instanceMetadata.SeriesTime !== undefined &&
typeof instanceMetadata.SeriesTime !== 'string'
) {
const timeString = convertInterfaceTimeToString(
instanceMetadata.SeriesTime
)
instanceMetadata.SeriesTime = timeString
}
if (
ptImageModule.frameReferenceTime &&
ptImageModule.frameReferenceTime !== undefined
) {
instanceMetadata.FrameReferenceTime = ptImageModule.frameReferenceTime
}
if (
ptImageModule.actualFrameDuration &&
ptImageModule.actualFrameDuration !== undefined
) {
instanceMetadata.ActualFrameDuration = ptImageModule.actualFrameDuration
}
if (
patientStudyModule.patientSex &&
patientStudyModule.patientSex !== undefined
) {
instanceMetadata.PatientSex = patientStudyModule.patientSex
}
if (ptClinicalData && ptClinicalData.PatientSex != null && ptClinicalData.PatientSex !== '') {
instanceMetadata.PatientSex = ptClinicalData.PatientSex
}
if (
patientStudyModule.patientSize &&
patientStudyModule.patientSize !== undefined
) {
instanceMetadata.PatientSize = patientStudyModule.patientSize
}
return instanceMetadata return instanceMetadata
} }
@ -244,15 +138,13 @@ function convertInterfaceTimeToString(time) {
'0' '0'
) )
const timeString = `${hours}${minutes}${seconds}.${fractionalSeconds}` return `${hours}${minutes}${seconds}.${fractionalSeconds}`
return timeString
} }
function convertInterfaceDateToString(date) { function convertInterfaceDateToString(date) {
const month = `${date.month}`.padStart(2, '0') const month = `${date.month}`.padStart(2, '0')
const day = `${date.day}`.padStart(2, '0') const day = `${date.day}`.padStart(2, '0')
const dateString = `${date.year}${month}${day}` return `${date.year}${month}${day}`
return dateString
} }
export { getPTImageIdInstanceMetadata } export { getPTImageIdInstanceMetadata }
@ -271,7 +163,7 @@ function getInstanceIdFromImageId(imageId) {
} }
function normalizeDicomTime(value) { function normalizeDicomTime(value) {
if (value === undefined || value === null || value === '') return null if (!hasValue(value)) return null
if (typeof value === 'object') { if (typeof value === 'object') {
return convertInterfaceTimeToString(value) return convertInterfaceTimeToString(value)
} }
@ -293,3 +185,34 @@ function normalizeDicomTime(value) {
const frac = `${fracRaw || ''}`.padEnd(6, '0').slice(0, 6) const frac = `${fracRaw || ''}`.padEnd(6, '0').slice(0, 6)
return `${base}.${frac}` return `${base}.${frac}`
} }
function hasValue(value) {
return value !== undefined && value !== null && value !== ''
}
function firstValue(...values) {
return values.find(hasValue)
}
function toNumber(...values) {
const value = firstValue(...values)
if (!hasValue(value)) return value
const numberValue = Number(value)
return Number.isFinite(numberValue) ? numberValue : value
}
function toDicomTimeString(value) {
if (!hasValue(value)) return undefined
return typeof value === 'string' ? value : convertInterfaceTimeToString(value)
}
function toDicomDateString(value) {
if (!hasValue(value)) return undefined
return typeof value === 'string' ? value : convertInterfaceDateToString(value)
}
function assignIfPresent(target, key, value) {
if (!hasValue(value)) return
target[key] = value
}

View File

@ -2,14 +2,28 @@ import { utilities as csUtils } from '@cornerstonejs/core'
const scalingPerImageId = {} const scalingPerImageId = {}
function normalizeImageURI(imageURI) {
if (!imageURI) return imageURI
const qIndex = imageURI.indexOf('?')
if (qIndex === -1) return imageURI
const base = imageURI.slice(0, qIndex)
const query = imageURI.slice(qIndex + 1)
const params = new URLSearchParams(query)
if (!params.has('frame')) return imageURI
params.delete('frame')
const rest = params.toString()
return rest ? `${base}?${rest}` : base
}
function addInstance(imageId, scalingMetaData) { function addInstance(imageId, scalingMetaData) {
const imageURI = csUtils.imageIdToURI(imageId) // 统一缩放元数据的 key忽略 frame 参数,避免 getCurrentImageId() 带 frame 导致查不到 scalingModule
const imageURI = normalizeImageURI(csUtils.imageIdToURI(imageId))
scalingPerImageId[imageURI] = scalingMetaData scalingPerImageId[imageURI] = scalingMetaData
} }
function get(type, imageId) { function get(type, imageId) {
if (type === 'scalingModule') { if (type === 'scalingModule') {
const imageURI = csUtils.imageIdToURI(imageId) const imageURI = normalizeImageURI(csUtils.imageIdToURI(imageId))
return scalingPerImageId[imageURI] return scalingPerImageId[imageURI]
} }
} }

View File

@ -99,6 +99,37 @@
<el-tooltip class="item" effect="dark" :content="series.description" placement="right"> <el-tooltip class="item" effect="dark" :content="series.description" placement="right">
<div style="">{{ series.description }}</div> <div style="">{{ series.description }}</div>
</el-tooltip> </el-tooltip>
<div class="patient-info" style="position: absolute;right: 0;top: 30px;" v-if="['PT','PET'].includes(series.modality)">
<el-popover placement="right" trigger="hover" popper-class="patient-info-popper">
<h4>{{ $t('trials:ptData:title') }}</h4>
<div class="patient-info-row">
<label>{{ $t('trials:ptData:label:patientSex') }}</label>
<span>{{ study.PatientSex }}</span>
</div>
<div class="patient-info-row">
<label>{{ $t('trials:ptData:label:patientWeight') }}</label>
<span>{{ study.PatientWeight }}</span>
</div>
<div class="patient-info-row">
<label>{{ $t('trials:ptData:label:totalDose') }}</label>
<span>{{ study.RadionuclideTotalDose }}</span>
</div>
<div class="patient-info-row">
<label>{{ $t('trials:ptData:label:halfLife') }}</label>
<span>{{ study.RadionuclideHalfLife }}</span>
</div>
<div class="patient-info-row">
<label>{{ $t('trials:ptData:label:injectTime') }}</label>
<span>{{ study.RadiopharmaceuticalStartTime }}</span>
</div>
<div class="patient-info-row">
<label>{{ $t('trials:ptData:label:acquisitionTime') }}</label>
<span>{{ study.AcquisitionTime }}</span>
</div>
<i slot="reference" class="el-icon-document"
style="font-size: 15px;cursor: pointer;color: #f5f7fa;" />
</el-popover>
</div>
</p> </p>
<p v-show="series.sliceThickness && !study.IsCriticalSequence"> <p v-show="series.sliceThickness && !study.IsCriticalSequence">
@ -1108,3 +1139,46 @@ export default {
background-color: #213a54; background-color: #213a54;
} }
</style> </style>
<style lang="scss">
.patient-info-popper {
font-size: 12px;
color: #ddd;
background-color: #2f2f2f;
border-color: #5a5a5a;
padding: 8px 10px;
}
.patient-info-popper[x-placement^='right'] .popper__arrow {
border-right-color: #5a5a5a;
}
.patient-info-popper[x-placement^='right'] .popper__arrow::after {
border-right-color: #2f2f2f;
}
.patient-info-popper .patient-info-row {
display: grid;
grid-template-columns: 100px minmax(0, 1fr);
column-gap: 12px;
align-items: center;
line-height: 18px;
}
.patient-info-popper .patient-info-row + .patient-info-row {
margin-top: 4px;
}
.patient-info-popper label {
color: #bbb;
// font-weight: 600;
white-space: nowrap;
}
.patient-info-popper span {
text-align: left;
min-width: 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
</style>

View File

@ -1081,7 +1081,8 @@ export default {
// resolve() // resolve()
// }) // })
this.loading = true this.loading = true
cornerstone.metaData.addProvider(metaDataProvider, 1); cornerstone.metaData.removeProvider(metaDataProvider)
cornerstone.metaData.addProvider(metaDataProvider, 100000)
cornerstone.loadAndCacheImage(this.stack.imageIds[this.stack.currentImageIdIndex]) cornerstone.loadAndCacheImage(this.stack.imageIds[this.stack.currentImageIdIndex])
.then(async image => { .then(async image => {
if (this.stack.imageIds.indexOf(image.imageId) !== -1) { if (this.stack.imageIds.indexOf(image.imageId) !== -1) {

View File

@ -577,7 +577,7 @@ import colorMap from './colorMap.vue'
import RectangleROITool from './tools/RectangleROITool' import RectangleROITool from './tools/RectangleROITool'
import ScaleOverlayTool from './tools/ScaleOverlayTool' import ScaleOverlayTool from './tools/ScaleOverlayTool'
import SegmentBidirectionalTool from './tools/SegmentBidirectionalTool' import SegmentBidirectionalTool from './tools/SegmentBidirectionalTool'
import { setPTClinicalDataForInstance } from '@/utils/ptClinicalDataCache' import { setPTClinicalDataForInstance, clearPTClinicalDataCache } from '@/utils/ptClinicalDataCache'
import FixedRadiusCircleROITool from './tools/FixedRadiusCircleROITool' import FixedRadiusCircleROITool from './tools/FixedRadiusCircleROITool'
import uploadDicomAndNonedicom from '@/components/uploadDicomAndNonedicom' import uploadDicomAndNonedicom from '@/components/uploadDicomAndNonedicom'
import downloadDicomAndNonedicom from '@/components/downloadDicomAndNonedicom' import downloadDicomAndNonedicom from '@/components/downloadDicomAndNonedicom'
@ -1156,7 +1156,7 @@ export default {
let keySeriesIndex = -1 let keySeriesIndex = -1
const arr = res1.Result const arr = res1.Result
arr.forEach((study, studyIndex) => { arr.forEach((study, studyIndex) => {
// PT SUV 3D suvFactor // PT/PET study 3D SUV DICOM
const ptClinicalData = { const ptClinicalData = {
PatientSex: study.PatientSex, PatientSex: study.PatientSex,
PatientWeight: study.PatientWeight, PatientWeight: study.PatientWeight,
@ -1165,17 +1165,21 @@ export default {
RadiopharmaceuticalStartTime: study.RadiopharmaceuticalStartTime, RadiopharmaceuticalStartTime: study.RadiopharmaceuticalStartTime,
AcquisitionTime: study.AcquisitionTime AcquisitionTime: study.AcquisitionTime
} }
const isPtStudy = ['PT、CT', 'CT、PT', 'PET-CT'].includes(study.Modalities)
const hasPtClinicalData = const hasPtClinicalData =
ptClinicalData.PatientWeight != null || isPtStudy &&
ptClinicalData.RadionuclideTotalDose != null || (
ptClinicalData.RadionuclideHalfLife != null || ptClinicalData.PatientWeight !== null ||
ptClinicalData.RadiopharmaceuticalStartTime != null || ptClinicalData.RadionuclideTotalDose !== null ||
ptClinicalData.AcquisitionTime != null ptClinicalData.RadionuclideHalfLife !== null ||
ptClinicalData.RadiopharmaceuticalStartTime !== null ||
ptClinicalData.AcquisitionTime !== null
)
study.SeriesList.forEach((series, seriesIndex) => { study.SeriesList.forEach((series, seriesIndex) => {
const imageIds = [] const imageIds = []
const stack = [] const stack = []
series.InstanceInfoList.forEach((instance, instanceIndex) => { series.InstanceInfoList.forEach((instance, instanceIndex) => {
if (hasPtClinicalData) { if (hasPtClinicalData && ['PT', 'PET'].includes(String(series.Modality).toUpperCase())) {
setPTClinicalDataForInstance(instance.Id, ptClinicalData) setPTClinicalDataForInstance(instance.Id, ptClinicalData)
} }
if (study.IsCriticalSequence) { if (study.IsCriticalSequence) {
@ -4423,6 +4427,7 @@ export default {
}, },
}, },
beforeDestroy() { beforeDestroy() {
clearPTClinicalDataCache()
DicomEvent.$off('isCanActiveNoneDicomTool') DicomEvent.$off('isCanActiveNoneDicomTool')
DicomEvent.$off('removeNoneDicomMeasureData') DicomEvent.$off('removeNoneDicomMeasureData')
DicomEvent.$off('addNoneDicomMeasureData') DicomEvent.$off('addNoneDicomMeasureData')

View File

@ -76,7 +76,7 @@
</div> </div>
<div v-if="series.Description" class="text-desc" :title="series.Description" style="position: relative;"> <div v-if="series.Description" class="text-desc" :title="series.Description" style="position: relative;">
{{ series.Description }} {{ series.Description }}
<div class="patient-info" style="position: absolute;right: 0;top: 0;" v-if="taskInfo && taskInfo.IsExistsClinicalData && ['PT','PET'].includes(series.Modality)"> <div class="patient-info" style="position: absolute;right: 0;top: 0;" v-if="['PT','PET'].includes(series.Modality)">
<el-popover placement="right" trigger="hover" popper-class="patient-info-popper"> <el-popover placement="right" trigger="hover" popper-class="patient-info-popper">
<h4>{{ $t('trials:ptData:title') }}</h4> <h4>{{ $t('trials:ptData:title') }}</h4>
<div class="patient-info-row"> <div class="patient-info-row">