cornerstone3D 通过二进制渲染影像

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}
相关推荐
一只小阿乐2 小时前
react路由中使用context
前端·javascript·react.js·context 上下文
Hilaku2 小时前
一周狂揽40K+ Star⭐ 的 Pretext 到底有多变态?
前端·javascript·html
Southern Wind2 小时前
AI Skill Server 动态技能中台
前端·后端·mysql·node.js
锦木烁光2 小时前
多端项目太乱?我是这样用 Monorepo 重构的
前端·架构
上山打牛2 小时前
cornerstone3D基本使用
前端
阿鑫_9962 小时前
通用-Nvm基础知识
前端
xinzheng新政3 小时前
Javascript·深入学习基础知识
前端·javascript·学习
前端付豪3 小时前
实现记忆开关
前端·后端
前端开发呀3 小时前
约定式路由的极简主义实践:一个插件搞定 React/Vue × Vite/Rspack
前端