十字线功能优化;MR图像显示默认使用图像的窗宽窗位;

main
caiyiling 2026-05-21 13:41:39 +08:00
parent a9d62cf28d
commit 6af2f0555e
6 changed files with 3466 additions and 2705 deletions

View File

@ -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

View File

@ -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;

View File

@ -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

View File

@ -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': ''

View File

@ -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