Files
his/MD/specs/RECONSTRUCTION_3D_DEEP_DESIGN.md
华佗 f87b9215c1 feat(V31): CSSD消毒供应追溯+影像3D重建(选配模块深度实现)
- 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重建查看器)
- 后端编译通过,前端构建通过
2026-06-07 09:30:00 +08:00

609 lines
24 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 影像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. **权限分级**: 普通医生查看,主治以上审核报告