# 影像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 parseSeries(List dicomFiles) { List 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 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 ``` #### VrViewer.vue (VR容积渲染) ```vue ``` #### MeasurementToolbar.vue (测量工具) ```vue ``` --- ## 四、数据库设计(补充) ### 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. **权限分级**: 普通医生查看,主治以上审核报告