将 OHIF 医学影像查看器集成到微信小程序:从架构到实战
本文深度解析如何将开源医学影像查看器 OHIF 集成到微信小程序,实现医生随时随地查看患者影像的需求。
一、背景与需求
1.1 业务场景
医生需要:
- 📱 移动端快速查看患者影像
- 🏥 会诊时随时调阅历史影像
- 🚑 急诊场景下的远程诊断
- 👥 多学科协作时的影像共享
1.2 OHIF 简介
OHIF 是开源医学影像查看器,基于:
- React + Cornerstone3D
- DICOMweb 标准
- 插件化架构
二、技术方案对比
方案一:WebView 嵌入(快速但受限)
实现:
<web-view src="https://your-domain.com/ohif/viewer?StudyInstanceUID=xxx"></web-view>
优点: 快速、复用现有功能 缺点: 无法使用小程序 API、性能受限
方案二:混合架构(推荐)
架构:
小程序原生页面(患者列表、影像列表)
↓
WebView(影像查看器)
↓
中间层 API(认证、缓存、转换)
↓
PACS/DICOMweb 服务器
三、推荐方案详细设计
3.1 前端层(小程序)
// pages/patient-list/index.js
Page({
data: { patients: [] },
async onLoad() {
const patients = await wx.cloud.callFunction({
name: 'getPatients'
});
this.setData({ patients: patients.result.data });
},
viewStudies(e) {
const { patientId } = e.currentTarget.dataset;
wx.navigateTo({
url: `/pages/study-list/index?patientId=${patientId}`
});
}
});
// pages/study-list/index.js
Page({
async onLoad(options) {
const studies = await this.fetchStudies(options.patientId);
this.setData({ studies });
},
openViewer(e) {
const { studyInstanceUID } = e.currentTarget.dataset;
const token = wx.getStorageSync('token');
const url = `https://your-domain.com/viewer?` +
`StudyInstanceUID=${studyInstanceUID}&token=${token}`;
wx.navigateTo({
url: `/pages/viewer/index?url=${encodeURIComponent(url)}`
});
}
});
3.2 中间层 API
// server/routes/api.js
const express = require('express');
const router = express.Router();
// 影像列表接口
router.get('/api/studies', authenticateToken, async (req, res) => {
const { patientId } = req.query;
// 调用 PACS DICOMweb 接口
const response = await axios.get(
`${PACS_URL}/dicomweb/studies`,
{
params: { PatientID: patientId },
headers: { 'Accept': 'application/dicom+json' }
}
);
// 数据转换
const studies = response.data.map(study => ({
studyInstanceUID: study['0020000D'].Value[0],
studyDate: study['00080020']?.Value[0],
studyDescription: study['00081030']?.Value[0],
modality: study['00080060']?.Value[0]
}));
res.json({ success: true, data: studies });
});
// 影像数据代理
router.get('/api/wado/*', authenticateToken, async (req, res) => {
const wadoPath = req.params[0];
const response = await axios.get(
`${PACS_URL}/dicomweb/${wadoPath}`,
{ responseType: 'arraybuffer' }
);
res.set(response.headers);
res.send(response.data);
});
3.3 OHIF 配置
// ohif-config.js
window.config = {
showStudyList: false,
dataSources: [{
namespace: '@ohif/extension-default.dataSourcesModule.dicomweb',
sourceName: 'miniprogram',
configuration: {
qidoRoot: 'https://your-api.com/api/qido',
wadoRoot: 'https://your-api.com/api/wado',
enableStudyLazyLoad: true,
onConfiguration: (config, { query }) => {
const token = query.get('token');
if (token) {
config.headers = { 'Authorization': `Bearer ${token}` };
}
return config;
}
}
}],
maxNumberOfWebWorkers: 2 // 移动端优化
};
四、核心难点与解决方案
4.1 快速定位患者影像
解决方案:
- 智能搜索
async onSearch(keyword) {
const results = await wx.cloud.callFunction({
name: 'searchPatients',
data: {
keyword,
searchFields: ['name', 'patientId', 'admissionNumber'],
fuzzyMatch: true
}
});
this.setData({ searchResults: results.data });
}
- 扫码快速打开
async scanQRCode() {
const res = await wx.scanCode();
const patientInfo = JSON.parse(res.result);
this.openPatientStudies(patientInfo);
}
- 收藏与最近查看
// 收藏患者
await wx.cloud.database().collection('favorites').add({
data: { doctorId, patientId, createTime: new Date() }
});
// 记录最近查看
await wx.cloud.database().collection('recent').add({
data: { doctorId, patientId, studyUID, viewTime: new Date() }
});
4.2 大文件传输优化
分层加载策略:
class LayeredImageLoader {
// 第一层:缩略图
async loadLevel1() {
const thumbnails = await this.fetchThumbnails();
return thumbnails;
}
// 第二层:预览质量
async loadLevel2(seriesUID) {
const mediumQuality = await this.fetchImages(seriesUID, 'medium');
return mediumQuality;
}
// 第三层:全分辨率
async loadLevel3(instanceUID) {
if (this.cache.has(instanceUID)) {
return this.cache.get(instanceUID);
}
const fullRes = await this.fetchFullResolution(instanceUID);
this.cache.set(instanceUID, fullRes);
return fullRes;
}
}
缓存策略:
class CacheManager {
constructor() {
this.maxSize = 100 * 1024 * 1024; // 100MB
}
async set(key, data) {
if (this.needCleanup(data)) {
await this.cleanupLRU();
}
await wx.setStorage({ key, data });
}
async cleanupLRU() {
// 清理最久未使用的缓存
const entries = this.getSortedEntries();
for (const [key] of entries) {
await wx.removeStorage({ key });
if (this.hasEnoughSpace()) break;
}
}
}
4.3 触摸交互适配
class TouchGestureAdapter {
handleTouchStart(e) {
const touches = e.touches;
if (touches.length === 1) {
this.startPan(touches[0]); // 单指平移
} else if (touches.length === 2) {
this.startPinch(touches); // 双指缩放
}
}
startPan(touch) {
this.lastX = touch.clientX;
this.lastY = touch.clientY;
}
updatePan(touch) {
const deltaX = touch.clientX - this.lastX;
const deltaY = touch.clientY - this.lastY;
cornerstone.pan(this.element, { x: deltaX, y: deltaY });
}
startPinch(touches) {
this.initialDistance = this.getDistance(touches[0], touches[1]);
}
updatePinch(touches) {
const currentDistance = this.getDistance(touches[0], touches[1]);
const scale = currentDistance / this.initialDistance;
cornerstone.zoom(this.element, scale);
}
}
4.4 数据安全
认证流程:
// 1. 小程序登录
wx.login({
success: async (res) => {
const { token } = await wx.cloud.callFunction({
name: 'login',
data: { code: res.code }
});
wx.setStorageSync('token', token);
}
});
// 2. API 请求带 token
const response = await fetch(url, {
headers: {
'Authorization': `Bearer ${token}`
}
});
// 3. 服务端验证
function authenticateToken(req, res, next) {
const token = req.headers['authorization']?.split(' ')[1];
if (!token) return res.sendStatus(401);
jwt.verify(token, SECRET_KEY, (err, user) => {
if (err) return res.sendStatus(403);
req.user = user;
next();
});
}
数据加密:
// 敏感数据加密传输
const crypto = require('crypto');
function encryptData(data) {
const cipher = crypto.createCipher('aes-256-cbc', SECRET_KEY);
let encrypted = cipher.update(JSON.stringify(data), 'utf8', 'hex');
encrypted += cipher.final('hex');
return encrypted;
}
function decryptData(encrypted) {
const decipher = crypto.createDecipher('aes-256-cbc', SECRET_KEY);
let decrypted = decipher.update(encrypted, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return JSON.parse(decrypted);
}
五、性能优化
5.1 网络优化
// 请求合并
class RequestBatcher {
constructor() {
this.queue = [];
this.timer = null;
}
add(request) {
this.queue.push(request);
this.scheduleFlush();
}
scheduleFlush() {
if (this.timer) return;
this.timer = setTimeout(() => {
this.flush();
}, 50);
}
async flush() {
const batch = this.queue.splice(0);
await this.executeBatch(batch);
this.timer = null;
}
}
// 并发控制
class ConcurrencyController {
constructor(maxConcurrency = 5) {
this.max = maxConcurrency;
this.running = 0;
this.queue = [];
}
async run(fn) {
while (this.running >= this.max) {
await this.waitForSlot();
}
this.running++;
try {
return await fn();
} finally {
this.running--;
this.processQueue();
}
}
}
5.2 渲染优化
// 虚拟滚动
class VirtualScroller {
constructor(container, itemHeight) {
this.container = container;
this.itemHeight = itemHeight;
this.visibleCount = Math.ceil(container.height / itemHeight) + 2;
}
render(scrollTop, totalItems) {
const startIndex = Math.floor(scrollTop / this.itemHeight);
const endIndex = Math.min(startIndex + this.visibleCount, totalItems);
return {
startIndex,
endIndex,
offsetY: startIndex * this.itemHeight
};
}
}
// 帧率控制
function throttleRender(fn, fps = 30) {
const interval = 1000 / fps;
let lastTime = 0;
return function(...args) {
const now = Date.now();
if (now - lastTime >= interval) {
lastTime = now;
fn.apply(this, args);
}
};
}
六、部署方案
6.1 服务端部署
# docker-compose.yml
version: '3'
services:
api:
image: node:18
volumes:
- ./server:/app
ports:
- "3000:3000"
environment:
- PACS_URL=http://pacs:8042
- REDIS_URL=redis://redis:6379
redis:
image: redis:7
ports:
- "6379:6379"
nginx:
image: nginx:latest
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf
- ./ohif-dist:/usr/share/nginx/html
ports:
- "80:80"
- "443:443"
七、总结
实施步骤
-
第一阶段(1-2周): 搭建基础架构
- 部署 PACS 服务器
- 开发中间层 API
- 配置 OHIF
-
第二阶段(2-3周): 开发小程序
- 患者列表页
- 影像列表页
- WebView 集成
-
第三阶段(1-2周): 优化与测试
- 性能优化
- 安全加固
- 用户测试
技术栈总结
前端:
- 小程序原生框架
- OHIF Viewer
- Cornerstone3D
后端:
- Node.js / Python
- Redis(缓存)
- Nginx(反向代理)
存储:
- PACS(Orthanc / DCM4CHEE)
- 云存储(OSS / S3)
关键指标
- 首屏加载:< 3秒
- 影像切换:< 500ms
- 缓存命中率:> 80%
- 并发支持:> 100 用户
作者: [whm]
标签: #医学影像 #OHIF #微信小程序 #DICOMweb