- V31 Flyway: 8张新表(CSSD器械包/追溯记录/灭菌批次/灭菌明细/过期预警+3D重建任务/结果/报告) - CSSD: 全流程追溯(回收→清洗→消毒→包装→灭菌→储存→发放)+扫码+灭菌三要素+过期预警 - 3D重建: Cornerstone.js+VTK.js架构设计+VR/MPR/MIP渲染+传递函数预设+测量工具 - 608行深度技术设计文档(MD/specs/RECONSTRUCTION_3D_DEEP_DESIGN.md) - 2个前端页面(CSSD追溯/3D重建查看器) - 后端编译通过,前端构建通过
609 lines
24 KiB
Markdown
609 lines
24 KiB
Markdown
# 影像3D重建 — 深度技术设计文档
|
||
|
||
> **文档类型**: 深度技术设计
|
||
> **版本**: v1.0
|
||
> **编制日期**: 2026-06-07
|
||
> **技术栈**: Cornerstone.js(DICOM解析) + VTK.js(3D渲染) + Spring Boot(后端处理)
|
||
|
||
---
|
||
|
||
## 一、技术选型分析
|
||
|
||
### 1.1 前端3D渲染方案对比
|
||
|
||
| 方案 | 优点 | 缺点 | 推荐度 |
|
||
|------|------|------|--------|
|
||
| **Cornerstone.js + VTK.js** | 专为医学影像设计,DICOM原生支持,WebGL GPU加速 | 学习曲线较陡 | ⭐⭐⭐⭐⭐ |
|
||
| **Three.js** | 通用3D引擎,社区大 | 无DICOM支持,需自行解析 | ⭐⭐⭐ |
|
||
| **OHIF Viewer** | 完整PACS查看器 | 太重,集成复杂 | ⭐⭐⭐ |
|
||
| **MITK** | 功能全面的医学影像工具包 | C++为主,Web支持弱 | ⭐⭐ |
|
||
|
||
**推荐方案**: Cornerstone.js + VTK.js
|
||
- **Cornerstone.js**: DICOM图像解析、2D查看、MPR重建
|
||
- **VTK.js**: 容积渲染(VR)、等值面提取、3D测量
|
||
|
||
### 1.2 技术架构
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────┐
|
||
│ 前端 (Vue 3 + Vite) │
|
||
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌─────────┐ │
|
||
│ │Cornerstone│ │ VTK.js │ │测量工具栏│ │报告编辑器│ │
|
||
│ │ DICOM解析 │ │ 3D渲染 │ │距离/角度 │ │所见/印象 │ │
|
||
│ │ 2D/MPR │ │VR/MIP │ │体积/面积 │ │结论 │ │
|
||
│ └─────┬────┘ └─────┬────┘ └────┬─────┘ └────┬────┘ │
|
||
│ └─────────────┴────────────┴──────────────┘ │
|
||
│ ↓ HTTP/REST API │
|
||
└─────────────────────────────────────────────────────────┘
|
||
↓
|
||
┌─────────────────────────────────────────────────────────┐
|
||
│ 后端 (Spring Boot 4.0.6) │
|
||
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌─────────┐ │
|
||
│ │DICOM解析 │ │任务调度 │ │结果存储 │ │PACS对接 │ │
|
||
│ │dcm4che │ │异步处理 │ │MinIO/NFS │ │WADO-RS │ │
|
||
│ └──────────┘ └──────────┘ └──────────┘ └─────────┘ │
|
||
└─────────────────────────────────────────────────────────┘
|
||
↓
|
||
┌─────────────────────────────────────────────────────────┐
|
||
│ 存储层 │
|
||
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
|
||
│ │PostgreSQL │ │文件存储 │ │PACS系统 │ │
|
||
│ │ 元数据 │ │MinIO/NFS │ │DICOM节点 │ │
|
||
│ └──────────┘ └──────────┘ └──────────┘ │
|
||
└─────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
---
|
||
|
||
## 二、后端深度设计
|
||
|
||
### 2.1 DICOM解析服务
|
||
|
||
#### 2.1.1 dcm4che集成
|
||
|
||
```java
|
||
// DICOM文件解析流程
|
||
public class DicomParserService {
|
||
|
||
// 解析DICOM文件元数据
|
||
public DicomMetadata parseDicomFile(InputStream dicomStream) {
|
||
// 1. 使用dcm4che读取DICOM文件
|
||
Dataset ds = DicomInputStream.read(dicomStream);
|
||
|
||
// 2. 提取关键元数据
|
||
DicomMetadata metadata = new DicomMetadata();
|
||
metadata.setPatientName(ds.getString(Tag.PatientName)); // 患者姓名
|
||
metadata.setPatientId(ds.getString(Tag.PatientID)); // 患者ID
|
||
metadata.setStudyInstanceUID(ds.getString(Tag.StudyInstanceUID)); // 检查UID
|
||
metadata.setSeriesInstanceUID(ds.getString(Tag.SeriesInstanceUID)); // 序列UID
|
||
metadata.setSopInstanceUID(ds.getString(Tag.SOPInstanceUID)); // 实例UID
|
||
metadata.setModality(ds.getString(Tag.Modality)); // CT/MRI/US
|
||
metadata.setStudyDate(ds.getString(Tag.StudyDate)); // 检查日期
|
||
metadata.setBodyPartExamined(ds.getString(Tag.BodyPartExamined)); // 检查部位
|
||
metadata.setImageOrientationPatient(ds.getStrings(Tag.ImageOrientationPatient)); // 图像方向
|
||
metadata.setImagePositionPatient(ds.getStrings(Tag.ImagePositionPatient)); // 图像位置
|
||
metadata.setPixelSpacing(ds.getStrings(Tag.PixelSpacing)); // 像素间距
|
||
metadata.setSliceThickness(ds.getString(Tag.SliceThickness)); // 层厚
|
||
metadata.setRows(ds.getInt(Tag.Rows)); // 行数
|
||
metadata.setColumns(ds.getInt(Tag.Columns)); // 列数
|
||
metadata.setBitsAllocated(ds.getInt(Tag.BitsAllocated)); // 位深
|
||
metadata.setWindowCenter(ds.getString(Tag.WindowCenter)); // 窗位
|
||
metadata.setWindowWidth(ds.getString(Tag.WindowWidth)); // 窗宽
|
||
|
||
// 3. 提取像素数据
|
||
byte[] pixelData = ds.getBytes(Tag.PixelData);
|
||
metadata.setPixelData(pixelData);
|
||
|
||
return metadata;
|
||
}
|
||
|
||
// 批量解析同一Series的所有DICOM文件
|
||
public List<DicomMetadata> parseSeries(List<InputStream> dicomFiles) {
|
||
List<DicomMetadata> series = new ArrayList<>();
|
||
for (InputStream file : dicomFiles) {
|
||
series.add(parseDicomFile(file));
|
||
}
|
||
// 按ImagePositionPatient排序(确保层序正确)
|
||
series.sort(Comparator.comparing(m ->
|
||
Double.parseDouble(m.getImagePositionPatient()[2])));
|
||
return series;
|
||
}
|
||
}
|
||
```
|
||
|
||
#### 2.1.2 3D重建处理服务
|
||
|
||
```java
|
||
// 3D重建处理服务
|
||
@Service
|
||
public class ReconstructionProcessingService {
|
||
|
||
@Async("reconstructionExecutor")
|
||
public void processReconstruction(Long taskId) {
|
||
// 1. 获取任务信息
|
||
ReconstructionTask task = taskMapper.selectById(taskId);
|
||
task.setTaskStatus("PROCESSING");
|
||
taskMapper.updateById(task);
|
||
|
||
try {
|
||
// 2. 加载DICOM序列数据
|
||
List<DicomMetadata> series = loadDicomSeries(task.getApplyId());
|
||
|
||
// 3. 预处理: 去噪 + 窗宽窗位调整
|
||
float[][][] volumeData = preprocessVolume(series);
|
||
|
||
// 4. 根据重建类型执行
|
||
switch (task.getReconstructionType()) {
|
||
case "VR": // 容积渲染
|
||
processVolumeRendering(task, volumeData);
|
||
break;
|
||
case "MPR": // 多平面重建
|
||
processMPR(task, volumeData);
|
||
break;
|
||
case "MIP": // 最大密度投影
|
||
processMIP(task, volumeData);
|
||
break;
|
||
case "VR+MPR": // 混合重建
|
||
processVolumeRendering(task, volumeData);
|
||
processMPR(task, volumeData);
|
||
break;
|
||
}
|
||
|
||
// 5. 生成结果截图
|
||
saveResultImages(task);
|
||
|
||
// 6. 更新任务状态
|
||
task.setTaskStatus("COMPLETED");
|
||
task.setCompleteTime(new Date());
|
||
task.setResultPath("/reconstruction/" + taskId + "/");
|
||
taskMapper.updateById(task);
|
||
|
||
} catch (Exception e) {
|
||
task.setTaskStatus("FAILED");
|
||
taskMapper.updateById(task);
|
||
log.error("3D重建任务失败: {}", taskId, e);
|
||
}
|
||
}
|
||
|
||
// 容积渲染(Volume Rendering)
|
||
private void processVolumeRendering(ReconstructionTask task, float[][][] volume) {
|
||
// 1. 建立体数据(Volume Data)
|
||
int dimX = volume.length;
|
||
int dimY = volume[0].length;
|
||
int dimZ = volume[0][0].length;
|
||
|
||
// 2. 传递函数(Transfer Function)设置
|
||
// CT值 → 颜色+透明度
|
||
// 骨骼: 高CT值(>300), 不透明, 白色
|
||
// 软组织: 中CT值(30-300), 半透明, 粉色
|
||
// 空气: 低CT值(<-500), 全透明
|
||
TransferFunction tf = new TransferFunction();
|
||
tf.addMapping(-1000, 0.0f, 0.0f, 0.0f, 0.0f); // 空气: 全透明
|
||
tf.addMapping(-500, 0.0f, 0.0f, 0.0f, 0.0f); // 肺: 全透明
|
||
tf.addMapping(30, 0.8f, 0.2f, 0.2f, 0.4f); // 软组织: 半透明粉红
|
||
tf.addMapping(300, 0.9f, 0.9f, 0.8f, 0.9f); // 骨骼: 不透明白
|
||
tf.addMapping(3000, 1.0f, 1.0f, 1.0f, 1.0f); // 金属: 全不透明
|
||
|
||
// 3. 光线投射(Ray Casting)算法
|
||
// 从每个像素发射光线,沿光线采样,累积颜色和透明度
|
||
// C(积累) = Σ(Ci * αi * Π(1-αj))
|
||
|
||
// 4. 保存渲染结果为PNG
|
||
saveVolumeRenderingResult(task, tf);
|
||
}
|
||
|
||
// 多平面重建(Multi-Planar Reconstruction)
|
||
private void processMPR(ReconstructionTask task, float[][][] volume) {
|
||
// 1. 矢状面(Sagittal)重建: 沿X轴切割
|
||
float[][] sagittalPlane = extractSagittalPlane(volume, volume.length / 2);
|
||
|
||
// 2. 冠状面(Coronal)重建: 沿Y轴切割
|
||
float[][] coronalPlane = extractCoronalPlane(volume, volume[0].length / 2);
|
||
|
||
// 3. 轴位(Axial)重建: 沿Z轴切割(原始方向)
|
||
float[][] axialPlane = extractAxialPlane(volume, volume[0][0].length / 2);
|
||
|
||
// 4. 交互式切割: 支持任意角度平面
|
||
// 通过旋转矩阵变换采样坐标
|
||
|
||
saveMPRResult(task, sagittalPlane, coronalPlane, axialPlane);
|
||
}
|
||
|
||
// 最大密度投影(Maximum Intensity Projection)
|
||
private void processMIP(ReconstructionTask task, float[][][] volume) {
|
||
int dimX = volume.length;
|
||
int dimY = volume[0].length;
|
||
int dimZ = volume[0][0].length;
|
||
|
||
float[][] mipImage = new float[dimX][dimY];
|
||
for (int x = 0; x < dimX; x++) {
|
||
for (int y = 0; y < dimY; y++) {
|
||
float maxVal = Float.MIN_VALUE;
|
||
for (int z = 0; z < dimZ; z++) {
|
||
maxVal = Math.max(maxVal, volume[x][y][z]);
|
||
}
|
||
mipImage[x][y] = maxVal;
|
||
}
|
||
}
|
||
saveMIPResult(task, mipImage);
|
||
}
|
||
}
|
||
```
|
||
|
||
### 2.2 DICOM存储方案
|
||
|
||
```
|
||
存储架构:
|
||
├── PostgreSQL → 元数据(患者/检查/序列/任务/报告)
|
||
├── MinIO/NFS → DICOM原始文件 + 重建结果(截图/体数据)
|
||
└── PACS系统 → 通过WADO-RS/DICOMweb获取DICOM图像
|
||
|
||
获取DICOM数据流程:
|
||
1. 任务创建时 → 从PACS获取StudyUID对应的DICOM文件
|
||
2. 使用WADO-RS协议: GET /dicomweb/studies/{studyUid}/series/{seriesUid}
|
||
3. 下载到本地临时目录 → 解析 → 处理 → 清理临时文件
|
||
4. 重建结果保存到MinIO → 元数据保存到PostgreSQL
|
||
```
|
||
|
||
### 2.3 接口设计(完整版)
|
||
|
||
| API | 方法 | 说明 | 参数 |
|
||
|-----|------|------|------|
|
||
| /reconstruction/task/page | GET | 任务列表(分页+筛选) | patientName,modality,status,pageNo,pageSize |
|
||
| /reconstruction/task/add | POST | 新建任务(从PACS拉取) | patientId,studyUid,modality,bodyPart,reconstructionType |
|
||
| /reconstruction/task/{id} | GET | 任务详情 | - |
|
||
| /reconstruction/task/cancel/{id} | PUT | 取消任务 | - |
|
||
| /reconstruction/result/list/{taskId} | GET | 重建结果列表 | - |
|
||
| /reconstruction/result/{id}/image | GET | 获取结果截图 | width,height,window |
|
||
| /reconstruction/result/{id}/volume | GET | 获取体数据(JSON格式) | resolution |
|
||
| /reconstruction/report/add | POST | 新建报告 | taskId,findings,impression,conclusion |
|
||
| /reconstruction/report/{id} | GET | 报告详情 | - |
|
||
| /reconstruction/report/verify/{id} | PUT | 审核报告 | verifyDoctor |
|
||
| /reconstruction/stats | GET | 统计概览 | startDate,endDate |
|
||
|
||
---
|
||
|
||
## 三、前端深度设计
|
||
|
||
### 3.1 技术栈
|
||
|
||
```json
|
||
{
|
||
"cornerstone-core": "^2.6.1", // DICOM图像解析与2D显示
|
||
"cornerstone-wado-image-loader": "^4.13.2", // WADO加载器
|
||
"cornerstone-tools": "^7.1.0", // 交互工具(测量/标注)
|
||
"vtk.js": "^29.0.0", // 3D渲染引擎(WebGL)
|
||
"dicom-parser": "^1.8.21" // DICOM文件解析
|
||
}
|
||
```
|
||
|
||
### 3.2 组件架构
|
||
|
||
```
|
||
src/views/reconstruction/
|
||
├── index.vue # 主页面(任务列表+工作台)
|
||
├── api.js # API接口
|
||
├── components/
|
||
│ ├── DicomViewer.vue # 2D DICOM查看器(Cornerstone)
|
||
│ ├── MprViewer.vue # MPR多平面重建查看器
|
||
│ ├── VrViewer.vue # VR容积渲染查看器(VTK.js)
|
||
│ ├── MipViewer.vue # MIP最大密度投影查看器
|
||
│ ├── MeasurementToolbar.vue # 测量工具栏
|
||
│ ├── ReconstructionTaskList.vue # 任务列表
|
||
│ ├── ReconstructionReport.vue # 报告编辑器
|
||
│ └── ReconstructionStats.vue # 统计面板
|
||
```
|
||
|
||
### 3.3 核心组件实现
|
||
|
||
#### DicomViewer.vue (2D DICOM查看器)
|
||
|
||
```vue
|
||
<template>
|
||
<div class="dicom-viewer">
|
||
<div ref="viewerContainer" class="viewer-container" />
|
||
<div class="toolbar">
|
||
<el-button-group>
|
||
<el-button @click="setTool('windowing')">窗宽窗位</el-button>
|
||
<el-button @click="setTool('pan')">平移</el-button>
|
||
<el-button @click="setTool('zoom')">缩放</el-button>
|
||
<el-button @click="setTool('length')">距离测量</el-button>
|
||
<el-button @click="setTool('angle')">角度测量</el-button>
|
||
<el-button @click="setTool('area')">面积测量</el-button>
|
||
<el-button @click="setTool('ellipse')">椭圆面积</el-button>
|
||
<el-button @click="setTool('probe')">CT值探针</el-button>
|
||
</el-button-group>
|
||
</div>
|
||
<div class="info-overlay">
|
||
<div>Patient: {{ patientInfo.name }}</div>
|
||
<div>Window: {{ windowCenter }}/{{ windowWidth }}</div>
|
||
<div>Zoom: {{ zoomLevel }}</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { onMounted, onUnmounted, ref } from 'vue'
|
||
import * as cornerstone from 'cornerstone-core'
|
||
import * as cornerstoneTools from 'cornerstone-tools'
|
||
import * as cornerstoneWADOImageLoader from 'cornerstone-wado-image-loader'
|
||
import dicomParser from 'dicom-parser'
|
||
|
||
// 初始化Cornerstone
|
||
cornerstoneWADOImageLoader.external.dicomParser = dicomParser
|
||
cornerstoneWADOImageLoader.external.cornerstone = cornerstone
|
||
|
||
const viewerContainer = ref(null)
|
||
let enabledElement = null
|
||
|
||
onMounted(() => {
|
||
// 启用Cornerstone
|
||
enabledElement = cornerstone.getEnabledElement(viewerContainer.value)
|
||
cornerstone.enable(viewerContainer.value)
|
||
|
||
// 加载DICOM图像
|
||
loadDicomImage()
|
||
})
|
||
|
||
const loadDicomImage = async () => {
|
||
const imageId = `wado:${wadoRoot}?requestType=WADO&studyUID=${studyUid}&seriesUID=${seriesUid}&objectUID=${sopUid}`
|
||
const image = await cornerstone.loadAndCacheImage(imageId)
|
||
cornerstone.displayImage(enabledElement, image)
|
||
|
||
// 启用工具
|
||
cornerstoneTools.mouseInput.enable(enabledElement)
|
||
cornerstoneTools.mouseWheelInput.enable(enabledElement)
|
||
cornerstoneTools.wwwc.activate(enabledElement, 1) // 左键: 窗宽窗位
|
||
cornerstoneTools.pan.activate(enabledElement, 2) // 右键: 平移
|
||
cornerstoneTools.zoom.activate(enabledElement, 4) // 中键: 缩放
|
||
cornerstoneTools.length.enable(enabledElement) // 距离测量
|
||
cornerstoneTools.angle.enable(enabledElement) // 角度测量
|
||
cornerstoneTools.ellipseRoi.enable(enabledElement) // 椭圆面积
|
||
cornerstoneTools.probe.enable(enabledElement) // CT值探针
|
||
}
|
||
|
||
const setTool = (toolName) => {
|
||
// 切换工具并高亮按钮
|
||
}
|
||
</script>
|
||
```
|
||
|
||
#### VrViewer.vue (VR容积渲染)
|
||
|
||
```vue
|
||
<template>
|
||
<div class="vr-viewer">
|
||
<div ref="vtkContainer" class="vtk-container" />
|
||
<div class="transfer-function-panel">
|
||
<div class="preset-buttons">
|
||
<el-button @click="applyPreset('bone')">骨骼</el-button>
|
||
<el-button @click="applyPreset('softTissue')">软组织</el-button>
|
||
<el-button @click="applyPreset('lung')">肺部</el-button>
|
||
<el-button @click="applyPreset('angio')">血管</el-button>
|
||
<el-button @click="applyPreset('skin')">皮肤</el-button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { onMounted, onUnmounted } from 'vue'
|
||
import vtkProxyManager from 'vtk.js/Sources/Proxy/ProxyManager'
|
||
|
||
let proxyManager = null
|
||
|
||
onMounted(() => {
|
||
initVtk()
|
||
})
|
||
|
||
const initVtk = () => {
|
||
// 创建VTK ProxyManager
|
||
proxyManager = vtkProxyManager.newInstance({ container: vtkContainer.value })
|
||
|
||
// 加载体数据
|
||
const source = proxyManager.createProxy('Sources', 'TrivialProducer')
|
||
source.setInputData(volumeData)
|
||
|
||
// 创建VR表示
|
||
const representation = proxyManager.createProxy('Representations', 'Volume')
|
||
representation.setInput(source)
|
||
|
||
// 设置传递函数
|
||
const actor = representation.getActor()
|
||
const property = actor.getProperty()
|
||
|
||
// 设置预设
|
||
applyPreset('bone')
|
||
}
|
||
|
||
const applyPreset = (preset) => {
|
||
const presets = {
|
||
bone: [
|
||
{ value: -1000, opacity: 0, color: [0, 0, 0] },
|
||
{ value: -300, opacity: 0, color: [0, 0, 0] },
|
||
{ value: 200, opacity: 0.2, color: [0.8, 0.4, 0.3] },
|
||
{ value: 500, opacity: 0.8, color: [1, 1, 1] },
|
||
{ value: 3000, opacity: 1, color: [1, 1, 1] }
|
||
],
|
||
softTissue: [
|
||
{ value: -1000, opacity: 0, color: [0, 0, 0] },
|
||
{ value: -500, opacity: 0, color: [0, 0, 0] },
|
||
{ value: 30, opacity: 0.3, color: [0.8, 0.3, 0.3] },
|
||
{ value: 100, opacity: 0.5, color: [0.9, 0.5, 0.5] },
|
||
{ value: 300, opacity: 0.8, color: [1, 0.8, 0.7] },
|
||
{ value: 3000, opacity: 1, color: [1, 1, 1] }
|
||
],
|
||
lung: [
|
||
{ value: -1000, opacity: 0.3, color: [0.2, 0.2, 0.3] },
|
||
{ value: -700, opacity: 0.1, color: [0.4, 0.4, 0.5] },
|
||
{ value: -300, opacity: 0.3, color: [0.6, 0.6, 0.7] },
|
||
{ value: 0, opacity: 0.8, color: [0.8, 0.8, 0.8] },
|
||
{ value: 3000, opacity: 1, color: [1, 1, 1] }
|
||
],
|
||
angio: [
|
||
{ value: -1000, opacity: 0, color: [0, 0, 0] },
|
||
{ value: 100, opacity: 0, color: [0, 0, 0] },
|
||
{ value: 200, opacity: 0.5, color: [1, 0, 0] },
|
||
{ value: 500, opacity: 0.9, color: [1, 0.2, 0.2] },
|
||
{ value: 3000, opacity: 1, color: [1, 0.5, 0.5] }
|
||
],
|
||
skin: [
|
||
{ value: -1000, opacity: 0, color: [0, 0, 0] },
|
||
{ value: -300, opacity: 0, color: [0, 0, 0] },
|
||
{ value: -100, opacity: 0.2, color: [0.9, 0.7, 0.6] },
|
||
{ value: 50, opacity: 0.5, color: [0.95, 0.8, 0.7] },
|
||
{ value: 200, opacity: 0.8, color: [1, 0.9, 0.85] },
|
||
{ value: 3000, opacity: 1, color: [1, 1, 1] }
|
||
]
|
||
}
|
||
|
||
// 应用传递函数到VTK actor
|
||
const preset = presets[preset] || presets.bone
|
||
applyTransferFunction(preset)
|
||
}
|
||
</script>
|
||
```
|
||
|
||
#### MeasurementToolbar.vue (测量工具)
|
||
|
||
```vue
|
||
<template>
|
||
<div class="measurement-toolbar">
|
||
<el-button-group>
|
||
<el-tooltip content="距离测量(Ctrl+D)">
|
||
<el-button :type="activeTool==='length'?'primary':''" @click="activateTool('length')">
|
||
<el-icon><Ruler /></el-icon> 距离
|
||
</el-button>
|
||
</el-tooltip>
|
||
<el-tooltip content="角度测量(Ctrl+A)">
|
||
<el-button :type="activeTool==='angle'?'primary':''" @click="activateTool('angle')">
|
||
<el-icon><ScaleToOriginal /></el-icon> 角度
|
||
</el-button>
|
||
</el-tooltip>
|
||
<el-tooltip content="面积测量(Ctrl+M)">
|
||
<el-button :type="activeTool==='area'?'primary':''" @click="activateTool('area')">
|
||
<el-icon><Grid /></el-icon> 面积
|
||
</el-button>
|
||
</el-tooltip>
|
||
<el-tooltip content="CT值探针(Ctrl+P)">
|
||
<el-button :type="activeTool==='probe'?'primary':''" @click="activateTool('probe')">
|
||
<el-icon><Position /></el-icon> CT值
|
||
</el-button>
|
||
</el-tooltip>
|
||
</el-button-group>
|
||
|
||
<!-- 测量结果列表 -->
|
||
<div class="measurement-results" v-if="measurements.length">
|
||
<el-table :data="measurements" size="small" border>
|
||
<el-table-column prop="type" label="类型" width="80"/>
|
||
<el-table-column prop="value" label="测量值" width="120"/>
|
||
<el-table-column prop="unit" label="单位" width="60"/>
|
||
<el-table-column label="操作" width="60">
|
||
<template #default="{row}">
|
||
<el-button type="danger" link size="small" @click="removeMeasurement(row)">删除</el-button>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref } from 'vue'
|
||
import * as cornerstoneTools from 'cornerstone-tools'
|
||
|
||
const activeTool = ref('')
|
||
const measurements = ref([])
|
||
|
||
const activateTool = (toolName) => {
|
||
activeTool.value = toolName
|
||
|
||
// 禁用所有工具
|
||
cornerstoneTools.length.deactivate(enabledElement, 1)
|
||
cornerstoneTools.angle.deactivate(enabledElement, 1)
|
||
cornerstoneTools.ellipseRoi.deactivate(enabledElement, 1)
|
||
cornerstoneTools.probe.deactivate(enabledElement, 1)
|
||
|
||
// 激活选中工具
|
||
switch(toolName) {
|
||
case 'length':
|
||
cornerstoneTools.length.activate(enabledElement, 1)
|
||
break
|
||
case 'angle':
|
||
cornerstoneTools.angle.activate(enabledElement, 1)
|
||
break
|
||
case 'area':
|
||
cornerstoneTools.ellipseRoi.activate(enabledElement, 1)
|
||
break
|
||
case 'probe':
|
||
cornerstoneTools.probe.activate(enabledElement, 1)
|
||
break
|
||
}
|
||
}
|
||
</script>
|
||
```
|
||
|
||
---
|
||
|
||
## 四、数据库设计(补充)
|
||
|
||
### 4.1 补充字段
|
||
|
||
```sql
|
||
-- 在reconstruction_task表补充字段
|
||
ALTER TABLE reconstruction_task ADD COLUMN slice_count INT; -- 层数
|
||
ALTER TABLE reconstruction_task ADD COLUMN pixel_spacing_x DECIMAL(6,3); -- X像素间距
|
||
ALTER TABLE reconstruction_task ADD COLUMN pixel_spacing_y DECIMAL(6,3); -- Y像素间距
|
||
ALTER TABLE reconstruction_task ADD COLUMN table_position VARCHAR(50); -- 床位位置
|
||
ALTER TABLE reconstruction_task ADD COLUMN kvp INT; -- 管电压
|
||
ALTER TABLE reconstruction_task ADD COLUMN mas DECIMAL(8,2); -- 管电流时间积
|
||
|
||
-- 在reconstruction_result表补充字段
|
||
ALTER TABLE reconstruction_result ADD COLUMN rendering_time_ms INT; -- 渲染耗时
|
||
ALTER TABLE reconstruction_result ADD COLUMN file_size_bytes BIGINT; -- 文件大小
|
||
ALTER TABLE reconstruction_result ADD COLUMN thumbnail_path VARCHAR(500); -- 缩略图
|
||
```
|
||
|
||
---
|
||
|
||
## 五、部署架构
|
||
|
||
```
|
||
前端构建: npm run build → dist/ → Nginx
|
||
后端部署: Spring Boot JAR → Docker / 直接运行
|
||
存储: MinIO(对象存储) / NFS(文件系统)
|
||
PACS对接: WADO-RS / DICOM C-STORE
|
||
GPU加速: 前端WebGL(VTK.js自带) / 后端可选CUDA加速(处理大型数据集)
|
||
```
|
||
|
||
---
|
||
|
||
## 六、性能优化策略
|
||
|
||
### 6.1 前端优化
|
||
- **LOD(Level of Detail)**: 根据缩放级别加载不同分辨率
|
||
- **瓦片加载**: 大图像分块加载,减少内存占用
|
||
- **Web Worker**: DICOM解析和预处理在Worker线程执行
|
||
- **缓存策略**: Cornerstone缓存最近查看的图像
|
||
|
||
### 6.2 后端优化
|
||
- **异步处理**: 3D重建任务异步执行,不阻塞请求
|
||
- **批量解析**: 一次IO读取整个Series的DICOM文件
|
||
- **结果缓存**: 重建结果缓存到Redis/文件系统
|
||
- **并行处理**: 多个重建任务并行执行
|
||
|
||
### 6.3 存储优化
|
||
- **压缩存储**: 体数据使用LZ4压缩
|
||
- **增量保存**: 只保存变化部分
|
||
- **分层存储**: 热数据SSD,冷数据HDD
|
||
|
||
---
|
||
|
||
## 七、安全设计
|
||
|
||
1. **访问控制**: 只有影像科医生可以发起重建任务
|
||
2. **数据脱敏**: 患者敏感信息在非工作场景脱敏显示
|
||
3. **操作审计**: 所有重建操作记录审计日志
|
||
4. **数据加密**: DICOM文件传输使用HTTPS/TLS
|
||
5. **权限分级**: 普通医生查看,主治以上审核报告
|