Skip to content

Instantly share code, notes, and snippets.

@PantelisGeorgiadis
Last active August 30, 2023 09:29
Show Gist options
  • Save PantelisGeorgiadis/476564c36b81a1698303c6309ad0cc58 to your computer and use it in GitHub Desktop.
Save PantelisGeorgiadis/476564c36b81a1698303c6309ad0cc58 to your computer and use it in GitHub Desktop.
DICOM Multiframe True Color Secondary Capture dataset generator for Node.js
// 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