使用 vtk.js 读取 DICOM 序列并进行 3D 体绘制重建

作者:页面旅途

时间:2026-11-03

浏览:447

详细讲解如何在 Web 前端使用 vtk.js 与 itk-wasm 读取医学 DICOM 切片序列,并基于体绘制(Volume Rendering)技术完成三维体重建、光线投射配置与传递函数调优。

文章目录
在 Web 浏览器中直接实现医学影像的三维重建,关键在于解决两个核心环节:利用 WebAssembly 库(如 itk-wasm)在前端高效解析多张 2D DICOM 切片并恢复准确的空间几何关系,再将其交给基于 WebGL 的 vtk.js 构建体绘制(Volume Rendering)管线,通过光学传递函数将体素的 CT 值直接映射为颜色与不透明度。本文完整演示从切片数据加载、标量场组装到体绘制视口渲染的实现全过程。
使用 vtk.js 读取 DICOM 序列并进行 3D 体绘制重建
图 1:从 DICOM 切片序列解析到 vtk.js 3D 体绘制重建的整体架构路线

一、技术原理与依赖准备

在传统桌面端医学图像处理中,VTK (C++) 与 ITK 已经成为成熟的标准工具链。在现代 Web 端,Kitware 官方推出了 vtk.js 与 itk-wasm。二者有着清晰的分工:

  • itk-wasm:负责底层复杂的医学图像解码。DICOM 包含复杂的头信息字典、压缩格式(如 JPEG Baseline, JPEG-LS, RLE)、非等间距切片校验以及空间坐标系变换。itk-wasm 将 C++ 的 ITK 核心编译为 WebAssembly,可在浏览器内高效完成切片序列排布与体素数组还原。
  • vtk.js:专精于 WebGL / WebGPU 场景下的三维渲染。它提供完整的流水线机制(Pipeline),其中的体绘制映射器(vtkVolumeMapper)通过在片元着色器中执行光线投射算法(GPU Ray Casting),实时计算光线穿透三维体素时的光学积分。

在前端项目中,使用 npm 安装核心依赖包:

npm install @kitware/vtk.js itk-wasm
环境提示: itk-wasm 依赖 WebAssembly 与 Web Worker 运行环境。在 Vite 或 Webpack 构建配置中,需要确保将 itk-wasm 提供的 itk-wasm/dist/pipeline/ 相关的 .wasm 与 .js worker 静态文件正确暴露给静态资源目录,或通过官方推荐的公共 CDN 地址进行分发配置。
使用 vtk.js 读取 DICOM 序列并进行 3D 体绘制重建
图 2:使用 vtk.js 与 itk-wasm 重建 DICOM 序列的标准操作阶段图

二、读取 DICOM 序列并组装为 vtkImageData

DICOM(Digital Imaging and Communications in Medicine)序列通常由几十到上百个 .dcm 单切片文件构成。为了重建三维体,必须根据各切片中记录的 ImagePositionPatient(切片在病人空间坐标系中的原点)和 ImageOrientationPatient(切片方向余弦)计算切片间距并进行严格排序,否则重建出的体积会发生错位、倒置或拉伸。

itk-wasm 的 readImageDICOMFileSeries 函数内部自动处理了上述复杂的排序与方向余弦校正。它接收一个原生的 File 数组,解析后输出包含空间维度信息(Dimensions、Spacing、Origin、Direction)和体素标量数组的 ITK Image 对象。

import { readImageDICOMFileSeries } from 'itk-wasm';
import vtkITKHelper from '@kitware/vtk.js/Common/DataModel/ITKHelper';

/**
 * 读取 DICOM 文件序列并转换为 vtkImageData
 * @param {File[]} fileList - 用户选择的 DICOM 切片文件列表
 * @returns {Promise<vtkImageData>}
 */
async function loadDicomSeries(fileList) {
    if (!fileList || fileList.length === 0) {
        throw new Error('未提供有效的 DICOM 文件列表');
    }

    // 调用 itk-wasm 在 Web Worker 中解析序列
    // readImageDICOMFileSeries 内部会提取空间元数据并按空间轴排序
    const { image: itkImage } = await readImageDICOMFileSeries(fileList);

    // 利用 vtkITKHelper 将 ITK 内部图像对象桥接为 vtk.js 能够识别的 vtkImageData
    const vtkImage = vtkITKHelper.convertItkToVtkImage(itkImage);

    // 验证体积参数输出
    const dims = vtkImage.getDimensions();
    const spacing = vtkImage.getSpacing();
    console.log(`体数据网格尺寸: ${dims[0]} x ${dims[1]} x ${dims[2]}`);
    console.log(`体素物理间距 (mm): x=${spacing[0]}, y=${spacing[1]}, z=${spacing[2]}`);

    return vtkImage;
}

经 vtkITKHelper.convertItkToVtkImage 转换后得到的 vtkImageData 是一个标准的规则体素标量网格,其标量类型通常为 Int16Array 或 Float32Array,数值直接对应真实的 CT Hounsfield Unit (HU) 标量。

使用 vtk.js 读取 DICOM 序列并进行 3D 体绘制重建
图 3:vtk.js 体渲染的核心管道组件与属性关联架构图

三、构建 vtk.js 体渲染管线与传递函数映射

体绘制(Volume Rendering)不同于传统由三角网格构成的面绘制(Surface Rendering)。面绘制只渲染物体的外部几何外壳,而体绘制是将整个三维体素标量场作为一个“半透明的发光吸收介质”。显卡通过光线投射(Ray Casting),沿视线从前到后步进采样,并对体素的不透明度与颜色进行数值积分合成。

要让体绘制展现出符合解剖学特征的形态,核心就在于传递函数(Transfer Functions)的精确配置:

  • 颜色传递函数(vtkColorTransferFunction):在不同的标量值(HU)位置插入 RGB 关键点,vtk.js 在渲染时会自动进行线性颜色插值。
  • 标量不透明度传递函数(vtkPiecewiseFunction):决定特定标量值区间的体素是不透明还是完全透明。例如,将空气(-1000 HU)和脂肪(-100 HU)的不透明度设为 0,即可将其滤除,只保留骨骼(> 300 HU)或充盈造影剂的血管。

配置体渲染管道的完整代码如下:

import vtkVolume from '@kitware/vtk.js/Rendering/Core/Volume';
import vtkVolumeMapper from '@kitware/vtk.js/Rendering/Core/VolumeMapper';
import vtkColorTransferFunction from '@kitware/vtk.js/Rendering/Core/ColorTransferFunction';
import vtkPiecewiseFunction from '@kitware/vtk.js/Common/DataModel/PiecewiseFunction';

/**
 * 构建体绘制 Actor 并配置针对骨骼渲染的传递函数
 * @param {vtkImageData} vtkImage - 准备好的三维体素数据
 * @returns {vtkVolume}
 */
function createVolumePipeline(vtkImage) {
    // 1. 创建体绘制映射器 (VolumeMapper)
    const mapper = vtkVolumeMapper.newInstance();
    mapper.setInputData(vtkImage);

    // 设置采样间距:数值越小精度越高,但会增加片元着色器采样负担
    // 建议默认与体素最小间距相当
    const spacing = vtkImage.getSpacing();
    const minSpacing = Math.min(spacing[0], spacing[1], spacing[2]);
    mapper.setSampleDistance(minSpacing);

    // 2. 创建体绘制 Actor
    const actor = vtkVolume.newInstance();
    actor.setMapper(mapper);

    // 3. 配置颜色传递函数 (CT 值 -> RGB 映射)
    const cfun = vtkColorTransferFunction.newInstance();
    cfun.addRGBPoint(-1000, 0.0, 0.0, 0.0);   // 空气:黑色
    cfun.addRGBPoint(-400,  0.4, 0.2, 0.1);   // 肺部/低密组织:浅褐色
    cfun.addRGBPoint(100,   0.8, 0.5, 0.4);   // 软组织/肌肉:肉色
    cfun.addRGBPoint(400,   0.9, 0.85, 0.7);  // 松质骨:象牙黄
    cfun.addRGBPoint(1200,  1.0, 1.0, 1.0);   // 皮质骨:高亮白色

    // 4. 配置不透明度传递函数 (CT 值 -> Alpha 映射)
    const ofun = vtkPiecewiseFunction.newInstance();
    ofun.addPoint(-1000, 0.0);   // 空气完全透明
    ofun.addPoint(100,   0.0);   // 滤除低于 100 HU 的大部分软组织背景
    ofun.addPoint(300,   0.15);  // 开始显现浅层骨质轮廓
    ofun.addPoint(600,   0.65);  // 高密骨质半透明
    ofun.addPoint(1200,  0.95);  // 坚硬骨质高不透明度

    // 5. 绑定传递函数与材质属性
    const property = actor.getProperty();
    property.setRGBTransferFunction(0, cfun);
    property.setScalarOpacity(0, ofun);
    property.setInterpolationTypeToLinear(); // 开启三线性插值,平滑体素锯齿

    // 配置光照参数,增强三维立体感
    property.setShade(true);
    property.setAmbient(0.2);
    property.setDiffuse(0.7);
    property.setSpecular(0.3);
    property.setSpecularPower(8.0);

    return actor;
}
使用 vtk.js 读取 DICOM 序列并进行 3D 体绘制重建
图 4:CT Hounsfield 标量区间与片元光线投射合成原理图

四、完整端到端重建示例代码

以下提供一个包含 DOM 初始化、文件读取、管线搭建、相机视角重置及三维鼠标交互的完整模块化实现:

import '@kitware/vtk.js/Rendering/Profiles/Volume'; // 自动注册体绘制着色器
import vtkFullScreenRenderWindow from '@kitware/vtk.js/Rendering/Misc/FullScreenRenderWindow';
import { readImageDICOMFileSeries } from 'itk-wasm';
import vtkITKHelper from '@kitware/vtk.js/Common/DataModel/ITKHelper';
import vtkVolume from '@kitware/vtk.js/Rendering/Core/Volume';
import vtkVolumeMapper from '@kitware/vtk.js/Rendering/Core/VolumeMapper';
import vtkColorTransferFunction from '@kitware/vtk.js/Rendering/Core/ColorTransferFunction';
import vtkPiecewiseFunction from '@kitware/vtk.js/Common/DataModel/PiecewiseFunction';

/**
 * 主执行函数:在指定的 DOM 容器内完成 DICOM 序列加载与 3D 体重建
 * @param {HTMLElement} container - 渲染画布容器
 * @param {File[]} dicomFiles - 用户选中的 DICOM 切片文件集合
 */
export async function initDicomVolumeViewer(container, dicomFiles) {
    // 1. 初始化全屏或容器渲染窗口
    const fullScreenRenderer = vtkFullScreenRenderWindow.newInstance({
        rootContainer: container,
        background: [0.09, 0.20, 0.30] // 背景深海蓝 (#17324D)
    });
    const renderer = fullScreenRenderer.getRenderer();
    const renderWindow = fullScreenRenderer.getRenderWindow();

    try {
        console.log('正在解析 DICOM 序列...');
        // 2. 解码并重组序列为 ITK 图像
        const { image: itkImage } = await readImageDICOMFileSeries(dicomFiles);

        // 3. 转成 vtkImageData
        const vtkImage = vtkITKHelper.convertItkToVtkImage(itkImage);

        // 4. 创建 Mapper
        const mapper = vtkVolumeMapper.newInstance();
        mapper.setInputData(vtkImage);
        
        // 动态根据数据体素间距设置光线采样间距
        const spacing = vtkImage.getSpacing();
        mapper.setSampleDistance(Math.min(...spacing));

        // 5. 创建 Actor 与设置骨骼预设传递函数
        const actor = vtkVolume.newInstance();
        actor.setMapper(mapper);

        const cfun = vtkColorTransferFunction.newInstance();
        cfun.addRGBPoint(-1000, 0.0, 0.0, 0.0);
        cfun.addRGBPoint(100, 0.7, 0.4, 0.3);
        cfun.addRGBPoint(400, 0.9, 0.82, 0.75);
        cfun.addRGBPoint(1000, 1.0, 1.0, 1.0);

        const ofun = vtkPiecewiseFunction.newInstance();
        ofun.addPoint(-1000, 0.0);
        ofun.addPoint(150, 0.0);
        ofun.addPoint(400, 0.45);
        ofun.addPoint(1000, 0.90);

        const prop = actor.getProperty();
        prop.setRGBTransferFunction(0, cfun);
        prop.setScalarOpacity(0, ofun);
        prop.setInterpolationTypeToLinear();
        prop.setShade(true);
        prop.setAmbient(0.25);
        prop.setDiffuse(0.65);
        prop.setSpecular(0.4);

        // 6. 添加到视口并更新相机
        renderer.addVolume(actor);
        renderer.resetCamera();

        // 7. 执行首帧渲染
        renderWindow.render();
        console.log('3D 体绘制重建完成');

        return {
            renderer,
            renderWindow,
            actor,
            mapper,
            destroy: () => fullScreenRenderer.delete()
        };
    } catch (err) {
        console.error('DICOM 重建失败:', err);
        throw err;
    }
}

五、工程实践中的关键问题与优化策略

在将 Web 端 DICOM 3D 体重建投入实际业务(如远程问诊、手术规划、教学演示)时,通常会遇到以下三个现实挑战:

1. 显存消耗与多精度降采样

典型的增强胸腹部 CT 序列可能包含 500 到 1000 张切片,完整体素数据高达 512×512×1000,解压后的未压缩内存占用超过 500MB。在移动端浏览器或低配集成显卡上,极易触发 WebGL 显存溢出或掉帧。

应对措施:在进入 vtkImageData 之前,可以利用 itk-wasm 对体素进行双线性或三线性降采样(如将 XY 分辨率从 512 降到 256),使体数据体积缩减至原始的四分之一;同时在体绘制映射器上开启 mapper.setAutoAdjustSampleDistances(true),使得在用户拖拽鼠标旋转视角时自动拉大采样步长,静止后恢复高精采样,兼顾交互流畅度与静止清晰度。

2. 切片间距(Slice Thickness vs Spacing Between Slices)不均匀

部分临床 CT 设备输出的切片厚度与切片中心物理间距并不相等,或者床位移动速率发生变化导致 Z 轴间距非等距。

应对措施:由于 vtkImageData 要求三维方向的间距必须为常数向量(Uniform Grid),若遇变间距数据,需先在 ITK 层面执行 ResampleImageFilter 沿 Z 轴重采样为规则网格,避免出现三维拉伸畸变。

3. 窗宽窗位(W/L)交互联动

不同科室医生对观察目标的要求不同。骨科医生需要突出骨性结构(高 HU),胸外科医生关注肺窗(低 HU),血管外科则侧重造影剂强化血管。在实际产品中,应为传递函数设计预设字典(Preset Matrix),允许用户通过滑块实时平移和缩放 vtkColorTransferFunction 与 vtkPiecewiseFunction 的控制点,无需重新解析底层数据即可毫秒级更新三维显示外观。

使用 vtk.js 读取 DICOM 序列并进行 3D 体绘制重建
图 5:Web 端 DICOM 3D 体绘制工程化落地四大维度架构矩阵

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章