质控数据匿名

uat_us
wangxiaoshuang 2026-04-23 15:50:15 +08:00
parent 47b05526a2
commit f0eb814492
5 changed files with 508 additions and 419 deletions

View File

@ -407,4 +407,20 @@ export function changeSegmentationSavedStatus(data) {
method: 'post',
data
})
}
// 图像数据匿名
export function studyMaskImage(data) {
return request({
url: `/Study/studyMaskImage`,
method: 'post',
data
})
}
// 撤销匿名
export function studyUndoMaskImage(data) {
return request({
url: `/Study/studyUndoMaskImage`,
method: 'post',
data
})
}

View File

@ -1,15 +1,7 @@
<template>
<div
id="canvas"
ref="canvas"
v-loading="loading"
:element-loading-text="NSTip"
element-loading-background="rgba(0, 0, 0, 0.8)"
style="width:100%;height:100%;position:relative;"
class="cornerstone-element"
@contextmenu.prevent="onContextmenu"
@mouseup="sliderMouseup"
>
<div id="canvas" ref="canvas" v-loading="loading" :element-loading-text="NSTip"
element-loading-background="rgba(0, 0, 0, 0.8)" style="width:100%;height:100%;position:relative;"
class="cornerstone-element" @contextmenu.prevent="onContextmenu" @mouseup="sliderMouseup">
<div v-show="dicomInfo.series" class="info-series">
<div>Series #{{ dicomInfo.series }}</div>
<div>Image #{{ dicomInfo.frame }}</div>
@ -27,14 +19,13 @@
<!-- <div v-show="toolState.clipPlaying">FPS {{ dicomInfo.fps }}</div> -->
<div v-show="mousePosition.mo">
Pos: {{ mousePosition.x ? mousePosition.x.toFixed(0) : '' }}, {{ mousePosition.y ? mousePosition.y.toFixed(0) :
'' }}
'' }}
</div>
<div
v-if="(dicomInfo.modality === 'CT' || dicomInfo.modality === 'DR' || dicomInfo.modality === 'CR') && mousePosition.mo"
>HU: {{ mousePosition.mo }}</div>
<div
v-else-if="(dicomInfo.modality === 'PT' && mousePosition.suv)"
>SUVbw(g/ml): {{ mousePosition.suv.toFixed(3) }}</div>
v-if="(dicomInfo.modality === 'CT' || dicomInfo.modality === 'DR' || dicomInfo.modality === 'CR') && mousePosition.mo">
HU: {{ mousePosition.mo }}</div>
<div v-else-if="(dicomInfo.modality === 'PT' && mousePosition.suv)">SUVbw(g/ml): {{ mousePosition.suv.toFixed(3)
}}</div>
<div v-else-if="mousePosition.mo">Density: {{ mousePosition.mo }}</div>
<div>W*H: {{ dicomInfo.size }}</div>
@ -52,17 +43,12 @@
<!-- <div v-show="dicomInfo.acc">ACC {{ dicomInfo.acc }}</div> -->
<!-- <div>{{ dicomInfo.time }}</div> -->
</div>
<div
ref="sliderBox"
class="my_slider_box"
<div ref="sliderBox" class="my_slider_box"
style="position: absolute;right: 1px;height: calc(100% - 100px);transform: translateY(-50%);top: calc(50% - 30px);width: 10px;background: #333;cursor: pointer"
@click.stop="goViewer($event)"
>
<div
:style="{ top: height + '%' }"
@click.stop="goViewer($event)">
<div :style="{ top: height + '%' }"
style="z-index:10;background: #9e9e9e;height: 20px;width: 100%;position: absolute;top: 0;cursor: move"
@mousedown="sliderMousedown($event)"
/>
@mousedown="sliderMousedown($event)" />
</div>
<div style="position: absolute;left: 50%;top: 15px;color: #f44336;">{{ markers.top }}</div>
<div style="position: absolute;top: 50%;right: 15px;color: #f44336;">{{ markers.right }}</div>
@ -81,19 +67,9 @@
<!-- <div v-show="stack.firstImageLoading" class="load-indicator">
Loading Series #{{ stack.seriesNumber }}...
</div>-->
<el-dialog
v-if="dcmTag.visible"
:visible.sync="dcmTag.visible"
:close-on-click-modal="false"
:title="dcmTag.title"
width="1000px"
custom-class="base-dialog-wrapper"
append-to-body
>
<dicom-tags
:image-id="stack.imageIds[stack.currentImageIdIndex]"
@close="dcmTag.visible = false"
/>
<el-dialog v-if="dcmTag.visible" :visible.sync="dcmTag.visible" :close-on-click-modal="false" :title="dcmTag.title"
width="1000px" custom-class="base-dialog-wrapper" append-to-body>
<dicom-tags :image-id="stack.imageIds[stack.currentImageIdIndex]" @close="dcmTag.visible = false" />
</el-dialog>
</div>
</template>
@ -228,7 +204,7 @@ export default {
this.stack.imageIds = dicomSeries.imageIds
this.stack.currentImageIdIndex =
dicomSeries.imageIdIndex &&
dicomSeries.imageIdIndex < dicomSeries.imageIds.length
dicomSeries.imageIdIndex < dicomSeries.imageIds.length
? dicomSeries.imageIdIndex
: 0
this.stack.firstImageLoading = true
@ -316,12 +292,14 @@ export default {
if (
!cornerstoneTools.getToolForElement(element, Note_RectangleRoiTool)
) {
console.log(
Note_RectangleRoiTool.name,
'Note_RectangleRoiTool is add'
)
console.log(element, 'element')
cornerstoneTools.addToolForElement(element, Note_RectangleRoiTool)
cornerstoneTools.addToolForElement(element, Note_RectangleRoiTool, {
configuration: {
color: '#f00',
lineWidth: 0.5,
drawHandles: false,
fillColor: 'rgba(0, 0, 0, 1)',
},
})
}
if (
!cornerstoneTools.getToolForElement(
@ -412,9 +390,8 @@ export default {
data.string('x00080030')
)
this.dicomInfo.series = data.string('x00200011')
this.dicomInfo.frame = `${this.stack.currentImageIdIndex + 1}/${
this.stack.imageIds.length
}`
this.dicomInfo.frame = `${this.stack.currentImageIdIndex + 1}/${this.stack.imageIds.length
}`
this.dicomInfo.size = `${data.uint16('x00280011')}x${data.uint16(
'x00280010'
)}`
@ -624,8 +601,8 @@ export default {
index > this.stack.imageIds.length
? this.stack.imageIds.length
: index < 0
? 0
: index
? 0
: index
// if (!cornerstone.imageCache.getImageLoadObject(this.stack.imageIds[index])) return
this.height = height
if (this.stack.currentImageIdIndex !== index) {
@ -853,8 +830,108 @@ export default {
setToolPassive(toolName) {
cornerstoneTools.setToolPassiveForElement(this.canvas, toolName)
},
async reloadImage(newImageId = null) {
// 1. imageIdimageId
let element = this.canvas
this.stack.imageIds.splice(this.stack.currentImageIdIndex, 1, newImageId)
const currentImageId =
newImageId || cornerstone.getImage(element)?.imageId
if (!currentImageId) {
console.error('没有找到可用的imageId')
return
}
// 2.
this.clearToolStateForImage(element, currentImageId)
// 3.
this.removeImageFromCache(currentImageId)
// 4.
await this.loadAndRenderImage(element, currentImageId)
},
clearToolStateForImage(element, imageId) {
//
const globalToolStateManager =
cornerstoneTools.globalImageIdSpecificToolStateManager
if (globalToolStateManager) {
// 1
globalToolStateManager.clearImageIdToolState(imageId)
console.log(`已清除影像 ${imageId} 的所有标注数据`)
}
// 2使
const elementToolStateManager =
cornerstoneTools.getElementToolStateManager(element)
if (elementToolStateManager && elementToolStateManager.get(element)) {
//
elementToolStateManager.clear(element)
}
},
removeImageFromCache(imageId) {
const imageCache = cornerstone.imageCache
if (imageCache && imageCache[imageId]) {
//
delete imageCache[imageId]
console.log(`已从缓存中移除影像: ${imageId}`)
}
// 使API
if (typeof cornerstone.imageCache.removeImage === 'function') {
cornerstone.imageCache.removeImageLoadObject(imageId)
}
},
async loadAndRenderImage(element, imageId) {
try {
// 1.
const canvas = element.querySelector('canvas')
if (canvas) {
const ctx = canvas.getContext('2d')
ctx.clearRect(0, 0, canvas.width, canvas.height)
}
// 2.
const image = await cornerstone.loadAndCacheImage(imageId)
// 3.
let viewport = cornerstone.getViewport(element)
if (!viewport) {
viewport = cornerstone.getDefaultViewport(element, image)
}
// 4.
cornerstone.displayImage(element, image, viewport)
// 5.
cornerstone.updateImage(element)
console.log(`影像 ${imageId} 重新加载并渲染完成`)
return image
} catch (error) {
console.error('加载图像失败:', error)
throw error
}
},
getNote_RectangleRoi() {
return new Promise(resolve => {
let toolInfo = cornerstoneTools.getToolState(this.canvas, 'Note_RectangleRoi')
let image = cornerstone.getImage(this.canvas)
resolve({ toolInfo, image })
})
// console.log(
// cornerstoneTools.getToolState(this.canvas, 'Note_RectangleRoi')
// )
// console.log(cornerstone.getImage(this.canvas))
// let image = cornerstone.getImage(this.canvas)
// // cornerstone.imageCache.removeImageLoadObject(image.imageId)
// this.reloadImage(this.canvas, image.imageId)
},
setToolActive(toolName) {
console.log(this.canvas, 'this.canvas')
cornerstoneTools.setToolActiveForElement(this.canvas, toolName, {
mouseButtonMask: 1,
})

View File

@ -6,102 +6,68 @@
<dicom-canvas ref="dicomCanvas" style="width:100%;height:100%" />
</div>
</div>-->
<div v-show="layoutRow>=1" class="dicom-row" :style="{height: rowHeight}">
<div
v-show="layoutRow>=1&&layoutCol>=1"
class="dicom-item"
:class="{'activeItem':activeItem=='dicomCanvas0'}"
data-index="0"
@click="activateDicomCanvas(0)"
@dblclick="setFullScreen($event)"
>
<div class="Anonymous" v-if="isAnonymous">
<div :class="{ btn: true, activeBtn: activeTool === 'Note_RectangleRoi' }"
@click="setToolActive($event, 'Note_RectangleRoi')">矩形</div>
<div :class="{ btn: true, activeBtn: activeTool === 'Eraser' }" @click="setToolActive($event, 'Eraser')">清除
</div>
<div class="btn" @click="anonymousImage"></div>
<div class="btn">应用整个序列</div>
<!-- <div class="btn">刷新图像</div> -->
<div class="btn" v-if="!isComparison"></div>
<div class="btn" v-else>退</div>
<div class="btn">恢复</div>
</div>
<div v-show="layoutRow >= 1" class="dicom-row" :style="{ height: rowHeight }">
<div v-show="layoutRow >= 1 && layoutCol >= 1" class="dicom-item"
:class="{ 'activeItem': activeItem == 'dicomCanvas0' }" data-index="0" @click="activateDicomCanvas(0)"
@dblclick="setFullScreen($event)">
<dicom-canvas ref="dicomCanvas0" style="width:100%;height:100%" />
</div>
<div
v-show="layoutRow>=1&&layoutCol>=2"
class="dicom-item"
:class="{'activeItem':activeItem=='dicomCanvas1'}"
data-index="1"
@click="activateDicomCanvas(1)"
@dblclick="setFullScreen($event)"
>
<div v-show="layoutRow >= 1 && layoutCol >= 2" class="dicom-item"
:class="{ 'activeItem': activeItem == 'dicomCanvas1' }" data-index="1" @click="activateDicomCanvas(1)"
@dblclick="setFullScreen($event)">
<dicom-canvas ref="dicomCanvas1" style="width:100%;height:100%" />
</div>
<div
v-show="layoutRow>=1&&layoutCol>=3"
class="dicom-item"
:class="{'activeItem':activeItem=='dicomCanvas2'}"
data-index="2"
@click="activateDicomCanvas(2)"
@dblclick="setFullScreen($event)"
>
<div v-show="layoutRow >= 1 && layoutCol >= 3" class="dicom-item"
:class="{ 'activeItem': activeItem == 'dicomCanvas2' }" data-index="2" @click="activateDicomCanvas(2)"
@dblclick="setFullScreen($event)">
<dicom-canvas ref="dicomCanvas2" style="width:100%;height:100%" />
</div>
</div>
<div v-show="layoutRow>=2" class="dicom-row" :style="{height: rowHeight}">
<div
v-show="layoutRow>=2&&layoutCol>=1"
class="dicom-item"
:class="{'activeItem':activeItem=='dicomCanvas3'}"
data-index="3"
@click="activateDicomCanvas(3)"
@dblclick="setFullScreen($event)"
>
<div v-show="layoutRow >= 2" class="dicom-row" :style="{ height: rowHeight }">
<div v-show="layoutRow >= 2 && layoutCol >= 1" class="dicom-item"
:class="{ 'activeItem': activeItem == 'dicomCanvas3' }" data-index="3" @click="activateDicomCanvas(3)"
@dblclick="setFullScreen($event)">
<dicom-canvas ref="dicomCanvas3" style="width:100%;height:100%" />
</div>
<div
v-show="layoutRow>=2&&layoutCol>=2"
class="dicom-item"
:class="{'activeItem':activeItem=='dicomCanvas4'}"
data-index="4"
@click="activateDicomCanvas(4)"
@dblclick="setFullScreen($event)"
>
<div v-show="layoutRow >= 2 && layoutCol >= 2" class="dicom-item"
:class="{ 'activeItem': activeItem == 'dicomCanvas4' }" data-index="4" @click="activateDicomCanvas(4)"
@dblclick="setFullScreen($event)">
<dicom-canvas ref="dicomCanvas4" style="width:100%;height:100%" />
</div>
<div
v-show="layoutRow>=2&&layoutCol>=3"
class="dicom-item"
:class="{'activeItem':activeItem=='dicomCanvas5'}"
data-index="5"
@click="activateDicomCanvas(5)"
@dblclick="setFullScreen($event)"
>
<div v-show="layoutRow >= 2 && layoutCol >= 3" class="dicom-item"
:class="{ 'activeItem': activeItem == 'dicomCanvas5' }" data-index="5" @click="activateDicomCanvas(5)"
@dblclick="setFullScreen($event)">
<dicom-canvas ref="dicomCanvas5" style="width:100%;height:100%" />
</div>
</div>
<div v-show="layoutRow==3" class="dicom-row" :style="{height: rowHeight}">
<div
v-show="layoutRow==3&&layoutCol>=1"
class="dicom-item"
:class="{'activeItem':activeItem=='dicomCanvas6'}"
data-index="6"
@click="activateDicomCanvas(6)"
@dblclick="setFullScreen($event)"
>
<div v-show="layoutRow == 3" class="dicom-row" :style="{ height: rowHeight }">
<div v-show="layoutRow == 3 && layoutCol >= 1" class="dicom-item"
:class="{ 'activeItem': activeItem == 'dicomCanvas6' }" data-index="6" @click="activateDicomCanvas(6)"
@dblclick="setFullScreen($event)">
<dicom-canvas ref="dicomCanvas6" style="width:100%;height:100%" />
</div>
<div
v-show="layoutRow==3&&layoutCol>=2"
class="dicom-item"
:class="{'activeItem':activeItem=='dicomCanvas7'}"
data-index="7"
@click="activateDicomCanvas(7)"
@dblclick="setFullScreen($event)"
>
<div v-show="layoutRow == 3 && layoutCol >= 2" class="dicom-item"
:class="{ 'activeItem': activeItem == 'dicomCanvas7' }" data-index="7" @click="activateDicomCanvas(7)"
@dblclick="setFullScreen($event)">
<dicom-canvas ref="dicomCanvas7" style="width:100%;height:100%" />
</div>
<div
v-show="layoutRow==3&&layoutCol>=3"
class="dicom-item"
:class="{'activeItem':activeItem=='dicomCanvas8'}"
data-index="8"
@click="activateDicomCanvas(8)"
@dblclick="setFullScreen($event)"
>
<div v-show="layoutRow == 3 && layoutCol >= 3" class="dicom-item"
:class="{ 'activeItem': activeItem == 'dicomCanvas8' }" data-index="8" @click="activateDicomCanvas(8)"
@dblclick="setFullScreen($event)">
<dicom-canvas ref="dicomCanvas8" style="width:100%;height:100%" />
</div>
</div>
@ -113,7 +79,7 @@
<div class="sideTool-title">{{ $t('trials:reading:button:layout') }}</div>
<div class="sideTool-wrapper">
<label>{{ $t('trials:reading:button:layout') }}:</label>
<select class="sidetool-select" style="width:90px" @change="changeLayout($event)">
<select class="sidetool-select" style="width:90px" :disabled="isAnonymous" @change="changeLayout($event)">
<option value="1x1" selected>1x1</option>
<option value="1x2">1x2</option>
<option value="2x1">2x1</option>
@ -124,18 +90,15 @@
<option value="3x2">3x2</option>
<option value="3x3">3x3</option>
</select>
<div class="btnBox" @click="openAnonymous"></div>
</div>
</div>
<!-- 图像变换 -->
<div class="measureTool-wrapper">
<div class="sideTool-title">{{ $t('trials:dicom-show:transform') }}</div>
<div class="sideTool-wrapper">
<button
:title="$t('trials:reading:button:wwwc')"
class="btn-link"
data-tool="Wwwc"
@click="setToolActive($event,'Wwwc')"
>
<button :title="$t('trials:reading:button:wwwc')" class="btn-link" data-tool="Wwwc"
@click="setToolActive($event, 'Wwwc')">
<svg-icon icon-class="reverse" style="font-size:20px;" />
</button>
<!-- <button
@ -146,75 +109,37 @@
>
<svg-icon icon-class="wwwcRegion" style="font-size:20px;" />
</button>-->
<button
:title="$t('trials:reading:button:reverseColor')"
class="btn-link"
@click="toggleInvert"
>
<button :title="$t('trials:reading:button:reverseColor')" class="btn-link" @click="toggleInvert">
<svg-icon icon-class="reversecolor" style="font-size:20px;" />
</button>
<button
:title="$t('trials:reading:button:zoom')"
class="btn-link"
data-tool="Zoom"
@click="setToolActive($event,'Zoom')"
>
<button :title="$t('trials:reading:button:zoom')" class="btn-link" data-tool="Zoom"
@click="setToolActive($event, 'Zoom')">
<svg-icon icon-class="magnifier" style="font-size:20px;" />
</button>
<button
:title="$t('trials:dicom-show:lens')"
class="btn-link"
data-tool="Magnify"
@click="setToolActive($event,'Magnify')"
>
<button :title="$t('trials:dicom-show:lens')" class="btn-link" data-tool="Magnify"
@click="setToolActive($event, 'Magnify')">
<svg-icon icon-class="zoom" style="font-size:20px;" />
</button>
<button
:title="$t('trials:reading:button:rotate')"
class="btn-link dropdown"
data-tool="Rotate"
>
<button :title="$t('trials:reading:button:rotate')" class="btn-link dropdown" data-tool="Rotate">
<svg-icon icon-class="rotate" style="font-size:20px;" />
<div class="dropdown-content">
<div
@click.stop="setDicomCanvasRotate(1)"
>{{ $t('trials:reading:button:rotateDefault') }}</div>
<div
@click.stop="setDicomCanvasRotate(2)"
>{{ $t('trials:reading:button:rotateHorizontal') }}</div>
<div
@click.stop="setDicomCanvasRotate(3)"
>{{ $t('trials:reading:button:rotateVertical') }}</div>
<div
@click.stop="setDicomCanvasRotate(4)"
>{{ $t('trials:reading:button:rotateTurnLeft') }}</div>
<div
@click.stop="setDicomCanvasRotate(5)"
>{{ $t('trials:reading:button:rotateTurnRight') }}</div>
<div @click.stop="setDicomCanvasRotate(1)">{{ $t('trials:reading:button:rotateDefault') }}</div>
<div @click.stop="setDicomCanvasRotate(2)">{{ $t('trials:reading:button:rotateHorizontal') }}</div>
<div @click.stop="setDicomCanvasRotate(3)">{{ $t('trials:reading:button:rotateVertical') }}</div>
<div @click.stop="setDicomCanvasRotate(4)">{{ $t('trials:reading:button:rotateTurnLeft') }}</div>
<div @click.stop="setDicomCanvasRotate(5)">{{ $t('trials:reading:button:rotateTurnRight') }}</div>
</div>
</button>
<button
:title="$t('trials:reading:button:move')"
class="btn-link"
data-tool="Pan"
@click="setToolActive($event,'Pan')"
>
<button :title="$t('trials:reading:button:move')" class="btn-link" data-tool="Pan"
@click="setToolActive($event, 'Pan')">
<svg-icon icon-class="move" style="font-size:20px;" />
</button>
<button
:title="$t('trials:reading:button:fitWindow')"
class="btn-link"
data-tool="fitToWindow"
@click="fitToType($event,'fitToWindow')"
>
<button :title="$t('trials:reading:button:fitWindow')" class="btn-link" data-tool="fitToWindow"
@click="fitToType($event, 'fitToWindow')">
<svg-icon icon-class="fitToWindow" style="font-size:20px;" />
</button>
<button
:title="$t('trials:reading:button:fitImage')"
class="btn-link"
data-tool="fitToImage"
@click="fitToType($event,'fitToImage')"
>
<button :title="$t('trials:reading:button:fitImage')" class="btn-link" data-tool="fitToImage"
@click="fitToType($event, 'fitToImage')">
<svg-icon icon-class="fitToImage" style="font-size:20px;" />
</button>
<!-- <button title="旋转" class="btn-link dropdown" data-tool="Rotate" @click="setToolActive($event,'Rotate')"> -->
@ -225,169 +150,94 @@
<div class="sideTool-title">{{ $t('trials:dicom-show:measurementLabeling') }}</div>
<div class="sideTool-wrapper">
<!-- 探针 -->
<button
:title="$t('trials:dicom-show:Probe')"
class="btn-link"
data-tool="Probe"
@click="setToolActive($event,'Probe')"
>
<button :title="$t('trials:dicom-show:Probe')" class="btn-link" data-tool="Probe"
@click="setToolActive($event, 'Probe')">
<svg-icon icon-class="pixel" style="font-size:20px;" />
</button>
<!-- 长度测量 -->
<button
:title="$t('trials:dicom-show:Length')"
class="btn-link"
data-tool="Length"
@click="setToolActive($event,'Length')"
>
<button :title="$t('trials:dicom-show:Length')" class="btn-link" data-tool="Length"
@click="setToolActive($event, 'Length')">
<svg-icon icon-class="length" style="font-size:20px;" />
</button>
<!-- 角度测量 -->
<button
:title="$t('trials:dicom-show:Angle')"
class="btn-link"
data-tool="Angle"
@click="setToolActive($event,'Angle')"
>
<button :title="$t('trials:dicom-show:Angle')" class="btn-link" data-tool="Angle"
@click="setToolActive($event, 'Angle')">
<svg-icon icon-class="angle" style="font-size:20px;" />
</button>
<!-- Cobb测量 -->
<button
:title="$t('trials:dicom-show:CobbAngle')"
class="btn-link"
data-tool="CobbAngle"
@click="setToolActive($event,'CobbAngle')"
>
<button :title="$t('trials:dicom-show:CobbAngle')" class="btn-link" data-tool="CobbAngle"
@click="setToolActive($event, 'CobbAngle')">
<svg-icon icon-class="cobb" style="font-size:20px;" />
</button>
<!-- 椭圆测量 -->
<button
:title="$t('trials:dicom-show:EllipticalRoi')"
class="btn-link"
data-tool="EllipticalRoi"
@click="setToolActive($event,'EllipticalRoi')"
>
<button :title="$t('trials:dicom-show:EllipticalRoi')" class="btn-link" data-tool="EllipticalRoi"
@click="setToolActive($event, 'EllipticalRoi')">
<svg-icon icon-class="oval" style="font-size:20px;" />
</button>
<!-- 矩形测量 -->
<button
:title="$t('trials:dicom-show:RectangleRoi')"
class="btn-link"
data-tool="RectangleRoi"
@click="setToolActive($event,'RectangleRoi')"
>
<button :title="$t('trials:dicom-show:RectangleRoi')" class="btn-link" data-tool="RectangleRoi"
@click="setToolActive($event, 'RectangleRoi')">
<svg-icon icon-class="rectangle" style="font-size:20px;" />
</button>
<!-- 多边形标记 -->
<button
:title="$t('trials:dicom-show:FreehandRoi')"
class="btn-link"
data-tool="FreehandRoi"
@click="setToolActive($event,'FreehandRoi')"
>
<button :title="$t('trials:dicom-show:FreehandRoi')" class="btn-link" data-tool="FreehandRoi"
@click="setToolActive($event, 'FreehandRoi')">
<svg-icon icon-class="polygon" style="font-size:20px;" />
</button>
<!-- 十字线 -->
<button
:title="$t('trials:dicom-show:Bidirectional')"
class="btn-link"
data-tool="Bidirectional"
@click="setToolActive($event,'Bidirectional')"
>
<button :title="$t('trials:dicom-show:Bidirectional')" class="btn-link" data-tool="Bidirectional"
@click="setToolActive($event, 'Bidirectional')">
<svg-icon icon-class="bidirection" style="font-size:20px;" />
</button>
<!-- 文字标注 -->
<button
:title="$t('trials:dicom-show:ArrowAnnotate')"
class="btn-link"
data-tool="ArrowAnnotate"
@click="setToolActive($event,'ArrowAnnotate')"
>
<button :title="$t('trials:dicom-show:ArrowAnnotate')" class="btn-link" data-tool="ArrowAnnotate"
@click="setToolActive($event, 'ArrowAnnotate')">
<svg-icon icon-class="label" style="font-size:20px;" />
</button>
<!-- 清除测量和标记 -->
<button
:title="$t('trials:dicom-show:Eraser')"
class="btn-link"
data-tool="Eraser"
@click="setToolActive($event,'Eraser')"
>
<button :title="$t('trials:dicom-show:Eraser')" class="btn-link" data-tool="Eraser"
@click="setToolActive($event, 'Eraser')">
<svg-icon icon-class="clear" style="font-size:20px;" />
</button>
<!-- 截屏 -->
<button
:title="$t('trials:dicom-show:image')"
class="btn-link"
@click="currentDicomCanvas.saveImage()"
>
<button :title="$t('trials:dicom-show:image')" class="btn-link" @click="currentDicomCanvas.saveImage()">
<svg-icon icon-class="image" style="font-size:20px;" />
</button>
<!-- 标签 -->
<button
:title="$t('trials:dicom-show:tags')"
class="btn-link"
@click="currentDicomCanvas.showTags()"
>
<button :title="$t('trials:dicom-show:tags')" class="btn-link" @click="currentDicomCanvas.showTags()">
<svg-icon icon-class="dictionary" style="font-size:20px;" />
</button>
</div>
</div>
<div class="measureTool-wrapper">
<button
:title="$t('trials:dicom-show:EllipticalRoi')"
class="btn-link"
@click="setToolActive($event,'Note_RectangleRoi')"
>
<svg-icon icon-class="oval" style="font-size:20px;" />
</button>
<!-- 播放 -->
<div class="sideTool-title">{{ $t('trials:dicom-show:play') }}</div>
<div class="sideTool-wrapper">
<!-- 第一帧 -->
<button
class="btn-link"
:title="$t('trials:dicom-show:firstframe')"
@click="currentDicomCanvas.scrollPage(-9999)"
>
<button class="btn-link" :title="$t('trials:dicom-show:firstframe')"
@click="currentDicomCanvas.scrollPage(-9999)">
<svg-icon icon-class="firstframe" style="font-size:20px;" />
</button>
<!-- 显示上一张影像 -->
<button
class="btn-link"
:title="$t('trials:dicom-show:previousframe')"
@click="currentDicomCanvas.scrollPage(-1)"
>
<button class="btn-link" :title="$t('trials:dicom-show:previousframe')"
@click="currentDicomCanvas.scrollPage(-1)">
<svg-icon icon-class="previousframe" style="font-size:20px;" />
</button>
<!-- 播放 -->
<button class="btn-link" :title="$t('trials:dicom-show:play')" @click="clipPlay">
<svg-icon
:icon-class="currentDicomCanvas.toolState.clipPlaying ? 'stop' : 'play'"
style="font-size:20px;"
/>
<svg-icon :icon-class="currentDicomCanvas.toolState.clipPlaying ? 'stop' : 'play'"
style="font-size:20px;" />
</button>
<!-- 下一帧 -->
<button
class="btn-link"
:title="$t('trials:dicom-show:nextframe')"
@click="currentDicomCanvas.scrollPage(1)"
>
<button class="btn-link" :title="$t('trials:dicom-show:nextframe')" @click="currentDicomCanvas.scrollPage(1)">
<svg-icon icon-class="nextframe" style="font-size:20px;" />
</button>
<!-- 最后一帧 -->
<button
class="btn-link"
:title="$t('trials:dicom-show:lastframe')"
@click="currentDicomCanvas.scrollPage(9999)"
>
<button class="btn-link" :title="$t('trials:dicom-show:lastframe')"
@click="currentDicomCanvas.scrollPage(9999)">
<svg-icon icon-class="lastframe" style="font-size:20px;" />
</button>
<select
v-model="fps"
class="sidetool-select"
style="width:60px"
@change="setDicomCanvasfps($event)"
>
<select v-model="fps" class="sidetool-select" style="width:60px" @change="setDicomCanvasfps($event)">
<!-- 默认值 -->
<option :value="5">5</option>
<option :value="10">10</option>
@ -403,21 +253,15 @@
<div class="sideTool-wrapper">
<!-- 预设窗位值 -->
<label>{{ $t('trials:dicom-show:dicomCanvasWwwc') }}:</label>
<select
v-model="wwwcList[currentDicomCanvasIndex]"
class="sidetool-select"
style="width:100px"
@change="setDicomCanvasWwwc($event)"
>
<select v-model="wwwcList[currentDicomCanvasIndex]" class="sidetool-select" style="width:100px"
@change="setDicomCanvasWwwc($event)">
<!-- 默认值 -->
<option :value="-1">{{ $t('trials:dicom-show:default') }}</option>
<!-- 自定义 -->
<option :value="0">{{ $t('trials:dicom-show:custom') }}</option>
<!-- 区域窗宽 -->
<option
:value="1"
style="border-bottom:1px solid #fff;"
>{{ $t('trials:reading:button:wwwcRegion') }}</option>
<option :value="1" style="border-bottom:1px solid #fff;">{{ $t('trials:reading:button:wwwcRegion') }}
</option>
<option :value="2">CT Abdomen</option>
<option :value="3">CT Angio</option>
<option :value="4">CT Bone</option>
@ -430,32 +274,17 @@
<div class="sideTool-wrapper">
<!-- 伪彩色 -->
<label>{{ $t('trials:dicom-show:pseudocolor') }}:</label>
<select
v-model="colorList[currentDicomCanvasIndex]"
class="sidetool-select"
style="width:90px"
@change="setColormap($event)"
>
<select v-model="colorList[currentDicomCanvasIndex]" class="sidetool-select" style="width:90px"
@change="setColormap($event)">
<!-- 默认值 -->
<option value>{{ $t('trials:dicom-show:default') }}</option>
<option
v-for="(item,index) in colormapsList"
:key="index"
:value="item.id"
>{{ item.name }}</option>
<option v-for="(item, index) in colormapsList" :key="index" :value="item.id">{{ item.name }}</option>
</select>
</div>
</div>
</div>
<el-dialog
v-if="customWwc.visible"
:visible.sync="customWwc.visible"
:close-on-click-modal="false"
:title="customWwc.title"
width="400px"
custom-class="base-dialog-wrapper"
append-to-body
>
<el-dialog v-if="customWwc.visible" :visible.sync="customWwc.visible" :close-on-click-modal="false"
:title="customWwc.title" width="400px" custom-class="base-dialog-wrapper" append-to-body>
<CustomWwwcForm @close="customWwc.visible = false" @setWwwc="setWwwc" />
</el-dialog>
</div>
@ -473,16 +302,26 @@ import Hammer from 'hammerjs'
cornerstoneTools.external.cornerstone = cornerstone
cornerstoneTools.external.Hammer = Hammer
cornerstoneTools.external.cornerstoneMath = cornerstoneMath
console.log(cornerstoneTools, 'cornerstoneTools')
console.log(cornerstone, 'cornerstone')
import '@/utils/dialog'
import { studyMaskImage, studyUndoMaskImage } from "@/api/reading"
export default {
name: 'DicomsViewer',
components: {
DicomCanvas,
CustomWwwcForm,
},
props: {
loading: {
type: Boolean,
default: false
}
},
data() {
return {
isAnonymous: false,
isComparison: false,
activeTool: '',
activeItem: 'dicomCanvas0',
layoutRow: 1,
@ -503,6 +342,7 @@ export default {
wwwcList: [],
layout: null,
seriesList: [],
series: {},
customWwc: { visible: false, title: null },
fps: 15,
}
@ -520,11 +360,83 @@ export default {
},
methods: {
anonymousImage() {
console.log(this.series, 'this.series')
this.$refs[`dicomCanvas0`].getNote_RectangleRoi().then(async obj => {
let { toolInfo, image } = obj
console.log(image, 'image')
if (!toolInfo || toolInfo.data.length <= 0) return this.$confirm(this.$t("DicomViewer:anonymous:notMark"))
let instanceInfo = this.series.instanceInfoList.find(item => item.ImageId === image.imageId)
let data = {
// SeriesId: this.series.seriesId,
instanceIdList: [instanceInfo.Id],
MaskRegionList: []
}
toolInfo.data.forEach(item => {
let currentStart = item.handles.start
let currentEnd = item.handles.end
let start = {
x: currentStart.x > currentEnd.x ? Math.round(currentEnd.x) : Math.round(currentStart.x),
y: currentStart.y > currentEnd.y ? Math.round(currentEnd.y) : Math.round(currentStart.y),
}
let end = {
x: currentStart.x > currentEnd.x ? Math.round(currentStart.x) : Math.round(currentEnd.x),
y: currentStart.y > currentEnd.y ? Math.round(currentStart.y) : Math.round(currentEnd.y),
}
let width = end.x - start.x
let height = end.y - start.y
data.MaskRegionList.push({
X: start.x,
Y: start.y,
Width: width,
Height: height
})
})
let res = await this.studyMaskImage(data)
if (!res) return false
// this.$emit("update:loading", true)
// let strs = image.imageId.split("?")
// let newImageId = `${strs[0]}-MaskImage?${strs[1]}`
// console.log(newImageId, 'newImageId')
// this.$refs[`dicomCanvas0`].reloadImage(newImageId)
})
},
openAnonymous() {
this.isAnonymous = !this.isAnonymous
if (!this.isAnonymous) {
const elements = document.querySelectorAll('.dicom-item')
const scope = this
scope.activeTool = null
Array.from(elements).forEach((element, index) => {
if (element.style.display !== 'none') {
scope.$refs[`dicomCanvas${index}`].setToolPassive(toolName)
}
})
} else {
this.activateDicomCanvas(0)
this.changeLayout('1x1')
}
},
async studyMaskImage(data) {
try {
this.$emit("update:loading", true)
let res = await studyMaskImage(data)
this.$emit("update:loading", false)
if (res.IsSuccess) {
return true
}
return false
} catch (err) {
console.log(err)
this.$emit("update:loading", false)
return false
}
},
loadImageStack(dicomSeries) {
this.currentDicomCanvas.toolState.clipPlaying = false
this.$nextTick(() => {
const series = Object.assign({}, dicomSeries)
this.currentDicomCanvas.loadImageStack(series)
this.series = Object.assign({}, dicomSeries)
this.currentDicomCanvas.loadImageStack(this.series)
})
},
loadOtherImageStack(seriesList) {
@ -573,7 +485,7 @@ export default {
}
},
changeLayout(event) {
const arr = event.target.value.split('x')
const arr = event.target ? event.target.value.split('x') : event.split('x')
this.layoutRow = parseInt(arr[0])
this.layoutCol = parseInt(arr[1])
this.rowHeight = 100 / this.layoutRow + '%'
@ -610,11 +522,21 @@ export default {
cornerstone.resize(e.currentTarget.children[0])
}
},
fillColor() {
const elements = document.querySelectorAll('.dicom-item')
const scope = this
Array.from(elements).forEach((element, index) => {
if (element.style.display !== 'none') {
scope.$refs[`dicomCanvas${index}`].fillColor()
}
})
},
setToolActive(e, toolName) {
const elements = document.querySelectorAll('.dicom-item')
if (e.currentTarget.classList.contains('activeTool')) {
e.currentTarget.classList.remove('activeTool')
const scope = this
scope.activeTool = null
Array.from(elements).forEach((element, index) => {
if (element.style.display !== 'none') {
scope.$refs[`dicomCanvas${index}`].setToolPassive(toolName)
@ -628,6 +550,7 @@ export default {
e.currentTarget.classList.add('activeTool')
const scope = this
scope.activeTool = toolName
Array.from(elements).forEach((element, index) => {
if (element.style.display !== 'none') {
scope.$refs[`dicomCanvas${index}`].setToolActive(toolName)
@ -769,6 +692,7 @@ export default {
.dicom-wrapper {
display: flex;
}
.dicom-wrapper .case-dialog-class {
position: fixed;
left: 25%;
@ -780,28 +704,84 @@ export default {
.dicom-wrapper .case-dialog-div {
pointer-events: none;
}
.dicom-wrapper .case-dialog-class .el-dialog__body {
max-height: 300px;
overflow-y: auto;
}
.dicom-wrapper .el-dialog__header {
padding: 15px;
}
.dicom-wrapper .el-dialog__body {
padding: 10px 20px;
}
.dicom-viewer {
display: flex;
flex-direction: column;
flex: 1;
position: relative;
}
.Anonymous {
position: absolute;
border-radius: 5px;
bottom: 30px;
left: 5%;
right: 5%;
width: 90%;
height: 60px;
padding: 0 10px;
background-color: rgba(255, 255, 255, .2);
display: flex;
align-items: center;
justify-content: space-between;
z-index: 9999;
.btn {
width: 15%;
text-align: center;
height: 40px;
line-height: 30px;
border-radius: 15px;
background-color: rgba(255, 255, 255, .3);
cursor: pointer;
padding: 5px 10px;
border: 1px solid rgba(255, 255, 255, .7);
&:hover {
background-color: rgba(255, 255, 255, .5);
}
}
.activeBtn {
background-color: rgba(255, 255, 255, .5);
}
}
.btnBox {
display: inline-block;
width: 80px;
text-align: center;
height: 30px;
line-height: 20px;
border-radius: 15px;
background-color: rgba(255, 255, 255, .3);
cursor: pointer;
padding: 5px 10px;
border: 1px solid rgba(255, 255, 255, .7);
margin: 5px;
}
.dicom-wrapper .dicom-row {
display: flex;
flex-direction: row;
flex: 1;
width: 100%;
}
.dicom-wrapper .dicom-item {
position: relative;
width: 0;
@ -811,9 +791,11 @@ export default {
padding: 5px;
border: 1px solid #c8c8c8;
}
.dicom-wrapper .activeItem {
border: 2px solid chocolate;
}
.dicom-item-fullscreen {
position: absolute;
left: 0;
@ -822,14 +804,17 @@ export default {
height: 100%;
z-index: 1;
}
.dicom-wrapper ::-webkit-scrollbar {
width: 7px;
height: 7px;
}
.dicom-wrapper ::-webkit-scrollbar-thumb {
border-radius: 10px;
background: gray;
}
.dicom-wrapper .dicom-tools {
/* display: flex;
flex-direction: column; */
@ -844,25 +829,30 @@ export default {
font-size: 13px;
overflow: hidden;
}
.dicom-wrapper .measure-wrapper {
display: flex;
flex-direction: column;
flex: 1;
overflow: hidden;
}
.measure-wrapper .measure-list {
flex: 1;
overflow-y: auto;
}
.measure-list-header {
padding: 2px 0px;
border-bottom: 1px solid gray;
}
.measure-wrapper ul {
margin: 0;
padding: 0 5px;
list-style: none;
}
.measure-wrapper ul li {
height: 20px;
display: flex;
@ -879,9 +869,11 @@ export default {
font-size: 13px;
color: #606626;
}
.measure-wrapper select > option {
.measure-wrapper select>option {
color: #323232;
}
/* .measure-wrapper select {
height: 20px;
background-color: rgba(0, 0, 0, 0);
@ -901,6 +893,7 @@ export default {
.dicom-canvas.active {
border: 1px solid #337ab7;
}
.sideTool-title {
padding: 5px 8px;
background-color: #525252;
@ -908,30 +901,36 @@ export default {
font-size: 14px;
margin: 4px;
}
.sideTool-wrapper {
border-bottom: 1px solid gray;
margin: 0px 4px;
padding: 4px;
font-size: 12px;
}
.sideTool-wrapper .btn {
font-size: 12px;
}
.sidetool-select {
height: 30px;
background-color: rgba(0, 0, 0, 0);
color: #d0d0d0;
border-radius: 4px;
}
.sidetool-select > option {
.sidetool-select>option {
background-color: #323232;
}
.sideTool-wrapper button.btn-link {
height: 40px;
padding: 8px !important;
border: 1px solid rgba(37, 37, 37, 1);
margin: 1px 1px;
}
.sideTool-wrapper .btn-link {
display: inline-block;
height: 40px;
@ -939,6 +938,7 @@ export default {
border: 1px solid rgba(37, 37, 37, 1);
margin: 1px 1px;
}
.dicom-wrapper .btn-group {
position: relative;
display: inline-block;
@ -955,6 +955,7 @@ export default {
border: 1px solid rgba(37, 37, 37, 1);
min-height: 30px;
}
.dicom-wrapper .btn-group .btn-left {
position: relative;
float: left;
@ -963,15 +964,18 @@ export default {
border-bottom-right-radius: 0;
border-top-right-radius: 0;
}
.dicom-wrapper .activeTool {
background: #16477b90 !important;
border: none;
}
.dicom-wrapper .btn-link {
color: #cccccc;
background-color: #ffffff00;
cursor: pointer;
}
.dicom-wrapper .btn-submit {
cursor: pointer;
/* background-color: #eeeeee; */
@ -991,10 +995,12 @@ export default {
.dicom-wrapper .iconHover:hover {
color: red;
}
.dicom-wrapper .dropdown {
position: relative;
display: inline-block;
}
.dicom-wrapper .dropdown-content {
display: none;
position: absolute;
@ -1007,14 +1013,17 @@ export default {
border: 1px solid #4e4e4e;
padding: 5px;
}
.dicom-wrapper .dropdown:hover .dropdown-content {
display: block;
}
.dicom-wrapper .dropdown-content div {
height: 25px;
line-height: 25px;
cursor: point;
}
.dicom-wrapper .dropdown-content div:hover {
background-color: #16477b90;
}

View File

@ -123,7 +123,7 @@
</div>
</div>
<div class="viewerContent">
<dicom-viewer id="dicomViewer" ref="dicomViewer" style="height:100%" />
<dicom-viewer id="dicomViewer" ref="dicomViewer" style="height:100%" :loading.sync="loading" />
</div>
<!-- <div class="viewerRightSidePanel">
<dicom-tools />

View File

@ -1,110 +1,97 @@
import cornerstone from 'cornerstone-core';
import cornerstoneTools from 'cornerstone-tools';
// Note_RectangleRoiTool.js
import * as cornerstoneTools from 'cornerstone-tools';
const BaseAnnotationTool = cornerstoneTools.importInternal('base/BaseAnnotationTool');
const getToolState = cornerstoneTools.getToolState;
const drawHandles = cornerstoneTools.importInternal('drawing/drawHandles');
export default class Note_RectangleRoiTool extends BaseAnnotationTool {
export default class Note_RectangleRoiTool extends cornerstoneTools.RectangleRoiTool {
constructor(props = {}) {
super({
const defaultProps = {
name: 'Note_RectangleRoi',
supportedInteractionTypes: ['Mouse'],
configuration: {
customColor: '#FF4444',
showLabel: true,
handleRadius: 6,
...props.configuration
}
});
}
createNewMeasurement(eventData) {
const { currentPoints } = eventData;
const startPoint = currentPoints.canvas;
return {
visible: true,
active: true,
color: this.configuration.customColor,
handles: {
start: { x: startPoint.x, y: startPoint.y, highlight: true, active: false },
end: { x: startPoint.x, y: startPoint.y, highlight: true, active: false },
textBox: {
active: false, hasMoved: false, movesIndependently: false,
drawnIndependently: true, allowedOutsideImage: true, hasBoundingBox: true
}
fillColor: 'rgba(255, 255, 0, 0.3)', // 默认填充颜色:半透明黄色
strokeColor: 'yellow', // 边框颜色
lineWidth: 2, // 边框宽度
drawHandles: true, // 是否绘制控制点
handleColor: 'white', // 控制点颜色
}
};
super({ ...defaultProps, ...props });
}
pointNearTool(element, data, coords) {
const minX = Math.min(data.handles.start.x, data.handles.end.x);
const maxX = Math.max(data.handles.start.x, data.handles.end.x);
const minY = Math.min(data.handles.start.y, data.handles.end.y);
const maxY = Math.max(data.handles.start.y, data.handles.end.y);
const threshold = 5;
return coords.x >= minX - threshold && coords.x <= maxX + threshold &&
coords.y >= minY - threshold && coords.y <= maxY + threshold;
}
// 稳健的 renderToolData 实现
renderToolData(evt) {
// 1. 获取 element尝试多种来源
const element = evt.detail?.element || evt.element || evt?.detail?.eventData?.element;
if (!element) {
console.warn('renderToolData: Cannot find element');
const eventData = evt.detail;
const { element } = eventData;
// 获取工具状态
const toolData = cornerstoneTools.getToolState(element, this.name);
if (!toolData || !toolData.data || !toolData.data.length) {
return;
}
// 2. 获取工具数据
const toolData = getToolState(element, this.name);
if (!toolData || !toolData.data || toolData.data.length === 0) return;
const canvas = eventData.canvasContext.canvas;
const context = canvas.getContext('2d');
// 3. 获取 canvas context
const enabledElement = cornerstone.getEnabledElement(element);
const context = enabledElement.canvas.getContext('2d');
if (!context) return;
// 4. 绘制所有标注
context.save();
toolData.data.forEach(data => {
if (!data.visible) return;
// 获取配置
const fillColor = this.configuration.fillColor || 'rgba(255, 255, 0, 0.3)';
const strokeColor = this.configuration.strokeColor || 'yellow';
const lineWidth = this.configuration.lineWidth || 2;
const drawHandles = this.configuration.drawHandles !== false;
context.strokeStyle = data.color || this.configuration.customColor;
context.fillStyle = 'transparent';
context.lineWidth = 2;
const start = data.handles.start;
const end = data.handles.end;
const width = end.x - start.x;
const height = end.y - start.y;
context.strokeRect(start.x, start.y, width, height);
if (data.active) {
drawHandles(context, { element }, [start, end], {
handleRadius: this.configuration.handleRadius,
color: data.color
});
// 遍历所有矩形标注
toolData.data.forEach((measurement) => {
if (!measurement.handles?.start || !measurement.handles?.end) {
return;
}
if (this.configuration.showLabel) {
const area = Math.abs(width * height);
const text = `${area.toFixed(0)}px²`;
const textCoords = {
x: (start.x + end.x) / 2,
y: Math.min(start.y, end.y) - 10
};
const start = measurement.handles.start;
const end = measurement.handles.end;
context.font = '12px Arial';
context.fillStyle = data.color;
context.fillText(text, textCoords.x, textCoords.y);
// 计算矩形坐标和尺寸
const x = Math.min(start.x, end.x);
const y = Math.min(start.y, end.y);
const width = Math.abs(end.x - start.x);
const height = Math.abs(end.y - start.y);
// 1. 绘制填充
if (fillColor) {
context.fillStyle = fillColor;
context.fillRect(x, y, width, height);
}
// 2. 绘制边框
context.strokeStyle = strokeColor;
context.lineWidth = lineWidth;
context.strokeRect(x, y, width, height);
// 3. 绘制控制点(可选)
if (drawHandles) {
this.drawHandles(context, measurement, eventData);
}
});
context.restore();
}
/**
* 绘制控制点
*/
drawHandles(context, measurement, eventData) {
const handles = measurement.handles;
const handleColor = this.configuration.handleColor || 'white';
// 绘制所有控制点
Object.keys(handles).forEach(key => {
const handle = handles[key];
if (handle && typeof handle.x === 'number' && typeof handle.y === 'number') {
context.beginPath();
context.arc(handle.x, handle.y, 5, 0, 2 * Math.PI);
context.fillStyle = handleColor;
context.fill();
context.strokeStyle = 'black';
context.lineWidth = 1;
context.stroke();
}
});
}
}