/* * This script uses nodejs to generate a JSON file from a DICOM study folder. * You need to have dcmjs installed in your project. * The JSON file can be used to load the study into the OHIF Viewer. You can get more detail * in the DICOM JSON Data source on docs.ohif.org * * Usage: node dicom-json-generator.js <studyFolder> <urlPrefix> <outputJSONPath> <optional scheme> * * params: * - studyFolder: path to the study folder which contains the DICOM files * - urlPrefix: prefix to the url that will be used to load the study into the viewer. For instance * we use https://ohif-assets.s3.us-east-2.amazonaws.com/dicom-json/data as the urlPrefix for the * example since the data is hosted on S3 and each study is in a folder. So the url in the generated * json file for the first instance of the first series of the first study will be * dicomweb:https://ohif-assets.s3.us-east-2.amazonaws.com/dicom-json/data/Series1/Instance1 * * as you see the dicomweb is a prefix that is used to load the data into the viewer, which is suited when * the .dcm file is hosted statically and can be accessed via a URL (like our example above) * However, you can specify a new scheme bellow. * * - outputJSONPath: path to the output JSON file * - scheme: default dicomweb if not provided */ const dcmjs = require('dcmjs'); const path = require('path'); const fs = require('fs').promises; const args = process.argv.slice(2); const [studyDirectory, urlPrefix, outputPath, scheme = 'dicomweb'] = args; if (args.length < 3 || args.length > 4) { console.error( 'Usage: node dicom-json-generator.js <studyFolder> <urlPrefix> <outputJSONPath> [scheme]' ); process.exit(1); } const model = { studies: [], }; async function convertDICOMToJSON(studyDirectory, urlPrefix, outputPath, scheme) { try { const files = await recursiveReadDir(studyDirectory); console.debug('Processing...'); for (const file of files) { if (!file.includes('.DS_Store') && !file.includes('.xml')) { const arrayBuffer = await fs.readFile(file); const dicomDict = dcmjs.data.DicomMessage.readFile(arrayBuffer.buffer); const instance = dcmjs.data.DicomMetaDictionary.naturalizeDataset(dicomDict.dict); instance.fileLocation = createImageId(file, urlPrefix, studyDirectory, scheme); processInstance(instance); } } console.log('Successfully loaded data'); model.studies.forEach(study => { study.NumInstances = findInstancesNumber(study); study.Modalities = findModalities(study).join('/'); }); await fs.writeFile(outputPath, JSON.stringify(model, null, 2)); console.log('JSON saved'); } catch (error) { console.error(error); } } async function recursiveReadDir(dir) { let results = []; const list = await fs.readdir(dir); for (const file of list) { const filePath = path.resolve(dir, file); const stat = await fs.stat(filePath); if (stat.isDirectory()) { const res = await recursiveReadDir(filePath); results = results.concat(res); } else { results.push(filePath); } } return results; } function createImageId(fileLocation, urlPrefix, studyDirectory, scheme) { const relativePath = path.relative(studyDirectory, fileLocation); const normalizedPath = path.normalize(relativePath).replace(/\\/g, '/'); return `${scheme}:${urlPrefix}${normalizedPath}`; } function processInstance(instance) { const { StudyInstanceUID, SeriesInstanceUID } = instance; let study = getStudy(StudyInstanceUID); if (!study) { study = createStudyMetadata(StudyInstanceUID, instance); model.studies.push(study); } let series = getSeries(StudyInstanceUID, SeriesInstanceUID); if (!series) { series = createSeriesMetadata(instance); study.series.push(series); } const instanceMetaData = instance.NumberOfFrames > 1 ? createInstanceMetaDataMultiFrame(instance) : createInstanceMetaData(instance); series.instances.push(...[].concat(instanceMetaData)); } function getStudy(StudyInstanceUID) { return model.studies.find(study => study.StudyInstanceUID === StudyInstanceUID); } function getSeries(StudyInstanceUID, SeriesInstanceUID) { const study = getStudy(StudyInstanceUID); return study ? study.series.find(series => series.SeriesInstanceUID === SeriesInstanceUID) : undefined; } const findInstancesNumber = study => { let numInstances = 0; study.series.forEach(aSeries => { numInstances = numInstances + aSeries.instances.length; }); return numInstances; }; const findModalities = study => { let modalities = new Set(); study.series.forEach(aSeries => { modalities.add(aSeries.Modality); }); return Array.from(modalities); }; function createStudyMetadata(StudyInstanceUID, instance) { return { StudyInstanceUID, StudyDescription: instance.StudyDescription, StudyDate: instance.StudyDate, StudyTime: instance.StudyTime, PatientName: instance.PatientName, PatientID: instance.PatientID || '1234', // this is critical to have AccessionNumber: instance.AccessionNumber, PatientAge: instance.PatientAge, PatientSex: instance.PatientSex, PatientWeight: instance.PatientWeight, series: [], }; } function createSeriesMetadata(instance) { return { SeriesInstanceUID: instance.SeriesInstanceUID, SeriesDescription: instance.SeriesDescription, SeriesNumber: instance.SeriesNumber, SeriesTime: instance.SeriesTime, Modality: instance.Modality, SliceThickness: instance.SliceThickness, instances: [], }; } function commonMetaData(instance) { return { Columns: instance.Columns, Rows: instance.Rows, InstanceNumber: instance.InstanceNumber, SOPClassUID: instance.SOPClassUID, AcquisitionNumber: instance.AcquisitionNumber, PhotometricInterpretation: instance.PhotometricInterpretation, BitsAllocated: instance.BitsAllocated, BitsStored: instance.BitsStored, PixelRepresentation: instance.PixelRepresentation, SamplesPerPixel: instance.SamplesPerPixel, PixelSpacing: instance.PixelSpacing, HighBit: instance.HighBit, ImageOrientationPatient: instance.ImageOrientationPatient, ImagePositionPatient: instance.ImagePositionPatient, FrameOfReferenceUID: instance.FrameOfReferenceUID, ImageType: instance.ImageType, Modality: instance.Modality, SOPInstanceUID: instance.SOPInstanceUID, SeriesInstanceUID: instance.SeriesInstanceUID, StudyInstanceUID: instance.StudyInstanceUID, WindowCenter: instance.WindowCenter, WindowWidth: instance.WindowWidth, RescaleIntercept: instance.RescaleIntercept, RescaleSlope: instance.RescaleSlope, }; } function conditionalMetaData(instance) { return { ...(instance.ConceptNameCodeSequence && { ConceptNameCodeSequence: instance.ConceptNameCodeSequence, }), ...(instance.SeriesDate && { SeriesDate: instance.SeriesDate }), ...(instance.ReferencedSeriesSequence && { ReferencedSeriesSequence: instance.ReferencedSeriesSequence, }), ...(instance.SharedFunctionalGroupsSequence && { SharedFunctionalGroupsSequence: instance.SharedFunctionalGroupsSequence, }), ...(instance.PerFrameFunctionalGroupsSequence && { PerFrameFunctionalGroupsSequence: instance.PerFrameFunctionalGroupsSequence, }), ...(instance.ContentSequence && { ContentSequence: instance.ContentSequence }), ...(instance.ContentTemplateSequence && { ContentTemplateSequence: instance.ContentTemplateSequence, }), ...(instance.CurrentRequestedProcedureEvidenceSequence && { CurrentRequestedProcedureEvidenceSequence: instance.CurrentRequestedProcedureEvidenceSequence, }), ...(instance.CodingSchemeIdentificationSequence && { CodingSchemeIdentificationSequence: instance.CodingSchemeIdentificationSequence, }), ...(instance.RadiopharmaceuticalInformationSequence && { RadiopharmaceuticalInformationSequence: instance.RadiopharmaceuticalInformationSequence, }), ...(instance.ROIContourSequence && { ROIContourSequence: instance.ROIContourSequence, }), ...(instance.StructureSetROISequence && { StructureSetROISequence: instance.StructureSetROISequence, }), ...(instance.ReferencedFrameOfReferenceSequence && { ReferencedFrameOfReferenceSequence: instance.ReferencedFrameOfReferenceSequence, }), ...(instance.CorrectedImage && { CorrectedImage: instance.CorrectedImage }), ...(instance.Units && { Units: instance.Units }), ...(instance.DecayCorrection && { DecayCorrection: instance.DecayCorrection }), ...(instance.AcquisitionDate && { AcquisitionDate: instance.AcquisitionDate }), ...(instance.AcquisitionTime && { AcquisitionTime: instance.AcquisitionTime }), ...(instance.PatientWeight && { PatientWeight: instance.PatientWeight }), ...(instance.NumberOfFrames && { NumberOfFrames: instance.NumberOfFrames }), ...(instance.FrameTime && { FrameTime: instance.FrameTime }), ...(instance.EncapsulatedDocument && { EncapsulatedDocument: instance.EncapsulatedDocument }), ...(instance.SequenceOfUltrasoundRegions && { SequenceOfUltrasoundRegions: instance.SequenceOfUltrasoundRegions, }), }; } function createInstanceMetaData(instance) { const metadata = { ...commonMetaData(instance), ...conditionalMetaData(instance), }; return { metadata, url: instance.fileLocation }; } function createInstanceMetaDataMultiFrame(instance) { const instances = []; const commonData = commonMetaData(instance); const conditionalData = conditionalMetaData(instance); for (let i = 1; i <= instance.NumberOfFrames; i++) { const metadata = { ...commonData, ...conditionalData }; const result = { metadata, url: instance.fileLocation + `?frame=${i}` }; instances.push(result); } return instances; } convertDICOMToJSON(studyDirectory, urlPrefix, outputPath, scheme);