From f7afca6e735660caa28ca9cdf1fff1d043233418 Mon Sep 17 00:00:00 2001
From: wangxiaoshuang <825034831@qq.com>
Date: Wed, 22 Apr 2026 15:06:39 +0800
Subject: [PATCH] =?UTF-8?q?=E6=B3=A8=E9=87=8A=EF=BC=88=E6=9C=AA=E5=AE=8C?=
=?UTF-8?q?=E6=88=90=EF=BC=89?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/components/Dicom/DicomCanvas.vue | 243 +++++++++------
src/components/Dicom/DicomViewer.vue | 292 ++++++++++++------
.../RectangleRoi/Note_RectangleRoiTool.js | 110 +++++++
3 files changed, 461 insertions(+), 184 deletions(-)
create mode 100644 src/views/trials/trials-panel/reading/dicoms/tools/RectangleRoi/Note_RectangleRoiTool.js
diff --git a/src/components/Dicom/DicomCanvas.vue b/src/components/Dicom/DicomCanvas.vue
index c9d0cfc5..e480a307 100644
--- a/src/components/Dicom/DicomCanvas.vue
+++ b/src/components/Dicom/DicomCanvas.vue
@@ -1,7 +1,15 @@
-
+
Series #{{ dicomInfo.series }}
Image #{{ dicomInfo.frame }}
@@ -15,25 +23,20 @@
Slice Thickness {{ dicomInfo.thick }}mm
WW/WC {{ dicomInfo.wwwc }}
Zoom {{ dicomInfo.zoom }}
-
Location {{ dicomInfo.location }}mm
-->
+
Location {{ dicomInfo.location }}mm
-->
Pos: {{ mousePosition.x ? mousePosition.x.toFixed(0) : '' }}, {{ mousePosition.y ? mousePosition.y.toFixed(0) :
- '' }}
+ '' }}
- HU: {{ mousePosition.mo }}
-
-
- SUVbw(g/ml): {{ mousePosition.suv.toFixed(3) }}
-
-
- Density: {{ mousePosition.mo }}
-
-
- W*H: {{ dicomInfo.size }}
-
+ v-if="(dicomInfo.modality === 'CT' || dicomInfo.modality === 'DR' || dicomInfo.modality === 'CR') && mousePosition.mo"
+ >HU: {{ mousePosition.mo }}
+
SUVbw(g/ml): {{ mousePosition.suv.toFixed(3) }}
+
Density: {{ mousePosition.mo }}
+
W*H: {{ dicomInfo.size }}
Zoom: {{ dicomInfo.zoom }}
@@ -49,31 +52,28 @@
-
-
-
- {{ markers.top }}
-
-
- {{ markers.right }}
+ @mousedown="sliderMousedown($event)"
+ />
+
{{ markers.top }}
+
{{ markers.right }}
-
- {{ markers.bottom }}
-
-
- {{ markers.left }}
-
+
{{ markers.bottom }}
+
{{ markers.left }}
+
-->
Location: {{ dicomInfo.location }}
Slice Thickness: {{ dicomInfo.thick }}mm
WW/WL: {{ dicomInfo.wwwc }}
@@ -81,9 +81,19 @@
-
-
+
+
@@ -101,6 +111,7 @@ import invertOrientationString from '@/views/trials/trials-panel/reading/dicoms/
import calculateSUV from '@/views/trials/trials-panel/reading/dicoms/tools/calculateSUV'
// import requestPoolManager from '@/utils/request-pool'
import ScaleOverlayTool from '@/views/trials/trials-panel/reading/dicoms/tools/ScaleOverlay/ScaleOverlayTool'
+import Note_RectangleRoiTool from '@/views/trials/trials-panel/reading/dicoms/tools/RectangleRoi/Note_RectangleRoiTool'
cornerstoneTools.external.cornerstone = cornerstone
cornerstoneTools.external.Hammer = Hammer
cornerstoneTools.external.cornerstoneMath = cornerstoneMath
@@ -117,7 +128,7 @@ export default {
computed: {
NSTip() {
return `${this.$store.state.trials.downloadSize}, NS: ${this.$store.state.trials.downloadTip}`
- }
+ },
},
data() {
return {
@@ -130,7 +141,7 @@ export default {
seriesNumber: '',
imageIds: [],
currentImageIdIndex: 0,
- firstImageLoading: false
+ firstImageLoading: false,
// preventCache: true
},
dicomInfo: {
@@ -150,14 +161,14 @@ export default {
wwwc: '',
zoom: 0,
location: '',
- fps: 5
+ fps: 5,
},
toolState: {
initialized: false,
activeTool: 'none',
dicomInfoVisible: false,
clipPlaying: false,
- viewportInvert: false
+ viewportInvert: false,
},
loadImagePromise: null,
AnnotationSync: null,
@@ -168,18 +179,20 @@ export default {
sliderInfo: {
oldB: null,
oldM: null,
- isMove: false
+ isMove: false,
},
mousePosition: { x: '', y: '', mo: '' },
markers: { top: '', right: '', bottom: '', left: '' },
orientationMarkers: [],
originalMarkers: [],
- dcmTag: { visible: false, title: this.$t('trials:dicom-tag:title') }
+ dcmTag: { visible: false, title: this.$t('trials:dicom-tag:title') },
}
},
mounted() {
- this.type = this.$router.currentRoute.query.type ? this.$router.currentRoute.query.type : ''
+ this.type = this.$router.currentRoute.query.type
+ ? this.$router.currentRoute.query.type
+ : ''
this.canvas = this.$refs.canvas
this.canvas.addEventListener('cornerstonenewimage', this.onNewImage)
this.canvas.addEventListener(
@@ -200,7 +213,10 @@ export default {
document.addEventListener('mousemove', (e) => {
this.sliderMousemove(e)
})
- this.canvas.addEventListener('cornerstonetoolsstackscroll', this.stackScrollCallback)
+ this.canvas.addEventListener(
+ 'cornerstonetoolsstackscroll',
+ this.stackScrollCallback
+ )
},
methods: {
@@ -210,7 +226,11 @@ export default {
this.stack.seriesId = dicomSeries.seriesId
this.stack.seriesNumber = dicomSeries.seriesNumber
this.stack.imageIds = dicomSeries.imageIds
- this.stack.currentImageIdIndex = dicomSeries.imageIdIndex && dicomSeries.imageIdIndex < dicomSeries.imageIds.length ? dicomSeries.imageIdIndex : 0
+ this.stack.currentImageIdIndex =
+ dicomSeries.imageIdIndex &&
+ dicomSeries.imageIdIndex < dicomSeries.imageIds.length
+ ? dicomSeries.imageIdIndex
+ : 0
this.stack.firstImageLoading = true
this.stack.description = dicomSeries.description
this.toolState.viewportInvert = false
@@ -226,13 +246,17 @@ export default {
this.toolState.clipPlaying = false
this.loading = true
- cornerstone.loadAndCacheImage(this.stack.imageIds[this.stack.currentImageIdIndex])
- .then(image => {
+ cornerstone
+ .loadAndCacheImage(
+ this.stack.imageIds[this.stack.currentImageIdIndex]
+ )
+ .then((image) => {
this.loading = false
if (this.stack.imageIds.indexOf(image.imageId) !== -1) {
this.onFirstImageLoaded(image)
}
- }).catch((error) => {
+ })
+ .catch((error) => {
this.loading = false
if (error.error && error.error.message) {
this.$alert(error.error.message)
@@ -255,15 +279,17 @@ export default {
const apiTool = cornerstoneTools[`${toolName}Tool`]
if (apiTool) {
- const toolAlreadyAddedToElement = cornerstoneTools.getToolForElement(
- element,
- apiTool
- )
+ const toolAlreadyAddedToElement =
+ cornerstoneTools.getToolForElement(element, apiTool)
if (!toolAlreadyAddedToElement) {
if (toolName === 'RectangleRoi') {
- cornerstoneTools.addToolForElement(element, apiTool, { configuration: { showMinMax: true, showStatsText: true } })
+ cornerstoneTools.addToolForElement(element, apiTool, {
+ configuration: { showMinMax: true, showStatsText: true },
+ })
} else if (toolName === 'EllipticalRoi') {
- cornerstoneTools.addToolForElement(element, apiTool, { configuration: { showMinMax: true } })
+ cornerstoneTools.addToolForElement(element, apiTool, {
+ configuration: { showMinMax: true },
+ })
} else {
cornerstoneTools.addToolForElement(element, apiTool)
}
@@ -287,8 +313,26 @@ export default {
false
)
})
- if (!cornerstoneTools.getToolForElement(element, cornerstoneTools.WwwcRegionTool)) {
- cornerstoneTools.addToolForElement(element, cornerstoneTools.WwwcRegionTool)
+ if (
+ !cornerstoneTools.getToolForElement(element, Note_RectangleRoiTool)
+ ) {
+ console.log(
+ Note_RectangleRoiTool.name,
+ 'Note_RectangleRoiTool is add'
+ )
+ console.log(element, 'element')
+ cornerstoneTools.addToolForElement(element, Note_RectangleRoiTool)
+ }
+ if (
+ !cornerstoneTools.getToolForElement(
+ element,
+ cornerstoneTools.WwwcRegionTool
+ )
+ ) {
+ cornerstoneTools.addToolForElement(
+ element,
+ cornerstoneTools.WwwcRegionTool
+ )
}
if (
!cornerstoneTools.getToolForElement(
@@ -347,7 +391,9 @@ export default {
// var instanceId = image.imageId.split('/')[image.imageId.split('/').length - 1]
// instanceId = instanceId.split('.')[0]
// this.stack.instanceId = instanceId
- this.height = (this.stack.currentImageIdIndex) * 100 / (this.stack.imageIds.length - 1)
+ this.height =
+ (this.stack.currentImageIdIndex * 100) /
+ (this.stack.imageIds.length - 1)
this.resetWwwc()
},
onNewImage(e) {
@@ -366,8 +412,9 @@ 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'
)}`
@@ -379,21 +426,30 @@ export default {
if (this.dicomInfo.thick) {
this.dicomInfo.thick = this.dicomInfo.thick.toFixed(2)
}
- const newImageIdIndex = this.stack.imageIds.findIndex(i => i === e.detail.image.imageId)
+ const newImageIdIndex = this.stack.imageIds.findIndex(
+ (i) => i === e.detail.image.imageId
+ )
if (newImageIdIndex === -1) return
this.stack.currentImageIdIndex = newImageIdIndex
this.stack.imageIdIndex = newImageIdIndex
this.series.imageIdIndex = newImageIdIndex
- this.height = (this.stack.currentImageIdIndex) * 100 / (this.stack.imageIds.length - 1)
+ this.height =
+ (this.stack.currentImageIdIndex * 100) /
+ (this.stack.imageIds.length - 1)
this.resetWwwc()
},
stackScrollCallback(e) {
const { detail } = e
if (this.isScrollSync) {
- this.$emit('scrollSync', { canvasIndex: this.canvasIndex, direction: detail.direction })
+ this.$emit('scrollSync', {
+ canvasIndex: this.canvasIndex,
+ direction: detail.direction,
+ })
}
this.stack.currentImageIdIndex = e.detail.newImageIdIndex
- this.height = (this.stack.currentImageIdIndex) * 100 / (this.stack.imageIds.length - 1)
+ this.height =
+ (this.stack.currentImageIdIndex * 100) /
+ (this.stack.imageIds.length - 1)
// var priority = new Date(new Date().setHours(23, 59, 59, 999)).getTime()
// requestPoolManager.loadAndCacheImagePlus(this.stack.imageIds[this.stack.currentImageIdIndex], this.stack.seriesId, priority)
@@ -408,7 +464,9 @@ export default {
if (date) {
date = `${date.substr(0, 4)}-${date.substr(4, 2)}-${date.substr(6, 2)}`
}
- if (time) { time = `${time.substr(0, 2)}:${time.substr(2, 2)}:${time.substr(4, 2)}` }
+ if (time) {
+ time = `${time.substr(0, 2)}:${time.substr(2, 2)}:${time.substr(4, 2)}`
+ }
return time ? `${date} ${time}` : `${date} 00:00:00`
},
@@ -453,7 +511,7 @@ export default {
top: oppositeColumn,
bottom: column,
left: oppositeRow,
- right: row
+ right: row,
}
if (!markers) {
return
@@ -500,13 +558,7 @@ export default {
if (image.color) {
stats.storedPixels = this.getRGBPixels(element, x, y, 1, 1)
} else {
- stats.storedPixels = cornerstone.getStoredPixels(
- element,
- x,
- y,
- 1,
- 1
- )
+ stats.storedPixels = cornerstone.getStoredPixels(element, x, y, 1, 1)
stats.sp = stats.storedPixels[0]
stats.mo = stats.sp * image.slope + image.intercept
stats.suv = calculateSUV(image, stats.sp)
@@ -533,7 +585,8 @@ export default {
if (enabledElement.image.color) {
for (row = 0; row < height; row++) {
for (column = 0; column < width; column++) {
- spIndex = ((row + y) * enabledElement.image.columns + (column + x)) * 4
+ spIndex =
+ ((row + y) * enabledElement.image.columns + (column + x)) * 4
const red = pixelData[spIndex]
const green = pixelData[spIndex + 1]
const blue = pixelData[spIndex + 2]
@@ -551,7 +604,8 @@ export default {
},
sliderMousedown(e) {
var boxHeight = this.$refs['sliderBox'].clientHeight
- this.sliderInfo.oldB = parseInt(e.srcElement.style.top) * boxHeight / 100
+ this.sliderInfo.oldB =
+ (parseInt(e.srcElement.style.top) * boxHeight) / 100
this.sliderInfo.oldM = e.clientY
this.sliderInfo.isMove = true
e.stopImmediatePropagation()
@@ -564,9 +618,14 @@ export default {
var boxHeight = this.$refs['sliderBox'].clientHeight
if (PX < 0) return
if (PX > boxHeight) return
- var height = PX * 100 / boxHeight
- var index = Math.trunc(this.stack.imageIds.length * this.height / 100)
- index = index > this.stack.imageIds.length ? this.stack.imageIds.length : index < 0 ? 0 : index
+ var height = (PX * 100) / boxHeight
+ var index = Math.trunc((this.stack.imageIds.length * this.height) / 100)
+ index =
+ index > this.stack.imageIds.length
+ ? this.stack.imageIds.length
+ : index < 0
+ ? 0
+ : index
// if (!cornerstone.imageCache.getImageLoadObject(this.stack.imageIds[index])) return
this.height = height
if (this.stack.currentImageIdIndex !== index) {
@@ -578,9 +637,9 @@ export default {
},
goViewer(e) {
// console.log(this.$refs['sliderBox'].clientHeight)
- var height = e.offsetY * 100 / this.$refs['sliderBox'].clientHeight
+ var height = (e.offsetY * 100) / this.$refs['sliderBox'].clientHeight
this.height = height
- var index = Math.trunc(this.stack.imageIds.length * this.height / 100)
+ var index = Math.trunc((this.stack.imageIds.length * this.height) / 100)
scroll(this.canvas, index)
},
onClipStopped() {
@@ -673,10 +732,7 @@ export default {
}
this.toolState.clipPlaying = true
cornerstoneTools.playClip(this.canvas, this.dicomInfo.fps)
- cornerstoneTools.getToolState(
- this.canvas,
- 'playClip'
- ).data[0].loop = true
+ cornerstoneTools.getToolState(this.canvas, 'playClip').data[0].loop = true
},
setFps(fps) {
this.dicomInfo.fps = fps
@@ -712,8 +768,8 @@ export default {
invert: false,
preventZoomOutsideImage: false,
minScale: 0.1,
- maxScale: 20.0
- }
+ maxScale: 20.0,
+ },
})
cornerstoneTools.setToolActive('Zoom', { mouseButtonMask: 1 })
this.toolState.activeTool = 'zoom'
@@ -798,8 +854,9 @@ export default {
cornerstoneTools.setToolPassiveForElement(this.canvas, toolName)
},
setToolActive(toolName) {
+ console.log(this.canvas, 'this.canvas')
cornerstoneTools.setToolActiveForElement(this.canvas, toolName, {
- mouseButtonMask: 1
+ mouseButtonMask: 1,
})
},
setAllToolsPassive() {
@@ -833,7 +890,7 @@ export default {
)
}
cornerstoneTools.setToolEnabledForElement(this.canvas, 'ReferenceLines', {
- synchronizationContext: synchronizer
+ synchronizationContext: synchronizer,
})
// cornerstoneTools.addTool(cornerstoneTools.CrosshairsTool)
@@ -864,7 +921,7 @@ export default {
}
cornerstoneTools.setToolActiveForElement(this.canvas, toolName, {
mouseButtonMask: 1,
- synchronizationContext: synchronizer
+ synchronizationContext: synchronizer,
})
},
disabledViewPortToolSync(synchronizer, toolName) {
@@ -1095,7 +1152,7 @@ export default {
'CobbAngle',
'Angle',
'Bidirectional',
- 'FreehandRoi'
+ 'FreehandRoi',
]
for (let i = 0; i < toolROITypes.length; i++) {
const toolROIType = toolROITypes[i]
@@ -1115,12 +1172,12 @@ export default {
removeLabel(item) {
const promise = scroll(this.canvas, item.data.imageIdIndex)
const scope = this
- Promise.all([promise]).then(res => {
+ Promise.all([promise]).then((res) => {
cornerstoneTools.removeToolState(scope.canvas, item.type, item.data)
cornerstone.updateImage(scope.canvas)
})
- }
- }
+ },
+ },
}
diff --git a/src/components/Dicom/DicomViewer.vue b/src/components/Dicom/DicomViewer.vue
index 6bf1934a..c5692c5b 100644
--- a/src/components/Dicom/DicomViewer.vue
+++ b/src/components/Dicom/DicomViewer.vue
@@ -16,10 +16,7 @@
@click="activateDicomCanvas(0)"
@dblclick="setFullScreen($event)"
>
-
+
- -->
+ -->
-
@@ -190,65 +225,138 @@
{{ $t('trials:dicom-show:measurementLabeling') }}
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
@@ -368,7 +479,7 @@ export default {
name: 'DicomsViewer',
components: {
DicomCanvas,
- CustomWwwcForm
+ CustomWwwcForm,
},
data() {
return {
@@ -379,12 +490,12 @@ export default {
currentDicomCanvasIndex: 0,
currentDicomCanvas: {
toolState: {
- clipPlaying: false
- }
+ clipPlaying: false,
+ },
},
rowHeight: '100%',
sync: {
- Wwwc: null
+ Wwwc: null,
},
colormapsList: [],
rotateList: [],
@@ -393,11 +504,14 @@ export default {
layout: null,
seriesList: [],
customWwc: { visible: false, title: null },
- fps: 15
+ fps: 15,
}
},
mounted() {
- this.customWwc = { visible: false, title: this.$t('DicomViewer:data:customWwc') }
+ this.customWwc = {
+ visible: false,
+ title: this.$t('DicomViewer:data:customWwc'),
+ }
this.rotateList[0] = '1'
this.colorList[0] = ''
this.wwwcList[0] = '-1'
@@ -646,13 +760,12 @@ export default {
}
}
}
- }
- }
+ },
+ },
}
diff --git a/src/views/trials/trials-panel/reading/dicoms/tools/RectangleRoi/Note_RectangleRoiTool.js b/src/views/trials/trials-panel/reading/dicoms/tools/RectangleRoi/Note_RectangleRoiTool.js
new file mode 100644
index 00000000..c473bbef
--- /dev/null
+++ b/src/views/trials/trials-panel/reading/dicoms/tools/RectangleRoi/Note_RectangleRoiTool.js
@@ -0,0 +1,110 @@
+import cornerstone from 'cornerstone-core';
+import 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 {
+ constructor(props = {}) {
+ super({
+ 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
+ }
+ }
+ };
+ }
+
+ 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');
+ return;
+ }
+
+ // 2. 获取工具数据
+ const toolData = getToolState(element, this.name);
+ if (!toolData || !toolData.data || toolData.data.length === 0) return;
+
+ // 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;
+
+ 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
+ });
+ }
+
+ 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
+ };
+
+ context.font = '12px Arial';
+ context.fillStyle = data.color;
+ context.fillText(text, textCoords.x, textCoords.y);
+ }
+ });
+
+ context.restore();
+ }
+}
\ No newline at end of file