vue中通过vtk上传cbct文件的项目实战

前言

上篇文章记录了如何在vue中引入vtk和使用,目前vtk在项目中实际应用是上传cbct文件,通过对cbct文件进行xyz轴的剪切展示,接下来先上效果图

效果图

cbct上传效果图

上代码

cbct文件上传

QQ截图20221230142342.png

<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轴的红路黄线的联动效果,贴上后续效果图,点赞点赞!!!!!!!!!!!

效果图

QQ截图20221230145330.png

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值