UNPKG

custom-cornerstone-tools

Version:

Medical imaging tools for the Cornerstone library - customized for DrNuvem

698 lines (595 loc) 26 kB
/*! cornerstone-wado-image-loader - v0.5.2 - 2015-04-04 | (c) 2014 Chris Hafey | https://github.com/chafey/cornerstoneWADOImageLoader */ // // This is a cornerstone image loader for WADO requests. It currently does not support compressed // transfer syntaxes or big endian transfer syntaxes. It will support implicit little endian transfer // syntaxes but explicit little endian is strongly preferred to avoid any parsing issues related // to SQ elements. To request that the WADO object be returned as explicit little endian, append // the following on your WADO url: &transferSyntax=1.2.840.10008.1.2.1 // var cornerstoneWADOImageLoader = (function ($, cornerstone, cornerstoneWADOImageLoader) { "use strict"; if(cornerstoneWADOImageLoader === undefined) { cornerstoneWADOImageLoader = {}; } function isColorImage(photoMetricInterpretation) { if(photoMetricInterpretation === "RGB" || photoMetricInterpretation === "PALETTE COLOR" || photoMetricInterpretation === "YBR_FULL" || photoMetricInterpretation === "YBR_FULL_422" || photoMetricInterpretation === "YBR_PARTIAL_422" || photoMetricInterpretation === "YBR_PARTIAL_420" || photoMetricInterpretation === "YBR_RCT") { return true; } else { return false; } } function createImageObject(dataSet, imageId, frame) { if(frame === undefined) { frame = 0; } // make the image based on whether it is color or not var photometricInterpretation = dataSet.string('x00280004'); var isColor = isColorImage(photometricInterpretation); if(isColor === false) { return cornerstoneWADOImageLoader.makeGrayscaleImage(imageId, dataSet, dataSet.byteArray, photometricInterpretation, frame); } else { return cornerstoneWADOImageLoader.makeColorImage(imageId, dataSet, dataSet.byteArray, photometricInterpretation, frame); } } var multiFrameCacheHack = {}; // Loads an image given an imageId // wado url example: // http://localhost:3333/wado?requestType=WADO&studyUID=1.3.6.1.4.1.25403.166563008443.5076.20120418075541.1&seriesUID=1.3.6.1.4.1.25403.166563008443.5076.20120418075541.2&objectUID=1.3.6.1.4.1.25403.166563008443.5076.20120418075557.1&contentType=application%2Fdicom&transferSyntax=1.2.840.10008.1.2.1 // NOTE: supposedly the instance will be returned in Explicit Little Endian transfer syntax if you don't // specify a transferSyntax but Osirix doesn't do this and seems to return it with the transfer syntax it is // stored as. function loadImage(imageId) { // create a deferred object // TODO: Consider not using jquery for deferred - maybe cujo's when library var deferred = $.Deferred(); // build a url by parsing out the url scheme and frame index from the imageId var url = imageId; url = url.substring(9); var frameIndex = url.indexOf('frame='); var frame; if(frameIndex !== -1) { var frameStr = url.substr(frameIndex + 6); frame = parseInt(frameStr); url = url.substr(0, frameIndex-1); } // if multiframe and cached, use the cached data set to extract the frame if(frame !== undefined && multiFrameCacheHack.hasOwnProperty(url)) { var dataSet = multiFrameCacheHack[url]; var imagePromise = createImageObject(dataSet, imageId, frame); imagePromise.then(function(image) { deferred.resolve(image); }, function() { deferred.reject(); }); return deferred; } // Make the request for the DICOM data // TODO: consider using cujo's REST library here? var oReq = new XMLHttpRequest(); oReq.open("get", url, true); oReq.responseType = "arraybuffer"; //oReq.setRequestHeader("Accept", "multipart/related; type=application/dicom"); oReq.onreadystatechange = function(oEvent) { // TODO: consider sending out progress messages here as we receive the pixel data if (oReq.readyState === 4) { if (oReq.status === 200) { // request succeeded, create an image object and resolve the deferred // Parse the DICOM File var dicomPart10AsArrayBuffer = oReq.response; var byteArray = new Uint8Array(dicomPart10AsArrayBuffer); var dataSet = dicomParser.parseDicom(byteArray); // if multiframe, cache the parsed data set to speed up subsequent // requests for the other frames if(frame !== undefined) { multiFrameCacheHack[url] = dataSet; } var imagePromise = createImageObject(dataSet, imageId, frame); imagePromise.then(function(image) { deferred.resolve(image); }, function() { deferred.reject(); }); } // TODO: Check for errors and reject the deferred if they happened else { // TODO: add some error handling here // request failed, reject the deferred deferred.reject(); } } }; oReq.onprogress = function(oProgress) { // console.log('progress:',oProgress) if (oProgress.lengthComputable) { //evt.loaded the bytes browser receive //evt.total the total bytes seted by the header // var loaded = oProgress.loaded; var total = oProgress.total; var percentComplete = Math.round((loaded / total)*100); $(cornerstone.events).trigger('CornerstoneImageLoadProgress', { imageId: imageId, loaded: loaded, total: total, percentComplete: percentComplete }); } }; oReq.send(); return deferred; } // steam the http and https prefixes so we can use wado URL's directly cornerstone.registerImageLoader('dicomweb', loadImage); return cornerstoneWADOImageLoader; }($, cornerstone, cornerstoneWADOImageLoader)); /** */ var cornerstoneWADOImageLoader = (function (cornerstoneWADOImageLoader) { "use strict"; if(cornerstoneWADOImageLoader === undefined) { cornerstoneWADOImageLoader = {}; } function decodeRGB(rgbBuffer, rgbaBuffer) { if(rgbBuffer === undefined) { throw "decodeRGB: rgbBuffer must not be undefined"; } if(rgbBuffer.length % 3 !== 0) { throw "decodeRGB: rgbBuffer length must be divisble by 3"; } var numPixels = rgbBuffer.length / 3; var rgbIndex = 0; var rgbaIndex = 0; for(var i= 0; i < numPixels; i++) { rgbaBuffer[rgbaIndex++] = rgbBuffer[rgbIndex++]; // red rgbaBuffer[rgbaIndex++] = rgbBuffer[rgbIndex++]; // green rgbaBuffer[rgbaIndex++] = rgbBuffer[rgbIndex++]; // blue rgbaBuffer[rgbaIndex++] = 255; //alpha } } // module exports cornerstoneWADOImageLoader.decodeRGB = decodeRGB; return cornerstoneWADOImageLoader; }(cornerstoneWADOImageLoader)); /** */ var cornerstoneWADOImageLoader = (function (cornerstoneWADOImageLoader) { "use strict"; if(cornerstoneWADOImageLoader === undefined) { cornerstoneWADOImageLoader = {}; } function decodeYBRFull(ybrBuffer, rgbaBuffer) { if(ybrBuffer === undefined) { throw "decodeRGB: ybrBuffer must not be undefined"; } if(ybrBuffer.length % 3 !== 0) { throw "decodeRGB: ybrBuffer length must be divisble by 3"; } var numPixels = ybrBuffer.length / 3; var ybrIndex = 0; var rgbaIndex = 0; for(var i= 0; i < numPixels; i++) { var y = ybrBuffer[ybrIndex++]; var cb = ybrBuffer[ybrIndex++]; var cr = ybrBuffer[ybrIndex++]; rgbaBuffer[rgbaIndex++] = y + 1.40200 * (cr - 128);// red rgbaBuffer[rgbaIndex++] = y - 0.34414 * (cb -128) - 0.71414 * (cr- 128); // green rgbaBuffer[rgbaIndex++] = y + 1.77200 * (cb - 128); // blue rgbaBuffer[rgbaIndex++] = 255; //alpha } } // module exports cornerstoneWADOImageLoader.decodeYBRFull = decodeYBRFull; return cornerstoneWADOImageLoader; }(cornerstoneWADOImageLoader)); var cornerstoneWADOImageLoader = (function (cornerstoneWADOImageLoader) { "use strict"; if(cornerstoneWADOImageLoader === undefined) { cornerstoneWADOImageLoader = {}; } function getPixelSpacing(dataSet) { // NOTE - these are not required for all SOP Classes // so we return them as undefined. We also do not // deal with the complexity associated with projection // radiographs here and leave that to a higher layer var pixelSpacing = dataSet.string('x00280030'); if(pixelSpacing && pixelSpacing.length > 0) { var split = pixelSpacing.split('\\'); return { row: parseFloat(split[0]), column: parseFloat(split[1]) }; } else { return { row: undefined, column: undefined }; } } // module exports cornerstoneWADOImageLoader.getPixelSpacing = getPixelSpacing; return cornerstoneWADOImageLoader; }(cornerstoneWADOImageLoader)); var cornerstoneWADOImageLoader = (function (cornerstoneWADOImageLoader) { "use strict"; if(cornerstoneWADOImageLoader === undefined) { cornerstoneWADOImageLoader = {}; } function getRescaleSlopeAndIntercept(dataSet) { // NOTE - we default these to an identity transform since modality LUT // module is not required for all SOP Classes var result = { intercept : 0.0, slope: 1.0 }; //var rescaleIntercept = dicomElements.x00281052; //var rescaleSlope = dicomElements.x00281053; var rescaleIntercept = dataSet.floatString('x00281052'); var rescaleSlope = dataSet.floatString('x00281053'); if(rescaleIntercept ) { result.intercept = rescaleIntercept; } if(rescaleSlope ) { result.slope = rescaleSlope; } return result; } // module exports cornerstoneWADOImageLoader.getRescaleSlopeAndIntercept = getRescaleSlopeAndIntercept; return cornerstoneWADOImageLoader; }(cornerstoneWADOImageLoader)); var cornerstoneWADOImageLoader = (function (cornerstoneWADOImageLoader) { "use strict"; if(cornerstoneWADOImageLoader === undefined) { cornerstoneWADOImageLoader = {}; } function getWindowWidthAndCenter(dataSet) { // NOTE - Default these to undefined since they may not be present as // they are not present or required for all sop classes. We leave it up // to a higher layer to determine reasonable default values for these // if they are not provided. We also use the first ww/wc values if // there are multiple and again leave it up the higher levels to deal with // this var result = { windowCenter : undefined, windowWidth: undefined }; var windowCenter = dataSet.floatString('x00281050'); var windowWidth = dataSet.floatString('x00281051'); if(windowCenter) { result.windowCenter = windowCenter; } if(windowWidth ) { result.windowWidth = windowWidth; } return result; } // module exports cornerstoneWADOImageLoader.getWindowWidthAndCenter = getWindowWidthAndCenter; return cornerstoneWADOImageLoader; }(cornerstoneWADOImageLoader)); var cornerstoneWADOImageLoader = (function ($, cornerstone, cornerstoneWADOImageLoader) { "use strict"; if(cornerstoneWADOImageLoader === undefined) { cornerstoneWADOImageLoader = {}; } var canvas = document.createElement('canvas'); var lastImageIdDrawn = ""; function arrayBufferToString(buffer) { return binaryToString(String.fromCharCode.apply(null, Array.prototype.slice.apply(new Uint8Array(buffer)))); } function binaryToString(binary) { var error; try { return decodeURIComponent(escape(binary)); } catch (_error) { error = _error; if (error instanceof URIError) { return binary; } else { throw error; } } } function extractStoredPixels(dataSet, byteArray, photometricInterpretation, width, height, frame) { canvas.height = height; canvas.width = width; var pixelDataElement = dataSet.elements.x7fe00010; var pixelDataOffset = pixelDataElement.dataOffset; var transferSyntax = dataSet.string('x00020010'); var frameSize = width * height * 3; var frameOffset = pixelDataOffset + frame * frameSize; var encodedPixelData;// = new Uint8Array(byteArray.buffer, frameOffset); var context = canvas.getContext('2d'); var imageData = context.createImageData(width, height); var deferred = $.Deferred(); if (photometricInterpretation === "RGB") { encodedPixelData = new Uint8Array(byteArray.buffer, frameOffset, frameSize); cornerstoneWADOImageLoader.decodeRGB(encodedPixelData, imageData.data); deferred.resolve(imageData); return deferred; } else if (photometricInterpretation === "YBR_FULL") { encodedPixelData = new Uint8Array(byteArray.buffer, frameOffset, frameSize); cornerstoneWADOImageLoader.decodeYBRFull(encodedPixelData, imageData.data); deferred.resolve(imageData); return deferred; } else if(photometricInterpretation === "YBR_FULL_422" && transferSyntax === "1.2.840.10008.1.2.4.50") { encodedPixelData = dicomParser.readEncapsulatedPixelData(dataSet, dataSet.elements.x7fe00010, frame); // need to read the encapsulated stream here i think var imgBlob = new Blob([encodedPixelData], {type: "image/jpeg"}); var r = new FileReader(); if(r.readAsBinaryString === undefined) { r.readAsArrayBuffer(imgBlob); } else { r.readAsBinaryString(imgBlob); // doesn't work on IE11 } r.onload = function(){ var img=new Image(); img.onload = function() { context.drawImage(this, 0, 0); imageData = context.getImageData(0, 0, width, height); deferred.resolve(imageData); }; img.onerror = function(z) { deferred.reject(); }; if(r.readAsBinaryString === undefined) { img.src = "data:image/jpeg;base64,"+window.btoa(arrayBufferToString(r.result)); } else { img.src = "data:image/jpeg;base64,"+window.btoa(r.result); // doesn't work on IE11 } }; return deferred; } throw "no codec for " + photometricInterpretation; } function makeColorImage(imageId, dataSet, byteArray, photometricInterpretation, frame) { // extract the DICOM attributes we need var pixelSpacing = cornerstoneWADOImageLoader.getPixelSpacing(dataSet); var rows = dataSet.uint16('x00280010'); var columns = dataSet.uint16('x00280011'); var rescaleSlopeAndIntercept = cornerstoneWADOImageLoader.getRescaleSlopeAndIntercept(dataSet); var bytesPerPixel = 4; var numPixels = rows * columns; var sizeInBytes = numPixels * bytesPerPixel; var windowWidthAndCenter = cornerstoneWADOImageLoader.getWindowWidthAndCenter(dataSet); var deferred = $.Deferred(); // Decompress and decode the pixel data for this image var imageDataPromise = extractStoredPixels(dataSet, byteArray, photometricInterpretation, columns, rows, frame); imageDataPromise.then(function(imageData) { function getPixelData() { return imageData.data; } function getImageData() { return imageData; } function getCanvas() { if(lastImageIdDrawn === imageId) { return canvas; } canvas.height = rows; canvas.width = columns; var context = canvas.getContext('2d'); context.putImageData(imageData, 0, 0 ); lastImageIdDrawn = imageId; return canvas; } // Extract the various attributes we need var image = { imageId : imageId, minPixelValue : 0, maxPixelValue : 255, slope: rescaleSlopeAndIntercept.slope, intercept: rescaleSlopeAndIntercept.intercept, windowCenter : windowWidthAndCenter.windowCenter, windowWidth : windowWidthAndCenter.windowWidth, render: cornerstone.renderColorImage, getPixelData: getPixelData, getImageData: getImageData, getCanvas: getCanvas, rows: rows, columns: columns, height: rows, width: columns, color: true, columnPixelSpacing: pixelSpacing.column, rowPixelSpacing: pixelSpacing.row, data: dataSet, invert: false, sizeInBytes: sizeInBytes }; if(image.windowCenter === undefined) { image.windowWidth = 255; image.windowCenter = 128; } deferred.resolve(image); }, function() { deferred.reject(); }); return deferred; } // module exports cornerstoneWADOImageLoader.makeColorImage = makeColorImage; return cornerstoneWADOImageLoader; }($, cornerstone, cornerstoneWADOImageLoader)); var cornerstoneWADOImageLoader = (function ($, cornerstone, cornerstoneWADOImageLoader) { "use strict"; if(cornerstoneWADOImageLoader === undefined) { cornerstoneWADOImageLoader = {}; } function getPixelFormat(dataSet) { var pixelRepresentation = dataSet.uint16('x00280103'); var bitsAllocated = dataSet.uint16('x00280100'); if(pixelRepresentation === 0 && bitsAllocated === 8) { return 1; // unsigned 8 bit } else if(pixelRepresentation === 0 && bitsAllocated === 16) { return 2; // unsigned 16 bit } else if(pixelRepresentation === 1 && bitsAllocated === 16) { return 3; // signed 16 bit data } } function extractJPEG2000Pixels(dataSet, width, height, frame) { var compressedPixelData = dicomParser.readEncapsulatedPixelData(dataSet, dataSet.elements.x7fe00010, frame); var jpxImage = new JpxImage(); jpxImage.parse(compressedPixelData); var j2kWidth = jpxImage.width; var j2kHeight = jpxImage.height; if(j2kWidth !== width) { throw 'JPEG2000 decoder returned width of ' + j2kWidth + ', when ' + width + ' is expected'; } if(j2kHeight !== height) { throw 'JPEG2000 decoder returned width of ' + j2kHeight + ', when ' + height + ' is expected'; } var componentsCount = jpxImage.componentsCount; if(componentsCount !== 1) { throw 'JPEG2000 decoder returned a componentCount of ' + componentsCount + ', when 1 is expected'; } var tileCount = jpxImage.tiles.length; if(tileCount !== 1) { throw 'JPEG2000 decoder returned a tileCount of ' + tileCount + ', when 1 is expected'; } var tileComponents = jpxImage.tiles[0]; var pixelData = tileComponents.items; return pixelData; } function extractUncompressedPixels(dataSet, width, height, frame) { var pixelFormat = getPixelFormat(dataSet); var pixelDataElement = dataSet.elements.x7fe00010; var pixelDataOffset = pixelDataElement.dataOffset; var numPixels = width * height; // Note - we may want to sanity check the rows * columns * bitsAllocated * samplesPerPixel against the buffer size var frameOffset = 0; if(pixelFormat === 1) { frameOffset = pixelDataOffset + frame * numPixels; return new Uint8Array(dataSet.byteArray.buffer, frameOffset, numPixels); } else if(pixelFormat === 2) { frameOffset = pixelDataOffset + frame * numPixels * 2; return new Uint16Array(dataSet.byteArray.buffer, frameOffset, numPixels); } else if(pixelFormat === 3) { frameOffset = pixelDataOffset + frame * numPixels * 2; return new Int16Array(dataSet.byteArray.buffer, frameOffset, numPixels); } } function extractStoredPixels(dataSet, width, height, frame) { var transferSyntax = dataSet.string('x00020010'); if(transferSyntax === "1.2.840.10008.1.2.4.90" || // JPEG 2000 lossless transferSyntax === "1.2.840.10008.1.2.4.91") // JPEG 2000 lossy { return extractJPEG2000Pixels(dataSet, width, height, frame); } return extractUncompressedPixels(dataSet, width, height, frame); } function getBytesPerPixel(dataSet) { var pixelFormat = getPixelFormat(dataSet); if(pixelFormat ===1) { return 1; } else if(pixelFormat ===2 || pixelFormat ===3){ return 2; } throw "unknown pixel format"; } function getMinMax(storedPixelData) { // we always calculate the min max values since they are not always // present in DICOM and we don't want to trust them anyway as cornerstone // depends on us providing reliable values for these var min = 65535; var max = -32768; var numPixels = storedPixelData.length; var pixelData = storedPixelData; for(var index = 0; index < numPixels; index++) { var spv = pixelData[index]; // TODO: test to see if it is faster to use conditional here rather than calling min/max functions min = Math.min(min, spv); max = Math.max(max, spv); } return { min: min, max: max }; } function makeGrayscaleImage(imageId, dataSet, byteArray, photometricInterpretation, frame) { // extract the DICOM attributes we need var pixelSpacing = cornerstoneWADOImageLoader.getPixelSpacing(dataSet); var rows = dataSet.uint16('x00280010'); var columns = dataSet.uint16('x00280011'); var rescaleSlopeAndIntercept = cornerstoneWADOImageLoader.getRescaleSlopeAndIntercept(dataSet); var bytesPerPixel = getBytesPerPixel(dataSet); var numPixels = rows * columns; var sizeInBytes = numPixels * bytesPerPixel; var invert = (photometricInterpretation === "MONOCHROME1"); var windowWidthAndCenter = cornerstoneWADOImageLoader.getWindowWidthAndCenter(dataSet); // Decompress and decode the pixel data for this image var storedPixelData = extractStoredPixels(dataSet, columns, rows, frame); var minMax = getMinMax(storedPixelData); function getPixelData() { return storedPixelData; } // Extract the various attributes we need var image = { imageId : imageId, minPixelValue : minMax.min, maxPixelValue : minMax.max, slope: rescaleSlopeAndIntercept.slope, intercept: rescaleSlopeAndIntercept.intercept, windowCenter : windowWidthAndCenter.windowCenter, windowWidth : windowWidthAndCenter.windowWidth, render: cornerstone.renderGrayscaleImage, getPixelData: getPixelData, rows: rows, columns: columns, height: rows, width: columns, color: false, columnPixelSpacing: pixelSpacing.column, rowPixelSpacing: pixelSpacing.row, data: dataSet, invert: invert, sizeInBytes: sizeInBytes }; // TODO: deal with pixel padding and all of the various issues by setting it to min pixel value (or lower) // TODO: Mask out overlays embedded in pixel data above high bit if(image.windowCenter === undefined) { var maxVoi = image.maxPixelValue * image.slope + image.intercept; var minVoi = image.minPixelValue * image.slope + image.intercept; image.windowWidth = maxVoi - minVoi; image.windowCenter = (maxVoi + minVoi) / 2; } var deferred = $.Deferred(); deferred.resolve(image); return deferred; } // module exports cornerstoneWADOImageLoader.makeGrayscaleImage = makeGrayscaleImage; return cornerstoneWADOImageLoader; }($, cornerstone, cornerstoneWADOImageLoader));