From 266698cc799dcc2ed00bd7eeb6275a196088dd90 Mon Sep 17 00:00:00 2001 From: caiyiling <1321909229@qq.com> Date: Tue, 14 Apr 2026 21:19:00 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=9A=E5=9C=86=E5=B7=A5=E5=85=B7=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=E5=8F=8A=E5=8A=9F=E8=83=BD=E5=AE=9E=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 4 +- .../reading/dicoms3D/components/ReadPage.vue | 7 ++- ...nColorMapTransferFunctionForVolumeActor.js | 25 +++++++- .../reading/dicoms3D/components/toolConfig.js | 9 +++ .../tools/FixedRadiusCircleROITool.js | 43 ++++++++++---- .../reading-unit/components/ReadingRules.vue | 59 ++++++++++++++++++- 6 files changed, 125 insertions(+), 22 deletions(-) diff --git a/package.json b/package.json index 8641f081..79ed4a88 100644 --- a/package.json +++ b/package.json @@ -16,10 +16,10 @@ "dependencies": { "@aws-sdk/client-s3": "3.726.1", "@cornerstonejs/adapters": "^4.19.2", - "@cornerstonejs/polymorphic-segmentation": "4.19.2", "@cornerstonejs/calculate-suv": "^1.1.0", "@cornerstonejs/core": "^4.19.2", "@cornerstonejs/dicom-image-loader": "^4.19.2", + "@cornerstonejs/polymorphic-segmentation": "^4.19.2", "@cornerstonejs/tools": "^4.19.2", "@fingerprintjs/fingerprintjs": "^4.6.2", "@icr/polyseg-wasm": "^0.4.0", @@ -125,4 +125,4 @@ "not dead", "not op_mini all" ] -} \ No newline at end of file +} diff --git a/src/views/trials/trials-panel/reading/dicoms3D/components/ReadPage.vue b/src/views/trials/trials-panel/reading/dicoms3D/components/ReadPage.vue index 1bc59f41..b40015b1 100644 --- a/src/views/trials/trials-panel/reading/dicoms3D/components/ReadPage.vue +++ b/src/views/trials/trials-panel/reading/dicoms3D/components/ReadPage.vue @@ -1605,9 +1605,10 @@ export default { toolGroup.addTool(EllipticalROITool.toolName, { getTextLines: this.getEllipticalROIToolTextLines }) - toolGroup.addTool(FixedRadiusCircleROITool.toolName), { - getTextLines: this.getCircleROIToolTextLines - } + toolGroup.addTool(FixedRadiusCircleROITool.toolName, { + radius: Number.isFinite(this.taskInfo.CircleRadius) ? this.taskInfo.CircleRadius : 1, + getTextLines: this.getCircleROIToolTextLines + }) toolGroup.addTool(AngleTool.toolName, { getTextLines: this.getAngleToolTextLines }) diff --git a/src/views/trials/trials-panel/reading/dicoms3D/components/helpers/setNmFusionColorMapTransferFunctionForVolumeActor.js b/src/views/trials/trials-panel/reading/dicoms3D/components/helpers/setNmFusionColorMapTransferFunctionForVolumeActor.js index 757c3ee0..e7fb2773 100644 --- a/src/views/trials/trials-panel/reading/dicoms3D/components/helpers/setNmFusionColorMapTransferFunctionForVolumeActor.js +++ b/src/views/trials/trials-panel/reading/dicoms3D/components/helpers/setNmFusionColorMapTransferFunctionForVolumeActor.js @@ -1,5 +1,6 @@ import vtkColorTransferFunction from "@kitware/vtk.js/Rendering/Core/ColorTransferFunction"; 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"; const { getColormap } = utilities.colormap; @@ -14,7 +15,7 @@ function getWindowCenterFromVolumeId(volumeId) { ? voiLutModule.windowCenter[0] : voiLutModule?.windowCenter; const center = Number(rawCenter); - return Number.isFinite(center) ? center : null; + return center; } export default function setPetColorMapTransferFunctionForVolumeActor({ @@ -33,7 +34,25 @@ export default function setPetColorMapTransferFunctionForVolumeActor({ const center = getWindowCenterFromVolumeId(volumeId); 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); -} + //低信号更明显,系数可以更小 + 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); +} diff --git a/src/views/trials/trials-panel/reading/dicoms3D/components/toolConfig.js b/src/views/trials/trials-panel/reading/dicoms3D/components/toolConfig.js index 9531a991..f93549c7 100644 --- a/src/views/trials/trials-panel/reading/dicoms3D/components/toolConfig.js +++ b/src/views/trials/trials-panel/reading/dicoms3D/components/toolConfig.js @@ -311,6 +311,15 @@ const config = { 'isDisabled': false, 'disabledReason': '' }, + { + 'name': '定圆工具', + 'icon': 'oval', + 'toolName': 'FixedRadiusCircleROI', + 'props': ['radius', 'area', 'mean', 'max', 'stdDev'], + 'i18nKey': 'trials:reading:button:fixedCircle', + 'isDisabled': false, + 'disabledReason': '' + } ], 'customizeStandardsNoneDicom': [ { diff --git a/src/views/trials/trials-panel/reading/dicoms3D/components/tools/FixedRadiusCircleROITool.js b/src/views/trials/trials-panel/reading/dicoms3D/components/tools/FixedRadiusCircleROITool.js index da073e7f..2aacc73d 100644 --- a/src/views/trials/trials-panel/reading/dicoms3D/components/tools/FixedRadiusCircleROITool.js +++ b/src/views/trials/trials-panel/reading/dicoms3D/components/tools/FixedRadiusCircleROITool.js @@ -28,10 +28,12 @@ class FixedRadiusCircleROITool extends cornerstoneTools.CircleROITool { preventHandleOutsideImage: false, storePointData: false, centerPointRadius: 0, + calculateStats: true, radius: 10, // Default radius in mm radiusUnit: 'mm', statsCalculator: BasicStatsCalculator, getTextLines: defaultGetTextLines, + simplified: true, }, } ) { @@ -67,9 +69,12 @@ class FixedRadiusCircleROITool extends cornerstoneTools.CircleROITool { ); const FrameOfReferenceUID = viewport.getFrameOfReferenceUID(); + const targetId = this.getTargetId(viewport); // 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. // We want a point 'radius' distance away from center. // We can use viewUp or any vector in the plane. @@ -106,7 +111,20 @@ class FixedRadiusCircleROITool extends cornerstoneTools.CircleROITool { points: [[...worldPos], [...endPos]], 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) { - const cachedVolumeStats = data.cachedStats[targetId]; + const cachedVolumeStats = data?.cachedStats?.[targetId]; + if (!cachedVolumeStats) { + return []; + } const { radius, radiusUnit, @@ -195,32 +215,33 @@ function defaultGetTextLines(data, targetId) { modalityUnit, } = cachedVolumeStats; const textLines = []; - - if (radius) { + if (csUtils.isNumber(radius)) { const radiusLine = isEmptyArea ? `Radius: Oblique not supported` : `Radius: ${csUtils.roundNumber(radius)} ${radiusUnit}`; textLines.push(radiusLine); } - if (area) { + if (csUtils.isNumber(area)) { const areaLine = isEmptyArea ? `Area: Oblique not supported` : `Area: ${csUtils.roundNumber(area)} ${areaUnit}`; textLines.push(areaLine); } - if (mean) { + if (csUtils.isNumber(mean)) { textLines.push(`Mean: ${csUtils.roundNumber(mean)} ${modalityUnit}`); } - if (max) { + if (csUtils.isNumber(max)) { textLines.push(`Max: ${csUtils.roundNumber(max)} ${modalityUnit}`); } - if (stdDev) { + if (csUtils.isNumber(stdDev)) { textLines.push(`Std Dev: ${csUtils.roundNumber(stdDev)} ${modalityUnit}`); } return textLines; -} \ No newline at end of file +} + +export default FixedRadiusCircleROITool; diff --git a/src/views/trials/trials-panel/setting/reading-unit/components/ReadingRules.vue b/src/views/trials/trials-panel/setting/reading-unit/components/ReadingRules.vue index 2ca38593..0e76f5c2 100644 --- a/src/views/trials/trials-panel/setting/reading-unit/components/ReadingRules.vue +++ b/src/views/trials/trials-panel/setting/reading-unit/components/ReadingRules.vue @@ -34,13 +34,30 @@ + !hasPermi(['trials:trials-panel:setting:reading-unit:edit'])" + @change="handleReadingToolListChange" + > {{ $t(`${tool.i18nKey}`) }} + + + + + + @@ -460,7 +477,8 @@ export default { ImageUploadEnum: null, IsImageFilter: false, KeyFileListStr: '', - KeyFileList: [] + KeyFileList: [], + CircleRadius: null }, rules: { IsAutoCreate: [ @@ -613,6 +631,10 @@ export default { 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: [ // { 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 + }); + }, }, }