Cornerstone3D源码-DICOMLoaderIImage 详解
Cornerstone3D 中的 DICOMLoaderIImage 详解
在 Cornerstone3D 里,从 DICOM 文件到屏幕上的图像,中间会经过解析、解码和封装几步。最终交给渲染和测量使用的,是一个实现 IImage 接口的对象;而当这个对象来自 DICOM 加载器时,其具体类型就是 DICOMLoaderIImage。本文说明 DICOMLoaderIImage 的含义、全部属性(含继承自 IImage 的),以及它和 IImageFrame 的关系,方便在阅读源码或做二次开发时心里有数。
一、图像从哪来、到哪去
Cornerstone3D 的图像大致会经历这样一条管线:
- 获取 DICOM 数据:通过 WADO-URI、WADO-RS 或本地文件拿到 DICOM 字节(P10 文件)。
-
解析与解码:用
dicom-parser把字节解析成DataSet,再按传输语法(Transfer Syntax)解码像素。 -
封装成“图像对象”:把解码后的像素和元数据(窗宽窗位、间距等)封装成一个实现
IImage的对象,放进 core 的缓存、供 Viewport 使用。 - 渲染:core 和 vtk.js 根据 IImage 提供的像素和元数据在 2D/3D 里绘制。
这里的 IImage(图像接口)是 @cornerstonejs/core 定义的通用图像接口,不关心图像来自 DICOM 还是别的来源;DICOMLoaderIImage 则是 @cornerstonejs/dicom-image-loader 在加载 DICOM 时产出的子类型,在满足 IImage 契约之外,额外带上 DICOM 与解码相关的字段。
补充两点:DataSet 是 dicom-parser 解析 DICOM 字节后得到的结构化数据,可以按 tag 读各种元素;传输语法 UID 则决定像素是如何压缩/编码的(如 JPEG、RLE、未压缩等),解码器靠它还原像素。后文第六节会专门对比 IImage 与 IImageFrame 的语义与职责,并给出二者之间的赋值源码。
二、DICOMLoaderIImage 是什么
DICOMLoaderIImage 表示「由 DICOM 加载器创建、并可供 Cornerstone3D 使用的一张单帧图像」。定义在 packages/dicomImageLoader/src/types/DICOMLoaderIImage.ts:
export interface DICOMLoaderIImage extends Types.IImage {
decodeTimeInMS: number;
floatPixelData?: ByteArray | Float32Array;
loadTimeInMS?: number;
totalTimeInMS?: number;
data?: DataSet;
imageFrame?: Types.IImageFrame;
transferSyntaxUID?: string;
}
它继承 Types.IImage,所以拥有 core 需要的全部“图像”能力(尺寸、像素、间距、窗宽窗位、getPixelData、getCanvas 等);同时新增上述 7 个与 DICOM 加载/解码相关的字段。可以简单记成:DICOMLoaderIImage = IImage + DICOM 加载与解码的元数据。
下面先看它自己的属性,再看从 IImage 继承来的属性(不分组,一表列全)。
三、DICOMLoaderIImage 自身属性
| 属性 | 类型 | 必选 | 含义 |
|---|---|---|---|
| decodeTimeInMS | number |
是 | 解码该帧像素所花时间(毫秒),用于性能统计与调试。 |
| floatPixelData | ByteArray | Float32Array |
否 | 浮点像素数据;部分算法或模态(如 PET SUV)需要,若解码时已生成会放这里。 |
| loadTimeInMS | number |
否 | 从开始加载到拿到可解码数据所花时间(毫秒)。 |
| totalTimeInMS | number |
否 | 加载 + 解码的总耗时(毫秒)。 |
| data | DataSet |
否 | 原始 DICOM 解析结果;保留后可再按 tag 读元素,用于测量、标注、导出等。 |
| imageFrame | Types.IImageFrame |
否 | 该图像对应的帧级数据(尺寸、位深、调色板、解码后的 pixelData 等);createImage 会把解码得到的 frame 挂在这里。 |
| transferSyntaxUID | string |
否 | DICOM 传输语法 UID,标识该帧像素的压缩/编码方式。 |
其中 ByteArray、DataSet 来自 dicom-parser,Types.IImageFrame 即 core 的 IImageFrame(后文第六节会说明它和 IImage 的区别)。与 IImage 重叠的字段(如 decodeTimeInMS)在 DICOMLoaderIImage 上以本节定义为准。
四、从 IImage 继承的属性(完整一览)
DICOMLoaderIImage 继承自 Types.IImage,因此下面这些 IImage 上的属性在 DICOMLoaderIImage 上同样存在、含义一致。此处按属性名列出,并标注必选/可选(类型带 ? 或为可选字段)。
| 属性 | 类型 | 必选 | 含义 |
|---|---|---|---|
| imageId | string |
是 | 图像唯一标识(如带 wadouri/wadors 的 URL 或带 frame 的 id),用于缓存与查找。 |
| referencedImageId |
string? |
否 | 若本图由其他图像派生(如重建、融合),指向被引用图像的 imageId。 |
| sharedCacheKey |
string? |
否 | 多帧/多实例共享缓存时的键。 |
| isPreScaled |
boolean? |
否 | 加载时是否已做预缩放(如 Rescale Slope/Intercept 或 SUV)。 |
| preScale |
object? |
否 | 预缩放配置:是否启用、是否已缩放、以及 modality、rescaleSlope、rescaleIntercept、PT suvbw 等。 |
| minPixelValue | number |
是 | 图像最小像素值。 |
| maxPixelValue | number |
是 | 图像最大像素值。 |
| slope | number |
是 | 灰度映射斜率(常为 Modality LUT / Rescale Slope)。 |
| intercept | number |
是 | 灰度映射截距(常为 Rescale Intercept)。 |
| windowCenter | number[] | number |
是 | 窗位(VOI),可多值。 |
| windowWidth | number[] | number |
是 | 窗宽(VOI),可多值。 |
| voiLUTFunction | VOILUTFunctionType |
是 | 窗宽窗位应用的函数类型:LINEAR、SIGMOID、LINEAR_EXACT。 |
| invert | boolean |
是 | 是否反转显示(如 MONOCHROME1)。 |
| modalityLUT |
CPUFallbackLUT? |
否 | CPU 渲染用 Modality LUT。 |
| voiLUT |
CPUFallbackLUT? |
否 | CPU 渲染用 VOI LUT。 |
| getPixelData | () => PixelDataTypedArray |
是 | 返回当前帧像素数组,core 与工具通过它取像素。 |
| getCanvas | () => HTMLCanvasElement |
是 | 返回用于 CPU 渲染的 2D 画布(颜色图常用)。 |
| dataType | PixelDataTypedArrayString |
是 | 像素数组类型名,如 "Int16Array"、"Uint8Array"。 |
| sizeInBytes | number |
是 | 像素数据占用的字节数。 |
| bufferView |
{ buffer: ArrayBuffer; offset: number }? |
否 | 像素在更大 ArrayBuffer 中的视图,用于零拷贝或共享缓冲。 |
| rows | number |
是 | 行数(高度方向像素数)。 |
| columns | number |
是 | 列数(宽度方向像素数)。 |
| height | number |
是 | 显示高度(通常等于 rows)。 |
| width | number |
是 | 显示宽度(通常等于 columns)。 |
| columnPixelSpacing | number |
是 | 列方向像素间距(mm)。 |
| rowPixelSpacing | number |
是 | 行方向像素间距(mm)。 |
| sliceThickness |
number? |
否 | 层厚(mm)。 |
| numberOfComponents | number |
是 | 每像素分量数(1=灰度,3=RGB,4=RGBA)。 |
| color | boolean |
是 | 是否为颜色图。 |
| rgba | boolean |
是 | 是否为 RGBA(含 alpha)。 |
| photometricInterpretation |
string? |
否 | DICOM 光度解释,如 "MONOCHROME2"、"RGB"、"PALETTE COLOR"。 |
| calibration |
IImageCalibration? |
否 | 校准信息:像素间距、比例、类型、提示文案、超声区域等。 |
| scaling |
object? |
否 | 与缩放相关的元数据,如 PT 的 SUV 相关因子。 |
| FrameOfReferenceUID |
string? |
否 | DICOM Frame of Reference UID,用于空间配准与多序列对齐。 |
| render |
function? |
否 | CPU 回退时的自定义绘制函数。 |
| stats |
object? |
否 | 上次渲染、LUT 生成、像素存取等耗时统计。 |
| cachedLut |
object? |
否 | 缓存的 LUT(windowWidth/Center、invert、lutArray、modalityLUT、voiLUT)。 |
| colormap |
CPUFallbackColormap? |
否 | CPU 伪彩色映射。 |
| imageQualityStatus |
ImageQualityStatus? |
否 | 帧的质量等级,数值越大越接近无损;用于渐进式加载或替换低质帧。 |
| imageFrame |
IImageFrame? |
否 | 帧级数据;在 IImage 上可选,DICOMLoaderIImage 中会挂 createImage 产出的 frame。 |
| voxelManager |
IVoxelManager<number> | IVoxelManager<RGB>? |
否 | 按坐标访问/采样体素的管理器。 |
| loadTimeInMS |
number? |
否 | 加载耗时(毫秒)。 |
| decodeTimeInMS |
number? |
否 | 解码耗时(毫秒);在 IImage 上可选,在 DICOMLoaderIImage 上为必选并覆盖。 |
相关子类型简要说明:IImageFrame 描述单帧的尺寸、位深、调色板、pixelData、transferSyntax 等;VOILUTFunctionType 为窗宽窗位函数枚举(LINEAR、SIGMOID、LINEAR_EXACT);ImageQualityStatus 表示损失程度/分辨率等级,数值越大越无损;IImageCalibration 为校准信息;PixelDataTypedArray 为像素数组联合类型,PixelDataTypedArrayString 为其类型名字符串。
五、为什么 IImage 和 DICOMLoaderIImage 都定义了 imageFrame?
两边都写了 imageFrame?: IImageFrame,类型相同、都是可选,并不是在“覆盖”或“重写”属性,而是同一属性在不同层级上的语义区分。
在 IImage 里,imageFrame 是可选的,因为 IImage 要兼容各种来源(DICOM、内存、Canvas、其他 loader),很多来源没有“帧”的概念,所以基类只表示“有的图像会带帧数据”。
在 DICOMLoaderIImage 里再次声明,是为了在类型层面明确:由 DICOM 加载器产出的图像会携带帧级数据;createImage 在构造时会把解码得到的 frame 挂上去,运行时通常都有值,但类型上仍保持可选,以便和 IImage 一致。
在 core 里,缓存/清理时可能会访问 image.imageFrame(例如 delete image.imageFrame.pixelData 以释放像素引用),而 StackViewport 等用 image?.imageFrame 做可选链访问,以兼容没有 imageFrame 的图像。
总结:两处定义的是同一个属性,基类表示“可选、部分图像有”,子类再次列出表示“DICOM 加载器会填这个字段”,更多是语义与文档上的强调。
六、IImageFrame 与 IImage 的语义与功能区别
两者有不少重复字段(如 rows、columns、min/max、preScale、decodeTimeInMS、photometricInterpretation),是因为所处阶段和职责不同,重复是有意为之。
IImageFrame 处于解码阶段:由 getImageFrame() 从元数据填一部分,再由 decodeImageFrame() 填像素等,在 loader 包内流动。它贴近 DICOM/解码结果,包含 bitsAllocated、bitsStored、pixelRepresentation、调色板、pixelData、transferSyntax、decodeLevel 等,使用 DICOM 用语(smallestPixelValue / largestPixelValue)。
IImage 处于运行时阶段:由 createImage() 用已解码的 IImageFrame 拼出来,作为缓存的条目和 Viewport/工具消费的“图像”。它贴近显示与测量,提供 getPixelData()、getCanvas()、窗宽窗位、几何与校准等,使用 minPixelValue / maxPixelValue,并可选地持有 imageFrame 以便访问原始帧。
之所以在 IImage 上再保留一份 rows、min/max 等,是因为 core 的 cache、Viewport、工具只依赖 IImage,不依赖“一定有 imageFrame”;很多图像来源没有 frame,所以 IImage 需要自包含,调用方用 image.rows、image.minPixelValue 即可。在 DICOM 路径下,createImage 从同一个 IImageFrame 拷贝这些值到 IImage,并把该 frame 挂到 image.imageFrame,所以同一份信息会同时出现在 frame 和 image 上。
简言之:Frame = 解码管线里的“一帧原始数据”;Image = 对外暴露的“一张可显示/可测量的图像”。重复不是为了冗余,而是让 IImage 作为对外契约能独立使用,IImageFrame 则保留解码与格式细节;frame 驱动解码与构造,image 驱动缓存与渲染。
IImageFrame 与 IImage 之间的赋值:源码说明
1. IImageFrame 的创建与填充
帧对象先由元数据构造出“壳”,再交给解码器填像素。在 createImage 里(packages/dicomImageLoader/src/imageLoader/createImage.ts):
const imageFrame = getImageFrame(imageId); // 从 metaData 得到 imagePixelModule,构造初始 IImageFrame
imageFrame.decodeLevel = options.decodeLevel;
// ...
const decodePromise = decodeImageFrame(
imageFrame,
transferSyntax,
pixelData,
canvas,
options,
decodeConfig
);
getImageFrame(packages/dicomImageLoader/src/imageLoader/getImageFrame.ts)根据 imageId 从 metaData 取出 imagePixelModule,返回一个只含像素描述(rows、columns、bitsAllocated、调色板等)、pixelData 仍为 undefined 的 IImageFrame。随后 decodeImageFrame 在同一对象上写入 pixelData、smallestPixelValue、largestPixelValue、preScale、decodeTimeInMS 等,并 resolve 该 imageFrame。
2. 从 IImageFrame 赋到 IImage(createImage)
解码完成后,在 decodePromise.then 里用同一个 imageFrame 构造 DICOMLoaderIImage,既把帧上的字段拷贝到 image,又把帧引用挂到 image.imageFrame(见 createImage.ts):
decodePromise.then(function (imageFrame: Types.IImageFrame) {
// ...
const image: DICOMLoaderIImage = {
imageId,
dataType: imageFrame.pixelData.constructor.name as Types.PixelDataTypedArrayString,
columns: imageFrame.columns,
height: imageFrame.rows,
rows: imageFrame.rows,
width: imageFrame.columns,
preScale: imageFrame.preScale,
minPixelValue: imageFrame.smallestPixelValue,
maxPixelValue: imageFrame.largestPixelValue,
sizeInBytes: imageFrame.pixelData.byteLength,
decodeTimeInMS: imageFrame.decodeTimeInMS,
imageFrame, // 整帧引用挂到 image 上
getPixelData: () => imageFrame.pixelData,
// ... 其余来自 imagePlaneModule、voiLutModule、modalityLutModule 等
};
// ...
});
未提供窗宽窗位时,会用 image.imageFrame 上的最小/最大像素值算默认窗宽窗位:
if (image.windowCenter === undefined || image.windowWidth === undefined) {
const windowLevel = utilities.windowLevel.toWindowLevel(
image.imageFrame.smallestPixelValue,
image.imageFrame.largestPixelValue
);
image.windowWidth = windowLevel.windowWidth;
image.windowCenter = windowLevel.windowCenter;
}
3. core 侧对 image.imageFrame 的使用
图像进入 core 后,若需要补建 voxelManager(例如原本没有 voxelManager 的 loader),会在 ensureVoxelManager 里用 image.getPixelData() 创建 voxelManager,并删除 frame 上的 pixelData 引用以释放内存(packages/core/src/loaders/imageLoader.ts):
function ensureVoxelManager(image: IImage): void {
if (!image.voxelManager) {
const voxelManager = VoxelManager.createImageVoxelManager({
scalarData: image.getPixelData(),
width: image.width,
height: image.height,
numberOfComponents: image.numberOfComponents,
});
image.voxelManager = voxelManager;
image.getPixelData = () => voxelManager.getScalarData() as PixelDataTypedArray;
delete image.imageFrame.pixelData; // 释放对原始 pixelData 的引用
}
}
当前 core 实现中并未对 image.imageFrame 做存在性判断,实际调用链里由 DICOM loader(createImage)保证会挂上 imageFrame;若自定义 loader 不提供 imageFrame,在走 ensureVoxelManager 前需自行做存在性判断或避免调用会访问 imageFrame 的逻辑。
七、DICOMLoaderIImage 的用途
-
作为 loadImage / createImage 的返回类型:
wadouri/loadImage、wadors/loadImage和createImage在完成 DICOM 解析与像素解码后会构造并返回 DICOMLoaderIImage(或个别路径先返回 IImageFrame 再封装),调用方将该对象交给 core。 - 注入 core 缓存与 Viewport:core 的缓存和 StackViewport 接收实现 IImage 的对象,DICOMLoaderIImage 可直接被缓存并用于 2D 显示、测量、窗宽窗位、多帧滚动等。
- 保留 DICOM 上下文:通过 data(DataSet)、transferSyntaxUID、imageFrame 等,后续仍可访问原始 DICOM 元素与帧级信息,用于测量、标注、导出或高级渲染(如浮点像素、SUV 显示)。
- 性能与质量决策:decodeTimeInMS、loadTimeInMS、totalTimeInMS 用于监控加载性能;imageQualityStatus 用于渐进式加载或用高质帧替换低质帧。
整体上,DICOMLoaderIImage 就是在 IImage 基础上、专门表示“由 DICOM 加载器产生”的图像类型,并带有 DICOM 与解码相关的扩展字段,贯穿从 DICOM 到 Cornerstone3D 渲染的整条管线。若你做自定义 loader(例如从非 DICOM 源加载图像)并希望产出的对象能被 core 缓存与 Viewport 使用,需要实现 IImage 接口(至少提供 imageId、getPixelData、getCanvas、尺寸与窗宽窗位等必选字段);若也要保留“帧”级原始数据供清理或高级用法,可像 DICOMLoaderIImage 一样可选挂载 imageFrame。
相关文件
packages/dicomImageLoader/src/types/DICOMLoaderIImage.tspackages/core/src/types/IImage.tspackages/core/src/types/IImageFrame.tspackages/dicomImageLoader/src/imageLoader/createImage.tspackages/dicomImageLoader/src/imageLoader/getImageFrame.tspackages/core/src/loaders/imageLoader.ts