SPECT融合更改

uat_us
caiyiling 2026-03-25 16:45:44 +08:00
parent 9a3e972723
commit 77a633d699
3 changed files with 158 additions and 75 deletions

View File

@ -2,9 +2,9 @@
<div ref="viewport-fusion" class="viewport-wrapper" v-loading="loading" :element-loading-text="NSTip"
element-loading-background="rgba(0, 0, 0, 0.8)" @mouseup="sliderMouseup" @mousemove="sliderMousemove"
@mouseleave="sliderMouseleave" :style="{ color: series.Modality === 'PT' || series.Modality === 'NM' || isMip ? '#666' : '#ddd' }">
<div v-if="isFusion && series.Modality === 'NM'" class="opacity-slider-wrapper" @mousedown.stop @mousemove.stop @mouseup.stop @wheel.stop>
<div class="slider-title">{{ Math.round(nmOpacity * 100) }}%</div>
<input type="range" min="0" max="1" step="0.05" v-model.number="nmOpacity" @input="applyNmOpacity"
<div v-if="isFusion" class="opacity-slider-wrapper" @mousedown.stop @mousemove.stop @mouseup.stop @wheel.stop>
<div class="slider-title">{{ Math.round(fusionOpacity * 100) }}%</div>
<input type="range" min="0" max="1" step="0.05" v-model.number="fusionOpacity" @input="applyFusionOpacity"
class="opacity-slider" />
</div>
<div v-if="series && taskInfo" class="left-top-text">
@ -22,29 +22,13 @@
<div v-if="isFusion">{{ ctSeries.Modality }} / {{ series.Modality }}</div>
<div v-if="isMip">MIP</div>
</div>
<!-- <div v-if="series && taskInfo && taskInfo.IsReadingTaskViewInOrder === 1 && series.TaskInfo && !isMip && !isFusion"
class="top-center-tool">
<div class="toggle-visit-container">
<div class="arrw_icon"
:style="{ cursor: series.TaskInfo.VisitTaskNum !== 0 ? 'pointer' : 'not-allowed', color: series.TaskInfo.VisitTaskNum !== 0 ? '#fff' : '#6b6b6b' }"
@click.stop.prevent="toggleTask($event, series.TaskInfo.VisitTaskNum, -1)"
@dblclick.stop="preventDefault($event)">
<i class="el-icon-caret-left" />
</div>
<div class="arrow_text">
{{ series.TaskInfo.TaskBlindName }}
</div>
<div class="arrw_icon"
:style="{ cursor: series.TaskInfo.VisitTaskNum < taskInfo.VisitNum ? 'pointer' : 'not-allowed', color: series.TaskInfo.VisitTaskNum < taskInfo.VisitNum ? '#fff' : '#6b6b6b' }"
@click.stop.prevent="toggleTask($event, series.TaskInfo.VisitTaskNum, 1)"
@dblclick.stop="preventDefault($event)">
<i class="el-icon-caret-right" />
</div>
</div>
</div> -->
<div v-if="series && !isMip && !isFusion" class="right-top-text">
<div>{{ series.Description }}</div>
</div>
<div v-if="isFusion && !isMip" class="fusion-order-toggle" @mousedown.stop @mouseup.stop
@click.stop="toggleFusionRenderOrder">
{{ fusionCtOnTop ? `${ctSeries.Modality}/${series.Modality}` : `${series.Modality}/${ctSeries.Modality}` }}
</div>
<div v-if="series" class="left-bottom-text">
<div v-show="mousePosition.index.length > 0 && !isMip">
Pos: {{ mousePosition.index[0] }}, {{ mousePosition.index[1] }}, {{ mousePosition.index[2] }}
@ -111,7 +95,7 @@ import {
import * as cornerstoneTools from '@cornerstonejs/tools'
import cornerstoneDICOMImageLoader from '@cornerstonejs/dicom-image-loader';
import vtkColorMaps from '@kitware/vtk.js/Rendering/Core/ColorTransferFunction/ColorMaps'
import vtkPiecewiseFunction from '@kitware/vtk.js/Common/DataModel/PiecewiseFunction'
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 { setPetColorMapTransferFunctionForVolumeActor } from '@/views/trials/trials-panel/reading/dicoms/components/Fusion/js/setPetColorMapTransferFunctionForVolumeActor'
@ -146,6 +130,7 @@ export default {
petSeries: {},
isFusion: false,
isMip: false,
fusionCtOnTop: false,
taskInfo: null,
sliderInfo: {
oldB: null,
@ -185,9 +170,10 @@ export default {
},
ptVolumeId: null,
loading: false,
Colorbar: null,
nmOpacity: 0.6,
nmFusionVolumeActor: null
Colorbar: null,
fusionOpacity: 0.5,
topFusionVolumeActor: null,
currentVoiUpper: null
}
},
mounted() {
@ -288,6 +274,14 @@ Colorbar: null,
}
if (properties && properties.voiRange) {
var { lower, upper } = properties.voiRange
if ((!upper || upper === 0) && this.currentVoiUpper > 0) {
upper = this.currentVoiUpper
} else if (upper) {
this.currentVoiUpper = upper
}
if (!upper) return
const { windowWidth, windowCenter } = csUtils.windowLevel.toWindowLevel(
lower,
upper
@ -296,6 +290,9 @@ Colorbar: null,
if (this.series.Modality === 'PT' || this.series.Modality === 'NM' || this.isFusion) {
this.$emit('upperRangeChange', Math.round(upper))
}
if (this.isFusion && !this.fusionCtOnTop && this.topFusionVolumeActor) {
this.applyFusionOpacity()
}
}
},
getOrientationMarker() {
@ -416,6 +413,11 @@ Colorbar: null,
voiChange(v) {
const renderingEngine = getRenderingEngine(this.renderingEngineId)
const voiRange = { lower: 0, upper: v }
this.currentVoiUpper = v
if (this.isFusion) {
console.log('voiChange', v)
}
const viewport = renderingEngine.getViewport(this.viewportId)
if (!viewport) return
let volumeId = this.isFusion ? this.ptVolumeId : this.volumeId
@ -425,6 +427,11 @@ Colorbar: null,
)
viewport.setProperties({ voiRange }, volumeId)
// if (this.isFusion && !this.fusionCtOnTop && this.topFusionVolumeActor) {
// this.applyFusionOpacity()
// }
viewportsContainingVolumeUID.forEach((vp) => {
vp.render()
// this.$refs[vp.id].voiModified()
@ -464,17 +471,21 @@ Colorbar: null,
ctx.fillStyle = gradient
ctx.fillRect(0, 0, rectWidth, rectHeight)
},
applyNmOpacity() {
if (!this.nmFusionVolumeActor?.getProperty) {
return;
}
const ofun = vtkPiecewiseFunction.newInstance();
ofun.addPoint(0, 0.0);
ofun.addPoint(0.1, 0.9 * this.nmOpacity);
ofun.addPoint(5, 1.0 * this.nmOpacity);
this.nmFusionVolumeActor.getProperty().setScalarOpacity(0, ofun);
opacityChange(opacity) {
this.fusionOpacity = opacity
const renderingEngine = getRenderingEngine(this.renderingEngineId)
renderingEngine?.render?.();
const viewport = renderingEngine.getViewport(this.viewportId)
if (!viewport) return
let volumeId = this.isFusion ? this.ptVolumeId : this.volumeId
viewport.setProperties(
{ colormap: { opacity: Number(opacity) } },
volumeId
)
viewport.render()
},
applyFusionOpacity() {
this.opacityChange(this.fusionOpacity)
},
setPreset(presetName) {
this.presetName = presetName
@ -507,6 +518,85 @@ Colorbar: null,
})
this.loading = false
},
getFusionVolumes() {
const ctVolumeId = this.ctSeries?.SeriesInstanceUid
const ptFusionVolumeId = this.ptVolumeId
if (!ctVolumeId || !ptFusionVolumeId) {
return []
}
const ctEntry = {
volumeId: ctVolumeId,
callback: (r) => {
setCtTransferFunctionForVolumeActor({ ...r, volumeId: ctVolumeId })
if (this.fusionCtOnTop) {
this.topFusionVolumeActor = r.volumeActor
this.applyFusionOpacity()
}
console.log("融合ct渲染成功")
}
}
const ptFusionEntry = {
volumeId: ptFusionVolumeId,
callback: (r) => {
setPetColorMapTransferFunctionForVolumeActor({ ...r, volumeId: ptFusionVolumeId })
if (!this.fusionCtOnTop) {
this.topFusionVolumeActor = r.volumeActor
this.applyFusionOpacity()
}
console.log("融合pet渲染成功")
}
}
const volumes = []
if (this.series.Modality !== 'NM') {
volumes.push({
volumeId: this.volumeId,
callback: (r) => {
console.log("融合pet渲染成功");
}
})
}
if (this.fusionCtOnTop) {
volumes.push(ptFusionEntry, ctEntry)
} else {
volumes.push(ctEntry, ptFusionEntry)
}
return volumes
},
async applyFusionRenderOrder() {
if (!this.isFusion) return
const renderingEngine = getRenderingEngine(this.renderingEngineId)
const viewport = renderingEngine?.getViewport?.(this.viewportId)
if (!viewport) return
const volumes = this.getFusionVolumes()
if (!volumes.length) return
const camera = viewport.getCamera?.()
const savedVoiUpper = this.currentVoiUpper
await viewport.setVolumes(volumes)
if (camera) {
viewport.setCamera(camera)
}
if (savedVoiUpper) {
viewport.setProperties({ voiRange: { lower: 0, upper: savedVoiUpper } }, this.ptVolumeId)
}
viewport.render()
},
toggleFusionRenderOrder() {
if (!this.isFusion) return
this.fusionCtOnTop = !this.fusionCtOnTop
this.applyFusionRenderOrder()
this.$emit('upperRangeChange', Math.round(this.currentVoiUpper))
},
async setSeriesInfo(obj, isLocate = false, option = {}) {
try {
let { data } = obj
@ -519,11 +609,12 @@ Colorbar: null,
this.volumeId = data.SeriesInstanceUid
this.ptVolumeId = null
this.series = {}
this.nmFusionVolumeActor = null
this.topFusionVolumeActor = null
let { isFusion, isMip, colorMap } = option
this.isFusion = isFusion;
this.isMip = isMip;
if (this.isFusion) {
this.fusionCtOnTop = false
this.$nextTick(() => {
this.renderColorBar(this.presetName)
})
@ -531,34 +622,10 @@ Colorbar: null,
let { ct, data } = obj
this.series = { ...data }
this.ctSeries = { ...ct }
let volumes = [
{
volumeId: ct.SeriesInstanceUid, callback: (r) => {
setCtTransferFunctionForVolumeActor(r)
console.log("融合ct渲染成功")
}
},
{
volumeId: this.ptVolumeId, callback: (r) => {
setPetColorMapTransferFunctionForVolumeActor(r)
if (this.series.Modality === 'NM') {
this.nmFusionVolumeActor = r.volumeActor
this.applyNmOpacity()
}
console.log("融合pet渲染成功")
}
},
]
if (this.series.Modality !== 'NM') {
volumes.unshift({
volumeId: this.volumeId, callback: (r) => {
// setPetColorMapTransferFunctionForVolumeActor(r)
console.log("融合pet渲染成功");
}
})
}
await viewport.setVolumes(volumes)
await viewport.setVolumes(this.getFusionVolumes())
} else {
this.series = { ...data }
if (this.isMip) {
@ -593,9 +660,9 @@ Colorbar: null,
volumeId: this.volumeId, callback: (r) => {
if (this.series.Modality === 'PT' || this.series.Modality === 'NM') {
// setPetColorMapTransferFunctionForVolumeActor(r, true)
setPetTransferFunctionForVolumeActor(r)
setPetTransferFunctionForVolumeActor({ ...r, volumeId: this.volumeId })
} else {
setCtTransferFunctionForVolumeActor(r)
setCtTransferFunctionForVolumeActor({ ...r, volumeId: this.volumeId })
}
}
}])
@ -603,6 +670,7 @@ Colorbar: null,
}
viewport.render()
this.voiChange(this.currentVoiUpper)
} catch (e) {
console.log(e)
}
@ -782,7 +850,7 @@ Colorbar: null,
},
beforeDestroy() {
this.series = null
this.nmFusionVolumeActor = null
this.topFusionVolumeActor = null
},
computed: {
NSTip() {
@ -892,6 +960,20 @@ Colorbar: null,
font-size: 12px;
}
.fusion-order-toggle {
position: absolute;
right: 5px;
top: 5px;
z-index: 2;
font-size: 12px;
padding: 2px 8px;
background: rgba(0, 0, 0, 0.35);
border: 1px solid rgba(255, 255, 255, 0.25);
border-radius: 2px;
cursor: pointer;
user-select: none;
}
.left-bottom-text {
position: absolute;
left: 5px;

View File

@ -273,8 +273,12 @@
</div>
<!-- 伪彩 -->
<template v-if="readingTool === 2">
<colorMap v-show="isFusion" ref="colorMap" :unit="fusionOverlayModality === 'NM' ? 'counts' : 'g/ml'"
:modality="fusionOverlayModality" @setColorMap="setColorMap" @voiChange="voiChange" />
<colorMap
v-show="isFusion"
ref="colorMap"
:modality="fusionOverlayModality"
@setColorMap="setColorMap"
@voiChange="voiChange" />
</template>
</div>

View File

@ -34,7 +34,7 @@
<el-input v-model="range" size="mini" style="width:120px" :maxlength="maxLength"
oninput="if(value){value=value.replace(/[^\d]/g,'')} if(value<=0){value=''}"
@change="upperRangeChange">
<template slot="append">{{ unit }}</template>
<template slot="append">g/ml</template>
</el-input>
</div>
<div id="slider" style="position: absolute;left: 6px;top:5px;cursor: pointer;">
@ -55,10 +55,6 @@ const { registerColormap, getColormapNames, getColormap } = csUtils.colormap
export default {
name: "colorMap",
props: {
unit: {
type: String,
default: 'g/ml'
},
modality: {
type: String,
default: ''
@ -163,6 +159,7 @@ export default {
position = parseInt((position / maxLeft) * upper)
if (position > upper) position = upper
if (position < 0) position = 0
if (this.modality === 'NM') {
positionValue.textContent = upper > 0 ? Math.round((position / upper) * 100) + '%' : '0%'
} else {