非DICOM页面添加关键图像和标注图像的标识,阅片工具添加定圆工具
continuous-integration/drone/push Build is passing
Details
continuous-integration/drone/push Build is passing
Details
parent
377cdb968d
commit
dc8fb450fb
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Reference in New Issue