UNPKG

dicom-microscopy-viewer-changed

Version:
734 lines (668 loc) 23.7 kB
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <title>JSDoc: Source: utils.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: utils.js</h1> <section> <article> <pre class="prettyprint source linenums"><code>import { inv, multiply } from 'mathjs' import { getPointResolution } from 'ol/proj' import { v4 as createUUIDv4, v5 as createUUIDv5 } from 'uuid' const _UUID_NAMESPACE = 'c4f09b11-bac0-4f3a-8dc1-9f0046637383' /** * Generates a UUID-derived DICOM UID with root `2.25`. * * @returns {string} Unique identifier * * @private */ function _generateUID ({ value } = {}) { /** * A UUID can be represented as a single integer value. * http://dicom.nema.org/medical/dicom/current/output/chtml/part05/sect_B.2.html * https://www.itu.int/rec/T-REC-X.667-201210-I/en * To obtain the single integer value of the UUID, the 16 octets of the * binary representation shall be treated as an unsigned integer encoding * with the most significant bit of the integer encoding as the most * significant bit (bit 7) of the first of the sixteen octets (octet 15) and * the least significant bit as the least significant bit (bit 0) of the last * of the sixteen octets (octet 0). */ let uuid if (value != null) { uuid = createUUIDv5(value, _UUID_NAMESPACE) } else { uuid = createUUIDv4() } const hex = '0x' + uuid.replace(/-/g, '') const decimal = BigInt(hex) return '2.25.' + decimal.toString() } /** * Create a rotation matrix. * * @param {Object} options - Options * @param {number[]} options.orientation - Direction cosines along the row and column direction of the Total Pixel Matrix for each of the three axis of the slide coordinate system * * @returns {number[][]} 2x2 rotation matrix * * @memberof utils */ function createRotationMatrix (options) { if (!('orientation' in options)) { throw new Error('Option "orientation" is required.') } const orientation = options.orientation const rowDirection = orientation.slice(0, 3) const columnDirection = orientation.slice(3, 6) return [ [rowDirection[0], columnDirection[0]], [rowDirection[1], columnDirection[1]], [rowDirection[2], columnDirection[3]] ] } /** * Rescale intensity from [minInput, maxInput] to [minOutput, maxOutput]. * * @param {number} value - Input value that should be rescaled * @param {number} minInput - Lower bound of the full input value range * @param {number} maxInput - Upper bound of the full input value range * @param {number} minOutput - Lower bound of the full output value range * @param {number} maxOutput - Upper bound of the full output value range * * @returns {number} Rescaled value * * @memberof utils */ function rescale ( value, minInput, maxInput, minOutput, maxOutput ) { return ( (value - minInput) * (maxOutput - minOutput) / (maxInput - minInput) + minOutput ) } /** * Create window. * * @param {number} lowerBound - Lower bound of the window * @param {number} upperBound - Upper bound of the window * * @returns {number[]} Window center and width * * @memberof utils */ function createWindow (lowerBound, upperBound) { const windowCenter = (lowerBound + upperBound) / 2 const windowWidth = upperBound - lowerBound return [windowCenter, windowWidth] } /** * Compute the rotation of the image with respect to the frame of reference. * * @param {Object} options - Options * @param {number[]} options.orientation - Direction cosines along the row and column direction of the Total Pixel Matrix for each of the three axis of the slide coordinate system * @param {boolean} options.inDegrees - Whether angle should be returned in degrees instead of radians * * @returns {number} Angle * * @memberof utils */ function computeRotation (options) { const rot = createRotationMatrix({ orientation: options.orientation }) const angle = Math.atan2(-rot[0][1], rot[0][0]) let inDegrees = false if ('inDegrees' in options) { inDegrees = true } if (inDegrees) { return angle / (Math.PI / 180) } else { return angle } } /** * Build an affine transformation matrix to map coordinates in the Total * Pixel Matrix into the slide coordinate system. * * @param {Object} options - Options * @param {number[]} options.offset - X and Y offset of the image in the slide coordinate system * @param {number[]} options.orientation - Direction cosines along the row and column direction of the Total Pixel Matrix for each of the three axis of the slide coordinate system * @param {number[]} options.spacing - Spacing between pixel rows and columns of the Total Pixel Matrix * * @returns {number[][]} 3x3 affine transformation matrix * * @memberof utils */ function buildTransform ({ offset, orientation, spacing }) { // X and Y Offset in Slide Coordinate System if (offset == null) { throw new Error('Option "offset" is required.') } if (!Array.isArray(offset)) { throw new Error('Option "offset" must be an array.') } if (offset.length !== 2) { throw new Error('Option "offset" must be an array with 2 elements.') } // Image Orientation Slide with direction cosines for Row and Column direction if (orientation == null) { throw new Error('Option "orientation" is required.') } if (!Array.isArray(orientation)) { throw new Error('Option "orientation" must be an array.') } if (orientation.length !== 6) { throw new Error('Option "orientation" must be an array with 6 elements.') } // Pixel Spacing along the Row and Column direction if (spacing == null) { throw new Error('Option "spacing" is required.') } if (!Array.isArray(spacing)) { throw new Error('Option "spacing" must be an array.') } if (spacing.length !== 2) { throw new Error('Option "spacing" must be an array with 2 elements.') } const affine = [ [orientation[0] * spacing[1], orientation[3] * spacing[0], offset[0]], [orientation[1] * spacing[1], orientation[4] * spacing[0], offset[1]], [0, 0, 1] ] const correction = [ [1.0, 0.0, -0.5], [0.0, 1.0, -0.5], [0.0, 0.0, 1.0] ] return multiply(affine, correction) } /** * Apply an affine transformation to an image coordinate in the total pixel * matrix to map it into the slide coordinate system. * * @param {Object} options - Options * @param {number[]} options.coordinate - (column, row) image coordinate * @param {number[][]} options.affine - 3x3 affine transformation matrix * * @returns {number[]} (x, y) reference coordinate * * @memberof utils */ function applyTransform ({ coordinate, affine }) { if (coordinate == null) { throw new Error('Option "coordinate" is required.') } if (!Array.isArray(coordinate)) { throw new Error('Option "coordinate" must be an array.') } if (coordinate.length !== 2) { throw new Error('Option "coordinate" must be an array with 2 elements.') } if (affine == null) { throw new Error('Option "affine" is required.') } if (!Array.isArray(affine)) { throw new Error('Option "affine" must be an array.') } if (affine.length !== 3) { throw new Error('Option "affine" must be a 3x3 array.') } if (!Array.isArray(affine[0])) { throw new Error('Option "affine" must be a 3x3 array.') } if (affine[0].length !== 3 || affine[1].length !== 3) { throw new Error('Option "affine" must be a 3x3 array.') } const imageCoordinate = [[coordinate[0]], [coordinate[1]], [1]] const slideCoordinate = multiply(affine, imageCoordinate) const x = Number(slideCoordinate[0][0].toFixed(4)) const y = Number(slideCoordinate[1][0].toFixed(4)) return [x, y] } /** * Build an affine transformation matrix to map coordinates in the slide * coordinate system into the Total Pixel Matrix. * * @param {number[]} options.offset - X and Y offset of the image in the slide coordinate system * @param {number[]} options.orientation - Direction cosines along the row and column direction of the Total Pixel Matrix for each of the three axis of the slide coordinate system * @param {number[]} options.spacing - Spacing between pixel rows and columns of the Total Pixel Matrix * * @returns {number[][]} 3x3 affine transformation matrix * * @memberof utils */ function buildInverseTransform ({ offset, orientation, spacing }) { // X and Y Offset in Slide Coordinate System if (offset == null) { throw new Error('Option "offset" is required.') } if (!Array.isArray(offset)) { throw new Error('Option "offset" must be an array.') } if (offset.length !== 2) { throw new Error('Option "offset" must be an array with 2 elements.') } // Image Orientation Slide with direction cosines for Row and Column direction if (orientation == null) { throw new Error('Option "orientation" is required.') } if (!Array.isArray(orientation)) { throw new Error('Option "orientation" must be an array.') } if (orientation.length !== 6) { throw new Error('Option "orientation" must be an array with 6 elements.') } // Pixel Spacing along the Row and Column direction if (spacing == null) { throw new Error('Option "spacing" is required.') } if (!Array.isArray(spacing)) { throw new Error('Option "spacing" must be an array.') } if (spacing.length !== 2) { throw new Error('Option "spacing" must be an array with 2 elements.') } const affine = inv([ [orientation[0] * spacing[1], orientation[3] * spacing[0], offset[0]], [orientation[1] * spacing[1], orientation[4] * spacing[0], offset[1]], [0, 0, 1] ]) const correction = [ [1.0, 0.0, 0.5], [0.0, 1.0, 0.5], [0.0, 0.0, 1.0] ] return multiply(correction, affine) } /** * Apply an affine transformation to a reference coordinate in the slide * coordinate system to map it into the total pixel matrix. * * @param {Object} options - Options * @param {number[]} options.coordinate - (x, y) reference coordinate * @param {number[][]} options.affine - 3x3 affine transformation matrix * * @returns {number[]} (column, row) image coordinate * * @memberof utils */ function applyInverseTransform ({ coordinate, affine }) { if (coordinate == null) { throw new Error('Option "coordinate" is required.') } if (!Array.isArray(coordinate)) { throw new Error('Option "coordinate" must be an array.') } if (coordinate.length !== 2) { throw new Error('Option "coordinate" must be an array with 2 elements.') } if (affine == null) { throw new Error('Option "affine" is required.') } if (!Array.isArray(affine)) { throw new Error('Option "affine" must be an array.') } if (affine.length !== 3) { throw new Error('Option "affine" must be a 3x3 array.') } if (!Array.isArray(affine[0])) { throw new Error('Option "affine" must be a 3x3 array.') } if (affine[0].length !== 3 || affine[1].length !== 3) { throw new Error('Option "affine" must be a 3x3 array.') } const slideCoordinate = [[coordinate[0]], [coordinate[1]], [1]] const pixelCoordinate = multiply(affine, slideCoordinate) const col = Number(pixelCoordinate[0][0].toFixed(4)) const row = Number(pixelCoordinate[1][0].toFixed(4)) return [col, row] } /** * Map 2D (column, row) image coordinates in the Total Pixel Matrix * to 3D (x, y, z) slide coordinates in the Frame of Reference. * * @param {Object} options - Options * @param {number[]} options.offset - X and Y offset in the slide coordinate system * @param {number[]} options.orientation - Direction cosines along the row and column direction of the Total Pixel Matrix for each of the three axis of the slide coordinate system * @param {number[]} options.spacing - Spacing between pixels along the Column and Row direction of the Total Pixel Matrix * @param {number[]} options.point - (colum, row) image coordinates * * @returns {number[]} (x, y, z) slide coordinates * * @memberof utils */ function mapPixelCoordToSlideCoord ({ point, offset, orientation, spacing }) { if (point == null) { throw new Error('Option "point" is required.') } if (!Array.isArray(point)) { throw new Error('Option "point" must be an array.') } if (point.length !== 2) { throw new Error('Option "point" must be an array with 2 elements.') } const affine = buildTransform({ orientation, offset, spacing }) return applyTransform({ coordinate: point, affine }) } /** * Map 3D (x, y, z) slide coordinates in the Frame of Reference to * 2D (column, row) image coordinates in the Total Pixel Matrix. * * @param {Object} options - Options * @param {number[]} options.offset - X and Y offset in the slide coordinate system * @param {number[]} options.orientation - Direction cosines along the row and column direction of the Total Pixel Matrix for each of the three axis of the slide coordinate system * @param {number[]} options.spacing - Spacing between pixels along the Column and Row direction of the Total Pixel Matrix * @param {number[]} options.point - (x, y, z) slide coordinates * * @returns {number[]} (row, column) image coordinates * * @memberof utils */ function mapSlideCoordToPixelCoord ({ point, offset, orientation, spacing }) { if (point == null) { throw new Error('Option "point" is required.') } if (!Array.isArray(point)) { throw new Error('Option "point" must be an array.') } if (point.length !== 2) { throw new Error('Option "point" must be an array with 2 elements.') } const affine = buildInverseTransform({ orientation, offset, spacing }) return applyInverseTransform({ coordinate: point, affine }) } /** * Check if 2D arrays are equal. * * @param {number[]} array a * @param {number[]} array b * @param {number} eps * * @returns {boolean} yes/no answer * * @memberof utils */ function are2DArraysAlmostEqual (a, b, eps = 1.e-6) { if (a === b) return true if (a == null || b == null) return false if (a.length !== b.length) return false for (let i = 0; i &lt; a.length; ++i) { if (a[i].length !== b[i].length) return false for (let j = 0; j &lt; a[i].length; ++j) { if (!areNumbersAlmostEqual(a[i][j], b[i][j], eps)) { return false } } } return true } /** * Check if 1D arrays are equal. * * @param {number[]} array a * @param {number[]} array b * @param {number} eps * * @returns {boolean} yes/no answer * * @memberof utils */ function are1DArraysAlmostEqual (a, b, eps = 1.e-6) { if (a == null || b == null) return false if (a.length !== b.length) return false for (let i = 0; i &lt; a.length; ++i) { if (!areNumbersAlmostEqual(a[i], b[i], eps)) { return false } } return true } /** * Check if two numbers are equal. * * @param {number} a * @param {number} b * @param {number} eps * * @returns {boolean} yes/no answer * * @memberof utils */ function areNumbersAlmostEqual (a, b, eps = 1.e-6) { return Math.abs(a - b) &lt; eps } /** * Get view unit suffix. * * @param {object} view Map view * * @returns {string} unit suffix * * @private */ function _getUnitSuffix (view) { const UnitsEnum = { METERS: 'm' } const DEFAULT_DPI = 25.4 / 0.28 const center = view.getCenter() const projection = view.getProjection() const resolution = view.getResolution() const pointResolutionUnits = UnitsEnum.METERS let pointResolution = getPointResolution( projection, resolution, center, pointResolutionUnits ) const DEFAULT_MIN_WIDTH = 65 const minWidth = (DEFAULT_MIN_WIDTH * DEFAULT_DPI) / DEFAULT_DPI const nominalCount = minWidth * pointResolution let suffix = '' if (nominalCount &lt; 0.001) { suffix = 'μm' pointResolution *= 1000000 } else if (nominalCount &lt; 1) { suffix = 'mm' pointResolution *= 1000 } else if (nominalCount &lt; 1000) { suffix = 'm' } else { suffix = 'km' pointResolution /= 1000 } return suffix } /** * Get name coded concept from content item. * * @param {object} contentItem * * @returns {object} The concept name coded concept * * @memberof utils */ const getContentItemNameCodedConcept = (contentItem) => contentItem.ConceptNameCodeSequence[0] /** * Check whether coded concepts are equal. * * @param {object} codedConcept1 * @param {object} codedConcept2 * * @returns {boolean} yes/no answer * * @memberof utils */ const areCodedConceptsEqual = (codedConcept1, codedConcept2) => { if ( codedConcept2.CodeValue === codedConcept1.CodeValue &amp;&amp; codedConcept2.CodingSchemeDesignator === codedConcept1.CodingSchemeDesignator ) { if ( codedConcept2.CodingSchemeVersion &amp;&amp; codedConcept1.CodingSchemeVersion ) { return ( codedConcept2.CodingSchemeVersion === codedConcept1.CodingSchemeVersion ) } return true } return false } /** * Check wether two content items match. * * @param {object} contentItem1 * @param {object} contentItem2 * * @returns {boolean} yes/no answer * * @memberof utils */ const doContentItemsMatch = (contentItem1, contentItem2) => { const contentItem1NameCodedConcept = getContentItemNameCodedConcept( contentItem1 ) const contentItem2NameCodedConcept = getContentItemNameCodedConcept( contentItem2 ) return contentItem1NameCodedConcept.equals ? contentItem1NameCodedConcept.equals(contentItem2NameCodedConcept) : areCodedConceptsEqual( contentItem1NameCodedConcept, contentItem2NameCodedConcept ) } /** * Fetch bulkdata. * * @param {object} options * @param {object} options.client - DICOMweb client @param {object} * options.reference - Data Element in DICOM JSON format containing "vr" and * "BulkDataURI" fields * * @returns {Promise&lt;TypedArray>} bulkdata * * @private */ async function _fetchBulkdata ({ client, reference }) { const retrieveOptions = { BulkDataURI: reference.BulkDataURI } return await client.retrieveBulkData(retrieveOptions).then(data => { const byteArray = new Uint8Array(data[0]) if (reference.vr === 'OB') { return byteArray } else if (reference.vr === 'OW') { return new Uint16Array( byteArray.buffer, byteArray.byteOffset, byteArray.byteLength / 2 ) } else if (reference.vr === 'OL') { return new Int32Array( byteArray.buffer, byteArray.byteOffset, byteArray.byteLength / 4 ) } else if (reference.vr === 'OV') { // There is no Int64Array, so we represent data as Float64Array instead return new Float64Array( byteArray.buffer, byteArray.byteOffset, byteArray.byteLength / 8 ) } else if (reference.vr === 'OF') { return new Float32Array( byteArray.buffer, byteArray.byteOffset, byteArray.byteLength / 4 ) } else if (reference.vr === 'OD') { return new Float64Array( byteArray.buffer, byteArray.byteOffset, byteArray.byteLength / 8 ) } else { throw new Error( `Unexpected Value Representation "${reference.vr}" for ` + `bulkdata element with URI "${reference.BulkDataURI}".` ) } }) } /** * Convert RGB color triplet into hex code. * * @param {Number[]} values - RGB triplet * @returns {String} Hex code * * @private */ function rgb2hex (values) { const r = values[0] const g = values[1] const b = values[2] return '#' + (0x1000000 + (r &lt;&lt; 16) + (g &lt;&lt; 8) + b).toString(16).slice(1) } export { _getUnitSuffix, applyInverseTransform, applyTransform, buildInverseTransform, buildTransform, computeRotation, createWindow, _fetchBulkdata, _generateUID, mapPixelCoordToSlideCoord, mapSlideCoordToPixelCoord, areNumbersAlmostEqual, are1DArraysAlmostEqual, are2DArraysAlmostEqual, doContentItemsMatch, areCodedConceptsEqual, getContentItemNameCodedConcept, rgb2hex, rescale } </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>