export default function (image, storedPixelValue) { const patientStudyModule = cornerstone.metaData.get('patientStudyModule', image.imageId); const seriesModule = cornerstone.metaData.get('generalSeriesModule', image.imageId); if (!patientStudyModule || !seriesModule) { return; } const modality = seriesModule.modality; // Image must be PET if (modality !== 'PT') { return; } const modalityPixelValue = storedPixelValue * image.slope + image.intercept; const patientWeight = patientStudyModule.patientWeight; // In kg if (!patientWeight) { return; } const petSequenceModule = cornerstone.metaData.get('petIsotopeModule', image.imageId); if (!petSequenceModule) { return; } const radiopharmaceuticalInfo = petSequenceModule.radiopharmaceuticalInfo; const startTime = radiopharmaceuticalInfo.radiopharmaceuticalStartTime; const totalDose = radiopharmaceuticalInfo.radionuclideTotalDose; const halfLife = radiopharmaceuticalInfo.radionuclideHalfLife; const seriesAcquisitionTime = seriesModule.seriesTime; if (!startTime || !totalDose || !halfLife || !seriesAcquisitionTime) { return; } const acquisitionTimeInSeconds = fracToDec(seriesAcquisitionTime.fractionalSeconds || 0) + seriesAcquisitionTime.seconds + seriesAcquisitionTime.minutes * 60 + seriesAcquisitionTime.hours * 60 * 60; const injectionStartTimeInSeconds = fracToDec(startTime.fractionalSeconds) + startTime.seconds + startTime.minutes * 60 + startTime.hours * 60 * 60; const durationInSeconds = acquisitionTimeInSeconds - injectionStartTimeInSeconds; const correctedDose = totalDose * Math.exp(-durationInSeconds * Math.log(2) / halfLife); const suv = modalityPixelValue * patientWeight / correctedDose * 1000; return suv; }
function getCompression(imageId) { const generalImageModule = cornerstone.metaData.get('generalImageModule', imageId) || {}; const { lossyImageCompression, lossyImageCompressionRatio, lossyImageCompressionMethod } = generalImageModule; if (lossyImageCompression === '01' && lossyImageCompressionRatio !== '') { const compressionMethod = lossyImageCompressionMethod || 'Lossy: '; const compressionRatio = formatNumberPrecision( lossyImageCompressionRatio, 2 ); return compressionMethod + compressionRatio + ' : 1'; } return 'Lossless / Uncompressed'; }
function getImageFrame (imageId) { const imagePixelModule = cornerstone.metaData.get('imagePixelModule', imageId); return { samplesPerPixel: imagePixelModule.samplesPerPixel, photometricInterpretation: imagePixelModule.photometricInterpretation, planarConfiguration: imagePixelModule.planarConfiguration, rows: imagePixelModule.rows, columns: imagePixelModule.columns, bitsAllocated: imagePixelModule.bitsAllocated, pixelRepresentation: imagePixelModule.pixelRepresentation, // 0 = unsigned, smallestPixelValue: imagePixelModule.smallestPixelValue, largestPixelValue: imagePixelModule.largestPixelValue, redPaletteColorLookupTableDescriptor: imagePixelModule.redPaletteColorLookupTableDescriptor, greenPaletteColorLookupTableDescriptor: imagePixelModule.greenPaletteColorLookupTableDescriptor, bluePaletteColorLookupTableDescriptor: imagePixelModule.bluePaletteColorLookupTableDescriptor, redPaletteColorLookupTableData: imagePixelModule.redPaletteColorLookupTableData, greenPaletteColorLookupTableData: imagePixelModule.greenPaletteColorLookupTableData, bluePaletteColorLookupTableData: imagePixelModule.bluePaletteColorLookupTableData, pixelData: undefined // populated later after decoding }; }
function onImageRendered (e, eventData) { // If we have no toolData for this element, return immediately as there is nothing to do const toolData = getToolState(e.currentTarget, toolType); if (!toolData) { return; } const image = eventData.image; const element = eventData.element; const lineWidth = toolStyle.getToolWidth(); const config = ellipticalRoi.getConfiguration(); const context = eventData.canvasContext.canvas.getContext('2d'); const seriesModule = cornerstone.metaData.get('generalSeriesModule', image.imageId); let modality; if (seriesModule) { modality = seriesModule.modality; } context.setTransform(1, 0, 0, 1, 0, 0); // If we have tool data for this element - iterate over each set and draw it for (let i = 0; i < toolData.data.length; i++) { context.save(); const data = toolData.data[i]; // Apply any shadow settings defined in the tool configuration if (config && config.shadow) { context.shadowColor = config.shadowColor || '#000000'; context.shadowOffsetX = config.shadowOffsetX || 1; context.shadowOffsetY = config.shadowOffsetY || 1; } // Check which color the rendered tool should be const color = toolColors.getColorIfActive(data.active); // Convert Image coordinates to Canvas coordinates given the element const handleStartCanvas = cornerstone.pixelToCanvas(element, data.handles.start); const handleEndCanvas = cornerstone.pixelToCanvas(element, data.handles.end); // Retrieve the bounds of the ellipse (left, top, width, and height) // In Canvas coordinates const leftCanvas = Math.min(handleStartCanvas.x, handleEndCanvas.x); const topCanvas = Math.min(handleStartCanvas.y, handleEndCanvas.y); const widthCanvas = Math.abs(handleStartCanvas.x - handleEndCanvas.x); const heightCanvas = Math.abs(handleStartCanvas.y - handleEndCanvas.y); // Draw the ellipse on the canvas context.beginPath(); context.strokeStyle = color; context.lineWidth = lineWidth; drawEllipse(context, leftCanvas, topCanvas, widthCanvas, heightCanvas); context.closePath(); // If the tool configuration specifies to only draw the handles on hover / active, // Follow this logic if (config && config.drawHandlesOnHover) { // Draw the handles if the tool is active if (data.active === true) { drawHandles(context, eventData, data.handles, color); } else { // If the tool is inactive, draw the handles only if each specific handle is being // Hovered over const handleOptions = { drawHandlesIfActive: true }; drawHandles(context, eventData, data.handles, color, handleOptions); } } else { // If the tool has no configuration settings, always draw the handles drawHandles(context, eventData, data.handles, color); } // Define variables for the area and mean/standard deviation let area, meanStdDev, meanStdDevSUV; // Perform a check to see if the tool has been invalidated. This is to prevent // Unnecessary re-calculation of the area, mean, and standard deviation if the // Image is re-rendered but the tool has not moved (e.g. during a zoom) if (data.invalidated === false) { // If the data is not invalidated, retrieve it from the toolData meanStdDev = data.meanStdDev; meanStdDevSUV = data.meanStdDevSUV; area = data.area; } else { // If the data has been invalidated, we need to calculate it again // Retrieve the bounds of the ellipse in image coordinates const ellipse = { 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 // Deviation will be calculated for color images. if (!image.color) { // Retrieve the array of pixels that the ellipse bounds cover const 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 = calculateEllipseStatistics(pixels, ellipse); if (modality === 'PT') { // If the image is from a PET scan, use the DICOM tags to // Calculate the SUV from the mean and standard deviation. // Note that because we are using modality pixel values from getPixels, and // The calculateSUV routine also rescales to modality pixel values, we are first // Returning the values to storedPixel values before calcuating SUV with them. // TODO: Clean this up? Should we add an option to not scale in calculateSUV? meanStdDevSUV = { mean: calculateSUV(image, (meanStdDev.mean - image.intercept) / image.slope), stdDev: calculateSUV(image, (meanStdDev.stdDev - image.intercept) / image.slope) }; } // If the mean and standard deviation values are sane, store them for later retrieval if (meanStdDev && !isNaN(meanStdDev.mean)) { data.meanStdDev = meanStdDev; data.meanStdDevSUV = meanStdDevSUV; } } // Retrieve the pixel spacing values, and if they are not // Real non-zero values, set them to 1 const columnPixelSpacing = image.columnPixelSpacing || 1; const rowPixelSpacing = image.rowPixelSpacing || 1; // Calculate the image area from the ellipse dimensions and pixel spacing area = Math.PI * (ellipse.width * columnPixelSpacing / 2) * (ellipse.height * rowPixelSpacing / 2); // If the area value is sane, store it for later retrieval if (!isNaN(area)) { data.area = area; } // Set the invalidated flag to false so that this data won't automatically be recalculated data.invalidated = false; } // Define an array to store the rows of text for the textbox const textLines = []; // If the mean and standard deviation values are present, display them if (meanStdDev && meanStdDev.mean !== undefined) { // If the modality is CT, add HU to denote Hounsfield Units let moSuffix = ''; if (modality === 'CT') { moSuffix = ' HU'; } // Create a line of text to display the mean and any units that were specified (i.e. HU) let meanText = `Mean: ${numberWithCommas(meanStdDev.mean.toFixed(2))}${moSuffix}`; // Create a line of text to display the standard deviation and any units that were specified (i.e. HU) let stdDevText = `StdDev: ${numberWithCommas(meanStdDev.stdDev.toFixed(2))}${moSuffix}`; // If this image has SUV values to display, concatenate them to the text line if (meanStdDevSUV && meanStdDevSUV.mean !== undefined) { const SUVtext = ' SUV: '; meanText += SUVtext + numberWithCommas(meanStdDevSUV.mean.toFixed(2)); stdDevText += SUVtext + numberWithCommas(meanStdDevSUV.stdDev.toFixed(2)); } // Add these text lines to the array to be displayed in the textbox textLines.push(meanText); textLines.push(stdDevText); } // If the area is a sane value, display it if (area) { // Determine the area suffix based on the pixel spacing in the image. // If pixel spacing is present, use millimeters. Otherwise, use pixels. // This uses Char code 178 for a superscript 2 let suffix = ` mm${String.fromCharCode(178)}`; if (!image.rowPixelSpacing || !image.columnPixelSpacing) { suffix = ` pixels${String.fromCharCode(178)}`; } // Create a line of text to display the area and its units const areaText = `Area: ${numberWithCommas(area.toFixed(2))}${suffix}`; // Add this text line to the array to be displayed in the textbox textLines.push(areaText); } // If the textbox has not been moved by the user, it should be displayed on the right-most // Side of the tool. if (!data.handles.textBox.hasMoved) { // Find the rightmost side of the ellipse at its vertical center, and place the textbox here // Note that this calculates it in image coordinates data.handles.textBox.x = Math.max(data.handles.start.x, data.handles.end.x); data.handles.textBox.y = (data.handles.start.y + data.handles.end.y) / 2; } // Convert the textbox Image coordinates into Canvas coordinates const textCoords = cornerstone.pixelToCanvas(element, data.handles.textBox); // Set options for the textbox drawing function const options = { centering: { x: false, y: true } }; // Draw the textbox and retrieves it's bounding box for mouse-dragging and highlighting const boundingBox = drawTextBox(context, textLines, textCoords.x, textCoords.y, color, options); // Store the bounding box data in the handle for mouse-dragging and highlighting data.handles.textBox.boundingBox = boundingBox; // If the textbox has moved, we would like to draw a line linking it with the tool // This section decides where to draw this line to on the Ellipse based on the location // Of the textbox relative to the ellipse. if (data.handles.textBox.hasMoved) { // Draw dashed link line between tool and text // The initial link position is at the center of the // Textbox. const link = { start: {}, end: { x: textCoords.x, y: textCoords.y } }; // First we calculate the ellipse points (top, left, right, and bottom) const ellipsePoints = [{ // Top middle point of ellipse x: leftCanvas + widthCanvas / 2, y: topCanvas }, { // Left middle point of ellipse x: leftCanvas, y: topCanvas + heightCanvas / 2 }, { // Bottom middle point of ellipse x: leftCanvas + widthCanvas / 2, y: topCanvas + heightCanvas }, { // Right middle point of ellipse x: leftCanvas + widthCanvas, y: topCanvas + heightCanvas / 2 }]; // We obtain the link starting point by finding the closest point on the ellipse to the // Center of the textbox link.start = cornerstoneMath.point.findClosestPoint(ellipsePoints, link.end); // Next we calculate the corners of the textbox bounding box const 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 }]; // Now we recalculate the link endpoint by identifying which corner of the bounding box // Is closest to the start point we just calculated. link.end = cornerstoneMath.point.findClosestPoint(boundingBoxPoints, link.start); // Finally we draw the dashed linking line context.beginPath(); context.strokeStyle = color; context.lineWidth = lineWidth; context.setLineDash([2, 3]); context.moveTo(link.start.x, link.start.y); context.lineTo(link.end.x, link.end.y); context.stroke(); } context.restore(); } }
function minimalStrategy (eventData) { const element = eventData.element; const enabledElement = cornerstone.getEnabledElement(element); const image = enabledElement.image; const context = enabledElement.canvas.getContext('2d'); context.setTransform(1, 0, 0, 1, 0, 0); const color = toolColors.getActiveColor(); const font = textStyle.getFont(); const config = dragProbe.getConfiguration(); context.save(); if (config && config.shadow) { context.shadowColor = config.shadowColor || '#000000'; context.shadowOffsetX = config.shadowOffsetX || 1; context.shadowOffsetY = config.shadowOffsetY || 1; } const seriesModule = cornerstone.metaData.get('generalSeriesModule', image.imageId); let modality; if (seriesModule) { modality = seriesModule.modality; } let toolCoords; if (eventData.isTouchEvent === true) { toolCoords = cornerstone.pageToPixel(element, eventData.currentPoints.page.x, eventData.currentPoints.page.y - textStyle.getFontSize() * 4); } else { toolCoords = cornerstone.pageToPixel(element, eventData.currentPoints.page.x, eventData.currentPoints.page.y - textStyle.getFontSize() / 2); } let storedPixels; let text = ''; if (toolCoords.x < 0 || toolCoords.y < 0 || toolCoords.x >= image.columns || toolCoords.y >= image.rows) { return; } if (image.color) { storedPixels = getRGBPixels(element, toolCoords.x, toolCoords.y, 1, 1); text = `R: ${storedPixels[0]} G: ${storedPixels[1]} B: ${storedPixels[2]}`; } else { storedPixels = cornerstone.getStoredPixels(element, toolCoords.x, toolCoords.y, 1, 1); const sp = storedPixels[0]; const mo = sp * eventData.image.slope + eventData.image.intercept; const modalityPixelValueText = parseFloat(mo.toFixed(2)); if (modality === 'CT') { text += `HU: ${modalityPixelValueText}`; } else if (modality === 'PT') { text += modalityPixelValueText; const suv = calculateSUV(eventData.image, sp); if (suv) { text += ` SUV: ${parseFloat(suv.toFixed(2))}`; } } else { text += modalityPixelValueText; } } // Prepare text const textCoords = cornerstone.pixelToCanvas(element, toolCoords); context.font = font; context.fillStyle = color; // Translate the x/y away from the cursor let translation; const handleRadius = 6; const width = context.measureText(text).width; if (eventData.isTouchEvent === true) { translation = { x: -width / 2 - 5, y: -textStyle.getFontSize() - 10 - 2 * handleRadius }; } else { translation = { x: 12, y: -(textStyle.getFontSize() + 10) / 2 }; } context.beginPath(); context.strokeStyle = color; context.arc(textCoords.x, textCoords.y, handleRadius, 0, 2 * Math.PI); context.stroke(); drawTextBox(context, text, textCoords.x + translation.x, textCoords.y + translation.y, color); context.restore(); }
render() { const imageId = this.props.imageId; if (!imageId) { return null; } const zoom = this.props.viewport.scale * 100; const seriesMetadata = cornerstone.metaData.get('generalSeriesModule', imageId) || {}; const imagePlaneModule = cornerstone.metaData.get('imagePlaneModule', imageId) || {}; const { rows, columns, sliceThickness, sliceLocation } = imagePlaneModule; const { seriesNumber, seriesDescription } = seriesMetadata; const generalStudyModule = cornerstone.metaData.get('generalStudyModule', imageId) || {}; const { studyDate, studyTime, studyDescription } = generalStudyModule; const patientModule = cornerstone.metaData.get('patientModule', imageId) || {}; const { patientId, patientName } = patientModule; const generalImageModule = cornerstone.metaData.get('generalImageModule', imageId) || {}; const { instanceNumber } = generalImageModule; const cineModule = cornerstone.metaData.get('cineModule', imageId) || {}; const { frameTime } = cineModule; const frameRate = formatNumberPrecision(1000 / frameTime, 1); const compression = getCompression(imageId); const windowWidth = this.props.viewport.voi.windowWidth || 0; const windowCenter = this.props.viewport.voi.windowCenter || 0; const wwwc = `W: ${windowWidth.toFixed(0)} L: ${windowCenter.toFixed(0)}`; const { imageIds } = this.props.stack; const imageIndex = imageIds.indexOf(this.props.imageId) + 1; const numImages = imageIds.length; const imageDimensions = `${columns} x ${rows}`; const normal = ( <React.Fragment> <div className="top-left overlay-element"> <div>{formatPN(patientName)}</div> <div>{patientId}</div> </div> <div className="top-right overlay-element"> <div>{studyDescription}</div> <div> {formatDA(studyDate)} {formatTM(studyTime)} </div> </div> <div className="bottom-right overlay-element"> <div>Zoom: {formatNumberPrecision(zoom, 0)}%</div> <div>{wwwc}</div> <div className="compressionIndicator">{compression}</div> </div> <div className="bottom-left overlay-element"> <div>{seriesNumber >= 0 ? `Ser: ${seriesNumber}` : ''}</div> <div> {numImages > 1 ? `Img: ${instanceNumber} ${imageIndex}/${numImages}` : ''} </div> <div> {frameRate >= 0 ? `${formatNumberPrecision(frameRate, 2)} FPS` : ''} <div>{imageDimensions}</div> <div> {isValidNumber(sliceLocation) ? `Loc: ${formatNumberPrecision(sliceLocation, 2)} mm ` : ''} {sliceThickness ? `Thick: ${formatNumberPrecision(sliceThickness, 2)} mm` : ''} </div> <div>{seriesDescription}</div> </div> </div> </React.Fragment> ); const rightOnly = ( <React.Fragment> <div className="top-right overlay-element"> <div>{formatPN(patientName)}</div> <div>{patientId}</div> <div>{studyDescription}</div> <div> {formatDA(studyDate)} {formatTM(studyTime)} </div> </div> <div className="bottom-right overlay-element"> <div>{seriesNumber >= 0 ? `Ser: ${seriesNumber}` : ''}</div> <div> {numImages > 1 ? `Img: ${instanceNumber} ${imageIndex}/${numImages}` : ''} </div> <div> {frameRate >= 0 ? `${formatNumberPrecision(frameRate, 2)} FPS` : ''} </div> <div>{imageDimensions}</div> <div>{seriesDescription}</div> <div>Zoom: {formatNumberPrecision(zoom, 0)}%</div> <div className="compressionIndicator">{compression}</div> <div>{wwwc}</div> </div> </React.Fragment> ); const leftOnly = ( <React.Fragment> <div className="top-left overlay-element"> <div>{formatPN(patientName)}</div> <div>{patientId}</div> <div>{studyDescription}</div> <div> {formatDA(studyDate)} {formatTM(studyTime)} </div> </div> <div className="bottom-left overlay-element"> <div>{seriesNumber >= 0 ? `Ser: ${seriesNumber}` : ''}</div> <div> {numImages > 1 ? `Img: ${instanceNumber} ${imageIndex}/${numImages}` : ''} </div> <div> {frameRate >= 0 ? `${formatNumberPrecision(frameRate, 2)} FPS` : ''} </div> <div>{imageDimensions}</div> <div>{seriesDescription}</div> <div>Zoom: {formatNumberPrecision(zoom, 0)}%</div> <div className="compressionIndicator">{compression}</div> <div>{wwwc}</div> </div> </React.Fragment> ); return <div className="ViewportOverlay">{normal}</div>; }
// Renders the active reference line export default function (context, eventData, targetElement, referenceElement) { const targetImage = cornerstone.getEnabledElement(targetElement).image; const referenceImage = cornerstone.getEnabledElement(referenceElement).image; // Make sure the images are actually loaded for the target and reference if (!targetImage || !referenceImage) { return; } const targetImagePlane = cornerstone.metaData.get('imagePlane', targetImage.imageId); const referenceImagePlane = cornerstone.metaData.get('imagePlane', referenceImage.imageId); // Make sure the target and reference actually have image plane metadata if (!targetImagePlane || !referenceImagePlane || !targetImagePlane.rowCosines || !targetImagePlane.columnCosines || !targetImagePlane.imagePositionPatient || !referenceImagePlane.rowCosines || !referenceImagePlane.columnCosines || !referenceImagePlane.imagePositionPatient) { return; } // The image planes must be in the same frame of reference if (targetImagePlane.frameOfReferenceUID !== referenceImagePlane.frameOfReferenceUID) { return; } // The image plane normals must be > 30 degrees apart const targetNormal = targetImagePlane.rowCosines.clone().cross(targetImagePlane.columnCosines); const referenceNormal = referenceImagePlane.rowCosines.clone().cross(referenceImagePlane.columnCosines); let angleInRadians = targetNormal.angleTo(referenceNormal); angleInRadians = Math.abs(angleInRadians); if (angleInRadians < 0.5) { // 0.5 radians = ~30 degrees return; } const referenceLine = calculateReferenceLine(targetImagePlane, referenceImagePlane); if (!referenceLine) { return; } const refLineStartCanvas = cornerstone.pixelToCanvas(eventData.element, referenceLine.start); const refLineEndCanvas = cornerstone.pixelToCanvas(eventData.element, referenceLine.end); const color = toolColors.getActiveColor(); const lineWidth = toolStyle.getToolWidth(); // Draw the referenceLines context.setTransform(1, 0, 0, 1, 0, 0); context.save(); context.beginPath(); context.strokeStyle = color; context.lineWidth = lineWidth; context.moveTo(refLineStartCanvas.x, refLineStartCanvas.y); context.lineTo(refLineEndCanvas.x, refLineEndCanvas.y); context.stroke(); context.restore(); }
sopClassUID: dataSet.string('x00080016'), sopInstanceUID: dataSet.string('x00080018') }; } if (type === 'petIsotopeModule') { const radiopharmaceuticalInfo = dataSet.elements.x00540016; if (radiopharmaceuticalInfo === undefined) { return; } const firstRadiopharmaceuticalInfoDataSet = radiopharmaceuticalInfo.items[0].dataSet; return { radiopharmaceuticalInfo: { radiopharmaceuticalStartTime: dicomParser.parseTM(firstRadiopharmaceuticalInfoDataSet.string('x00181072') || ''), radionuclideTotalDose: firstRadiopharmaceuticalInfoDataSet.floatString('x00181074'), radionuclideHalfLife: firstRadiopharmaceuticalInfoDataSet.floatString('x00181075') } }; } } // register our metadata provider cornerstone.metaData.addProvider(metaDataProvider); export default metaDataProvider;