Vue3 + Cornerstone.js 实现 DICOM (DCM) 医学影像文件预览
📋 前言
DICOM (Digital Imaging and Communications in Medicine) 是医学影像的国际标准格式,广泛应用于医疗影像系统中。本文将介绍如何在 Vue3 项目中使用 Cornerstone.js 实现 DICOM 文件的在线预览功能。
🛠 技术栈
- Vue 3 - 前端框架
- TypeScript - 类型安全
- Element Plus - UI 组件库
- Cornerstone Core - DICOM 图像渲染引擎
- Cornerstone WADO Image Loader - DICOM 图像加载器
- dicom-parser - DICOM 文件解析器
📦 安装依赖
bash
pnpm add cornerstone-core cornerstone-wado-image-loader dicom-parser
🔧 实现步骤
1. 导入必要的库
typescript
import * as cornerstone from "cornerstone-core";
import * as cornerstoneWADOImageLoader from "cornerstone-wado-image-loader";
import * as dicomParser from "dicom-parser";
2. 初始化 Cornerstone WADO Image Loader
typescript
const initCornerstoneWADOImageLoader = () => {
cornerstoneWADOImageLoader.external.cornerstone = cornerstone;
cornerstoneWADOImageLoader.external.dicomParser = dicomParser;
cornerstoneWADOImageLoader.configure({
useWebWorkers: false,
});
};
// 组件加载时初始化
initCornerstoneWADOImageLoader();
3. 创建 DICOM 容器和状态管理
typescript
const dicomContainer = ref<HTMLElement | null>(null); // DICOM 容器元素
const dicomImageId = ref(""); // DICOM 图像 ID
// 判断是否为 DICOM 文件
const isDicom = computed(() => {
return ["dcm", "dicom"].includes(fileExtension.value);
});
4. 实现 DICOM 文件加载函数
typescript
const loadDicomFile = async (blob: Blob) => {
try {
console.log("开始加载 DICOM 文件,大小:", blob.size);
const url = URL.createObjectURL(blob);
const imageId = cornerstoneWADOImageLoader.wadouri.fileManager.add(blob);
// 先设置这些值,触发容器渲染
dicomImageId.value = imageId;
fileUrl.value = url;
// 等待 DOM 更新,让容器被渲染
await nextTick();
if (!dicomContainer.value) {
throw new Error("DICOM 容器元素不存在,请检查 DOM 渲染");
}
// 确保容器有尺寸
if (dicomContainer.value.offsetWidth === 0 || dicomContainer.value.offsetHeight === 0) {
await new Promise(resolve => setTimeout(resolve, 300));
}
// 启用 cornerstone 元素
try {
cornerstone.enable(dicomContainer.value);
} catch (e) {
console.warn("启用 cornerstone 元素警告:", e);
}
// 加载并显示图像
console.log("开始加载图像...");
const image = await cornerstone.loadImage(imageId);
// 显示图像
cornerstone.displayImage(dicomContainer.value, image);
// 获取并设置视口(自动调整窗宽窗位)
const viewport = cornerstone.getDefaultViewportForImage(
dicomContainer.value,
image
);
// 设置视口
cornerstone.setViewport(dicomContainer.value, viewport);
// 强制重绘
cornerstone.updateImage(dicomContainer.value);
} catch (error) {
ElMessage.error(`DICOM 文件加载失败: ${error.message || error}`);
throw error;
}
};
5. 在文件加载逻辑中集成
typescript
const loadFile = async () => {
if (!props.fileInfo?.id) return;
loading.value = true;
fileUrl.value = "";
dicomImageId.value = "";
try {
const res = await getFileUrl(props.fileInfo.id);
if (res && res instanceof Blob) {
if (isDicom.value) {
// DICOM 文件:使用 cornerstone 渲染
// 先关闭 loading,让容器渲染出来
loading.value = false;
await loadDicomFile(res);
}
// ... 其他文件类型处理
}
} finally {
loading.value = false;
}
};
6. 添加模板结构
vue
<template>
<el-dialog
:model-value="visible"
:title="fileInfo?.fileName || '文件预览'"
width="65%"
:before-close="handleClose"
destroy-on-close
>
<div class="file-preview-container">
<!-- 加载状态 -->
<div v-if="loading" class="loading-container">
<el-icon class="is-loading">
<Loading />
</el-icon>
<span>正在加载文件...</span>
</div>
<!-- DICOM 预览 -->
<div v-else-if="fileUrl && isDicom" class="preview-content">
<div class="dicom-preview">
<div ref="dicomContainer" class="dicom-viewer" />
</div>
</div>
</div>
<template #footer>
<div class="dialog-footer">
<el-button @click="handleClose">关闭</el-button>
<el-button type="primary" @click="downloadFile">下载</el-button>
</div>
</template>
</el-dialog>
</template>
7. 添加样式
scss
.dicom-preview {
width: 100%;
height: 600px;
display: flex;
align-items: center;
justify-content: center;
background: #000; // 医学影像标准使用黑色背景
border-radius: 4px;
overflow: hidden;
.dicom-viewer {
width: 100%;
height: 100%;
min-width: 512px;
min-height: 512px;
position: relative;
background: #000;
}
}
8. 资源清理
typescript
// 关闭弹窗时清理
const handleClose = () => {
// 清理 cornerstone 元素
if (dicomContainer.value && isDicom.value) {
try {
cornerstone.disable(dicomContainer.value);
} catch (e) {
console.warn("清理 cornerstone 元素失败:", e);
}
}
// 清理 blob URL
if (fileUrl.value) {
URL.revokeObjectURL(fileUrl.value);
fileUrl.value = "";
}
dicomImageId.value = "";
emit("update:visible", false);
};
// 组件卸载时清理
onUnmounted(() => {
if (dicomContainer.value && isDicom.value) {
try {
cornerstone.disable(dicomContainer.value);
} catch (e) {
console.warn("清理 cornerstone 元素失败:", e);
}
}
if (fileUrl.value) {
URL.revokeObjectURL(fileUrl.value);
}
});
✨ 功能特性
- ✅ 自动窗宽窗位调整 - 自动优化图像对比度
- ✅ 高性能渲染 - 基于 Canvas 的快速渲染
- ✅ 内存管理 - 自动清理资源,避免内存泄漏
- ✅ 错误处理 - 完善的错误提示和处理机制
- ✅ 响应式布局 - 适应不同尺寸的容器
⚠️ 关键注意事项
1. Loading 状态控制
确保在调用 loadDicomFile 之前将 loading 设置为 false,否则容器不会渲染:
typescript
loading.value = false; // 先设置为 false
await loadDicomFile(res); // 然后加载
2. 容器渲染时序
必须先设置 fileUrl 和 dicomImageId,触发 Vue 的响应式更新,容器才会渲染:
typescript
dicomImageId.value = imageId;
fileUrl.value = url;
await nextTick(); // 等待 DOM 更新
3. 容器尺寸检查
在启用 Cornerstone 之前,确保容器有有效的尺寸:
typescript
if (dicomContainer.value.offsetWidth === 0 || dicomContainer.value.offsetHeight === 0) {
await new Promise(resolve => setTimeout(resolve, 300));
}
4. 资源清理
每次关闭预览或切换文件时,务必清理:
- Cornerstone 实例 (
cornerstone.disable) - Blob URL (
URL.revokeObjectURL) - 图像 ID 引用
🎯 实现效果
- 支持
.dcm和.dicom扩展名 - 黑色背景显示,符合医学影像查看习惯
- 自动调整窗宽窗位,获得最佳显示效果
- 支持下载原始 DICOM 文件
🐛 常见问题
1. 预览显示黑屏
原因 :容器在 loadDicomFile 执行时还未渲染到 DOM 中
解决方案:
- 在调用
loadDicomFile之前设置loading.value = false - 确保先设置
fileUrl和dicomImageId - 使用
await nextTick()等待 DOM 更新
2. 控制台报错:容器元素不存在
原因 :条件渲染 v-else-if 的逻辑问题
解决方案:
- 确保
fileUrl在容器渲染前已设置 - 检查
loading状态是否正确控制
3. 图像显示但窗宽窗位不对
解决方案:
typescript
const viewport = cornerstone.getDefaultViewportForImage(
dicomContainer.value,
image
);
cornerstone.setViewport(dicomContainer.value, viewport);
📚 扩展功能
可以进一步添加以下功能:
-
交互工具
- 缩放(Zoom)
- 平移(Pan)
- 窗宽窗位调整(Window Level)
- 测量工具(Length、Angle)
-
多帧支持
- 播放 DICOM 序列
- 帧速率控制
-
图像增强
- 伪彩色
- 反转
- 旋转和翻转
🔗 参考资源
📝 总结
通过 Cornerstone.js,我们可以在 Web 应用中轻松实现 DICOM 医学影像的预览功能。关键点在于:
- 正确的初始化顺序
- 容器渲染时序控制
- 完善的资源清理机制
- 错误处理和用户提示
希望这篇文章能帮助你在项目中顺利集成 DICOM 文件预览功能!