分割添加阈值工具
continuous-integration/drone/push Build is passing Details

uat_us
wangxiaoshuang 2026-03-26 16:05:34 +08:00
parent 0273601f7b
commit 5c76bda1e7
4 changed files with 166 additions and 41 deletions

View File

@ -0,0 +1 @@
<svg width="24px" height="24px" viewBox="0 0 24 24" class="h-6 w-6"><g id="tool-seg-threshold" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"><rect id="Rectangle" x="0" y="0" width="24" height="24"></rect><g id="Group" transform="translate(2.4184, 14.3673)" stroke="currentColor" stroke-width="1.5"><line x1="16.5816327" y1="2.13265306" x2="19.0816327" y2="2.13265306" id="Line-2" stroke-linecap="round"></line><line x1="0.0816326531" y1="2.13265306" x2="12.0816327" y2="2.13265306" id="Line-2" stroke-linecap="round"></line><circle id="Oval" cx="14.2244898" cy="2.09183673" r="2.09183673"></circle></g><g id="Group" transform="translate(11.7092, 7.4592) scale(-1, 1) translate(-11.7092, -7.4592)translate(1.9184, 5.3673)" stroke="currentColor" stroke-width="1.5"><line x1="16" y1="2.13265306" x2="19.0816327" y2="2.13265306" id="Line-2" stroke-linecap="round"></line><line x1="0.0816326531" y1="2.13265306" x2="11" y2="2.13265306" id="Line-2" stroke-linecap="round"></line><circle id="Oval" cx="13.2244898" cy="2.09183673" r="2.09183673"></circle></g></g></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -273,11 +273,7 @@
</div>
<!-- 伪彩 -->
<template v-if="readingTool === 2">
<colorMap
v-show="isFusion"
ref="colorMap"
:modality="fusionOverlayModality"
@setColorMap="setColorMap"
<colorMap v-show="isFusion" ref="colorMap" :modality="fusionOverlayModality" @setColorMap="setColorMap"
@voiChange="voiChange" />
</template>
</div>
@ -308,7 +304,7 @@
:class="['viewports-box', isFusion || isMPR ? 'viewports-box-down' : '', fullScreenIndex !== null ? 'viewports-box-full-screen' : '']"
:style="gridStyle">
<div v-for="(v, index) in cellsMax" v-show="index < cells.length" :key="`viewport-${index}`"
:class="['grid-cell', isMPR && index === 2 ? 'grid-cell-3' : '', index === activeViewportIndex ? 'cell_active' : '', index === fullScreenIndex ? 'cell-full-screen' : '']"
:class="['grid-cell', index === activeViewportIndex ? 'cell_active' : '', index === fullScreenIndex ? 'cell-full-screen' : '']"
@dblclick="toggleFullScreen($event, index)" @click="activeViewport(index)">
<VolumeViewport :ref="`viewport-${index}`" :data-viewport-uid="`viewport-${index}`"
:rendering-engine-id="renderingEngineId" :viewport-id="`viewport-${index}`" :viewport-index="index"
@ -325,7 +321,7 @@
:style="gridStyleMPR">
<div v-for="(v, index) in 3" :key="`viewport-MPR-${index}`" v-show="index < cells.length"
:style="cellStyle"
:class="['grid-cell', isMPR && index === 0 ? 'grid-cell-3' : '', index === activeViewportIndex ? 'cell_active' : '', index === fullScreenIndex ? 'cell-full-screen' : '']"
:class="['grid-cell', index === 0 ? 'grid-cell-3' : '', index === activeViewportIndex ? 'cell_active' : '', index === fullScreenIndex ? 'cell-full-screen' : '']"
@dblclick="toggleFullScreen($event, index)" @click="activeViewport(index)">
<MPRViewport :ref="`viewport-MPR-${index}`" :data-viewport-uid="`viewport-MPR-${index}`"
:rendering-engine-id="renderingEngineId" :viewport-id="`viewport-MPR-${index}`"
@ -369,10 +365,11 @@
</el-tab-pane>
<el-tab-pane :label="$t('trials:reading:dicom3D:tabs:segment')" name="segment">
<Segmentations ref="Segmentations" :visitInfo="taskInfo" :isMPR="isMPR"
:volumeToolGroupId="volumeToolGroupId" :viewportKey="viewportKey"
:rendering-engine-id="renderingEngineId" :activeViewportIndex="activeViewportIndex"
:activeTool.sync="activeTool" :actionConfiguration="actionConfiguration"
@setToolsPassive="setToolsPassive" @resetQuestion="resetQuestion" />
:volumeToolGroupId="volumeToolGroupId" :viewportKey="viewportKey" :global-loading.sync="loading"
:loadingText.sync="loadingText" :rendering-engine-id="renderingEngineId"
:activeViewportIndex="activeViewportIndex" :activeTool.sync="activeTool"
:actionConfiguration="actionConfiguration" @setToolsPassive="setToolsPassive"
@resetQuestion="resetQuestion" />
</el-tab-pane>
</el-tabs>
<template v-else>
@ -1379,6 +1376,20 @@ export default {
}
)
toolGroup.addToolInstance(
'ThresholdCircle',
BrushTool.toolName,
{
activeStrategy: 'THRESHOLD_INSIDE_CIRCLE',
}
)
toolGroup.addToolInstance(
'ThresholdSphere',
BrushTool.toolName,
{
activeStrategy: 'THRESHOLD_INSIDE_SPHERE',
}
)
toolGroup.addTool(LabelMapEditWithContourTool.toolName);
toolGroup.addTool(SegmentBidirectionalTool.toolName);
}

View File

@ -12,6 +12,12 @@
@click.prevent="setToolActive('LabelMapEditWithContour')">
<svg-icon icon-class="contour" class="svg-icon" />
</div>
<div :title="$t('trials:Segmentations:tools:thresholecircle')"
:class="['tool-item', ThresholdTools.includes(activeTool) && segmentList.length > 0 ? 'tool-item-active' : '']"
:style="{ cursor: segmentList.length <= 0 || (curSegment && curSegment.lock) ? 'not-allowed' : 'pointer' }"
@click.prevent="initThreshold">
<svg-icon icon-class="thresholecircle" class="svg-icon" />
</div>
<div :title="$t('trials:Segmentations:tools:circularbrush')"
:class="['tool-item', activeTool === 'CircularBrush' && segmentList.length > 0 ? 'tool-item-active' : '']"
:style="{ cursor: segmentList.length <= 0 || (curSegment && curSegment.lock) ? 'not-allowed' : 'pointer' }"
@ -28,11 +34,41 @@
</div> -->
</div>
<div class="ConfigBox">
<div class="EraserConfig" v-if="activeTool === 'CircularEraser' || activeTool === 'CircularBrush'">
<div class="EraserConfig"
v-if="activeTool === 'CircularEraser' || activeTool === 'CircularBrush' || ThresholdTools.includes(activeTool)">
<span>{{ $t('trials:reading:Segmentations:title:EraserConfig') }}</span>
<el-slider v-model="brushSize" show-input :step="1" :max="100" input-size="mini"
:show-input-controls="false" />
</div>
<div class="EraserConfig" v-if="ThresholdTools.includes(activeTool)">
<span>{{ $t('trials:reading:Segmentations:title:thresholdType') }}</span>
<el-radio-group v-model="thresholdType" @input="(key) => setToolActive(key)">
<el-radio-button :label="tool" v-for="tool of ThresholdTools" :key="tool" size="mini">
{{ $t(`trials:reading:Segmentations:radioButton:${tool}`) }}
</el-radio-button>
</el-radio-group>
</div>
<div class="EraserConfig" v-if="ThresholdTools.includes(activeTool)">
<span>{{ $t('trials:reading:Segmentations:title:isDynamic') }}</span>
<el-radio-group v-model="brushThreshold.isDynamic">
<el-radio-button :label="true" size="mini">
{{ $t('trials:reading:Segmentations:radioButton:Dynamic') }}
</el-radio-button>
<el-radio-button :label="false" size="mini">
{{ $t('trials:reading:Segmentations:radioButton:Rang') }}
</el-radio-button>
</el-radio-group>
</div>
<div class="EraserConfig" v-if="ThresholdTools.includes(activeTool) && !brushThreshold.isDynamic">
<span>{{ $t('trials:reading:Segmentations:title:range') }}</span>
<div style="display: flex;align-items: center;">
<el-input-number v-model="brushThreshold.range[0]" :controls="false" :step="1"
:max="brushThreshold.range[1]" size="small" />
<span style="margin: 0 10px;">--</span>
<el-input-number v-model="brushThreshold.range[1]" :controls="false" :step="1"
:min="brushThreshold.range[0]" size="small" />
</div>
</div>
</div>
</el-collapse-item>
<el-collapse-item name="Segment">
@ -84,7 +120,7 @@
</el-switch>
<span style="margin-left: 5px;">{{
$t('trials:reading:Segmentations:title:InactiveSegmentationsShow')
}}</span>
}}</span>
</div>
<!-- <div class="SegmentConfig" v-if="SegmentConfig.InactiveSegmentations.show">
<span>{{ $t('trials:reading:Segmentations:title:Opacity') }}</span>
@ -112,7 +148,7 @@
<i slot="reference" class="el-icon-more" style="cursor: pointer;color:#fff" />
</el-popover>
<el-select v-model="segmentationId" placeholder="" @change="selectSegmentGroup">
<el-option v-for="item in segmentList" :key="item.segmentationId" :label="item.name"
<el-option v-for="item in segmentList" :key="`${item.segmentationId}`" :label="item.name"
:value="item.segmentationId">
</el-option>
</el-select>
@ -155,7 +191,7 @@
<div v-for="k in statsKey" :key="k" class="statsBox">
<span>{{ k }}</span>
<span v-if="item.stats[k]">{{ Number(item.stats[k].value).toFixed(2)
}}<i>{{ item.stats[k].unit }}</i></span>
}}<i>{{ item.stats[k].unit }}</i></span>
</div>
</template>
<div class="serialNum" slot="reference">{{ item.segmentIndex }}</div>
@ -262,6 +298,14 @@ export default {
type: String,
required: true
},
loadingText: {
type: String,
default: ''
},
globalLoading: {
type: Boolean,
default: false
}
},
data() {
return {
@ -269,6 +313,13 @@ export default {
series: {},
activeNames: ['tools', 'Segment'],
brushSize: 10,
brushThreshold: {
dynamicRadius: 0,
isDynamic: false,
range: [200, 1000]
},
ThresholdTools: ['ThresholdCircle', 'ThresholdSphere'],
thresholdType: null,
showSegmentConfig: false,
SegmentConfig: {
renderOutline: true,
@ -344,12 +395,25 @@ export default {
},
brushSize: {
handler() {
this.setBrushSize()
this.setBrushSize(this.activeTool)
},
deep: true
},
brushThreshold: {
handler() {
this.setBrushThreshold()
},
deep: true
}
},
methods: {
initThreshold() {
if (!this.ThresholdTools.includes(this.activeTool)) {
this.setToolActive(this.ThresholdTools[0])
this.thresholdType = this.ThresholdTools[0]
}
},
createSegmentConfiguration(segmentIndex, segmentationId, otherSegments) {
const containedSegmentIndices = otherSegments
? { has: (segmentIndex) => otherSegments.indexOf(segmentIndex) !== -1 }
@ -469,8 +533,12 @@ export default {
toolGroup.setToolActive(toolName, {
bindings: [{ mouseButton: MouseBindings.Primary }]
})
this.setBrushSize()
this.$emit('update:activeTool', toolName)
this.setBrushSize(toolName)
if (this.ThresholdTools.includes(toolName)) {
this.setBrushThreshold()
}
}
},
changeShowSegmentConfig() {
@ -504,12 +572,12 @@ export default {
} else {
const points = an.data.handles.points;
const worldPoint = points[0]; //
let imageData = cache.getVolume(this.series.SeriesInstanceUid).imageData
let volume = cache.getVolume(this.series.SeriesInstanceUid)
let { imageData, numFrames } = volume
const ijk = imageData.worldToIndex(worldPoint);
const sliceIndex = Math.abs(Math.round(ijk[2]));
let imageIds = viewport.getImageIds(this.series.SeriesInstanceUid)
// console.log(sliceIndex, 'sliceIndex')
csUtils.jumpToSlice(viewport.element, { imageIndex: imageIds.length - sliceIndex - 1 });
csUtils.jumpToSlice(viewport.element, { imageIndex: numFrames - sliceIndex - 1 });
}
}
@ -1052,9 +1120,30 @@ export default {
}
)
},
setBrushSize() {
setBrushSize(toolName = null) {
const toolGroupId = this.isMPR ? this.volumeToolGroupId : `${this.viewportKey}-${this.activeViewportIndex}`
CStUtils.segmentation.setBrushSizeForToolGroup(toolGroupId, this.brushSize);
if (toolName === 'ThresholdSphere') {
this.setDynamicRadius()
}
},
setDynamicRadius() {
let volume = cache.getVolume(this.series.SeriesInstanceUid);
let { spacing, numFrames } = volume
let constant = numFrames * spacing[2] / 100
this.brushThreshold.dynamicRadius = Math.ceil(this.brushSize * constant)
// console.log(this.brushThreshold.dynamicRadius)
},
setBrushThreshold() {
const toolGroupId = this.isMPR ? this.volumeToolGroupId : `${this.viewportKey}-${this.activeViewportIndex}`
let brushThreshold = {
isDynamic: this.brushThreshold.isDynamic,
dynamicRadius: this.brushThreshold.dynamicRadius,
}
if (!this.brushThreshold.isDynamic) {
brushThreshold.range = this.brushThreshold.range
}
CStUtils.segmentation.setBrushThresholdForToolGroup(toolGroupId, brushThreshold);
},
async createSegmentation(segmentationId) {
const toolGroupId = this.isMPR ? this.volumeToolGroupId : `${this.viewportKey}-${this.activeViewportIndex}`
@ -1070,18 +1159,21 @@ export default {
}
)
}
segmentation.addSegmentations([
{
segmentationId,
representation: {
type: cornerstoneTools.Enums.SegmentationRepresentations
.Labelmap,
data: {
volumeId: segmentationId
if (!segmentation.state.getSegmentation(segmentationId)) {
segmentation.addSegmentations([
{
segmentationId,
representation: {
type: cornerstoneTools.Enums.SegmentationRepresentations
.Labelmap,
data: {
volumeId: segmentationId
}
}
}
}
]);
]);
}
},
segmentationModifiedCallback(evt) {
const { detail } = evt;
@ -1097,6 +1189,7 @@ export default {
this.drawing = true
},
async calculateStatistics(indices, segmentationId, mode) {
if (!segmentation.state.getSegmentation(segmentationId)) return false
const stats = await segmentationUtils.getStatistics({
segmentationId,
segmentIndices: indices,
@ -1151,16 +1244,19 @@ export default {
if (!this.isDel) {
this.calculateStatistics([this.segmentIndex], this.segmentationId, 'individual');
let segmentGroup = this.segmentList.find(item => item.segmentationId === this.segmentationId)
let segment = segmentGroup.segments.find(item => item.segmentIndex === this.segmentIndex)
this.getBidirectional([segment])
let segment = segmentGroup ? segmentGroup.segments.find(item => item.segmentIndex === this.segmentIndex) : null
if (segment) this.getBidirectional([segment])
} else {
let segmentGroup = this.segmentList.find(item => item.segmentationId === this.segmentationId)
let segmentIndexs = []
segmentGroup.segments.forEach(item => {
segmentIndexs.push(item.segmentIndex)
})
this.getBidirectional(segmentGroup.segments)
this.calculateStatistics(segmentIndexs, this.segmentationId, 'individual');
if (segmentGroup && segmentGroup.segments && segmentGroup.segments.length > 0) {
let segmentIndexs = []
segmentGroup.segments.forEach(item => {
segmentIndexs.push(item.segmentIndex)
})
this.getBidirectional(segmentGroup.segments)
this.calculateStatistics(segmentIndexs, this.segmentationId, 'individual');
}
}
this.isDel = false
@ -1243,6 +1339,8 @@ export default {
let confirm = await this.$confirm(this.$t("segment:confirm:questionNeedChange"))
if (!confirm) return false
}
this.$emit("update:globalLoading", true)
this.$emit("update:loadingText", this.$t("segment:loadingText:saveSegmentation"))
for (let i = 0; i < this.segmentList.length; i++) {
let segmentGroup = this.segmentList[i]
//
@ -1266,6 +1364,7 @@ export default {
})
}
if (bindingList.length > 0) this.saveSegmentBindingAndAnswer(bindingList)
this.$emit("update:globalLoading", false)
} catch (err) {
this.loading = false
console.log(err)
@ -1310,6 +1409,7 @@ export default {
let res = await getSegmentationList(data);
this.loading = false;
if (res.IsSuccess) {
this.segmentList = []
let list = res.Result.CurrentPageData;
for (let i = 0; i < list.length; i++) {
let item = list[i]
@ -1356,7 +1456,6 @@ export default {
})
}
}
// console.log(segmentation.state.getSegmentations(), 'segmentation.state.getSegmentations()')
} catch (err) {
this.loading = false
console.log(err)
@ -1701,6 +1800,19 @@ export default {
}
}
.EraserConfig {
margin: 5px 0;
::v-deep .el-input-number {
width: 50px;
}
::v-deep .el-input-number.is-without-controls .el-input__inner {
padding: 0 5px;
width: 50px;
}
}
.SegmentConfig {
padding: 5px;
}

View File

@ -270,7 +270,8 @@ export default {
const viewport = renderingEngine.getViewport(this.viewportId)
const zoom = viewport.getZoom()
this.imageInfo.zoom = zoom.toFixed(4)
let imageId = viewport.getCurrentImageId()
let imageIds = viewport.getImageIds(this.volumeId)
let imageId = imageIds[0]
if (imageId) {
const imagePlaneModule = metaData.get('imagePlaneModule', imageId)
this.imageInfo.imageOrientationPatient = imagePlaneModule.imageOrientationPatient