From dc8fb450fbc7fde44e442f6485f964627070cab8 Mon Sep 17 00:00:00 2001 From: caiyiling <1321909229@qq.com> Date: Fri, 23 Jan 2026 15:40:50 +0800 Subject: [PATCH] =?UTF-8?q?=E9=9D=9EDICOM=E9=A1=B5=E9=9D=A2=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=E5=85=B3=E9=94=AE=E5=9B=BE=E5=83=8F=E5=92=8C=E6=A0=87?= =?UTF-8?q?=E6=B3=A8=E5=9B=BE=E5=83=8F=E7=9A=84=E6=A0=87=E8=AF=86=EF=BC=8C?= =?UTF-8?q?=E9=98=85=E7=89=87=E5=B7=A5=E5=85=B7=E6=B7=BB=E5=8A=A0=E5=AE=9A?= =?UTF-8?q?=E5=9C=86=E5=B7=A5=E5=85=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../reading/dicoms3D/components/ReadPage.vue | 7 + .../tools/FixedRadiusCircleROITool.js | 226 ++++++++++++++++++ .../visit-review/components/FileViewer.vue | 3 + .../visit-review/components/ReadPage.vue | 14 +- .../visit-review/components/StudyList.vue | 38 ++- 5 files changed, 282 insertions(+), 6 deletions(-) create mode 100644 src/views/trials/trials-panel/reading/dicoms3D/components/tools/FixedRadiusCircleROITool.js diff --git a/src/views/trials/trials-panel/reading/dicoms3D/components/ReadPage.vue b/src/views/trials/trials-panel/reading/dicoms3D/components/ReadPage.vue index 4f4cda01..bbd214b6 100644 --- a/src/views/trials/trials-panel/reading/dicoms3D/components/ReadPage.vue +++ b/src/views/trials/trials-panel/reading/dicoms3D/components/ReadPage.vue @@ -494,6 +494,7 @@ import FusionForm from './FusionForm.vue' import colorMap from './colorMap.vue' import RectangleROITool from './tools/RectangleROITool' import ScaleOverlayTool from './tools/ScaleOverlayTool' +import FixedRadiusCircleROITool from './tools/FixedRadiusCircleROITool' import uploadDicomAndNonedicom from '@/components/uploadDicomAndNonedicom' import downloadDicomAndNonedicom from '@/components/downloadDicomAndNonedicom' import { getNetWorkSpeed, setNetWorkSpeedSizeAll, workSpeedclose } from "@/utils" @@ -1188,6 +1189,7 @@ export default { cornerstoneTools.addTool(BidirectionalTool) cornerstoneTools.addTool(ScaleOverlayTool) cornerstoneTools.addTool(CircleROITool) + cornerstoneTools.addTool(FixedRadiusCircleROITool) cornerstoneTools.addTool(EllipticalROITool) cornerstoneTools.addTool(AngleTool) cornerstoneTools.addTool(CobbAngleTool) @@ -1256,6 +1258,9 @@ export default { toolGroup.addTool(EllipticalROITool.toolName, { getTextLines: this.getEllipticalROIToolTextLines }) + toolGroup.addTool(FixedRadiusCircleROITool.toolName), { + getTextLines: this.getCircleROIToolTextLines + } toolGroup.addTool(AngleTool.toolName, { getTextLines: this.getAngleToolTextLines }) @@ -1316,6 +1321,7 @@ export default { toolGroup.setToolPassive(BidirectionalTool.toolName) toolGroup.setToolPassive(CircleROITool.toolName) toolGroup.setToolPassive(EllipticalROITool.toolName) + toolGroup.setToolPassive(FixedRadiusCircleROITool.toolName) toolGroup.setToolPassive(AngleTool.toolName) toolGroup.setToolPassive(CobbAngleTool.toolName) } else { @@ -1326,6 +1332,7 @@ export default { toolGroup.setToolEnabled(BidirectionalTool.toolName) toolGroup.setToolEnabled(CircleROITool.toolName) toolGroup.setToolEnabled(EllipticalROITool.toolName) + toolGroup.setToolEnabled(FixedRadiusCircleROITool.toolName) toolGroup.setToolEnabled(AngleTool.toolName) toolGroup.setToolEnabled(CobbAngleTool.toolName) } diff --git a/src/views/trials/trials-panel/reading/dicoms3D/components/tools/FixedRadiusCircleROITool.js b/src/views/trials/trials-panel/reading/dicoms3D/components/tools/FixedRadiusCircleROITool.js new file mode 100644 index 00000000..da073e7f --- /dev/null +++ b/src/views/trials/trials-panel/reading/dicoms3D/components/tools/FixedRadiusCircleROITool.js @@ -0,0 +1,226 @@ +import { getEnabledElement, utilities as csUtils } from '@cornerstonejs/core'; + +const { + utilities, + annotation, + cursors +} = cornerstoneTools +const { addAnnotation } = annotation.state +const { triggerAnnotationCompleted } = annotation.state +const { hideElementCursor } = cursors.elementCursor +const { getViewportIdsWithToolToRender } = utilities.viewportFilters +const { BasicStatsCalculator } = utilities.math.BasicStatsCalculator +const { triggerAnnotationRenderForViewportIds } = utilities + +/** + * FixedRadiusCircleTool lets you draw annotations that measures the statistics + * such as area, max, mean and stdDev of a circular region of interest with a fixed radius. + */ +class FixedRadiusCircleROITool extends cornerstoneTools.CircleROITool { + static toolName = 'FixedRadiusCircleROI'; + + constructor( + toolProps = {}, + defaultToolProps = { + supportedInteractionTypes: ['Mouse', 'Touch'], + configuration: { + shadow: true, + preventHandleOutsideImage: false, + storePointData: false, + centerPointRadius: 0, + radius: 10, // Default radius in mm + radiusUnit: 'mm', + statsCalculator: BasicStatsCalculator, + getTextLines: defaultGetTextLines, + }, + } + ) { + super(toolProps, defaultToolProps); + } + + /** + * Based on the current position of the mouse and the current imageId to create + * a CircleROI Annotation and stores it in the annotationManager + * + * @param evt - EventTypes.NormalizedMouseEventType + * @returns The annotation object. + * + */ + addNewAnnotation = ( + evt + ) => { + const eventDetail = evt.detail; + const { currentPoints, element } = eventDetail; + const worldPos = currentPoints.world; + + const enabledElement = getEnabledElement(element); + const { viewport } = enabledElement; + + const camera = viewport.getCamera(); + const { viewPlaneNormal, viewUp } = camera; + + const referencedImageId = this.getReferencedImageId( + viewport, + worldPos, + viewPlaneNormal, + viewUp + ); + + const FrameOfReferenceUID = viewport.getFrameOfReferenceUID(); + + // Calculate end point based on fixed radius + const radius = this.configuration.radius || 10; + // viewUp is a normalized vector. + // We want a point 'radius' distance away from center. + // We can use viewUp or any vector in the plane. + const endPos = [ + worldPos[0] + viewUp[0] * radius, + worldPos[1] + viewUp[1] * radius, + worldPos[2] + viewUp[2] * radius, + ]; + + const annotation = { + highlighted: true, + invalidated: true, + metadata: { + toolName: this.getToolName(), + viewPlaneNormal: [...viewPlaneNormal], + viewUp: [...viewUp], + FrameOfReferenceUID, + referencedImageId, + ...viewport.getViewReference({ points: [worldPos] }), + }, + data: { + label: '', + handles: { + textBox: { + hasMoved: false, + worldPosition: [0, 0, 0], + worldBoundingBox: { + topLeft: [0, 0, 0], + topRight: [0, 0, 0], + bottomLeft: [0, 0, 0], + bottomRight: [0, 0, 0], + }, + }, + points: [[...worldPos], [...endPos]], + activeHandleIndex: null, + }, + cachedStats: {}, + }, + }; + + addAnnotation(annotation, element); + + const viewportIdsToRender = getViewportIdsWithToolToRender( + element, + this.getToolName() + ); + + triggerAnnotationRenderForViewportIds(viewportIdsToRender); + triggerAnnotationCompleted(annotation); + + return annotation; + }; + + /** + * Overriding handleSelectedCallback to prevent resizing if needed, + * or just to handle the "fixed" nature. + * If we want it to be truly fixed radius, we should prevent dragging the second handle. + */ + handleSelectedCallback = ( + evt, + annotation, + handle + ) => { + const eventDetail = evt.detail; + const { element } = eventDetail; + const { data } = annotation; + + const { points } = data.handles; + let handleIndex = points.findIndex((p) => p === handle); + + // If handleIndex is 1 (the rim/radius handle), ignore the drag to keep radius fixed. + if (handleIndex === 1) { + return; + } + + // Otherwise, allow moving the circle (center handle) + // Implementation copied from CircleROITool because super.handleSelectedCallback is not available + // as it is an arrow function on the parent instance. + + annotation.highlighted = true; + + let movingTextBox = false; + + if ((handle).worldPosition) { + movingTextBox = true; + handleIndex = undefined; + } + + // Find viewports to render on drag. + const viewportIdsToRender = getViewportIdsWithToolToRender( + element, + this.getToolName() + ); + + this.editData = { + annotation, + viewportIdsToRender, + handleIndex, + movingTextBox, + }; + this._activateModify(element); + + hideElementCursor(element); + + triggerAnnotationRenderForViewportIds(viewportIdsToRender); + + evt.preventDefault(); + }; +} + +export default FixedRadiusCircleROITool; +function defaultGetTextLines(data, targetId) { + const cachedVolumeStats = data.cachedStats[targetId]; + const { + radius, + radiusUnit, + area, + mean, + stdDev, + max, + isEmptyArea, + areaUnit, + modalityUnit, + } = cachedVolumeStats; + const textLines = []; + + if (radius) { + const radiusLine = isEmptyArea + ? `Radius: Oblique not supported` + : `Radius: ${csUtils.roundNumber(radius)} ${radiusUnit}`; + textLines.push(radiusLine); + } + + if (area) { + const areaLine = isEmptyArea + ? `Area: Oblique not supported` + : `Area: ${csUtils.roundNumber(area)} ${areaUnit}`; + textLines.push(areaLine); + } + + if (mean) { + textLines.push(`Mean: ${csUtils.roundNumber(mean)} ${modalityUnit}`); + } + + if (max) { + textLines.push(`Max: ${csUtils.roundNumber(max)} ${modalityUnit}`); + } + + if (stdDev) { + textLines.push(`Std Dev: ${csUtils.roundNumber(stdDev)} ${modalityUnit}`); + } + + return textLines; +} \ No newline at end of file diff --git a/src/views/trials/trials-panel/reading/visit-review/components/FileViewer.vue b/src/views/trials/trials-panel/reading/visit-review/components/FileViewer.vue index 88d7a28f..931ddabc 100644 --- a/src/views/trials/trials-panel/reading/visit-review/components/FileViewer.vue +++ b/src/views/trials/trials-panel/reading/visit-review/components/FileViewer.vue @@ -1057,6 +1057,7 @@ export default { // 临时标记 return } + this.$emit('getMarkedFileIds', { type: "remove", visitTaskId: this.taskInfo.VisitTaskId, fileId: annotation.noneDicomFileId}) if (annotation.visitTaskId === this.taskInfo.VisitTaskId) { this.$emit('getEcrf', { type: "verifyAnnotationIsBound", VisitTaskId: annotation.visitTaskId, annotation }) this.$nextTick(async () => { @@ -1119,6 +1120,7 @@ export default { }) } + } catch (e) { cornerstoneTools.annotation.state.addAnnotation(annotation) const renderingEngine = getRenderingEngine(renderingEngineId) @@ -1259,6 +1261,7 @@ export default { // } } }) + this.$emit('getMarkedFileIds', { type: "add", visitTaskId: this.taskInfo.VisitTaskId, fileId: annotation.noneDicomFileId}) }, annotationSelectionChangeListener(e) { console.log('selection') diff --git a/src/views/trials/trials-panel/reading/visit-review/components/ReadPage.vue b/src/views/trials/trials-panel/reading/visit-review/components/ReadPage.vue index c4c7b33b..453387c3 100644 --- a/src/views/trials/trials-panel/reading/visit-review/components/ReadPage.vue +++ b/src/views/trials/trials-panel/reading/visit-review/components/ReadPage.vue @@ -13,7 +13,7 @@
+ :visit-task-info="s" @selectFile="selectFile" :currentMarkedFiles="currentMarkedFiles"/>
@@ -37,7 +37,7 @@ /> --> + @previewCD="previewCD" @setPS="setPS" @getEcrf="getEcrf" @getMarkedFileIds="getMarkedFileIds"/>
@@ -137,7 +137,8 @@ export default { readingTaskState: 0, ecrf: { IsHaveBindingQuestion: false - } + }, + currentMarkedFiles: {} } }, computed: { @@ -292,6 +293,9 @@ export default { this.relatedStudyInfo = { fileInfo, visitTaskInfo: this.visitTaskList[idx], fileList: studyList[0].NoneDicomStudyFileList, fileIndex: 0, studyId: studyList[0].Id } } } + if (this.readingTaskState < 2) { + this.$refs[res.Result[idx].VisitTaskId][0].initCurrentMaredFiles() + } this.loading = false } catch (e) { console.log(e) @@ -357,6 +361,10 @@ export default { this.psArr.push(obj) } }, + getMarkedFileIds(obj) { + const { visitTaskId } = obj + this.$refs[visitTaskId][0].getMarkedFileIds(obj) + }, // 切换任务 toggleTask(taskInfo) { this.setActiveTaskVisitId(taskInfo.VisitTaskId) diff --git a/src/views/trials/trials-panel/reading/visit-review/components/StudyList.vue b/src/views/trials/trials-panel/reading/visit-review/components/StudyList.vue index 38561a57..65bd7541 100644 --- a/src/views/trials/trials-panel/reading/visit-review/components/StudyList.vue +++ b/src/views/trials/trials-panel/reading/visit-review/components/StudyList.vue @@ -45,7 +45,10 @@
{{ k.FileName }}
- +
+ + +
@@ -75,7 +78,8 @@ export default { taskInfo: null, studyList: [], pdf, - BodyPart: {} + BodyPart: {}, + currentMarkedFiles: {} } }, async mounted() { @@ -160,7 +164,35 @@ export default { }, handleChange(v) { console.log(v) - } + }, + getMarkedFileIds(obj) { + const { type, fileId, visitTaskId } = obj + if (this.visitTaskInfo.ReadingTaskState >= 2 || visitTaskId !== this.taskInfo.VisitTaskId) { + return + } + if (!fileId) return + if (type === 'remove') { + if (Object.hasOwn(this.currentMarkedFiles, fileId)) { + this.currentMarkedFiles[fileId].count-- + } + } else if (type === 'add') { + if (!Object.hasOwn(this.currentMarkedFiles, fileId)) { + this.$set(this.currentMarkedFiles, fileId, { count: 1 }) + } else { + this.currentMarkedFiles[fileId].count++ + } + + } + }, + initCurrentMaredFiles() { + this.visitTaskInfo.Annotations.map(i=>{ + if (!Object.hasOwn(this.currentMarkedFiles, i.NoneDicomFileId)) { + this.$set(this.currentMarkedFiles, i.NoneDicomFileId, { count: 1 }) + } else { + this.currentMarkedFiles[i.NoneDicomFileId].count++ + } + }) + }, } }