定圆工具配置及功能实现
continuous-integration/drone/push Build is passing Details

uat_us
caiyiling 2026-04-14 21:19:00 +08:00
parent eac6b25b9e
commit 266698cc79
6 changed files with 125 additions and 22 deletions

View File

@ -16,10 +16,10 @@
"dependencies": { "dependencies": {
"@aws-sdk/client-s3": "3.726.1", "@aws-sdk/client-s3": "3.726.1",
"@cornerstonejs/adapters": "^4.19.2", "@cornerstonejs/adapters": "^4.19.2",
"@cornerstonejs/polymorphic-segmentation": "4.19.2",
"@cornerstonejs/calculate-suv": "^1.1.0", "@cornerstonejs/calculate-suv": "^1.1.0",
"@cornerstonejs/core": "^4.19.2", "@cornerstonejs/core": "^4.19.2",
"@cornerstonejs/dicom-image-loader": "^4.19.2", "@cornerstonejs/dicom-image-loader": "^4.19.2",
"@cornerstonejs/polymorphic-segmentation": "^4.19.2",
"@cornerstonejs/tools": "^4.19.2", "@cornerstonejs/tools": "^4.19.2",
"@fingerprintjs/fingerprintjs": "^4.6.2", "@fingerprintjs/fingerprintjs": "^4.6.2",
"@icr/polyseg-wasm": "^0.4.0", "@icr/polyseg-wasm": "^0.4.0",

View File

@ -1605,9 +1605,10 @@ export default {
toolGroup.addTool(EllipticalROITool.toolName, { toolGroup.addTool(EllipticalROITool.toolName, {
getTextLines: this.getEllipticalROIToolTextLines getTextLines: this.getEllipticalROIToolTextLines
}) })
toolGroup.addTool(FixedRadiusCircleROITool.toolName), { toolGroup.addTool(FixedRadiusCircleROITool.toolName, {
radius: Number.isFinite(this.taskInfo.CircleRadius) ? this.taskInfo.CircleRadius : 1,
getTextLines: this.getCircleROIToolTextLines getTextLines: this.getCircleROIToolTextLines
} })
toolGroup.addTool(AngleTool.toolName, { toolGroup.addTool(AngleTool.toolName, {
getTextLines: this.getAngleToolTextLines getTextLines: this.getAngleToolTextLines
}) })

View File

@ -1,5 +1,6 @@
import vtkColorTransferFunction from "@kitware/vtk.js/Rendering/Core/ColorTransferFunction"; import vtkColorTransferFunction from "@kitware/vtk.js/Rendering/Core/ColorTransferFunction";
import vtkPiecewiseFunction from "@kitware/vtk.js/Common/DataModel/PiecewiseFunction"; import vtkPiecewiseFunction from "@kitware/vtk.js/Common/DataModel/PiecewiseFunction";
import { Scale } from "@kitware/vtk.js/Rendering/Core/ColorTransferFunction/Constants";
import { cache, metaData, utilities } from "@cornerstonejs/core"; import { cache, metaData, utilities } from "@cornerstonejs/core";
const { getColormap } = utilities.colormap; const { getColormap } = utilities.colormap;
@ -14,7 +15,7 @@ function getWindowCenterFromVolumeId(volumeId) {
? voiLutModule.windowCenter[0] ? voiLutModule.windowCenter[0]
: voiLutModule?.windowCenter; : voiLutModule?.windowCenter;
const center = Number(rawCenter); const center = Number(rawCenter);
return Number.isFinite(center) ? center : null; return center;
} }
export default function setPetColorMapTransferFunctionForVolumeActor({ export default function setPetColorMapTransferFunctionForVolumeActor({
@ -33,7 +34,25 @@ export default function setPetColorMapTransferFunctionForVolumeActor({
const center = getWindowCenterFromVolumeId(volumeId); const center = getWindowCenterFromVolumeId(volumeId);
const upper = center > 1 ? center : 5; const upper = center > 1 ? center : 5;
cfun.setMappingRange(1, upper); const safeUpper = Number.isFinite(upper) && upper > 0 ? upper : 5;
const rangeMin = 0;
const rangeMax = safeUpper;
cfun.setScale(Scale.LOG10);
cfun.setMappingRange(rangeMin, rangeMax);
volumeActor.getProperty().setRGBTransferFunction(0, cfun); volumeActor.getProperty().setRGBTransferFunction(0, cfun);
//低信号更明显,系数可以更小
const thresholdValue0 = Math.max(rangeMin, rangeMax * 0.002);
const thresholdValue1 = Math.max(thresholdValue0, rangeMax * 0.02);
const delta = Math.abs(rangeMax - rangeMin) * 0.001;
const threshold0MinusDelta = Math.max(rangeMin, thresholdValue0 - delta);
const ofun = vtkPiecewiseFunction.newInstance();
ofun.addPoint(rangeMin, 0.0);
ofun.addPoint(threshold0MinusDelta, 0.0);
//低信号更明显,系数可以更小
ofun.addPoint(thresholdValue0, 0.08);
ofun.addPoint(thresholdValue1, 0.9);
ofun.addPoint(rangeMax, 1.0);
volumeActor.getProperty().setScalarOpacity(0, ofun);
} }

View File

@ -311,6 +311,15 @@ const config = {
'isDisabled': false, 'isDisabled': false,
'disabledReason': '' 'disabledReason': ''
}, },
{
'name': '定圆工具',
'icon': 'oval',
'toolName': 'FixedRadiusCircleROI',
'props': ['radius', 'area', 'mean', 'max', 'stdDev'],
'i18nKey': 'trials:reading:button:fixedCircle',
'isDisabled': false,
'disabledReason': ''
}
], ],
'customizeStandardsNoneDicom': [ 'customizeStandardsNoneDicom': [
{ {

View File

@ -28,10 +28,12 @@ class FixedRadiusCircleROITool extends cornerstoneTools.CircleROITool {
preventHandleOutsideImage: false, preventHandleOutsideImage: false,
storePointData: false, storePointData: false,
centerPointRadius: 0, centerPointRadius: 0,
calculateStats: true,
radius: 10, // Default radius in mm radius: 10, // Default radius in mm
radiusUnit: 'mm', radiusUnit: 'mm',
statsCalculator: BasicStatsCalculator, statsCalculator: BasicStatsCalculator,
getTextLines: defaultGetTextLines, getTextLines: defaultGetTextLines,
simplified: true,
}, },
} }
) { ) {
@ -67,9 +69,12 @@ class FixedRadiusCircleROITool extends cornerstoneTools.CircleROITool {
); );
const FrameOfReferenceUID = viewport.getFrameOfReferenceUID(); const FrameOfReferenceUID = viewport.getFrameOfReferenceUID();
const targetId = this.getTargetId(viewport);
// Calculate end point based on fixed radius // Calculate end point based on fixed radius
const radius = this.configuration.radius || 10; const radius = Number.isFinite(this.configuration.radius)
? this.configuration.radius
: 10;
// viewUp is a normalized vector. // viewUp is a normalized vector.
// We want a point 'radius' distance away from center. // We want a point 'radius' distance away from center.
// We can use viewUp or any vector in the plane. // We can use viewUp or any vector in the plane.
@ -106,7 +111,20 @@ class FixedRadiusCircleROITool extends cornerstoneTools.CircleROITool {
points: [[...worldPos], [...endPos]], points: [[...worldPos], [...endPos]],
activeHandleIndex: null, activeHandleIndex: null,
}, },
cachedStats: {}, cachedStats: {
[targetId]: {
Modality: null,
radius: null,
radiusUnit: null,
area: null,
mean: null,
stdDev: null,
max: null,
isEmptyArea: null,
areaUnit: null,
modalityUnit: null,
},
},
}, },
}; };
@ -180,9 +198,11 @@ class FixedRadiusCircleROITool extends cornerstoneTools.CircleROITool {
}; };
} }
export default FixedRadiusCircleROITool;
function defaultGetTextLines(data, targetId) { function defaultGetTextLines(data, targetId) {
const cachedVolumeStats = data.cachedStats[targetId]; const cachedVolumeStats = data?.cachedStats?.[targetId];
if (!cachedVolumeStats) {
return [];
}
const { const {
radius, radius,
radiusUnit, radiusUnit,
@ -195,32 +215,33 @@ function defaultGetTextLines(data, targetId) {
modalityUnit, modalityUnit,
} = cachedVolumeStats; } = cachedVolumeStats;
const textLines = []; const textLines = [];
if (csUtils.isNumber(radius)) {
if (radius) {
const radiusLine = isEmptyArea const radiusLine = isEmptyArea
? `Radius: Oblique not supported` ? `Radius: Oblique not supported`
: `Radius: ${csUtils.roundNumber(radius)} ${radiusUnit}`; : `Radius: ${csUtils.roundNumber(radius)} ${radiusUnit}`;
textLines.push(radiusLine); textLines.push(radiusLine);
} }
if (area) { if (csUtils.isNumber(area)) {
const areaLine = isEmptyArea const areaLine = isEmptyArea
? `Area: Oblique not supported` ? `Area: Oblique not supported`
: `Area: ${csUtils.roundNumber(area)} ${areaUnit}`; : `Area: ${csUtils.roundNumber(area)} ${areaUnit}`;
textLines.push(areaLine); textLines.push(areaLine);
} }
if (mean) { if (csUtils.isNumber(mean)) {
textLines.push(`Mean: ${csUtils.roundNumber(mean)} ${modalityUnit}`); textLines.push(`Mean: ${csUtils.roundNumber(mean)} ${modalityUnit}`);
} }
if (max) { if (csUtils.isNumber(max)) {
textLines.push(`Max: ${csUtils.roundNumber(max)} ${modalityUnit}`); textLines.push(`Max: ${csUtils.roundNumber(max)} ${modalityUnit}`);
} }
if (stdDev) { if (csUtils.isNumber(stdDev)) {
textLines.push(`Std Dev: ${csUtils.roundNumber(stdDev)} ${modalityUnit}`); textLines.push(`Std Dev: ${csUtils.roundNumber(stdDev)} ${modalityUnit}`);
} }
return textLines; return textLines;
} }
export default FixedRadiusCircleROITool;

View File

@ -34,13 +34,30 @@
<el-form-item v-if="CriterionType === 0 && form.ReadingVersionEnum === 1" <el-form-item v-if="CriterionType === 0 && form.ReadingVersionEnum === 1"
:label="$t('trials:readingUnit:readingRules:title:measureTool')"> :label="$t('trials:readingUnit:readingRules:title:measureTool')">
<el-checkbox-group v-model="form.ReadingToolList" :disabled="isConfirm || <el-checkbox-group v-model="form.ReadingToolList" :disabled="isConfirm ||
!hasPermi(['trials:trials-panel:setting:reading-unit:edit']) !hasPermi(['trials:trials-panel:setting:reading-unit:edit'])"
"> @change="handleReadingToolListChange"
>
<el-checkbox v-for="tool in tools" :key="tool.toolName" :label="tool.toolName" name="ReadingToolList"> <el-checkbox v-for="tool in tools" :key="tool.toolName" :label="tool.toolName" name="ReadingToolList">
{{ $t(`${tool.i18nKey}`) }} {{ $t(`${tool.i18nKey}`) }}
</el-checkbox> </el-checkbox>
</el-checkbox-group> </el-checkbox-group>
</el-form-item> </el-form-item>
<!-- 定圆工具半径 -->
<el-form-item v-if="form.ReadingToolList.includes('FixedRadiusCircleROI')"
:label="$t('trials:readingUnit:readingRules:title:circleRadius')"
prop="CircleRadius"
>
<el-input
v-model="form.CircleRadius"
@input="(val) => handleNumberInput(val, 'CircleRadius')"
clearable
:disabled="isConfirm ||
!hasPermi(['trials:trials-panel:setting:reading-unit:edit'])
"
>
<template slot="append">mm</template>
</el-input>
</el-form-item>
<!--分割工具 && (form.ReadingTool === 0 || form.ReadingTool === 1 || form.ReadingTool === 2)--> <!--分割工具 && (form.ReadingTool === 0 || form.ReadingTool === 1 || form.ReadingTool === 2)-->
<el-form-item v-if="CriterionType === 0 && form.ReadingVersionEnum === 1 && form.ReadingTool === 3" <el-form-item v-if="CriterionType === 0 && form.ReadingVersionEnum === 1 && form.ReadingTool === 3"
:label="$t('trials:readingUnit:readingRules:title:segmentTool')"> :label="$t('trials:readingUnit:readingRules:title:segmentTool')">
@ -460,7 +477,8 @@ export default {
ImageUploadEnum: null, ImageUploadEnum: null,
IsImageFilter: false, IsImageFilter: false,
KeyFileListStr: '', KeyFileListStr: '',
KeyFileList: [] KeyFileList: [],
CircleRadius: null
}, },
rules: { rules: {
IsAutoCreate: [ IsAutoCreate: [
@ -613,6 +631,10 @@ export default {
trigger: ['blur', 'change'], trigger: ['blur', 'change'],
}, },
], ],
CircleRadius: [
{ required: true, message: this.$t('common:ruleMessage:specify'), trigger: ['blur', 'change'], },
{ pattern: /^(0|[1-9]\d*)(\.\d{1,2})?$/, message: '请输入正确的数值格式,最多两位小数', trigger: 'blur' }
]
// IsReadingTaskViewInOrder: [ // IsReadingTaskViewInOrder: [
// { required: true, message: this.$t('common:ruleMessage:select'), trigger: ['blur', 'change'] } // { required: true, message: this.$t('common:ruleMessage:select'), trigger: ['blur', 'change'] }
// ] // ]
@ -900,6 +922,37 @@ export default {
}) })
}) })
}, },
handleReadingToolListChange(v) {
if(!v.includes('FixedRadiusCircleROI')) {
this.form.CircleRadius = null
}
},
handleNumberInput(value, field) {
if (!value) {
this.form[field] = ''
return
}
let val = value.toString()
// 1. .
val = val.replace(/[^\d.]/g, '')
// 2. .
val = val.replace(/^\./g, '')
// 3. .
val = val.replace(/\.{2,}/g, '.')
val = val.replace('.', '$#$').replace(/\./g, '').replace('$#$', '.')
// 4.
val = val.replace(/^(\-)*(\d+)\.(\d{2}).*$/, '$1$2.$3')
// 5. 01 1
if (val.length > 1 && val[0] === '0' && val[1] !== '.') {
val = val.replace(/^0+/, '')
if (val === '') val = '0'
}
this.$nextTick(() => {
this.ruleForm[field] = val
});
},
}, },
} }
</script> </script>