irc_web/.svn/pristine/1c/1c0a067c5290fb4b8739fc6e188...

549 lines
15 KiB
Plaintext

import * as cornerstoneTools from 'cornerstone-tools'
const external = cornerstoneTools.external
// State
const getToolState = cornerstoneTools.getToolState
const toolStyle = cornerstoneTools.toolStyle
const toolColors = cornerstoneTools.toolColors
// Drawing
const getNewContext = cornerstoneTools.import('drawing/getNewContext')
const draw = cornerstoneTools.import('drawing/draw')
const drawHandles = cornerstoneTools.import('drawing/drawHandles')
const drawRect = cornerstoneTools.import('drawing/drawRect')
const drawLinkedTextBox = cornerstoneTools.import('drawing/drawLinkedTextBox')
const setShadow = cornerstoneTools.import('drawing/setShadow')
// Util
const calculateSUV = cornerstoneTools.import('util/calculateSUV')
const getROITextBoxCoords = cornerstoneTools.import('util/getROITextBoxCoords')
const numbersWithCommas = cornerstoneTools.import('util/numbersWithCommas')
const throttle = cornerstoneTools.import('util/throttle')
const rectangleRoiCursor = cornerstoneTools.import('tools/cursors')
const getPixelSpacing = cornerstoneTools.import('util/getPixelSpacing')
const getModule = cornerstoneTools.getModule
/**
* @public
* @class RectangleRoiTool
* @memberof Tools.Annotation
* @classdesc Tool for drawing rectangular regions of interest, and measuring
* the statistics of the enclosed pixels.
* @extends Tools.Base.BaseAnnotationTool
*/
export default class RectangleRoiTool extends cornerstoneTools.RectangleRoiTool {
constructor(props = {}) {
const defaultProps = {
name: 'RectangleRoi',
supportedInteractionTypes: ['Mouse', 'Touch'],
configuration: {
drawHandles: true,
drawHandlesOnHover: false,
hideHandlesIfMoving: false,
renderDashed: false
// showMinMax: false,
// showHounsfieldUnits: true
},
svgCursor: rectangleRoiCursor
}
super(props, defaultProps)
this.throttledUpdateCachedStats = throttle(this.updateCachedStats, 110)
}
createNewMeasurement(eventData) {
const goodEventData =
eventData && eventData.currentPoints && eventData.currentPoints.image
if (!goodEventData) {
console.log(
`required eventData not supplied to tool ${this.name}'s createNewMeasurement`
)
return
}
return {
visible: true,
active: true,
color: undefined,
invalidated: true,
handles: {
start: {
x: eventData.currentPoints.image.x,
y: eventData.currentPoints.image.y,
highlight: true,
active: false
},
end: {
x: eventData.currentPoints.image.x,
y: eventData.currentPoints.image.y,
highlight: true,
active: true
},
initialRotation: eventData.viewport.rotation,
textBox: {
active: false,
hasMoved: false,
movesIndependently: false,
drawnIndependently: true,
allowedOutsideImage: true,
hasBoundingBox: true
}
}
}
}
pointNearTool(element, data, coords, interactionType) {
const hasStartAndEndHandles =
data && data.handles && data.handles.start && data.handles.end
const validParameters = hasStartAndEndHandles
if (!validParameters) {
console.log(
`invalid parameters supplied to tool ${this.name}'s pointNearTool`
)
}
if (!validParameters || data.visible === false) {
return false
}
const distance = interactionType === 'mouse' ? 15 : 25
const startCanvas = external.cornerstone.pixelToCanvas(
element,
data.handles.start
)
const endCanvas = external.cornerstone.pixelToCanvas(
element,
data.handles.end
)
const rect = {
left: Math.min(startCanvas.x, endCanvas.x),
top: Math.min(startCanvas.y, endCanvas.y),
width: Math.abs(startCanvas.x - endCanvas.x),
height: Math.abs(startCanvas.y - endCanvas.y)
}
const distanceToPoint = external.cornerstoneMath.rect.distanceToPoint(
rect,
coords
)
return distanceToPoint < distance
}
updateCachedStats(image, element, data) {
const seriesModule =
external.cornerstone.metaData.get('generalSeriesModule', image.imageId) ||
{}
const modality = seriesModule.modality
const pixelSpacing = getPixelSpacing(image)
const stats = _calculateStats(
image,
element,
data.handles,
modality,
pixelSpacing
)
data.cachedStats = stats
data.invalidated = false
}
renderToolData(evt) {
const toolData = getToolState(evt.currentTarget, this.name)
if (!toolData) {
return
}
const eventData = evt.detail
const { image, element } = eventData
const lineWidth = toolStyle.getToolWidth()
const lineDash = getModule('globalConfiguration').configuration.lineDash
const {
handleRadius,
drawHandlesOnHover,
hideHandlesIfMoving,
renderDashed
} = this.configuration
const context = getNewContext(eventData.canvasContext.canvas)
const { rowPixelSpacing, colPixelSpacing } = getPixelSpacing(image)
// Meta
const seriesModule =
external.cornerstone.metaData.get('generalSeriesModule', image.imageId) ||
{}
// Pixel Spacing
const modality = seriesModule.modality
const hasPixelSpacing = rowPixelSpacing && colPixelSpacing
draw(context, context => {
// If we have tool data for this element - iterate over each set and draw it
for (let i = 0; i < toolData.data.length; i++) {
const data = toolData.data[i]
if (data.visible === false) {
continue
}
// Configure
const color = toolColors.getColorIfActive(data)
const handleOptions = {
color,
handleRadius,
drawHandlesIfActive: drawHandlesOnHover,
hideHandlesIfMoving
}
setShadow(context, this.configuration)
const rectOptions = { color }
if (renderDashed) {
rectOptions.lineDash = lineDash
}
// Draw
drawRect(
context,
element,
data.handles.start,
data.handles.end,
rectOptions,
'pixel',
data.handles.initialRotation
)
if (this.configuration.drawHandles) {
drawHandles(context, eventData, data.handles, handleOptions)
}
// Update textbox stats
if (data.invalidated === true) {
if (data.cachedStats) {
this.throttledUpdateCachedStats(image, element, data)
} else {
this.updateCachedStats(image, element, data)
}
}
// Default to textbox on right side of ROI
if (!data.handles.textBox.hasMoved) {
const defaultCoords = getROITextBoxCoords(
eventData.viewport,
data.handles
)
Object.assign(data.handles.textBox, defaultCoords)
}
const textBoxAnchorPoints = handles =>
_findTextBoxAnchorPoints(handles.start, handles.end)
const textBoxContent = _createTextBoxContent(
context,
image.color,
data.cachedStats,
modality,
hasPixelSpacing,
this.configuration,
data
)
data.unit = _getUnit(modality, this.configuration.showHounsfieldUnits)
drawLinkedTextBox(
context,
element,
data.handles.textBox,
textBoxContent,
data.handles,
textBoxAnchorPoints,
color,
lineWidth,
10,
true
)
}
})
}
}
/**
* TODO: This is the same method (+ GetPixels) for the other ROIs
* TODO: The pixel filtering is the unique bit
*
* @param {*} startHandle
* @param {*} endHandle
* @returns {{ left: number, top: number, width: number, height: number}}
*/
function _getRectangleImageCoordinates(startHandle, endHandle) {
return {
left: Math.min(startHandle.x, endHandle.x),
top: Math.min(startHandle.y, endHandle.y),
width: Math.abs(startHandle.x - endHandle.x),
height: Math.abs(startHandle.y - endHandle.y)
}
}
/**
*
*
* @param {*} image
* @param {*} element
* @param {*} handles
* @param {*} modality
* @param {*} pixelSpacing
* @returns {Object} The Stats object
*/
function _calculateStats(image, element, handles, modality, pixelSpacing) {
// Retrieve the bounds of the rectangle in image coordinates
const roiCoordinates = _getRectangleImageCoordinates(
handles.start,
handles.end
)
// Retrieve the array of pixels that the rectangle bounds cover
const pixels = external.cornerstone.getPixels(
element,
roiCoordinates.left,
roiCoordinates.top,
roiCoordinates.width,
roiCoordinates.height
)
// Calculate the mean & standard deviation from the pixels and the rectangle details
const roiMeanStdDev = _calculateRectangleStats(pixels, roiCoordinates)
let meanStdDevSUV
if (modality === 'PT') {
meanStdDevSUV = {
mean: calculateSUV(image, roiMeanStdDev.mean, true) || 0,
stdDev: calculateSUV(image, roiMeanStdDev.stdDev, true) || 0
}
}
// Calculate the image area from the rectangle dimensions and pixel spacing
const area =
roiCoordinates.width *
(pixelSpacing.colPixelSpacing || 1) *
(roiCoordinates.height * (pixelSpacing.rowPixelSpacing || 1))
const perimeter =
roiCoordinates.width * 2 * (pixelSpacing.colPixelSpacing || 1) +
roiCoordinates.height * 2 * (pixelSpacing.rowPixelSpacing || 1)
return {
area: area || 0,
perimeter,
count: roiMeanStdDev.count || 0,
mean: roiMeanStdDev.mean || 0,
variance: roiMeanStdDev.variance || 0,
stdDev: roiMeanStdDev.stdDev || 0,
min: roiMeanStdDev.min || 0,
max: roiMeanStdDev.max || 0,
meanStdDevSUV
}
}
/**
*
*
* @param {*} sp
* @param {*} rectangle
* @returns {{ count, number, mean: number, variance: number, stdDev: number, min: number, max: number }}
*/
function _calculateRectangleStats(sp, rectangle) {
let sum = 0
let sumSquared = 0
let count = 0
let index = 0
let min = sp ? sp[0] : null
let max = sp ? sp[0] : null
for (let y = rectangle.top; y < rectangle.top + rectangle.height; y++) {
for (let x = rectangle.left; x < rectangle.left + rectangle.width; x++) {
sum += sp[index]
sumSquared += sp[index] * sp[index]
min = Math.min(min, sp[index])
max = Math.max(max, sp[index])
count++ // TODO: Wouldn't this just be sp.length?
index++
}
}
if (count === 0) {
return {
count,
mean: 0.0,
variance: 0.0,
stdDev: 0.0,
min: 0.0,
max: 0.0
}
}
const mean = sum / count
const variance = sumSquared / count - mean * mean
return {
count,
mean,
variance,
stdDev: Math.sqrt(variance),
min,
max
}
}
/**
*
*
* @param {*} startHandle
* @param {*} endHandle
* @returns {Array.<{x: number, y: number}>}
*/
function _findTextBoxAnchorPoints(startHandle, endHandle) {
const { left, top, width, height } = _getRectangleImageCoordinates(
startHandle,
endHandle
)
return [
{
// Top middle point of rectangle
x: left + width / 2,
y: top
},
{
// Left middle point of rectangle
x: left,
y: top + height / 2
},
{
// Bottom middle point of rectangle
x: left + width / 2,
y: top + height
},
{
// Right middle point of rectangle
x: left + width,
y: top + height / 2
}
]
}
/**
*
*
* @param {*} area
* @param {*} hasPixelSpacing
* @returns {string} The formatted label for showing area
*/
// function _formatArea(area, hasPixelSpacing) {
// // This uses Char code 178 for a superscript 2
// const suffix = hasPixelSpacing
// ? ` mm${String.fromCharCode(178)}`
// : ` px${String.fromCharCode(178)}`
// return `Area: ${numbersWithCommas(area.toFixed(2))}${suffix}`
// }
function _getUnit(modality, showHounsfieldUnits) {
return modality === 'CT' && showHounsfieldUnits !== false ? 'HU' : ''
}
/**
* TODO: This is identical to EllipticalROI's same fn
* TODO: We may want to make this a utility for ROIs with these values?
*
* @param {*} context
* @param {*} isColorImage
* @param {*} { area, mean, stdDev, min, max, meanStdDevSUV }
* @param {*} modality
* @param {*} hasPixelSpacing
* @param {*} [options={}]
* @returns {string[]}
*/
function _createTextBoxContent(
context,
isColorImage,
{ area, mean, stdDev, min, max, meanStdDevSUV },
modality,
hasPixelSpacing,
options = {},
data
) {
const showMinMax = options.showMinMax || false
const textLines = []
const otherLines = []
if (!isColorImage) {
const hasStandardUptakeValues = meanStdDevSUV && meanStdDevSUV.mean !== 0
const unit = _getUnit(modality, options.showHounsfieldUnits)
let meanString = `Mean: ${numbersWithCommas(mean.toFixed(2))} ${unit}`
const stdDevString = `Std Dev: ${numbersWithCommas(
stdDev.toFixed(2)
)} ${unit}`
// If this image has SUV values to display, concatenate them to the text line
if (hasStandardUptakeValues) {
const SUVtext = ' SUV: '
const meanSuvString = `${SUVtext}${numbersWithCommas(
meanStdDevSUV.mean.toFixed(2)
)}`
const stdDevSuvString = `${SUVtext}${numbersWithCommas(
meanStdDevSUV.stdDev.toFixed(2)
)}`
const targetStringLength = Math.floor(
context.measureText(`${stdDevString} `).width
)
while (context.measureText(meanString).width < targetStringLength) {
meanString += ' '
}
otherLines.push(`${meanString}${meanSuvString}`)
otherLines.push(`${stdDevString} ${stdDevSuvString}`)
} else {
otherLines.push(`${meanString}`)
otherLines.push(`${stdDevString}`)
}
if (showMinMax) {
let minString = `Min: ${min} ${unit}`
const maxString = `Max: ${max} ${unit}`
const targetStringLength = hasStandardUptakeValues
? Math.floor(context.measureText(`${stdDevString} `).width)
: Math.floor(context.measureText(`${meanString} `).width)
while (context.measureText(minString).width < targetStringLength) {
minString += ' '
}
otherLines.push(`${minString}${maxString}`)
}
}
// textLines.push(_formatArea(area, hasPixelSpacing))
// otherLines.forEach(x => textLines.push(x))
if (data.hasOwnProperty('remark')) {
if (data.hasOwnProperty('status') && data.status) {
textLines.push(`${data.remark}(${data.status})`)
} else {
textLines.push(data.remark)
}
}
return textLines
}