Last active
August 30, 2023 09:29
-
-
Save PantelisGeorgiadis/476564c36b81a1698303c6309ad0cc58 to your computer and use it in GitHub Desktop.
DICOM Multiframe True Color Secondary Capture dataset generator for Node.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// dcmjs-sc-jpeg-gen | |
// This Node.js utility can generate DICOM Multiframe True Color Secondary Capture datasets | |
// with JPEG baseline frames, for testing purposes. Custom frame parameters could be defined, | |
// such as count, width and height. | |
// The following npm packages are required: | |
// canvas | |
// dcmjs | |
// faker | |
// moment | |
// parse-int | |
// e.g. npm install canvas dcmjs faker@5.5.3 moment parse-int | |
// Usage: | |
// [OPTIONS] node dcmjs-sc-jpeg-gen.js | |
// [OPTIONS]: | |
// FRAME_WIDTH: Frame width (default: 512). | |
// FRAME_HEIGHT: Frame height (default: 512). | |
// NUMBER_OF_FRAMES: Number of frames (default: 3). | |
// FRAMES_PER_SECOND: Frame increment rate (default: 10). | |
// OUTPUT_FILE: Output file path (default: sc.dcm). | |
// e.g. FRAME_WIDTH=256 FRAME_HEIGHT=256 NUMBER_OF_FRAMES=5 FRAMES_PER_SECOND=20 node dcmjs-sc-jpeg-gen.js | |
const fs = require('fs'); | |
const parseInt = require('parse-int'); | |
const faker = require('faker'); | |
const moment = require('moment'); | |
const { createCanvas } = require('canvas'); | |
const dcmjs = require('dcmjs'); | |
const { DicomMetaDictionary, DicomDict } = dcmjs.data; | |
////////////////////////////////////////////////////////////////////////// | |
// Constant UIDs | |
/** | |
* Secondary Capture Image Storage SOP class UID. | |
* @constant {string} | |
*/ | |
const MultiframeTrueColorSecondaryCaptureSopClassUid = '1.2.840.10008.5.1.4.1.1.7.4'; | |
/** | |
* Baseline JPEG transfer syntax UID. | |
* @constant {string} | |
*/ | |
const JpegBaselineTransferSyntaxUid = '1.2.840.10008.1.2.4.50'; | |
/** | |
* Implementation UID (using fo-dicom's!). | |
* @constant {string} | |
*/ | |
const ImplementationUid = '1.3.6.1.4.1.30071.8'; | |
////////////////////////////////////////////////////////////////////////// | |
// Functions | |
/** | |
* Generates a DICOM Multiframe True Color Secondary Capture dataset. | |
* @method | |
* @param {number} frameWidth - The frame width. | |
* @param {number} frameHeight - The frame height. | |
* @param {number} numberOfFrames - Number of frames. | |
* @param {number} framesPerSecond - The frame increment rate. | |
* @returns {Buffer} The DICOM dataset buffer. | |
*/ | |
function generateDataset(frameWidth, frameHeight, numberOfFrames, framesPerSecond) { | |
// Create dataset dates and times | |
const birthDate = `${moment(faker.date.between('1950-01-01', '2000-01-01')).format('YYYYMMDD')}`; | |
const date = `${moment(faker.date.between('1950-01-01', '2010-01-01')).format('YYYYMMDD')}`; | |
const time = '000000'; | |
// Create dataset UIDs | |
const studyInstanceUid = DicomMetaDictionary.uid(); | |
const seriesInstanceUid = DicomMetaDictionary.uid(); | |
const sopInstanceUid = DicomMetaDictionary.uid(); | |
// Create JPEG frame buffers | |
const frameBuffers = []; | |
for (let i = 0; i < numberOfFrames; i++) { | |
let frameBuffer = generateFrame(frameWidth, frameHeight, `${i}`); | |
if (frameBuffer.length & 1) { | |
frameBuffer = Buffer.concat([frameBuffer, Buffer.from([0x00])]); | |
} | |
frameBuffers.push(frameBuffer.buffer); | |
} | |
// Create dataset | |
const dataset = { | |
_vrMap: { | |
PixelData: 'OB', | |
}, | |
_meta: { | |
_vrMap: {}, | |
FileMetaInformationVersion: new Uint8Array([0, 1]).buffer, | |
MediaStorageSOPClassUID: MultiframeTrueColorSecondaryCaptureSopClassUid, | |
MediaStorageSOPInstanceUID: sopInstanceUid, | |
TransferSyntaxUID: JpegBaselineTransferSyntaxUid, | |
ImplementationClassUID: ImplementationUid, | |
}, | |
// Patient Module Attributes | |
PatientID: `${faker.random.alphaNumeric(6).toUpperCase()}`, | |
PatientName: `${faker.name.firstName().toUpperCase()}^${faker.name.lastName().toUpperCase()}`, | |
PatientBirthDate: birthDate, | |
PatientSex: 'O', | |
// General Study Module Attributes | |
StudyInstanceUID: studyInstanceUid, | |
StudyDate: date, | |
StudyTime: time, | |
StudyID: `${faker.random.alphaNumeric(3).toUpperCase()}`, | |
AccessionNumber: `${faker.random.alphaNumeric(6).toUpperCase()}`, | |
ReferringPhysicianName: `${faker.name.firstName().toUpperCase()}^${faker.name | |
.lastName() | |
.toUpperCase()}`, | |
// General Series Module Attributes | |
Modality: 'OT', | |
SeriesInstanceUID: seriesInstanceUid, | |
SeriesNumber: `${faker.datatype.number(100)}`, | |
Laterality: '', | |
// General/SC Equipment Module Attributes | |
Manufacturer: `${faker.company.companyName().toUpperCase()}`, | |
ManufacturerModelName: `${faker.commerce.productName().toUpperCase()}`, | |
DeviceSerialNumber: `${faker.random.alphaNumeric(6).toUpperCase()}`, | |
SoftwareVersions: `${faker.random.alphaNumeric(1).toUpperCase()}.${faker.random | |
.alphaNumeric(2) | |
.toUpperCase()}`, | |
ConversionType: 'SYN', | |
// General/SC Image Module Attributes | |
InstanceNumber: '1', | |
AcquisitionDateTime: `${date}${time}`, | |
ContentDate: date, | |
ContentTime: time, | |
BurnedInAnnotation: 'NO', | |
PatientOrientation: '', | |
// Image Pixel Module Attributes | |
SamplesPerPixel: 3, | |
PhotometricInterpretation: 'YBR_FULL_422', | |
PlanarConfiguration: 0, | |
NumberOfFrames: `${numberOfFrames}`, | |
Rows: frameHeight, | |
Columns: frameWidth, | |
BitsAllocated: 8, | |
BitsStored: 8, | |
HighBit: 7, | |
PixelRepresentation: 0, | |
PixelData: frameBuffers, | |
// Multi-frame Module Attributes | |
FrameIncrementPointer: attributeNameToIdentifier('FrameTime'), | |
// Cine Module Attributes | |
FrameTime: `${numberOfFrames === 1 ? '0' : 1000 / framesPerSecond}`, | |
FrameDelay: '0.0', | |
// SOP Common Module Attributes | |
SOPClassUID: MultiframeTrueColorSecondaryCaptureSopClassUid, | |
SOPInstanceUID: sopInstanceUid, | |
SpecificCharacterSet: 'ISO_IR 100', | |
}; | |
// Convert the JSON dataset into a DICOM part10 file | |
const denaturalizedMetaHeader = DicomMetaDictionary.denaturalizeDataset(dataset._meta); | |
const dicomDict = new DicomDict(denaturalizedMetaHeader); | |
dicomDict.dict = DicomMetaDictionary.denaturalizeDataset(dataset); | |
return Buffer.from(dicomDict.write({ fragmentMultiframe: false })); | |
} | |
/** | |
* Generates a DICOM frame. | |
* @method | |
* @param {number} width - The frame width. | |
* @param {number} height - The frame height. | |
* @param {string} text - Text to write on frame. | |
* @returns {Buffer} The frame buffer. | |
*/ | |
function generateFrame(width, height, text) { | |
const canvas = createCanvas(width, height); | |
const context = canvas.getContext('2d'); | |
context.font = 'bold 50pt Arial'; | |
context.textAlign = 'center'; | |
context.textBaseline = 'middle'; | |
context.fillStyle = '#fff'; | |
context.fillText(text, width / 2, height / 2); | |
context.strokeStyle = '#fff'; | |
context.setLineDash([3, 3]); | |
context.strokeRect(0, 0, width, height); | |
return canvas.toBuffer('image/jpeg', 1); | |
} | |
/** | |
* Get the attribute identifier from name. | |
* @method | |
* @param {string} text - Attribute name. | |
* @returns {number} Attribute identifier. | |
*/ | |
function attributeNameToIdentifier(attributeName) { | |
let identifier = undefined; | |
Object.keys(DicomMetaDictionary.dictionary).forEach((tag) => { | |
const dictionaryEntry = DicomMetaDictionary.dictionary[tag]; | |
if (dictionaryEntry.version === 'DICOM' && attributeName === dictionaryEntry.name) { | |
const group = Number(`0x${dictionaryEntry.tag.substring(1, 5)}`); | |
const element = Number(`0x${dictionaryEntry.tag.substring(6, 10)}`); | |
identifier = (group << 16) | (element & 0xffff); | |
} | |
}); | |
return identifier; | |
} | |
////////////////////////////////////////////////////////////////////////// | |
// Gather params | |
const frameWidth = parseInt(process.env.FRAME_WIDTH) || 512; | |
const frameHeight = parseInt(process.env.FRAME_HEIGHT) || 512; | |
const numberOfFrames = parseInt(process.env.NUMBER_OF_FRAMES) || 3; | |
const framesPerSecond = parseInt(process.env.FRAMES_PER_SECOND) || 10; | |
const outFile = process.env.OUTPUT_FILE || 'sc.dcm'; | |
////////////////////////////////////////////////////////////////////////// | |
// Generate dataset and persist it to file | |
const datasetBuffer = generateDataset(frameWidth, frameHeight, numberOfFrames, framesPerSecond); | |
fs.writeFileSync(outFile, datasetBuffer); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment