
🏥【DICOM Viewer】
🧭 一、顶部工具栏
🔧 1. 布局工具组(Layout)
html
<button @click="setLayout('1x1')">1X1</button>
<button @click="setLayout('1x2')">1X2</button>
<button @click="setLayout('2x2')">2X2</button>
<button @click="toggleMPR">MPR</button>
🎯 功能:
- 切换视口布局:支持 1x1(单图)、1x2(左右)、2x2(四宫格)
- MPR 按钮:伪功能,弹窗提示"需专业服务器支持"
🧠 数据流:
js
data: { layout: '1x1' } → computed: layoutClass → class绑定 → 重新渲染视口容器
⚙️ 底层逻辑:
- 切换
layout
→watch
监听 → 销毁旧视口 → 创建新视口 → 重新加载图像 - 视口数量:1x1=1个, 1x2=2个, 2x2=4个
💡 用户体验:
- 按钮高亮显示当前布局
- 切换后自动聚焦第一个视口(
activeViewport = 0
)
👁️ 2. 视图模式工具组(View Mode)
html
<button @click="setViewMode('axial')">轴向视图</button>
<button @click="setViewMode('coronal')">冠状视图</button>
<button @click="setViewMode('sagittal')">矢状视图</button>
<button @click="toggle3D">3D</button>
🎯 功能:
- 切换视图方向:轴向(横断面)、冠状(前后)、矢状(左右)------ 仅视觉提示,无实际图像变换
- 3D 按钮:伪功能,弹窗提示"需专业服务器支持"
🧠 数据流:
js
data: { viewMode: 'axial' } → 仅用于按钮高亮,无实际图像处理
💡 说明:真正的 MPR/3D 需要多平面重建和体绘制,本组件未实现,仅预留入口。
📏 3. 测量工具组(Measurement Tools)
html
<button @click="setTool('clear')">清除</button>
<button @click="setTool('EllipticalRoi')">椭圆ROI</button>
<button @click="setTool('RectangleRoi')">矩形ROI</button>
<button @click="setTool('CobbAngle')">Cobb角</button>
<button @click="setTool('Angle')">角度测量</button>
<button @click="setTool('Probe')">探针</button>
<button @click="setTool('Length')">直线</button>
<button @click="setTool('FreehandRoi')">手绘</button>
🎯 功能:
激活不同测量工具,在图像上进行标注:
工具名 | 作用 | Cornerstone 工具类 |
---|---|---|
clear |
清除当前视口所有标注 | clearToolState(element, tool) |
EllipticalRoi |
绘制椭圆ROI,计算面积/均值 | EllipticalRoiTool |
RectangleRoi |
绘制矩形ROI | RectangleRoiTool |
CobbAngle |
测量 Cobb 角(脊柱侧弯) | CobbAngleTool |
Angle |
测量任意三点夹角 | AngleTool |
Probe |
显示像素位置的原始值 | ProbeTool |
Length |
测量两点间直线距离 | LengthTool |
FreehandRoi |
自由手绘ROI | FreehandRoiTool |
🧠 数据流:
js
点击按钮 → setTool(toolName) → 遍历所有视口 → setToolForElement(element, toolName)
→ 禁用其他工具 → 激活目标工具 → cornerstoneTools.setToolActive(...)
📊 测量结果自动收集:
js
// 在 mounted 或 init 时监听事件(虽然当前代码未完整实现,但预留了结构)
cornerstoneTools.MeasurementAddedCallback(...)
// 数据存入:
measurements: [
{ type: 'Length', value: '12.50 mm', viewport: 0 },
{ type: 'Angle', value: '35.20 °', viewport: 1 },
...
]
💡 用户体验:
- 激活工具按钮高亮
- 标注完成后自动出现在右侧"测量结果"面板
- 可点击 × 删除某条测量
🖼️ 4. 图像处理工具组(Image Manipulation)
html
<button @click="setTool('Wwwc')">调窗</button>
<button @click="setTool('Pan')">拖移</button>
<button @click="setTool('Zoom')">缩放</button>
<button @click="setTool('Rotate')">旋转</button>
<button @click="setTool('FlipH')">水平</button>
<button @click="setTool('FlipV')">垂直</button>
🎯 功能详解:
工具名 | 作用 | 交互方式 | 状态同步 |
---|---|---|---|
Wwwc |
调节窗宽窗位 | 鼠标拖拽 | 同步到右侧滑块 + 状态栏 |
Pan |
平移图像 | 鼠标左键拖拽 | 无 |
Zoom |
缩放图像 | 鼠标左键拖拽 | 同步到右侧滑块 |
Rotate |
旋转图像 | 鼠标左键拖拽 | 同步到右侧滑块 |
FlipH |
水平翻转 | 点击即生效 | 修改 viewport.hflip |
FlipV |
垂直翻转 | 点击即生效 | 修改 viewport.vflip |
⚠️ 注意:
FlipH/V
不是工具,是立即执行的命令,所以setToolForElement
中做了特殊处理:
js
case 'FlipH':
this.flipImage(element, 'horizontal') // 立即执行
break
🔄 窗宽窗位(WW/WL)原理:
windowWidth
: 控制对比度(值越大,对比度越低)windowCenter
: 控制亮度(值越大,图像越亮)- 实时修改
cornerstone.getViewport().voi
并setViewport
🧩 5. 预设与重置工具组
html
<button @click="showPresets = !showPresets">预设</button>
<button @click="resetAll">重置</button>
<button @click="toggleToolsPanel">〓工具</button>
🎯 功能:
-
预设:弹出面板,一键应用骨骼、肺部、脑部等常用窗宽窗位
jspresets: { bone: { ww: 2000, wc: 300 }, lung: { ww: 1500, wc: -600 }, ... } → applyPreset → 修改当前视口 ww/wc → updateViewport
-
重置 :
resetAll()
→ 调用cornerstone.reset(element)
,恢复默认窗宽窗位+缩放+旋转 -
工具面板开关 :控制右侧
side-panel
的显示/隐藏,通过 classcollapsed
控制宽度 280px ↔ 0
📤 6. 上传与导出工具组
html
<input type="file" @change="loadDicomFiles" ref="fileInput" accept=".dcm" multiple />
<button @click="$refs.fileInput.click()">上传</button>
<button @click="exportImage">导出</button>
<button @click="printImage">打印</button>
🎯 功能:
-
上传:
- 支持多文件选择 + 拖拽上传(
@drop
/@dragover
) - 文件限制:
.dcm
后缀 - 流程:
File[] → add to fileManager → loadImage → displayImage
- 支持多文件选择 + 拖拽上传(
-
导出 :当前为
alert('需实现')
,可扩展为 canvas.toDataURL 导出 PNG -
打印 :调用
window.print()
,触发浏览器打印(可配合 CSS@media print
优化)
🖼️ 二、主显示区域 ------ 多视口渲染引擎
📐 1. 布局容器(viewport-layout)
根据 layout
动态生成 1/2/4 个 .viewport-item
每个视口包含:
div.dicom-element
:Cornerstone 渲染画布div.placeholder
:未加载时的占位提示div.viewport-overlay
:叠加显示 WW/WL、Zoom、Slice 信息input.slice-slider
:切片选择滑块(当前未启用多帧,固定为 1/1)
🎨 样式控制:
css
.layout-1x1 .viewport-item { width: 100%; height: 100%; }
.layout-1x2 .viewport-item { width: 50%; height: 100%; }
.layout-2x2 .viewport-item { width: 50%; height: 50%; }
🖱️ 2. 视口交互行为
交互 | 事件 | 处理函数 | 作用 |
---|---|---|---|
点击视口 | @click |
setActiveViewport |
设置当前激活视口 |
双击视口 | @dblclick |
toggleFullscreen |
进入/退出全屏 |
滚轮 | @wheel |
handleWheel |
缩放/调窗(Ctrl/Shift) |
右键 | @contextmenu |
showContextMenu |
显示右键菜单 |
🌀 滚轮精细控制(核心交互):
js
handleWheel(evt, vpIndex) {
if (evt.ctrlKey) {
// 缩放
csViewport.scale *= 1 + (deltaY > 0 ? -0.1 : 0.1)
} else if (evt.shiftKey) {
// 调窗位
viewport.windowCenter += deltaY > 0 ? -10 : 10
} else {
// 调窗宽
viewport.windowWidth += deltaY > 0 ? -20 : 20
}
}
✅ 专业级交互:符合 RadiAnt、OsiriX 等专业软件操作习惯。
📊 三、右侧面板 ------ 工具控制中枢
🛠️ 1. 窗宽窗位控制
html
<input v-model="activeViewportData.windowWidth" @input="updateViewport" />
<input v-model="activeViewportData.windowCenter" @input="updateViewport" />
- 滑块范围:WW [1, 5000],WC [-1000, 1000]
- 实时同步到图像:
cornerstone.setViewport(... voi: { ww, wc })
🔍 2. 缩放控制
html
<input v-model="activeViewportData.zoomPercentage" @input="updateZoom" />
<button @click="zoomIn">+</button>
<button @click="zoomOut">-</button>
<button @click="fitToWindow">适合</button>
zoomPercentage
: 10% ~ 500%fitToWindow
: 调用cornerstone.fitToWindow(element)
自适应
🔄 3. 旋转控制
html
<input v-model="activeViewportData.rotation" @input="updateRotation" />
- 范围:0° ~ 360°
- 底层:
cornerstone.getViewport().rotation = value
📐 4. 测量结果列表
html
<div v-for="(m, index) in measurements" :key="index">
<div class="measurement-type">{{ getMeasurementType(m.type) }}</div>
<div class="measurement-value">{{ m.value }}</div>
<button @click="deleteMeasurement(index)">×</button>
</div>
measurements
数组由工具标注时自动添加(需完善事件监听)- 支持删除,从数组中
splice
🧾 5. DICOM 信息面板
js
dicomInfo: {
patientName: image.data.string('x00100010'),
patientId: 'x00100020',
studyDate: 'x00080020',
seriesDescription: 'x0008103e',
...
}
- 从
cornerstone.loadImage
返回的image.data
中提取 - 显示在面板中,供医生参考
📁 6. 文件列表
html
<div v-for="(file, index) in loadedFiles" @click="switchToFile(index)">
<span>{{ file.name }}</span>
<span>{{ formatFileSize(file.size) }}</span>
</div>
- 显示已上传文件列表
- 点击切换到当前视口显示该文件
- 双击切换到视口0并加载(
switchToFile(index, 0)
)
📉 四、底部状态栏 ------ 实时信息汇总
html
<span>布局: {{ layout }}</span>
<span>当前视口: {{ activeViewport + 1 }}</span>
<span>工具: {{ getActiveToolName() }}</span>
<span>图像: {{ loadedFiles.length }} 个文件</span>
<span>{{ currentTime }}</span>
- 实时显示系统状态
- 时间每秒更新(
setInterval
)
🖱️ 五、右键上下文菜单 ------ 快捷操作
html
<div class="context-menu" v-if="contextMenu.visible">
<div @click="copyImage">复制图像</div>
<div @click="saveAs">另存为...</div>
<div @click="showProperties">属性</div>
<div class="menu-separator" />
<div @click="closeContextMenu">关闭</div>
</div>
- 位置跟随鼠标右键点击位置
- 功能目前为伪实现(alert),可扩展:
copyImage
: canvas.toBlob + Clipboard APIsaveAs
: 触发 downloadshowProperties
: 弹窗显示完整 DICOM Tag
⏳ 六、加载指示器
html
<div v-if="loading" class="loading-overlay">
<div class="loading-spinner" />
<div>正在加载DICOM文件...</div>
</div>
- 文件上传/加载时显示
- 防止用户误操作
🧩 七、数据状态全景图(data())
js
data() {
return {
layout: '1x1', // 当前布局
viewMode: 'axial', // 当前视图模式(UI only)
activeViewport: 0, // 当前激活视口索引
activeTool: 'Wwwc', // 当前激活工具
showPresets: false, // 预设面板显隐
showToolsPanel: true, // 右侧面板显隐
loading: false, // 加载状态
currentTime: '', // 当前时间
loadedFiles: [], // 已上传文件列表
activeFileIndex: 0, // 当前选中文件索引
measurements: [], // 测量结果数组
contextMenu: { visible: false, x: 0, y: 0, viewportIndex: 0 },
// 核心:4个视口状态
viewports: [
{ imageLoaded: false, windowWidth: 400, windowCenter: 40, zoomPercentage: 100, rotation: 0, imageId: null, element: null, currentSlice: 1, totalSlices: 1, dicomInfo: null },
// ... 重复3次
],
presets: { ... } // 预设窗配置
}
}
✅ 每个视口完全独立,互不影响 ------ 多视图协同阅片的基础!
🔄 八、核心方法速查表
方法名 | 作用 | 调用时机 |
---|---|---|
initCornerstone |
初始化引擎+工具 | mounted |
loadDicomFiles |
上传文件并加载到视口 | 文件选择 / 拖拽 |
setTool |
设置当前工具 | 点击工具按钮 |
setLayout |
切换布局 | 点击布局按钮 |
updateViewport |
同步窗宽窗位到图像 | 滑块拖动 |
handleWheel |
处理滚轮缩放/调窗 | 鼠标滚轮 |
switchToFile |
切换文件到指定视口 | 点击文件列表 |
resetAll |
重置所有视口 | 点击"重置"按钮 |
applyPreset |
应用预设窗 | 点击预设项 |
deleteMeasurement |
删除某条测量 | 点击测量项的 × |
toggleFullscreen |
视口全屏 | 双击视口 |
✅ 组件结构分层
plaintext
DICOM Viewer
├── 顶部工具栏(布局、视图、测量、图像处理、预设、上传)
├── 中部主视图容器(支持多视口布局)
│ ├── 视口容器(1x1 / 1x2 / 2x2)
│ │ ├── 占位图 / 图像渲染区
│ │ ├── 切片滑块
│ │ └── 信息叠加层(WW/WL、Zoom、Slice)
├── 右侧面板(工具面板、测量列表、DICOM 信息、文件列表)
├── 底部状态栏(布局、当前视口、工具、文件数、时间)
├── 上下文菜单(右键操作)
└── 加载遮罩层
💡 所有 UI 元素与数据状态(
viewports
数组)绑定,通过activeViewport
控制焦点视口,实现"多视图独立控制"。
⚙️ 核心技术栈与生态整合
技术模块 | 作用说明 |
---|---|
cornerstone-core |
基础图像渲染引擎,负责图像显示、缩放、平移、窗宽窗位等底层操作。 |
cornerstone-tools |
提供测量工具(长度、角度、ROI、Cobb角等),支持交互式标注。 |
cornerstone-wado-image-loader |
DICOM 文件加载器,支持本地文件与 WADO-URI 协议。 |
dicom-parser |
解析 DICOM Tag,提取患者信息、序列描述等元数据。 |
Vue 2.x |
响应式数据绑定,管理视口状态、工具状态、布局切换等复杂交互。 |
🧩 关键:
- 工具状态绑定到当前
activeViewport.element
- 每个视口独立维护
imageId
、element
、viewport配置
- 使用
purgeListeners
避免重复绑定导致内存泄漏
🧨 核心难点与解决方案
🎯 难点 1:布局切换导致图像丢失或重复加载
❗ 问题表现:
切换布局(如 1x1 → 2x2)后,图像不显示或控制异常。
✅ 解决方案:
js
watch: {
layout: {
handler() {
// 1. 彻底卸载旧节点监听器 + 禁用 cornerstone
for (let i = 0; i < this.viewports.length; i++) {
const el = this.viewports[i].element
if (el) {
cornerstone.disable(el) // 关键!
this.viewports[i].element = null
this.viewports[i].imageLoaded = false
}
}
this.$nextTick(() => {
const count = this.getViewportCount()
const files = this.loadedFiles
for (let i = 0; i < count; i++) {
const file = files[Math.min(i, files.length - 1)]
const el = this.$refs[`dicomElement${i}`][0]
// 2. 重新启用元素 + 加载图像
cornerstone.enable(el)
this.viewports[i].element = el
const imageId = cornerstoneWADOImageLoader.wadouri.fileManager.add(file)
cornerstone.loadImage(imageId).then(image => {
cornerstone.displayImage(el, image)
// 同步视口状态
})
}
})
}
}
}
💡 核心思想:布局切换 = 重新初始化所有视口元素,避免状态残留。
🎯 难点 2:滚轮冲突 ------ 工具与缩放/窗宽窗位互斥
❗ 问题表现:
启用 ZoomTool
后,自定义滚轮缩放失效;或反之。
✅ 解决方案:
js
// 初始化时禁用 cornerstone-tools 的滚轮
cornerstoneTools.init({
mouseWheelEnabled: false, // 👈 关键配置!
})
// 自定义滚轮处理
handleWheel(evt, vpIndex) {
evt.preventDefault()
evt.stopPropagation()
const vp = this.viewports[vpIndex]
const csViewport = cornerstone.getViewport(vp.element)
if (evt.ctrlKey) {
// Ctrl + 滚轮 → 缩放
csViewport.scale *= 1 + (evt.deltaY > 0 ? -0.1 : 0.1)
} else if (evt.shiftKey) {
// Shift + 滚轮 → 调窗位
vp.windowCenter += evt.deltaY > 0 ? -10 : 10
} else {
// 普通滚轮 → 调窗宽
vp.windowWidth += evt.deltaY > 0 ? -20 : 20
}
cornerstone.setViewport(vp.element, csViewport)
this.updateViewport() // 同步到面板滑块
}
🛡️ 设计原则:
- 完全接管滚轮事件,通过
stopPropagation
阻断工具库处理- 通过修饰键(Ctrl/Shift)实现多模式切换,符合专业软件交互习惯
🎯 难点 3:多视口工具状态同步与隔离
❗ 问题表现:
切换工具后,部分视口未更新;或测量标注出现在错误视口。
✅ 解决方案:
js
setTool(toolName) {
this.activeTool = toolName
// 遍历所有启用的视口,逐个设置工具状态
for (let i = 0; i < this.getViewportCount(); i++) {
const el = this.viewports[i].element
if (el) this.setToolForElement(el, toolName)
}
}
setToolForElement(element, toolName) {
// 1. 先禁用所有工具
const allTools = ['Length', 'Angle', 'Wwwc', ...]
allTools.forEach(tool => cornerstoneTools.setToolDisabled(element, tool))
// 2. 根据名称激活对应工具
switch (toolName) {
case 'Length':
cornerstoneTools.setToolActive(element, 'Length', { mouseButtonMask: 1 })
break
case 'clear':
allTools.forEach(tool => cornerstoneTools.clearToolState(element, tool))
cornerstone.updateImage(element)
break
// ... 其他工具
}
}
🔄 同步机制:
- 工具状态以
activeTool
为单源真相- 每次切换,遍历所有视口重新设置,确保一致性
- 清除标注时,仅作用于当前
element
,避免跨视口污染
🚀 四、性能优化与工程化实践
✅ 1. 视口懒加载与复用
js
// 文件切换时,只重载当前激活视口,而非全部
switchToFile(fileIndex, vpIndex = this.activeViewport) {
const targetVp = this.viewports[vpIndex]
const el = this.$refs[`dicomElement${vpIndex}`][0]
// 复用 element,仅更换 imageId
const imageId = cornerstoneWADOImageLoader.wadouri.fileManager.add(file)
cornerstone.loadImage(imageId).then(image => {
cornerstone.displayImage(el, image)
// 更新元数据、窗宽窗位等
})
}
✅ 2. 内存泄漏预防 ------ 卸载监听器
js
beforeDestroy() {
this.viewports.forEach(vp => {
if (vp.element) {
vp.element.removeEventListener('wheel', this.onWheelScale)
try { cornerstone.disable(vp.element) } catch(e) {}
}
})
}
✅ 3. 响应式布局与移动端适配
css
/* 2x2 布局容器 */
.layout-2x2 {
flex-wrap: wrap;
}
.layout-2x2 .viewport-item {
width: 50%;
height: 50%;
}
/* 移动端降级为纵向 1x2 */
@media (max-width: 768px) {
.layout-1x2 .viewport-item {
width: 100%;
height: 50%;
}
}
🎨 五、UI/UX 设计亮点
功能 | 交互细节 |
---|---|
布局高亮 | 当前布局按钮 active 状态高亮,视觉反馈明确。 |
视口激活边框 | 点击视口后,蓝色边框 + 阴影提示当前操作对象。 |
右键上下文菜单 | 支持复制、另存、属性查看,符合桌面端用户习惯。 |
工具面板折叠 | 可收起右侧面板,最大化图像显示区域,适合阅片场景。 |
预设窗弹窗 | 一键切换骨骼/肺部/脑部窗宽窗位,提升诊断效率。 |
状态栏实时信息 | 显示当前布局、视口、工具、时间,辅助用户定位状态。 |
📦 六、可扩展性设计
✅ 插件式工具注册
js
registerAllTools() {
const tools = [
cornerstoneTools.LengthTool,
cornerstoneTools.AngleTool,
// ... 可动态扩展
]
tools.forEach(tool => cornerstoneTools.addTool(tool))
}
✅ 预设窗配置化
js
presets: {
bone: { windowWidth: 2000, windowCenter: 300 },
lung: { windowWidth: 1500, windowCenter: -600 },
// 新增预设只需添加对象,无需改逻辑
}
✅ 测量结果结构化
js
measurements: [{
type: 'Length',
value: '12.34 mm',
viewport: 0,
data: { /* 原始测量数据 */ }
}]
支持导出、统计、与 PACS 系统对接。
🧪 七、待优化与未来方向
方向 | 说明 |
---|---|
MPR/3D 本地支持 | 当前为伪功能,需集成 cornerstone3D 或 vtk.js 实现真三维重建。 |
多帧 DICOM 支持 | 当前仅支持单帧,需扩展 currentSlice 逻辑加载多帧序列。 |
标注持久化 | 测量结果可导出为 DICOM SR 或 JSON,支持重新加载。 |
性能监控 | 大图加载时添加骨架屏、进度条,避免白屏。 |
主题切换 | 支持暗色/亮色模式,适配不同阅片环境。 |
📌 总结:一个"小而全"的医学影像前端架构范本
✅ 它不只是一个查看器,更是一个架构良好的工程化项目:
- 状态管理清晰 :
viewports
数组 +activeViewport
焦点控制- 事件解耦彻底:自定义滚轮、右键菜单、拖拽上传互不干扰
- 性能优化到位:监听器清理、懒加载、复用机制
- 扩展性强:工具、预设、测量均可配置化扩展
- 符合医疗场景:专业交互、信息密度高、操作精准
🏁 适用场景:
- 医院 PACS 系统前端
- 医学 AI 标注平台
- 教学/科研用 DICOM 查看器
- 远程会诊系统影像模块
💡 如果你正在构建医学影像相关产品,这个组件可直接作为核心模块复用 ------ 它解决了 80% 的通用需求,并预留了 20% 的定制化空间。
📌 附:关键方法速查表
方法名 | 作用 |
---|---|
setLayout |
切换 1x1 / 1x2 / 2x2 布局 |
setTool |
设置当前激活工具 |
loadDicomFileToViewport |
加载文件到指定视口 |
updateViewport |
同步窗宽窗位到图像 |
handleWheel |
自定义滚轮控制 |
switchToFile |
切换文件并重载视口 |
resetAll |
重置所有视口到初始状态 |