irc_web/.svn/pristine/7b/7b1fd66c7d62e85c3d138f74f59...

1550 lines
50 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<template>
<div v-loading="loading" class="dicom-viewer-container">
<!-- 工具条 -->
<div class="dicom-tools">
<!-- 窗宽窗位 -->
<el-tooltip class="item" effect="dark" :content="`${$t('trials:reading:button:wwwc')}`" placement="bottom">
<div class="tool-wrapper">
<div
class="icon"
:class="[activeTool==='WindowLevel'?'tool_active':'']"
data-tool="Zoom"
>
<svg-icon icon-class="reverse" class="svg-icon" @click.prevent="setBasicToolActive('WindowLevel')" />
</div>
<!-- 调窗 -->
<div class="text">{{ $t('trials:reading:button:wwwc') }}</div>
</div>
</el-tooltip>
<!-- 反色 -->
<el-tooltip class="item" effect="dark" :content="`${$t('trials:reading:button:reverseColor')}`" placement="bottom">
<div class="tool-wrapper">
<div
class="icon"
data-tool="reverse"
>
<svg-icon icon-class="reversecolor" class="svg-icon" @click.prevent="toggleInvert" />
</div>
<!-- 调窗 -->
<div class="text">{{ $t('trials:reading:button:reverseColor') }}</div>
</div>
</el-tooltip>
<!-- 缩放 -->
<el-tooltip class="item" effect="dark" :content="`${$t('trials:reading:button:zoom')}`" placement="bottom">
<div class="tool-wrapper">
<div
class="icon"
:class="[activeTool==='Zoom'?'tool_active':'']"
data-tool="Zoom"
>
<svg-icon icon-class="magnifier" class="svg-icon" @click.prevent="setBasicToolActive('Zoom')" />
</div>
<!-- 缩放 -->
<div class="text">{{ $t('trials:reading:button:zoom') }}</div>
</div>
</el-tooltip>
<!-- 移动 -->
<el-tooltip class="item" effect="dark" :content="`${$t('trials:reading:button:move')}`" placement="bottom">
<div class="tool-wrapper">
<div
class="icon"
:class="[activeTool==='Pan'?'tool_active':'']"
data-tool="Pan"
>
<svg-icon icon-class="move" class="svg-icon" @click.prevent="setBasicToolActive('Pan')" />
</div>
<!-- 移动 -->
<div class="text">{{ $t('trials:reading:button:move') }}</div>
</div>
</el-tooltip>
<!-- 旋转 -->
<el-tooltip class="item" effect="dark" :content="`${$t('trials:reading:button:rotate')}`" placement="bottom">
<div class="tool-wrapper" @click.stop="showPanel($event)" @mouseleave="handleMouseout">
<div class="dropdown">
<div
class="icon"
:class="[activeTool==='Rotate'?'tool_active':'']"
data-tool="Pan"
>
<svg-icon icon-class="rotate" class="svg-icon" @click.prevent="setBasicToolActive('TrackballRotate')" />
<i class="el-icon-arrow-down" style="color:#fff;" />
</div>
<!-- 移动 -->
<div class="text">{{ $t('trials:reading:button:rotate') }}</div>
<div class="dropdown-content">
<ul style="width:100px;">
<li v-for="rotate in rotateArr" :key="rotate.label" style="text-align:left;">
<a href="#" @click.prevent="setDicomCanvasRotate(rotate.val)">{{ rotate.label }}</a>
</li>
</ul>
</div>
</div>
</div>
</el-tooltip>
<!-- 椭圆oval -->
<template v-for="tool in measuredTools">
<el-tooltip v-if="isCurrentTask && readingTaskState !== 2" :key="tool.toolName" class="item" effect="dark" :content="tool.text" placement="bottom">
<div class="tool-wrapper">
<div
class="icon"
:class="[activeTool===tool.toolName?'tool_active':'']"
>
<svg-icon :icon-class="tool.icon" class="svg-icon" @click.prevent="setMeasureToolActive(tool.toolName)" />
</div>
<div class="text">{{ tool.text }}</div>
</div>
</el-tooltip>
</template>
<!-- <el-tooltip v-if="isCurrentTask" class="item" effect="dark" content="圆形测量" placement="bottom">
<div class="tool-wrapper">
<div
class="icon"
:class="[activeTool==='Probe'?'tool_active':'']"
data-tool="Probe"
>
<svg-icon icon-class="oval" class="svg-icon" @click.prevent="setBasicToolActive('Probe')" />
</div>
<div class="text">探针</div>
</div>
</el-tooltip> -->
<!-- 重置 -->
<el-tooltip class="item" effect="dark" :content="`${$t('trials:reading:button:reset')}`" placement="bottom">
<div class="tool-wrapper">
<div
class="icon"
@click.prevent="resetViewport"
>
<svg-icon icon-class="refresh" class="svg-icon" />
</div>
<div class="text">{{ $t('trials:reading:button:reset') }}</div>
</div>
</el-tooltip>
<!-- 截屏 -->
<!-- <el-tooltip class="item" effect="dark" :content="`${$t('trials:reading:button:screenShot')}`" placement="bottom">
<div class="tool-wrapper">
<div
class="icon"
@click.prevent="saveImage"
>
<svg-icon icon-class="image" class="svg-icon" />
</div>
<div class="text">{{ $t('trials:reading:button:screenShot') }}</div>
</div>
</el-tooltip> -->
<!-- 椭圆oval -->
</div>
<div class="dicom-datas">
<!-- 影像 -->
<div class="dicom-container box box_2_2">
<Viewport
ref="CT_AXIAL"
:index="1"
:active-index="activeIndex"
:is-reading-show-subject-info="isReadingShowSubjectInfo"
:series-info="ctSeries"
:rendering-engine-id="renderingEngineId"
viewport-id="CT_AXIAL"
:volume="ctVolume"
:measure-datas="measureDatas"
/>
<Viewport
ref="PT_AXIAL"
:index="2"
:active-index="activeIndex"
:is-reading-show-subject-info="isReadingShowSubjectInfo"
:series-info="petSeries"
:rendering-engine-id="renderingEngineId"
viewport-id="PT_AXIAL"
:volume="ptVolume"
:measure-datas="measureDatas"
/>
<Viewport
ref="FUSION_AXIAL"
:index="3"
:active-index="activeIndex"
:is-reading-show-subject-info="isReadingShowSubjectInfo"
:series-info="petSeries"
:rendering-engine-id="renderingEngineId"
viewport-id="FUSION_AXIAL"
:volume="ptVolume"
:measure-datas="measureDatas"
/>
<Viewport
:index="4"
:active-index="activeIndex"
:is-reading-show-subject-info="isReadingShowSubjectInfo"
:series-info="petSeries"
:rendering-engine-id="renderingEngineId"
viewport-id="PET_MIP_CORONAL"
:measure-datas="measureDatas"
/>
</div>
<!-- 表单 -->
<div class="form-container" style="overflow-y: auto;">
<div style="color:#fff;padding: 0 5px;">
<h3 v-if="isReadingShowSubjectInfo" style="color: #ddd;padding: 5px 0px;margin: 0;">
<span v-if="subjectCode">{{ subjectCode }} </span>
<span style="margin-left:5px;">{{ taskBlindName }}</span>
</h3>
<Questions ref="questions" @setNonTargetMeasurementStatus="setNonTargetMeasurementStatus" />
<TableQuestions ref="tableQuestions" />
</div>
</div>
</div>
</div>
</template>
<script>
import {
RenderingEngine,
Enums,
setVolumesForViewports,
volumeLoader,
getRenderingEngine,
eventTarget,
cache,
imageLoader,
utilities as csUtils
} from '@cornerstonejs/core'
import * as cornerstone3D from '@cornerstonejs/core'
import * as cornerstoneTools from '@cornerstonejs/tools'
import initLibraries from './js/initLibraries'
import { createImageIdsAndCacheMetaData } from './js/createImageIdsAndCacheMetaData'
import setCtTransferFunctionForVolumeActor from './js/setCtTransferFunctionForVolumeActor'
import setPetTransferFunctionForVolumeActor from './js/setPetTransferFunctionForVolumeActor'
import setPetColorMapTransferFunctionForVolumeActor from './js/setPetColorMapTransferFunctionForVolumeActor'
import store from '@/store'
import { mapGetters, mapMutations } from 'vuex'
import { changeURLStatic } from '@/utils/history.js'
import Viewport from './Viewport'
import Questions from './Questions'
import TableQuestions from './TableQuestions'
import { getTableAnswerRowInfoList } from '@/api/trials'
import FusionEvent from './FusionEvent'
// import CircleROITool from './tools/CircleROITool'
const {
ToolGroupManager,
Enums: csToolsEnums,
WindowLevelTool,
PanTool,
ZoomTool,
StackScrollMouseWheelTool,
synchronizers,
MIPJumpToClickTool,
VolumeRotateMouseWheelTool,
// SynchronizerManager,
// LengthTool,
EllipticalROITool,
CircleROITool,
CrosshairsTool,
TrackballRotateTool,
ProbeTool,
ScaleOverlayTool,
utilities,
// eslint-disable-next-line no-unused-vars
annotation,
// eslint-disable-next-line no-unused-vars
cursors
// stateManagement
} = cornerstoneTools
const { MouseBindings } = csToolsEnums
const { ViewportType, BlendModes } = Enums
const { createCameraPositionSynchronizer, createVOISynchronizer } = synchronizers
let renderingEngine
const renderingEngineId = 'myRenderingEngine'
const volumeLoaderScheme = 'cornerstoneStreamingImageVolume' // Loader id which defines which volume loader to use
const ctVolumeName = 'CT_VOLUME_ID' // Id of the volume less loader prefix
const ctVolumeId = `${volumeLoaderScheme}:${ctVolumeName}` // VolumeId with loader id + volume id
const ptVolumeName = 'PT_VOLUME_ID'
const ptVolumeId = `${volumeLoaderScheme}:${ptVolumeName}`
const ctToolGroupId = 'CT_TOOLGROUP_ID'
const ptToolGroupId = 'PT_TOOLGROUP_ID'
const fusionToolGroupId = 'FUSION_TOOLGROUP_ID'
const mipToolGroupUID = 'MIP_TOOLGROUP_ID'
var axialCameraPositionSynchronizer
var ctVoiSynchronizer
var ptVoiSynchronizer
const viewportIds = {
CT: { AXIAL: 'CT_AXIAL', SAGITTAL: 'CT_SAGITTAL', CORONAL: 'CT_CORONAL' },
PT: { AXIAL: 'PT_AXIAL', SAGITTAL: 'PT_SAGITTAL', CORONAL: 'PT_CORONAL' },
FUSION: {
AXIAL: 'FUSION_AXIAL',
SAGITTAL: 'FUSION_SAGITTAL',
CORONAL: 'FUSION_CORONAL'
},
PETMIP: {
CORONAL: 'PET_MIP_CORONAL'
}
}
var element_ct
var element_pet
var element_fusion
var element_mip
var ctToolGroup
var ptToolGroup
var fusionToolGroup
var mipToolGroup
const axialCameraSynchronizerId = 'AXIAL_CAMERA_SYNCHRONIZER_ID'
const ctVoiSynchronizerId = 'CT_VOI_SYNCHRONIZER_ID'
const ptVoiSynchronizerId = 'PT_VOI_SYNCHRONIZER_ID'
export default {
name: 'Fusion',
components: {
Viewport,
Questions,
TableQuestions
},
data() {
return {
activeIndex: 0,
activeTool: '',
loading: false,
fitType: 0,
rotateArr: [
// { label: this.$t('trials:reading:button:rotateDefault'), val: 1 }, // 默认值
{ label: this.$t('trials:reading:button:rotateVertical'), val: 2 }, // 垂直翻转
{ label: this.$t('trials:reading:button:rotateHorizontal'), val: 3 } // 水平翻转
// { label: this.$t('trials:reading:button:rotateTurnLeft'), val: 4 }, // 左转90度
// { label: this.$t('trials:reading:button:rotateTurnRight'), val: 5 }// 右转90度
],
isCurrentTask: false,
ctSeries: {},
petSeries: {},
renderingEngineId: renderingEngineId,
ctVolumeId: `${volumeLoaderScheme}:${ctVolumeName}`,
ptVolumeId: `${volumeLoaderScheme}:${ptVolumeName}`,
ctVolume: null,
ptVolume: null,
isReadingShowSubjectInfo: false,
subjectCode: '',
taskBlindName: '',
measuredTools: [
// 直径测量
{ toolName: 'CircleROI', text: '圆形测量', icon: 'oval', isDisabled: false,
disabledReason: '' }
],
measureDatas: [],
isInitMeasureDatas: false,
isNonTargetMeasurement: false,
readingTaskState: 2,
isLocate: true
}
},
computed: {
...mapGetters(['visitTaskList'])
},
watch: {
// activeTool: {
// deep: true,
// immediate: false,
// handler(v) {
// if (!v) {
// const elements = [element_ct, element_pet, element_fusion]
// elements.map(el => {
// console.log(cursors)
// // cursors.setCursorForElement(el, 'default')
// // cursors.elementCursor.hideElementCursor(el)
// })
// }
// }
// }
},
mounted() {
window.addEventListener('message', this.receiveMsg)
console.log(cornerstoneTools)
console.log(cornerstone3D)
this.$i18n.locale = this.$route.query.lang
this.setLanguage(this.$route.query.lang)
this.readingTaskState = parseInt(this.$route.query.readingTaskState)
this.isReadingShowSubjectInfo = this.$route.query.isReadingShowSubjectInfo === 'true'
this.subjectCode = this.$route.query.subjectCode
this.taskBlindName = this.$route.query.taskBlindName
this.isCurrentTask = this.$route.query.isReadingShowSubjectInfo === 'true'
const digitPlaces = parseInt(this.$route.query.digitPlaces)
this.digitPlaces = digitPlaces === -1 ? 2 : digitPlaces
element_ct = document.getElementById('viewport1')
element_pet = document.getElementById('viewport2')
element_fusion = document.getElementById('viewport3')
element_mip = document.getElementById('viewport4')
if (this.$router.currentRoute.query.TokenKey) {
store.dispatch('user/setToken', this.$route.query.TokenKey)
changeURLStatic('TokenKey', '')
}
this.handleElementsClick()
this.initPage()
FusionEvent.$on('getAnnotations', () => {
console.log('getAnnotations')
this.getAnnotations()
})
FusionEvent.$on('addOrUpdateAnnotations', (obj) => {
console.log('addOrUpdateAnnotations')
this.addOrUpdateAnnotations(obj)
})
FusionEvent.$on('removeAnnotation', (obj) => {
console.log('removeAnnotation')
this.removeAnnotation(obj)
})
FusionEvent.$on('imageLocation', (obj) => {
console.log('imageLocation')
this.imageLocation(obj)
})
},
destroyed() {
cornerstoneTools.destroy()
eventTarget.reset()
cache.purgeCache()
renderingEngine.destroy()
imageLoader.unregisterAllImageLoaders()
ToolGroupManager.destroyToolGroup('volume')
FusionEvent.$off('getAnnotations')
FusionEvent.$off('addOrUpdateAnnotations')
FusionEvent.$off('removeAnnotation')
FusionEvent.$off('imageLocation')
},
methods: {
initPage() {
const resizeObserver = new ResizeObserver(() => {
if (element_ct.style.width) {
console.log('Size changed')
renderingEngine = getRenderingEngine(renderingEngineId)
if (renderingEngine) {
this.$nextTick(() => {
renderingEngine.resize(true, false)
})
}
}
})
const elements = [
element_ct,
element_pet,
element_fusion
]
elements.forEach((element) => {
element.oncontextmenu = (e) => e.preventDefault()
resizeObserver.observe(element)
})
element_mip.oncontextmenu = (e) => e.preventDefault()
resizeObserver.observe(element_mip)
this.$nextTick(() => {
this.run()
})
},
async run() {
this.loading = true
// 初始化Cornerstone和相关库
await initLibraries()
renderingEngine = new RenderingEngine(renderingEngineId)
this.ctSeries = JSON.parse(sessionStorage.getItem('ctSeriesInfo'))
this.petSeries = JSON.parse(sessionStorage.getItem('petSeriesInfo'))
await this.getImages()
// 设置viewport
await this.setUpDisplay()
// 设置viewportTools and synchronizers
this.setUpToolGroups()
this.setUpSynchronizers()
this.$refs['CT_AXIAL'].scroll(0)
this.$refs['PT_AXIAL'].scroll(0)
this.$refs['FUSION_AXIAL'].scroll(0)
await this.getAnnotations()
eventTarget.addEventListener(cornerstoneTools.Enums.Events.ANNOTATION_ADDED, (e) => {
this.onAnnotationAdded(e)
})
const debouncedCallback = this.debounce((e) => {
this.onAnnotationModified(e)
}, 120)
eventTarget.addEventListener(cornerstoneTools.Enums.Events.ANNOTATION_MODIFIED, (e) => {
debouncedCallback(e)
})
this.loading = false
},
getAnnotations() {
return new Promise(resolve => {
annotation.state.removeAllAnnotations()
this.isInitMeasureDatas = false
var visitTaskId = this.$route.query.visitTaskId
getTableAnswerRowInfoList(visitTaskId).then(res => {
var arr = []
res.Result.forEach(el => {
if (el.OtherMeasureData) {
el.OtherMeasureData = JSON.parse(el.OtherMeasureData)
el.OtherMeasureData.invalidated = false
// el.OtherMeasureData.highlighted = false
if (this.readingTaskState === 2) {
el.OtherMeasureData.isLocked = true
}
el.OtherMeasureData.data.remark = el.OrderMarkName
const viewport = renderingEngine.getViewport('PT_AXIAL')
annotation.state.addAnnotation(el.OtherMeasureData, viewport.element)
}
arr.push(el)
})
this.measureDatas = arr
this.isInitMeasureDatas = true
resolve()
})
})
},
addOrUpdateAnnotations(obj) {
var idx = this.measureDatas.findIndex(item => item.QuestionId === obj.data.QuestionId)
if (idx > -1) {
for (const k in this.measureDatas[idx]) {
if (obj.data[k]) {
this.measureDatas[idx][k] = obj.data[k]
}
}
console.log('更新标记成功', idx)
} else {
this.measureDatas.push(obj.data)
console.log('新增标记成功')
}
},
removeAnnotation(obj) {
const { otherMeasureData, type } = obj
const { annotationUID } = otherMeasureData
const i = this.measureDatas.findIndex(item => item.OtherMeasureData.annotationUID === annotationUID)
if (i > -1) {
if (type === 'delete') {
this.measureDatas.splice(i, 1)
} else {
if (this.measureDatas[i].FristAddTaskId) {
this.measureDatas[i].OtherMeasureData = ''
} else {
this.measureDatas.splice(i, 1)
}
annotation.state.removeAnnotation(annotationUID)
renderingEngine.render()
}
}
},
onAnnotationAdded(e) {
this.isLocate = false
const { detail } = e
const { annotation } = detail
const { metadata } = annotation
const measureData = {}
measureData.frame = 0
measureData.data = annotation
measureData.type = metadata.toolName
if (this.isNonTargetMeasurement || annotation.data.remark === 'Liver' || annotation.data.remark === 'Lung') {
this.$refs['questions'].setMeasuredData(measureData)
} else {
this.$refs['tableQuestions'].setMeasuredData(measureData)
}
this.setToolMode('passive', metadata.toolName)
this.activeTool = ''
},
onAnnotationModified(e) {
if (this.isLocate) {
this.isLocate = false
return
}
const { detail } = e
const { annotation } = detail
const { metadata, data } = annotation
var idx = this.measureDatas.findIndex(item => item.OtherMeasureData && item.OtherMeasureData.data.remark === data.remark)
if (idx > -1) {
var questionInfo = this.measureDatas[idx]
const measureData = {}
measureData.frame = 0
measureData.data = annotation
measureData.type = metadata.toolName
measureData.suvMax = data.cachedStats[`volumeId:${ptVolumeId}`] && data.cachedStats[`volumeId:${ptVolumeId}`].max ? data.cachedStats[`volumeId:${ptVolumeId}`].max.toFixed(this.digitPlaces) : null
measureData.modalityUnit = data.cachedStats[`volumeId:${ptVolumeId}`].modalityUnit
if (data.remark === 'Liver' || data.remark === 'Lung') {
this.$refs['questions'].setMeasuredData(measureData)
this.isNonTargetMeasurement = false
} else {
this.$refs['tableQuestions'].modifyMeasuredData({ measureData, questionInfo })
}
this.setToolMode('passive', metadata.toolName)
if (this.activeTool) {
this.activeTool = ''
}
}
},
setNonTargetMeasurementStatus(obj) {
this.isNonTargetMeasurement = obj.status
if (obj.toolName) {
this.setMeasureToolActive(obj.toolName)
}
},
getImageInfo(viewportId, referencedImageId) {
if (!viewportId || !referencedImageId) return
var imageArr = referencedImageId.split('/')
var instanceId = imageArr[imageArr.length - 1]
if (viewportId === 'CT_AXIAL') {
return { studyId: this.ctSeries.studyId, seriesId: this.ctSeries.seriesId, instanceId, viewportId }
} else if (viewportId === 'PT_AXIAL') {
return { studyId: this.petSeries.studyId, seriesId: this.petSeries.seriesId, instanceId, viewportId }
} else {
return { studyId: this.petSeries.studyId, seriesId: this.petSeries.seriesId, instanceId, viewportId }
}
},
debounce(callback, delay) {
let timerId
return function() {
clearTimeout(timerId)
timerId = setTimeout(() => {
callback.apply(this, arguments)
}, delay)
}
},
async getImages() {
// .splice(0, 10)
const ctImageIds = await createImageIdsAndCacheMetaData({
modality: 'CT',
imageIds: this.ctSeries.imageIds
})
const ptImageIds = await createImageIdsAndCacheMetaData({
modality: 'PT',
imageIds: this.petSeries.imageIds
})
this.ctVolume = await volumeLoader.createAndCacheVolume(ctVolumeId, {
imageIds: ctImageIds
})
this.ptVolume = await volumeLoader.createAndCacheVolume(ptVolumeId, {
imageIds: ptImageIds
})
},
setUpToolGroups() {
cornerstoneTools.addTool(WindowLevelTool)
cornerstoneTools.addTool(ZoomTool)
cornerstoneTools.addTool(StackScrollMouseWheelTool)
cornerstoneTools.addTool(MIPJumpToClickTool)
cornerstoneTools.addTool(VolumeRotateMouseWheelTool)
cornerstoneTools.addTool(EllipticalROITool)
cornerstoneTools.addTool(CircleROITool)
cornerstoneTools.addTool(CrosshairsTool)
cornerstoneTools.addTool(TrackballRotateTool)
cornerstoneTools.addTool(ProbeTool)
cornerstoneTools.addTool(PanTool)
cornerstoneTools.addTool(ScaleOverlayTool)
ctToolGroup = ctToolGroup = ToolGroupManager.createToolGroup(ctToolGroupId)
ctToolGroup.addViewport(viewportIds.CT.AXIAL, renderingEngineId)
ptToolGroup = ToolGroupManager.createToolGroup(ptToolGroupId)
ptToolGroup.addViewport(viewportIds.PT.AXIAL, renderingEngineId)
fusionToolGroup = ToolGroupManager.createToolGroup(fusionToolGroupId)
fusionToolGroup.addViewport(viewportIds.FUSION.AXIAL, renderingEngineId)
const toolGroups = [ctToolGroup, ptToolGroup]
toolGroups.forEach((toolGroup) => {
toolGroup.addTool(PanTool.toolName)
toolGroup.addTool(ZoomTool.toolName)
toolGroup.addTool(StackScrollMouseWheelTool.toolName)
toolGroup.addTool(EllipticalROITool.toolName)
toolGroup.addTool(CircleROITool.toolName, {
getTextLines: this.getTextLines
})
toolGroup.addTool(WindowLevelTool.toolName)
toolGroup.addTool(ProbeTool.toolName)
toolGroup.addTool(ScaleOverlayTool.toolName)
})
fusionToolGroup.addTool(PanTool.toolName)
fusionToolGroup.addTool(ZoomTool.toolName)
fusionToolGroup.addTool(StackScrollMouseWheelTool.toolName)
fusionToolGroup.addTool(EllipticalROITool.toolName, {
volumeId: ptVolumeId
})
fusionToolGroup.addTool(CircleROITool.toolName, {
volumeId: ptVolumeId,
getTextLines: this.getTextLines
})
fusionToolGroup.addTool(ProbeTool.toolName)
fusionToolGroup.addTool(ScaleOverlayTool.toolName)
// Here is the difference in the toolGroups used, that we need to specify the
// volume to use for the WindowLevelTool for the fusion viewports
fusionToolGroup.addTool(WindowLevelTool.toolName, {
volumeId: ptVolumeId
});
[ctToolGroup, ptToolGroup, fusionToolGroup].forEach((toolGroup) => {
// toolGroup.setToolActive(ProbeTool.toolName, {
// bindings: [
// {
// mouseButton: MouseBindings.Primary // Left Click
// }
// ]
// })
toolGroup.setToolActive(PanTool.toolName, {
bindings: [
{
mouseButton: MouseBindings.Auxiliary // Middle Click
}
]
})
toolGroup.setToolActive(ZoomTool.toolName, {
bindings: [
{
mouseButton: MouseBindings.Secondary // Right Click
}
]
})
toolGroup.setToolActive(StackScrollMouseWheelTool.toolName)
// toolGroup.setToolPassive(EllipticalROITool.toolName)
if (this.readingTaskState === 2) {
toolGroup.setToolEnabled(CircleROITool.toolName)
} else {
toolGroup.setToolPassive(CircleROITool.toolName)
}
// toolGroup.setToolPassive(ProbeTool.toolName)
// toolGroup.setToolPassive(WindowLevelTool.toolName)
// toolGroup.setToolEnabled(ScaleOverlayTool.toolName)
})
// MIP Tool Groups
mipToolGroup = null
if (!ToolGroupManager.getToolGroup(mipToolGroupUID)) {
mipToolGroup = ToolGroupManager.createToolGroup(mipToolGroupUID)
} else {
mipToolGroup = ToolGroupManager.getToolGroup(mipToolGroupUID)
}
mipToolGroup.addTool('VolumeRotateMouseWheel')
mipToolGroup.addTool('MIPJumpToClickTool', {
//
toolGroupId: ptToolGroupId
})
// Set the initial state of the tools, here we set one tool active on left click.
// This means left click will draw that tool.
mipToolGroup.setToolActive('MIPJumpToClickTool', {
bindings: [
{
mouseButton: MouseBindings.Primary // Left ClickR
}
]
})
// As the Stack Scroll mouse wheel is a tool using the `mouseWheelCallback`
// hook instead of mouse buttons, it does not need to assign any mouse button.
mipToolGroup.setToolActive('VolumeRotateMouseWheel')
mipToolGroup.addViewport(viewportIds.PETMIP.CORONAL, renderingEngineId)
},
getTextLines(data, targetId) {
const cachedVolumeStats = data.cachedStats[targetId]
const {
radius,
radiusUnit,
area,
mean,
stdDev,
max,
isEmptyArea,
// Modality,
areaUnit,
modalityUnit
} = cachedVolumeStats
const textLines = []
if (data.remark) {
textLines.push(data.remark)
}
if (radius) {
const radiusLine = isEmptyArea
? `Radius: Oblique not supported`
: `Radius: ${this.roundNumber(radius)} ${radiusUnit}`
textLines.push(radiusLine)
}
if (area) {
const areaLine = isEmptyArea
? `Area: Oblique not supported`
: `Area: ${this.roundNumber(area)} ${areaUnit}`
textLines.push(areaLine)
}
if (mean) {
textLines.push(`Mean: ${this.roundNumber(mean)} ${modalityUnit}`)
}
if (max) {
textLines.push(`Max: ${this.roundNumber(max)} ${modalityUnit}`)
}
if (stdDev) {
textLines.push(`Std Dev: ${this.roundNumber(stdDev)} ${modalityUnit}`)
}
return textLines
},
roundNumber(value) {
if (value === undefined || value === null || value === '') {
return 'NaN'
}
value = Number(value)
return value.toFixed(this.digitPlaces)
},
// 设置测量工具启用(不会对输入作出反应)
setBasicToolActive(toolName) {
var toolGroupIds = [ctToolGroupId, ptToolGroupId, fusionToolGroupId]
toolGroupIds.forEach((toolGroupId) => {
const toolGroup = ToolGroupManager.getToolGroup(toolGroupId)
if (this.activeTool === toolName) {
toolGroup.setToolPassive(toolName)
} else {
if (this.activeTool) {
toolGroup.setToolPassive(this.activeTool)
}
var bindings = []
if (toolName === 'Pan') {
bindings = [
{
mouseButton: MouseBindings.Auxiliary // Middle Click
},
{
mouseButton: MouseBindings.Primary // Left Click
}
]
} else if (toolName === 'Zoom') {
bindings = [
{
mouseButton: MouseBindings.Secondary // Right Click
},
{
mouseButton: MouseBindings.Primary // Left Click
}
]
} else {
bindings = [
{
mouseButton: MouseBindings.Primary // Left Click
}
]
}
toolGroup.setToolActive(toolName, {
bindings: bindings
})
// if (toolName === 'CircleROI') {
// toolGroup.setToolConfiguration(CircleROITool.toolName, {
// // centerPointRadius: 4,
// disableCursor: true
// })
// }
}
})
if (this.activeTool === toolName) {
this.activeTool = ''
} else {
this.activeTool = toolName
}
},
setMeasureToolActive(toolName) {
var toolObj = this.$refs['tableQuestions'].isCanActiveTool(toolName)
if (!toolObj || toolObj.isDisabled) return
this.setBasicToolActive(toolName)
},
// 鼠标移入测量工具时,判断工具是否可激活
enter(e, toolName) {
var i = this.measuredTools.findIndex(item => item.toolName === toolName)
if (i === -1) return
var isCurrentTask = this.isCurrentTask
var readingTaskState = this.readingTaskState
if (!isCurrentTask || readingTaskState >= 2) {
this.measuredTools[i].isDisabled = true
e.target.style.cursor = 'not-allowed'
if (this.activeTool) {
this.setToolMode('enabled', toolName)
this.activeTool = ''
}
} else {
var obj = this.$refs['tableQuestions'].isCanActiveTool(toolName, true)
this.measuredTools[i].disabledReason = obj.reason
if (!obj.isCanActiveTool) {
if (this.activeTool === toolName) {
this.setToolMode('passive', toolName)
this.activeTool = ''
}
this.measuredTools[i].isDisabled = true
e.target.style.cursor = 'not-allowed'
} else {
this.measuredTools[i].isDisabled = false
e.target.style.cursor = 'pointer'
}
}
},
setToolMode(mode, toolName) {
var toolGroupIds = [ctToolGroupId, ptToolGroupId, fusionToolGroupId]
toolGroupIds.forEach((toolGroupId) => {
const toolGroup = ToolGroupManager.getToolGroup(toolGroupId)
toolGroup.setToolPassive(toolName)
if (mode === 'enabled') {
toolGroup.setToolEnabled(toolName)
} else if (mode === 'passive') {
toolGroup.setToolPassive(toolName)
}
})
},
// 截屏
saveImage() {
// const canvas = this.canvas.querySelector('canvas')
// var pictureBaseStr = canvas.toDataURL('image/png', 1)
// return pictureBaseStr
},
setUpSynchronizers() {
// const axialCameraSynchronizerId = 'AXIAL_CAMERA_SYNCHRONIZER_ID'
// const ctVoiSynchronizerId = 'CT_VOI_SYNCHRONIZER_ID'
// const ptVoiSynchronizerId = 'PT_VOI_SYNCHRONIZER_ID'
axialCameraPositionSynchronizer = createCameraPositionSynchronizer(
axialCameraSynchronizerId
)
ctVoiSynchronizer = createVOISynchronizer(ctVoiSynchronizerId)
ptVoiSynchronizer = createVOISynchronizer(ptVoiSynchronizerId);
// Add viewports to camera synchronizers
[
viewportIds.CT.AXIAL,
viewportIds.PT.AXIAL,
viewportIds.FUSION.AXIAL
].forEach((viewportId) => {
axialCameraPositionSynchronizer.add({
renderingEngineId,
viewportId
})
});
// Add viewports to VOI synchronizers
[
viewportIds.CT.AXIAL
].forEach((viewportId) => {
ctVoiSynchronizer.add({
renderingEngineId,
viewportId
})
});
[
viewportIds.FUSION.AXIAL
].forEach((viewportId) => {
// In this example, the fusion viewports are only targets for CT VOI
// synchronization, not sources
ctVoiSynchronizer.addTarget({
renderingEngineId,
viewportId
})
});
[
viewportIds.PT.AXIAL,
viewportIds.FUSION.AXIAL,
viewportIds.PETMIP.CORONAL
].forEach((viewportId) => {
ptVoiSynchronizer.add({
renderingEngineId,
viewportId
})
})
},
async setUpDisplay() {
// 创建 viewports
const viewportInputArray = [
{
viewportId: viewportIds.CT.AXIAL,
type: ViewportType.ORTHOGRAPHIC,
element: element_ct,
defaultOptions: {
orientation: Enums.OrientationAxis.AXIAL
// background: [0, 0, 0]
}
},
{
viewportId: viewportIds.PT.AXIAL,
type: ViewportType.ORTHOGRAPHIC,
element: element_pet,
defaultOptions: {
orientation: Enums.OrientationAxis.AXIAL,
background: [1, 1, 1]
}
},
{
viewportId: viewportIds.FUSION.AXIAL,
type: ViewportType.ORTHOGRAPHIC,
element: element_fusion,
defaultOptions: {
orientation: Enums.OrientationAxis.AXIAL
// background: [0, 0, 0]
}
},
{
viewportId: viewportIds.PETMIP.CORONAL,
type: ViewportType.ORTHOGRAPHIC,
element: element_mip,
defaultOptions: {
orientation: Enums.OrientationAxis.CORONAL,
background: [1, 1, 1]
}
}
]
renderingEngine.setViewports(viewportInputArray)
// Set the volumes to load
this.ptVolume.load()
this.ctVolume.load()
// Set volumes on the viewports
await setVolumesForViewports(
renderingEngine,
[
{
volumeId: ctVolumeId,
callback: setCtTransferFunctionForVolumeActor
}
],
[viewportIds.CT.AXIAL]
)
await setVolumesForViewports(
renderingEngine,
[
{
volumeId: ptVolumeId,
callback: setPetTransferFunctionForVolumeActor
}
],
[viewportIds.PT.AXIAL]
)
await setVolumesForViewports(
renderingEngine,
[
{
volumeId: ctVolumeId,
callback: setCtTransferFunctionForVolumeActor
},
{
volumeId: ptVolumeId,
callback: setPetColorMapTransferFunctionForVolumeActor
}
],
[viewportIds.FUSION.AXIAL]
)
// Calculate size of fullBody pet mip
const ptVolumeDimensions = this.ptVolume.dimensions
// Only make the MIP as large as it needs to be.
const slabThickness = Math.sqrt(
ptVolumeDimensions[0] * ptVolumeDimensions[0] +
ptVolumeDimensions[1] * ptVolumeDimensions[1] +
ptVolumeDimensions[2] * ptVolumeDimensions[2]
)
setVolumesForViewports(
renderingEngine,
[
{
volumeId: ptVolumeId,
callback: setPetTransferFunctionForVolumeActor,
blendMode: BlendModes.MAXIMUM_INTENSITY_BLEND,
slabThickness
}
],
[viewportIds.PETMIP.CORONAL]
)
await this.initializeCameraSync(renderingEngine)
// Render the viewports
renderingEngine.render()
},
initCameraSynchronization(sViewport, tViewport) {
// Initialise the sync as they viewports will have
// Different initial zoom levels for viewports of different sizes.
const camera = sViewport.getCamera()
tViewport.setCamera(camera)
},
async initializeCameraSync(renderingEngine) {
return new Promise(resolve => {
// The fusion scene is the target as it is scaled to both volumes.
// TODO -> We should have a more generic way to do this,
// So that when all data is added we can synchronize zoom/position before interaction.
const axialCtViewport = renderingEngine.getViewport(viewportIds.CT.AXIAL)
const axialPtViewport = renderingEngine.getViewport(viewportIds.PT.AXIAL)
const axialFusionViewport = renderingEngine.getViewport(
viewportIds.FUSION.AXIAL
)
this.initCameraSynchronization(axialFusionViewport, axialCtViewport)
this.initCameraSynchronization(axialFusionViewport, axialPtViewport)
renderingEngine.render()
resolve()
})
},
resetViewport() {
if (this.activeTool) {
this.setToolMode('passive', this.activeTool)
this.activeTool = ''
}
// const viewportIds = [
// 'CT_AXIAL',
// 'PT_AXIAL',
// 'FUSION_AXIAL'
// ]
const viewports = [
{ viewportId: 'CT_AXIAL', volumeId: ctVolumeId },
{ viewportId: 'PT_AXIAL', volumeId: ptVolumeId },
{ viewportId: 'FUSION_AXIAL', volumeId: ptVolumeId }
]
viewports.map(v => {
const { viewportId, volumeId } = v
// Get the rendering engine
const renderingEngine = getRenderingEngine(renderingEngineId)
// Get the volume viewport
const viewport = renderingEngine.getViewport(viewportId)
// Resets the viewport's camera
viewport.resetCamera(true, true, false)
// TODO reset the viewport properties, we don't have API for this.
viewport.render()
const actorEntry = viewport.getActor(volumeId)
const volumeActor = actorEntry.actor
console.log(volumeActor)
// const rgbTransferFunction = volumeActor
// .getProperty()
// .getRGBTransferFunction(0)
// var defaultWindowLevel = this.$refs[viewportId].defaultWindowLevel
// const { windowWidth, windowCenter } = defaultWindowLevel
// const lower = windowCenter - windowWidth / 2.0
// const upper = windowCenter + windowWidth / 2.0
// rgbTransferFunction.setMappingRange(lower, upper)
// console.log(rgbTransferFunction)
})
},
toggleInvert() {
if (this.activeTool) {
this.setToolMode('passive', this.activeTool)
this.activeTool = ''
}
const viewports = [
{ viewportId: 'CT_AXIAL', volumeId: ctVolumeId },
{ viewportId: 'PT_AXIAL', volumeId: ptVolumeId }
// { viewportId: 'FUSION_AXIAL', volumeId: ptVolumeId }
]
viewports.map(v => {
const { viewportId, volumeId } = v
const renderingEngine = getRenderingEngine(renderingEngineId)
// Get the volume viewport
const viewport = (
renderingEngine.getViewport(viewportId)
)
// Get the volume actor from the viewport
const actorEntry = viewport.getActor(volumeId)
const volumeActor = actorEntry.actor
const rgbTransferFunction = volumeActor
.getProperty()
.getRGBTransferFunction(0)
// Todo: implement invert in setProperties
csUtils.invertRgbTransferFunction(rgbTransferFunction)
viewport.render()
})
},
showPanel(e) {
// console.log(e.currentTarget.parentNode.parentNode)
// e.currentTarget.parentNode.parentNode.lastChild.style.display = 'block'
e.currentTarget.firstChild.lastChild.style.display = 'block'
},
handleMouseout(e) {
e.currentTarget.firstChild.lastChild.style.display = 'none'
},
setDicomCanvasRotate(v) {
if (this.activeTool) {
this.setToolMode('passive', this.activeTool)
this.activeTool = ''
}
// rotateArr: [
// { label: this.$t('trials:reading:button:rotateDefault'), val: 1 }, // 默认值
// { label: this.$t('trials:reading:button:rotateVertical'), val: 2 }, // 垂直翻转
// { label: this.$t('trials:reading:button:rotateHorizontal'), val: 3 }, // 水平翻转
// { label: this.$t('trials:reading:button:rotateTurnLeft'), val: 4 }, // 左转90度
// { label: this.$t('trials:reading:button:rotateTurnRight'), val: 5 }// 右转90度
// ],
// Get the rendering engine
const renderingEngine = getRenderingEngine(renderingEngineId)
const viewportIds = [
'CT_AXIAL',
'PT_AXIAL',
'FUSION_AXIAL'
]
viewportIds.forEach(viewportId => {
const viewport = renderingEngine.getViewport(viewportId)
console.log(viewport.getCamera())
const { flipHorizontal, flipVertical } = viewport.getCamera()
// Flip the viewport horizontally
if (v === 2) {
viewport.setCamera({ flipHorizontal: !flipHorizontal })
} else if (v === 3) {
viewport.setCamera({ flipVertical: !flipVertical })
} else if (v === 4) {
viewport.setCamera({ viewAngle: -90 })
} else if (v === 5) {
viewport.setCamera({ viewAngle: 90 })
}
viewport.render()
})
},
handleElementsClick() {
var elements = [element_ct, element_pet, element_fusion, element_mip]
elements.forEach((el, index) => {
el.addEventListener('click', () => {
this.activeIndex = index + 1
})
})
},
imageLocation(obj) {
this.setToolToTarget(obj)
if (!obj.otherMeasureData) return
this.isLocate = true
const { metadata } = obj.otherMeasureData
var imageArr = metadata.referencedImageId.split('/')
var instanceId = imageArr[imageArr.length - 1]
var viewportId = null
// var seriesId = data.seriesId
// var instanceId = data.instanceId
var index = -1
index = this.petSeries.instanceList.findIndex(i => i === instanceId)
if (index > -1) {
viewportId = 'PT_AXIAL'
this.$refs[viewportId].scroll(index)
return
}
index = this.ctSeries.instanceList.findIndex(i => i === instanceId)
if (index > -1) {
viewportId = 'CT_AXIAL'
this.$refs[viewportId].scroll(index)
return
}
},
setToolToTarget(obj) {
if (this.readingTaskState < 2 && obj.markTool && !obj.isMarked) {
this.setBasicToolActive(obj.markTool)
}
},
getTargetIdImage(
targetId,
renderingEngine
) {
if (targetId.startsWith('imageId:')) {
const imageId = targetId.split('imageId:')[1]
const imageURI = utilities.imageIdToURI(imageId)
let viewports = utilities.getViewportsWithImageURI(
imageURI,
renderingEngine.id
)
if (!viewports || !viewports.length) {
return
}
viewports = viewports.filter((viewport) => {
return viewport.getCurrentImageId() === imageId
})
if (!viewports || !viewports.length) {
return
}
return viewports[0].getImageData()
} else if (targetId.startsWith('volumeId:')) {
const volumeId = targetId.split('volumeId:')[1]
const viewports = utilities.getViewportsWithVolumeId(
volumeId,
renderingEngine.id
)
if (!viewports || !viewports.length) {
return
}
return viewports[0].getImageData()
} else {
throw new Error(
'getTargetIdImage: targetId must start with "imageId:" or "volumeId:"'
)
}
},
receiveMsg(event) {
if (event.data.type === 'readingPageUpdate') {
this.$refs['questions'].initList()
this.$refs['tableQuestions'].initList()
this.isLocate = true
this.getAnnotations()
} else if (event.data.type === 'readingPageUpdate') {
this.readingTaskState = event.data.data.readingTaskState
}
},
...mapMutations({ setLanguage: 'lang/setLanguage' })
}
}
</script>
<style lang="scss" scoped>
.dicom-viewer-container{
display:flex;
flex-direction: column;
height: 100%;
background-color: #000;
padding: 5px 2px;
}
::-webkit-scrollbar {
width: 5px;
height: 5px;
}
::-webkit-scrollbar-thumb {
border-radius: 10px;
background: #d0d0d0;
}
.dicom-tools{
box-sizing: border-box;
width: 100%;
height: 61px;
padding: 0 5px;
border: 1px solid #727272;
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
.tool-wrapper{
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
margin-right: 30px;
.icon{
padding: 5px;
border: 1px solid #404040;
cursor: pointer;
text-align: center;
.svg-icon{
font-size:20px;
color:#ddd;
}
}
.text{
position: relative;
font-size: 12px;
margin-top: 5px;
color: #d0d0d0;
display: none;
}
}
.tool_active{
background-color: #607d8b;
}
.tool_disabled{
cursor:not-allowed
}
.icon:hover{
background-color: #607d8b;
}
.dropdown {
position: relative;
display: inline-block;
.icon-content{
display: flex;
align-items: center;
border: 1px solid #404040;
}
.text{
text-align: center;
}
.tool-icon{
padding: 5px;
cursor: pointer;
text-align: center;
.svg-icon{
font-size:20px;
color:#ddd;
}
}
.arrow-icon{
cursor: pointer;
padding: 7px 2px 7px 0px;
}
.arrow-icon:hover{
background-color: #607d8b;
}
.icon-content-d:hover{
background-color: #607d8b;
}
.tool-icon-d{
padding: 5px;
.svg-icon{
font-size:20px;
color:#ddd;
}
}
}
.dropdown-content {
display: none;
position: absolute;
background-color: #383838;
color: #fff;
box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2);
z-index: 9999;
font-size: 12px;
ul{
list-style: none;
margin: 0;
padding: 0;
text-align: center;
li{
a{
display: block;
padding: 5px;
}
}
}
ul li:hover{
background-color: #727272;
}
}
.layout-content ul li{
border-top:1px solid #ddd;
border-left:1px solid #ddd;
}
.layout-content ul .flex_row{
// border: 1px solid #ddd;
display: flex;
justify-content: space-between;
flex-direction: row;
align-items: center;
// padding: 2px;
margin-bottom: 2px;
}
.layout-content ul .flex_column{
display: flex;
justify-content: space-between;
flex-direction: column;
align-items: center;
margin-bottom: 2px;
}
.layout_box_1_1{
flex:1;
// border: 1px solid #ddd;
line-height: 30px;
font-size: 12px;
text-align: center;
border-bottom:1px solid #ddd;
border-right:1px solid #ddd;
// padding: 0 5px;
}
.layout_box_1_2{
flex:1;
line-height: 15px;
font-size: 10px;
text-align: center;
border-bottom:1px solid #ddd;
border-right:1px solid #ddd;
}
.layout-content li .layout_box_1_1 :last-child{
color: red;
}
.layout-content li:hover {
cursor: pointer;
background-color: #727272;
}
}
.dicom-datas{
box-sizing: border-box;
flex: 1;
margin-top: 5px;
height: 100%;
display: flex;
flex-direction: row;
justify-content: flex-start;
overflow: hidden;
.form-container{
width: 350px;
height: 100%;
border: 1px solid #727272;
}
.dicom-container{
box-sizing: border-box;
flex: 1;
height: 100%;
border: 1px solid #727272;
}
.measurement-container{
overflow-y: auto;
}
.box{
display: grid;
box-sizing: border-box;
height: 100%;
padding: 0;
.item{
box-sizing: border-box;
position: relative;
border: 1px solid rgba(255, 255, 255, 0.21);
position: relative;
&_active{
// border: 2px solid #ffeb3b;fff
border: 1px dashed #428bca;
}
}
}
.box_2_2{
grid-template-columns: repeat(2, 50%); //1列占50%
grid-template-rows: repeat(2, 50%); //1行占50%
}
}
</style>