概览
本文按阶段梳理 Cornerstone3D 中从 原始 DICOM 二进制数据 到 屏幕上最终图像 的完整调用链。
涉及的主要模块:
@cornerstonejs/dicom-image-loader:负责从 WADO‑URI / WADO‑RS / 本地文件获取 DICOM,并解码像素dicom-parser:解析 DICOM 字节为DataSet,并帮助处理 encapsulated 像素数据@cornerstonejs/core:管理 Image / Volume / 缓存 / Viewport- vtk.js:负责 GPU 渲染(2D
vtkImageSlice,3DvtkVolume)
下面以 WADO‑URI / WADO‑RS 为主线说明(其他来源类似)。
1. 获取 DICOM 二进制
单张图像的入口通常是 DICOM Image Loader 的 loadImage(imageId, options)。 对于 wadouri: / dicomweb: schema,会走到:
43:70:packages/dicomImageLoader/src/imageLoader/wadouri/loadImage.ts
function loadImageFromPromise(
dataSetPromise: Promise<DataSet>,
imageId: string,
frame = 0,
sharedCacheKey: string,
options: DICOMLoaderImageOptions,
callbacks?
): Types.IImageLoadObject {
// ...
dataSetPromise.then(
(dataSet /* , xhr*/) => {
const pixelData = getPixelData(dataSet, frame);
const transferSyntax = dataSet.string('x00020010');
const imagePromise = createImage(
imageId,
pixelData,
transferSyntax,
options
);
// ...
}
);
}
这里的 dataSetPromise 来自于具体 scheme 对应的 loader:
wadouri:/dicomweb:→xhrRequest(通过 HTTP 请求 DICOMweb 接口)dicomfile:→loadFileRequest(本地文件)
二者最终都会拿到一个 DICOM P10 文件的 ArrayBuffer。
2. 用 dicom-parser 解析为 DataSet
拿到原始字节后,由 dataSetCacheManager 使用 dicom-parser 解析为 DataSet, 并在本地做缓存,避免重复下载和重复解析:
1:8:packages/dicomImageLoader/src/imageLoader/wadouri/dataSetCacheManager.ts
import type { DataSet } from 'dicom-parser';
import * as dicomParser from 'dicom-parser';
// ...
// 某处:
const dataSet = dicomParser.parseDicom(byteArray);
到这一步为止:
- 原始 DICOM 字节 →
Uint8Array→dicomParser.DataSet
接下来基于 DataSet 取出像素数据。
3. 从 DataSet 中提取像素 ByteArray(支持压缩与非压缩)
像素提取由 getPixelData(dataSet, frameIndex) 完成:
5:18:packages/dicomImageLoader/src/imageLoader/wadouri/getPixelData.ts
function getPixelData(dataSet: DataSet, frameIndex = 0): ByteArray {
const pixelDataElement =
dataSet.elements.x7fe00010 || dataSet.elements.x7fe00008;
if (!pixelDataElement) {
return null;
}
if (pixelDataElement.encapsulatedPixelData) {
return getEncapsulatedImageFrame(dataSet, frameIndex);
}
return getUncompressedImageFrame(dataSet, frameIndex);
}
对于 encapsulated(封装/压缩)像素数据 ,会进一步调用 getEncapsulatedImageFrame,内部大量使用 dicom‑parser 提供的工具函数:
1:22:packages/dicomImageLoader/src/imageLoader/wadouri/getEncapsulatedImageFrame.ts
import type { ByteArray, DataSet } from 'dicom-parser';
import * as dicomParser from 'dicom-parser';
export default function getEncapsulatedImageFrame(
dataSet: DataSet,
frameIndex: number
): ByteArray {
if (
dataSet.elements.x7fe00010 &&
dataSet.elements.x7fe00010.basicOffsetTable.length
) {
return dicomParser.readEncapsulatedImageFrame(
dataSet,
dataSet.elements.x7fe00010,
frameIndex
);
}
const basicOffsetTable = dicomParser.createJPEGBasicOffsetTable(
dataSet,
dataSet.elements.x7fe00010
);
return dicomParser.readEncapsulatedImageFrame(
dataSet,
dataSet.elements.x7fe00010,
frameIndex,
basicOffsetTable
);
}
到这一步为止:
DataSet→ 按帧(frame)获取到 像素 ByteArray(可能是压缩的,也可能已是未压缩)。
4. 按 Transfer Syntax 解码像素并构建 Image 对象
4.1 createImage:从 ByteArray 到 Cornerstone Image
createImage 是 ByteArray + Transfer Syntax → Cornerstone Image 的核心桥梁:
24:33:packages/dicomImageLoader/src/imageLoader/createImage.ts
function createImage(
imageId: string,
pixelData: ByteArray,
transferSyntax: string,
options: DICOMLoaderImageOptions = {}
): Promise<DICOMLoaderIImage | Types.IImageFrame> {
// ...
const canvas = document.createElement('canvas');
const imageFrame = getImageFrame(imageId);
// ...
const decodePromise = decodeImageFrame(
imageFrame,
transferSyntax,
pixelData,
canvas,
options,
decodeConfig
);
// ...
}
4.2 decodeImageFrame:根据 Transfer Syntax 选择解码路径
真正的解码逻辑在 decodeImageFrame 中,根据 Transfer Syntax UID 分发到不同 codec, 大多数情况通过 WebWorker + 对应编解码库完成:
49:66:packages/dicomImageLoader/src/imageLoader/decodeImageFrame.ts
function decodeImageFrame(
imageFrame,
transferSyntax,
pixelData,
canvas,
options = {},
decodeConfig
) {
switch (transferSyntax) {
case '1.2.840.10008.1.2': // Implicit VR Little Endian
case '1.2.840.10008.1.2.1': // Explicit VR Little Endian
// ...
return processDecodeTask(
imageFrame,
transferSyntax,
pixelData,
options,
decodeConfig
);
// 其他如 RLE、JPEG‑LS、JPEG2000/HTJ2K 等也在这里路由
}
}
Worker 返回时,已经得到解码后的 imageFrame.pixelData,类型是适当的 TypedArray (例如 Uint8Array、Int16Array、Float32Array 等)。
4.3 在 createImage 中应用缩放、颜色转换和元数据
decodePromise 解析完成后,createImage 还会做几件关键事情:
- 根据元数据(Rescale Slope/Intercept、Modality LUT 等)做 预缩放(preScale)
- 视需要做 颜色空间转换(YBR→RGB 等)
- 根据情况调用
setPixelDataType设置最终像素类型 - 通过 core 的
metaData读取:IMAGE_PLANE模块(空间方向、像素间距等)VOI_LUT模块(窗宽窗位)MODALITY_LUT模块(模态 LUT)
- 把上述信息封装成
DICOMLoaderIImage,实现@cornerstonejs/core的Types.IImage接口
到这个阶段为止:
- 像素 ByteArray → 解码为
TypedArray像素 + 元数据 → Cornerstone Image 对象
5. 从 Image 到 core 缓存与 Viewport
5.1 Stack(2D 翻页)路径
对于 Stack(2D 多帧)场景:
- Image 通过 core 的 imageLoader 与 cache 进入缓存;
- 调用
StackViewport.setStack(imageIds)时:- 从缓存中根据当前索引取出
Image - 利用其
voxelManager/ 像素数组构造vtkImageData:
- 从缓存中根据当前索引取出
1751:1768:packages/core/src/RenderingEngine/StackViewport.ts
createVTKImageData({
origin,
direction,
dimensions,
spacing,
numberOfComponents,
pixelArray,
}) {
const values = new pixelArray.constructor(pixelArray.length);
const scalarArray = vtkDataArray.newInstance({
name: 'Pixels',
numberOfComponents: numberOfComponents,
values: values,
});
const imageData = vtkImageData.newInstance();
imageData.setDimensions(dimensions);
imageData.setSpacing(spacing);
imageData.setDirection(direction);
imageData.setOrigin(origin);
imageData.getPointData().setScalars(scalarArray);
return imageData;
}
- 再通过
vtkImageMapper + vtkImageSlice创建 Actor,加入当前 Viewport 的vtkRenderer:
629:642:packages/core/src/RenderingEngine/StackViewport.ts
private createActorMapper = (imageData: vtkImageData) => {
const mapper = vtkImageMapper.newInstance();
mapper.setInputData(imageData);
const actor = vtkImageSlice.newInstance();
actor.setMapper(mapper);
// ...
return actor;
};
5.2 Volume(3D 体)路径
对于 3D 体数据(多帧 DICOM 序列):
- 调用
volumeLoader.createAndCacheVolume(volumeId, { imageIds }):- 内部批量调用上面的
loadImage/createImage - 聚合成一个
ImageVolume:- 内含
vtkImageData(维度、spacing、origin、direction 等) - 内含
vtkStreamingOpenGLTexture(用于把体素数据流式上传到 GPU)
- 内含
- 内部批量调用上面的
154:178:packages/core/src/cache/classes/ImageVolume.ts
if (!imageData) {
imageData = vtkImageData.newInstance();
imageData.setDimensions(dimensions);
imageData.setSpacing(spacing);
imageData.setDirection(direction);
imageData.setOrigin(origin);
}
imageData.set(
{
dataType: dataType,
voxelManager: this.voxelManager,
id: volumeId,
numberOfComponents: numberOfComponents || 1,
},
this.suppressWarnings
);
this.imageData = imageData;
this.vtkOpenGLTexture = vtkStreamingOpenGLTexture.newInstance();
this.vtkOpenGLTexture.setVolumeId(volumeId);
VolumeViewport.setVolumes([{ volumeId }])中通过createVolumeActor创建vtkVolumeActor:
44:73:packages/core/src/RenderingEngine/helpers/createVolumeActor.ts
const imageVolume = await loadVolume(volumeId);
const { imageData, vtkOpenGLTexture } = imageVolume;
const volumeMapper = createVolumeMapper(imageData, vtkOpenGLTexture);
const volumeActor = vtkVolume.newInstance();
volumeActor.setMapper(volumeMapper);
其中 createVolumeMapper 使用自定义的 vtkSharedVolumeMapper, 通过共享 vtkStreamingOpenGLTexture 在多 viewport 间复用 GPU 纹理。
6. RenderingEngine + vtk.js 渲染到 Canvas
所有 Viewport(Stack / Volume / Video 等)都挂在同一个 RenderingEngine 实例下:
- 内部有一个离屏的
vtkRenderWindow(以及一个vtkStreamingOpenGLRenderWindow) - 每个 viewport 对应一个
vtkRenderer(id 为 viewportId)
向 Viewport 添加 Actor 时,本质上是添加到对应的 vtk renderer:
681:702:packages/core/src/RenderingEngine/Viewport.ts
public addActor(actorEntry: ActorEntry): void {
const { uid: actorUID, actor } = actorEntry;
const renderingEngine = this.getRenderingEngine();
// ...
const renderer = this.getRenderer();
renderer?.addActor(actor as vtkActor);
this._actors.set(actorUID, Object.assign({}, actorEntry));
// ...
}
当应用调用 renderingEngine.render()(或 renderViewport(viewportId))时:
BaseRenderingEngine使用requestAnimationFrame安排在下一帧渲染- 对于需要渲染的每个 viewport:
- 取其对应的
vtkRenderer,设置在离屏 renderWindow 中的viewport区域 - 调用
renderWindow.render(),交由 vtk.js 完成 GPU 绘制(包括 image slice、volume 等全部 Actor)
- 取其对应的
- 渲染完成后,将离屏 WebGL canvas 中对应区域拷贝到该 viewport 对应的页面
<canvas>上
到这一步为止,最初的 DICOM 二进制数据已经变成了屏幕上的像素图像。
7. 整体调用链小结
以典型的 WADO‑URI 单帧 DICOM 为例,可以将整个链路概括为:
- 获取字节 :HTTP / 文件 →
ArrayBuffer - 解析 DICOM :
dicomParser.parseDicom→DataSet - 提取帧像素 :
getPixelData→ByteArray(按 frame) - 按 Transfer Syntax 解码 :
createImage→decodeImageFrame→ 解码后的TypedArray像素 - 构建 Cornerstone Image :
DICOMLoaderIImage(像素 + Modality/VOI/Plane 等元数据) - 缓存 / 体构建 :
ImageVolume/StreamingImageVolume(包含vtkImageData+vtkStreamingOpenGLTexture) - 构建 vtk 数据结构 :
- Stack:
vtkImageData+vtkImageSlice - Volume:
vtkImageData+vtkStreamingOpenGLTexture+vtkVolume
- Stack:
- 渲染 :
RenderingEngine+ vtk.js(vtkRenderWindow+vtkRenderer)→ 离屏 canvas → 拷贝到页面 canvas
这条管线清晰地区分了三个层次的职责:
- 解析/解码层:dicom‑parser + 各类 codec,负责从 DICOM 字节到像素数组
- 成像语义层:Cornerstone3D core,负责 Image/Volume 抽象、元数据和 viewport 管理
- 渲染层:vtk.js,负责在 GPU 上进行 2D/3D 渲染并输出到 canvas