前言
上篇文章记录了如何在vue中引入vtk和使用,目前vtk在项目中实际应用是上传cbct文件,通过对cbct文件进行xyz轴的剪切展示,接下来先上效果图
效果图
上代码
cbct文件上传
<div class="cbct-renderer-page">
<template v-if="imageData">
<cc-cbct-view></cc-cbct-view>
</template>
<template v-else>
<div class="vtk-file">
<input type="file" webkitdirectory directory @change="handleInputChange" />
</div>
</template>
</div>
<script>
// ccCbctView组件就是对cbct文件的解析
import ccCbctView from '@/components/cc-cbct-renderer/cc-cbct-view.vue'
import ITKHelper from '@kitware/vtk.js/Common/DataModel/ITKHelper'
import readImageDICOMFileSeries from 'itk/readImageDICOMFileSeries'
export default {
name: 'cbct-renderer-page',
components: {
ccCbctView,
},
data() {
return {
imageData: null,
}
},
methods: {
async handleInputChange(e) {
const files = e.target.files
if (!files || !files.length) return
// convertItkToVtkImage:将itk-wasm图像转换为vtkImageData格式
// readImageDICOMFileSeries:从存储在Array或FileList中的一系列 DICOM File或Blob中读取图像
const itkImage = await readImageDICOMFileSeries(files)
const { image } = itkImage
const { convertItkToVtkImage } = ITKHelper
// this.imageData就是vtk格式的文件,将imageData通过provide透传到子组件中去
this.imageData = convertItkToVtkImage(image)
},
},
provide() {
return {
imageData: () => this.imageData,
}
},
mounted() {},
}
<style lang="scss" scoped>
.vtkDiv {
position: relative;
width: 100%;
height: 100%;
}
.vtk-file {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
}
</style>
ccCbctView组件
<template>
<div class="viewer">
<div class="left">
<div v-for="(view, key) in viewDataArray" :key="key" class="viewer_child">
// view-2d-mpr:对cbct文件进行top、left、front实现xyz三个轴的切片展示
<view-2d-mpr
:volumes="volumes"
:sliceIntersection="sliceIntersection"
:views="viewDataArray"
:onCreated="saveComponentRefGenerator(key)"
:index="key"
></view-2d-mpr>
</div>
// View3D:对cbct文件进行3D处理
<View3D :volumes="volumes"></View3D>
</div>
</div>
</template>
<script>
import { mapState } from "vuex";
import View2dMPR from "@/src/components/2DMPRView";
import View3D from "@/src/components/View3D";
import vtkVolume from "@kitware/vtk.js/Rendering/Core/Volume";
import vtkVolumeMapper from "@kitware/vtk.js/Rendering/Core/VolumeMapper";
import vtkPlane from "@kitware/vtk.js/Common/DataModel/Plane";
import vtkInteractorStyleMPRWindowLevel from "@/src/vtk/vtkInteractorStyleMPRWindowLevel";
export default {
components: {
"view-2d-mpr": View2dMPR,
View3D,
},
data() {
return {
showDistance:false,
volumes: [],
components: [],
loading: true,
sliceIntersection: [0, 0, 0],
// TODO: refactor into prop.
syncWindowLevels: true,
top: {
color: "#f00",
slicePlaneNormal: [0, 0, -1],
sliceViewUp: [0, -1, 0],
slicePlaneXRotation: 0,
slicePlaneYRotation: 0,
viewRotation: 0,
sliceThickness: 0.1,
blendMode: "none",
window: {
width: 0,
center: 0,
},
},
left: {
color: "#0f0",
slicePlaneNormal: [1, 0, 0],
sliceViewUp: [0, 0, -1],
slicePlaneXRotation: 0,
slicePlaneYRotation: 0,
viewRotation: 0,
sliceThickness: 0.1,
blendMode: "none",
window: {
width: 0,
center: 0,
},
},
front: {
color: "#00f",
slicePlaneNormal: [0, -1, 0],
sliceViewUp: [0, 0, -1],
slicePlaneXRotation: 0,
slicePlaneYRotation: 0,
viewRotation: 0,
sliceThickness: 0.1,
blendMode: "none",
window: {
width: 0,
center: 0,
},
},
};
},
computed: {
// 创建top、left、front三个方向的切片
viewDataArray() {
return { top: this.top, left: this.left, front: this.front };
},
},
mounted() {
this.resizeFunction = () => {
// 在调整大小事件和正确的数据之间似乎没有足够的时间
window.setTimeout(() => {
this.onScrolled();
}, 10);
};
// 更新交点时,窗口调整大小
window.addEventListener("resize", this.resizeFunction);
this.init();
},
methods: {
// 初始化
init() {
const volumeActor = vtkVolume.newInstance();
const volumeMapper = vtkVolumeMapper.newInstance();
volumeMapper.setSampleDistance(1);
volumeActor.setMapper(volumeMapper);
volumeMapper.setInputData(this.imageData);
const rgbTransferFunction = volumeActor
.getProperty()
.getRGBTransferFunction(0);
rgbTransferFunction.setMappingRange(500, 3000);
Object.values(this.viewDataArray).forEach((view) => {
view.window.center = 500;
view.window.width = 3000;
});
this.sliceIntersection = getVolumeCenter(volumeMapper);
this.volumes = [volumeActor];
},
getRenderWindow(index) {
return this.components[index].genericRenderWindow;
},
saveComponentRefGenerator(viewportIndex) {
return (component) => {
this.components[viewportIndex] = component;
const { windowWidth, windowLevel } = getVOI(component.volumes[0]);
// get initial window leveling
this[viewportIndex].windowWidth = windowWidth;
this[viewportIndex].windowLevel = windowLevel;
const renderWindow = component.genericRenderWindow.getRenderWindow();
// const renderer = component.genericRenderWindow.getRenderer();
renderWindow.getInteractor().getInteractorStyle().setVolumeMapper(null);
// default to the level tool
this.setLevelTool([viewportIndex, component]);
renderWindow.render();
};
},
setLevelTool([viewportIndex, component]) {
const istyle = vtkInteractorStyleMPRWindowLevel.newInstance();
istyle.setOnScroll(this.onScrolled);
istyle.setOnLevelsChanged((levels) => {
this.updateLevels({ ...levels, index: viewportIndex });
});
setInteractor(component, istyle);
},
updateLevels({ windowCenter, windowWidth, index }) {
this[index].window.center = windowCenter;
this[index].window.width = windowWidth;
if (this.syncWindowLevels) {
Object.entries(this.components)
.filter(([key]) => key !== index)
.forEach(([key, component]) => {
this[key].window.center = windowCenter;
this[key].window.width = windowWidth;
component.genericRenderWindow
.getInteractor()
.getInteractorStyle()
.setWindowLevel(windowWidth, windowCenter);
component.genericRenderWindow.getRenderWindow().render();
});
}
},
onScrolled() {
let planes = [];
Object.values(this.components).forEach((component) => {
const camera = component.genericRenderWindow
.getRenderer()
.getActiveCamera();
planes.push({
position: camera.getFocalPoint(),
normal: camera.getDirectionOfProjection(),
// this[viewportIndex].slicePlaneNormal
});
});
const newPoint = getPlaneIntersection(...planes);
// console.log(newPoint, "newPointnewPointnewPointnewPoint");
if (!Number.isNaN(newPoint)) {
this.sliceIntersection = newPoint;
}
return newPoint;
},
getSliceIntersection() {
return this.sliceIntersection;
},
},
provide() {
return {
getSliceIntersection: this.getSliceIntersection,
onScrolled: this.onScrolled,
getRenderWindow: this.getRenderWindow,
}
},
inject: ['imageData'],
};
function setInteractor(component, istyle) {
const renderWindow = component.genericRenderWindow.getRenderWindow();
// We are assuming the old style is always extended from the MPRSlice style
const oldStyle = renderWindow.getInteractor().getInteractorStyle();
renderWindow.getInteractor().setInteractorStyle(istyle);
// NOTE: react-vtk-viewport's code put this here, so we're copying it. Seems redundant?
istyle.setInteractor(renderWindow.getInteractor());
// Make sure to set the style to the interactor itself, because reasons...?!
const inter = renderWindow.getInteractor();
inter.setInteractorStyle(istyle);
// Copy previous interactors styles into the new one.
if (istyle.setSliceNormal && oldStyle.getSliceNormal()) {
istyle.setSliceNormal(oldStyle.getSliceNormal(), oldStyle.getViewUp(),[0,0,0]);
}
if (istyle.setSlabThickness && oldStyle.getSlabThickness()) {
istyle.setSlabThickness(oldStyle.getSlabThickness());
}
istyle.setVolumeMapper(component.volumes[0]);
}
function getPlaneIntersection(plane1, plane2, plane3) {
// console.log(plane1, plane2, plane3, "ppppppppppppppppppppp");
try {
let line = vtkPlane.intersectWithPlane(
plane1.position,
plane1.normal,
plane2.position,
plane2.normal
);
if (line.intersection) {
const { l0, l1 } = line;
const intersectionLocation = vtkPlane.intersectWithLine(
l0,
l1,
plane3.position,
plane3.normal
);
if (intersectionLocation.intersection) {
return intersectionLocation.x;
}
}
} catch (err) {
// console.log("some issue calculating the plane intersection");
}
return NaN;
}
function getVOI(volume) {
const rgbTransferFunction = volume.getProperty().getRGBTransferFunction(0);
const range = rgbTransferFunction.getMappingRange();
const windowWidth = range[0] + range[1];
const windowCenter = range[0] + windowWidth / 2;
return {
windowCenter,
windowWidth,
};
}
function getVolumeCenter(volumeMapper) {
const bounds = volumeMapper.getBounds();
return [
(bounds[0] + bounds[1]) / 2.0,
(bounds[2] + bounds[3]) / 2.0,
(bounds[4] + bounds[5]) / 2.0,
];
}
</script>
<style lang='scss' scoped>
.viewer {
width: 100%;
height: 100%;
display: flex;
margin: 0;
.left {
width: 100%;
height: 100%;
display: flex;
/* grid-template-rows: 1fr 1fr;
grid-template-columns: 1fr 1fr; */
flex-wrap: wrap;
.viewer_child {
width: 50%;
height: 50%;
}
}
}
</style>
view-2d-mpr组件
<template>
<div class="mpr" v-if="volumes && volumes.length">
<div class="container" ref="container"></div>
// ViewportOverlay:小圆点和W/L展示的组件
<ViewportOverlay :voi="voi" :active="isActive" :color="viewColor" />
</div>
</template>
<script>
import vtkGenericRenderWindow from "@kitware/vtk.js/Rendering/Misc/GenericRenderWindow";
import "@kitware/vtk.js/Rendering/Profiles/Volume";
import ViewportOverlay from "./ViewportOverlay/ViewportOverlay.vue";
import { quat, vec3, mat4 } from "gl-matrix";
import vtkInteractorStyleMPRSlice from "@/src/vtk/vtkInteractorStyleMPRSlice";
import { degrees2radians } from "@/src/utils/math.js";
export default {
components: {
ViewportOverlay,
},
props: {
volumes: { type: Array, required: true },
views: { type: Object, required: true },
// Front, Side, Top, etc, for which view data to use
index: String,
sliceIntersection: {
type: Array,
default() {
return [0, 0, 0];
},
},
onCreated: Function,
},
data() {
return {
width: 0,
height: 0,
renderer: null,
subs: {
interactor: createSub(),
data: createSub(),
},
};
},
created() {
this.genericRenderWindow = null;
this.cachedSlicePlane = [];
this.cachedSliceViewUp = [];
},
inject:['onScrolled','getSliceIntersection'],
mounted() {
window.addEventListener("resize", this.onResize);
setTimeout(() => {
this.onMounted();
}, 300);
},
beforeDestroy() {
window.removeEventListener("resize", this.onResize);
// Delete the render context
this.genericRenderWindow.delete();
delete this.genericRenderWindow;
Object.keys(this.subs).forEach((k) => {
this.subs[k].unsubscribe();
});
},
computed: {
// Cribbed from the index and views
slicePlaneNormal() {
return this.views[this.index].slicePlaneNormal;
},
slicePlaneXRotation() {
return this.views[this.index].slicePlaneXRotation;
},
slicePlaneYRotation() {
return this.views[this.index].slicePlaneYRotation;
},
sliceViewUp() {
return this.views[this.index].sliceViewUp;
},
viewRotation() {
return this.views[this.index].viewRotation;
},
window() {
return this.views[this.index].window;
},
viewColor() {
return this.views[this.index].color;
},
isActive() {
return this.views[this.index].active;
},
voi() {
return {
windowWidth: this.window.width,
windowCenter: this.window.center,
};
},
},
watch: {
volumes(newVolumes) {
this.updateVolumesForRendering(newVolumes);
},
// // Calculate the new normals after applying rotations to the untouched originals
slicePlaneNormal() {
this.updateSlicePlane();
},
slicePlaneXRotation() {
this.updateSlicePlane();
},
slicePlaneYRotation() {
this.updateSlicePlane();
},
sliceViewUp() {
this.updateSlicePlane();
},
viewRotation() {
this.updateSlicePlane();
},
parallel(p) {
this.renderer.getActiveCamera().setParallelProjection(p);
},
},
methods: {
onMounted() {
this.cachedSlicePlane = [...this.slicePlaneNormal];
this.cachedSliceViewUp = [...this.sliceViewUp];
this.genericRenderWindow = vtkGenericRenderWindow.newInstance({
background: [0, 0, 0],
});
this.genericRenderWindow.setContainer(this.$refs.container);
let widgets = [];
this.renderWindow = this.genericRenderWindow.getRenderWindow();
this.renderer = this.genericRenderWindow.getRenderer();
if (this.parallel) {
this.renderer.getActiveCamera().setParallelProjection(true);
}
// update view node tree so that vtkOpenGLHardwareSelector can access the vtkOpenGLRenderer instance.
const oglrw = this.genericRenderWindow.getOpenGLRenderWindow();
oglrw.buildPass(true);
const istyle = vtkInteractorStyleMPRSlice.newInstance();
// istyle.setOnScroll(this.onStackScroll);
const inter = this.renderWindow.getInteractor();
//setInteractorStyle 切换操作者 External switching between joystick/trackball/new? modes.
inter.setInteractorStyle(istyle);
// TODO: assumes the volume is always set for this mounted state...Throw an error?
const istyleVolumeMapper = this.volumes[0].getMapper();
istyle.setVolumeMapper(istyleVolumeMapper);
//start with the volume center slice
const range = istyle.getSliceRange();
// console.log('view mounted: setting the initial range', range)
istyle.setSlice((range[0] + range[1]) / 2);
// add the current volumes to the vtk renderer
this.updateVolumesForRendering(this.volumes);
this.updateSlicePlane();
// force the initial draw to set the canvas to the parent bounds.
this.onResize();
if (this.onCreated) {
/**
* Note: The contents of this Object are
* considered part of the API contract
* we make with consumers of this component.
*/
this.onCreated({
genericRenderWindow: this.genericRenderWindow,
widgetManager: this.widgetManager,
container: this.$refs.container,
widgets,
volumes: [...this.volumes],
_component: this,
});
}
},
onResize() {
// TODO: debounce for performance reasons?
this.genericRenderWindow.resize();
const [width, height] = [
this.$refs.container.offsetWidth,
this.$refs.container.offsetHeight,
];
this.width = width;
this.height = height;
},
updateVolumesForRendering(volumes) {
if (!this.renderer) return;
this.renderer.removeAllVolumes();
if (volumes && volumes.length) {
volumes.forEach((volume) => {
if (!volume.isA("vtkVolume")) {
console.warn("Data to <Vtk2D> is not vtkVolume data");
} else {
this.renderer.addVolume(volume);
}
});
}
this.renderWindow.render();
},
updateSlicePlane() {
// TODO: optimize so you don't have to calculate EVERYTHING every time?
// rotate around the vector of the cross product of the plane and viewup as the X component
let sliceXRotVector = [];
vec3.cross(sliceXRotVector, this.sliceViewUp, this.slicePlaneNormal);
vec3.normalize(sliceXRotVector, sliceXRotVector);
// rotate the viewUp vector as the Y component
let sliceYRotVector = this.sliceViewUp;
// const yQuat = quat.create();
// quat.setAxisAngle(yQuat, input.sliceViewUp, degrees2radians(this.slicePlaneYRotation));
// quat.normalize(yQuat, yQuat);
// Rotate the slicePlaneNormal using the x & y rotations.
// const planeQuat = quat.create();
// quat.add(planeQuat, xQuat, yQuat);
// quat.normalize(planeQuat, planeQuat);
// vec3.transformQuat(this.cachedSlicePlane, this.slicePlaneNormal, planeQuat);
const planeMat = mat4.create();
mat4.rotate(
planeMat,
planeMat,
degrees2radians(this.slicePlaneYRotation),
sliceYRotVector
);
mat4.rotate(
planeMat,
planeMat,
degrees2radians(this.slicePlaneXRotation),
sliceXRotVector
);
vec3.transformMat4(
this.cachedSlicePlane,
this.slicePlaneNormal,
planeMat
);
// Rotate the viewUp in 90 degree increments
const viewRotQuat = quat.create();
// Use - degrees since the axis of rotation should really be the direction of projection, which is the negative of the plane normal
quat.setAxisAngle(
viewRotQuat,
this.cachedSlicePlane,
degrees2radians(-this.viewRotation)
);
quat.normalize(viewRotQuat, viewRotQuat);
// rotate the ViewUp with the x and z rotations
const xQuat = quat.create();
quat.setAxisAngle(
xQuat,
sliceXRotVector,
degrees2radians(this.slicePlaneXRotation)
);
quat.normalize(xQuat, xQuat);
const viewUpQuat = quat.create();
quat.add(viewUpQuat, xQuat, viewRotQuat);
vec3.transformQuat(this.cachedSliceViewUp, this.sliceViewUp, viewRotQuat);
// update the view's slice
const renderWindow = this.genericRenderWindow.getRenderWindow();
const istyle = renderWindow.getInteractor().getInteractorStyle();
if (istyle && istyle.setSliceNormal) {
istyle.setSliceNormal(this.cachedSlicePlane, this.cachedSliceViewUp,this.onScrolled()
);
}
renderWindow.render();
},
},
};
</script>
<style scoped lang='scss'>
.mpr,
.container {
position: relative;
width: 100%;
height: 100%;
}
</style>
viewportOverlay组件
<template>
<div class="ViewportOverlay" :style="borderStyle">
<div v-if="color" class="viewColor" :style="colorStyle"></div>
<div class="border overlay-element" :style="borderStyle" />
<div class="top-left overlay-element">
<div>{{ formatPN(patientName) }}</div>
<div>{{ patientId }}</div>
</div>
<div class="top-right overlay-element">
<div>{{ studyDescription }}</div>
</div>
<div class="bottom-left overlay-element">
<div>{{ wwwc }}</div>
<!-- <div>{{wl.window+'/'+wl.level}}</div> -->
</div>
<div class="bottom-right overlay-element">
<div>{{ seriesNumber >= 0 ? `Ser: ${seriesNumber}` : "" }}</div>
<div>
<div>{{ seriesDescription }}</div>
</div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
wl: {
window: 0,
level: 0
}
};
},
props: {
voi: {
type: Object,
default: () => ({
windowWidth: 0,
windowCenter: 0
})
},
active: Boolean,
studyDate: String,
studyTime: String,
studyDescription: String,
patientName: String,
patientId: String,
seriesNumber: String,
seriesDescription: String,
color: String
},
methods: {
formatNumberPrecision(number, precision) {
if (number !== null) {
return parseFloat(number).toFixed(precision)
}
}
/**
* Formats a patient name for display purposes
*/
formatPN(name) {
if (!name) {
return
}
// Convert the first ^ to a ', '. String.replace() only affects
// the first appearance of the character.
const commaBetweenFirstAndLast = name.replace('^', ', ')
// Replace any remaining '^' characters with spaces
const cleaned = commaBetweenFirstAndLast.replace(/\^/g, ' ')
// Trim any extraneous whitespace
return cleaned.trim()
}
isValidNumber(value) {
return typeof value === 'number' && !isNaN(value)
}
},
computed: {
borderStyle() {
return (
(this.active &&
this.color &&
`border-color: ${this.color}; border-width:2px`) ||
""
);
},
colorStyle() {
return (this.color && `background: ${this.color}`) || "";
},
wwwc() {
return `W/L: ${this.voi.windowWidth.toFixed(
0
)}/${this.voi.windowCenter.toFixed(0)}`;
}
}
};
</script>
<style lang="scss">
:root {
--viewport-tag-padding: 20px;
}
.ViewportOverlay {
color: white;
.viewColor {
position: absolute;
top: 4px;
right: 4px;
width: 12px;
height: 12px;
z-index: 100;
border-radius: 12px;
}
.border {
border: 1px solid #666;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 100;
}
.overlay-element {
position: absolute;
font-weight: normal;
text-shadow: -1px -1px 0 #000, 1px -1px 0 #000, -1px 1px 0 #000,
1px 1px 0 #000;
pointer-events: none;
-ms-user-select: none;
-webkit-touch-callout: none;
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
}
.top-left {
top: var(--viewport-tag-padding);
left: var(--viewport-tag-padding);
}
.top-center {
top: var(--viewport-tag-padding);
padding-top: var(--viewport-tag-padding);
width: 100%;
text-align: center;
}
.top-right {
top: var(--viewport-tag-padding);
right: var(--viewport-tag-padding);
text-align: right;
}
.bottom-left {
bottom: var(--viewport-tag-padding);
left: var(--viewport-tag-padding);
}
.bottom-right {
bottom: var(--viewport-tag-padding);
right: var(--viewport-tag-padding);
text-align: right;
}
}
</style>
view3D 组件
<template>
<div class="view_3d">
<div class="container" ref="container"></div>
<div class="gauss" ref="gauss"></div>
</div>
</template>
<script>
import {mapMutations} from 'vuex'
import vtkWidgetManager from "@kitware/vtk.js/Widgets/Core/WidgetManager";
import vtkGenericRenderWindow from "@kitware/vtk.js/Rendering/Misc/GenericRenderWindow";
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 vtkColorMaps from "@kitware/vtk.js/Rendering/Core/ColorTransferFunction/ColorMaps";
import vtkPiecewiseFunction from "@kitware/vtk.js/Common/DataModel/PiecewiseFunction";
export default {
props: {
volumes: Array,
},
data() {
return {
background: [0, 0, 0],
};
},
created() {
this.genericRenderWindow = null;
this.widgetManager = vtkWidgetManager.newInstance();
},
mounted() {
this.genericRenderWindow = vtkGenericRenderWindow.newInstance({
background: this.background,
});
this.genericRenderWindow.setContainer(this.$refs.container);
this.renderer = this.genericRenderWindow.getRenderer();
this.renderWindow = this.genericRenderWindow.getRenderWindow();
this.widgetManager.setRenderer(this.renderer);
// this.updateVolumesForRendering(this.volumes);
this.renderer.resetCamera();
this.renderer.updateLightsGeometryToFollowCamera();
window.gen = this.genericRenderWindow;
window.vol = this.volumes[0];
this.actor = vtkVolume.newInstance();
this.mapper = vtkVolumeMapper.newInstance();
console.log(this.mapper.setInputConnection);
// this.mapper.setInputConnection()
this.actor.setMapper(this.mapper);
this.mapper.setInputData(this.$store.state.loader.imageData)
this.renderer.addVolume(this.actor);
this.genericRenderWindow.resize();
this.piecewiseFun = vtkPiecewiseFunction.newInstance();
this.lookupTable = vtkColorTransferFunction.newInstance();
this.lookupTable.applyColorMap(
vtkColorMaps.getPresetByName("Cool to Warm")
);
const range = this.$store.state.loader.imageData
.getPointData()
.getScalars()
.getRange();
console.log(range);
this.lookupTable.setMappingRange(...range);
this.lookupTable.updateRange();
for (let i = 0; i <= 4; i++) {
this.piecewiseFun.addPoint(i * 320, i / 4);
}
this.actor.getProperty().setRGBTransferFunction(0, this.lookupTable);
this.actor.getProperty().setScalarOpacity(0, this.piecewiseFun);
/* this.setPiecewiseFun(this.piecewiseFun)
this.setLookupTable(this.lookupTable) */
this.set3DInfo({
actor:this.actor,
renderer:this.renderer,
renderWindow:this.renderWindow,
piecewiseFun:this.piecewiseFun,
lookupTable:this.lookupTable,
mapper:this.mapper,
widgetManager:this.widgetManager,
range:range,
volumes:this.volumes,
genericRenderWindow:this.genericRenderWindow
})
// this.mapper.setInputConnection(this.volumes[0]);
window.imageData = this.$store.state.loader.imageData;
this.renderer.resetCamera();
this.renderWindow.render();
},
methods: {
...mapMutations('three',['set3DInfo']),
updateVolumesForRendering(volumes) {
if (volumes) {
volumes.forEach((volume) => {
if (volume.isA("vtkVolume")) {
this.renderer.addVolume(volume);
} else {
console.warn("DATA is not vtkVolume data");
}
});
}
},
},
};
</script>
<style scoped lang='scss'>
.view_3d {
width: 50%;
height: 50%;
.container {
width: 100%;
height: 100%;
}
}
/* .gauss {
width: 400px;
height: 40px;
position: fixed;
bottom: 0;
right: 0;
} */
</style>
总结
cbct文件首先通过itkhelper进行解析,再转换为vtk文件格式,vtk库总体使用比较复杂,大部分代码还是有注释的,目前功能只做了cbct文件的xyz轴切面展示,后续还做了滑动滚动条与ct照的联动效果,还有xyz轴的红路黄线的联动效果,贴上后续效果图,点赞点赞!!!!!!!!!!!