十字线功能优化;MR图像显示默认使用图像的窗宽窗位;
parent
a9d62cf28d
commit
6af2f0555e
|
|
@ -0,0 +1 @@
|
|||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1779246817161" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5348" width="200" height="200" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M473 71a8 8 0 0 1 8-8h64a8 8 0 0 1 8 8v77.161C722.848 166.622 857.812 301.289 876.729 471H952a8 8 0 0 1 8 8v64a8 8 0 0 1-8 8h-75.053C858.871 721.652 723.515 857.305 553 875.839V951a8 8 0 0 1-8 8h-64a8 8 0 0 1-8-8v-75.161C302.818 857.341 167.659 722.182 149.161 552H72a8 8 0 0 1-8-8v-64a8 8 0 0 1 8-8h77.161C167.659 301.818 302.818 166.659 473 148.161V71z m326 441c0-157.953-128.047-286-286-286S227 354.047 227 512s128.047 286 286 286 286-128.047 286-286z m-286 60c33.137 0 60-26.863 60-60s-26.863-60-60-60-60 26.863-60 60 26.863 60 60 60z" fill="#ffffff" p-id="5349"></path></svg>
|
||||
|
After Width: | Height: | Size: 913 B |
File diff suppressed because it is too large
Load Diff
|
|
@ -82,6 +82,18 @@
|
|||
<div :style="{ top: sliderInfo.height + '%' }" class="slider" @click.stop.prevent="() => { return }"
|
||||
@mousedown.stop="sliderMousedown($event)" />
|
||||
</div>
|
||||
<div
|
||||
v-if="annotationContextMenu.visible"
|
||||
ref="annotation-context-menu"
|
||||
class="annotation-context-menu"
|
||||
:style="{ left: `${annotationContextMenu.x}px`, top: `${annotationContextMenu.y}px` }"
|
||||
@mousedown.stop
|
||||
@mouseup.stop
|
||||
@click.stop
|
||||
@contextmenu.prevent
|
||||
>
|
||||
<div class="annotation-context-menu__item" @click.stop="copyCircleAnnotation()">复制</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
|
|
@ -98,7 +110,7 @@ import cornerstoneDICOMImageLoader from '@cornerstonejs/dicom-image-loader';
|
|||
import vtkColorMaps from '@kitware/vtk.js/Rendering/Core/ColorTransferFunction/ColorMaps'
|
||||
|
||||
import { createImageIdsAndCacheMetaData } from '@/views/trials/trials-panel/reading/dicoms/components/Fusion/js/createImageIdsAndCacheMetaData'
|
||||
import setCtTransferFunctionForVolumeActor from '@/views/trials/trials-panel/reading/dicoms/components/Fusion/js/setCtTransferFunctionForVolumeActor'
|
||||
import setCtTransferFunctionForVolumeActor, { setCtMappingRange } from '@/views/trials/trials-panel/reading/dicoms/components/Fusion/js/setCtTransferFunctionForVolumeActor'
|
||||
import { setPetColorMapTransferFunctionForVolumeActor } from '@/views/trials/trials-panel/reading/dicoms/components/Fusion/js/setPetColorMapTransferFunctionForVolumeActor'
|
||||
import {
|
||||
setMipTransferFunctionForVolumeActor,
|
||||
|
|
@ -108,6 +120,15 @@ import setNmFusionColorMapTransferFunctionForVolumeActor from './helpers/setNmFu
|
|||
const { BlendModes, OrientationAxis } = Enums;
|
||||
const { getColormap } = csUtils.colormap;
|
||||
import { vec3, mat4 } from 'gl-matrix'
|
||||
const DEFAULT_COLOR_MAP_UPPER = 6
|
||||
|
||||
function getVoiValue(value) {
|
||||
if (Array.isArray(value)) {
|
||||
return Number(value[0])
|
||||
}
|
||||
return Number(value)
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'ImageViewport',
|
||||
props: {
|
||||
|
|
@ -180,7 +201,13 @@ export default {
|
|||
Colorbar: null,
|
||||
fusionOpacity: 0.95,
|
||||
topFusionVolumeActor: null,
|
||||
currentVoiUpper: null
|
||||
currentVoiUpper: null,
|
||||
annotationContextMenu: {
|
||||
visible: false,
|
||||
x: 0,
|
||||
y: 0,
|
||||
annotation: null,
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
|
|
@ -200,7 +227,7 @@ export default {
|
|||
renderingEngine.resize(true, false)
|
||||
}
|
||||
})
|
||||
this.element.oncontextmenu = (e) => e.preventDefault()
|
||||
this.element.oncontextmenu = this.handleElementContextMenu
|
||||
// resizeObserver.observe(this.element)
|
||||
this.element.addEventListener("CORNERSTONE_VOLUME_NEW_IMAGE", this.stackNewImage)
|
||||
this.element.addEventListener('CORNERSTONE_VOI_MODIFIED', this.voiModified)
|
||||
|
|
@ -213,9 +240,303 @@ export default {
|
|||
})
|
||||
document.addEventListener('mouseup', this.handleDocumentMouseUp)
|
||||
document.addEventListener('mousemove', this.handleDocumentMouseMove)
|
||||
document.addEventListener('mousedown', this.handleDocumentMouseDown)
|
||||
// console.log(cornerstoneTools)
|
||||
// element.addEventListener('CORNERSTONE_STACK_NEW_IMAGE', this.stackNewImage)
|
||||
},
|
||||
getViewportInstance() {
|
||||
const renderingEngine = getRenderingEngine(this.renderingEngineId)
|
||||
return renderingEngine?.getViewport(this.viewportId)
|
||||
},
|
||||
hideAnnotationContextMenu() {
|
||||
if (!this.annotationContextMenu.visible) return
|
||||
this.annotationContextMenu.visible = false
|
||||
this.annotationContextMenu.annotation = null
|
||||
},
|
||||
handleDocumentMouseDown(e) {
|
||||
const menu = this.$refs['annotation-context-menu']
|
||||
if (menu && menu.contains(e.target)) return
|
||||
this.hideAnnotationContextMenu()
|
||||
},
|
||||
handleElementContextMenu(e) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
|
||||
// const annotation = this.getCircleAnnotationFromContextMenuEvent(e)
|
||||
// if (!annotation) {
|
||||
// this.hideAnnotationContextMenu()
|
||||
// return false
|
||||
// }
|
||||
|
||||
// if (cornerstoneTools.annotation.locking.isAnnotationLocked(annotation.annotationUID)) {
|
||||
// this.hideAnnotationContextMenu()
|
||||
// return false
|
||||
// }
|
||||
|
||||
// this.$emit('activeViewport', this.viewportIndex)
|
||||
|
||||
// const { x, y } = this.getContextMenuPosition(e)
|
||||
// this.annotationContextMenu.visible = true
|
||||
// this.annotationContextMenu.x = x
|
||||
// this.annotationContextMenu.y = y
|
||||
// this.annotationContextMenu.annotation = annotation
|
||||
// return false
|
||||
},
|
||||
getContextMenuPosition(e) {
|
||||
const rect = this.element?.getBoundingClientRect?.()
|
||||
if (!rect) {
|
||||
return { x: 0, y: 0 }
|
||||
}
|
||||
|
||||
const menuWidth = 128
|
||||
const menuHeight = 40
|
||||
const padding = 8
|
||||
const maxX = Math.max(padding, rect.width - menuWidth - padding)
|
||||
const maxY = Math.max(padding, rect.height - menuHeight - padding)
|
||||
|
||||
return {
|
||||
x: Math.min(Math.max(e.clientX - rect.left, padding), maxX),
|
||||
y: Math.min(Math.max(e.clientY - rect.top, padding), maxY),
|
||||
}
|
||||
},
|
||||
getCircleAnnotationFromContextMenuEvent(e) {
|
||||
const viewport = this.getViewportInstance()
|
||||
if (!viewport || !this.element) return null
|
||||
|
||||
const rect = this.element.getBoundingClientRect()
|
||||
const canvasPoint = [e.clientX - rect.left, e.clientY - rect.top]
|
||||
const currentVisitTaskId = this.series?.TaskInfo?.VisitTaskId
|
||||
const toolNames = ['CircleROI'] //FixedRadiusCircleROI看后期是不是要补充
|
||||
const candidates = []
|
||||
|
||||
toolNames.forEach((toolName) => {
|
||||
const annotations = cornerstoneTools.annotation.state.getAnnotations(toolName, this.element) || []
|
||||
annotations.forEach((annotation) => {
|
||||
if (!annotation?.data?.handles?.points?.length) return
|
||||
if (currentVisitTaskId && annotation.visitTaskId && annotation.visitTaskId !== currentVisitTaskId) return
|
||||
|
||||
const hitInfo = this.getCircleAnnotationHitInfo(annotation, canvasPoint, viewport)
|
||||
if (!hitInfo.hit || hitInfo.score === null) return
|
||||
|
||||
candidates.push({
|
||||
annotation,
|
||||
score: hitInfo.score,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
if (!candidates.length) return null
|
||||
|
||||
candidates.sort((a, b) => a.score - b.score)
|
||||
return candidates[0].annotation
|
||||
},
|
||||
getCircleAnnotationHitInfo(annotation, canvasPoint, viewport) {
|
||||
const points = annotation?.data?.handles?.points || []
|
||||
if (points.length < 2) {
|
||||
return { hit: false, score: null }
|
||||
}
|
||||
|
||||
const centerCanvas = viewport.worldToCanvas(points[0]) // 圆心
|
||||
const edgeCanvas = viewport.worldToCanvas(points[1]) // 边缘点
|
||||
const radius = this.distanceToPoint(centerCanvas, edgeCanvas)
|
||||
if (radius <= 0) {
|
||||
return { hit: false, score: null }
|
||||
}
|
||||
|
||||
const dx = canvasPoint[0] - centerCanvas[0]
|
||||
const dy = canvasPoint[1] - centerCanvas[1]
|
||||
const distance = (dx * dx + dy * dy) / (radius * radius)
|
||||
const hit = distance <= 1.2
|
||||
|
||||
return {
|
||||
hit,
|
||||
score: hit ? distance : null,
|
||||
}
|
||||
},
|
||||
createCopiedCachedStats(sourceCachedStats = {}) {
|
||||
const cloned = JSON.parse(JSON.stringify(sourceCachedStats || {}))
|
||||
const keys = Object.keys(cloned)
|
||||
|
||||
if (!keys.length) {
|
||||
const fallbackKey = this.isFusion ? (this.ptVolumeId || this.volumeId) : this.volumeId
|
||||
if (fallbackKey) {
|
||||
cloned[fallbackKey] = {}
|
||||
}
|
||||
}
|
||||
|
||||
Object.keys(cloned).forEach((key) => {
|
||||
cloned[key] = {
|
||||
...cloned[key],
|
||||
area: null,
|
||||
count: null,
|
||||
isEmptyArea: null,
|
||||
max: null,
|
||||
mean: null,
|
||||
min: null,
|
||||
perimeter: null,
|
||||
pointsInShape: null,
|
||||
radius: null,
|
||||
statsArray: [],
|
||||
stdDev: null,
|
||||
total: null,
|
||||
}
|
||||
})
|
||||
|
||||
return cloned
|
||||
},
|
||||
getCopySide(sourceCenterCanvas, radius) {
|
||||
const canvasWidth = this.element.clientWidth || 0
|
||||
const padding = Math.max(radius * 0.5, 8)
|
||||
const requiredSpace = Math.max(radius * 2.1, 16) + padding // 放置新标注所需要的空间
|
||||
const leftSpace = sourceCenterCanvas[0] - padding
|
||||
const rightSpace = canvasWidth - sourceCenterCanvas[0] - padding
|
||||
|
||||
const canPlaceLeft = leftSpace >= requiredSpace
|
||||
const canPlaceRight = rightSpace >= requiredSpace
|
||||
|
||||
if (canPlaceLeft && canPlaceRight) {
|
||||
return rightSpace >= leftSpace ? 'right' : 'left'
|
||||
}
|
||||
|
||||
if (canPlaceRight) {
|
||||
return 'right'
|
||||
}
|
||||
|
||||
if (canPlaceLeft) {
|
||||
return 'left'
|
||||
}
|
||||
|
||||
return rightSpace >= leftSpace ? 'right' : 'left'
|
||||
},
|
||||
buildCopiedCircleAnnotation(sourceAnnotation, side) {
|
||||
const viewport = this.getViewportInstance()
|
||||
if (!viewport) return null
|
||||
|
||||
const points = sourceAnnotation.data.handles.points || []
|
||||
if (points.length < 2) return null
|
||||
|
||||
const sourceCenterCanvas = viewport.worldToCanvas(points[0])
|
||||
const sourceEdgeCanvas = viewport.worldToCanvas(points[1])
|
||||
|
||||
const radiusVector = [
|
||||
sourceEdgeCanvas[0] - sourceCenterCanvas[0],
|
||||
sourceEdgeCanvas[1] - sourceCenterCanvas[1],
|
||||
]
|
||||
const radius = this.distanceToPoint(sourceCenterCanvas, sourceEdgeCanvas)
|
||||
if (radius <= 0) return null
|
||||
|
||||
const offsetDirection = side === 'left' ? -1 : 1
|
||||
const offsetDistance = Math.max(radius * 2.1, 16)
|
||||
const padding = Math.max(radius * 0.5, 8)
|
||||
const canvasWidth = this.element?.clientWidth || 0
|
||||
const canvasHeight = this.element?.clientHeight || 0
|
||||
const targetCenterCanvas = [
|
||||
sourceCenterCanvas[0] + offsetDirection * offsetDistance,
|
||||
sourceCenterCanvas[1],
|
||||
]
|
||||
|
||||
if (canvasWidth > 0) {
|
||||
targetCenterCanvas[0] = Math.min(Math.max(targetCenterCanvas[0], padding), canvasWidth - padding)
|
||||
}
|
||||
if (canvasHeight > 0) {
|
||||
targetCenterCanvas[1] = Math.min(Math.max(targetCenterCanvas[1], padding), canvasHeight - padding)
|
||||
}
|
||||
|
||||
const targetEdgeCanvas = [
|
||||
targetCenterCanvas[0] + radiusVector[0],
|
||||
targetCenterCanvas[1] + radiusVector[1],
|
||||
]
|
||||
const centerWorld = viewport.canvasToWorld(targetCenterCanvas)
|
||||
const edgeWorld = viewport.canvasToWorld(targetEdgeCanvas)
|
||||
const camera = viewport.getCamera()
|
||||
|
||||
return {
|
||||
highlighted: true,
|
||||
invalidated: true,
|
||||
metadata: {
|
||||
toolName: sourceAnnotation.metadata?.toolName || 'CircleROI',
|
||||
viewPlaneNormal: [...(camera?.viewPlaneNormal || sourceAnnotation.metadata?.viewPlaneNormal || [])],
|
||||
viewUp: [...(camera?.viewUp || sourceAnnotation.metadata?.viewUp || [])],
|
||||
FrameOfReferenceUID: viewport.getFrameOfReferenceUID(),
|
||||
referencedImageId: sourceAnnotation.metadata?.referencedImageId || viewport.getCurrentImageId?.(),
|
||||
...viewport.getViewReference({ points: [centerWorld] }),
|
||||
},
|
||||
data: {
|
||||
label: '',
|
||||
handles: {
|
||||
textBox: {
|
||||
hasMoved: false,
|
||||
worldPosition: [0, 0, 0],
|
||||
worldBoundingBox: {
|
||||
topLeft: [0, 0, 0],
|
||||
topRight: [0, 0, 0],
|
||||
bottomLeft: [0, 0, 0],
|
||||
bottomRight: [0, 0, 0],
|
||||
},
|
||||
},
|
||||
points: [[...centerWorld], [...edgeWorld]],
|
||||
activeHandleIndex: null,
|
||||
},
|
||||
cachedStats: this.createCopiedCachedStats(sourceAnnotation.data?.cachedStats),
|
||||
},
|
||||
}
|
||||
},
|
||||
copyCircleAnnotation(side) {
|
||||
const sourceAnnotation = this.annotationContextMenu.annotation
|
||||
this.hideAnnotationContextMenu()
|
||||
|
||||
if (!sourceAnnotation || !this.element) return
|
||||
|
||||
let targetSide = side
|
||||
if (!targetSide) {
|
||||
const viewport = this.getViewportInstance()
|
||||
const points = sourceAnnotation.data.handles.points || []
|
||||
if (!viewport || points.length < 2) return
|
||||
|
||||
const sourceCenterCanvas = viewport.worldToCanvas(points[0])
|
||||
const sourceEdgeCanvas = viewport.worldToCanvas(points[1])
|
||||
if (
|
||||
!Array.isArray(sourceCenterCanvas) ||sourceCenterCanvas.length < 2 || !Array.isArray(sourceEdgeCanvas) || sourceEdgeCanvas.length < 2
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
const radius = this.distanceToPoint(sourceCenterCanvas, sourceEdgeCanvas)
|
||||
if (radius <= 0) return
|
||||
|
||||
targetSide = this.getCopySide(sourceCenterCanvas, radius)
|
||||
}
|
||||
|
||||
const annotation = this.buildCopiedCircleAnnotation(sourceAnnotation, targetSide)
|
||||
if (!annotation) return
|
||||
|
||||
cornerstoneTools.annotation.state.addAnnotation(annotation, this.element)
|
||||
|
||||
const viewportIdsToRender = cornerstoneTools.utilities.viewportFilters.getViewportIdsWithToolToRender(
|
||||
this.element,
|
||||
annotation.metadata.toolName
|
||||
)
|
||||
cornerstoneTools.utilities.triggerAnnotationRenderForViewportIds(viewportIdsToRender)
|
||||
|
||||
setTimeout(() => {
|
||||
cornerstoneTools.annotation.state.triggerAnnotationCompleted(annotation)
|
||||
}, 0)
|
||||
},
|
||||
distanceToPoint(p1, p2) {
|
||||
return Math.sqrt(this.distanceToPointSquared(p1, p2))
|
||||
},
|
||||
distanceToPointSquared(p1, p2) {
|
||||
if (p1.length !== p2.length) {
|
||||
throw Error('Both points should have the same dimensionality')
|
||||
}
|
||||
const [x1, y1, z1 = 0] = p1
|
||||
const [x2, y2, z2 = 0] = p2
|
||||
const dx = x2 - x1
|
||||
const dy = y2 - y1
|
||||
const dz = z2 - z1
|
||||
|
||||
return dx * dx + dy * dy + dz * dz
|
||||
},
|
||||
handleDocumentMouseUp(e) {
|
||||
this.sliderMouseup(e)
|
||||
if (this.isMip) {
|
||||
|
|
@ -588,8 +909,27 @@ export default {
|
|||
this.loading = false
|
||||
}
|
||||
},
|
||||
getInitialWindowLevelFromVolumeId(volumeId) {
|
||||
if (!volumeId) return null
|
||||
const volume = cache.getVolume(volumeId)
|
||||
const imageId = volume?.imageIds?.[0]
|
||||
if (!imageId) return null
|
||||
|
||||
const voiLutModule = metaData.get('voiLutModule', imageId)
|
||||
const windowCenter = getVoiValue(voiLutModule?.windowCenter)
|
||||
const windowWidth = getVoiValue(voiLutModule?.windowWidth)
|
||||
|
||||
if (windowWidth <= 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
windowCenter,
|
||||
windowWidth,
|
||||
}
|
||||
},
|
||||
getFusionVolumes() {
|
||||
const ctVolumeId = this.ctSeries?.SeriesInstanceUid
|
||||
const ctVolumeId = this.ctSeries.SeriesInstanceUid
|
||||
const ptFusionVolumeId = this.ptVolumeId
|
||||
if (!ctVolumeId || !ptFusionVolumeId) {
|
||||
return []
|
||||
|
|
@ -598,6 +938,10 @@ export default {
|
|||
const ctEntry = {
|
||||
volumeId: ctVolumeId,
|
||||
callback: (r) => {
|
||||
const ctWindowLevel = this.getInitialWindowLevelFromVolumeId(ctVolumeId)
|
||||
if (ctWindowLevel) {
|
||||
setCtMappingRange(ctWindowLevel.windowWidth, ctWindowLevel.windowCenter)
|
||||
}
|
||||
setCtTransferFunctionForVolumeActor({ ...r, volumeId: ctVolumeId })
|
||||
if (this.fusionCtOnTop) {
|
||||
this.topFusionVolumeActor = r.volumeActor
|
||||
|
|
@ -656,13 +1000,13 @@ export default {
|
|||
async applyFusionRenderOrder() {
|
||||
if (!this.isFusion) return
|
||||
const renderingEngine = getRenderingEngine(this.renderingEngineId)
|
||||
const viewport = renderingEngine?.getViewport?.(this.viewportId)
|
||||
const viewport = renderingEngine.getViewport(this.viewportId)
|
||||
if (!viewport) return
|
||||
|
||||
const volumes = this.getFusionVolumes()
|
||||
if (!volumes.length) return
|
||||
|
||||
const camera = viewport.getCamera?.()
|
||||
const camera = viewport.getCamera()
|
||||
|
||||
const savedVoiUpper = this.currentVoiUpper
|
||||
|
||||
|
|
@ -694,6 +1038,7 @@ export default {
|
|||
if (isLocate) return csUtils.jumpToSlice(viewport.element, { imageIndex: data.SliceIndex });
|
||||
this.volumeId = data.SeriesInstanceUid
|
||||
this.ptVolumeId = null
|
||||
this.currentVoiUpper = null
|
||||
this.series = {}
|
||||
this.topFusionVolumeActor = null
|
||||
let { isFusion, isMip, colorMap } = option
|
||||
|
|
@ -705,6 +1050,7 @@ export default {
|
|||
this.renderColorBar(this.presetName)
|
||||
})
|
||||
this.ptVolumeId = `fusion_${this.volumeId}`
|
||||
this.currentVoiUpper = DEFAULT_COLOR_MAP_UPPER
|
||||
let { ct, data } = obj
|
||||
this.series = { ...data }
|
||||
this.ctSeries = { ...ct }
|
||||
|
|
@ -715,6 +1061,7 @@ export default {
|
|||
} else {
|
||||
this.series = { ...data }
|
||||
if (this.isMip) {
|
||||
this.currentVoiUpper = DEFAULT_COLOR_MAP_UPPER
|
||||
let volume = cache.getVolume(this.volumeId)
|
||||
const ptVolumeDimensions = volume.dimensions
|
||||
const slabThickness = Math.sqrt(
|
||||
|
|
@ -747,9 +1094,14 @@ export default {
|
|||
.setVolumes([{
|
||||
volumeId: this.volumeId, callback: (r) => {
|
||||
if (this.series.Modality === 'PT' || this.series.Modality === 'NM') {
|
||||
this.currentVoiUpper = DEFAULT_COLOR_MAP_UPPER
|
||||
// setPetColorMapTransferFunctionForVolumeActor(r, true)
|
||||
setPetTransferFunctionForVolumeActor({ ...r, volumeId: this.volumeId })
|
||||
} else {
|
||||
const ctWindowLevel = this.getInitialWindowLevelFromVolumeId(this.volumeId)
|
||||
if (ctWindowLevel) {
|
||||
setCtMappingRange(ctWindowLevel.windowWidth, ctWindowLevel.windowCenter)
|
||||
}
|
||||
setCtTransferFunctionForVolumeActor({ ...r, volumeId: this.volumeId })
|
||||
}
|
||||
}
|
||||
|
|
@ -758,7 +1110,9 @@ export default {
|
|||
|
||||
}
|
||||
viewport.render()
|
||||
this.voiChange(this.currentVoiUpper)
|
||||
if (this.currentVoiUpper > 0) {
|
||||
this.voiChange(this.currentVoiUpper)
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
|
|
@ -982,8 +1336,13 @@ export default {
|
|||
},
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.hideAnnotationContextMenu()
|
||||
if (this.element) {
|
||||
this.element.oncontextmenu = null
|
||||
}
|
||||
document.removeEventListener('mouseup', this.handleDocumentMouseUp)
|
||||
document.removeEventListener('mousemove', this.handleDocumentMouseMove)
|
||||
document.removeEventListener('mousedown', this.handleDocumentMouseDown)
|
||||
this.series = null
|
||||
this.topFusionVolumeActor = null
|
||||
},
|
||||
|
|
@ -1001,6 +1360,30 @@ export default {
|
|||
position: relative;
|
||||
cursor: default !important;
|
||||
|
||||
.annotation-context-menu {
|
||||
position: absolute;
|
||||
z-index: 30;
|
||||
min-width: 120px;
|
||||
padding: 4px 0;
|
||||
background: rgba(24, 24, 24, 0.96);
|
||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 6px 18px rgba(0, 0, 0, 0.35);
|
||||
user-select: none;
|
||||
|
||||
&__item {
|
||||
padding: 8px 12px;
|
||||
font-size: 12px;
|
||||
color: #ddd;
|
||||
line-height: 1.2;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.opacity-slider-wrapper {
|
||||
position: absolute;
|
||||
right: 15px;
|
||||
|
|
|
|||
|
|
@ -186,6 +186,10 @@
|
|||
@click.prevent="openFusion">
|
||||
<svg-icon icon-class="fusion" class="svg-icon" />
|
||||
</div>
|
||||
<div v-if="isFusion" :class="['tool-item', activeTool === 'FusionJumpToPointTool' ? 'tool-item-active' : '']"
|
||||
:title="$t('trials:reading:button:crosshairsPosition')" @click.prevent="setToolActive('FusionJumpToPointTool')">
|
||||
<svg-icon icon-class="position" class="svg-icon" />
|
||||
</div>
|
||||
<div v-for="tool in tools" :key="tool.toolName"
|
||||
:class="['tool-item', readingTaskState === 2 ? 'tool-disabled' : '', activeTool === tool.toolName ? 'tool-item-active' : '']"
|
||||
:style="{ cursor: tool.isDisabled ? 'not-allowed' : 'pointer' }"
|
||||
|
|
@ -576,6 +580,7 @@ import FusionForm from './FusionForm.vue'
|
|||
import SegmentForm from './SegmentForm.vue'
|
||||
import colorMap from './colorMap.vue'
|
||||
import RectangleROITool from './tools/RectangleROITool'
|
||||
import CircleROITool from './tools/CircleROITool'
|
||||
import ScaleOverlayTool from './tools/ScaleOverlayTool'
|
||||
import SegmentBidirectionalTool from './tools/SegmentBidirectionalTool'
|
||||
import FusionJumpToPointTool from './tools/FusionJumpToPointTool'
|
||||
|
|
@ -590,6 +595,8 @@ import md5 from 'js-md5'
|
|||
const { visibility } = annotation
|
||||
const { ViewportType, Events } = Enums
|
||||
const renderingEngineId = 'myRenderingEngine'
|
||||
const DEFAULT_FUSION_COLOR_MAP_RANGE = 40
|
||||
const DEFAULT_FUSION_COLOR_MAP_UPPER = 6
|
||||
const {
|
||||
ToolGroupManager,
|
||||
Enums: csToolsEnums,
|
||||
|
|
@ -608,7 +615,6 @@ const {
|
|||
ArrowAnnotateTool,
|
||||
// RectangleROITool,
|
||||
PlanarFreehandROITool,
|
||||
CircleROITool,
|
||||
AngleTool,
|
||||
CobbAngleTool,
|
||||
EraserTool,
|
||||
|
|
@ -2482,12 +2488,14 @@ export default {
|
|||
getCircleROIToolTextLines(data, targetId) {
|
||||
const cachedVolumeStats = data.cachedStats[targetId]
|
||||
const {
|
||||
Modality,
|
||||
radius,
|
||||
radiusUnit,
|
||||
area,
|
||||
mean,
|
||||
stdDev,
|
||||
max,
|
||||
total,
|
||||
isEmptyArea,
|
||||
areaUnit,
|
||||
modalityUnit
|
||||
|
|
@ -2524,6 +2532,10 @@ export default {
|
|||
textLines.push(`Std Dev: ${this.reRound(stdDev, this.digitPlaces)} ${modalityUnit}`)
|
||||
}
|
||||
|
||||
if (total !== undefined && total !== null ) { //&& Modality === 'NM'
|
||||
textLines.push(`Total: ${this.formatStatSum(total)}`)
|
||||
}
|
||||
|
||||
return textLines
|
||||
},
|
||||
getEllipticalROIToolTextLines(data, targetId) {
|
||||
|
|
@ -2615,6 +2627,16 @@ export default {
|
|||
}
|
||||
return this.processSingle(result, finalPrecision)
|
||||
},
|
||||
formatStatSum(value) {
|
||||
const num = Number(value)
|
||||
if (!Number.isFinite(num)) return value
|
||||
|
||||
if (Math.abs(num - Math.round(num)) < 1e-6) {
|
||||
return String(Math.round(num))
|
||||
}
|
||||
|
||||
return this.reRound(num, this.digitPlaces)
|
||||
},
|
||||
processSingle(str, precision) {
|
||||
const num = parseFloat(str)
|
||||
if (isNaN(num)) return 'NaN'
|
||||
|
|
@ -2645,8 +2667,9 @@ export default {
|
|||
const renderingEngine = getRenderingEngine(renderingEngineId)
|
||||
if (!renderingEngine) return
|
||||
const toolGroup = ToolGroupManager.getToolGroup(this.fusionToolGroupId)
|
||||
const instance = toolGroup?.getToolInstance(FusionJumpToPointTool.toolName)
|
||||
if (!instance?.setPoint) return
|
||||
if (!toolGroup) return
|
||||
const instance = toolGroup.getToolInstance(FusionJumpToPointTool.toolName)
|
||||
if (!instance || !instance.setPoint) return
|
||||
|
||||
instance.setPoint(worldPoint, viewportId, renderingEngine.id, {
|
||||
jumpToTargetViewports: false,
|
||||
|
|
@ -2654,28 +2677,53 @@ export default {
|
|||
})
|
||||
},
|
||||
setFusionMipJumpEnabled(enabled) {
|
||||
if (!this.isFusion) return
|
||||
const toolGroup = ToolGroupManager.getToolGroup(this.fusionToolGroupId)
|
||||
if (!toolGroup || !toolGroup.hasTool(FusionJumpToPointTool.toolName)) return
|
||||
if (enabled) {
|
||||
if (!this.isFusion) return
|
||||
toolGroup.setToolActive(FusionJumpToPointTool.toolName, {
|
||||
bindings: [{ mouseButton: MouseBindings.Primary }]
|
||||
})
|
||||
this.dispatchFusionCenterPoint()
|
||||
} else {
|
||||
toolGroup.setToolDisabled(FusionJumpToPointTool.toolName)
|
||||
this.clearFusionJumpToPointAnnotations()
|
||||
}
|
||||
},
|
||||
clearFusionJumpToPointAnnotations() {
|
||||
const annotations = cornerstoneTools.annotation.state.getAllAnnotations() || []
|
||||
const removeList = []
|
||||
for (let i = 0; i < annotations.length; i++) {
|
||||
const item = annotations[i]
|
||||
if (!item || !item.metadata) continue
|
||||
if (item.metadata.toolName !== FusionJumpToPointTool.toolName) continue
|
||||
removeList.push(item)
|
||||
}
|
||||
if (!removeList.length) return
|
||||
removeList.forEach(i => {
|
||||
cornerstoneTools.annotation.state.removeAnnotation(i.annotationUID)
|
||||
})
|
||||
const renderingEngine = getRenderingEngine(renderingEngineId)
|
||||
if (!renderingEngine) return
|
||||
const viewportIds = ['viewport-fusion-0', 'viewport-fusion-1', 'viewport-fusion-2', 'viewport-fusion-3', 'viewport-fusion-hidden-sag']
|
||||
viewportIds.forEach(viewportId => {
|
||||
const viewport = renderingEngine.getViewport(viewportId)
|
||||
if (viewport && viewport.render) {
|
||||
viewport.render()
|
||||
}
|
||||
})
|
||||
},
|
||||
dispatchFusionCenterPoint(retryCount = 0) {
|
||||
const renderingEngine = getRenderingEngine(renderingEngineId)
|
||||
if (!renderingEngine) return
|
||||
const toolGroup = ToolGroupManager.getToolGroup(this.fusionToolGroupId)
|
||||
const instance = toolGroup?.getToolInstance?.(FusionJumpToPointTool.toolName)
|
||||
if (!instance?.setPoint) return
|
||||
if (!toolGroup || !toolGroup.getToolInstance) return
|
||||
const instance = toolGroup.getToolInstance(FusionJumpToPointTool.toolName)
|
||||
if (!instance || !instance.setPoint) return
|
||||
const candidates = ['viewport-fusion-2', 'viewport-fusion-1', 'viewport-fusion-0']
|
||||
for (const viewportId of candidates) {
|
||||
const viewport = renderingEngine.getViewport(viewportId)
|
||||
if (!viewport?.canvasToWorld || !viewport?.element) continue
|
||||
if (!viewport || !viewport.canvasToWorld || !viewport.element) continue
|
||||
const width = viewport.element.clientWidth
|
||||
const height = viewport.element.clientHeight
|
||||
let worldPoint = null
|
||||
|
|
@ -2683,7 +2731,10 @@ export default {
|
|||
worldPoint = viewport.canvasToWorld([width / 2, height / 2])
|
||||
}
|
||||
if ((!worldPoint || worldPoint.length < 3) && viewport.getCamera) {
|
||||
worldPoint = viewport.getCamera()?.focalPoint
|
||||
const camera = viewport.getCamera()
|
||||
if (camera && camera.focalPoint) {
|
||||
worldPoint = camera.focalPoint
|
||||
}
|
||||
}
|
||||
if (!worldPoint || worldPoint.length < 3) continue
|
||||
instance.setPoint(worldPoint, viewportId, renderingEngine.id, {
|
||||
|
|
@ -2724,6 +2775,8 @@ export default {
|
|||
}
|
||||
this.setFusionMipJumpEnabled(true)
|
||||
// this.setFusionMipRotateEnabled(true)
|
||||
} else if (toolName === FusionJumpToPointTool.toolName) {
|
||||
this.setFusionMipJumpEnabled(false)
|
||||
} else {
|
||||
toolGroup.setToolPassive(this.activeTool)
|
||||
}
|
||||
|
|
@ -2736,31 +2789,37 @@ export default {
|
|||
}
|
||||
this.setFusionMipJumpEnabled(true)
|
||||
// this.setFusionMipRotateEnabled(true)
|
||||
} else if (this.activeTool === FusionJumpToPointTool.toolName) {
|
||||
this.setFusionMipJumpEnabled(false)
|
||||
} else {
|
||||
toolGroup.setToolPassive(this.activeTool)
|
||||
}
|
||||
}
|
||||
toolGroup.setToolActive(toolName, {
|
||||
bindings: [{ mouseButton: MouseBindings.Primary }]
|
||||
})
|
||||
if (toolName === CrosshairsTool.toolName) {
|
||||
if (this.isFusion) {
|
||||
// const instance = toolGroup.getToolInstance?.(CrosshairsTool.toolName)
|
||||
// if (instance && !instance.__fusionSameForPatched) {
|
||||
// instance.__fusionSameForPatched = true
|
||||
// const original = instance._checkIfViewportsRenderingSameScene?.bind(instance)
|
||||
// instance._checkIfViewportsRenderingSameScene = (viewport, otherViewport) => {
|
||||
// try {
|
||||
// const a = viewport?.getFrameOfReferenceUID?.()
|
||||
// const b = otherViewport?.getFrameOfReferenceUID?.()
|
||||
// if (a && b && a === b) return true
|
||||
// } catch (e) { }
|
||||
// return original ? original(viewport, otherViewport) : true
|
||||
// }
|
||||
// }
|
||||
if (toolName === FusionJumpToPointTool.toolName) {
|
||||
this.setFusionMipJumpEnabled(true)
|
||||
} else {
|
||||
toolGroup.setToolActive(toolName, {
|
||||
bindings: [{ mouseButton: MouseBindings.Primary }]
|
||||
})
|
||||
if (toolName === CrosshairsTool.toolName) {
|
||||
if (this.isFusion) {
|
||||
// const instance = toolGroup.getToolInstance?.(CrosshairsTool.toolName)
|
||||
// if (instance && !instance.__fusionSameForPatched) {
|
||||
// instance.__fusionSameForPatched = true
|
||||
// const original = instance._checkIfViewportsRenderingSameScene?.bind(instance)
|
||||
// instance._checkIfViewportsRenderingSameScene = (viewport, otherViewport) => {
|
||||
// try {
|
||||
// const a = viewport?.getFrameOfReferenceUID?.()
|
||||
// const b = otherViewport?.getFrameOfReferenceUID?.()
|
||||
// if (a && b && a === b) return true
|
||||
// } catch (e) { }
|
||||
// return original ? original(viewport, otherViewport) : true
|
||||
// }
|
||||
// }
|
||||
}
|
||||
this.setFusionMipJumpEnabled(false)
|
||||
// this.setFusionMipRotateEnabled(false)
|
||||
}
|
||||
this.setFusionMipJumpEnabled(false)
|
||||
// this.setFusionMipRotateEnabled(false)
|
||||
}
|
||||
this.activeTool = toolName
|
||||
}
|
||||
|
|
@ -2807,6 +2866,8 @@ export default {
|
|||
if (toolGroup.hasTool(this.activeTool)) {
|
||||
toolGroup.setToolDisabled(this.activeTool)
|
||||
}
|
||||
} else if (toolName === FusionJumpToPointTool.toolName) {
|
||||
this.setFusionMipJumpEnabled(false)
|
||||
} else {
|
||||
toolGroup.setToolPassive(this.activeTool)
|
||||
}
|
||||
|
|
@ -2817,6 +2878,8 @@ export default {
|
|||
if (toolGroup.hasTool(this.activeTool)) {
|
||||
toolGroup.setToolDisabled(this.activeTool)
|
||||
}
|
||||
} else if (this.activeTool === FusionJumpToPointTool.toolName) {
|
||||
this.setFusionMipJumpEnabled(false)
|
||||
} else {
|
||||
toolGroup.setToolPassive(this.activeTool)
|
||||
}
|
||||
|
|
@ -2844,6 +2907,8 @@ export default {
|
|||
if (toolGroup.hasTool(this.activeTool)) {
|
||||
toolGroup.setToolDisabled(this.activeTool)
|
||||
}
|
||||
} else if (this.activeTool === FusionJumpToPointTool.toolName) {
|
||||
this.setFusionMipJumpEnabled(false)
|
||||
} else {
|
||||
toolGroup.setToolPassive(this.activeTool)
|
||||
}
|
||||
|
|
@ -2866,6 +2931,8 @@ export default {
|
|||
if (toolGroup.hasTool(this.activeTool)) {
|
||||
toolGroup.setToolDisabled(this.activeTool)
|
||||
}
|
||||
} else if (this.activeTool === FusionJumpToPointTool.toolName) {
|
||||
this.setFusionMipJumpEnabled(false)
|
||||
} else {
|
||||
toolGroup.setToolPassive(this.activeTool)
|
||||
}
|
||||
|
|
@ -2898,6 +2965,8 @@ export default {
|
|||
if (toolGroup.hasTool(this.activeTool)) {
|
||||
toolGroup.setToolDisabled(this.activeTool)
|
||||
}
|
||||
} else if (this.activeTool === FusionJumpToPointTool.toolName) {
|
||||
this.setFusionMipJumpEnabled(false)
|
||||
} else {
|
||||
toolGroup.setToolPassive(this.activeTool)
|
||||
}
|
||||
|
|
@ -3031,7 +3100,7 @@ export default {
|
|||
this.resetCrosshairsAnnotationsForViewports(fusionAllViewportIds)
|
||||
renderingEngine.render()
|
||||
this.dispatchFusionCenterPoint()
|
||||
if (this.fusionOverlayModality === 'NM' && Number.isFinite(this.fusionOverlayDefaultUpper) && Number.isFinite(this.fusionOverlayDefaultRange)) {
|
||||
if (Number.isFinite(this.fusionOverlayDefaultUpper) && Number.isFinite(this.fusionOverlayDefaultRange)) {
|
||||
this.lastUpper = null
|
||||
this.hasFusionUpperInitialized = false
|
||||
if (this.$refs.colorMap) {
|
||||
|
|
@ -3622,8 +3691,14 @@ export default {
|
|||
DicomEvent.$emit('SegmentationLoading', `${this.viewportKey}-${this.activeViewportIndex}`)
|
||||
})
|
||||
}
|
||||
if (this.activeTool !== CrosshairsTool.toolName) {
|
||||
this.setToolsPassive()
|
||||
if (this.activeTool && this.activeTool !== CrosshairsTool.toolName && this.activeTool !== FusionJumpToPointTool.toolName) {
|
||||
const toolGroupId = this.getActiveToolGroupId()
|
||||
const toolGroup = ToolGroupManager.getToolGroup(toolGroupId)
|
||||
if (toolGroup && toolGroup.hasTool(this.activeTool)) {
|
||||
toolGroup.setToolActive(this.activeTool, {
|
||||
bindings: [{ mouseButton: MouseBindings.Primary }]
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
getRelatedSeries(visitTaskInfo, baselineSeries) {
|
||||
|
|
@ -4335,9 +4410,6 @@ export default {
|
|||
this.$refs[`viewport-1`][0].setSeriesInfo(pt)
|
||||
this.$refs[`viewport-2`][0].setSeriesInfo(pt)
|
||||
this.$refs[`viewport-3`][0].setSeriesInfo(pt)
|
||||
this.$nextTick(() => {
|
||||
this.setFusionMipJumpEnabled(true)
|
||||
})
|
||||
// this.resetAnnotation = false
|
||||
return true
|
||||
}
|
||||
|
|
@ -4374,29 +4446,17 @@ export default {
|
|||
await this.initFusionHiddenSagViewport(pt)
|
||||
// this.resetAnnotation = false
|
||||
this.$nextTick(() => {
|
||||
const defaultRange = this.getFusionDefaultColorMapRange(pt)
|
||||
const defaultUpper = this.getFusionDefaultColorMapUpper(defaultRange)
|
||||
this.$refs[`colorMap`].init()
|
||||
if (this.fusionOverlayModality === 'NM') {
|
||||
const imageIds = this.sortImageIdsByImagePositionPatient(pt.ImageIds)
|
||||
const imageId = imageIds?.[0]
|
||||
const voiLutModule = imageId ? metaData.get('voiLutModule', imageId) : null
|
||||
const rawWidth = Array.isArray(voiLutModule?.windowWidth) ? voiLutModule.windowWidth[0] : voiLutModule?.windowWidth
|
||||
const nmMax = Number(rawWidth)
|
||||
if (Number.isFinite(nmMax) && nmMax > 0) {
|
||||
const halfMax = Math.round(nmMax * 0.5)
|
||||
this.fusionOverlayDefaultRange = Math.round(nmMax)
|
||||
this.fusionOverlayDefaultUpper = halfMax
|
||||
this.lastUpper = null
|
||||
this.hasFusionUpperInitialized = false
|
||||
this.$refs.colorMap.range = Math.round(nmMax)
|
||||
this.$refs.colorMap.upper = halfMax
|
||||
this.$refs.colorMap.upperRangeChange(Math.round(nmMax))
|
||||
this.voiChange(halfMax)
|
||||
}
|
||||
} else {
|
||||
this.fusionOverlayDefaultRange = null
|
||||
this.fusionOverlayDefaultUpper = null
|
||||
}
|
||||
this.setFusionMipJumpEnabled(true)
|
||||
this.fusionOverlayDefaultRange = defaultRange
|
||||
this.fusionOverlayDefaultUpper = defaultUpper
|
||||
this.lastUpper = null
|
||||
this.hasFusionUpperInitialized = false
|
||||
this.$refs.colorMap.range = defaultRange
|
||||
this.$refs.colorMap.upper = defaultUpper
|
||||
this.$refs.colorMap.upperRangeChange(defaultRange)
|
||||
this.voiChange(defaultUpper)
|
||||
})
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
|
|
@ -4480,6 +4540,41 @@ export default {
|
|||
pairs.sort((a, b) => b.distance - a.distance);
|
||||
return pairs.map((p) => p.imageId);
|
||||
},
|
||||
getFusionDefaultColorMapRange(series) {
|
||||
if (this.fusionOverlayModality !== 'NM') {
|
||||
return DEFAULT_FUSION_COLOR_MAP_RANGE
|
||||
}
|
||||
|
||||
const imageIds = this.sortImageIdsByImagePositionPatient(series?.ImageIds || [])
|
||||
const imageId = imageIds?.[0]
|
||||
if (!imageId) {
|
||||
return DEFAULT_FUSION_COLOR_MAP_RANGE
|
||||
}
|
||||
|
||||
const voiLutModule = metaData.get('voiLutModule', imageId)
|
||||
const rawWidth = Array.isArray(voiLutModule?.windowWidth)
|
||||
? voiLutModule.windowWidth[0]
|
||||
: voiLutModule?.windowWidth
|
||||
const windowWidth = Math.round(Number(rawWidth))
|
||||
|
||||
if (!Number.isFinite(windowWidth) || windowWidth <= 0) {
|
||||
return DEFAULT_FUSION_COLOR_MAP_RANGE
|
||||
}
|
||||
|
||||
return windowWidth
|
||||
},
|
||||
getFusionDefaultColorMapUpper(range) {
|
||||
if (this.fusionOverlayModality !== 'NM') {
|
||||
return DEFAULT_FUSION_COLOR_MAP_UPPER
|
||||
}
|
||||
|
||||
const safeRange = Number(range)
|
||||
if (!Number.isFinite(safeRange) || safeRange <= 0) {
|
||||
return DEFAULT_FUSION_COLOR_MAP_UPPER
|
||||
}
|
||||
|
||||
return Math.round(safeRange * 0.5)
|
||||
},
|
||||
upperRangeChange(upper) {
|
||||
if (!this.hasFusionUpperInitialized) {
|
||||
if (!upper) return
|
||||
|
|
|
|||
|
|
@ -270,7 +270,7 @@ const config = {
|
|||
'name': '圆形工具',
|
||||
'icon': 'oval',
|
||||
'toolName': 'CircleROI',
|
||||
'props': ['radius', 'area', 'mean', 'max', 'stdDev'],
|
||||
'props': ['radius', 'area', 'mean', 'max', 'stdDev', 'total'],
|
||||
'i18nKey': 'trials:reading:button:Circle',
|
||||
'isDisabled': false,
|
||||
'disabledReason': ''
|
||||
|
|
|
|||
|
|
@ -0,0 +1,199 @@
|
|||
import { EPSILON, utilities as csUtils } from "@cornerstonejs/core"
|
||||
import * as cornerstoneTools from "@cornerstonejs/tools"
|
||||
const { triggerAnnotationModified } = cornerstoneTools.annotation.state
|
||||
|
||||
const { transformWorldToIndex } = csUtils
|
||||
const { BaseTool, Enums, utilities } = cornerstoneTools
|
||||
const { MeasurementType, ChangeTypes } = Enums
|
||||
const { getCanvasCircleCorners } = utilities.math.circle
|
||||
const { pointInEllipse } = utilities.math.ellipse
|
||||
const { getCalibratedLengthUnitsAndScale } = utilities
|
||||
|
||||
class CircleROITool extends cornerstoneTools.CircleROITool {
|
||||
static toolName = "CircleROI"
|
||||
|
||||
constructor(toolProps = {}, defaultToolProps) {
|
||||
super(toolProps, defaultToolProps)
|
||||
|
||||
this._calculateCachedStats = (
|
||||
annotation,
|
||||
viewport,
|
||||
renderingEngine,
|
||||
enabledElement
|
||||
) => {
|
||||
if (!this.configuration.calculateStats) return
|
||||
|
||||
const { data } = annotation
|
||||
const { element } = viewport
|
||||
const wasInvalidated = annotation.invalidated
|
||||
const { points } = data.handles
|
||||
const canvasCoordinates = points.map((point) =>
|
||||
viewport.worldToCanvas(point)
|
||||
)
|
||||
const canvasCenter = canvasCoordinates[0]
|
||||
const canvasTop = canvasCoordinates[1]
|
||||
const [topLeftCanvas, bottomRightCanvas] = getCanvasCircleCorners([
|
||||
canvasCenter,
|
||||
canvasTop
|
||||
])
|
||||
const topLeftWorld = viewport.canvasToWorld(topLeftCanvas)
|
||||
const bottomRightWorld = viewport.canvasToWorld(bottomRightCanvas)
|
||||
const { cachedStats } = data
|
||||
const targetIds = Object.keys(cachedStats)
|
||||
|
||||
for (let i = 0; i < targetIds.length; i++) {
|
||||
const targetId = targetIds[i];
|
||||
const image = this.getTargetImageData(targetId)
|
||||
|
||||
if (!image) {
|
||||
console.warn("image not found for stats:", targetId)
|
||||
delete cachedStats[targetId]
|
||||
continue
|
||||
}
|
||||
|
||||
const { dimensions, imageData, metadata, voxelManager } = image
|
||||
const handles = points.map((point) => imageData.worldToIndex(point))
|
||||
const calibrate = getCalibratedLengthUnitsAndScale(image, handles)
|
||||
const radius = CircleROITool.calculateLengthInIndex(
|
||||
calibrate,
|
||||
handles.slice(0, 2)
|
||||
)
|
||||
const area = Math.PI * radius * radius
|
||||
const perimeter = 2 * Math.PI * radius
|
||||
const isEmptyArea = radius === 0
|
||||
const { unit, areaUnit } = calibrate
|
||||
const namedArea = {
|
||||
name: "area",
|
||||
value: area,
|
||||
unit: areaUnit,
|
||||
type: MeasurementType.Area,
|
||||
}
|
||||
const namedCircumference = {
|
||||
name: "circumference",
|
||||
value: perimeter,
|
||||
unit,
|
||||
type: MeasurementType.Linear,
|
||||
}
|
||||
const namedRadius = {
|
||||
name: "radius",
|
||||
value: radius,
|
||||
unit,
|
||||
type: MeasurementType.Linear,
|
||||
}
|
||||
const statsArray = [namedArea, namedRadius, namedCircumference]
|
||||
|
||||
cachedStats[targetId] = {
|
||||
Modality: metadata.Modality,
|
||||
area,
|
||||
isEmptyArea,
|
||||
areaUnit,
|
||||
radius,
|
||||
radiusUnit: unit,
|
||||
perimeter,
|
||||
statsArray,
|
||||
}
|
||||
|
||||
const pos1Index = transformWorldToIndex(imageData, topLeftWorld)
|
||||
const pos2Index = transformWorldToIndex(imageData, bottomRightWorld)
|
||||
|
||||
this.isHandleOutsideImage = !BaseTool.isInsideVolume(dimensions, [
|
||||
pos1Index,
|
||||
pos2Index,
|
||||
])
|
||||
if (!this.isHandleOutsideImage) {
|
||||
const iMin = Math.min(pos1Index[0], pos2Index[0])
|
||||
const iMax = Math.max(pos1Index[0], pos2Index[0])
|
||||
const jMin = Math.min(pos1Index[1], pos2Index[1])
|
||||
const jMax = Math.max(pos1Index[1], pos2Index[1])
|
||||
const kMin = Math.min(pos1Index[2], pos2Index[2])
|
||||
const kMax = Math.max(pos1Index[2], pos2Index[2])
|
||||
const boundsIJK = [
|
||||
[iMin, iMax],
|
||||
[jMin, jMax],
|
||||
[kMin, kMax],
|
||||
]
|
||||
const center = points[0];
|
||||
const xRadius = Math.abs(topLeftWorld[0] - bottomRightWorld[0]) / 2
|
||||
const yRadius = Math.abs(topLeftWorld[1] - bottomRightWorld[1]) / 2
|
||||
const zRadius = Math.abs(topLeftWorld[2] - bottomRightWorld[2]) / 2
|
||||
const ellipseObj = {
|
||||
center,
|
||||
xRadius: xRadius < EPSILON / 2 ? 0 : xRadius,
|
||||
yRadius: yRadius < EPSILON / 2 ? 0 : yRadius,
|
||||
zRadius: zRadius < EPSILON / 2 ? 0 : zRadius,
|
||||
}
|
||||
const pixelUnitsOptions = {
|
||||
isPreScaled: utilities.viewport.isViewportPreScaled(
|
||||
viewport,
|
||||
targetId
|
||||
),
|
||||
isSuvScaled: this.isSuvScaled(
|
||||
viewport,
|
||||
targetId,
|
||||
annotation.metadata.referencedImageId
|
||||
),
|
||||
}
|
||||
const modalityUnit = utilities.getPixelValueUnits(
|
||||
metadata.Modality,
|
||||
annotation.metadata.referencedImageId,
|
||||
pixelUnitsOptions
|
||||
)
|
||||
let pointsInShape
|
||||
if (voxelManager) {
|
||||
pointsInShape = voxelManager.forEach(
|
||||
this.configuration.statsCalculator.statsCallback,
|
||||
{
|
||||
isInObject: (pointLPS) =>
|
||||
pointInEllipse(ellipseObj, pointLPS, { fast: true }),
|
||||
boundsIJK,
|
||||
imageData,
|
||||
returnPoints: this.configuration.storePointData,
|
||||
}
|
||||
)
|
||||
}
|
||||
const stats = this.configuration.statsCalculator.getStatistics()
|
||||
const mean = stats.mean?.value
|
||||
const count = stats.count?.value
|
||||
const total =
|
||||
Array.isArray(mean) && Number.isFinite(count)
|
||||
? mean.map((value) => value * count)
|
||||
: Number.isFinite(mean) && Number.isFinite(count)
|
||||
? mean * count
|
||||
: undefined;
|
||||
cachedStats[targetId] = {
|
||||
...cachedStats[targetId],
|
||||
Modality: metadata.Modality,
|
||||
mean,
|
||||
max: stats.max?.value,
|
||||
min: stats.min?.value,
|
||||
total,
|
||||
count,
|
||||
pointsInShape,
|
||||
stdDev: stats.stdDev?.value,
|
||||
modalityUnit,
|
||||
statsArray: [...statsArray, ...stats.array],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
annotation.invalidated = false
|
||||
if (wasInvalidated) {
|
||||
triggerAnnotationModified(
|
||||
annotation,
|
||||
element,
|
||||
ChangeTypes.StatsUpdated
|
||||
)
|
||||
}
|
||||
|
||||
return cachedStats
|
||||
}
|
||||
|
||||
this._throttledCalculateCachedStats = utilities.throttle(
|
||||
this._calculateCachedStats,
|
||||
100,
|
||||
{ trailing: true }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default CircleROITool
|
||||
Loading…
Reference in New Issue