非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 colorMap from './colorMap.vue'
|
||||||
import RectangleROITool from './tools/RectangleROITool'
|
import RectangleROITool from './tools/RectangleROITool'
|
||||||
import ScaleOverlayTool from './tools/ScaleOverlayTool'
|
import ScaleOverlayTool from './tools/ScaleOverlayTool'
|
||||||
|
import FixedRadiusCircleROITool from './tools/FixedRadiusCircleROITool'
|
||||||
import uploadDicomAndNonedicom from '@/components/uploadDicomAndNonedicom'
|
import uploadDicomAndNonedicom from '@/components/uploadDicomAndNonedicom'
|
||||||
import downloadDicomAndNonedicom from '@/components/downloadDicomAndNonedicom'
|
import downloadDicomAndNonedicom from '@/components/downloadDicomAndNonedicom'
|
||||||
import { getNetWorkSpeed, setNetWorkSpeedSizeAll, workSpeedclose } from "@/utils"
|
import { getNetWorkSpeed, setNetWorkSpeedSizeAll, workSpeedclose } from "@/utils"
|
||||||
|
|
@ -1188,6 +1189,7 @@ export default {
|
||||||
cornerstoneTools.addTool(BidirectionalTool)
|
cornerstoneTools.addTool(BidirectionalTool)
|
||||||
cornerstoneTools.addTool(ScaleOverlayTool)
|
cornerstoneTools.addTool(ScaleOverlayTool)
|
||||||
cornerstoneTools.addTool(CircleROITool)
|
cornerstoneTools.addTool(CircleROITool)
|
||||||
|
cornerstoneTools.addTool(FixedRadiusCircleROITool)
|
||||||
cornerstoneTools.addTool(EllipticalROITool)
|
cornerstoneTools.addTool(EllipticalROITool)
|
||||||
cornerstoneTools.addTool(AngleTool)
|
cornerstoneTools.addTool(AngleTool)
|
||||||
cornerstoneTools.addTool(CobbAngleTool)
|
cornerstoneTools.addTool(CobbAngleTool)
|
||||||
|
|
@ -1256,6 +1258,9 @@ export default {
|
||||||
toolGroup.addTool(EllipticalROITool.toolName, {
|
toolGroup.addTool(EllipticalROITool.toolName, {
|
||||||
getTextLines: this.getEllipticalROIToolTextLines
|
getTextLines: this.getEllipticalROIToolTextLines
|
||||||
})
|
})
|
||||||
|
toolGroup.addTool(FixedRadiusCircleROITool.toolName), {
|
||||||
|
getTextLines: this.getCircleROIToolTextLines
|
||||||
|
}
|
||||||
toolGroup.addTool(AngleTool.toolName, {
|
toolGroup.addTool(AngleTool.toolName, {
|
||||||
getTextLines: this.getAngleToolTextLines
|
getTextLines: this.getAngleToolTextLines
|
||||||
})
|
})
|
||||||
|
|
@ -1316,6 +1321,7 @@ export default {
|
||||||
toolGroup.setToolPassive(BidirectionalTool.toolName)
|
toolGroup.setToolPassive(BidirectionalTool.toolName)
|
||||||
toolGroup.setToolPassive(CircleROITool.toolName)
|
toolGroup.setToolPassive(CircleROITool.toolName)
|
||||||
toolGroup.setToolPassive(EllipticalROITool.toolName)
|
toolGroup.setToolPassive(EllipticalROITool.toolName)
|
||||||
|
toolGroup.setToolPassive(FixedRadiusCircleROITool.toolName)
|
||||||
toolGroup.setToolPassive(AngleTool.toolName)
|
toolGroup.setToolPassive(AngleTool.toolName)
|
||||||
toolGroup.setToolPassive(CobbAngleTool.toolName)
|
toolGroup.setToolPassive(CobbAngleTool.toolName)
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -1326,6 +1332,7 @@ export default {
|
||||||
toolGroup.setToolEnabled(BidirectionalTool.toolName)
|
toolGroup.setToolEnabled(BidirectionalTool.toolName)
|
||||||
toolGroup.setToolEnabled(CircleROITool.toolName)
|
toolGroup.setToolEnabled(CircleROITool.toolName)
|
||||||
toolGroup.setToolEnabled(EllipticalROITool.toolName)
|
toolGroup.setToolEnabled(EllipticalROITool.toolName)
|
||||||
|
toolGroup.setToolEnabled(FixedRadiusCircleROITool.toolName)
|
||||||
toolGroup.setToolEnabled(AngleTool.toolName)
|
toolGroup.setToolEnabled(AngleTool.toolName)
|
||||||
toolGroup.setToolEnabled(CobbAngleTool.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
|
return
|
||||||
}
|
}
|
||||||
|
this.$emit('getMarkedFileIds', { type: "remove", visitTaskId: this.taskInfo.VisitTaskId, fileId: annotation.noneDicomFileId})
|
||||||
if (annotation.visitTaskId === this.taskInfo.VisitTaskId) {
|
if (annotation.visitTaskId === this.taskInfo.VisitTaskId) {
|
||||||
this.$emit('getEcrf', { type: "verifyAnnotationIsBound", VisitTaskId: annotation.visitTaskId, annotation })
|
this.$emit('getEcrf', { type: "verifyAnnotationIsBound", VisitTaskId: annotation.visitTaskId, annotation })
|
||||||
this.$nextTick(async () => {
|
this.$nextTick(async () => {
|
||||||
|
|
@ -1119,6 +1120,7 @@ export default {
|
||||||
})
|
})
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
cornerstoneTools.annotation.state.addAnnotation(annotation)
|
cornerstoneTools.annotation.state.addAnnotation(annotation)
|
||||||
const renderingEngine = getRenderingEngine(renderingEngineId)
|
const renderingEngine = getRenderingEngine(renderingEngineId)
|
||||||
|
|
@ -1259,6 +1261,7 @@ export default {
|
||||||
// }
|
// }
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
this.$emit('getMarkedFileIds', { type: "add", visitTaskId: this.taskInfo.VisitTaskId, fileId: annotation.noneDicomFileId})
|
||||||
},
|
},
|
||||||
annotationSelectionChangeListener(e) {
|
annotationSelectionChangeListener(e) {
|
||||||
console.log('selection')
|
console.log('selection')
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@
|
||||||
<div v-for="s in visitTaskList" v-show="activeTaskVisitId === s.VisitTaskId" :key="s.VisitTaskId"
|
<div v-for="s in visitTaskList" v-show="activeTaskVisitId === s.VisitTaskId" :key="s.VisitTaskId"
|
||||||
style="height:100%;">
|
style="height:100%;">
|
||||||
<study-list v-if="selectArr.includes(s.VisitTaskId) && s.StudyList.length > 0" :ref="s.VisitTaskId"
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -37,7 +37,7 @@
|
||||||
/> -->
|
/> -->
|
||||||
<file-viewer ref="fileViewer" :related-study-info="relatedStudyInfo" :ps-arr="psArr" :ecrf="ecrf"
|
<file-viewer ref="fileViewer" :related-study-info="relatedStudyInfo" :ps-arr="psArr" :ecrf="ecrf"
|
||||||
@toggleTaskByViewer="toggleTaskByViewer" @toggleTask="toggleTask" @toggleImage="toggleImage"
|
@toggleTaskByViewer="toggleTaskByViewer" @toggleTask="toggleTask" @toggleImage="toggleImage"
|
||||||
@previewCD="previewCD" @setPS="setPS" @getEcrf="getEcrf" />
|
@previewCD="previewCD" @setPS="setPS" @getEcrf="getEcrf" @getMarkedFileIds="getMarkedFileIds"/>
|
||||||
</div>
|
</div>
|
||||||
<!-- 表单 -->
|
<!-- 表单 -->
|
||||||
<div class="right-panel">
|
<div class="right-panel">
|
||||||
|
|
@ -137,7 +137,8 @@ export default {
|
||||||
readingTaskState: 0,
|
readingTaskState: 0,
|
||||||
ecrf: {
|
ecrf: {
|
||||||
IsHaveBindingQuestion: false
|
IsHaveBindingQuestion: false
|
||||||
}
|
},
|
||||||
|
currentMarkedFiles: {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
|
@ -292,6 +293,9 @@ export default {
|
||||||
this.relatedStudyInfo = { fileInfo, visitTaskInfo: this.visitTaskList[idx], fileList: studyList[0].NoneDicomStudyFileList, fileIndex: 0, studyId: studyList[0].Id }
|
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
|
this.loading = false
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log(e)
|
console.log(e)
|
||||||
|
|
@ -357,6 +361,10 @@ export default {
|
||||||
this.psArr.push(obj)
|
this.psArr.push(obj)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
getMarkedFileIds(obj) {
|
||||||
|
const { visitTaskId } = obj
|
||||||
|
this.$refs[visitTaskId][0].getMarkedFileIds(obj)
|
||||||
|
},
|
||||||
// 切换任务
|
// 切换任务
|
||||||
toggleTask(taskInfo) {
|
toggleTask(taskInfo) {
|
||||||
this.setActiveTaskVisitId(taskInfo.VisitTaskId)
|
this.setActiveTaskVisitId(taskInfo.VisitTaskId)
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,10 @@
|
||||||
<div class="file-text" :title="k.FileName">
|
<div class="file-text" :title="k.FileName">
|
||||||
{{ k.FileName }}
|
{{ k.FileName }}
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -75,7 +78,8 @@ export default {
|
||||||
taskInfo: null,
|
taskInfo: null,
|
||||||
studyList: [],
|
studyList: [],
|
||||||
pdf,
|
pdf,
|
||||||
BodyPart: {}
|
BodyPart: {},
|
||||||
|
currentMarkedFiles: {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async mounted() {
|
async mounted() {
|
||||||
|
|
@ -160,7 +164,35 @@ export default {
|
||||||
},
|
},
|
||||||
handleChange(v) {
|
handleChange(v) {
|
||||||
console.log(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>
|
</script>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue