非DICOM页面添加关键图像和标注图像的标识,阅片工具添加定圆工具
continuous-integration/drone/push Build is passing Details

main
caiyiling 2026-01-23 15:40:50 +08:00
parent 377cdb968d
commit dc8fb450fb
5 changed files with 282 additions and 6 deletions

View File

@ -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)
}

View File

@ -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;
}

View File

@ -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')

View File

@ -13,7 +13,7 @@
<div v-for="s in visitTaskList" v-show="activeTaskVisitId === s.VisitTaskId" :key="s.VisitTaskId"
style="height:100%;">
<study-list v-if="selectArr.includes(s.VisitTaskId) && s.StudyList.length > 0" :ref="s.VisitTaskId"
:visit-task-info="s" @selectFile="selectFile" />
:visit-task-info="s" @selectFile="selectFile" :currentMarkedFiles="currentMarkedFiles"/>
</div>
</div>
@ -37,7 +37,7 @@
/> -->
<file-viewer ref="fileViewer" :related-study-info="relatedStudyInfo" :ps-arr="psArr" :ecrf="ecrf"
@toggleTaskByViewer="toggleTaskByViewer" @toggleTask="toggleTask" @toggleImage="toggleImage"
@previewCD="previewCD" @setPS="setPS" @getEcrf="getEcrf" />
@previewCD="previewCD" @setPS="setPS" @getEcrf="getEcrf" @getMarkedFileIds="getMarkedFileIds"/>
</div>
<!-- 表单 -->
<div class="right-panel">
@ -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)

View File

@ -45,7 +45,10 @@
<div class="file-text" :title="k.FileName">
{{ k.FileName }}
</div>
<div>
<i v-if="taskInfo.ReadingTaskState < 2 && (currentMarkedFiles[k.Id] && currentMarkedFiles[k.Id].count > 0)" class="el-icon-star-on" style="font-size: 12px;color: #ff5722;" />
<i v-else-if="taskInfo.ReadingTaskState >= 2 && k.IsBeMark" class="el-icon-star-on" style="font-size: 12px;color: #ff5722;" />
</div>
</div>
</div>
</div>
@ -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++
}
})
},
}
}
</script>