diff --git a/Packages/ohif-cornerstone/client/cornerstone.js b/Packages/ohif-cornerstone/client/cornerstone.js index 21eed2b46..cc2278829 100644 --- a/Packages/ohif-cornerstone/client/cornerstone.js +++ b/Packages/ohif-cornerstone/client/cornerstone.js @@ -1,4 +1,4 @@ -/*! cornerstone - v0.10.2 - 2017-02-21 | (c) 2014 Chris Hafey | https://github.com/chafey/cornerstone */ +/*! cornerstone - v0.10.4 - 2017-04-05 | (c) 2014 Chris Hafey | https://github.com/chafey/cornerstone */ if(typeof cornerstone === 'undefined'){ cornerstone = { internal : {}, @@ -191,11 +191,29 @@ if(typeof cornerstone === 'undefined'){ "use strict"; - function enable(element) { + function enable(element, options) { if(element === undefined) { throw "enable: parameter element cannot be undefined"; } + // If this enabled element has the option set for WebGL, we should + // check if this device actually supports it + if (options && options.renderer && options.renderer.toLowerCase() === 'webgl') { + if (cornerstone.webGL.renderer.isWebGLAvailable()) { + // If WebGL is available on the device, initialize the renderer + // and return the renderCanvas from the WebGL rendering path + console.log('Using WebGL rendering path'); + + cornerstone.webGL.renderer.initRenderer(); + options.renderer = 'webgl'; + } else { + // If WebGL is not available on this device, we will fall back + // to using the Canvas renderer + console.error('WebGL not available, falling back to Canvas renderer'); + delete options.renderer; + } + } + var canvas = document.createElement('canvas'); element.appendChild(canvas); @@ -205,6 +223,7 @@ if(typeof cornerstone === 'undefined'){ image : undefined, // will be set once image is loaded invalid: false, // true if image needs to be drawn, false if not needsRedraw:true, + options: options, data : {} }; cornerstone.addEnabledElement(el); @@ -234,9 +253,9 @@ if(typeof cornerstone === 'undefined'){ renderTimeInMs: diff }; - $(el.element).trigger("CornerstoneImageRendered", eventData); el.invalid = false; el.needsRedraw = false; + $(el.element).trigger("CornerstoneImageRendered", eventData); } cornerstone.requestAnimationFrame(draw); @@ -250,6 +269,7 @@ if(typeof cornerstone === 'undefined'){ // module/private exports cornerstone.enable = enable; }(cornerstone)); + (function (cornerstone) { "use strict"; @@ -576,7 +596,7 @@ if(typeof cornerstone === 'undefined'){ removeImagePromise(imageId); - $(cornerstone).trigger('CornerstoneImageCachePromiseRemoved', {imageId: lastCachedImage.imageId}); + $(cornerstone).trigger('CornerstoneImageCachePromiseRemoved', {imageId: imageId}); } var cacheInfo = cornerstone.imageCache.getCacheInfo(); @@ -598,6 +618,7 @@ if(typeof cornerstone === 'undefined'){ var cachedImage = { loaded : false, imageId : imageId, + sharedCacheKey: undefined, // the sharedCacheKey for this imageId. undefined by default imagePromise : imagePromise, timeStamp : new Date(), sizeInBytes: 0 @@ -619,6 +640,7 @@ if(typeof cornerstone === 'undefined'){ cachedImage.sizeInBytes = image.sizeInBytes; cacheSizeInBytes += cachedImage.sizeInBytes; + cachedImage.sharedCacheKey = image.sharedCacheKey; purgeCacheIfNecessary(); }); @@ -665,6 +687,8 @@ if(typeof cornerstone === 'undefined'){ }; } + // This method should only be called by `removeImagePromise` because it's + // the one that knows how to deal with shared cache keys and cache size. function decache(imagePromise, imageId) { imagePromise.then(function(image) { if(image.decache) { @@ -1189,25 +1213,30 @@ if(typeof cornerstone === 'undefined'){ var storedPixelData = image.getPixelData(); var localLut = lut; var localCanvasImageDataData = canvasImageDataData; - // NOTE: As of Nov 2014, most javascript engines have lower performance when indexing negative indexes. - // We have a special code path for this case that improves performance. Thanks to @jpambrun for this enhancement - if(minPixelValue < 0){ - while(storedPixelDataIndex < numPixels) { - localCanvasImageDataData[canvasImageDataIndex++] = localLut[storedPixelData[storedPixelDataIndex++] + (-minPixelValue)]; // red - localCanvasImageDataData[canvasImageDataIndex++] = localLut[storedPixelData[storedPixelDataIndex++] + (-minPixelValue)]; // green - localCanvasImageDataData[canvasImageDataIndex] = localLut[storedPixelData[storedPixelDataIndex] + (-minPixelValue)]; // blue - storedPixelDataIndex+=2; - canvasImageDataIndex+=2; + + // Wrap this intensive loop in an IIFE to prevent de-optimization if + // the image Object has members added to it. + (function () { + // NOTE: As of Nov 2014, most javascript engines have lower performance when indexing negative indexes. + // We have a special code path for this case that improves performance. Thanks to @jpambrun for this enhancement + if(minPixelValue < 0){ + while(storedPixelDataIndex < numPixels) { + localCanvasImageDataData[canvasImageDataIndex++] = localLut[storedPixelData[storedPixelDataIndex++] + (-minPixelValue)]; // red + localCanvasImageDataData[canvasImageDataIndex++] = localLut[storedPixelData[storedPixelDataIndex++] + (-minPixelValue)]; // green + localCanvasImageDataData[canvasImageDataIndex] = localLut[storedPixelData[storedPixelDataIndex] + (-minPixelValue)]; // blue + storedPixelDataIndex+=2; + canvasImageDataIndex+=2; + } + }else{ + while(storedPixelDataIndex < numPixels) { + localCanvasImageDataData[canvasImageDataIndex++] = localLut[storedPixelData[storedPixelDataIndex++]]; // red + localCanvasImageDataData[canvasImageDataIndex++] = localLut[storedPixelData[storedPixelDataIndex++]]; // green + localCanvasImageDataData[canvasImageDataIndex] = localLut[storedPixelData[storedPixelDataIndex]]; // blue + storedPixelDataIndex+=2; + canvasImageDataIndex+=2; + } } - }else{ - while(storedPixelDataIndex < numPixels) { - localCanvasImageDataData[canvasImageDataIndex++] = localLut[storedPixelData[storedPixelDataIndex++]]; // red - localCanvasImageDataData[canvasImageDataIndex++] = localLut[storedPixelData[storedPixelDataIndex++]]; // green - localCanvasImageDataData[canvasImageDataIndex] = localLut[storedPixelData[storedPixelDataIndex]]; // blue - storedPixelDataIndex+=2; - canvasImageDataIndex+=2; - } - } + })(); } // Module exports @@ -1247,19 +1276,22 @@ if(typeof cornerstone === 'undefined'){ var localPixelData = pixelData; var localLut = lut; var localCanvasImageDataData = canvasImageDataData; - // NOTE: As of Nov 2014, most javascript engines have lower performance when indexing negative indexes. - // We have a special code path for this case that improves performance. Thanks to @jpambrun for this enhancement - if(minPixelValue < 0){ - while(storedPixelDataIndex < localNumPixels) { - localCanvasImageDataData[canvasImageDataIndex] = localLut[localPixelData[storedPixelDataIndex++] + (-minPixelValue)]; // alpha - canvasImageDataIndex += 4; + + (function () { + // NOTE: As of Nov 2014, most javascript engines have lower performance when indexing negative indexes. + // We have a special code path for this case that improves performance. Thanks to @jpambrun for this enhancement + if(minPixelValue < 0){ + while(storedPixelDataIndex < localNumPixels) { + localCanvasImageDataData[canvasImageDataIndex] = localLut[localPixelData[storedPixelDataIndex++] + (-minPixelValue)]; // alpha + canvasImageDataIndex += 4; + } + }else{ + while(storedPixelDataIndex < localNumPixels) { + localCanvasImageDataData[canvasImageDataIndex] = localLut[localPixelData[storedPixelDataIndex++]]; // alpha + canvasImageDataIndex += 4; + } } - }else{ - while(storedPixelDataIndex < localNumPixels) { - localCanvasImageDataData[canvasImageDataIndex] = localLut[localPixelData[storedPixelDataIndex++]]; // alpha - canvasImageDataIndex += 4; - } - } + })(); } // Module exports @@ -1444,6 +1476,7 @@ if(typeof cornerstone === 'undefined'){ function invalidate(element) { var enabledElement = cornerstone.getEnabledElement(element); enabledElement.invalid = true; + enabledElement.needsRedraw = true; var eventData = { element: element }; @@ -1748,7 +1781,17 @@ if(typeof cornerstone === 'undefined'){ context.save(); cornerstone.setToPixelCoordinateSystem(enabledElement, context); - var renderCanvas = getRenderCanvas(enabledElement, image, invalidated); + var renderCanvas; + if (enabledElement.options && enabledElement.options.renderer && + enabledElement.options.renderer.toLowerCase() === 'webgl') { + // If this enabled element has the option set for WebGL, we should + // user it as our renderer. + renderCanvas = cornerstone.webGL.renderer.render(enabledElement); + } else { + // If no options are set we will retrieve the renderCanvas through the + // normal Canvas rendering path + renderCanvas = getRenderCanvas(enabledElement, image, invalidated); + } context.drawImage(renderCanvas, 0,0, image.width, image.height, 0, 0, image.width, image.height); @@ -1769,7 +1812,7 @@ if(typeof cornerstone === 'undefined'){ }(cornerstone)); /** - * This module is responsible for drawing a grayscale imageß + * This module is responsible for drawing a grayscale image */ (function (cornerstone) { @@ -1881,12 +1924,12 @@ if(typeof cornerstone === 'undefined'){ * @param invalidated - true if pixel data has been invaldiated and cached rendering should not be used */ function renderGrayscaleImage(enabledElement, invalidated) { - - if(enabledElement === undefined) { + if (enabledElement === undefined) { throw "drawImage: enabledElement parameter must not be undefined"; } + var image = enabledElement.image; - if(image === undefined) { + if (image === undefined) { throw "drawImage: image must be loaded before it can be drawn"; } @@ -1908,10 +1951,20 @@ if(typeof cornerstone === 'undefined'){ context.mozImageSmoothingEnabled = true; } - // save the canvas context state and apply the viewport properties + // Save the canvas context state and apply the viewport properties cornerstone.setToPixelCoordinateSystem(enabledElement, context); - var renderCanvas = getRenderCanvas(enabledElement, image, invalidated); + var renderCanvas; + if (enabledElement.options && enabledElement.options.renderer && + enabledElement.options.renderer.toLowerCase() === 'webgl') { + // If this enabled element has the option set for WebGL, we should + // user it as our renderer. + renderCanvas = cornerstone.webGL.renderer.render(enabledElement); + } else { + // If no options are set we will retrieve the renderCanvas through the + // normal Canvas rendering path + renderCanvas = getRenderCanvas(enabledElement, image, invalidated); + } // Draw the render canvas half the image size (because we set origin to the middle of the canvas above) context.drawImage(renderCanvas, 0,0, image.width, image.height, 0, 0, image.width, image.height); @@ -2065,6 +2118,12 @@ if(typeof cornerstone === 'undefined'){ setCanvasSize(element, enabledElement.canvas); + var eventData = { + element: element + }; + + $(element).trigger("CornerstoneElementResized", eventData); + if(enabledElement.image === undefined ) { return; } @@ -2081,6 +2140,7 @@ if(typeof cornerstone === 'undefined'){ cornerstone.resize = resize; }(cornerstone)); + /** * This module contains a function that will set the canvas context to the pixel coordinates system * making it easy to draw geometry on the image @@ -2192,4 +2252,977 @@ if(typeof cornerstone === 'undefined'){ // module exports cornerstone.updateImage = updateImage; +}(cornerstone)); +(function (cornerstone) { + + "use strict"; + + if (!cornerstone.webGL) { + cornerstone.webGL = {}; + } + + /** + * Creates and compiles a shader. + * + * @param {!WebGLRenderingContext} gl The WebGL Context. + * @param {string} shaderSource The GLSL source code for the shader. + * @param {number} shaderType The type of shader, VERTEX_SHADER or FRAGMENT_SHADER. + * + * @return {!WebGLShader} The shader. + */ + function compileShader(gl, shaderSource, shaderType) { + + // Create the shader object + var shader = gl.createShader(shaderType); + + // Set the shader source code. + gl.shaderSource(shader, shaderSource); + + // Compile the shader + gl.compileShader(shader); + + // Check if it compiled + var success = gl.getShaderParameter(shader, gl.COMPILE_STATUS); + if (!success && !gl.isContextLost()) { + // Something went wrong during compilation; get the error + var infoLog = gl.getShaderInfoLog(shader); + console.error("Could not compile shader:\n" + infoLog); + } + + return shader; + } + + /** + * Creates a program from 2 shaders. + * + * @param {!WebGLRenderingContext) gl The WebGL context. + * @param {!WebGLShader} vertexShader A vertex shader. + * @param {!WebGLShader} fragmentShader A fragment shader. + * @return {!WebGLProgram} A program. + */ + function createProgram(gl, vertexShader, fragmentShader) { + + // create a program. + var program = gl.createProgram(); + + // attach the shaders. + gl.attachShader(program, vertexShader); + gl.attachShader(program, fragmentShader); + + // link the program. + gl.linkProgram(program); + + // Check if it linked. + var success = gl.getProgramParameter(program, gl.LINK_STATUS); + if (!success && !gl.isContextLost()) { + // something went wrong with the link + var infoLog = gl.getProgramInfoLog(program); + console.error("WebGL program filed to link:\n" + infoLog); + } + + return program; + } + + /** + * Creates a program from 2 shaders source (Strings) + * @param {!WebGLRenderingContext} gl The WebGL context. + * @param {!WebGLShader} vertexShaderSrc Vertex shader string + * @param {!WebGLShader} fragShaderSrc Fragment shader string + * @return {!WebGLProgram} A program + */ + function createProgramFromString(gl, vertexShaderSrc, fragShaderSrc) { + var vertexShader = compileShader(gl, vertexShaderSrc, gl.VERTEX_SHADER); + var fragShader = compileShader(gl, fragShaderSrc, gl.FRAGMENT_SHADER); + return createProgram(gl, vertexShader, fragShader); + } + + cornerstone.webGL.createProgramFromString = createProgramFromString; + +}(cornerstone)); + +(function (cornerstone) { + + "use strict"; + + if (!cornerstone.webGL) { + cornerstone.webGL = {}; + } + + var renderCanvas = document.createElement('canvas'); + var renderCanvasContext; + var renderCanvasData; + var gl; + var programs; + var shader; + var texCoordBuffer, positionBuffer; + cornerstone.webGL.isWebGLInitialized = false; + + function getRenderCanvas() { + return renderCanvas; + } + + function initShaders() { + for (var id in cornerstone.webGL.shaders) { + //console.log("WEBGL: Loading shader", id); + var shader = cornerstone.webGL.shaders[ id ]; + shader.attributes = {}; + shader.uniforms = {}; + shader.vert = cornerstone.webGL.vertexShader; + + shader.program = cornerstone.webGL.createProgramFromString(gl, shader.vert, shader.frag); + + shader.attributes.texCoordLocation = gl.getAttribLocation(shader.program, "a_texCoord"); + gl.enableVertexAttribArray(shader.attributes.texCoordLocation); + + shader.attributes.positionLocation = gl.getAttribLocation(shader.program, "a_position"); + gl.enableVertexAttribArray(shader.attributes.positionLocation); + + shader.uniforms.resolutionLocation = gl.getUniformLocation(shader.program, "u_resolution"); + } + } + + function initRenderer() { + if (cornerstone.webGL.isWebGLInitialized === true) { + //console.log("WEBGL Renderer already initialized"); + return; + } + + if (initWebGL(renderCanvas)) { + initBuffers(); + initShaders(); + //console.log("WEBGL Renderer initialized!"); + cornerstone.webGL.isWebGLInitialized = true; + } + } + + function updateRectangle(gl, width, height) { + gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([ + width, height, + 0, height, + width, 0, + 0, 0]), gl.STATIC_DRAW); + } + + function handleLostContext(event) { + event.preventDefault(); + console.warn('WebGL Context Lost!'); + } + + function handleRestoredContext(event) { + event.preventDefault(); + cornerstone.webGL.isWebGLInitialized = false; + cornerstone.webGL.textureCache.purgeCache(); + initRenderer(); + //console.log('WebGL Context Restored.'); + } + + function initWebGL(canvas) { + + gl = null; + try { + // Try to grab the standard context. If it fails, fallback to experimental. + var options = { + preserveDrawingBuffer: true, // preserve buffer so we can copy to display canvas element + }; + + // ---------------- Testing purposes ------------- + if (cornerstone.webGL.debug === true && WebGLDebugUtils) { + renderCanvas = WebGLDebugUtils.makeLostContextSimulatingCanvas(renderCanvas); + } + // ---------------- Testing purposes ------------- + + gl = canvas.getContext("webgl", options) || canvas.getContext("experimental-webgl", options); + + // Set up event listeners for context lost / context restored + canvas.removeEventListener("webglcontextlost", handleLostContext, false); + canvas.addEventListener("webglcontextlost", handleLostContext, false); + + canvas.removeEventListener("webglcontextrestored", handleRestoredContext, false); + canvas.addEventListener("webglcontextrestored", handleRestoredContext, false); + + } catch(error) { + throw "Error creating WebGL context"; + } + + // If we don't have a GL context, give up now + if (!gl) { + console.error("Unable to initialize WebGL. Your browser may not support it."); + gl = null; + } + return gl; + } + + function getImageDataType(image) { + if (image.color) { + return 'rgb'; + } + + var datatype = 'int'; + if (image.minPixelValue >= 0) { + datatype = 'u' + datatype; + } + + if (image.maxPixelValue > 255) { + datatype += '16'; + } else { + datatype += '8'; + } + return datatype; + } + + function getShaderProgram(image) { + + var datatype = getImageDataType(image); + // We need a mechanism for + // choosing the shader based on the image datatype + // console.log("Datatype: " + datatype); + if (cornerstone.webGL.shaders.hasOwnProperty(datatype)) { + return cornerstone.webGL.shaders[datatype]; + } + + var shader = cornerstone.webGL.shaders.rgb; + return shader; + } + + function getImageTexture( image ) { + var imageTexture = cornerstone.webGL.textureCache.getImageTexture(image.imageId); + if (!imageTexture) { + //console.log("Generating texture for imageid: ", image.imageId); + imageTexture = generateTexture(image); + cornerstone.webGL.textureCache.putImageTexture(image, imageTexture); + } + return imageTexture.texture; + + } + + function generateTexture( image ) { + var TEXTURE_FORMAT = { + uint8: gl.LUMINANCE, + int8: gl.LUMINANCE_ALPHA, + uint16: gl.LUMINANCE_ALPHA, + int16: gl.RGB, + rgb: gl.RGB + }; + + var TEXTURE_BYTES = { + int8: 1, // Luminance + uint16: 2, // Luminance + Alpha + int16: 3, // RGB + rgb: 3 // RGB + }; + + var imageDataType = getImageDataType(image); + var format = TEXTURE_FORMAT[imageDataType]; + + // GL texture configuration + var texture = gl.createTexture(); + gl.bindTexture(gl.TEXTURE_2D, texture); + + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); + gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1); + + var imageData = cornerstone.webGL.dataUtilities[imageDataType].storedPixelDataToImageData(image, image.width, image.height); + + gl.texImage2D(gl.TEXTURE_2D, 0, format, image.width, image.height, 0, format, gl.UNSIGNED_BYTE, imageData); + + // Calculate the size in bytes of this image in memory + var sizeInBytes = image.width * image.height * TEXTURE_BYTES[imageDataType]; + var imageTexture = { + texture: texture, + sizeInBytes: sizeInBytes + }; + return imageTexture; + + } + + function initBuffers() { + positionBuffer = gl.createBuffer(); + gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer); + gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([ + 1, 1, + 0, 1, + 1, 0, + 0, 0 + ]), gl.STATIC_DRAW); + + + texCoordBuffer = gl.createBuffer(); + gl.bindBuffer(gl.ARRAY_BUFFER, texCoordBuffer); + gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([ + 1.0, 1.0, + 0.0, 1.0, + 1.0, 0.0, + 0.0, 0.0, + ]), gl.STATIC_DRAW); + } + + function renderQuad(shader, parameters, texture, width, height) { + gl.clearColor(1.0,0.0,0.0,1.0); + gl.viewport( 0, 0, width, height ); + + gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); + gl.useProgram(shader.program); + + gl.bindBuffer(gl.ARRAY_BUFFER, texCoordBuffer); + gl.vertexAttribPointer(shader.attributes.texCoordLocation, 2, gl.FLOAT, false, 0, 0); + + gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer); + gl.vertexAttribPointer(shader.attributes.positionLocation, 2, gl.FLOAT, false, 0, 0); + + for (var key in parameters) { + var uniformLocation = gl.getUniformLocation(shader.program, key); + if ( !uniformLocation ) { + throw "Could not access location for uniform: " + key; + } + + var uniform = parameters[key]; + + var type = uniform.type; + var value = uniform.value; + + if( type == "i" ) { + gl.uniform1i( uniformLocation, value ); + } else if( type == "f" ) { + gl.uniform1f( uniformLocation, value ); + } else if( type == "2f" ) { + gl.uniform2f( uniformLocation, value[0], value[1] ); + } + } + + updateRectangle(gl, width, height); + + gl.activeTexture(gl.TEXTURE0); + gl.bindTexture(gl.TEXTURE_2D, texture); + gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); + + } + + function render(enabledElement) { + // Resize the canvas + var image = enabledElement.image; + renderCanvas.width = image.width; + renderCanvas.height = image.height; + + var viewport = enabledElement.viewport; + + // Render the current image + var shader = getShaderProgram(image); + var texture = getImageTexture(image); + var parameters = { + "u_resolution": { type: "2f", value: [image.width, image.height] }, + "wc": { type: "f", value: viewport.voi.windowCenter }, + "ww": { type: "f", value: viewport.voi.windowWidth }, + "slope": { type: "f", value: image.slope }, + "intercept": { type: "f", value: image.intercept }, + //"minPixelValue": { type: "f", value: image.minPixelValue }, + "invert": { type: "i", value: viewport.invert ? 1 : 0 }, + }; + renderQuad(shader, parameters, texture, image.width, image.height ); + + return renderCanvas; + } + + function isWebGLAvailable() { + // Adapted from + // http://stackoverflow.com/questions/9899807/three-js-detect-webgl-support-and-fallback-to-regular-canvas + + var options = { + failIfMajorPerformanceCaveat: true + }; + + try { + var canvas = document.createElement("canvas"); + return !! + window.WebGLRenderingContext && + (canvas.getContext("webgl", options) || canvas.getContext("experimental-webgl", options)); + } catch(e) { + return false; + } + } + + cornerstone.webGL.renderer = { + render: render, + initRenderer: initRenderer, + getRenderCanvas: getRenderCanvas, + isWebGLAvailable: isWebGLAvailable + }; + +}(cornerstone)); + + +(function (cornerstone) { + + "use strict"; + + if (!cornerstone.webGL) { + cornerstone.webGL = {}; + } + + if (!cornerstone.webGL.shaders) { + cornerstone.webGL.shaders = {}; + } + + if (!cornerstone.webGL.dataUtilities) { + cornerstone.webGL.dataUtilities = {}; + } + + // Pack int16 into three uint8 channels (r, g, b) + var shader = {}; + + function storedPixelDataToImageData(image) { + + // Transfer image data to alpha and luminance channels of WebGL texture + // Credit to @jpambrun and @fernandojsg + + // Pack int16 into three uint8 channels (r, g, b) + var pixelData = image.getPixelData(); + var numberOfChannels = 3; + var data = new Uint8Array(image.width * image.height * numberOfChannels); + var offset = 0; + + for (var i = 0; i < pixelData.length; i++) { + var val = Math.abs(pixelData[i]); + data[offset++] = parseInt(val & 0xFF, 10); + data[offset++] = parseInt(val >> 8, 10); + data[offset++] = pixelData[i] < 0 ? 0: 1; // 0 For negative, 1 for positive + } + return data; + } + + cornerstone.webGL.dataUtilities.int16 = { + storedPixelDataToImageData: storedPixelDataToImageData + }; + + shader.frag = 'precision mediump float;' + + 'uniform sampler2D u_image;' + + 'uniform float ww;' + + 'uniform float wc;' + + 'uniform float slope;' + + 'uniform float intercept;' + + 'uniform int invert;' + + 'varying vec2 v_texCoord;' + + + 'void main() {' + + // Get texture + 'vec4 color = texture2D(u_image, v_texCoord);' + + + // Calculate luminance from packed texture + 'float intensity = color.r*256.0 + color.g*65536.0;'+ + + 'if (color.b == 0.0)' + + 'intensity = -intensity;' + + + // Rescale based on slope and window settings + 'intensity = intensity * slope + intercept;'+ + 'float center0 = wc - 0.5;'+ + 'float width0 = max(ww, 1.0);' + + 'intensity = (intensity - center0) / width0 + 0.5;'+ + + // Clamp intensity + 'intensity = clamp(intensity, 0.0, 1.0);' + + + // RGBA output + 'gl_FragColor = vec4(intensity, intensity, intensity, 1.0);' + + + // Apply any inversion necessary + 'if (invert == 1)' + + 'gl_FragColor.rgb = 1.0 - gl_FragColor.rgb;' + + '}'; + + cornerstone.webGL.shaders.int16 = shader; + +}(cornerstone)); +(function (cornerstone) { + + "use strict"; + + if (!cornerstone.webGL) { + cornerstone.webGL = {}; + } + + if (!cornerstone.webGL.shaders) { + cornerstone.webGL.shaders = {}; + } + + if (!cornerstone.webGL.dataUtilities) { + cornerstone.webGL.dataUtilities = {}; + } + + var shader = {}; + + function storedPixelDataToImageData(image) { + // Transfer image data to alpha channel of WebGL texture + // Store data in Uint8Array + var pixelData = image.getPixelData(); + var numberOfChannels = 2; + var data = new Uint8Array(image.width * image.height * numberOfChannels); + var offset = 0; + + for (var i = 0; i < pixelData.length; i++) { + data[offset++] = parseInt(pixelData[i], 10); + data[offset++] = pixelData[i] < 0 ? 0: 1; // 0 For negative, 1 for positive + } + return data; + } + + cornerstone.webGL.dataUtilities.int8 = { + storedPixelDataToImageData: storedPixelDataToImageData + }; + + shader.frag = 'precision mediump float;' + + 'uniform sampler2D u_image;' + + 'uniform float ww;' + + 'uniform float wc;' + + 'uniform float slope;' + + 'uniform float intercept;' + + 'uniform float minPixelValue;' + + 'uniform int invert;' + + 'varying vec2 v_texCoord;' + + + 'void main() {' + + // Get texture + 'vec4 color = texture2D(u_image, v_texCoord);' + + + // Calculate luminance from packed texture + 'float intensity = color.r*256.;'+ + + 'if (color.a == 0.0)' + + 'intensity = -intensity;' + + + // Rescale based on slope and window settings + 'intensity = intensity * slope + intercept;'+ + 'float center0 = wc - 0.5;'+ + 'float width0 = max(ww, 1.0);' + + 'intensity = (intensity - center0) / width0 + 0.5;'+ + + // Clamp intensity + 'intensity = clamp(intensity, 0.0, 1.0);' + + + // RGBA output + 'gl_FragColor = vec4(intensity, intensity, intensity, 1.0);' + + + // Apply any inversion necessary + 'if (invert == 1)' + + 'gl_FragColor.rgb = 1.0 - gl_FragColor.rgb;' + + '}'; + + cornerstone.webGL.shaders.int8 = shader; + + +}(cornerstone)); +(function (cornerstone) { + + "use strict"; + + if (!cornerstone.webGL) { + cornerstone.webGL = {}; + } + + if (!cornerstone.webGL.shaders) { + cornerstone.webGL.shaders = {}; + } + + if (!cornerstone.webGL.dataUtilities) { + cornerstone.webGL.dataUtilities = {}; + } + + // Pack RGB images into a 3-channel RGB texture + var shader = {}; + + function storedPixelDataToImageData(image) { + var minPixelValue = image.minPixelValue; + var canvasImageDataIndex = 0; + var storedPixelDataIndex = 0; + // Only 3 channels, since we use WebGL's RGB texture format + var numStoredPixels = image.width * image.height * 4; + var numOutputPixels = image.width * image.height * 3; + var storedPixelData = image.getPixelData(); + var data = new Uint8Array(numOutputPixels); + + // NOTE: As of Nov 2014, most javascript engines have lower performance when indexing negative indexes. + // We have a special code path for this case that improves performance. Thanks to @jpambrun for this enhancement + if (minPixelValue < 0){ + while (storedPixelDataIndex < numStoredPixels) { + data[canvasImageDataIndex++] = storedPixelData[storedPixelDataIndex++] + (-minPixelValue); // red + data[canvasImageDataIndex++] = storedPixelData[storedPixelDataIndex++] + (-minPixelValue); // green + data[canvasImageDataIndex++] = storedPixelData[storedPixelDataIndex++] + (-minPixelValue); // blue + storedPixelDataIndex += 1; // The stored pixel data has 4 channels + } + } else { + while (storedPixelDataIndex < numStoredPixels) { + data[canvasImageDataIndex++] = storedPixelData[storedPixelDataIndex++]; // red + data[canvasImageDataIndex++] = storedPixelData[storedPixelDataIndex++]; // green + data[canvasImageDataIndex++] = storedPixelData[storedPixelDataIndex++]; // blue + storedPixelDataIndex += 1; // The stored pixel data has 4 channels + } + } + return data; + } + + cornerstone.webGL.dataUtilities.rgb = { + storedPixelDataToImageData: storedPixelDataToImageData + }; + + shader.frag = 'precision mediump float;' + + 'uniform sampler2D u_image;' + + 'uniform float ww;' + + 'uniform float wc;' + + 'uniform float slope;' + + 'uniform float intercept;' + + 'uniform float minPixelValue;' + + 'uniform int invert;' + + 'varying vec2 v_texCoord;' + + + 'void main() {' + + + // Get texture + 'vec3 color = texture2D(u_image, v_texCoord).xyz;' + + + // Rescale based on slope and intercept + 'color = color * 256.0 * slope + intercept;' + + + // Apply window settings + 'float center0 = wc - 0.5 - minPixelValue;'+ + 'float width0 = max(ww, 1.0);' + + 'color = (color - center0) / width0 + 0.5;'+ + + // RGBA output + 'gl_FragColor = vec4(color, 1);' + + + // Apply any inversion necessary + 'if (invert == 1)' + + 'gl_FragColor.rgb = 1. - gl_FragColor.rgb;' + + '}'; + + cornerstone.webGL.shaders.rgb = shader; + +}(cornerstone)); +(function (cornerstone) { + + "use strict"; + + if (!cornerstone.webGL) { + cornerstone.webGL = {}; + } + + if (!cornerstone.webGL.shaders) { + cornerstone.webGL.shaders = {}; + } + + if (!cornerstone.webGL.dataUtilities) { + cornerstone.webGL.dataUtilities = {}; + } + + // For uint16 pack uint16 into two uint8 channels (r and a) + var shader = {}; + + function storedPixelDataToImageData(image) { + + // Transfer image data to alpha and luminance channels of WebGL texture + // Credit to @jpambrun and @fernandojsg + + // Pack uint16 into two uint8 channels (r and a) + var pixelData = image.getPixelData(); + var numberOfChannels = 2; + var data = new Uint8Array(image.width * image.height * numberOfChannels); + var offset = 0; + + for (var i = 0; i < pixelData.length; i++) { + var val = pixelData[i]; + data[offset++] = parseInt(val & 0xFF, 10); + data[offset++] = parseInt(val >> 8, 10); + } + return data; + } + + cornerstone.webGL.dataUtilities.uint16 = { + storedPixelDataToImageData: storedPixelDataToImageData + }; + + shader.frag = 'precision mediump float;' + + 'uniform sampler2D u_image;' + + 'uniform float ww;' + + 'uniform float wc;' + + 'uniform float slope;' + + 'uniform float intercept;' + + 'uniform int invert;' + + 'varying vec2 v_texCoord;' + + + 'void main() {' + + // Get texture + 'vec4 color = texture2D(u_image, v_texCoord);' + + + // Calculate luminance from packed texture + 'float intensity = color.r*256.0 + color.a*65536.0;'+ + + // Rescale based on slope and window settings + 'intensity = intensity * slope + intercept;'+ + 'float center0 = wc - 0.5;'+ + 'float width0 = max(ww, 1.0);' + + 'intensity = (intensity - center0) / width0 + 0.5;'+ + + // Clamp intensity + 'intensity = clamp(intensity, 0.0, 1.0);' + + + // RGBA output + 'gl_FragColor = vec4(intensity, intensity, intensity, 1.0);' + + + // Apply any inversion necessary + 'if (invert == 1)' + + 'gl_FragColor.rgb = 1.0 - gl_FragColor.rgb;' + + '}'; + + cornerstone.webGL.shaders.uint16 = shader; + +}(cornerstone)); +(function (cornerstone) { + + "use strict"; + + if (!cornerstone.webGL) { + cornerstone.webGL = {}; + } + + if (!cornerstone.webGL.shaders) { + cornerstone.webGL.shaders = {}; + } + + if (!cornerstone.webGL.dataUtilities) { + cornerstone.webGL.dataUtilities = {}; + } + + var shader = {}; + + function storedPixelDataToImageData(image) { + // Transfer image data to alpha channel of WebGL texture + // Store data in Uuint8Array + var pixelData = image.getPixelData(); + var data = new Uint8Array(pixelData.length); + for (var i = 0; i < pixelData.length; i++) { + data[i] = parseInt(pixelData[i], 10); + } + return data; + } + + cornerstone.webGL.dataUtilities.uint8 = { + storedPixelDataToImageData: storedPixelDataToImageData + }; + + shader.frag = 'precision mediump float;' + + 'uniform sampler2D u_image;' + + 'uniform float ww;' + + 'uniform float wc;' + + 'uniform float slope;' + + 'uniform float intercept;' + + //'uniform float minPixelValue;' + + 'uniform int invert;' + + 'varying vec2 v_texCoord;' + + + 'void main() {' + + // Get texture + 'vec4 color = texture2D(u_image, v_texCoord);' + + + // Calculate luminance from packed texture + 'float intensity = color.r*256.0;'+ + + // Rescale based on slope and window settings + 'intensity = intensity * slope + intercept;'+ + 'float center0 = wc - 0.5;'+ + 'float width0 = max(ww, 1.0);' + + 'intensity = (intensity - center0) / width0 + 0.5;'+ + + // Clamp intensity + 'intensity = clamp(intensity, 0.0, 1.0);' + + + // RGBA output + 'gl_FragColor = vec4(intensity, intensity, intensity, 1.0);' + + + // Apply any inversion necessary + 'if (invert == 1)' + + 'gl_FragColor.rgb = 1.0 - gl_FragColor.rgb;' + + '}'; + + cornerstone.webGL.shaders.uint8 = shader; + + +}(cornerstone)); +/** + * This module deals with caching image textures in VRAM for WebGL + */ + +(function (cornerstone) { + + "use strict"; + + var imageCache = {}; + + var cachedImages = []; + + var maximumSizeInBytes = 1024 * 1024 * 256; // 256 MB + var cacheSizeInBytes = 0; + + function setMaximumSizeBytes(numBytes) { + if (numBytes === undefined) { + throw "setMaximumSizeBytes: parameter numBytes must not be undefined"; + } + if (numBytes.toFixed === undefined) { + throw "setMaximumSizeBytes: parameter numBytes must be a number"; + } + + maximumSizeInBytes = numBytes; + purgeCacheIfNecessary(); + } + + function purgeCacheIfNecessary() { + // if max cache size has not been exceeded, do nothing + if (cacheSizeInBytes <= maximumSizeInBytes) { + return; + } + + // cache size has been exceeded, create list of images sorted by timeStamp + // so we can purge the least recently used image + function compare(a,b) { + if (a.timeStamp > b.timeStamp) { + return -1; + } + if (a.timeStamp < b.timeStamp) { + return 1; + } + return 0; + } + cachedImages.sort(compare); + + // remove images as necessary + while(cacheSizeInBytes > maximumSizeInBytes) { + var lastCachedImage = cachedImages[cachedImages.length - 1]; + cacheSizeInBytes -= lastCachedImage.sizeInBytes; + delete imageCache[lastCachedImage.imageId]; + cachedImages.pop(); + $(cornerstone).trigger('CornerstoneWebGLTextureRemoved', {imageId: lastCachedImage.imageId}); + } + + var cacheInfo = cornerstone.imageCache.getCacheInfo(); + console.log('CornerstoneWebGLTextureCacheFull'); + $(cornerstone).trigger('CornerstoneWebGLTextureCacheFull', cacheInfo); + } + + function putImageTexture(image, imageTexture) { + var imageId = image.imageId; + if (image === undefined) { + throw "putImageTexture: image must not be undefined"; + } + + if (imageId === undefined) { + throw "putImageTexture: imageId must not be undefined"; + } + + if (imageTexture === undefined) { + throw "putImageTexture: imageTexture must not be undefined"; + } + + if (imageCache.hasOwnProperty(imageId) === true) { + throw "putImageTexture: imageId already in cache"; + } + + var cachedImage = { + imageId : imageId, + imageTexture : imageTexture, + timeStamp : new Date(), + sizeInBytes: imageTexture.sizeInBytes + }; + + imageCache[imageId] = cachedImage; + cachedImages.push(cachedImage); + + if (imageTexture.sizeInBytes === undefined) { + throw "putImageTexture: imageTexture does not have sizeInBytes property or"; + } + if (imageTexture.sizeInBytes.toFixed === undefined) { + throw "putImageTexture: imageTexture.sizeInBytes is not a number"; + } + cacheSizeInBytes += cachedImage.sizeInBytes; + purgeCacheIfNecessary(); + } + + function getImageTexture(imageId) { + if (imageId === undefined) { + throw "getImageTexture: imageId must not be undefined"; + } + var cachedImage = imageCache[imageId]; + if (cachedImage === undefined) { + return undefined; + } + + // bump time stamp for cached image + cachedImage.timeStamp = new Date(); + return cachedImage.imageTexture; + } + + function removeImageTexture(imageId) { + if (imageId === undefined) { + throw "removeImageTexture: imageId must not be undefined"; + } + var cachedImage = imageCache[imageId]; + if (cachedImage === undefined) { + throw "removeImageTexture: imageId must not be undefined"; + } + cachedImages.splice( cachedImages.indexOf(cachedImage), 1); + cacheSizeInBytes -= cachedImage.sizeInBytes; + delete imageCache[imageId]; + + return cachedImage.imageTexture; + } + + function getCacheInfo() { + return { + maximumSizeInBytes : maximumSizeInBytes, + cacheSizeInBytes : cacheSizeInBytes, + numberOfImagesCached: cachedImages.length + }; + } + + function purgeCache() { + while (cachedImages.length > 0) { + var removedCachedImage = cachedImages.pop(); + delete imageCache[removedCachedImage.imageId]; + } + cacheSizeInBytes = 0; + } + + // module exports + cornerstone.webGL.textureCache = { + putImageTexture : putImageTexture, + getImageTexture: getImageTexture, + removeImageTexture: removeImageTexture, + setMaximumSizeBytes: setMaximumSizeBytes, + getCacheInfo : getCacheInfo, + purgeCache: purgeCache, + cachedImages: cachedImages + }; + +}(cornerstone)); + +(function (cornerstone) { + + "use strict"; + + if (!cornerstone.webGL) { + cornerstone.webGL = {}; + } + + cornerstone.webGL.vertexShader = 'attribute vec2 a_position;' + + 'attribute vec2 a_texCoord;' + + 'uniform vec2 u_resolution;' + + 'varying vec2 v_texCoord;' + + 'void main() {' + + 'vec2 zeroToOne = a_position / u_resolution;' + + 'vec2 zeroToTwo = zeroToOne * 2.0;' + + 'vec2 clipSpace = zeroToTwo - 1.0;' + + 'gl_Position = vec4(clipSpace * vec2(1, -1), 0, 1);' + + 'v_texCoord = a_texCoord;' + + '}'; + }(cornerstone)); \ No newline at end of file diff --git a/Packages/ohif-cornerstone/client/cornerstoneTools.js b/Packages/ohif-cornerstone/client/cornerstoneTools.js index 0daa23d7a..0d25ee275 100644 --- a/Packages/ohif-cornerstone/client/cornerstoneTools.js +++ b/Packages/ohif-cornerstone/client/cornerstoneTools.js @@ -1,9 +1,13 @@ -/*! cornerstoneTools - v0.8.3 - 2017-02-21 | (c) 2014 Chris Hafey | https://github.com/chafey/cornerstoneTools */ +/*! cornerstoneTools - v0.8.4 - 2017-04-05 | (c) 2014 Chris Hafey | https://github.com/chafey/cornerstoneTools */ // Begin Source: src/header.js if (typeof cornerstone === 'undefined') { cornerstone = {}; } +if (typeof cornerstoneMath === 'undefined') { + cornerstoneMath = {}; +} + if (typeof dicomParser === 'undefined') { dicomParser = {}; } @@ -36,6 +40,8 @@ if (typeof cornerstoneTools === 'undefined') { return; } + e.preventDefault(); + var element = e.currentTarget; var x; @@ -114,7 +120,7 @@ if (typeof cornerstoneTools === 'undefined') { 'use strict'; - var isClickEvent; + var isClickEvent = true; var preventClickTimeout; var clickDelay = 200; @@ -410,11 +416,11 @@ if (typeof cornerstoneTools === 'undefined') { isPress = false, pressMaxDistance = 5, pageDistanceMoved, - preventNextPinch = false; + preventNextPinch = false, + lastDelta; function onTouch(e) { - console.log(e.type); - var element = e.target.parentNode, + var element = e.currentTarget || e.srcEvent.currentTarget, event, eventType; @@ -659,13 +665,30 @@ if (typeof cornerstoneTools === 'undefined') { break; case 'panmove': + // using the delta-value of HammerJS, because it takes all pointers into account + // this is very important when using panning in combination with pinch-zooming + // but HammerJS' delta is relative to the start of the pan event + // so it needs to be converted to a per-event-delta for CornerstoneTools + var delta = { + x: e.deltaX - lastDelta.x, + y: e.deltaY - lastDelta.y + }; + + lastDelta = { + x: e.deltaX, + y: e.deltaY + }; + // calculate our current points in page and image coordinates currentPoints = { - page: cornerstoneMath.point.pageToPoint(e.pointers[0]), - image: cornerstone.pageToPixel(element, e.pointers[0].pageX, e.pointers[0].pageY), + page: { + x: lastPoints.page.x + delta.x, + y: lastPoints.page.y + delta.y + }, + image: cornerstone.pageToPixel(element, lastPoints.page.x + delta.x, lastPoints.page.y + delta.y), client: { - x: e.pointers[0].clientX, - y: e.pointers[0].clientY + x: lastPoints.client.x + delta.x, + y: lastPoints.client.y + delta.y } }; currentPoints.canvas = cornerstone.pixelToCanvas(element, currentPoints.image); @@ -711,6 +734,11 @@ if (typeof cornerstoneTools === 'undefined') { break; case 'panstart': + lastDelta = { + x: e.deltaX, + y: e.deltaY + }; + currentPoints = { page: cornerstoneMath.point.pageToPoint(e.pointers[0]), image: cornerstone.pageToPixel(element, e.pointers[0].pageX, e.pointers[0].pageY), @@ -1664,7 +1692,7 @@ if (typeof cornerstoneTools === 'undefined') { } cornerstone.updateImage(element); - $(element).on('CornerstoneToolsTouchStartActive', touchToolInterface.touchDownActivateCallback || touchDownActivateCallback); + $(element).on('CornerstoneToolsTouchStart', touchToolInterface.touchStartCallback || touchStartCallback); $(element).on('CornerstoneToolsTap', touchToolInterface.tapCallback || tapCallback); } @@ -1675,7 +1703,7 @@ if (typeof cornerstoneTools === 'undefined') { var distanceSq = 25; // Should probably make this a settable property later var handle = cornerstoneTools.getHandleNearImagePoint(element, data.handles, coords, distanceSq); if (handle) { - $(element).off('CornerstoneToolsTouchStartActive', touchToolInterface.touchDownActivateCallback || touchDownActivateCallback); + $(element).off('CornerstoneToolsTouchStart', touchToolInterface.touchStartCallback || touchStartCallback); $(element).off('CornerstoneToolsTap', touchToolInterface.tapCallback || tapCallback); data.active = true; handle.active = true; @@ -1692,7 +1720,7 @@ if (typeof cornerstoneTools === 'undefined') { for (i = 0; i < toolData.data.length; i++) { data = toolData.data[i]; if (touchToolInterface.pointNearTool(element, data, coords)) { - $(element).off('CornerstoneToolsTouchStartActive', touchToolInterface.touchDownActivateCallback || touchDownActivateCallback); + $(element).off('CornerstoneToolsTouchStart', touchToolInterface.touchStartCallback || touchStartCallback); $(element).off('CornerstoneToolsTap', touchToolInterface.tapCallback || tapCallback); data.active = true; cornerstone.updateImage(element); @@ -1732,7 +1760,7 @@ if (typeof cornerstoneTools === 'undefined') { } cornerstone.updateImage(eventData.element); - $(element).on('CornerstoneToolsTouchStartActive', touchToolInterface.touchDownActivateCallback || touchDownActivateCallback); + $(element).on('CornerstoneToolsTouchStart', touchToolInterface.touchStartCallback || touchStartCallback); $(element).on('CornerstoneToolsTap', touchToolInterface.tapCallback || tapCallback); if (touchToolInterface.pressCallback) { @@ -1759,7 +1787,7 @@ if (typeof cornerstoneTools === 'undefined') { var handle = cornerstoneTools.getHandleNearImagePoint(eventData.element, data.handles, coords, distance); if (handle) { - $(element).off('CornerstoneToolsTouchStartActive', touchToolInterface.touchDownActivateCallback || touchDownActivateCallback); + $(element).off('CornerstoneToolsTouchStart', touchToolInterface.touchStartCallback || touchStartCallback); $(element).off('CornerstoneToolsTap', touchToolInterface.tapCallback || tapCallback); if (touchToolInterface.pressCallback) { $(element).off('CornerstoneToolsTouchPress', touchToolInterface.pressCallback); @@ -1781,7 +1809,7 @@ if (typeof cornerstoneTools === 'undefined') { data = toolData.data[i]; if (touchToolInterface.pointNearTool(eventData.element, data, coords)) { - $(element).off('CornerstoneToolsTouchStartActive', touchToolInterface.touchDownActivateCallback || touchDownActivateCallback); + $(element).off('CornerstoneToolsTouchStart', touchToolInterface.touchStartCallback || touchStartCallback); $(element).off('CornerstoneToolsTap', touchToolInterface.tapCallback || tapCallback); if (touchToolInterface.pressCallback) { $(element).off('CornerstoneToolsTouchPress', touchToolInterface.pressCallback); @@ -1877,6 +1905,7 @@ if (typeof cornerstoneTools === 'undefined') { $(element).off('CornerstoneToolsTap', touchToolInterface.tapCallback || tapCallback); $(element).on('CornerstoneImageRendered', touchToolInterface.onImageRendered); + $(element).on('CornerstoneToolsTouchStart', touchToolInterface.touchStartCallback || touchStartCallback); //$(element).on('CornerstoneToolsTap', touchToolInterface.tapCallback || tapCallback); if (touchToolInterface.doubleTapCallback) { @@ -2913,6 +2942,12 @@ if (typeof cornerstoneTools === 'undefined') { context.shadowOffsetY = config.shadowOffsetY || 1; } + var seriesModule = cornerstone.metaData.get('generalSeriesModule', image.imageId); + var modality; + if (seriesModule) { + modality = seriesModule.modality; + } + var toolCoords; if (eventData.isTouchEvent === true) { toolCoords = cornerstone.pageToPixel(element, eventData.currentPoints.page.x, @@ -2937,18 +2972,18 @@ if (typeof cornerstoneTools === 'undefined') { storedPixels = cornerstone.getStoredPixels(element, toolCoords.x, toolCoords.y, 1, 1); var sp = storedPixels[0]; var mo = sp * eventData.image.slope + eventData.image.intercept; - var suv = cornerstoneTools.calculateSUV(eventData.image, sp); - var seriesModule = cornerstone.metaData.get('generalSeriesModule', image.imageId); - var modality = seriesModule.modality; + var modalityPixelValueText = parseFloat(mo.toFixed(2)); if (modality === 'CT') { - text += 'HU: '; - } - - // Draw text - text += parseFloat(mo.toFixed(2)); - if (suv) { - text += ' SUV: ' + parseFloat(suv.toFixed(2)); + text += 'HU: ' + modalityPixelValueText; + } else if (modality === 'PT') { + text += modalityPixelValueText; + var suv = cornerstoneTools.calculateSUV(eventData.image, sp); + if (suv) { + text += ' SUV: ' + parseFloat(suv.toFixed(2)); + } + } else { + text += modalityPixelValueText; } } @@ -3003,7 +3038,7 @@ if (typeof cornerstoneTools === 'undefined') { } function imageRenderedCallback() { - if(dragEventData) { + if (dragEventData) { cornerstoneTools.dragProbe.strategy(dragEventData); dragEventData = null; } @@ -3081,76 +3116,6 @@ if (typeof cornerstoneTools === 'undefined') { ///////// END ACTIVE TOOL /////// ///////// BEGIN IMAGE RENDERING /////// - function pointInEllipse(ellipse, location) { - var xRadius = ellipse.width / 2; - var yRadius = ellipse.height / 2; - - if (xRadius <= 0.0 || yRadius <= 0.0) { - return false; - } - - var center = { - x: ellipse.left + xRadius, - y: ellipse.top + yRadius - }; - - /* This is a more general form of the circle equation - * - * X^2/a^2 + Y^2/b^2 <= 1 - */ - - var normalized = { - x: location.x - center.x, - y: location.y - center.y - }; - - var inEllipse = ((normalized.x * normalized.x) / (xRadius * xRadius)) + ((normalized.y * normalized.y) / (yRadius * yRadius)) <= 1.0; - return inEllipse; - } - - function calculateMeanStdDev(sp, ellipse) { - // TODO: Get a real statistics library here that supports large counts - - var sum = 0; - var sumSquared = 0; - var count = 0; - var index = 0; - - for (var y = ellipse.top; y < ellipse.top + ellipse.height; y++) { - for (var x = ellipse.left; x < ellipse.left + ellipse.width; x++) { - if (pointInEllipse(ellipse, { - x: x, - y: y - }) === true) { - sum += sp[index]; - sumSquared += sp[index] * sp[index]; - count++; - } - - index++; - } - } - - if (count === 0) { - return { - count: count, - mean: 0.0, - variance: 0.0, - stdDev: 0.0 - }; - } - - var mean = sum / count; - var variance = sumSquared / count - mean * mean; - - return { - count: count, - mean: mean, - variance: variance, - stdDev: Math.sqrt(variance) - }; - } - function pointNearEllipse(element, data, coords, distance) { var startCanvas = cornerstone.pixelToCanvas(element, data.handles.start); var endCanvas = cornerstone.pixelToCanvas(element, data.handles.end); @@ -3169,8 +3134,8 @@ if (typeof cornerstoneTools === 'undefined') { height: Math.abs(startCanvas.y - endCanvas.y) + distance }; - var pointInMinorEllipse = pointInEllipse(minorEllipse, coords); - var pointInMajorEllipse = pointInEllipse(majorEllipse, coords); + var pointInMinorEllipse = cornerstoneTools.pointInEllipse(minorEllipse, coords); + var pointInMajorEllipse = cornerstoneTools.pointInEllipse(majorEllipse, coords); if (pointInMajorEllipse && !pointInMinorEllipse) { return true; @@ -3207,7 +3172,10 @@ if (typeof cornerstoneTools === 'undefined') { var config = cornerstoneTools.ellipticalRoi.getConfiguration(); var context = eventData.canvasContext.canvas.getContext('2d'); var seriesModule = cornerstone.metaData.get('generalSeriesModule', image.imageId); - var modality = seriesModule.modality; + var modality; + if (seriesModule) { + modality = seriesModule.modality; + } context.setTransform(1, 0, 0, 1, 0, 0); @@ -3282,10 +3250,10 @@ if (typeof cornerstoneTools === 'undefined') { // Retrieve the bounds of the ellipse in image coordinates var ellipse = { - left: Math.min(data.handles.start.x, data.handles.end.x), - top: Math.min(data.handles.start.y, data.handles.end.y), - width: Math.abs(data.handles.start.x - data.handles.end.x), - height: Math.abs(data.handles.start.y - data.handles.end.y) + left: Math.round(Math.min(data.handles.start.x, data.handles.end.x)), + top: Math.round(Math.min(data.handles.start.y, data.handles.end.y)), + width: Math.round(Math.abs(data.handles.start.x - data.handles.end.x)), + height: Math.round(Math.abs(data.handles.start.y - data.handles.end.y)) }; // First, make sure this is not a color image, since no mean / standard @@ -3295,7 +3263,7 @@ if (typeof cornerstoneTools === 'undefined') { var pixels = cornerstone.getPixels(element, ellipse.left, ellipse.top, ellipse.width, ellipse.height); // Calculate the mean & standard deviation from the pixels and the ellipse details - meanStdDev = calculateMeanStdDev(pixels, ellipse); + meanStdDev = cornerstoneTools.calculateEllipseStatistics(pixels, ellipse); if (modality === 'PT') { // If the image is from a PET scan, use the DICOM tags to @@ -3339,7 +3307,7 @@ if (typeof cornerstoneTools === 'undefined') { var textLines = []; // If the mean and standard deviation values are present, display them - if (meanStdDev && meanStdDev.mean) { + if (meanStdDev && meanStdDev.mean !== undefined) { // If the modality is CT, add HU to denote Hounsfield Units var moSuffix = ''; if (modality === 'CT') { @@ -3716,7 +3684,11 @@ if (typeof cornerstoneTools === 'undefined') { // Connect the end of the drawing to the handle nearest to the click if (handleNearby !== undefined){ - data.handles[config.currentHandle - 1].lines.push(data.handles[handleNearby]); + // only save x,y params from nearby handle to prevent circular reference + data.handles[config.currentHandle - 1].lines.push({ + x: data.handles[handleNearby].x, + y: data.handles[handleNearby].y + }); } if (config.modifying) { @@ -5692,9 +5664,9 @@ if (typeof cornerstoneTools === 'undefined') { if (data.handles.textBox.hasMoved) { // Draw dashed link line between tool and text var link = { - start: {}, - end: {} - }; + start: {}, + end: {} + }; link.end.x = textCoords.x; link.end.y = textCoords.y; @@ -5703,22 +5675,22 @@ if (typeof cornerstoneTools === 'undefined') { var boundingBoxPoints = [ { - // Top middle point of bounding box - x: boundingBox.left + boundingBox.width / 2, - y: boundingBox.top - }, { - // Left middle point of bounding box - x: boundingBox.left, - y: boundingBox.top + boundingBox.height / 2 - }, { - // Bottom middle point of bounding box - x: boundingBox.left + boundingBox.width / 2, - y: boundingBox.top + boundingBox.height - }, { - // Right middle point of bounding box - x: boundingBox.left + boundingBox.width, - y: boundingBox.top + boundingBox.height / 2 - }, + // Top middle point of bounding box + x: boundingBox.left + boundingBox.width / 2, + y: boundingBox.top + }, { + // Left middle point of bounding box + x: boundingBox.left, + y: boundingBox.top + boundingBox.height / 2 + }, { + // Bottom middle point of bounding box + x: boundingBox.left + boundingBox.width / 2, + y: boundingBox.top + boundingBox.height + }, { + // Right middle point of bounding box + x: boundingBox.left + boundingBox.width, + y: boundingBox.top + boundingBox.height / 2 + }, ]; link.end = cornerstoneMath.point.findClosestPoint(boundingBoxPoints, link.start); @@ -6504,7 +6476,7 @@ if (typeof cornerstoneTools === 'undefined') { // now check to see if there is a handle we can move if (!toolData) { - return; + return false; } if (eventData.handlePressed) { @@ -7045,6 +7017,15 @@ if (typeof cornerstoneTools === 'undefined') { } function correctShift(shift, viewport) { + // Apply Flips + if (viewport.hflip) { + shift.x *= -1; + } + + if (viewport.vflip) { + shift.y *= -1; + } + // Apply rotations if (viewport.rotation !== 0) { var angle = viewport.rotation * Math.PI / 180; @@ -7059,15 +7040,6 @@ if (typeof cornerstoneTools === 'undefined') { shift.y = newY; } - // Apply Flips - if (viewport.hflip) { - shift.x *= -1; - } - - if (viewport.vflip) { - shift.y *= -1; - } - return shift; } @@ -8443,7 +8415,14 @@ if (typeof cornerstoneTools === 'undefined') { var referenceImagePlane = cornerstoneTools.metaData.get('imagePlane', referenceImage.imageId); // Make sure the target and reference actually have image plane metadata - if (!targetImagePlane || !referenceImagePlane) { + if (!targetImagePlane || + !referenceImagePlane || + !targetImagePlane.rowCosines || + !targetImagePlane.columnCosines || + !targetImagePlane.imagePositionPatient || + !referenceImagePlane.rowCosines || + !referenceImagePlane.columnCosines || + !referenceImagePlane.imagePositionPatient) { return; } @@ -8726,6 +8705,88 @@ if (typeof cornerstoneTools === 'undefined') { var toolType = 'playClip'; + /** + * [private] Turns a Frame Time Vector (0018,1065) array into a normalized array of timeouts. Each element + * ... of the resulting array represents the amount of time each frame will remain on the screen. + * @param {Array} vector A Frame Time Vector (0018,1065) as specified in section C.7.6.5.1.2 of DICOM standard. + * @param {Number} speed A speed factor which will be applied to each element of the resulting array. + * @return {Array} An array with timeouts for each animation frame. + */ + function getPlayClipTimeouts(vector, speed) { + + var i, + sample, + delay, + sum = 0, + limit = vector.length, + timeouts = []; + + // initialize time varying to false + timeouts.isTimeVarying = false; + + if (typeof speed !== 'number' || speed <= 0) { + speed = 1; + } + + // first element of a frame time vector must be discarded + for (i = 1; i < limit; i++) { + delay = (+vector[i] / speed) | 0; // integral part only + timeouts.push(delay); + if (i === 1) { // use first item as a sample for comparison + sample = delay; + } else if (delay !== sample) { + timeouts.isTimeVarying = true; + } + + sum += delay; + } + + if (timeouts.length > 0) { + if (timeouts.isTimeVarying) { + // if it's a time varying vector, make the last item an average... + delay = (sum / timeouts.length) | 0; + } else { + delay = timeouts[0]; + } + + timeouts.push(delay); + } + + return timeouts; + + } + + /** + * [private] Performs the heavy lifting of stopping an ongoing animation. + * @param {Object} playClipData The data from playClip that needs to be stopped. + * @return void + */ + function stopClipWithData(playClipData) { + var id = playClipData.intervalId; + if (typeof id !== 'undefined') { + playClipData.intervalId = undefined; + if (playClipData.usingFrameTimeVector) { + clearTimeout(id); + } else { + clearInterval(id); + } + } + } + + /** + * [private] Trigger playClip tool stop event. + * @param element + * @return void + */ + function triggerStopEvent(element) { + var event, + eventDetail = { + element: element + }; + event = $.Event('CornerstoneToolsClipStopped', eventDetail); + $(element).trigger(event, eventDetail); + } + /** * Starts playing a clip or adjusts the frame rate of an already playing clip. framesPerSecond is * optional and defaults to 30 if not specified. A negative framesPerSecond will play the clip in reverse. @@ -8734,86 +8795,109 @@ if (typeof cornerstoneTools === 'undefined') { * @param framesPerSecond */ function playClip(element, framesPerSecond) { + + // hoisting of context variables + var stackToolData, + stackData, + playClipToolData, + playClipData, + playClipTimeouts, + playClipAction; + if (element === undefined) { throw 'playClip: element must not be undefined'; } - var stackToolData = cornerstoneTools.getToolState(element, 'stack'); + stackToolData = cornerstoneTools.getToolState(element, 'stack'); if (!stackToolData || !stackToolData.data || !stackToolData.data.length) { return; } - var stackData = stackToolData.data[0]; - - var playClipToolData = cornerstoneTools.getToolState(element, toolType); - var playClipData; + stackData = stackToolData.data[0]; + playClipToolData = cornerstoneTools.getToolState(element, toolType); if (!playClipToolData || !playClipToolData.data || !playClipToolData.data.length) { playClipData = { intervalId: undefined, framesPerSecond: 30, lastFrameTimeStamp: undefined, frameRate: 0, + frameTimeVector: undefined, + ignoreFrameTimeVector: false, + usingFrameTimeVector: false, + speed: 1, + reverse: false, loop: true }; cornerstoneTools.addToolState(element, toolType, playClipData); } else { playClipData = playClipToolData.data[0]; + // make sure the specified clip is not running before any property update + stopClipWithData(playClipData); } - // If a framerate is specified, update the playClipData now - if (framesPerSecond) { - playClipData.framesPerSecond = framesPerSecond; + // If a framesPerSecond is specified and is valid, update the playClipData now + if (framesPerSecond < 0 || framesPerSecond > 0) { + playClipData.framesPerSecond = +framesPerSecond; + playClipData.reverse = playClipData.framesPerSecond < 0; + // if framesPerSecond is given, frameTimeVector will be ignored... + playClipData.ignoreFrameTimeVector = true; } - // if already playing, do not set a new interval - if (playClipData.intervalId !== undefined) { - return; + // determine if frame time vector should be used instead of a fixed frame rate... + if ( + playClipData.ignoreFrameTimeVector !== true && + playClipData.frameTimeVector && + playClipData.frameTimeVector.length === stackData.imageIds.length + ) { + playClipTimeouts = getPlayClipTimeouts(playClipData.frameTimeVector, playClipData.speed); } - playClipData.intervalId = setInterval(function() { - var newImageIdIndex = stackData.currentImageIdIndex; + // this function encapsulates the frame rendering logic... + playClipAction = function playClipAction() { - if (playClipData.framesPerSecond > 0) { - newImageIdIndex++; - } else { + // hoisting of context variables + var loader, + viewport, + startLoadingHandler, + endLoadingHandler, + errorLoadingHandler, + newImageIdIndex = stackData.currentImageIdIndex, + imageCount = stackData.imageIds.length; + + if (playClipData.reverse) { newImageIdIndex--; + } else { + newImageIdIndex++; } - if (!playClipData.loop && (newImageIdIndex >= stackData.imageIds.length || newImageIdIndex < 0)) { - var eventDetail = { - element: element - }; - - var event = $.Event('CornerstoneToolsClipStopped', eventDetail); - $(element).trigger(event, eventDetail); - - clearInterval(playClipData.intervalId); - playClipData.intervalId = undefined; + if (!playClipData.loop && (newImageIdIndex < 0 || newImageIdIndex >= imageCount)) { + stopClipWithData(playClipData); + triggerStopEvent(element); return; } // loop around if we go outside the stack - if (newImageIdIndex >= stackData.imageIds.length) { + if (newImageIdIndex >= imageCount) { newImageIdIndex = 0; } if (newImageIdIndex < 0) { - newImageIdIndex = stackData.imageIds.length - 1; + newImageIdIndex = imageCount - 1; } if (newImageIdIndex !== stackData.currentImageIdIndex) { - var startLoadingHandler = cornerstoneTools.loadHandlerManager.getStartLoadHandler(); - var endLoadingHandler = cornerstoneTools.loadHandlerManager.getEndLoadHandler(); - var errorLoadingHandler = cornerstoneTools.loadHandlerManager.getErrorLoadingHandler(); + + startLoadingHandler = cornerstoneTools.loadHandlerManager.getStartLoadHandler(); + endLoadingHandler = cornerstoneTools.loadHandlerManager.getEndLoadHandler(); + errorLoadingHandler = cornerstoneTools.loadHandlerManager.getErrorLoadingHandler(); if (startLoadingHandler) { startLoadingHandler(element); } - var viewport = cornerstone.getViewport(element); + viewport = cornerstone.getViewport(element); - var loader; if (stackData.preventCache === true) { loader = cornerstone.loadImage(stackData.imageIds[newImageIdIndex]); } else { @@ -8832,8 +8916,25 @@ if (typeof cornerstoneTools === 'undefined') { errorLoadingHandler(element, imageId, error); } }); + } - }, 1000 / Math.abs(playClipData.framesPerSecond)); + + }; + + // if playClipTimeouts array is available, not empty and its elements are NOT uniform ... + // ... (at least one timeout is different from the others), use alternate setTimeout implementation + if (playClipTimeouts && playClipTimeouts.length > 0 && playClipTimeouts.isTimeVarying) { + playClipData.usingFrameTimeVector = true; + playClipData.intervalId = setTimeout(function playClipTimeoutHandler() { + playClipData.intervalId = setTimeout(playClipTimeoutHandler, playClipTimeouts[stackData.currentImageIdIndex]); + playClipAction(); + }, 0); + } else { + // ... otherwise user setInterval implementation which is much more efficient. + playClipData.usingFrameTimeVector = false; + playClipData.intervalId = setInterval(playClipAction, 1000 / Math.abs(playClipData.framesPerSecond)); + } + } /** @@ -8841,15 +8942,15 @@ if (typeof cornerstoneTools === 'undefined') { * * @param element */ function stopClip(element) { + var playClipToolData = cornerstoneTools.getToolState(element, toolType); + if (!playClipToolData || !playClipToolData.data || !playClipToolData.data.length) { return; } - var playClipData = playClipToolData.data[0]; + stopClipWithData(playClipToolData.data[0]); - clearInterval(playClipData.intervalId); - playClipData.intervalId = undefined; } // module/private exports @@ -8872,7 +8973,8 @@ Display scroll progress bar across bottom of image. var configuration = { backgroundColor: 'rgb(19, 63, 141)', - fillColor: 'white' + fillColor: 'white', + orientation: 'horizontal' }; function onImageRendered(e, eventData){ @@ -8892,7 +8994,11 @@ Display scroll progress bar across bottom of image. // draw indicator background context.fillStyle = config.backgroundColor; - context.fillRect(0, height - scrollBarHeight, width, scrollBarHeight); + if (config.orientation === 'horizontal') { + context.fillRect(0, height - scrollBarHeight, width, scrollBarHeight); + } else { + context.fillRect(0, 0, scrollBarHeight, height); + } // get current image index var stackData = cornerstoneTools.getToolState(element, 'stack'); @@ -8905,10 +9011,16 @@ Display scroll progress bar across bottom of image. // draw current image cursor var cursorWidth = width / imageIds.length; + var cursorHeight = height / imageIds.length; var xPosition = cursorWidth * currentImageIdIndex; + var yPosition = cursorHeight * currentImageIdIndex; context.fillStyle = config.fillColor; - context.fillRect(xPosition, height - scrollBarHeight, cursorWidth, scrollBarHeight); + if (config.orientation === 'horizontal') { + context.fillRect(xPosition, height - scrollBarHeight, cursorWidth, scrollBarHeight); + } else { + context.fillRect(0, yPosition, scrollBarHeight, cursorHeight); + } context.restore(); } @@ -9095,29 +9207,20 @@ Display scroll progress bar across bottom of image. requestPoolManager.startGrabbing(); } - function handleCacheFull(e) { - // Stop prefetching if the ImageCacheFull event is fired from cornerstone - // console.log('CornerstoneImageCacheFull full, stopping'); - var element = e.data.element; - - var stackPrefetchData = cornerstoneTools.getToolState(element, toolType); - if (!stackPrefetchData || !stackPrefetchData.data || !stackPrefetchData.data.length) { - return; - } - - // Disable the stackPrefetch tool - // stackPrefetchData.data[0].enabled = false; - - // Clear current prefetch requests from the requestPool - cornerstoneTools.requestPoolManager.clearRequestStack(requestType); - } - function promiseRemovedHandler(e, eventData) { // When an imagePromise has been pushed out of the cache, re-add its index // it to the indicesToRequest list so that it will be retrieved later if the // currentImageIdIndex is changed to an image nearby var element = e.data.element; - var stackData = cornerstoneTools.getToolState(element, 'stack'); + var stackData; + + try { + // It will throw an exception in some cases (eg: thumbnails) + stackData = cornerstoneTools.getToolState(element, 'stack'); + } catch(error) { + return; + } + if (!stackData || !stackData.data || !stackData.data.length) { return; } @@ -9194,11 +9297,6 @@ Display scroll progress bar across bottom of image. $(element).off('CornerstoneNewImage', onImageUpdated); $(element).on('CornerstoneNewImage', onImageUpdated); - $(cornerstone).off('CornerstoneImageCacheFull', handleCacheFull); - $(cornerstone).on('CornerstoneImageCacheFull', { - element: element - }, handleCacheFull); - $(cornerstone).off('CornerstoneImageCachePromiseRemoved', promiseRemovedHandler); $(cornerstone).on('CornerstoneImageCachePromiseRemoved', { element: element @@ -9209,7 +9307,6 @@ Display scroll progress bar across bottom of image. clearTimeout(resetPrefetchTimeout); $(element).off('CornerstoneNewImage', onImageUpdated); - $(cornerstone).off('CornerstoneImageCacheFull', handleCacheFull); $(cornerstone).off('CornerstoneImageCachePromiseRemoved', promiseRemovedHandler); var stackPrefetchData = cornerstoneTools.getToolState(element, toolType); @@ -11184,6 +11281,79 @@ Display scroll progress bar across bottom of image. // End Source; src/timeSeriesTools/timeSeriesScroll.js +// Begin Source: src/util/RoundToDecimal.js +(function($, cornerstone, cornerstoneTools) { + + 'use strict'; + + function roundToDecimal(value, precision) { + var multiplier = Math.pow(10, precision); + return (Math.round(value * multiplier) / multiplier); + } + + // module exports + cornerstoneTools.roundToDecimal = roundToDecimal; + +})($, cornerstone, cornerstoneTools); + +// End Source; src/util/RoundToDecimal.js + +// Begin Source: src/util/calculateEllipseStatistics.js +(function(cornerstoneTools) { + + 'use strict'; + + function calculateEllipseStatistics(sp, ellipse) { + // TODO: Get a real statistics library here that supports large counts + + var sum = 0; + var sumSquared = 0; + var count = 0; + var index = 0; + + for (var y = ellipse.top; y < ellipse.top + ellipse.height; y++) { + for (var x = ellipse.left; x < ellipse.left + ellipse.width; x++) { + var point = { + x: x, + y: y + }; + + if (cornerstoneTools.pointInEllipse(ellipse, point)) { + sum += sp[index]; + sumSquared += sp[index] * sp[index]; + count++; + } + + index++; + } + } + + if (count === 0) { + return { + count: count, + mean: 0.0, + variance: 0.0, + stdDev: 0.0 + }; + } + + var mean = sum / count; + var variance = sumSquared / count - mean * mean; + + return { + count: count, + mean: mean, + variance: variance, + stdDev: Math.sqrt(variance) + }; + } + + cornerstoneTools.calculateEllipseStatistics = calculateEllipseStatistics; + +})(cornerstoneTools); + +// End Source; src/util/calculateEllipseStatistics.js + // Begin Source: src/util/calculateSUV.js (function(cornerstoneTools) { @@ -11201,6 +11371,10 @@ Display scroll progress bar across bottom of image. var patientStudyModule = cornerstone.metaData.get('patientStudyModule', image.imageId); var seriesModule = cornerstone.metaData.get('generalSeriesModule', image.imageId); + if (!patientStudyModule || !seriesModule) { + return; + } + var modality = seriesModule.modality; // image must be PET @@ -11211,12 +11385,12 @@ Display scroll progress bar across bottom of image. var modalityPixelValue = storedPixelValue * image.slope + image.intercept; var patientWeight = patientStudyModule.patientWeight; // in kg - if (patientWeight === undefined) { + if (!patientWeight) { return; } var petSequenceModule = cornerstone.metaData.get('petIsotopeModule', image.imageId); - if (petSequenceModule === undefined) { + if (!petSequenceModule) { return; } @@ -11701,6 +11875,44 @@ Display scroll progress bar across bottom of image. // End Source; src/util/pauseEvent.js +// Begin Source: src/util/pointInEllipse.js +(function(cornerstoneTools) { + + 'use strict'; + + function pointInEllipse(ellipse, location) { + var xRadius = ellipse.width / 2; + var yRadius = ellipse.height / 2; + + if (xRadius <= 0.0 || yRadius <= 0.0) { + return false; + } + + var center = { + x: ellipse.left + xRadius, + y: ellipse.top + yRadius + }; + + /* This is a more general form of the circle equation + * + * X^2/a^2 + Y^2/b^2 <= 1 + */ + + var normalized = { + x: location.x - center.x, + y: location.y - center.y + }; + + var inEllipse = ((normalized.x * normalized.x) / (xRadius * xRadius)) + ((normalized.y * normalized.y) / (yRadius * yRadius)) <= 1.0; + return inEllipse; + } + + cornerstoneTools.pointInEllipse = pointInEllipse; + +})(cornerstoneTools); + +// End Source; src/util/pointInEllipse.js + // Begin Source: src/util/pointInsideBoundingBox.js (function(cornerstoneMath, cornerstoneTools) { @@ -11844,41 +12056,6 @@ Display scroll progress bar across bottom of image. // End Source; src/util/pointProjector.js -// Begin Source: src/util/requestAnimFrame.js -(function(cornerstoneTools) { - - 'use strict'; - - function requestAnimFrame(callback) { - // This functionality was moved to cornerstone. - console.warn('cornerstoneTools.requestAnimFrame() is deprecated, consider using cornerstone.requestAnimationFrame()'); - cornerstone.requestAnimationFrame(callback); - } - - // Module exports - cornerstoneTools.requestAnimFrame = requestAnimFrame; - -})(cornerstoneTools); - -// End Source; src/util/requestAnimFrame.js - -// Begin Source: src/util/RoundToDecimal.js -(function($, cornerstone, cornerstoneTools) { - - 'use strict'; - - function roundToDecimal(value, precision) { - var multiplier = Math.pow(10, precision); - return (Math.round(value * multiplier) / multiplier); - } - - // module exports - cornerstoneTools.roundToDecimal = roundToDecimal; - -})($, cornerstone, cornerstoneTools); - -// End Source; src/util/RoundToDecimal.js - // Begin Source: src/util/scroll.js (function(cornerstone, cornerstoneTools) { diff --git a/Packages/ohif-cornerstone/client/cornerstoneWADOImageLoader.js b/Packages/ohif-cornerstone/client/cornerstoneWADOImageLoader.js index 4bed01950..c6545b302 100644 --- a/Packages/ohif-cornerstone/client/cornerstoneWADOImageLoader.js +++ b/Packages/ohif-cornerstone/client/cornerstoneWADOImageLoader.js @@ -383,7 +383,7 @@ if(typeof cornerstoneWADOImageLoader === 'undefined'){ // JPEGBaseline (8 bits) is already returning the pixel data in the right format (rgba) // because it's using a canvas to load and decode images. - if(!cornerstoneWADOImageLoader.isJPEGBaseline8Bit(imageFrame, transferSyntax)) { + if(!cornerstoneWADOImageLoader.isJPEGBaseline8BitColor(imageFrame, transferSyntax)) { setPixelDataType(imageFrame); // convert color space @@ -447,10 +447,12 @@ if(typeof cornerstoneWADOImageLoader === 'undefined'){ image.render = cornerstone.renderGrayscaleImage; } - // Calculate min/max pixel values (do not trust DICOM Headers) - var minMax = cornerstoneWADOImageLoader.getMinMax(imageFrame.pixelData); - image.minPixelValue = minMax.min; - image.maxPixelValue = minMax.max; + // calculate min/max if not supplied + if(image.minPixelValue === undefined || image.maxPixelValue === undefined) { + var minMax = cornerstoneWADOImageLoader.getMinMax(imageFrame.pixelData); + image.minPixelValue = minMax.min; + image.maxPixelValue = minMax.max; + } // Modality LUT if(modalityLutModule.modalityLUTSequence && @@ -531,9 +533,12 @@ if(typeof cornerstoneWADOImageLoader === 'undefined'){ // JPEG Baseline lossy process 1 (8 bit) else if (transferSyntax === "1.2.840.10008.1.2.4.50") { - if(imageFrame.bitsAllocated === 8) + // Handle 8-bit JPEG Baseline color images using the browser's built-in + // JPEG decoding + if(imageFrame.bitsAllocated === 8 && + (imageFrame.samplesPerPixel === 3 || imageFrame.samplesPerPixel === 4)) { - return cornerstoneWADOImageLoader.decodeJPEGBaseline8Bit(imageFrame, pixelData, canvas); + return cornerstoneWADOImageLoader.decodeJPEGBaseline8BitColor(imageFrame, pixelData, canvas); } else { return addDecodeTask(imageFrame, transferSyntax, pixelData, options); } @@ -622,7 +627,7 @@ if(typeof cornerstoneWADOImageLoader === 'undefined'){ } } - function decodeJPEGBaseline8Bit(imageFrame, pixelData, canvas) { + function decodeJPEGBaseline8BitColor(imageFrame, pixelData, canvas) { var start = new Date().getTime(); var deferred = $.Deferred(); @@ -666,18 +671,20 @@ if(typeof cornerstoneWADOImageLoader === 'undefined'){ return deferred.promise(); } - function isJPEGBaseline8Bit(imageFrame, transferSyntax) { + function isJPEGBaseline8BitColor(imageFrame, transferSyntax) { transferSyntax = transferSyntax || imageFrame.transferSyntax; - if((imageFrame.bitsAllocated === 8) && (transferSyntax === "1.2.840.10008.1.2.4.50")) { + if(imageFrame.bitsAllocated === 8 && + transferSyntax === "1.2.840.10008.1.2.4.50" && + (imageFrame.samplesPerPixel === 3 || imageFrame.samplesPerPixel === 4)) { return true; } } // module exports - cornerstoneWADOImageLoader.decodeJPEGBaseline8Bit = decodeJPEGBaseline8Bit; - cornerstoneWADOImageLoader.isJPEGBaseline8Bit = isJPEGBaseline8Bit; + cornerstoneWADOImageLoader.decodeJPEGBaseline8BitColor = decodeJPEGBaseline8BitColor; + cornerstoneWADOImageLoader.isJPEGBaseline8BitColor = isJPEGBaseline8BitColor; }($, cornerstoneWADOImageLoader)); /** diff --git a/Packages/ohif-cornerstone/public/js/cornerstoneWADOImageLoaderWebWorker.es5.js b/Packages/ohif-cornerstone/public/js/cornerstoneWADOImageLoaderWebWorker.es5.js index 7c63db26a..7ad3ad999 100644 --- a/Packages/ohif-cornerstone/public/js/cornerstoneWADOImageLoaderWebWorker.es5.js +++ b/Packages/ohif-cornerstone/public/js/cornerstoneWADOImageLoaderWebWorker.es5.js @@ -1,2 +1,2 @@ /*! cornerstone-wado-image-loader - v0.14.3 - 2017-04-04 | (c) 2014 Chris Hafey | https://github.com/chafey/cornerstoneWADOImageLoader */ -cornerstoneWADOImageLoaderWebWorker={registerTaskHandler:void 0},function(){function a(a){if(!e){if(c=a.config,a.config.webWorkerTaskPaths)for(var b=0;b>8&255}function c(a,c){if(16===a.bitsAllocated){var d=c.buffer,e=c.byteOffset,f=c.length;e%2&&(d=d.slice(e),e=0),0===a.pixelRepresentation?a.pixelData=new Uint16Array(d,e,f/2):a.pixelData=new Int16Array(d,e,f/2);for(var g=0;gq;q++)n.pixelData[q]=p[q]}else if(c)if(Int16Array.from)n.pixelData=Int16Array.from(p);else{n.pixelData=new Int16Array(o);for(var q=0;o>q;q++)n.pixelData[q]=p[q]}else if(Uint16Array.from)n.pixelData=Uint16Array.from(p);else{n.pixelData=new Uint16Array(o);for(var q=0;o>q;q++)n.pixelData[q]=p[q]}var r=Date.now();return n.perf_timetodecode=r-k,g._free(d),g._free(e),g._free(m),g._free(f),g._free(h),g._free(i),g._free(j),n}function d(a,b){var d=a.bitsAllocated<=8?1:2,e=1===a.pixelRepresentation,f=c(b,d,e);return a.columns=f.sx,a.rows=f.sy,a.pixelData=f.pixelData,f.nbChannels>1&&(a.photometricInterpretation="RGB"),a}function e(a){if(!a.usePDFJS&&"undefined"==typeof OpenJPEG)throw"OpenJPEG decoder not loaded";if(!g&&(g=OpenJPEG(),!g||!g._jp2_decode))throw"OpenJPEG failed to initialize"}function f(a,c,f,g){return g=g||{},e(f),g.usePDFJS||f.usePDFJS?b(a,c):d(a,c)}var g;a.decodeJPEG2000=f,a.initializeJPEG2000=e}(cornerstoneWADOImageLoader),function(a){"use strict";function b(a,b){if("undefined"==typeof JpegImage)throw"No JPEG Baseline decoder loaded";var c=new JpegImage;return c.parse(b),c.colorTransform=!1,8===a.bitsAllocated?(a.pixelData=c.getData(a.columns,a.rows),a):16===a.bitsAllocated?(a.pixelData=c.getData16(a.columns,a.rows),a):void 0}a.decodeJPEGBaseline=b}(cornerstoneWADOImageLoader),function(a){function b(a,b){if("undefined"==typeof jpeg||"undefined"==typeof jpeg.lossless||"undefined"==typeof jpeg.lossless.Decoder)throw"No JPEG Lossless decoder loaded";var c=a.bitsAllocated<=8?1:2,d=b.buffer,e=new jpeg.lossless.Decoder,f=e.decode(d,b.byteOffset,b.length,c);return 0===a.pixelRepresentation?16===a.bitsAllocated?(a.pixelData=new Uint16Array(f.buffer),a):(a.pixelData=new Uint8Array(f.buffer),a):(a.pixelData=new Int16Array(f.buffer),a)}a.decodeJPEGLossless=b}(cornerstoneWADOImageLoader),function(a){function b(a,b){var c=e._malloc(a.length);e.writeArrayToMemory(a,c);var d=e._malloc(4),f=e._malloc(4),g=e._malloc(4),h=e._malloc(4),i=e._malloc(4),j=e._malloc(4),k=e._malloc(4),l=e._malloc(4),m=e._malloc(4),n=e.ccall("jpegls_decode","number",["number","number","number","number","number","number","number","number","number","number","number"],[c,a.length,d,f,g,h,i,j,l,k,m]),o={result:n,width:e.getValue(g,"i32"),height:e.getValue(h,"i32"),bitsPerSample:e.getValue(i,"i32"),stride:e.getValue(j,"i32"),components:e.getValue(l,"i32"),allowedLossyError:e.getValue(k,"i32"),interleaveMode:e.getValue(m,"i32"),pixelData:void 0},p=e.getValue(d,"*");return o.bitsPerSample<=8?(o.pixelData=new Uint8Array(o.width*o.height*o.components),o.pixelData.set(new Uint8Array(e.HEAP8.buffer,p,o.pixelData.length))):b?(o.pixelData=new Int16Array(o.width*o.height*o.components),o.pixelData.set(new Int16Array(e.HEAP16.buffer,p,o.pixelData.length))):(o.pixelData=new Uint16Array(o.width*o.height*o.components),o.pixelData.set(new Uint16Array(e.HEAP16.buffer,p,o.pixelData.length))),e._free(c),e._free(p),e._free(d),e._free(f),e._free(g),e._free(h),e._free(i),e._free(j),e._free(l),e._free(m),o}function c(){if("undefined"==typeof CharLS)throw"No JPEG-LS decoder loaded";if(!e&&(e=CharLS(),!e||!e._jpegls_decode))throw"JPEG-LS failed to initialize"}function d(a,d){c();var e=b(d,1===a.pixelRepresentation);if(0!==e.result&&6!==e.result)throw"JPEG-LS decoder failed to decode frame (error code "+e.result+")";return a.columns=e.width,a.rows=e.height,a.pixelData=e.pixelData,a}var e;a.decodeJPEGLS=d,a.initializeJPEGLS=c}(cornerstoneWADOImageLoader),function(a){function b(a,b){if(16===a.bitsAllocated){var c=b.buffer,d=b.byteOffset,e=b.length;d%2&&(c=c.slice(d),d=0),0===a.pixelRepresentation?a.pixelData=new Uint16Array(c,d,e/2):a.pixelData=new Int16Array(c,d,e/2)}else 8===a.bitsAllocated&&(a.pixelData=b);return a}a.decodeLittleEndian=b}(cornerstoneWADOImageLoader),function(a){function b(a,b){if(8===a.bitsAllocated)return c(a,b);if(16===a.bitsAllocated)return d(a,b);throw"unsupported pixel format for RLE"}function c(a,b){for(var c=b,d=a.rows*a.columns,e=new ArrayBuffer(d*a.samplesPerPixel),f=new DataView(c.buffer,c.byteOffset),g=new Int8Array(c.buffer,c.byteOffset),h=new Int8Array(e),i=0,j=f.getInt32(0,!0),k=0;j>k;++k){i=k;var l=f.getInt32(4*(k+1),!0),m=f.getInt32(4*(k+2),!0);0===m&&(m=c.length);for(var n=d*j;m>l;){var o=g[l++];if(o>=0&&127>=o)for(var p=0;o+1>p&&n>i;++p)h[i]=g[l++],i+=a.samplesPerPixel;else if(-1>=o&&o>=-127)for(var q=g[l++],r=0;-o+1>r&&n>i;++r)h[i]=q,i+=a.samplesPerPixel}}return a.pixelData=new Uint8Array(e),a}function d(a,b){for(var c=b,d=a.rows*a.columns,e=new ArrayBuffer(d*a.samplesPerPixel*2),f=new DataView(c.buffer,c.byteOffset),g=new Int8Array(c.buffer,c.byteOffset),h=new Int8Array(e),i=f.getInt32(0,!0),j=0;i>j;++j){var k=0,l=0===j?1:0,m=f.getInt32(4*(j+1),!0),n=f.getInt32(4*(j+2),!0);for(0===n&&(n=c.length);n>m;){var o=g[m++];if(o>=0&&127>=o)for(var p=0;o+1>p&&d>k;++p)h[2*k+l]=g[m++],k++;else if(-1>=o&&o>=-127)for(var q=g[m++],r=0;-o+1>r&&d>k;++r)h[2*k+l]=q,k++}}return 0===a.pixelRepresentation?a.pixelData=new Uint16Array(e):a.pixelData=new Int16Array(e),a}a.decodeRLE=b}(cornerstoneWADOImageLoader),function(a){"use strict";function b(a){for(var b,c=a[0],d=a[0],e=a.length,f=1;e>f;f++)b=a[f],c=Math.min(c,b),d=Math.max(d,b);return{min:c,max:d}}a.getMinMax=b}(cornerstoneWADOImageLoader); \ No newline at end of file +cornerstoneWADOImageLoaderWebWorker={registerTaskHandler:void 0},function(){function a(a){if(!e){if(c=a.config,a.config.webWorkerTaskPaths)for(var b=0;b>8&255}function c(a,c){if(16===a.bitsAllocated){var d=c.buffer,e=c.byteOffset,f=c.length;e%2&&(d=d.slice(e),e=0),0===a.pixelRepresentation?a.pixelData=new Uint16Array(d,e,f/2):a.pixelData=new Int16Array(d,e,f/2);for(var g=0;gq;q++)n.pixelData[q]=p[q]}else if(c)if(Int16Array.from)n.pixelData=Int16Array.from(p);else{n.pixelData=new Int16Array(o);for(var q=0;o>q;q++)n.pixelData[q]=p[q]}else if(Uint16Array.from)n.pixelData=Uint16Array.from(p);else{n.pixelData=new Uint16Array(o);for(var q=0;o>q;q++)n.pixelData[q]=p[q]}var r=Date.now();return n.perf_timetodecode=r-k,g._free(d),g._free(e),g._free(m),g._free(f),g._free(h),g._free(i),g._free(j),n}function d(a,b){var d=a.bitsAllocated<=8?1:2,e=1===a.pixelRepresentation,f=c(b,d,e);return a.columns=f.sx,a.rows=f.sy,a.pixelData=f.pixelData,f.nbChannels>1&&(a.photometricInterpretation="RGB"),a}function e(a){if(!a.usePDFJS&&"undefined"==typeof OpenJPEG)throw"OpenJPEG decoder not loaded";if(!g&&(g=OpenJPEG(),!g||!g._jp2_decode))throw"OpenJPEG failed to initialize"}function f(a,c,f,g){return g=g||{},e(f),g.usePDFJS||f.usePDFJS?b(a,c):d(a,c)}var g;a.decodeJPEG2000=f,a.initializeJPEG2000=e}(cornerstoneWADOImageLoader),function(a){"use strict";function b(a,b){if("undefined"==typeof JpegImage)throw"No JPEG Baseline decoder loaded";var c=new JpegImage;return c.parse(b),c.colorTransform=!1,8===a.bitsAllocated?(a.pixelData=c.getData(a.columns,a.rows),a):16===a.bitsAllocated?(a.pixelData=c.getData16(a.columns,a.rows),a):void 0}a.decodeJPEGBaseline=b}(cornerstoneWADOImageLoader),function(a){function b(a,b){if("undefined"==typeof jpeg||"undefined"==typeof jpeg.lossless||"undefined"==typeof jpeg.lossless.Decoder)throw"No JPEG Lossless decoder loaded";var c=a.bitsAllocated<=8?1:2,d=b.buffer,e=new jpeg.lossless.Decoder,f=e.decode(d,b.byteOffset,b.length,c);return 0===a.pixelRepresentation?16===a.bitsAllocated?(a.pixelData=new Uint16Array(f.buffer),a):(a.pixelData=new Uint8Array(f.buffer),a):(a.pixelData=new Int16Array(f.buffer),a)}a.decodeJPEGLossless=b}(cornerstoneWADOImageLoader),function(a){function b(a,b){var c=e._malloc(a.length);e.writeArrayToMemory(a,c);var d=e._malloc(4),f=e._malloc(4),g=e._malloc(4),h=e._malloc(4),i=e._malloc(4),j=e._malloc(4),k=e._malloc(4),l=e._malloc(4),m=e._malloc(4),n=e.ccall("jpegls_decode","number",["number","number","number","number","number","number","number","number","number","number","number"],[c,a.length,d,f,g,h,i,j,l,k,m]),o={result:n,width:e.getValue(g,"i32"),height:e.getValue(h,"i32"),bitsPerSample:e.getValue(i,"i32"),stride:e.getValue(j,"i32"),components:e.getValue(l,"i32"),allowedLossyError:e.getValue(k,"i32"),interleaveMode:e.getValue(m,"i32"),pixelData:void 0},p=e.getValue(d,"*");return o.bitsPerSample<=8?(o.pixelData=new Uint8Array(o.width*o.height*o.components),o.pixelData.set(new Uint8Array(e.HEAP8.buffer,p,o.pixelData.length))):b?(o.pixelData=new Int16Array(o.width*o.height*o.components),o.pixelData.set(new Int16Array(e.HEAP16.buffer,p,o.pixelData.length))):(o.pixelData=new Uint16Array(o.width*o.height*o.components),o.pixelData.set(new Uint16Array(e.HEAP16.buffer,p,o.pixelData.length))),e._free(c),e._free(p),e._free(d),e._free(f),e._free(g),e._free(h),e._free(i),e._free(j),e._free(l),e._free(m),o}function c(){if("undefined"==typeof CharLS)throw"No JPEG-LS decoder loaded";if(!e&&(e=CharLS(),!e||!e._jpegls_decode))throw"JPEG-LS failed to initialize"}function d(a,d){c();var e=b(d,1===a.pixelRepresentation);if(0!==e.result&&6!==e.result)throw"JPEG-LS decoder failed to decode frame (error code "+e.result+")";return a.columns=e.width,a.rows=e.height,a.pixelData=e.pixelData,a}var e;a.decodeJPEGLS=d,a.initializeJPEGLS=c}(cornerstoneWADOImageLoader),function(a){function b(a,b){if(16===a.bitsAllocated){var c=b.buffer,d=b.byteOffset,e=b.length;d%2&&(c=c.slice(d),d=0),0===a.pixelRepresentation?a.pixelData=new Uint16Array(c,d,e/2):a.pixelData=new Int16Array(c,d,e/2)}else 8===a.bitsAllocated&&(a.pixelData=b);return a}a.decodeLittleEndian=b}(cornerstoneWADOImageLoader),function(a){function b(a,b){if(8===a.bitsAllocated)return c(a,b);if(16===a.bitsAllocated)return d(a,b);throw"unsupported pixel format for RLE"}function c(a,b){for(var c=b,d=a.rows*a.columns,e=new ArrayBuffer(d*a.samplesPerPixel),f=new DataView(c.buffer,c.byteOffset),g=new Int8Array(c.buffer,c.byteOffset),h=new Int8Array(e),i=0,j=f.getInt32(0,!0),k=0;j>k;++k){i=k;var l=f.getInt32(4*(k+1),!0),m=f.getInt32(4*(k+2),!0);0===m&&(m=c.length);for(var n=d*j;m>l;){var o=g[l++];if(o>=0&&127>=o)for(var p=0;o+1>p&&n>i;++p)h[i]=g[l++],i+=a.samplesPerPixel;else if(-1>=o&&o>=-127)for(var q=g[l++],r=0;-o+1>r&&n>i;++r)h[i]=q,i+=a.samplesPerPixel}}return a.pixelData=new Uint8Array(e),a}function d(a,b){for(var c=b,d=a.rows*a.columns,e=new ArrayBuffer(d*a.samplesPerPixel*2),f=new DataView(c.buffer,c.byteOffset),g=new Int8Array(c.buffer,c.byteOffset),h=new Int8Array(e),i=f.getInt32(0,!0),j=0;i>j;++j){var k=0,l=0===j?1:0,m=f.getInt32(4*(j+1),!0),n=f.getInt32(4*(j+2),!0);for(0===n&&(n=c.length);n>m;){var o=g[m++];if(o>=0&&127>=o)for(var p=0;o+1>p&&d>k;++p)h[2*k+l]=g[m++],k++;else if(-1>=o&&o>=-127)for(var q=g[m++],r=0;-o+1>r&&d>k;++r)h[2*k+l]=q,k++}}return 0===a.pixelRepresentation?a.pixelData=new Uint16Array(e):a.pixelData=new Int16Array(e),a}a.decodeRLE=b}(cornerstoneWADOImageLoader),function(a){"use strict";function b(a){for(var b,c=a[0],d=a[0],e=a.length,f=1;e>f;f++)b=a[f],c=Math.min(c,b),d=Math.max(d,b);return{min:c,max:d}}a.getMinMax=b}(cornerstoneWADOImageLoader); \ No newline at end of file