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;
流程:
- 参考 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>
- 编写核心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;
}
})
});
},
- 显示效果: