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 @@ @@ -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') }}
- - - - - - - - - - - -
+
{{ $t('trials:dicom-show:play') }}
- - @@ -259,14 +367,27 @@ /> - - - @@ -293,25 +414,16 @@ - - - - - - - + + + + + + +
@@ -332,7 +444,6 @@ :value="item.id" >{{ item.name }} -
@@ -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