2026-07-11 18:16:32 +02:00
|
|
|
import OHIF, { utils } from '@ohif/core';
|
2022-07-27 18:39:04 +02:00
|
|
|
|
2026-07-11 18:16:32 +02:00
|
|
|
import type { InstanceMetadata, PhilipsPETPrivateGroup } from '@cornerstonejs/calculate-suv';
|
2022-07-27 18:39:04 +02:00
|
|
|
|
|
|
|
|
const metadataProvider = OHIF.classes.MetadataProvider;
|
|
|
|
|
|
2023-09-01 22:19:39 +02:00
|
|
|
export default function getPTImageIdInstanceMetadata(imageId: string): InstanceMetadata {
|
2022-07-27 18:39:04 +02:00
|
|
|
const dicomMetaData = metadataProvider.get('instance', imageId);
|
|
|
|
|
|
|
|
|
|
if (!dicomMetaData) {
|
|
|
|
|
throw new Error('dicom metadata are required');
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-11 18:16:32 +02:00
|
|
|
const radiopharmaceuticalInfo = firstSequenceItem<Record<string, unknown>>(
|
|
|
|
|
dicomMetaData.RadiopharmaceuticalInformationSequence
|
|
|
|
|
);
|
|
|
|
|
const radionuclideHalfLife = coerceNumber(radiopharmaceuticalInfo?.RadionuclideHalfLife);
|
|
|
|
|
const radionuclideTotalDose = coerceNumber(radiopharmaceuticalInfo?.RadionuclideTotalDose);
|
|
|
|
|
|
2022-07-27 18:39:04 +02:00
|
|
|
if (
|
|
|
|
|
dicomMetaData.SeriesDate === undefined ||
|
|
|
|
|
dicomMetaData.SeriesTime === undefined ||
|
|
|
|
|
dicomMetaData.CorrectedImage === undefined ||
|
|
|
|
|
dicomMetaData.Units === undefined ||
|
2026-07-11 18:16:32 +02:00
|
|
|
!radiopharmaceuticalInfo ||
|
|
|
|
|
radionuclideHalfLife === undefined ||
|
|
|
|
|
radionuclideTotalDose === undefined ||
|
2022-07-27 18:39:04 +02:00
|
|
|
dicomMetaData.DecayCorrection === undefined ||
|
|
|
|
|
dicomMetaData.AcquisitionDate === undefined ||
|
|
|
|
|
dicomMetaData.AcquisitionTime === undefined ||
|
2026-07-11 18:16:32 +02:00
|
|
|
(radiopharmaceuticalInfo.RadiopharmaceuticalStartDateTime === undefined &&
|
|
|
|
|
radiopharmaceuticalInfo.RadiopharmaceuticalStartTime === undefined)
|
2022-07-27 18:39:04 +02:00
|
|
|
) {
|
|
|
|
|
throw new Error('required metadata are missing');
|
|
|
|
|
}
|
|
|
|
|
|
2023-08-29 18:21:21 +02:00
|
|
|
if (dicomMetaData.PatientWeight === undefined) {
|
|
|
|
|
console.warn('PatientWeight missing from PT instance metadata');
|
|
|
|
|
}
|
|
|
|
|
|
2022-07-27 18:39:04 +02:00
|
|
|
const instanceMetadata: InstanceMetadata = {
|
|
|
|
|
CorrectedImage: dicomMetaData.CorrectedImage,
|
|
|
|
|
Units: dicomMetaData.Units,
|
2026-07-11 18:16:32 +02:00
|
|
|
RadionuclideHalfLife: radionuclideHalfLife,
|
|
|
|
|
RadionuclideTotalDose: radionuclideTotalDose,
|
|
|
|
|
RadiopharmaceuticalStartDateTime: radiopharmaceuticalInfo.RadiopharmaceuticalStartDateTime,
|
|
|
|
|
RadiopharmaceuticalStartTime: radiopharmaceuticalInfo.RadiopharmaceuticalStartTime,
|
2022-07-27 18:39:04 +02:00
|
|
|
DecayCorrection: dicomMetaData.DecayCorrection,
|
2026-07-11 18:16:32 +02:00
|
|
|
PatientWeight: coerceNumber(dicomMetaData.PatientWeight),
|
2022-07-27 18:39:04 +02:00
|
|
|
SeriesDate: dicomMetaData.SeriesDate,
|
|
|
|
|
SeriesTime: dicomMetaData.SeriesTime,
|
|
|
|
|
AcquisitionDate: dicomMetaData.AcquisitionDate,
|
|
|
|
|
AcquisitionTime: dicomMetaData.AcquisitionTime,
|
|
|
|
|
};
|
|
|
|
|
|
2026-07-11 18:16:32 +02:00
|
|
|
// Philips PET private group. Only populated with values that coerce to real
|
|
|
|
|
// numbers; an unresolved bulkdata object yields undefined and is dropped so it
|
|
|
|
|
// can never corrupt the SUV calculation. SUVScaleFactor is (7053,1000) and
|
|
|
|
|
// ActivityConcentrationScaleFactor is (7053,1009). These are resolved from
|
|
|
|
|
// bulkdata upstream during ingestion (utils.resolveBulkDataTags).
|
|
|
|
|
const suvScaleFactor = coerceNumber(dicomMetaData['70531000']);
|
|
|
|
|
const activityConcentrationScaleFactor = coerceNumber(dicomMetaData['70531009']);
|
|
|
|
|
if (suvScaleFactor !== undefined || activityConcentrationScaleFactor !== undefined) {
|
2022-07-27 18:39:04 +02:00
|
|
|
const philipsPETPrivateGroup: PhilipsPETPrivateGroup = {
|
2026-07-11 18:16:32 +02:00
|
|
|
SUVScaleFactor: suvScaleFactor,
|
|
|
|
|
ActivityConcentrationScaleFactor: activityConcentrationScaleFactor,
|
2022-07-27 18:39:04 +02:00
|
|
|
};
|
|
|
|
|
instanceMetadata.PhilipsPETPrivateGroup = philipsPETPrivateGroup;
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-11 18:16:32 +02:00
|
|
|
if (dicomMetaData['0009100d'] !== undefined) {
|
2022-07-27 18:39:04 +02:00
|
|
|
instanceMetadata.GEPrivatePostInjectionDateTime = dicomMetaData['0009100d'];
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-11 18:16:32 +02:00
|
|
|
const frameReferenceTime = coerceNumber(dicomMetaData.FrameReferenceTime);
|
|
|
|
|
if (frameReferenceTime !== undefined) {
|
|
|
|
|
instanceMetadata.FrameReferenceTime = frameReferenceTime;
|
2022-07-27 18:39:04 +02:00
|
|
|
}
|
|
|
|
|
|
2026-07-11 18:16:32 +02:00
|
|
|
const actualFrameDuration = coerceNumber(dicomMetaData.ActualFrameDuration);
|
|
|
|
|
if (actualFrameDuration !== undefined) {
|
|
|
|
|
instanceMetadata.ActualFrameDuration = actualFrameDuration;
|
2022-07-27 18:39:04 +02:00
|
|
|
}
|
|
|
|
|
|
2026-07-11 18:16:32 +02:00
|
|
|
if (dicomMetaData.PatientSex !== undefined) {
|
2022-07-27 18:39:04 +02:00
|
|
|
instanceMetadata.PatientSex = dicomMetaData.PatientSex;
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-11 18:16:32 +02:00
|
|
|
const patientSize = coerceNumber(dicomMetaData.PatientSize);
|
|
|
|
|
if (patientSize !== undefined) {
|
|
|
|
|
instanceMetadata.PatientSize = patientSize;
|
2022-07-27 18:39:04 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return instanceMetadata;
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-11 18:16:32 +02:00
|
|
|
export { getPTImageIdInstanceMetadata };
|
2022-07-27 18:39:04 +02:00
|
|
|
|
2026-07-11 18:16:32 +02:00
|
|
|
/**
|
|
|
|
|
* Coerces a naturalized DICOM value into a finite number, or returns undefined.
|
|
|
|
|
*
|
|
|
|
|
* Delegates to OHIF's `utils.toNumber` and then requires a finite scalar, so an
|
|
|
|
|
* object value - such as an unresolved bulkdata reference `{ BulkDataURI }` or
|
|
|
|
|
* an array - becomes undefined. This is the final backstop ensuring such a value
|
|
|
|
|
* can never reach calculate-suv (which treats it as truthy and silently corrupts
|
|
|
|
|
* the SUV factors). Bulkdata is meant to be resolved upstream during ingestion
|
|
|
|
|
* (see utils.resolveBulkDataTags); this guard catches anything that slips
|
|
|
|
|
* through.
|
|
|
|
|
*/
|
|
|
|
|
function coerceNumber(value: unknown): number | undefined {
|
|
|
|
|
const n = utils.toNumber(value);
|
|
|
|
|
return typeof n === 'number' && Number.isFinite(n) ? n : undefined;
|
2022-07-27 18:39:04 +02:00
|
|
|
}
|
|
|
|
|
|
2026-07-11 18:16:32 +02:00
|
|
|
/**
|
|
|
|
|
* Returns the first item of a DICOM sequence, tolerating either the dcmjs
|
|
|
|
|
* naturalized array shape or an already-flattened single-object shape.
|
|
|
|
|
*/
|
|
|
|
|
function firstSequenceItem<T = Record<string, unknown>>(seq: unknown): T | undefined {
|
|
|
|
|
if (seq == null || typeof seq !== 'object') {
|
|
|
|
|
return undefined;
|
|
|
|
|
}
|
|
|
|
|
return (Array.isArray(seq) ? seq[0] : seq) as T;
|
2022-07-27 18:39:04 +02:00
|
|
|
}
|