diff --git a/src/api/reading.js b/src/api/reading.js index 6e5b3872..53cdedae 100644 --- a/src/api/reading.js +++ b/src/api/reading.js @@ -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 + }) } \ No newline at end of file diff --git a/src/components/Dicom/DicomCanvas.vue b/src/components/Dicom/DicomCanvas.vue index e480a307..49f4e1cb 100644 --- a/src/components/Dicom/DicomCanvas.vue +++ b/src/components/Dicom/DicomCanvas.vue @@ -1,15 +1,7 @@ @@ -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. 获取当前imageId(如果未指定新imageId) + 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, }) diff --git a/src/components/Dicom/DicomViewer.vue b/src/components/Dicom/DicomViewer.vue index c5692c5b..1c60d3cb 100644 --- a/src/components/Dicom/DicomViewer.vue +++ b/src/components/Dicom/DicomViewer.vue @@ -6,102 +6,68 @@ --> - -
-
+
+
矩形
+
清除 +
+
应用
+
应用整个序列
+ +
对比
+
退出
+
恢复
+
+
+
-
+
-
+
-
-
+
+
-
+
-
+
-
-
+
+
-
+
-
+
@@ -113,7 +79,7 @@
{{ $t('trials:reading:button:layout') }}
- @@ -124,18 +90,15 @@ +
像素匿名
{{ $t('trials:dicom-show:transform') }}
- - - - - - - - @@ -225,169 +150,94 @@
{{ $t('trials:dicom-show:measurementLabeling') }}
- - - - - - - - - - - -
-
{{ $t('trials:dicom-show:play') }}
- - - - - @@ -403,21 +253,15 @@
- - + @@ -430,32 +274,17 @@
- - +
- +
@@ -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; } diff --git a/src/views/dicom-show/dicom-study.vue b/src/views/dicom-show/dicom-study.vue index 5533c10c..61c42b0f 100644 --- a/src/views/dicom-show/dicom-study.vue +++ b/src/views/dicom-show/dicom-study.vue @@ -123,7 +123,7 @@
- +