CT/NM融合更改
continuous-integration/drone/push Build is passing Details

uat_us
caiyiling 2026-03-24 14:54:46 +08:00
parent d07adc4948
commit 7870c3d74b
7 changed files with 296 additions and 80 deletions

View File

@ -1,7 +1,12 @@
<template> <template>
<div ref="viewport-fusion" class="viewport-wrapper" v-loading="loading" :element-loading-text="NSTip" <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" element-loading-background="rgba(0, 0, 0, 0.8)" @mouseup="sliderMouseup" @mousemove="sliderMousemove"
@mouseleave="sliderMouseleave" :style="{ color: series.Modality === 'PT' || isMip ? '#666' : '#ddd' }"> @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"
class="opacity-slider" />
</div>
<div v-if="series && taskInfo" class="left-top-text"> <div v-if="series && taskInfo" class="left-top-text">
<div v-if="taskInfo.IsExistsClinicalData && !isMip && !isFusion" class="cd-info" <div v-if="taskInfo.IsExistsClinicalData && !isMip && !isFusion" class="cd-info"
:title="$t('trials:reading:button:clinicalData')"> :title="$t('trials:reading:button:clinicalData')">
@ -104,9 +109,14 @@ import {
import * as cornerstoneTools from '@cornerstonejs/tools' import * as cornerstoneTools from '@cornerstonejs/tools'
import cornerstoneDICOMImageLoader from '@cornerstonejs/dicom-image-loader'; import cornerstoneDICOMImageLoader from '@cornerstonejs/dicom-image-loader';
import vtkColorMaps from '@kitware/vtk.js/Rendering/Core/ColorTransferFunction/ColorMaps' 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 { 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 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 { setPetColorMapTransferFunctionForVolumeActor } from '@/views/trials/trials-panel/reading/dicoms/components/Fusion/js/setPetColorMapTransferFunctionForVolumeActor'
import {
setMipTransferFunctionForVolumeActor,
setPetTransferFunctionForVolumeActor
} from './helpers/index.js'
const { BlendModes, OrientationAxis } = Enums; const { BlendModes, OrientationAxis } = Enums;
const { getColormap } = csUtils.colormap; const { getColormap } = csUtils.colormap;
import { vec3, mat4 } from 'gl-matrix' import { vec3, mat4 } from 'gl-matrix'
@ -172,7 +182,10 @@ export default {
isMove: false isMove: false
}, },
ptVolumeId: null, ptVolumeId: null,
loading: false loading: false,
Colorbar: null,
nmOpacity: 0.6,
nmFusionVolumeActor: null
} }
}, },
mounted() { mounted() {
@ -220,7 +233,9 @@ export default {
}, },
stackNewImage(e) { stackNewImage(e) {
const { detail } = e const { detail } = e
if (this.series) {
this.series.SliceIndex = detail.imageIndex this.series.SliceIndex = detail.imageIndex
}
this.sliderInfo.height = detail.imageIndex * 100 / detail.numberOfSlices this.sliderInfo.height = detail.imageIndex * 100 / detail.numberOfSlices
const renderingEngine = getRenderingEngine(this.renderingEngineId) const renderingEngine = getRenderingEngine(this.renderingEngineId)
const viewport = renderingEngine.getViewport(this.viewportId) const viewport = renderingEngine.getViewport(this.viewportId)
@ -267,7 +282,7 @@ export default {
const viewport = renderingEngine.getViewport(this.viewportId) const viewport = renderingEngine.getViewport(this.viewportId)
let properties = viewport.getProperties() let properties = viewport.getProperties()
if (this.isFusion) { if (this.isFusion) {
properties = viewport.getProperties(this.volumeId) properties = viewport.getProperties(this.ptVolumeId || this.volumeId)
} }
if (properties && properties.voiRange) { if (properties && properties.voiRange) {
var { lower, upper } = properties.voiRange var { lower, upper } = properties.voiRange
@ -445,6 +460,18 @@ export default {
ctx.fillStyle = gradient ctx.fillStyle = gradient
ctx.fillRect(0, 0, rectWidth, rectHeight) 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);
const renderingEngine = getRenderingEngine(this.renderingEngineId)
renderingEngine?.render?.();
},
setPreset(presetName) { setPreset(presetName) {
this.presetName = presetName this.presetName = presetName
}, },
@ -482,13 +509,13 @@ export default {
if (this.series && data.Id === this.series.Id && data.Description === this.series.Description && !isLocate) { if (this.series && data.Id === this.series.Id && data.Description === this.series.Description && !isLocate) {
data.SliceIndex = this.series.SliceIndex data.SliceIndex = this.series.SliceIndex
} }
// this.series = { ...data }
const renderingEngine = getRenderingEngine(this.renderingEngineId) const renderingEngine = getRenderingEngine(this.renderingEngineId)
const viewport = renderingEngine.getViewport(this.viewportId) const viewport = renderingEngine.getViewport(this.viewportId)
if (isLocate) return csUtils.jumpToSlice(viewport.element, { imageIndex: data.SliceIndex }); if (isLocate) return csUtils.jumpToSlice(viewport.element, { imageIndex: data.SliceIndex });
this.volumeId = data.SeriesInstanceUid this.volumeId = data.SeriesInstanceUid
this.ptVolumeId = null this.ptVolumeId = null
this.series = {} this.series = {}
this.nmFusionVolumeActor = null
let { isFusion, isMip, colorMap } = option let { isFusion, isMip, colorMap } = option
this.isFusion = isFusion; this.isFusion = isFusion;
this.isMip = isMip; this.isMip = isMip;
@ -499,16 +526,7 @@ export default {
this.ptVolumeId = `fusion_${this.volumeId}` this.ptVolumeId = `fusion_${this.volumeId}`
let { ct, data } = obj let { ct, data } = obj
this.series = { ...data } this.series = { ...data }
this.ctSeries = { ...ct } let volumes = [
this.petSeries = { ...data }
await viewport
.setVolumes([
{
volumeId: this.volumeId, callback: (r) => {
setPetColorMapTransferFunctionForVolumeActor(r)
console.log("融合pet渲染成功");
}
},
{ {
volumeId: ct.SeriesInstanceUid, callback: (r) => { volumeId: ct.SeriesInstanceUid, callback: (r) => {
setCtTransferFunctionForVolumeActor(r) setCtTransferFunctionForVolumeActor(r)
@ -516,16 +534,26 @@ export default {
} }
}, },
{ {
volumeId: this.ptVolumeId, callback: (r) => { volumeId: data.SeriesInstanceUid, callback: (r) => {
setPetColorMapTransferFunctionForVolumeActor(r) setPetColorMapTransferFunctionForVolumeActor(r)
console.log("融合pet渲染成功"); if (this.series.Modality === 'NM') {
this.nmFusionVolumeActor = r.volumeActor
this.applyNmOpacity()
}
console.log("融合pet渲染成功")
} }
}, },
]).then(res => { ]
if (colorMap) { if (this.series.Modality !== 'NM') {
this.setColorMap(this.presetName) volumes.unshift({
volumeId: this.volumeId, callback: (r) => {
// setPetColorMapTransferFunctionForVolumeActor(r)
console.log("融合pet渲染成功");
} }
}) })
}
await viewport.setVolumes(volumes)
} else { } else {
this.series = { ...data } this.series = { ...data }
if (this.isMip) { if (this.isMip) {
@ -540,7 +568,12 @@ export default {
.setVolumes([{ .setVolumes([{
volumeId: this.volumeId, volumeId: this.volumeId,
callback: (r) => { callback: (r) => {
setPetColorMapTransferFunctionForVolumeActor(r) if (this.series.Modality === 'NM') {
setMipTransferFunctionForVolumeActor({ ...r, volumeId: this.volumeId })
} else {
setPetTransferFunctionForVolumeActor(r)
}
// setPetColorMapTransferFunctionForVolumeActor(r)
console.log("mip渲染成功") console.log("mip渲染成功")
}, },
slabThickness, slabThickness,
@ -548,30 +581,19 @@ export default {
defaultOptions: { defaultOptions: {
orientation: OrientationAxis.CORONAL orientation: OrientationAxis.CORONAL
} }
}]).then(res => { }])
if (colorMap) {
this.setColorMap(this.presetName)
}
if (isLocate) {
setTimeout(() => { csUtils.jumpToSlice(viewport.element, { imageIndex: data.SliceIndex }); })
}
})
} else { } else {
viewport viewport
.setVolumes([{ .setVolumes([{
volumeId: this.volumeId, callback: (r) => { volumeId: this.volumeId, callback: (r) => {
if (this.series.Modality === 'PT') { if (this.series.Modality === 'PT' || this.series.Modality === 'NM') {
setPetColorMapTransferFunctionForVolumeActor(r, true) // setPetColorMapTransferFunctionForVolumeActor(r, true)
setPetTransferFunctionForVolumeActor(r)
} else { } else {
setCtTransferFunctionForVolumeActor(r) setCtTransferFunctionForVolumeActor(r)
} }
console.log("渲染成功")
} }
}]).then(res => { }])
if (colorMap) {
this.setColorMap(this.presetName)
}
})
} }
} }
@ -751,7 +773,11 @@ export default {
e.stopImmediatePropagation() e.stopImmediatePropagation()
e.stopPropagation() e.stopPropagation()
e.preventDefault() e.preventDefault()
} },
},
beforeDestroy() {
this.series = null
this.nmFusionVolumeActor = null
}, },
computed: { computed: {
NSTip() { NSTip() {
@ -767,6 +793,41 @@ export default {
position: relative; position: relative;
cursor: default !important; cursor: default !important;
.opacity-slider-wrapper {
position: absolute;
right: 15px;
top: 50%;
transform: translateY(-50%);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
z-index: 10;
background: rgba(0, 0, 0, 0.5);
padding: 10px;
border-radius: 4px;
width: 40px;
.slider-title {
font-size: 12px;
color: #ddd;
margin-bottom: 10px;
width: 30px;
text-align: center;
}
.opacity-slider {
-webkit-appearance: slider-vertical;
writing-mode: bt-lr;
width: 10px;
height: 150px;
padding: 0;
margin: 0;
cursor: pointer;
outline: none;
}
}
.left-top-text { .left-top-text {
position: absolute; position: absolute;
left: 5px; left: 5px;

View File

@ -273,7 +273,9 @@
</div> </div>
<!-- 伪彩 --> <!-- 伪彩 -->
<template v-if="readingTool === 2"> <template v-if="readingTool === 2">
<colorMap v-show="isFusion" ref="colorMap" @setColorMap="setColorMap" @voiChange="voiChange" /> <colorMap v-show="isFusion" ref="colorMap" :unit="fusionOverlayModality === 'NM' ? 'counts' : 'g/ml'"
:modality="fusionOverlayModality"
@setColorMap="setColorMap" @voiChange="voiChange" />
</template> </template>
</div> </div>
@ -299,11 +301,9 @@
<!-- viewports --> <!-- viewports -->
<div class="viewports-wrapper"> <div class="viewports-wrapper">
<div ref="container" class="grid-container"> <div ref="container" class="grid-container">
<!-- isMPR && index === 2 ? 'grid-cell-3' : '', --> <div :class="['viewports-box', isFusion || isMPR ? 'viewports-box-down' : '', fullScreenIndex !== null ? 'viewports-box-full-screen' : '']" :style="gridStyle">
<div :class="['viewports-box', isFusion || isMPR ? 'viewports-box-down' : '']" :style="gridStyle">
<div v-for="(v, index) in cellsMax" v-show="index < cells.length" :key="`viewport-${index}`" <div v-for="(v, index) in cellsMax" v-show="index < cells.length" :key="`viewport-${index}`"
:style="cellStyle" :class="['grid-cell', isMPR && index === 2 ? 'grid-cell-3' : '', index === activeViewportIndex ? 'cell_active' : '', index === fullScreenIndex ? 'cell-full-screen' : '']"
:class="['grid-cell', index === activeViewportIndex ? 'cell_active' : '', index === fullScreenIndex ? 'cell-full-screen' : '']"
@dblclick="toggleFullScreen($event, index)" @click="activeViewport(index)"> @dblclick="toggleFullScreen($event, index)" @click="activeViewport(index)">
<VolumeViewport :ref="`viewport-${index}`" :data-viewport-uid="`viewport-${index}`" <VolumeViewport :ref="`viewport-${index}`" :data-viewport-uid="`viewport-${index}`"
:rendering-engine-id="renderingEngineId" :viewport-id="`viewport-${index}`" :viewport-index="index" :rendering-engine-id="renderingEngineId" :viewport-id="`viewport-${index}`" :viewport-index="index"
@ -315,10 +315,12 @@
@renderAnnotations="renderAnnotations" @contentMouseup="contentMouseup" v-else /> @renderAnnotations="renderAnnotations" @contentMouseup="contentMouseup" v-else />
</div> </div>
</div> </div>
<div v-if="(criterionType === 0 && readingTool === 0) || readingTool === 3" <div v-if="criterionType === 0 && readingTool === 0 || readingTool === 3"
:class="['viewports-box', !isMPR ? 'viewports-box-down' : '']" :style="gridStyleMPR"> :class="['viewports-box', !isMPR ? 'viewports-box-down' : '', fullScreenIndex !== null ? 'viewports-box-full-screen' : '']" :style="gridStyleMPR">
<div v-for="(v, index) in 3" :key="`viewport-MPR-${index}`" :style="cellStyle" <div v-for="(v, index) in 3" :key="`viewport-MPR-${index}`"
:class="['grid-cell', index === 0 ? 'grid-cell-3' : '', index === activeViewportIndex ? 'cell_active' : '', index === fullScreenIndex ? 'cell-full-screen' : '']" v-show="index < cells.length"
:style="cellStyle"
:class="['grid-cell', isMPR && index === 0 ? 'grid-cell-3' : '', index === activeViewportIndex ? 'cell_active' : '', index === fullScreenIndex ? 'cell-full-screen' : '']"
@dblclick="toggleFullScreen($event, index)" @click="activeViewport(index)"> @dblclick="toggleFullScreen($event, index)" @click="activeViewport(index)">
<MPRViewport :ref="`viewport-MPR-${index}`" :data-viewport-uid="`viewport-MPR-${index}`" <MPRViewport :ref="`viewport-MPR-${index}`" :data-viewport-uid="`viewport-MPR-${index}`"
:rendering-engine-id="renderingEngineId" :viewport-id="`viewport-MPR-${index}`" :rendering-engine-id="renderingEngineId" :viewport-id="`viewport-MPR-${index}`"
@ -327,8 +329,7 @@
@renderAnnotations="renderAnnotations" @contentMouseup="contentMouseup" /> @renderAnnotations="renderAnnotations" @contentMouseup="contentMouseup" />
</div> </div>
</div> </div>
<div v-if="readingTool === 2" <div v-if="readingTool === 2" :class="['viewports-box', !isFusion ? 'viewports-box-down' : '', fullScreenIndex !== null ? 'viewports-box-full-screen' : '']"
:class="['viewports-box', !isFusion ? 'viewports-box-down' : '', fullScreenIndex !== null ? 'viewports-box-full-screen' : '']"
:style="gridStyle"> :style="gridStyle">
<div v-for="(v, index) in cellsMax" v-show="index < cells.length" :key="`viewport-fusion-${index}`" <div v-for="(v, index) in cellsMax" v-show="index < cells.length" :key="`viewport-fusion-${index}`"
:class="['grid-cell', index === activeViewportIndex ? 'cell_active' : '', index === fullScreenIndex ? 'cell-full-screen' : '']" :class="['grid-cell', index === activeViewportIndex ? 'cell_active' : '', index === fullScreenIndex ? 'cell-full-screen' : '']"
@ -500,7 +501,7 @@ import {
RenderingEngine, RenderingEngine,
Enums, Enums,
// imageLoader, // imageLoader,
// metaData, metaData,
volumeLoader, volumeLoader,
getRenderingEngine, getRenderingEngine,
eventTarget, eventTarget,
@ -862,7 +863,7 @@ export default {
} else { } else {
this.tools = getTools(this.criterionType) this.tools = getTools(this.criterionType)
} }
console.log(toolsEvents, 'toolsEvents')
this.trialId = this.$route.query.trialId this.trialId = this.$route.query.trialId
this.readingTaskState = this.taskInfo.ReadingTaskState this.readingTaskState = this.taskInfo.ReadingTaskState
if (!this.taskInfo.IsBaseLine && this.taskInfo.IsReadingTaskViewInOrder !== 0) { if (!this.taskInfo.IsBaseLine && this.taskInfo.IsReadingTaskViewInOrder !== 0) {
@ -1332,6 +1333,7 @@ export default {
if (volumeViewportIds.includes(viewportId)) { if (volumeViewportIds.includes(viewportId)) {
toolGroupId = this.volumeToolGroupId toolGroupId = this.volumeToolGroupId
} }
const toolGroup = ToolGroupManager.getToolGroup(toolGroupId) ? ToolGroupManager.getToolGroup(toolGroupId) : ToolGroupManager.createToolGroup(toolGroupId) const toolGroup = ToolGroupManager.getToolGroup(toolGroupId) ? ToolGroupManager.getToolGroup(toolGroupId) : ToolGroupManager.createToolGroup(toolGroupId)
toolGroup.addViewport(viewportId, renderingEngineId) toolGroup.addViewport(viewportId, renderingEngineId)
toolGroup.addTool(StackScrollTool.toolName, { toolGroup.addTool(StackScrollTool.toolName, {
@ -1668,6 +1670,7 @@ export default {
this.$refs[`ecrf_${this.lastViewportTaskId}`][0].setAnnotation({ annotation, toolName: annotation.metadata.toolName }) this.$refs[`ecrf_${this.lastViewportTaskId}`][0].setAnnotation({ annotation, toolName: annotation.metadata.toolName })
this.markedSeriesIds.push(series.Id) this.markedSeriesIds.push(series.Id)
} }
this.setToolsPassive() this.setToolsPassive()
}, },
annotationModifiedListener(e) { annotationModifiedListener(e) {
@ -1762,6 +1765,7 @@ export default {
// } // }
} }
} }
this.setToolsPassive() this.setToolsPassive()
}, },
validMarkName(markName) { validMarkName(markName) {
@ -3080,7 +3084,7 @@ export default {
} }
if (this.isFusion) { if (this.isFusion) {
this.activeViewportIndex = 0 this.activeViewportIndex = 0
if (series.Modality === 'PT') { if (series.Modality === 'PT' || series.Modality === 'NM') {
this.activeViewportIndex = 2 this.activeViewportIndex = 2
} }
series = { series = {
@ -3137,7 +3141,7 @@ export default {
} }
if (this.isFusion) { if (this.isFusion) {
this.activeViewportIndex = 0 this.activeViewportIndex = 0
if (series.Modality === 'PT') { if (series.Modality === 'PT' || series.Modality === 'NM') {
this.activeViewportIndex = 2 this.activeViewportIndex = 2
} }
series = { series = {
@ -3502,9 +3506,9 @@ export default {
syncColormap: false syncColormap: false
}) })
let viewportIds = [ let viewportIds = [
`viewport-MPR-0`, `viewport-volume-0`,
`viewport-MPR-1`, `viewport-volume-1`,
`viewport-MPR-2` `viewport-volume-2`
] ]
viewportIds.forEach((viewportId) => { viewportIds.forEach((viewportId) => {
MPRVoiSynchronizer.add({ MPRVoiSynchronizer.add({
@ -3528,7 +3532,8 @@ export default {
synchronizer.setEnabled(false); synchronizer.setEnabled(false);
}, },
setColorMap(rgbPresetName) { setColorMap(rgbPresetName) {
const fusionViewportIds = [`viewport-fusion-1`, `viewport-fusion-2`, `viewport-fusion-3`] const fusionViewportIds = [`viewport-fusion-2`]
// const fusionViewportIds = [`viewport-fusion-1`, `viewport-fusion-2`, `viewport-fusion-3`]
fusionViewportIds.forEach(id => { fusionViewportIds.forEach(id => {
this.$refs[id][0].setPreset(rgbPresetName) this.$refs[id][0].setPreset(rgbPresetName)
this.$refs[id][0].renderColorBar(rgbPresetName) this.$refs[id][0].renderColorBar(rgbPresetName)
@ -3590,6 +3595,7 @@ export default {
this.$refs[`ecrf_${this.taskInfo.VisitTaskId}`][0].removeAllNoSaveAnnotation() this.$refs[`ecrf_${this.taskInfo.VisitTaskId}`][0].removeAllNoSaveAnnotation()
} }
const { ct, pt } = data const { ct, pt } = data
this.fusionOverlayModality = pt?.Modality || null
if (ct.ImageIds.length > 400) { if (ct.ImageIds.length > 400) {
let res = await this.getSystemInfo() let res = await this.getSystemInfo()
if (!res) return false if (!res) return false
@ -3638,12 +3644,26 @@ export default {
this.$refs[`viewport-3`][0].setSeriesInfo(pt) this.$refs[`viewport-3`][0].setSeriesInfo(pt)
this.$refs[`viewport-fusion-0`][0].setSeriesInfo(ctData) this.$refs[`viewport-fusion-0`][0].setSeriesInfo(ctData)
this.$refs[`viewport-fusion-1`][0].setSeriesInfo(ptData, false, { colorMap: true }) this.$refs[`viewport-fusion-1`][0].setSeriesInfo(ptData, false, { colorMap: false })
this.$refs[`viewport-fusion-2`][0].setSeriesInfo(fusionData, false, { isFusion: true, colorMap: true }) this.$refs[`viewport-fusion-2`][0].setSeriesInfo(fusionData, false, { isFusion: true, colorMap: true })
this.$refs[`viewport-fusion-3`][0].setSeriesInfo(ptData, false, { isMip: true, colorMap: true }) this.$refs[`viewport-fusion-3`][0].setSeriesInfo(ptData, false, { isMip: true, colorMap: false })
// this.resetAnnotation = false // this.resetAnnotation = false
this.$nextTick(() => { this.$nextTick(() => {
this.$refs[`colorMap`].init() 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.$refs.colorMap.range = Math.round(nmMax)
this.$refs.colorMap.upper = halfMax
this.$refs.colorMap.upperRangeChange(Math.round(nmMax))
this.voiChange(halfMax)
}
}
}) })
} catch (err) { } catch (err) {
console.log(err) console.log(err)
@ -3665,12 +3685,56 @@ export default {
volume = cache.getVolume(volumeId) volume = cache.getVolume(volumeId)
} else { } else {
await this.$refs[`${this.viewportKey}-0`][0].createImageIdsAndCacheMetaData(serie) await this.$refs[`${this.viewportKey}-0`][0].createImageIdsAndCacheMetaData(serie)
volume = await volumeLoader.createAndCacheVolume(volumeId, { imageIds: serie.ImageIds }) let imageIds = this.sortImageIdsByImagePositionPatient(serie.ImageIds)
volume = await volumeLoader.createAndCacheVolume(volumeId, { imageIds: imageIds })
volume.load() volume.load()
} }
res({ volumeId, volume }) res({ volumeId, volume })
}) })
}, },
sortImageIdsByImagePositionPatient(imageIds) {
if (!Array.isArray(imageIds) || imageIds.length < 2) {
return imageIds;
}
const firstPlane = metaData.get('imagePlaneModule', imageIds[0]);
if (!firstPlane?.imagePositionPatient || !firstPlane?.imageOrientationPatient) {
return imageIds;
}
const reference = firstPlane.imagePositionPatient.map((v) => Number(v));
const iop = firstPlane.imageOrientationPatient.map((v) => Number(v));
const row = [iop[0], iop[1], iop[2]];
const col = [iop[3], iop[4], iop[5]];
const normal = [
row[1] * col[2] - row[2] * col[1],
row[2] * col[0] - row[0] * col[2],
row[0] * col[1] - row[1] * col[0],
];
const pairs = [];
for (const imageId of imageIds) {
const plane = metaData.get('imagePlaneModule', imageId);
const ipp = plane?.imagePositionPatient;
if (!ipp) {
return imageIds;
}
const pos = ipp.map((v) => Number(v));
const positionVector = [
reference[0] - pos[0],
reference[1] - pos[1],
reference[2] - pos[2],
];
const distance =
positionVector[0] * normal[0] +
positionVector[1] * normal[1] +
positionVector[2] * normal[2];
pairs.push({ imageId, distance });
}
pairs.sort((a, b) => b.distance - a.distance);
return pairs.map((p) => p.imageId);
},
upperRangeChange(upper) { upperRangeChange(upper) {
this.$refs.colorMap.upper = upper this.$refs.colorMap.upper = upper
this.$refs.colorMap.upperRangeChange(upper) this.$refs.colorMap.upperRangeChange(upper)
@ -3680,6 +3744,7 @@ export default {
}, },
closeFusion() { closeFusion() {
this.fusionVisible = false this.fusionVisible = false
this.fusionOverlayModality = null
}, },
getTrialCriterion() { getTrialCriterion() {
getCriterionReadingInfo({ getCriterionReadingInfo({
@ -4091,17 +4156,17 @@ export default {
} }
.viewports-box-down { .viewports-box-down {
>.grid-cell { > .grid-cell {
border-color: transparent; border-color: transparent;
} }
} }
.viewports-box-full-screen { .viewports-box-full-screen {
>.grid-cell { > .grid-cell {
display: none; display: none;
} }
>.grid-cell.cell-full-screen { > .grid-cell.cell-full-screen {
display: flex; display: flex;
} }
} }

View File

@ -498,7 +498,7 @@ export default {
viewport viewport
.setVolumes([{ .setVolumes([{
volumeId: this.volumeId, callback: (r) => { volumeId: this.volumeId, callback: (r) => {
if (this.series.Modality === 'PT') { if (this.series.Modality === 'PT' || this.series.Modality === 'NM') {
setPetColorMapTransferFunctionForVolumeActor(r, true) setPetColorMapTransferFunctionForVolumeActor(r, true)
} else { } else {
setCtTransferFunctionForVolumeActor(r) setCtTransferFunctionForVolumeActor(r)

View File

@ -30,11 +30,11 @@
</div> </div>
</div> </div>
</div> </div>
<div style="margin-left:-1px;border: 1px solid #424242;"> <div v-if="modality !== 'NM'" style="margin-left:-1px;border: 1px solid #424242;">
<el-input v-model="range" size="mini" style="width:120px" maxlength="3" <el-input v-model="range" size="mini" style="width:120px" :maxlength="maxLength"
oninput="if(value){value=value.replace(/[^\d]/g,'')} if(value<=0){value=''}" oninput="if(value){value=value.replace(/[^\d]/g,'')} if(value<=0){value=''}"
@change="upperRangeChange"> @change="upperRangeChange">
<template slot="append">g/ml</template> <template slot="append">{{ unit }}</template>
</el-input> </el-input>
</div> </div>
<div id="slider" style="position: absolute;left: 6px;top:5px;cursor: pointer;"> <div id="slider" style="position: absolute;left: 6px;top:5px;cursor: pointer;">
@ -54,6 +54,20 @@ import vtkColorMaps from '@kitware/vtk.js/Rendering/Core/ColorTransferFunction/C
const { registerColormap, getColormapNames, getColormap } = csUtils.colormap const { registerColormap, getColormapNames, getColormap } = csUtils.colormap
export default { export default {
name: "colorMap", name: "colorMap",
props: {
unit: {
type: String,
default: 'g/ml'
},
modality: {
type: String,
default: ''
},
maxLength: {
type: [Number, String],
default: 6
}
},
data() { data() {
return { return {
colorMaps: [], colorMaps: [],
@ -111,7 +125,11 @@ export default {
var positionValue = document.getElementById('slider-position') var positionValue = document.getElementById('slider-position')
var upper = this.range var upper = this.range
position = parseInt((position / maxLeft) * upper) position = parseInt((position / maxLeft) * upper)
if (this.modality === 'NM') {
positionValue.textContent = Math.round((position / upper) * 100) + '%'
} else {
positionValue.textContent = position positionValue.textContent = position
}
this.upper = position this.upper = position
this.voiChange(position) this.voiChange(position)
} }
@ -138,7 +156,11 @@ export default {
var slider = document.getElementById('slider') var slider = document.getElementById('slider')
slider.style.left = left + 'px' slider.style.left = left + 'px'
var positionValue = document.getElementById('slider-position') var positionValue = document.getElementById('slider-position')
if (this.modality === 'NM') {
positionValue.textContent = Math.round((this.upper / this.range) * 100) + '%'
} else {
positionValue.textContent = this.upper positionValue.textContent = this.upper
}
}, },
createColorBar(rgbPresetName, elId, width, height) { createColorBar(rgbPresetName, elId, width, height) {
var colorMap = null var colorMap = null

View File

@ -0,0 +1,8 @@
import setPetTransferFunctionForVolumeActor from "./setPetTransferFunctionForVolumeActor";
import setMipTransferFunctionForVolumeActor from "./setMipTransferFunctionForVolumeActor";
export {
setPetTransferFunctionForVolumeActor,
setMipTransferFunctionForVolumeActor
};

View File

@ -0,0 +1,49 @@
import { cache, metaData, utilities } from "@cornerstonejs/core";
import vtkPiecewiseFunction from "@kitware/vtk.js/Common/DataModel/PiecewiseFunction";
export default function setMipTransferFunctionForVolumeActor({
volumeActor,
volumeId,
}) {
const mapper = volumeActor.getMapper?.();
if (mapper?.setSampleDistance) {
mapper.setSampleDistance(1.0);
}
const rgbTransferFunction = volumeActor
.getProperty()
.getRGBTransferFunction(0);
let range = null;
const imageVolume = volumeId ? cache.getVolume(volumeId) : null;
const imageId = imageVolume?.imageIds?.[0];
if (imageId) {
const voiLutModule = metaData.get("voiLutModule", imageId);
const rawCenter = Array.isArray(voiLutModule?.windowCenter)
? voiLutModule.windowCenter[0]
: voiLutModule?.windowCenter;
const rawWidth = Array.isArray(voiLutModule?.windowWidth)
? voiLutModule.windowWidth[0]
: voiLutModule?.windowWidth;
const center = Number(rawCenter);
const width = Number(rawWidth);
if (Number.isFinite(center) && Number.isFinite(width) && width > 0) {
const upper = center + width / 2;
range = [0, upper];
} else if (Number.isFinite(center)) {
range = [0, center];
}
}
if (!range) {
range = [0, 5];
}
rgbTransferFunction.setRange(range[0], range[1]);
utilities.invertRgbTransferFunction(rgbTransferFunction);
const upper = Number(range[1]);
if (Number.isFinite(upper) && upper > 0) {
const ofun = vtkPiecewiseFunction.newInstance();
ofun.addPoint(0, 0.0);
ofun.addPoint(upper * 0.02, 0.9);
ofun.addPoint(upper, 1.0);
volumeActor.getProperty().setScalarOpacity(0, ofun);
}
}

View File

@ -0,0 +1,11 @@
import { utilities } from '@cornerstonejs/core';
export default function setPetTransferFunction({ volumeActor }) {
const rgbTransferFunction = volumeActor
.getProperty()
.getRGBTransferFunction(0);
rgbTransferFunction.setRange(0, 5);
utilities.invertRgbTransferFunction(rgbTransferFunction);
}