UNPKG

dicom-microscopy-viewer

Version:
635 lines (581 loc) 21.8 kB
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <title>JSDoc: Source: metadata.js</title> <script src="scripts/prettify/prettify.js"> </script> <script src="scripts/prettify/lang-css.js"> </script> <!--[if lt IE 9]> <script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script> <![endif]--> <link type="text/css" rel="stylesheet" href="styles/prettify-tomorrow.css"> <link type="text/css" rel="stylesheet" href="styles/jsdoc-default.css"> </head> <body> <div id="main"> <h1 class="page-title">Source: metadata.js</h1> <section> <article> <pre class="prettyprint source linenums"><code>import { tagToKeyword } from './dictionary' import { SOPClassUIDs } from './enums' import { _groupFramesPerMapping } from './mapping' const _metadata = Symbol('metadata') const _bulkdataReferences = Symbol('bulkdataReferences') function _base64ToUint8Array (value) { const blob = window.atob(value) const array = new Uint8Array(blob.length) for (let i = 0; i &lt; blob.length; i++) { array[i] = blob.charCodeAt(i) } return array } function _base64ToUint16Array (value) { const blob = window.atob(value) const n = Uint16Array.BYTES_PER_ELEMENT const length = blob.length / n const buffer = new ArrayBuffer(n) const view = new DataView(buffer) const array = new Uint16Array(length) let p = 0 for (let i = 0; i &lt; length; i++) { p = i * n for (let j = 0; j &lt; n; j++) { view.setUint8(j, blob.charCodeAt(p + j)) } array[i] = view.getUint16(0, true) } return array } function _base64ToUint32Array (value) { const blob = window.atob(value) const n = Uint32Array.BYTES_PER_ELEMENT const length = blob.length / n const buffer = new ArrayBuffer(n) const view = new DataView(buffer) const array = new Uint32Array(length) let p = 0 for (let i = 0; i &lt; length; i++) { p = i * n for (let j = 0; j &lt; n; j++) { view.setUint8(j, blob.charCodeAt(p + j)) } array[i] = view.getUint32(0, true) } return array } function _base64ToFloat32Array (value) { const blob = window.atob(value) const n = Float32Array.BYTES_PER_ELEMENT const length = blob.length / n const buffer = new ArrayBuffer(n) const view = new DataView(buffer) const array = new Float32Array(length) let p = 0 for (let i = 0; i &lt; length; i++) { p = i * n for (let j = 0; j &lt; n; j++) { view.setUint8(j, blob.charCodeAt(p + j)) } array[i] = view.getFloat32(0, true) } return array } function _base64ToFloat64Array (value) { const blob = window.atob(value) const n = Float64Array.BYTES_PER_ELEMENT const length = blob.length / n const buffer = new ArrayBuffer(n) const view = new DataView(buffer) const array = new Float64Array(length) let p = 0 for (let i = 0; i &lt; length; i++) { p = i * n for (let j = 0; j &lt; n; j++) { view.setUint8(j, blob.charCodeAt(p + j)) } array[i] = view.getFloat64(0, true) } return array } /** Determine the mapping of pyramid tile positions to frame numbers. * * @param {Object} Formatted metadata of a VL Whole Slide Microscopy Image instance * @returns {Object} Mapping of pyramid tile position (Row-Column) to frame URI * @private */ function getFrameMapping (metadata) { const rows = metadata.Rows const columns = metadata.Columns const totalPixelMatrixColumns = metadata.TotalPixelMatrixColumns const totalPixelMatrixRows = metadata.TotalPixelMatrixRows const sopInstanceUID = metadata.SOPInstanceUID const numberOfFrames = Number(metadata.NumberOfFrames || 1) /** * Handle images that may contain multiple "planes" * - z-planes (VL Whole Slide Microscopy Image) * - optical paths (VL Whole Slide Microscopy Image) * - segments (Segmentation) * - mappings (Parametric Map) */ const numberOfFocalPlanes = Number(metadata.NumberOfFocalPlanes || 1) if (numberOfFocalPlanes > 1) { throw new Error('Images with multiple focal planes are not yet supported.') } const { mappingNumberToFrameNumbers, frameNumberToMappingNumber } = _groupFramesPerMapping(metadata) let numberOfChannels = 0 let numberOfOpticalPaths = 0 let numberOfSegments = 0 let numberOfMappings = 0 if (metadata.OpticalPathSequence != null) { numberOfOpticalPaths = Number(metadata.NumberOfOpticalPaths || 1) numberOfChannels = numberOfOpticalPaths } else if (metadata.SegmentSequence != null) { numberOfSegments = Number(metadata.SegmentSequence.length) numberOfChannels = numberOfSegments } else if (Object.keys(mappingNumberToFrameNumbers).length > 0) { numberOfMappings = Number(Object.keys(mappingNumberToFrameNumbers).length) numberOfChannels = numberOfMappings } else { throw new Error('Could not determine the number of image channels.') } const tileColumns = Math.ceil(totalPixelMatrixColumns / columns) const tileRows = Math.ceil(totalPixelMatrixRows / rows) const frameMapping = {} /** * The values "TILED_SPARSE" and "TILED_FULL" were introduced in the 2018 * edition of the standard. Older datasets are equivalent to "TILED_SPARSE". */ const dimensionOrganizationType = ( metadata.DimensionOrganizationType || 'TILED_SPARSE' ) if (dimensionOrganizationType === 'TILED_FULL') { let number = 1 // Forth, along "channels" for (let i = 0; i &lt; numberOfChannels; i++) { // Third, along the depth direction from glass slide -> coverslip for (let p = 0; p &lt; numberOfFocalPlanes; p++) { // Second, along the column direction from top -> bottom for (let r = 0; r &lt; tileRows; r++) { // First, along the row direction from left -> right for (let c = 0; c &lt; tileColumns; c++) { /* * The standard currently only defines TILED_FULL for optical paths * and not any other types of "channels" such as segments or * parameter mappings. */ let channelIdentifier if (numberOfOpticalPaths > 0) { const opticalPath = metadata.OpticalPathSequence[i] channelIdentifier = String(opticalPath.OpticalPathIdentifier) } else if (numberOfSegments > 0) { const segment = metadata.SegmentSequence[i] channelIdentifier = String(segment.SegmentNumber) } else if (numberOfMappings > 0) { // TODO: ensure that frames are mapped accordingly channelIdentifier = String(frameNumberToMappingNumber[number]) } else { throw new Error( `Could not determine channel of frame #${number}.` ) } const key = `${r + 1}-${c + 1}-${channelIdentifier}` frameMapping[key] = `${sopInstanceUID}/frames/${number}` number += 1 } } } } } else { const sharedFuncGroups = metadata.SharedFunctionalGroupsSequence const perframeFuncGroups = metadata.PerFrameFunctionalGroupsSequence for (let j = 0; j &lt; numberOfFrames; j++) { const planePositions = perframeFuncGroups[j].PlanePositionSlideSequence[0] const rowPosition = planePositions.RowPositionInTotalImagePixelMatrix const columnPosition = planePositions.ColumnPositionInTotalImagePixelMatrix const rowIndex = Math.ceil(rowPosition / rows) const colIndex = Math.ceil(columnPosition / columns) const number = j + 1 let channelIdentifier if (numberOfOpticalPaths === 1) { try { channelIdentifier = String( sharedFuncGroups[0] .OpticalPathIdentificationSequence[0] .OpticalPathIdentifier ) } catch { channelIdentifier = String( perframeFuncGroups[j] .OpticalPathIdentificationSequence[0] .OpticalPathIdentifier ) } } else if (numberOfOpticalPaths > 1) { channelIdentifier = String( perframeFuncGroups[j] .OpticalPathIdentificationSequence[0] .OpticalPathIdentifier ) } else if (numberOfSegments === 1) { try { channelIdentifier = String( sharedFuncGroups[0] .SegmentIdentificationSequence[0] .ReferencedSegmentNumber ) } catch { channelIdentifier = String( perframeFuncGroups[j] .SegmentIdentificationSequence[0] .ReferencedSegmentNumber ) } } else if (numberOfSegments > 1) { channelIdentifier = String( perframeFuncGroups[j] .SegmentIdentificationSequence[0] .ReferencedSegmentNumber ) } else if (numberOfMappings > 0) { channelIdentifier = String(frameNumberToMappingNumber[number]) } else { throw new Error(`Could not determine channel of frame ${number}.`) } const key = `${rowIndex}-${colIndex}-${channelIdentifier}` const frameNumber = j + 1 frameMapping[key] = `${sopInstanceUID}/frames/${frameNumber}` } } return { frameMapping, numberOfChannels } } /** * Format DICOM metadata structured according to the DICOM JSON model. * * Transforms the DICOM JSON representation into a more human friendly * representation, where values of data elements can be directly accessed via * their keyword (e.g., "SOPInstanceUID"). * Bulkdata elements will be extracted and returned as a separate mapping. * * @param {Object} metadata - Metadata structured according to the DICOM JSON model * * @returns {Object} Formatted dataset and remaining bulkdata * * @memberof metadata */ function formatMetadata (metadata) { const loadJSONDataset = (elements) => { const dataset = {} const bulkdataReferences = {} Object.keys(elements).forEach(tag => { const keyword = tagToKeyword[tag] const vr = elements[tag].vr if ('BulkDataURI' in elements[tag]) { bulkdataReferences[keyword] = elements[tag] } else if ('Value' in elements[tag]) { const value = elements[tag].Value if (vr === 'SQ') { dataset[keyword] = [] const mappings = [] value.forEach(item => { const loaded = loadJSONDataset(item) dataset[keyword].push(loaded.dataset) mappings.push(loaded.bulkdataReferences) }) if (mappings.some(item => Object.keys(item).length > 0)) { bulkdataReferences[keyword] = mappings } } else { // Handle value multiplicity. if (value.length === 1) { if (vr === 'DS' || vr === 'IS') { dataset[keyword] = Number(value[0]) } else { dataset[keyword] = value[0] } } else { if (vr === 'DS' || vr === 'IS') { dataset[keyword] = value.map(v => Number(v)) } else { dataset[keyword] = value } } } } else if ('InlineBinary' in elements[tag]) { const value = elements[tag].InlineBinary if (vr === 'OB') { dataset[keyword] = _base64ToUint8Array(value) } else if (vr === 'OW') { dataset[keyword] = _base64ToUint16Array(value) } else if (vr === 'OL') { dataset[keyword] = _base64ToUint32Array(value) } else if (vr === 'OF') { dataset[keyword] = _base64ToFloat32Array(value) } else if (vr === 'OD') { dataset[keyword] = _base64ToFloat64Array(value) } } else { if (vr === 'SQ') { dataset[keyword] = [] } else { dataset[keyword] = null } } }) return { dataset, bulkdataReferences } } const { dataset, bulkdataReferences } = loadJSONDataset(metadata) // The top level (lowest resolution) image may be a single frame image in // which case the "NumberOfFrames" attribute is optional. We include it for // consistency. if (dataset === undefined) { throw new Error('Could not format metadata: ', metadata) } if (!('NumberOfFrames' in dataset) &amp;&amp; (dataset.Modality === 'SM')) { dataset.NumberOfFrames = 1 } return { dataset, bulkdataReferences } } /** * Group DICOM metadata of monochrome slides by Optical Path Identifier. * * @param {metadata.VLWholeSlideMicroscopyImage[]} images - DICOM VL Whole * Slide Microscopy Image instances. * * @returns {Object} Groups of DICOM VL Whole Slide Microscopy Image instances * @memberof metadata */ function groupMonochromeInstances (images) { const channels = {} images.forEach(img => { if ( img.SamplesPerPixel === 1 &amp;&amp; img.PhotometricInterpretation === 'MONOCHROME2' &amp;&amp; (img.ImageType[2] === 'VOLUME' || img.ImageType[2] === 'THUMBNAIL') ) { img.OpticalPathSequence.forEach((opticalPathItem, opticalPathIndex) => { const id = opticalPathItem.OpticalPathIdentifier if (id in channels) { channels[id].push(img) } else { channels[id] = [img] } }) } }) return channels } /** * Group DICOM metadata of color images slides by Optical Path Identifier. * * @param {metadata.VLWholeSlideMicroscopyImage[]} images - DICOM VL Whole * Slide Microscopy Image instances. * * @returns {Object} Groups of DICOM VL Whole Slide Microscopy Image instances * @memberof metadata */ function groupColorInstances (images) { const channels = {} images.forEach(img => { if ( img.SamplesPerPixel !== 1 &amp;&amp; (img.ImageType[2] === 'THUMBNAIL' || img.ImageType[2] === 'VOLUME') &amp;&amp; ( img.PhotometricInterpretation === 'RGB' || img.PhotometricInterpretation.includes('YBR') ) ) { const id = img.OpticalPathSequence[0].OpticalPathIdentifier if (id in channels) { channels[id].push(img) } else { channels[id] = [img] } } }) return channels } /** * DICOM Service Object Pair (SOP) Class. * * @class * @abstract * @memberof metadata */ class SOPClass { /** * @param {Object} options * @param {Object} options.metadata - Metadata of a DICOM SOP instance in DICOM JSON format */ constructor ({ metadata }) { if (metadata == null) { throw new Error( 'Cannot construct SOP Instance because no metadata was provided.' ) } const { dataset, bulkdataReferences } = formatMetadata(metadata) Object.assign(this, dataset) this[_metadata] = metadata this[_bulkdataReferences] = bulkdataReferences Object.freeze(this) } /** * Get metadata of instance in DICOM JSON format. * * The metadata may include bulkdata references via "BulkDataURI". * * @returns {Object} metadata in DICOM JSON format */ get json () { return this[_metadata] } /** * Get references to bulkdata of instance in DICOM JSON format. * * @returns {Object} bulkdata references in DICOM JSON format */ get bulkdataReferences () { return this[_bulkdataReferences] } } /** * DICOM VL Whole Slide Microscopy Image instance. * * @class * @extends metadata.SOPClass * @memberof metadata */ class VLWholeSlideMicroscopyImage extends SOPClass { /** * @param {Object} options * @param {Object} options.metadata - Metadata of a VL Whole Slide Microscopy Image in DICOM JSON format */ constructor ({ metadata }) { super({ metadata }) if (this.SOPClassUID !== SOPClassUIDs.VL_WHOLE_SLIDE_MICROSCOPY_IMAGE) { throw new Error( 'Cannot construct VL Whole Slide Microscopy Image instance ' + `given dataset with SOP Class UID "${this.SOPClassUID}"` ) } } } /** * DICOM Comprehensive 3D SR instance. * * @class * @extends metadata.SOPClass * @memberof metadata */ class Comprehensive3DSR extends SOPClass { /** * @param {Object} options * @param {Object} options.metadata - Metadata of DICOM Structured Report instance in DICOM JSON format */ constructor ({ metadata }) { super({ metadata }) if (this.SOPClassUID !== SOPClassUIDs.COMPREHENSIVE_3D_SR) { throw new Error( 'Cannot construct Comprehensive 3D SR instance ' + `given dataset with SOP Class UID "${this.SOPClassUID}"` ) } } } /** * DICOM Microscopy Bulk Simple Annotations instance. * * @class * @extends metadata.SOPClass * @memberof metadata */ class MicroscopyBulkSimpleAnnotations extends SOPClass { /** * @param {Object} options * @param {Object} options.metadata - Metadata of a DICOM Microscopy Bulk Simple Annotations instance in DICOM JSON format */ constructor ({ metadata }) { super({ metadata }) if (this.SOPClassUID !== SOPClassUIDs.MICROSCOPY_BULK_SIMPLE_ANNOTATIONS) { throw new Error( 'Cannot construct Microscopy Bulk Simple Annotations instance ' + `given dataset with SOP Class UID "${this.SOPClassUID}"` ) } } } /** * DICOM Parametric Map instance. * * @class * @extends metadata.SOPClass * @memberof metadata */ class ParametricMap extends SOPClass { /** * @param {Object} options * @param {Object} options.metadata - Metadata of a DICOM Parametric Map instance in DICOM JSON format */ constructor ({ metadata }) { super({ metadata }) if (this.SOPClassUID !== SOPClassUIDs.PARAMETRIC_MAP) { throw new Error( 'Cannot construct Parametric Map instance ' + `given dataset with SOP Class UID "${this.SOPClassUID}"` ) } } } /** * DICOM Segmentation instance. * * @class * @extends metadata.SOPClass * @memberof metadata */ class Segmentation extends SOPClass { /** * @param {Object} options * @param {Object} options.metadata - Metadata of a DICOM Segmentation instance in DICOM JSON format */ constructor ({ metadata }) { super({ metadata }) if (this.SOPClassUID !== SOPClassUIDs.SEGMENTATION) { throw new Error( 'Cannot construct Segmentation instance ' + `given dataset with SOP Class UID "${this.SOPClassUID}"` ) } } } export { Comprehensive3DSR, formatMetadata, groupMonochromeInstances, groupColorInstances, getFrameMapping, MicroscopyBulkSimpleAnnotations, ParametricMap, Segmentation, VLWholeSlideMicroscopyImage } </code></pre> </article> </section> </div> <nav> <h2><a href="index.html">Home</a></h2><h3>Namespaces</h3><ul><li><a href="annotation.html">annotation</a></li><li><a href="api.html">api</a></li><li><a href="color.html">color</a></li><li><a href="events.html">events</a></li><li><a href="mapping.html">mapping</a></li><li><a href="metadata.html">metadata</a></li><li><a href="opticalPath.html">opticalPath</a></li><li><a href="roi.html">roi</a></li><li><a href="scoord3d.html">scoord3d</a></li><li><a href="segment.html">segment</a></li><li><a href="utils.html">utils</a></li><li><a href="viewer.html">viewer</a></li></ul><h3>Classes</h3><ul><li><a href="annotation.AnnotationGroup.html">AnnotationGroup</a></li><li><a href="color.PaletteColorLookupTable.html">PaletteColorLookupTable</a></li><li><a href="mapping.ParameterMapping.html">ParameterMapping</a></li><li><a href="mapping.Transformation.html">Transformation</a></li><li><a href="metadata.Comprehensive3DSR.html">Comprehensive3DSR</a></li><li><a href="metadata.MicroscopyBulkSimpleAnnotations.html">MicroscopyBulkSimpleAnnotations</a></li><li><a href="metadata.ParametricMap.html">ParametricMap</a></li><li><a href="metadata.Segmentation.html">Segmentation</a></li><li><a href="metadata.SOPClass.html">SOPClass</a></li><li><a href="metadata.VLWholeSlideMicroscopyImage.html">VLWholeSlideMicroscopyImage</a></li><li><a href="module.exports_module.exports.html">exports</a></li><li><a href="opticalPath.OpticalPath.html">OpticalPath</a></li><li><a href="roi.ROI.html">ROI</a></li><li><a href="scoord3d.Ellipse.html">Ellipse</a></li><li><a href="scoord3d.Ellipsoid.html">Ellipsoid</a></li><li><a href="scoord3d.Multipoint.html">Multipoint</a></li><li><a href="scoord3d.Point.html">Point</a></li><li><a href="scoord3d.Polygon.html">Polygon</a></li><li><a href="scoord3d.Polyline.html">Polyline</a></li><li><a href="scoord3d.Scoord3D.html">Scoord3D</a></li><li><a href="segment.Segment.html">Segment</a></li><li><a href="viewer.LabelImageViewer.html">LabelImageViewer</a></li><li><a href="viewer.OverviewImageViewer.html">OverviewImageViewer</a></li><li><a href="viewer.VolumeImageViewer.html">VolumeImageViewer</a></li></ul><h3>Global</h3><ul><li><a href="global.html#addTask">addTask</a></li><li><a href="global.html#cancelTask">cancelTask</a></li><li><a href="global.html#decode">decode</a></li><li><a href="global.html#getStatistics">getStatistics</a></li><li><a href="global.html#handleMessageFromWorker">handleMessageFromWorker</a></li><li><a href="global.html#initialize">initialize</a></li><li><a href="global.html#loadWebWorkerTask">loadWebWorkerTask</a></li><li><a href="global.html#setTaskPriority">setTaskPriority</a></li><li><a href="global.html#spawnWebWorker">spawnWebWorker</a></li><li><a href="global.html#startTaskOnWebWorker">startTaskOnWebWorker</a></li><li><a href="global.html#terminateAllWebWorkers">terminateAllWebWorkers</a></li><li><a href="global.html#transform">transform</a></li></ul> </nav> <br class="clear"> <footer> Documentation generated by <a href="https://github.com/jsdoc/jsdoc">JSDoc 3.6.10</a> on Thu Sep 29 2022 16:54:54 GMT-0400 (Eastern Daylight Time) </footer> <script> prettyPrint(); </script> <script src="scripts/linenumber.js"> </script> </body> </html>