UNPKG

dicom-microscopy-viewer-changed

Version:
690 lines (637 loc) 24.3 kB
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <title>JSDoc: Source: pyramid.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: pyramid.js</h1> <section> <article> <pre class="prettyprint source linenums"><code>import * as dwc from 'dicomweb-client' import { _decodeAndTransformFrame } from './decode.js' import EVENT from './events' import publish from './eventPublisher' import { getFrameMapping, VLWholeSlideMicroscopyImage } from './metadata.js' import { getPixelSpacing } from './scoord3dUtils' import { are1DArraysAlmostEqual, are2DArraysAlmostEqual, _fetchBulkdata } from './utils.js' /** * Get Image ICC profiles. * * @param {Array&lt;metadata.VLWholeSlideMicroscopyImage>} pyramid - Metadata of * VL Whole Slide Microscopy Image instances * @param {object} client - dicom web client * * @returns {Promise&lt;Array&lt;TypedArray>>} image array with ICC profiles * * @private */ async function _getIccProfiles (metadata, client) { const profiles = [] for (let i = 0; i &lt; metadata.length; i++) { const image = metadata[i] if (image.SamplesPerPixel === 3) { if (image.bulkdataReferences.OpticalPathSequence == null) { console.warn( `no ICC Profile was not found for image "${image.SOPInstanceUID}"` ) continue } const bulkdata = await _fetchBulkdata({ client, reference: ( image .bulkdataReferences .OpticalPathSequence[0] .ICCProfile ) }) profiles.push(bulkdata) } } return profiles } /** * Compute image pyramid. * * @param {object[]} metadata - Metadata of VL Whole Slide Microscopy Image instances * @returns {object} Information about the image pyramid * * @private */ function _computeImagePyramid ({ metadata }) { if (metadata.length === 0) { throw new Error( 'No image metadata was provided to computate image pyramid structure.' ) } // Sort instances and optionally concatenation parts if present. metadata.sort((a, b) => { const sizeDiff = a.TotalPixelMatrixColumns - b.TotalPixelMatrixColumns if (sizeDiff === 0) { if (a.ConcatenationFrameOffsetNumber !== undefined) { return a.ConcatenationFrameOffsetNumber - b.ConcatenationFrameOffsetNumber } return sizeDiff } return sizeDiff }) const pyramidMetadata = [] const pyramidFrameMappings = [] let pyramidNumberOfChannels for (let i = 0; i &lt; metadata.length; i++) { if (metadata[0].FrameOfReferenceUID !== metadata[i].FrameOfReferenceUID) { throw new Error( 'Images of pyramid must all have the same Frame of Reference UID.' ) } if (metadata[0].ContainerIdentifier !== metadata[i].ContainerIdentifier) { throw new Error( 'Images of pyramid must all have the same Container Identifier.' ) } const numberOfFrames = Number(metadata[i].NumberOfFrames || 1) const cols = metadata[i].TotalPixelMatrixColumns || metadata[i].Columns const rows = metadata[i].TotalPixelMatrixRows || metadata[i].Rows const { frameMapping, numberOfChannels } = getFrameMapping(metadata[i]) if (i > 0) { if (pyramidNumberOfChannels !== numberOfChannels) { throw new Error( 'Images of pyramid must all have the same number of channels ' + '(optical paths, segments, mappings, etc.)' ) } } else { pyramidNumberOfChannels = numberOfChannels } /* * Instances may be broken down into multiple concatentation parts. * Therefore, we have to re-assemble instance metadata. */ let alreadyExists = false let index = null for (let j = 0; j &lt; pyramidMetadata.length; j++) { const c = ( pyramidMetadata[j].TotalPixelMatrixColumns || pyramidMetadata[j].Columns ) const r = ( pyramidMetadata[j].TotalPixelMatrixRows || pyramidMetadata[j].Rows ) if (r === rows &amp;&amp; c === cols) { alreadyExists = true index = j } } if (alreadyExists) { Object.assign(pyramidFrameMappings[index], frameMapping) /* * Create a new SOP Instance with metadata updated from current * concatentation part. */ const rawMetadata = pyramidMetadata[index].json rawMetadata['00280008'].Value[0] += numberOfFrames if ('PerFrameFunctionalGroupsSequence' in metadata[index]) { rawMetadata['52009230'].Value.push( ...metadata[i].PerFrameFunctionalGroupsSequence ) } if (!('SOPInstanceUIDOfConcatenationSource' in metadata[i])) { throw new Error( 'Multiple image instances for the same channel and ' + 'focal plane have identical dimensions, but the instances ' + 'are not part of a concatenation either. ' + 'The image metadata is probably incorrect.' ) } const sopInstanceUID = metadata[i].SOPInstanceUIDOfConcatenationSource rawMetadata['00080018'].Value[0] = sopInstanceUID delete rawMetadata['00200242'] // SOPInstanceUIDOfConcatenationSource delete rawMetadata['00209161'] // ConcatentationUID delete rawMetadata['00209162'] // InConcatenationNumber delete rawMetadata['00209228'] // ConcatenationFrameOffsetNumber pyramidMetadata[index] = new VLWholeSlideMicroscopyImage({ metadata: rawMetadata }) } else { pyramidMetadata.push(metadata[i]) pyramidFrameMappings.push(frameMapping) } } const nLevels = pyramidMetadata.length if (nLevels === 0) { console.error('empty pyramid - no levels found') } const pyramidBaseMetadata = pyramidMetadata[nLevels - 1] /* * Collect relevant information from DICOM metadata for each pyramid * level to construct the Openlayers map. */ const pyramidTileSizes = [] const pyramidGridSizes = [] const pyramidResolutions = [] const pyramidOrigins = [] const pyramidPixelSpacings = [] const pyramidImageSizes = [] const pyramidPhysicalSizes = [] const offset = [0, -1] const baseTotalPixelMatrixColumns = pyramidBaseMetadata.TotalPixelMatrixColumns const baseTotalPixelMatrixRows = pyramidBaseMetadata.TotalPixelMatrixRows for (let j = (nLevels - 1); j >= 0; j--) { const columns = pyramidMetadata[j].Columns const rows = pyramidMetadata[j].Rows const totalPixelMatrixColumns = pyramidMetadata[j].TotalPixelMatrixColumns const totalPixelMatrixRows = pyramidMetadata[j].TotalPixelMatrixRows const pixelSpacing = getPixelSpacing(pyramidMetadata[j]) const nColumns = Math.ceil(totalPixelMatrixColumns / columns) const nRows = Math.ceil(totalPixelMatrixRows / rows) pyramidTileSizes.push([ columns, rows ]) pyramidGridSizes.push([ nColumns, nRows ]) pyramidPixelSpacings.push(pixelSpacing) pyramidImageSizes.push([ totalPixelMatrixColumns, totalPixelMatrixRows ]) pyramidPhysicalSizes.push([ (totalPixelMatrixColumns * pixelSpacing[1]).toFixed(4), (totalPixelMatrixRows * pixelSpacing[0]).toFixed(4) ]) /* * Compute the resolution at each pyramid level, since the zoom * factor may not be the same between adjacent pyramid levels. */ const zoomFactor = Math.round( baseTotalPixelMatrixColumns / totalPixelMatrixColumns ) pyramidResolutions.push(zoomFactor) pyramidOrigins.push(offset) } pyramidResolutions.reverse() pyramidTileSizes.reverse() pyramidGridSizes.reverse() pyramidOrigins.reverse() pyramidPixelSpacings.reverse() pyramidImageSizes.reverse() pyramidPhysicalSizes.reverse() const uniquePhysicalSizes = [ ...new Set(pyramidPhysicalSizes.map(v => v.toString())) ].map(v => v.split(',')) if (uniquePhysicalSizes.length > 1) { console.warn( 'images of the image pyramid have different sizes: ', '\nsize [mm]: ', pyramidPhysicalSizes, '\npixel spacing [mm]: ', pyramidPixelSpacings, '\nsize [pixels]: ', pyramidImageSizes, '\ntile size [pixels]: ', pyramidTileSizes, '\ntile grid size [tiles]: ', pyramidGridSizes, '\nresolution [factors]: ', pyramidResolutions ) } /** * Frames may extend beyond the size of the total pixel matrix. * The excess pixels may contain garbage and should not be displayed. * We set the extent to the size of the actual image without taken * excess pixels into account. * Note that the vertical axis is flipped in the used tile source, * i.e., values on the axis lie in the range [-n, -1], where n is the * number of rows in the total pixel matrix. */ const extent = [ 0, // min X -(baseTotalPixelMatrixRows + 1), // min Y baseTotalPixelMatrixColumns, // max X -1 // max Y ] return { extent, origins: pyramidOrigins, resolutions: pyramidResolutions, gridSizes: pyramidGridSizes, tileSizes: pyramidTileSizes, pixelSpacings: pyramidPixelSpacings, metadata: pyramidMetadata, frameMappings: pyramidFrameMappings, numberOfChannels: pyramidNumberOfChannels } } function _areImagePyramidsEqual (pyramid, refPyramid) { // Check that all the channels have the same pyramid parameters if (!are2DArraysAlmostEqual(pyramid.extent, refPyramid.extent)) { console.warn( 'pyramid has different extent as reference pyramid: ', pyramid.extent, refPyramid.extent ) return false } if (!are2DArraysAlmostEqual(pyramid.origins, refPyramid.origins)) { console.warn( 'pyramid has different origins as reference pyramid: ', pyramid.origins, refPyramid.origins ) return false } if (!are2DArraysAlmostEqual(pyramid.resolutions, refPyramid.resolutions)) { console.warn( 'pyramid has different resolutions as reference pyramid: ', pyramid.resolutions, refPyramid.resolutions ) return false } if (!are2DArraysAlmostEqual(pyramid.gridSizes, refPyramid.gridSizes)) { console.warn( 'pyramid has different grid sizes as reference pyramid: ', pyramid.gridSizes, refPyramid.gridSizes ) return false } if (!are2DArraysAlmostEqual(pyramid.tileSizes, refPyramid.tileSizes)) { console.warn( 'pyramid has different tile sizes as reference pyramid: ', pyramid.tileSizes, refPyramid.tileSizes ) return false } if (!are2DArraysAlmostEqual(pyramid.pixelSpacings, refPyramid.pixelSpacings)) { console.warn( 'pyramid has different pixel spacings as reference pyramid: ', pyramid.pixelSpacings, refPyramid.pixelSpacings ) return false } return true } function _createEmptyTile ({ columns, rows, samplesPerPixel, bitsAllocated, photometricInterpretation }) { let pixelArray if (bitsAllocated &lt;= 8) { pixelArray = new Uint8Array(columns * rows * samplesPerPixel) } else { pixelArray = new Float32Array(columns * rows * samplesPerPixel) } // Fill white in case of color and black in case of monochrome. let fillValue = Math.pow(2, bitsAllocated) - 1 if (photometricInterpretation === 'MONOCHROME2') { if (bitsAllocated &lt;= 16) { fillValue = 0 } else { // Float pixel data fillValue = -(Math.pow(2, bitsAllocated) - 1) / 2 } } for (let i = 0; i &lt; pixelArray.length; i++) { pixelArray[i] = fillValue } return pixelArray } function _createTileLoadFunction ({ pyramid, client, channel, iccProfiles, targetElement }) { return async (z, y, x) => { let index = (x + 1) + '-' + (y + 1) index += `-${channel}` if (pyramid.metadata[z] === undefined) { throw new Error( `Could not load tile for channel "${channel}" ` + `at position (${x + 1}, ${y + 1}) at zoom level ${z} ` + ` because level ${z} does not exist.` ) } const studyInstanceUID = pyramid.metadata[z].StudyInstanceUID const seriesInstanceUID = pyramid.metadata[z].SeriesInstanceUID const path = pyramid.frameMappings[z][index] let src if (path != null) { src = '' if (client.wadoURL !== undefined) { src += client.wadoURL } src += ( '/studies/' + studyInstanceUID + '/series/' + seriesInstanceUID + '/instances/' + path ) } const refImage = pyramid.metadata[z] const columns = refImage.Columns const rows = refImage.Rows const bitsAllocated = refImage.BitsAllocated const pixelRepresentation = refImage.PixelRepresentation const samplesPerPixel = refImage.SamplesPerPixel const photometricInterpretation = refImage.PhotometricInterpretation const sopClassUID = refImage.SOPClassUID if (src != null) { const sopInstanceUID = dwc.utils.getSOPInstanceUIDFromUri(src) const frameNumbers = dwc.utils.getFrameNumbersFromUri(src) if (samplesPerPixel === 1) { console.info( `retrieve frame ${frameNumbers} of monochrome image ` + `for channel "${channel}" at tile position (${x + 1}, ${y + 1}) ` + `at zoom level ${z}` ) } else { console.info( `retrieve frame ${frameNumbers} of color image ` + `at tile position (${x + 1}, ${y + 1}) at zoom level ${z}` ) } const jpegMediaType = 'image/jpeg' const jpegTransferSyntaxUID = '1.2.840.10008.1.2.4.50' const jlsMediaType = 'image/jls' const jlsTransferSyntaxUIDlossless = '1.2.840.10008.1.2.4.80' const jlsTransferSyntaxUID = '1.2.840.10008.1.2.4.81' const jp2MediaType = 'image/jp2' const jp2TransferSyntaxUIDlossless = '1.2.840.10008.1.2.4.90' const jp2TransferSyntaxUID = '1.2.840.10008.1.2.4.91' const jpxMediaType = 'image/jpx' const jpxTransferSyntaxUIDlossless = '1.2.840.10008.1.2.4.92' const jpxTransferSyntaxUID = '1.2.840.10008.1.2.4.93' const octetStreamMediaType = 'application/octet-stream' /* * Use of the "*" transfer syntax is a hack to work around standard * compliance issues of the Google Cloud Healthcare API. * It will return bulkdata encoded with the transfer syntax of the * stored data set (uncompressed or compressed). The decoder can then not * rely on the media type specified by the "Content-Type" header in the * response message, but will need to determine it from the payload. */ const octetStreamTransferSyntaxUID = '*' const mediaTypes = [] mediaTypes.push(...[ { mediaType: jlsMediaType, transferSyntaxUID: jlsTransferSyntaxUIDlossless }, { mediaType: jlsMediaType, transferSyntaxUID: jlsTransferSyntaxUID }, { mediaType: jp2MediaType, transferSyntaxUID: jp2TransferSyntaxUIDlossless }, { mediaType: jp2MediaType, transferSyntaxUID: jp2TransferSyntaxUID }, { mediaType: jpxMediaType, transferSyntaxUID: jpxTransferSyntaxUIDlossless }, { mediaType: jpxMediaType, transferSyntaxUID: jpxTransferSyntaxUID }, { mediaType: octetStreamMediaType, transferSyntaxUID: octetStreamTransferSyntaxUID } ]) if (bitsAllocated &lt;= 8) { mediaTypes.push({ mediaType: jpegMediaType, transferSyntaxUID: jpegTransferSyntaxUID }) } const frameInfo = { studyInstanceUID, seriesInstanceUID, sopInstanceUID, sopClassUID, frameNumber: frameNumbers[0], channelIdentifier: String(channel) } publish(targetElement, EVENT.FRAME_LOADING_STARTED, frameInfo) const retrieveOptions = { studyInstanceUID, seriesInstanceUID, sopInstanceUID, frameNumbers, mediaTypes } return client.retrieveInstanceFrames(retrieveOptions).then( (rawFrames) => { return _decodeAndTransformFrame({ frame: rawFrames[0], frameNumber: frameNumbers[0], bitsAllocated, pixelRepresentation, columns, rows, samplesPerPixel, sopInstanceUID, metadata: pyramid.metadata, iccProfiles }).then(pixelArray => { if (pixelArray.constructor === Float64Array) { // TODO: handle Float64Array using LUT throw new Error( 'Double Float Pixel Data is not (yet) supported.' ) } publish( targetElement, EVENT.FRAME_LOADING_ENDED, { pixelArray, ...frameInfo } ) if (samplesPerPixel === 3 &amp;&amp; bitsAllocated === 8) { // Rendering of color images requires unsigned 8-bit integers return pixelArray } // Rendering of grayscale images requires floating point values return new Float32Array( pixelArray, pixelArray.byteOffset, pixelArray.byteLength / pixelArray.BYTES_PER_ELEMENT ) }) } ).catch( (error) => { publish(targetElement, EVENT.FRAME_LOADING_ENDED, frameInfo) publish(targetElement, EVENT.FRAME_LOADING_ERROR, frameInfo) return Promise.reject( new Error( `Failed to load frames ${frameNumbers} ` + `of SOP instance "${sopInstanceUID}" ` + `for channel "${channel}" ` + `at tile position (${x + 1}, ${y + 1}) ` + `at zoom level ${z}: `, error ) ) } ) } else { console.warn( `could not load tile "${index}" at level ${z}, ` + 'this tile does not exist' ) return _createEmptyTile({ columns, rows, samplesPerPixel, bitsAllocated, photometricInterpretation }) } } } function _fitImagePyramid (pyramid, refPyramid) { const matchingLevelIndices = [] for (let i = 0; i &lt; refPyramid.metadata.length; i++) { for (let j = 0; j &lt; pyramid.metadata.length; j++) { const doOriginsMatch = are1DArraysAlmostEqual( refPyramid.origins[i], pyramid.origins[j] ) const doPixelSpacingsMatch = are1DArraysAlmostEqual( refPyramid.pixelSpacings[i], pyramid.pixelSpacings[j] ) if (doOriginsMatch &amp;&amp; doPixelSpacingsMatch) { matchingLevelIndices.push([i, j]) } } } if (matchingLevelIndices.length === 0) { console.error(pyramid, refPyramid) throw new Error( 'Image pyramid cannot be fit to reference image pyramid.' ) } // Fit the pyramid levels to the reference image pyramid const fittedPyramid = { extent: [...refPyramid.extent], origins: [], resolutions: [], gridSizes: [], tileSizes: [], pixelSpacings: [], metadata: [], frameMappings: [] } for (let i = 0; i &lt; refPyramid.metadata.length; i++) { const index = matchingLevelIndices.find(element => element[0] === i) if (index) { const j = index[1] fittedPyramid.origins.push([...pyramid.origins[j]]) fittedPyramid.gridSizes.push([...pyramid.gridSizes[j]]) fittedPyramid.tileSizes.push([...pyramid.tileSizes[j]]) fittedPyramid.resolutions.push(Number(refPyramid.resolutions[i])) fittedPyramid.pixelSpacings.push([...pyramid.pixelSpacings[j]]) fittedPyramid.metadata.push(pyramid.metadata[j]) fittedPyramid.frameMappings.push(pyramid.frameMappings[j]) } } let minZoom = 0 for (let i = 0; i &lt; refPyramid.resolutions.length; i++) { for (let j = 0; j &lt; fittedPyramid.resolutions.length; j++) { if (refPyramid.resolutions[i] === fittedPyramid.resolutions[j]) { minZoom = i break } } } let maxZoom = refPyramid.resolutions.length - 1 for (let i = (refPyramid.resolutions.length - 1); i >= minZoom; i--) { for (let j = (fittedPyramid.resolutions.length - 1); j >= 0; j--) { if (refPyramid.resolutions[i] === fittedPyramid.resolutions[j]) { maxZoom = i break } } } return [fittedPyramid, minZoom, maxZoom] } export { _areImagePyramidsEqual, _computeImagePyramid, _createTileLoadFunction, _fitImagePyramid, _getIccProfiles } </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>