引言
在医疗信息化快速发展的今天,DICOM(Digital Imaging and Communications in Medicine)作为医学影像的标准格式,在各类医疗系统中扮演着重要角色。传统的DICOM查看器多为桌面应用,存在部署复杂、更新困难等问题。
本文将介绍如何使用现代Web技术(Vue.js + Cornerstone.js)开发一个功能完善的DICOM医学影像查看器,实现患者列表管理、DICOM图像加载与交互操作等核心功能。
技术选型
1. Cornerstone.js
Cornerstone.js是一个专门为医学影像设计的开源JavaScript库,提供:
- DICOM图像解析与渲染
- 图像交互工具(平移、缩放、窗宽窗位调节)
- 多帧图像支持
- 高性能渲染优化
2. Vue.js框架
Vue.js作为前端框架的优势:
- 响应式数据绑定,简化UI更新逻辑
- 组件化开发,提高代码复用性
- 丰富的生态系统(Vuex、Vue Router等)
3. Element UI
选用Element UI作为UI组件库的原因:
- 提供丰富的预制组件(表格、分页、卡片等)
- 符合医疗系统简洁专业的设计风格
- 良好的文档和社区支持
核心功能实现
1. 页面结构(患者列表+canvas医疗影像展示区域)
html
<template>
<div class="transmit-data">
<div class="page-title">
<span class="self">DICOM</span>
</div>
<div class="main">
<div class="dicom-container">
<el-row :gutter="20" class="top-row">
<!-- 患者列表 -->
<el-col class="top-table" :span="8">
<el-card class="patient-card">
<el-input
v-model="searchQuery"
placeholder="搜索患者ID/姓名"
clearable
@clear="fetchPatients"
@keyup.enter="fetchPatients"
></el-input>
<el-table
:data="currentPageData"
highlight-current-row
@row-click="handlePatientSelect"
height="660"
>
<el-table-column
prop="patientId"
label="患者ID"
min-width="120"
></el-table-column>
<el-table-column
prop="name"
label="姓名"
min-width="80"
></el-table-column>
<el-table-column
prop="studyDate"
label="检查日期"
min-width="120"
></el-table-column>
</el-table>
<el-pagination
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page="currentPage"
:page-sizes="[5, 10, 20, 50]"
:page-size="pageSize"
layout="total, sizes, prev, pager, next, jumper"
:total="patientList.length"
class="pagination-container"
></el-pagination>
</el-card>
</el-col>
<!-- DICOM 查看器 -->
<el-col :span="16">
<el-card class="viewer-card">
<div class="viewer-tools">
<el-button-group>
<el-button @click="zoomIn" icon="el-icon-zoom-in"></el-button>
<el-button
@click="zoomOut"
icon="el-icon-zoom-out"
></el-button>
<el-button
@click="resetView"
icon="el-icon-refresh-left"
></el-button>
</el-button-group>
<el-select
v-model="activeSeriesIndex"
placeholder="选择序列"
style="width: 200px; margin-left: 10px"
>
<el-option
v-for="(series, index) in seriesList"
:key="index"
:label="series.description"
:value="index"
></el-option>
</el-select>
</div>
<!-- DICOM 影像渲染区域 -->
<div id="dicom-viewer" ref="dicomViewer"></div>
<!-- 患者信息 -->
<div class="patient-info" v-if="currentPatient">
<h3>患者信息</h3>
<el-descriptions :column="2" border>
<el-descriptions-item label="患者ID">{{
currentPatient.patientId
}}</el-descriptions-item>
<el-descriptions-item label="姓名">{{
currentPatient.name
}}</el-descriptions-item>
<el-descriptions-item label="性别">{{
currentPatient.sex
}}</el-descriptions-item>
<el-descriptions-item label="年龄">{{
currentPatient.age
}}</el-descriptions-item>
<el-descriptions-item label="检查日期">{{
currentPatient.studyDate
}}</el-descriptions-item>
<el-descriptions-item label="检查类型">{{
currentPatient.studyDescription
}}</el-descriptions-item>
</el-descriptions>
</div>
</el-card>
</el-col>
</el-row>
</div>
</div>
</div>
</template>
2. 需要导入使用到的库
js
import cornerstone from "cornerstone-core";
import cornerstoneWADOImageLoader from "cornerstone-wado-image-loader";
import dicomParser from "dicom-parser";
3. 患者列表管理和其他所需数据
解释测试文件.dcm资源的引用位置和来源:
- imageIds 图像数组数据中 http://localhost:8888 是本地前端代码服务运行地址
- http://localhost:8888/dicom-images/series-000001/image-000001.dcm 默认访问的是你前端代码服务下public下的/dicom-images/series-000001/image-000001.dcm
- 测试文件直接点击 传送门 下载测试所需的影像,解压放入前端public下
- 为何路径前加'wadouri:',可以百度查询
js
data() {
return {
currentPage: 1,
pageSize: 5,
searchQuery: "",
patientList: [
{
patientId: "P2023060001",
name: "张明",
sex: "男",
age: "45岁",
studyDate: "2023-06-15",
studyDescription: "张明·头部CT",
imageIds: [
"wadouri:http://localhost:8888/dicom-images/series-000001/image-000001.dcm",
"wadouri:http://localhost:8888/dicom-images/series-000001/image-000002.dcm",
],
},
{
patientId: "P2023060002",
name: "王绪",
sex: "女",
age: "24岁",
studyDate: "2023-08-05",
studyDescription: "王绪·头部CT",
imageIds: [
"wadouri:http://localhost:8888/dicom-images/series-000001/image-000002.dcm",
"wadouri:http://localhost:8888/dicom-images/series-000001/image-000002.dcm",
],
},
{
patientId: "P2023060003",
name: "李亮",
sex: "男",
age: "28岁",
studyDate: "2023-12-11",
studyDescription: "李亮·头部CT",
imageIds: [
"wadouri:http://localhost:8888/dicom-images/series-000001/image-000003.dcm",
"wadouri:http://localhost:8888/dicom-images/series-000001/image-000003.dcm",
],
},
],
currentPatient: null,
seriesList: [],
activeSeriesIndex: 0,
showAnnotations: false,
viewerElement: null,
toolState: {},
// sb
isPanning: false, // 是否正在平移
lastPanPosition: { x: 0, y: 0 }, // 上一次平移位置
currentViewport: {
// 当前视口状态
translation: { x: 0, y: 0 },
scale: 1,
},
};
},
4. 监听选择的序列
js
watch: {
activeSeriesIndex(newVal) {
if (this.seriesList[newVal]) {
this.loadSeries(this.seriesList[newVal]);
}
},
},
computed: {
// 计算当前页显示的数据
currentPageData() {
const start = (this.currentPage - 1) * this.pageSize;
const end = start + this.pageSize;
return this.patientList.slice(start, end);
},
},
5. mounted()初始化内容
js
mounted() {
// cornerstone配置
cornerstoneWADOImageLoader.external.cornerstone = cornerstone;
cornerstoneWADOImageLoader.external.dicomParser = dicomParser;
// Web Worker配置
cornerstoneWADOImageLoader.webWorkerManager.initialize({
maxWebWorkers: navigator.hardwareConcurrency || 4,
startWebWorkersOnDemand: true,
webWorkerPath: "/cornerstoneWADOImageLoaderWebWorker.js",
taskConfiguration: {
decodeTask: {
loadCodecsOnStartup: true,
initializeCodecsOnStartup: false,
codecsPath: "/cornerstoneWADOImageLoaderCodecs.js",
},
},
});
this.viewerElement = this.$refs.dicomViewer;
cornerstone.enable(this.viewerElement);
this.handlePatientSelect(this.patientList[0]); // 默认选择第一个患者
// 初始化鼠标拖拽
this.initPanTool();
// 设置初始光标样式
this.viewerElement.style.cursor = "grab";
},
6. 鼠标拖拽
js
// 鼠标拖拽
initPanTool() {
const element = this.viewerElement;
element.addEventListener("mousedown", this.handlePanStart);
element.addEventListener("mousemove", this.handlePan);
element.addEventListener("mouseup", this.handlePanEnd);
element.addEventListener("mouseleave", this.handlePanEnd);
// 触摸支持
element.addEventListener("touchstart", this.handlePanStart);
element.addEventListener("touchmove", this.handlePan);
element.addEventListener("touchend", this.handlePanEnd);
},
7. DICOM图像加载
js
// 加载DICOM序列
async loadSeries(series) {
try {
const imageId = series.imageIds[0];
// 等待加载完成
const image = await cornerstone.loadImage(imageId);
// 启用图像容器(确保只启用一次)
if (!cornerstone.getEnabledElement(this.viewerElement)) {
cornerstone.enable(this.viewerElement);
}
// 显示图像(v2版本)
cornerstone.displayImage(this.viewerElement, image);
cornerstone.reset(this.viewerElement);
// 设置窗宽窗位
const viewport = cornerstone.getViewport(this.viewerElement);
viewport.voi.windowWidth = 400;
viewport.voi.windowCenter = 40;
cornerstone.setViewport(this.viewerElement, viewport);
// 预加载其余图像(可选)
series.imageIds.slice(1).forEach((imgId) => {
cornerstone.loadAndCacheImage(imgId);
});
} catch (err) {
console.error("加载图像失败:", err);
this.$message.error("DICOM 加载失败: " + err.message);
}
},
优化策略:
- 使用Web Worker加速图像解码
- 预加载相邻图像
- 错误处理和用户反馈
8. 图像交互功能
平移功能实现
javascript
// 平移开始
handlePanStart(e) {
this.isPanning = true;
const pos = this.getMousePosition(e);
this.lastPanPosition = { x: pos.x, y: pos.y };
this.viewerElement.style.cursor = "grabbing";
}
// 平移中
handlePan(e) {
if (!this.isPanning) return;
const pos = this.getMousePosition(e);
const deltaX = pos.x - this.lastPanPosition.x;
const deltaY = pos.y - this.lastPanPosition.y;
const viewport = cornerstone.getViewport(this.viewerElement);
viewport.translation.x += deltaX;
viewport.translation.y += deltaY;
cornerstone.setViewport(this.viewerElement, viewport);
this.lastPanPosition = pos;
}
// 平移结束
handlePanEnd() {
this.isPanning = false;
this.viewerElement.style.cursor = "grab";
}
9. 获取鼠标位置
js
// 获取鼠标位置
getMousePosition(e) {
const rect = this.viewerElement.getBoundingClientRect();
let clientX, clientY;
if (e.type.includes("touch")) {
clientX = e.touches[0].clientX;
clientY = e.touches[0].clientY;
} else {
clientX = e.clientX;
clientY = e.clientY;
}
return {
x: clientX - rect.left,
y: clientY - rect.top,
};
},
10. 缩放功能实现
javascript
zoomIn() {
const viewport = cornerstone.getViewport(this.viewerElement);
viewport.scale += 0.5;
cornerstone.setViewport(this.viewerElement, viewport);
}
zoomOut() {
const viewport = cornerstone.getViewport(this.viewerElement);
viewport.scale = Math.max(0.5, viewport.scale - 0.5);
cornerstone.setViewport(this.viewerElement, viewport);
}
11. 重置视图
js
resetView() {
cornerstone.reset(this.viewerElement);
},
12. 搜索患者获取患者列表
js
// 获取患者列表
async fetchPatients() {
const params = { q: this.searchQuery };
// 这里替换真实的接口
const res = await this.$axios.get("/api/dicom/patients", { params });
this.patientList = res.data;
},
13. 选择患者
js
async handlePatientSelect(patient) {
console.log(patient);
this.currentPatient = patient;
// 模拟从API获取序列数据
this.seriesList = [
{
description: patient.studyDescription,
imageIds: patient.imageIds,
},
];
this.activeSeriesIndex = 0;
// 初始化工具 需要的可以加上这个函数 可以初始化一些方法
// this.initTools();
// 加载系列
await this.loadSeries(this.seriesList[0]);
},
14. 分页
js
// 新增分页方法
handleSizeChange(val) {
this.pageSize = val;
this.currentPage = 1; // 重置到第一页
},
handleCurrentChange(val) {
this.currentPage = val;
},
交互优化:
- 支持鼠标拖拽平移
- 支持触摸屏操作
- 平滑的动画过渡
- 光标状态反馈
样式
css
<style lang="less" scoped>
/* 新增分页样式 */
.pagination-container {
margin-top: 16px;
text-align: right;
padding: 10px 0;
}
.file-list {
margin-top: 16px;
max-height: 200px;
overflow-y: auto;
border: 1px solid #ebeef5;
border-radius: 4px;
padding: 8px;
.file-item {
padding: 8px;
color: #606266;
font-size: 14px;
transition: background-color 0.3s;
&:hover {
background-color: #f5f7fa;
}
}
}
.upload-action {
margin-top: 16px;
text-align: right;
}
.watermark-tip {
margin-top: 20px;
padding: 5px 16px;
background: rgba(32, 132, 232, 0.1);
border-radius: 4px;
.tip-content {
color: #606266;
.warning {
padding-bottom: 12px;
// color: #e6a23c;
border-bottom: 1px solid #409eff;
.highlight {
color: #2084e8;
font-weight: bold;
}
}
.watermark-info {
display: flex;
width: 606px;
div {
margin-bottom: 10px;
}
}
}
}
.transmit-data {
min-width: auto !important;
overflow: hidden;
box-sizing: border-box;
.page-title {
font-size: 18px;
font-weight: bold;
background: #ffffff;
border-radius: 8px;
padding: 10px;
color: #999;
margin-bottom: 8px;
.self {
color: #2e303c;
}
}
.main {
display: flex;
height: calc(100% - 47px);
background: #ffffff;
box-shadow: 0px 4px 4px 1px rgba(34, 72, 177, 0.08);
border-radius: 8px 8px 8px 8px;
overflow: auto;
box-sizing: border-box;
.dicom-container {
padding: 20px;
width: 100%;
// height: calc(100vh - 60px);
.top-row {
display: flex;
height: 800px;
}
}
.patient-card,
.viewer-card {
height: 100%;
}
#dicom-viewer {
width: 100%;
height: 500px;
background-color: #000;
margin-bottom: 20px;
// 确保图像居中显示
display: flex;
justify-content: center;
align-items: center;
cursor: grab; /* 默认光标样式 */
user-select: none; /* 防止拖动时选中文本 */
canvas {
// 图像最大不超过容器
max-width: 100%;
max-height: 100%;
}
}
#dicom-viewer:active {
cursor: grabbing; /* 拖动时光标样式 */
}
.viewer-tools {
margin-bottom: 15px;
}
.patient-info {
margin-top: 20px;
}
}
.no-data {
height: calc(100% - 35px);
padding-top: 266px;
text-align: center;
color: #999;
background: #fff;
border-radius: 8px;
overflow: hidden;
box-sizing: border-box;
box-shadow: 0px 4px 4px 1px rgba(34, 72, 177, 0.08);
}
}
</style>
性能优化实践
-
图像加载优化
- 使用WADO Image Loader的渐进式加载
- 实现图像缓存策略
- 后台预加载相邻切片
-
内存管理
- 及时清理不再使用的图像数据
- 监听组件销毁事件释放资源
javascriptbeforeDestroy() { cornerstone.disable(this.viewerElement); const element = this.viewerElement; element.removeEventListener("mousedown", this.handlePanStart); element.removeEventListener("mousemove", this.handlePan); element.removeEventListener("mouseup", this.handlePanEnd); element.removeEventListener("mouseleave", this.handlePanEnd); element.removeEventListener("touchstart", this.handlePanStart); element.removeEventListener("touchmove", this.handlePan); element.removeEventListener("touchend", this.handlePanEnd); },
-
渲染性能优化
- 使用requestAnimationFrame优化动画
- 减少不必要的重绘
- 合理设置Canvas尺寸
文章总结与展望
本文介绍了一个基于Web技术的DICOM医学影像查看器的完整实现方案,核心功能包括:
- 患者信息管理与检索
- DICOM图像加载与渲染
- 丰富的图像交互功能(平移、缩放、重置)
- 响应式用户界面
未来扩展方向:
- 添加标注和测量工具
- 实现多平面重建(MPR)功能
- 集成AI辅助诊断
- 支持DICOM影像的导出和分享
通过现代Web技术实现的DICOM查看器,不仅具备了传统桌面应用的核心功能,还拥有更好的可访问性和易用性,为医疗信息化建设提供了新的解决方案。
附录
关键依赖库版本
- Cornerstone-core: ^2.6.1
- Cornerstone-wado-image-loader: ^4.1.1
- Vue: ^2.6.14
- Element UI: ^2.15.9
参考资料
- Cornerstone.js官方文档
- DICOM标准官方文档
- Web医疗影像处理最佳实践