Skip to content

Commit

Permalink
feat: Move to world calculation for segmentations instead of canvas
Browse files Browse the repository at this point in the history
  • Loading branch information
sedghi authored and swederik committed Mar 22, 2022
1 parent 2ebabc1 commit 0dca280
Show file tree
Hide file tree
Showing 14 changed files with 362 additions and 156 deletions.
116 changes: 71 additions & 45 deletions packages/cornerstone-tools/src/tools/annotation/EllipticalRoiTool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ import {
resetElementCursor,
hideElementCursor,
} from '../../cursors/elementCursor'
import getWorldWidthAndHeightFromTwoPoints from '../../util/planar/getWorldWidthAndHeightFromTwoPoints'
import getWorldWidthAndHeightFromCorners from '../../util/planar/getWorldWidthAndHeightFromCorners'
import { ToolSpecificToolData, Point3, Point2 } from '../../types'
import triggerAnnotationRenderForViewportUIDs from '../../util/triggerAnnotationRenderForViewportUIDs'
import pointInShapeCallback from '../../util/planar/pointInShapeCallback'
Expand Down Expand Up @@ -278,8 +278,14 @@ export default class EllipticalRoiTool extends BaseAnnotationTool {
height: Math.abs(canvasPoint1[1] - canvasPoint2[1]) + proximity,
}

const pointInMinorEllipse = pointInEllipse(minorEllipse, canvasCoords)
const pointInMajorEllipse = pointInEllipse(majorEllipse, canvasCoords)
const pointInMinorEllipse = this._pointInEllipseCanvas(
minorEllipse,
canvasCoords
)
const pointInMajorEllipse = this._pointInEllipseCanvas(
majorEllipse,
canvasCoords
)

if (pointInMajorEllipse && !pointInMinorEllipse) {
return true
Expand Down Expand Up @@ -934,25 +940,15 @@ export default class EllipticalRoiTool extends BaseAnnotationTool {
const { viewportUID, renderingEngineUID, sceneUID } = enabledElement

const { points } = data.handles
const { viewPlaneNormal, viewUp } = viewport.getCamera()

const canvasCoordinates = points.map((p) => viewport.worldToCanvas(p))

const canvasCorners = <Array<Point2>>(
const [topLeftCanvas, bottomRightCanvas] = <Array<Point2>>(
getCanvasEllipseCorners(canvasCoordinates)
)
const [canvasPoint1, canvasPoint2] = canvasCorners

const ellipse = {
left: Math.min(canvasPoint1[0], canvasPoint2[0]),
// todo: which top is minimum of y for points?
top: Math.min(canvasPoint1[1], canvasPoint2[1]),
width: Math.abs(canvasPoint1[0] - canvasPoint2[0]),
height: Math.abs(canvasPoint1[1] - canvasPoint2[1]),
}

const worldPos1 = viewport.canvasToWorld(canvasPoint1)
const worldPos2 = viewport.canvasToWorld(canvasPoint2)
const topLeftWorld = viewport.canvasToWorld(topLeftCanvas)
const bottomRightWorld = viewport.canvasToWorld(bottomRightCanvas)
const { cachedStats } = data

const targetUIDs = Object.keys(cachedStats)
Expand Down Expand Up @@ -993,26 +989,38 @@ export default class EllipticalRoiTool extends BaseAnnotationTool {
const kMin = Math.min(worldPos1Index[2], worldPos2Index[2])
const kMax = Math.max(worldPos1Index[2], worldPos2Index[2])

const { worldWidth, worldHeight } = getWorldWidthAndHeightFromTwoPoints(
viewPlaneNormal,
viewUp,
worldPos1,
worldPos2
)
const isEmptyArea = worldWidth === 0 && worldHeight === 0
const area = Math.PI * (worldWidth / 2) * (worldHeight / 2)
const boundsIJK = [
[iMin, iMax],
[jMin, jMax],
[kMin, kMax],
] as [Point2, Point2, Point2]

let isEmptyArea = false
if (boundsIJK.every(([min, max]) => min !== max)) {
isEmptyArea = true
}

const center = [
(topLeftWorld[0] + bottomRightWorld[0]) / 2,
(topLeftWorld[1] + bottomRightWorld[1]) / 2,
(topLeftWorld[2] + bottomRightWorld[2]) / 2,
] as Point3

const ellipseObj = {
center,
xRadius: Math.abs(topLeftWorld[0] - bottomRightWorld[0]) / 2,
yRadius: Math.abs(topLeftWorld[1] - bottomRightWorld[1]) / 2,
zRadius: Math.abs(topLeftWorld[2] - bottomRightWorld[2]) / 2,
}

const area = Math.PI * ellipseObj.xRadius * ellipseObj.yRadius

let count = 0
let mean = 0
let stdDev = 0
let max = -Infinity

const meanMaxCalculator = (
canvasCoords,
ijkCoords,
index,
newValue
) => {
const meanMaxCalculator = ({ value: newValue }) => {
if (newValue > max) {
max = newValue
}
Expand Down Expand Up @@ -1079,38 +1087,28 @@ export default class EllipticalRoiTool extends BaseAnnotationTool {
}

pointInShapeCallback(
[
[iMin, iMax],
[jMin, jMax],
[kMin, kMax],
],
viewport.worldToCanvas,
boundsIJK,
scalarData,
imageData,
dimensions,
(canvasCoords) => pointInEllipse(ellipse, canvasCoords),
(pointLPS, pointIJK) => pointInEllipse(ellipseObj, pointLPS),
meanMaxCalculator
)

mean /= count

const stdCalculator = (canvasCoords, ijkCoords, index, value) => {
const stdCalculator = ({ value }) => {
const valueMinusMean = value - mean

stdDev += valueMinusMean * valueMinusMean
}

pointInShapeCallback(
[
[iMin, iMax],
[jMin, jMax],
[kMin, kMax],
],
viewport.worldToCanvas,
boundsIJK,
scalarData,
imageData,
dimensions,
(canvasCoords) => pointInEllipse(ellipse, canvasCoords),
(pointLPS, pointIJK) => pointInEllipse(ellipseObj, pointLPS),
stdCalculator
)

Expand Down Expand Up @@ -1162,6 +1160,34 @@ export default class EllipticalRoiTool extends BaseAnnotationTool {
return `stackTarget:${viewport.uid}`
}

/**
* This is a temporary function to use the old ellipse's canvas-based
* calculation for pointNearTool, we should move the the world-based
* calculation to the tool's pointNearTool function.
*
* @param {Object} ellipse
* @param {Array} location
* @returns {Boolean}
*/
_pointInEllipseCanvas(ellipse, location: Point2): boolean {
const xRadius = ellipse.width / 2
const yRadius = ellipse.height / 2

if (xRadius <= 0.0 || yRadius <= 0.0) {
return false
}

const center = [ellipse.left + xRadius, ellipse.top + yRadius]
const normalized = [location[0] - center[0], location[1] - center[1]]

const inEllipse =
(normalized[0] * normalized[0]) / (xRadius * xRadius) +
(normalized[1] * normalized[1]) / (yRadius * yRadius) <=
1.0

return inEllipse
}

_getTargetVolumeUID = (scene) => {
if (this.configuration.volumeUID) {
return this.configuration.volumeUID
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ import { CornerstoneTools3DEvents as EVENTS } from '../../enums'
import { getViewportUIDsWithToolToRender } from '../../util/viewportFilters'
import rectangle from '../../util/math/rectangle'
import { getTextBoxCoordsCanvas } from '../../util/drawing'
import getWorldWidthAndHeightFromTwoPoints from '../../util/planar/getWorldWidthAndHeightFromTwoPoints'
import getWorldWidthAndHeightFromCorners from '../../util/planar/getWorldWidthAndHeightFromCorners'
import { indexWithinDimensions } from '../../util/vtkjs'
import {
resetElementCursor,
Expand Down Expand Up @@ -955,7 +955,7 @@ export default class RectangleRoiTool extends BaseAnnotationTool {
const kMin = Math.min(worldPos1Index[2], worldPos2Index[2])
const kMax = Math.max(worldPos1Index[2], worldPos2Index[2])

const { worldWidth, worldHeight } = getWorldWidthAndHeightFromTwoPoints(
const { worldWidth, worldHeight } = getWorldWidthAndHeightFromCorners(
viewPlaneNormal,
viewUp,
worldPos1,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ function eraseRectangle(
// Since always all points inside the boundsIJK is inside the rectangle...
const pointInShape = () => true

const callback = (canvasCoords, pointIJK, index, value) => {
const callback = ({ value, index }) => {
if (segmentsLocked.includes(value)) {
return
}
Expand All @@ -56,7 +56,6 @@ function eraseRectangle(

pointInShapeCallback(
boundsIJK,
viewport.worldToCanvas,
scalarData,
vtkImageData,
dimensions,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
import { vec3 } from 'gl-matrix'
import {
Point3,
IImageVolume,
IEnabledElement,
} from '@precisionmetrics/cornerstone-render/src/types'
} from '@ohif/cornerstone-render/src/types'

import { vec3 } from 'gl-matrix'
import { getBoundingBoxAroundShape } from '../../../util/segmentation'
import { pointInEllipse } from '../../../util/math/ellipse'
import { getCanvasEllipseCorners } from '../../../util/math/ellipse'
import pointInShapeCallback from '../../../util/planar/pointInShapeCallback'
import triggerLabelmapRender from '../../../util/segmentation/triggerLabelmapRender'
import {
getCanvasEllipseCorners,
pointInEllipse,
} from '../../../util/math/ellipse'
import {
getBoundingBoxAroundShape,
triggerLabelmapRender,
} from '../../../util/segmentation'
import { pointInShapeCallback } from '../../../util/planar'

type OperationData = {
points: any // Todo:fix
Expand Down Expand Up @@ -54,19 +58,20 @@ function fillCircle(
const { vtkImageData, dimensions, scalarData } = labelmapVolume
const { viewport, renderingEngine } = enabledElement

// Average the points to get the center of the ellipse
const center = vec3.fromValues(0, 0, 0)
points.forEach((point) => {
vec3.add(center, center, point)
})
vec3.scale(center, center, 1 / points.length)

const canvasCoordinates = points.map((p) => viewport.worldToCanvas(p))

// 1. From the drawn tool: Get the ellipse (circle) topLeft and bottomRight corners in canvas coordinates
// 1. From the drawn tool: Get the ellipse (circle) topLeft and bottomRight
// corners in canvas coordinates
const [topLeftCanvas, bottomRightCanvas] =
getCanvasEllipseCorners(canvasCoordinates)

const ellipse = {
left: Math.min(topLeftCanvas[0], bottomRightCanvas[0]),
top: Math.min(topLeftCanvas[1], bottomRightCanvas[1]),
width: Math.abs(topLeftCanvas[0] - bottomRightCanvas[0]),
height: Math.abs(topLeftCanvas[1] - bottomRightCanvas[1]),
}

// 2. Find the extent of the ellipse (circle) in IJK index space of the image
const topLeftWorld = viewport.canvasToWorld(topLeftCanvas)
const bottomRightWorld = viewport.canvasToWorld(bottomRightCanvas)
Expand All @@ -82,7 +87,15 @@ function fillCircle(
throw new Error('Oblique segmentation tools are not supported yet')
}

const callback = (canvasCoords, pointIJK, index, value) => {
// using circle as a form of ellipse
const ellipseObj = {
center: center,
xRadius: Math.abs(topLeftWorld[0] - bottomRightWorld[0]) / 2,
yRadius: Math.abs(topLeftWorld[1] - bottomRightWorld[1]) / 2,
zRadius: Math.abs(topLeftWorld[2] - bottomRightWorld[2]) / 2,
}

const callback = ({ value, index }) => {
if (segmentsLocked.includes(value)) {
return
}
Expand All @@ -91,11 +104,10 @@ function fillCircle(

pointInShapeCallback(
boundsIJK,
viewport.worldToCanvas,
scalarData,
vtkImageData,
dimensions,
(canvasCoords) => pointInEllipse(ellipse, canvasCoords),
(pointLPS, pointIJK) => pointInEllipse(ellipseObj, pointLPS),
callback
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ function fillRectangle(
// Since always all points inside the boundsIJK is inside the rectangle...
const pointInRectangle = () => true

const callback = (canvasCoords, pointIJK, index, value) => {
const callback = ({ value, index, pointIJK }) => {
if (segmentsLocked.includes(value)) {
return
}
Expand All @@ -71,7 +71,6 @@ function fillRectangle(

pointInShapeCallback(
boundsIJK,
viewport.worldToCanvas,
scalarData,
vtkImageData,
dimensions,
Expand Down
51 changes: 22 additions & 29 deletions packages/cornerstone-tools/src/util/math/ellipse/pointInEllipse.ts
Original file line number Diff line number Diff line change
@@ -1,40 +1,33 @@
import { Point2 } from '../../../types'
import { vec3 } from 'gl-matrix'
import { Point3 } from '../../../types'

type ellipse = {
left: number
top: number
width: number
height: number
type Ellipse = {
center: Point3 | vec3
xRadius: number
yRadius: number
zRadius: number
}

/**
* @function pointInEllipse Returns true if the `location ` is within the ellipse.
*
* A point is inside the ellipse if x^2/a^2 + y^2/b^2 <= 1,
* Where [x,y] is the coordinate and a and b are the x and y axes of the ellipse.
*
* @param {Object} ellipse Object defining the ellipse.
* @param {Point2} location The location of the point.
* @returns {boolean} True if the point is within the ellipse.
*/
export default function pointInEllipse(
ellipse: ellipse,
location: Point2
ellipse: Ellipse,
pointLPS: Point3
): boolean {
const xRadius = ellipse.width / 2
const yRadius = ellipse.height / 2
const { center: circleCenterWorld, xRadius, yRadius, zRadius } = ellipse
const [x, y, z] = pointLPS
const [x0, y0, z0] = circleCenterWorld

if (xRadius <= 0.0 || yRadius <= 0.0) {
return false
let inside = 0
if (xRadius !== 0) {
inside += ((x - x0) * (x - x0)) / (xRadius * xRadius)
}

const center = [ellipse.left + xRadius, ellipse.top + yRadius]
const normalized = [location[0] - center[0], location[1] - center[1]]
if (yRadius !== 0) {
inside += ((y - y0) * (y - y0)) / (yRadius * yRadius)
}

const inEllipse =
(normalized[0] * normalized[0]) / (xRadius * xRadius) +
(normalized[1] * normalized[1]) / (yRadius * yRadius) <=
1.0
if (zRadius !== 0) {
inside += ((z - z0) * (z - z0)) / (zRadius * zRadius)
}

return inEllipse
return inside <= 1
}
Loading

0 comments on commit 0dca280

Please sign in to comment.