Vue2 集成VTK.js 并显示3D影像

发布于:2025-04-15 ⋅ 阅读:(30) ⋅ 点赞:(0)

Vue2 集成VTK.js 并显示3D影像(核心代码)

作者:coder_fang
vtk.js目前官网只有vue3的示例,对于已有vue2系统的集成,需要使用指定版本的vtk,itk等库并修改部分配置即可。

需要的主要库和版本:

vue:2.3.4; vtk-v32.9.0.min.js,itk-wasm.min.js(用于读取dicom文件);pipeline.worker.js;

流程:

  1. 参考 vtk.js 官网 VolumeMapperBlendModes 示例,将UI集成到单独vue2 界面中。
<template>
    <div style="height:100%; background-color: #000">
        <Row>
            <Col offset="8" span="8">
            <table class="controls" ref="table">
                <Spin size="large" fix v-if="loading"></Spin>
                <tbody>
                    <tr>
                        <td colspan="2" style="text-align: center;"><label>{{ patname }}</label></td>
                    </tr>
                    <tr>
                        <td colspan="2">

                            <label style="display: table-cell;">模式:</label>
                            <Select style="display: table-cell;" v-model="blendMode" @on-change="chgBlendMode" transfer>
                                <Option v-for="item in modeSelects" :label="item.label" :value="item.value"
                                    :key="item.value">
                                    {{ item.label }}
                                </Option>

                            </Select>
                        </td>
                    </tr>

                    <tr>
                        <td id="ipScalar" style="display:none;">

                            <label>ScalarMin:</label>
                            <input id="scalarMin" type="range" min="0" max="1" v-model=scalar.scalarMin step="0.01"
                                @change="updateScalar()">

                        </td>
                        <td id="ipScalar" style="display:none;">
                            <label>ScalarMax:</label>
                            <input id="scalarMax" type="range" min="0" max="1" v-model=scalar.scalarMax step="0.01"
                                @change="updateScalar()">

                        </td>

                    </tr>


                    <tr id="radonScalar" style="display:none;">
                        <td>
                            <label> Min hounsfield:</label>
                            <input id="minHounsfield" type="range" min="-1024" max="3071" step="5"
                                :value="radonParameters.minHounsfield" @change="updateRadon()">
                        </td>
                        <td>
                            <label>Absorption:</label>
                            <input id="minAbsorption" type="range" min="0" max="0.1" step="0.001"
                                :value="radonParameters.minAbsorption" @change="updateRadon()">
                        </td>

                    </tr>
                    <tr id="radonScalar" style="display:none;">
                        <td>
                            <label> Max hounsfield:</label>
                            <input id="maxHounsfield" type="range" min="-1024" max="3071" step="5"
                                :value="radonParameters.maxHounsfield" @change="updateRadon()">
                        </td>
                        <td>
                            <label>Absorption:</label>
                            <input id="maxAbsorption" type="range" min="0" max="0.1" step="0.001"
                                :value="radonParameters.maxAbsorption" @change="updateRadon()">
                        </td>
                    </tr>
                    <tr>
                        <td>
                            <label>Sample distance:</label>
                            <input id="sampleDistance" type="range" min="0.1" max="2.5" step="0.1"
                                v-model="sampleDistance" @change="updateSample()">
                        </td>
                    </tr>

                </tbody>
            </table>
            </Col>
        </Row>
        <Row>
            <Col span="8" offset="8">
            <div ref="container" style="width: 512px;height: 512px;" />
            </Col>
        </Row>
    </div>
</template>
  1. 编写核心Vue2 脚本:
import '../../libs/third/itk-wasm.min.js';
import '../../libs/third/vtk-v32.9.0.js'

//更新渲染模式
 updateBlendMode(val) {
            if (!this.context || this.blendMode < 0)
                return;
            const currentBlendMode = this.blendMode;
            const ipScalarEls = document.querySelectorAll('#ipScalar');
            const radonScalars = document.querySelectorAll('#radonScalar');

            this.context.mapper.setBlendMode(currentBlendMode);


            this.context.mapper.setIpScalarRange(0.0, 1.0);
            if (currentBlendMode == 3 || currentBlendMode == 4) {
                this.context.mapper.setIpScalarRange(this.scalar.scalarMin, this.scalar.scalarMax);
            }


            // if average or additive blend mode
            for (let i = 0; i < ipScalarEls.length; i += 1) {
                const el = ipScalarEls[i];
                el.style.display = (currentBlendMode == 3 || currentBlendMode == 4 ? 'table-cell' : 'none');

            }

            // Radon
            for (let i = 0; i < radonScalars.length; i += 1) {
                const el = radonScalars[i];
                el.style.display = (currentBlendMode == 5 ? 'table-cell' : 'none');
            }

            const colorTransferFunction = vtk.Rendering.Core.vtkColorTransferFunction.newInstance();
            if (currentBlendMode === 5) {
                colorTransferFunction.addRGBPoint(0, 0, 0, 0);
                colorTransferFunction.addRGBPoint(1, 1, 1, 1);
            } else {
                colorTransferFunction.addRGBPoint(-3024, 0, 0, 0);
                colorTransferFunction.addRGBPoint(-637.62, 0, 0, 0);
                colorTransferFunction.addRGBPoint(700, 1, 1, 1);
                colorTransferFunction.addRGBPoint(3071, 1, 1, 1);
            }
            this.context.colorTransferFunction = colorTransferFunction;
            this.context.actor.getProperty().setRGBTransferFunction(0, colorTransferFunction);
            this.updatePiecewiseFunction();
            this.context.mapper.update();

            this.context.renderWindow.render();
        },
//类似更新窗宽窗位
 updatePiecewiseFunction() {


            const currentBlendMode = this.blendMode;
            let opacityFunction;

            if (currentBlendMode === 5) {
                opacityFunction = vtk.Rendering.Core.vtkVolumeMapper.createRadonTransferFunction(
                    this.radonParameters.minHounsfield.toFixed(0),
                    this.radonParameters.minAbsorption.toFixed(3),
                    this.radonParameters.maxHounsfield.toFixed(0),
                    this.radonParameters.maxAbsorption.toFixed(3)
                );
            } else {
                opacityFunction = vtk.Common.DataModel.vtkPiecewiseFunction.newInstance();
                opacityFunction.addPoint(-3024, 0.1);
                opacityFunction.addPoint(-637.62, 0.1);
                opacityFunction.addPoint(700, 0.5);
                opacityFunction.addPoint(3071, 0.9);
            }
            this.context.actor.getProperty().setScalarOpacity(0, opacityFunction);
        },


//获取数据并显示
getImages() {

			//此处可以通过网络获取或本地打开文件集
            const fetchFiles = this.images.map((file_path, index) => {
                const path = file_path + "&contentType=application%2Fdicom";
                return ajax.get(path, { responseType: 'blob' }).then((response) => {
                    const jsFile = new File([response.data], `${index}.dcm`);
                    return jsFile;
                });
            });
			
			//此处手动配置相关js目录,禁止网络获取
            itk.itkConfig.pipelineWorkerUrl = './third/pipeline.worker.js'
            itk.itkConfig.imageIOUrl = './'


            Promise.all(fetchFiles).then((path) => {
                itk.readImageDICOMFileSeries(path).then(({ webWorker, image }) => {
                //通过itk打开文件后转换成vtk 体数据
                    const imageData = vtk.Common.DataModel.vtkITKHelper.convertItkToVtkImage(image);
                    if (!this.context) {
                        const fullScreenRenderer = vtk.Rendering.Misc.vtkFullScreenRenderWindow.newInstance({

                            background: [0.1, 0.1, 0.1],
                            container: this.vtkContainer
                        });


                        const mapper = vtk.Rendering.Core.vtkVolumeMapper.newInstance();
                        mapper.setInputData(imageData);
                        mapper.setSampleDistance(this.sampleDistance);
                        mapper.setAutoAdjustSampleDistances(0)
                        mapper.setPreferSizeOverAccuracy(true);

                        const actor = vtk.Rendering.Core.vtkVolume.newInstance();
                        actor.setMapper(mapper);
                        actor.getProperty().setScalarOpacityUnitDistance(0, 3.0);
                        actor.getProperty().setInterpolationTypeToLinear();
                        actor.getProperty().setShade(true);
                        actor.getProperty().setAmbient(0.1);
                        actor.getProperty().setDiffuse(0.9);
                        actor.getProperty().setSpecular(0.2);
                        actor.getProperty().setSpecularPower(10.0);

                        const renderer = fullScreenRenderer.getRenderer();
                        const renderWindow = fullScreenRenderer.getRenderWindow();

                        renderer.addActor(actor);

                        this.context = {
                            fullScreenRenderer,
                            renderWindow,
                            renderer,
                            actor,
                            mapper,
                            imageData
                        };

                        renderer.resetCamera();
                        this.initOrientation();
                        this.updateBlendMode();
                        this.loading = false;
                    }
                })
            });

        },
  1. 显示效果:
    在这里插入图片描述

网站公告

今日签到

点亮在社区的每一天
去签到