返回系列

总览:DicomVision 是什么,以及为什么采用服务端渲染

从项目边界、运行形态和核心取舍开始,复盘一个服务端渲染型 DICOM Viewer 的工程形态。

问题引入

DicomVision 的目标不是做一个“能打开 dcm 文件”的小工具。它更像一个围绕 DICOM 数据建立的影像运行时:它要读本地或服务器上的 DICOM 序列,要把二维切片组织成三维体数据,要支持 2D、MPR、3D、4D、测量、Tag 查看、PACS 下载、DICOM SR/GSPS 导出和 QA 分析。这个目标决定了它不能只被理解成一个前端页面,也不能只被理解成一个 Python 脚本。

项目当前更准确的定位是:一个面向学习、验证和演示场景的 DICOM Viewer。它适合影像算法探索、低显存机器上的本地查看、Web Demo、桌面端一体化打包,也适合复盘医学影像工程中的关键问题。它不应该被描述成完整 PACS,更不应该被描述成诊断级商业工作站。PACS 要处理归档、检索、权限、审计、HL7/RIS 流程和长期存储;诊断工作站还需要合规、校准、报告、质控和大量边界病例验证。DicomVision 当前解决的是影像数据如何加载、组织、显示、重建和导出。

背景概念

DICOM Viewer 的复杂度来自数据本身。普通图片通常只有宽高、颜色和压缩格式;DICOM 文件同时包含病人、检查、序列、设备、几何、像素、显示建议和标准对象语义。一个 CT 序列不是一张图片,而是一组有空间位置和方向的切片。MPR 要把这些切片堆成体数据再切面。3D 要把体数据交给渲染器做采样或等值面。测量要把屏幕坐标转换回图像坐标和物理坐标。PACS 下载还涉及 DICOMweb 或 DIMSE 协议。

这就是 DicomVision 采用服务端渲染的核心原因。前端负责工作区、按钮、视口、叠加层和用户交互;后端负责 DICOM 读取、像素解码、NumPy 计算、SciPy 重采样、Pillow 编码、VTK 渲染和标准对象导出。前端把“用户想做什么”发给后端,后端把“影像结果是什么”推回前端。

项目实现

桌面端通过 Electron 包住 Vue 工作区,同时启动打包进应用里的 FastAPI 后端。Web 端则直接访问部署在服务器上的后端。两种形态共用后端影像逻辑,因此 2D、MPR、3D、4D、PACS、QA 的实现不需要维护两套。通信上,项目把低频资源创建放在 HTTP 中,把高频视口操作放在 Socket.IO 中。

典型链路是:

  1. 用户选择 DICOM 文件或文件夹。
  2. 前端调用 /api/v1/dicom/loadFolder,后端扫描目录并注册序列。
  3. 用户打开 Stack、MPR、3D、4D 或 Tag 视图。
  4. 前端调用 /api/v1/view/create 创建后端视口,拿到 viewId。
  5. 前端通过 Socket.IO bind_view 订阅这个视口。
  6. 前端测量 DOM 尺寸后调用 /api/v1/view/setSize。
  7. 后端根据 ViewRecord 渲染图像,通过 image_update 推送二进制图片和元数据。
  8. 之后滚轮、拖拽、窗宽窗位、MPR 十字线、3D 旋转都走 view_operation。

这个模型的关键好处是边界清楚:前端不直接持有完整 DICOM 体数据,后端不直接关心 UI 布局。视口状态落在 ViewRecord,序列状态落在 SeriesRecord,MPR 联动状态落在 ViewGroupRecord。当图像显示错位、MPR 不联动、3D 旋转异常时,可以沿这些对象追踪。

关键函数表

层次主要职责项目中的典型入口
文件扫描从文件或目录识别 DICOM,读取 header,按序列分组series_registry.load_folder
像素读取延迟解码 PixelData,应用 slope/intercept,缓存像素dicom_cache.get
2D 渲染窗宽窗位、仿射变换、叠加层、编码图片viewer_service、viewport_transformer
MPR把序列组织为 volume,按三正交面重采样services/mpr/*
3DNumPy volume 转 VTK,体渲染或表面渲染vtk_volume_renderer、vtk_surface_renderer
通信HTTP 建资源,Socket 推高频图像帧FastAPI、Socket.IO
标准对象导出 SR/GSPS,或识别导入的 SR/GSPSdicom_sr_export_service、dicom_gsps_export_service

自绘图表

用户 浏览器或桌面端
<rect x="270" y="28" width="210" height="125" rx="18" fill="var(--surface)" stroke="var(--line)" />
<text x="375" y="64" text-anchor="middle" fill="currentColor" font-weight="800">显示与交互层</text>
<text x="375" y="94" text-anchor="middle" fill="var(--muted)" font-size="14">Vue 工作区</text>
<text x="375" y="119" text-anchor="middle" fill="var(--muted)" font-size="14">Electron / Web 共用</text>

<rect x="560" y="28" width="230" height="125" rx="18" fill="var(--surface)" stroke="var(--line)" />
<text x="675" y="64" text-anchor="middle" fill="currentColor" font-weight="800">影像运行时</text>
<text x="675" y="94" text-anchor="middle" fill="var(--muted)" font-size="14">FastAPI + Socket.IO</text>
<text x="675" y="119" text-anchor="middle" fill="var(--muted)" font-size="14">pydicom / NumPy / VTK</text>

<rect x="555" y="235" width="235" height="126" rx="18" fill="var(--surface)" stroke="var(--line)" />
<text x="672" y="270" text-anchor="middle" fill="currentColor" font-weight="800">数据来源</text>
<text x="672" y="300" text-anchor="middle" fill="var(--muted)" font-size="14">本地 DICOM 文件</text>
<text x="672" y="325" text-anchor="middle" fill="var(--muted)" font-size="14">PACS / Web 上传 / 示例数据</text>

<rect x="270" y="235" width="210" height="126" rx="18" fill="var(--surface)" stroke="var(--line)" />
<text x="375" y="270" text-anchor="middle" fill="currentColor" font-weight="800">部署与发布</text>
<text x="375" y="300" text-anchor="middle" fill="var(--muted)" font-size="14">GitHub Actions</text>
<text x="375" y="325" text-anchor="middle" fill="var(--muted)" font-size="14">服务器 / 桌面安装包</text>

<line x1="205" y1="90" x2="270" y2="90" stroke="var(--accent)" stroke-width="3" marker-end="url(#arrow-overview)" />
<line x1="480" y1="90" x2="560" y2="90" stroke="var(--accent)" stroke-width="3" marker-end="url(#arrow-overview)" />
<line x1="675" y1="153" x2="675" y2="235" stroke="var(--accent)" stroke-width="3" marker-end="url(#arrow-overview)" />
<line x1="560" y1="300" x2="480" y2="300" stroke="var(--accent)" stroke-width="3" marker-end="url(#arrow-overview)" />
<text x="515" y="74" text-anchor="middle" fill="var(--muted)" font-size="13">HTTP / Socket</text>
<text x="805" y="203" text-anchor="middle" fill="var(--muted)" font-size="13">按需读像素</text>
DicomVision 的核心边界:前端负责操作和呈现,后端负责影像数据、计算和渲染。
DICOM 基础 pydicom 加载 NumPy 体数据 2D / MPR VTK 3D / 4D 性能优化

小结

后面的文章会围绕这个边界展开:先讲 DICOM 文件为什么复杂,再讲项目如何用 pydicom 把文件组织成序列,用 NumPy 把切片变成体数据,用 SciPy 和 VTK 做重采样与三维渲染,最后再讲缓存、清理、并发和低配服务器上的工程取舍。理解了这条主线,DicomVision 就不再是一堆功能按钮,而是一套从医学影像数据到可交互视图的运行系统。