UNPKG

qiprofile

Version:

Quantitative Imaging Profile ( QiPr) web application

316 lines (290 loc) 12.8 kB
define ['angular', 'lodash', 'cornerstone'], (ng, _, cornerstone) -> sliceDisplay = ng.module('qiprofile.slicedisplay', []) sliceDisplay.factory 'SliceDisplay', -> # The Cornerstone loader scheme. LOADER_SCHEME = 'qiprofile' # The image inversion color LUT flips the colors. # TODO - comment these settings. INVERSION_LUT = id: '1' firstValueMapped: 0 numBitsPerEntry: 8 # TODO - get the limit value from wherever it is defined. lut: _.range(255, 0, -1) # The integer data type pattern matcher. INT_DATATYPE_REGEX = /^u?int(\d\d?)$/ # Enable the Cornerstone viewports. # # TODO - this should be done in a directive link which has the # element already. A service should not dip into the view by # searching the page. Similarly, the display functions below # should be part of the directive link. # imageElt = document.getElementById('qi-slice-image') cornerstone.enable(imageElt) # TODO - see slice-display overlay TODO. # overlayElt = document.getElementById('qi-slice-overlay') # cornerstone.enable(overlayElt) # @returns a unique id for the given image imageIdFor = (timeSeries, volumeIndex, sliceIndex) -> # Append the volume and slice numbers to the time series image path. "#{ LOADER_SCHEME }:#{ timeSeries.image.path }/#{ volumeIndex + 1 }/#{ sliceIndex + 1 }" # @returns a unique id for the given overlay slice overlayIdFor = (overlay, sliceIndex) -> # Append the slice number to the overlay title. "#{ LOADER_SCHEME }:#{ overlay.parameterResult.title }/#{ sliceIndex + 1 }" # Selects the [x, y] view of the time series [time, x, y, z] ndarray. imageData = (timeSeries, volumeIndex, sliceIndex) -> # The unique slice image id. imageId = imageIdFor(timeSeries, sliceIndex, volumeIndex) # The image header. header = timeSeries.image.contents.header # The slice image data subarray. data = timeSeries.image.contents.data.pick(sliceIndex, null, null, volumeIndex) # Return the data adapted for Cornerstone. adaptImage(imageId, sliceIndex, header, data) overlayData = (overlay, sliceIndex) -> # The unique slice overlay id. imageId = overlayIdFor(overlay, sliceIndex) # The image header. header = overlay.contents.header # The slice overlay data subarray. # TODO - is the argument order correct? Should slice be first? # Check this before any overlay functionality is committed. data = overlay.contents.data.pick(null, null, sliceIndex) # Return the data adapted for Cornerstone. adaptImage(imageId, sliceIndex, header, data) # @param data the binary image data displayImage = (data) -> cornerstone.displayImage(imageElt, data) # @param data the binary overlay data displayOverlay = (data) -> # The viewport option applies the LUT. opts = modalityLUT: INVERSION_LUT cornerstone.displayImage(overlayElt, data, opts) # Converts the given 2D ndarray to a 1D array. # # @param data the 2D ndarray # @param datumSize the intensity value size in bytes # @returns the {data, min, max} object containing # the flattened 1D data array and the minimum # and maximum values flatten = (data, datumSize) -> colCnt = data.shape[0] rowCnt = data.shape[1] length = colCnt * rowCnt # Determine the min and max during the iteration. minValue = Math.pow(2, 15) - 1 maxValue = -minValue # The 1D array content. buffer = new ArrayBuffer(length * datumSize) # Flatten the ndarray. shape = [data.shape.reduce(_.multiply)] stride = data.stride[..0] flat = ndarray(data.data, shape=shape, stride=stride, offset=data.offset) # Alias the size property as length. Object.defineProperties flat, length: get: -> @size # Add a map function. flat.map = (fn) -> (fn(@get(i), i, this) for i in [0...@length]) # TODO - calculate the min/max in pipeline. for i in [0...flat.length] minValue = Math.min(minValue, flat.get(i)) maxValue = Math.max(maxValue, flat.get(i)) # Return the {data, min, max} object data: flat min: minValue max: maxValue # @param imageId the caching id # @param sliceIndex the zero-based slice index # @param header the parsed {nifti, dicom} object # @param data the NIfTI image byte array # @returns the Cornerstone image object adaptImage = (imageId, sliceIndex, header, data) -> # The datum size in bytes. match = INT_DATATYPE_REGEX.exec(header.nifti.datatype) if match? datumSize = parseInt(match[1]) else if header.nifti.datatype is 'float' datumSize = 4 else if header.nifti.datatype is 'double' datumSize = 8 else throw new Error("The NIfTI datatype is not recognized:" + " #{ header.nifti.datatype }") # The 1D intensity array. flat = flatten(data, datumSize) if not header.dicom? throw new Error("The NIfTI file is missing the embedded" + " DICOM meta-data extension") if not header.dicom.WindowCenter? throw new Error("The image is missing an embedded DICOM meta-data" + " WindowCenter tag") windowCenter = header.dicom.WindowCenter[sliceIndex] if not header.dicom.WindowWidth? throw new Error("The image is missing an embedded DICOM meta-data" + " WindowWidth tag") windowWidth = header.dicom.WindowWidth[sliceIndex] slope = header.nifti.scl_slope intercept = header.nifti.scl_inter # The columns and rows are the NIfTI shape first and # second items, resp. colCnt = data.shape[0] rowCnt = data.shape[1] # The column/row pixel spacing is the NRRD spacings # second and third items, resp. spacing = column: header.nrrd.spacings[1] row: header.nrrd.spacings[2] # The number of bytes in the image data. byteCnt = colCnt * rowCnt * datumSize imageId: imageId minPixelValue : flat.min maxPixelValue : flat.max slope: slope intercept: intercept windowCenter : windowCenter windowWidth : windowWidth render: cornerstone.renderGrayscaleImage getPixelData: -> flat.data columns: colCnt rows: rowCnt width: colCnt height: rowCnt color: false columnPixelSpacing: spacing.column rowPixelSpacing: spacing.row sizeInBytes: byteCnt # @param imageId the image id built by the imageIdFor function # @returns the image hierarchy array parseImageId = (imageId) -> path = imageId.split('/') [project, subject, session, scan] = path[...4] rest = path[4..] # The Cornerstone image loader callback function. loadImage = (imageId) -> parseImageId(imageId) cornerstone.registerImageLoader(LOADER_SCHEME, loadImage) # Displays the slice image and overlay. The input *data* argument # is a {image, overlay} object, where: # * *image* is the required 4D time series image [x, y, z, t] # intensity array # * *overlay* is the optional 3D [x, y, z] overlay array # # The overlay can be a binary mask, e.g. ROI, or a scalar modeling # result, e.g. Ktrans. #define ['angular', 'stacktrace'], (ng, stacktrace) -> # Print the full error message in an alert box. This is a work-around # for Chrome bug 331971 # (cf. https://code.google.com/p/chromium/issues/detail?id=331971) # which truncates long error messages. This code is copied from # http://stackoverflow.com/a/22218280/674326. # # TODO - The handler breaks with error: # sourceMappingURL not found # Uncomment, induce an error and fix. See following TODO and FIXME items. # # window.onerror = (errorMsg, url, lineNumber, columnNumber, errorObject) -> # # Check the errorObject as IE and FF don't pass it through (yet). # if errorObject? # errMsg = errorObject.message # else # errMsg = errorMsg # alert('Error: ' + errMsg) # # # The error handler module. # # # # Note: testing this module is prohibitively difficult. Suggestions for # # disabling the Mocha error trap don't work. ngMockE2E could be used to # # build a pseudo-app and HTML test harness, but that is not worth the # # trouble. The work-around to test this module is to introduce an error # # in the qiprofile code and check that it is logged on the server. # error = ng.module 'qiprofile.error', [] # # # Augment the Angular exception handler to print an error on both # # the console (the default behavior) and the server. # error.factory '$exceptionHandler', ['$log', '$window', ($log, $window) -> # (exception, cause) -> # # Print the error on the console. # $log.error.apply($log, arguments) # # Send the error to the server. # # TODO - given the FIXMEs below, revisit this module, look for good # # usage examples and either fix or kill it. # try # message = "#{ exception }" # # FIXME - stacktrace.fromError returns an empty string in # # E2E Session Detail testing when the download.isDisplayed # # function is not found, with the following message: # # Client error: TypeError: download.isDisplayed is not a function. # # Don't know if that is always the case. # # TODO - Induce an error by replacing download.isDisplayed with # # download.fooBar and isolate the problem. # # # # FIXME - after the AngularJS 1.4.9 upgrade, an error fetching # # a REST object results in a JSON parser error which in turn # # causes a secondary stackTrace.fromError error with the following # # message: # # TypeError('Given line number must be a positive integer') # # deep in the obscure stackTrace code. Work-around is to pass # # over the stack trace and handle the failed failure handler # # gracefully. # try # stackTrace = stacktrace.fromError(exception) # catch TypeError # stackTrace = null # # The object to send. # payload = # url: $window.location.href # name: exception.name # message: message # stackTrace: stackTrace # cause: cause or '' # # Send the object to the server. # # Note: can't use the File service, since File depends on # # $exceptionHandler. # xhr = new window.XMLHttpRequest() # xhr.open('POST', '/error', true) # xhr.setRequestHeader('Content-type', 'application/json') # # The XHR request is retransmitted several times. # # Consequently, the error is logged multiple times on the server. # # Prevent this by throttling the send back to once per second # # (which usually means only once). # # # # FIXME - the lodash throttle below has no effect. Why not? # # send = -> xhr.send(ng.toJson(payload)) # # throttled = _.throttle(send, 1000) # # throttled() # # TODO - fix above to replace below and remove the guard in the # # server.coffee /error handler. # xhr.send(ng.toJson(payload)) # catch loggingError # # Can't send the error to the server; # # Log to the client only. # $log.warn 'Error logging failed' # $log.log loggingError # ] # @param timeSeries the 4D TimeSeries object to display # @param volume the one-based volume number # @param slice the one-based slice number display: (timeSeries, volume, slice) -> displayLoaded = -> # Display the image. imgData = imageData(timeSeries, volume - 1, slice - 1) displayImage(imgData) # If there is an overlay, then display it on top of the image. if timeSeries.overlay? ovlData = overlayData(timeSeries.overlay, slice - 1) displayOverlay(ovlData) # Load the image, if necessary. # TODO - cache the image in Cornerstone. Register a loader which # calls imageData by parsing the image id. if not timeSeries.image.isLoaded() timeSeries.image.load().then -> # If there is a load callback, then call it. if timeSeries.image.loader.callback? timeSeries.image.loader.callback() displayLoaded() else displayLoaded()