cornerstonejs 3D 網頁醫學影像套件

是很完整的3D醫學影像套件
他預設是 nodejs 套件
預設也是 nodejs 環境
然而 javascript 的通用性
讓 python django 也可以套用到網頁上

套用的重點是需要先安裝 nodejs

https://nodejs.org/en

安裝完之後就可以使用 npm 了

然後在 django 裡面開一個資料夾
假設是 cornerstonejs
到 cornerstonejs 裡面用 npm 安裝 cornerstonejs

 

npm install @cornerstonejs/core
npm install @cornerstonejs/tools
npm install @cornerstonejs/dicom-image-loader
npm install @cornerstonejs/nifti-volume-loader
npm install --save-dev vite

npm install @vitejs/plugin-react
npm install @originjs/vite-plugin-commonjs
npm install @cornerstonejs/calculate-suv
npm install @cornerstonejs/polymorphic-segmentation
npm install @cornerstonejs/demo-utils

 

然後程式參考

https://www.cornerstonejs.org/docs/tutorials/basic-volume

重點是在於 這一行

import { createImageIdsAndCacheMetaData } from '../../../../utils/demo/helpers';

 

這邊的 helper 必須要自定義
例如改成

import createImageIdsAndCacheMetaDataZ from './my-helper';

 

my-helper 要自己新增
裡面的程式參考
https://github.com/cornerstonejs/cornerstone3D/blob/main/utils/demo/helpers/createImageIdsAndCacheMetaData.js

 

也就是說
必須要自訂義自己的 createImageIdsAndCacheMetaData 才可以


可以參考我寫的

 

import { api } from 'dicomweb-client';
import dcmjs from 'dcmjs';
import { calculateSUVScalingFactors } from '@cornerstonejs/calculate-suv';
import { getPTImageIdInstanceMetadata } from './helpers/getPTImageIdInstanceMetadata';
import { utilities } from '@cornerstonejs/core';
import cornerstoneDICOMImageLoader from '@cornerstonejs/dicom-image-loader';

import ptScalingMetaDataProvider from './helpers/ptScalingMetaDataProvider';
import { convertMultiframeImageIds } from './helpers/convertMultiframeImageIds';
import removeInvalidTags from './helpers/removeInvalidTags';

const { DicomMetaDictionary } = dcmjs.data;
const { calibratedPixelSpacingMetadataProvider, getPixelSpacingInformation } =
  utilities;

/**
/**
 * Uses dicomweb-client to fetch metadata of a study, cache it in cornerstone,
 * and return a list of imageIds for the frames.
 *
 * Uses the app config to choose which study to fetch, and which
 * dicom-web server to fetch it from.
 *
 * @returns {string[]} An array of imageIds for instances in the study.
 */

export default async function createImageIdsAndCacheMetaDataZ({
  modality,
  imagefs,
}) {
  const SOP_INSTANCE_UID = '00080018';
  const SERIES_INSTANCE_UID = '0020000E';
  const MODALITY = '00080060';
  const convertMultiframe = true;
  // const modality = modality;
  let imageIds = imagefs.map((ImageIdUrl) => {
    const SeriesInstanceUID = '00000';
    const SOPInstanceUIDToUse = '00001';
    const prefix = 'wadors:';

    var instanceMetaData = {
      '00080018' : {Value : [ '00000' ]}, //SOP_INSTANCE_UID
      '0020000E' : {Value : [ '00000' ]}, //SERIES_INSTANCE_UID
      '00080060' : {Value : [ modality ]}, //MODALITY
    };
    const imageId = ImageIdUrl;
    cornerstoneDICOMImageLoader.wadors.metaDataManager.add(
      imageId,
      instanceMetaData
    );
    return imageId;
  });

  // if the image ids represent multiframe information, creates a new list with one image id per frame
  // if not multiframe data available, just returns the same list given
  if (convertMultiframe) {
    imageIds = convertMultiframeImageIds(imageIds);
  }

  imageIds.forEach((imageId) => {
    let instanceMetaData = cornerstoneDICOMImageLoader.wadors.metaDataManager.get(imageId);
    if (!instanceMetaData) {
      return;
    }

    // It was using JSON.parse(JSON.stringify(...)) before but it is 8x slower
    instanceMetaData = removeInvalidTags(instanceMetaData);

    if (instanceMetaData) {
      // Add calibrated pixel spacing
      const metadata = DicomMetaDictionary.naturalizeDataset(instanceMetaData);
      const pixelSpacingInformation = getPixelSpacingInformation(metadata);
      const pixelSpacing = pixelSpacingInformation?.PixelSpacing;

      if (pixelSpacing) {
        calibratedPixelSpacingMetadataProvider.add(imageId, {
          rowPixelSpacing: parseFloat(pixelSpacing[0]),
          columnPixelSpacing: parseFloat(pixelSpacing[1]),
          type: pixelSpacingInformation.type,
        });
      }
    }
  });
  return imageIds;
}

 

可以發現裡面很多資料是寫死的
因為在

 

var ct_imageIds = await createImageIdsAndCacheMetaDataZ({
    modality : 'MD',
    imagefs : ct_load_imagefs,
});



這邊 createImageIdsAndCacheMetaDataZ 原本是用來配合 dicomweb 來讀取影像的
類似於你需要另外架設一個 dicom server 來顯示dicom影像
但這對於我們只是單純展示 dicom web 的概念就衝突了
我們只會是一個 web server

 

所以這邊我們才需要自訂義

然後 ct_load_imagefs 這邊我們可以給例如

 

['wadouri:/dicomfun/showCornerstonejsDicom/22388', 'wadouri:/dicomfun/showCornerstonejsDicom/22389', 'wadouri:/dic…

 

這樣的網址
而這些網址也就是 /dicomfun/showCornerstonejsDicom/22388 則直接吐出 dicom 檔案即可
dicom 可以經由 django 產生

例如:

# 建立 DICOM 檔案資料集
ds = FileDataset(None, {}, file_meta=file_meta, preamble=b"\x00"*128)
# if data_params['Modality'] == 'PT':
# ds = pydicom.dcmread('C:/Users/T34808/Downloads/228-PET-43656083.dcm')


ds.PatientName = 'NONE'
ds.PatientID = '0'
ds.Modality = data_params['Modality']
ds.SOPInstanceUID = file_meta.MediaStorageSOPInstanceUID
ds.SOPClassUID = file_meta.MediaStorageSOPClassUID
……
# 寫入 BytesIO
buffer = BytesIO()
ds.is_little_endian = True
ds.is_implicit_VR = False
ds.save_as(buffer)
buffer.seek(0)


# 回傳檔案
response = FileResponse(buffer, as_attachment=True, filename='generated_dicom.dcm')
response['Content-Type'] = 'application/dicom'
return response

 


所以只需要放進去設定好的dicom位置
再用 createAndCacheVolume 新增 Volume

 

var ct_imageIds = await createImageIdsAndCacheMetaDataZ({
    modality : 'MD',
    imagefs : ct_load_imagefs,
});
var ct_volume = await volumeLoader.createAndCacheVolume(ct_volumeId, {
    imageIds: ct_imageIds,
});
ct_volume.load();


view_volumeIds = [{
    volumeId: ct_volumeId,
}];

 


然後到 setVolumesForViewports 設定

 

setVolumesForViewports(
    renderingEngine,
    view_volumeIds,
    [viewportId_axial, viewportId_sagittal, viewportId_coronal],
).then(() => {
    // 設定初始
    
});
renderingEngine.renderViewports([viewportId_axial, viewportId_sagittal, viewportId_coronal]);

 

然後用vite 打包

 

npx vite build

 


打包成 bundle.js

設定檔案 vite.config.js 可以參考以下

 

import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { viteCommonjs } from '@originjs/vite-plugin-commonjs';
import { resolve } from 'path';


export default defineConfig({
  plugins: [
    react(),
    // for dicom-parser
    viteCommonjs(),
  ],
  // seems like only required in dev mode
  optimizeDeps: {
    exclude: ['@cornerstonejs/dicom-image-loader'],
    include: ['dicom-parser'],
  },
  worker: {
    format: 'es',
  },
  base: '/static/cornerstonejs/', // Django 靜態資源對應的路徑
  build: {
    rollupOptions: {
      input: {
        app: resolve(__dirname, 'main.js'),  // 這是你的入口檔案
      },
      output: {
        entryFileNames: 'bundle.js',
      },
    },
    outDir: '../static/cornerstonejs',  // Django static 資料夾的相對路徑
    assetsDir: '', // 避免產生 assets 子資料夾
    emptyOutDir: true,


    minify: false, // ❌ 不啟用 minify
    sourcemap: true, // ✅ 建議同時開啟 sourcemap,方便瀏覽器中 debug


  },
 
 
});

 

static/cornerstonejs 是對應 django 靜態資源位置

應該
基本上就能夠運作

希望對於有運用 cornerstonejs 的人有幫助喔
感恩

 

題外話:  cornerstonejs 相對冷門,但是你問 GPT如何套用他還是會給你答案,但是通常是 "幻覺",會給一堆不存在的函式,所以問GPT要有技巧,你問 npm , nodejs, javascript 都可以
但是問某個套件的應用方式,函式有哪些,這確實是緣木求魚,所以使用AI也是需要智慧的阿~