cornerstone3D 通过二进制渲染影像
实现步骤
1.初始化 Cornerstone3D 核心库
js
import {
init as csRenderInit,} from '@cornerstonejs/core';
await csRenderInit();
2.自定义图像加载器
注册一个自定义图像加载器,协议为 fakeImageLoader:。当遇到以该协议开头的 imageId 时,会调用 registerImageLoader 函数去加载图像
js
import {imageLoader} from '@cornerstonejs/core';
imageLoader.registerImageLoader('fakeImageLoader', registerImageLoader);
3.添加元数据
添加一个元数据提供者(优先级 10000),用于为 fakeImageLoader 协议的图像提供 DICOM 元数据
js
//下面用官方的
metaData.addProvider(fakeMetaDataProvider, 10000);
4.创建渲染引擎并启用视口
js
const renderingEngine = new RenderingEngine(renderingEngineId);
const viewportInput = {
viewportId,
type: ViewportType.STACK,
element: containerRef.value!,
defaultOptions: {
background: [0.2, 0, 0.2] as Types.Point3,
},
} as Types.PublicViewportInput;
renderingEngine.enableElement(viewportInput);
5.获取视口实例
js
const viewport = renderingEngine.getViewport(viewportId) as Types.IStackViewport;
6.构建图像
js
const imageIds = [
`fakeImageLoader:${encodeURIComponent(JSON.stringify({
uri: 'https://www.xx.com/CT.1.2.156.14702.1.1006.128.2.202401270115395626786.dcm'
}))}`,
`fakeImageLoader:${encodeURIComponent(JSON.stringify({
uri: 'https://www.xx.com/CT000000.dcm'
}))}`
];
7.设置堆栈
js
await viewport.setStack(imageIds);
8.预加载所有图像
预加载所有影像,如果不使用则是懒加载
js
imageIds.forEach(async (imageId) => {
try {
// 触发registerImageLoader 去加载图像,并将结果存入缓存
await imageLoader.loadAndCacheImage(imageId);
} catch (error) {
console.error(`预加载图像 ${imageId} 失败:`, error);
}
});
9.渲染视口
js
viewport.render();
10.registerImageLoader函数
registerImageLoader函数是一个自定义图像加载器,当imageIds中包含了该协议就会触发该函数,返回image对象,该对象必须包装为{promise}
js
const promise = fetch()
.then(res=>res.arrayBuffer())
.then(res=>{
const byteArray = new Uint8Array(buffer);
const dataSet = dicomParser.parseDicom(byteArray);
const columns = dataSet.uint16('x00280011') as number;
const rows = dataSet.uint16('x00280010') as number;
// 像素间距安全解析
let rowPixelSpacing = 1,
columnPixelSpacing = 1;
const pixelSpacingRaw = dataSet.string('x00280030');
if (pixelSpacingRaw && typeof pixelSpacingRaw === 'string') {
const parts = pixelSpacingRaw.split('\\');
if (parts.length >= 2) {
rowPixelSpacing = parseFloat(parts[0]);
columnPixelSpacing = parseFloat(parts[1]);
} else if (parts.length === 1) {
rowPixelSpacing = columnPixelSpacing = parseFloat(parts[0]);
}
}
// 窗宽窗位
let windowCenter = dataSet.floatString('x00281050');
let windowWidth = dataSet.floatString('x00281051');
if (isNaN(windowCenter) || isNaN(windowWidth)) {
windowCenter = 40;
windowWidth = 400;
}
// 像素数据元素
const pixelElem = dataSet.elements.x7fe00010;
if (!pixelElem) throw new Error('Missing pixel data');
// 注意:某些 DICOM 文件在像素数据前有 2 个额外字节(如奇偶对齐),尝试自动检测
let dataOffset = pixelElem.dataOffset;
let dataLength = pixelElem.length;
const bitsAllocated = dataSet.uint16('x00280100') || 8;
const pixelRepresentation = dataSet.uint16('x00280103') || 0;
const bytesPerPixel = bitsAllocated / 8;
const expectedLength = columns * rows * bytesPerPixel;
// 如果长度不匹配,尝试调整偏移量(常见偏移 2 或 4 字节)
if (dataLength > expectedLength) {
const excess = dataLength - expectedLength;
if (excess === 2 || excess === 4) {
dataOffset += 2; // 尝试跳过 2 个字节
dataLength = expectedLength;
console.log(`调整像素数据偏移 +2 字节,新长度 ${dataLength}`);
} else if (excess % 2 === 0 && excess <= 8) {
dataOffset += 2; // 通用尝试
dataLength = expectedLength;
console.warn(`长度不匹配,自动偏移 +2,原始长度 ${pixelElem.length}, 期望 ${expectedLength}`);
} else {
console.warn(`长度不匹配且无法自动修正: 实际 ${dataLength}, 期望 ${expectedLength}`);
}
} else if (dataLength < expectedLength) {
console.warn(`像素数据不足: 实际 ${dataLength}, 期望 ${expectedLength}`);
}
const pixelDataRaw = new Uint8Array(buffer, dataOffset, dataLength);
let scalarData: Uint8Array | Uint16Array | Int16Array;
if (bitsAllocated === 16) {
// 确保长度是偶数字节
const adjustedLength = Math.floor(pixelDataRaw.length / 2) * 2;
const uint16Data = new Uint16Array(pixelDataRaw.buffer, pixelDataRaw.byteOffset, adjustedLength / 2);
if (pixelRepresentation === 1) {
scalarData = new Int16Array(uint16Data.buffer);
} else {
scalarData = uint16Data;
}
} else {
scalarData = pixelDataRaw;
}
// 如果最终数据长度仍大于期望值,截断
if (scalarData.length > expectedLength / bytesPerPixel) {
const constructor = scalarData.constructor as any;
const truncated = new constructor(expectedLength / bytesPerPixel);
truncated.set(scalarData.slice(0, expectedLength / bytesPerPixel));
scalarData = truncated;
}
// 计算实际像素值范围
let minVal = Infinity,
maxVal = -Infinity;
for (let i = 0; i < scalarData.length; i++) {
const val = scalarData[i];
if (val < minVal) minVal = val;
if (val > maxVal) maxVal = val;
}
// 创建 VoxelManager
const imageVoxelManager = utilities.VoxelManager.createImageVoxelManager({
height: rows,
width: columns,
numberOfComponents: 1,
scalarData,
});
const image = {
rows,
columns,
width: columns,
height: rows,
imageId,
intercept: 0,
slope: 1,
voxelManager: imageVoxelManager,
invert: false,
minPixelValue: minVal,
maxPixelValue: maxVal,
windowCenter,
windowWidth,
rowPixelSpacing,
columnPixelSpacing,
getPixelData: () => scalarData,
sizeInBytes: rows * columns * 1, // 1 byte for now
FrameOfReferenceUID: 'Stack_Frame_Of_Reference',
// imageFrame: {
// photometricInterpretation: 'RGB',
// },
};
return image;
});
})
return {promise}