【VolView】純前端實現CT三維重建-CBCT

文章目錄

  • 什么是CBCT
  • CBCT技術路線
    • 使用第三方工具
    • 使用Python實現
    • 使用前端實現
  • 純前端實現方案優缺點
    • 使用VolView實現CBCT
  • VolView的使用
    • 1.克隆代碼
    • 2.配置依賴
    • 3.運行
    • 4.效果
  • 進階:VolView配合Python解決卡頓
    • 1.修改VtkThreeView.vue
    • 2.新增Custom3DView.vue
    • 3.Python生成stl三維文件
    • 4.最終效果

什么是CBCT

放射科影像是醫學軟件必不可少的一部分,對影像的顯示、編輯、處理等操作更是重點。在多種放射科影像中,CBCT是關鍵的一環。CBCT全稱為口腔頜面錐形束CT,其工作原理是通過錐形X射線束圍繞患者頭部旋轉掃描,結合計算機算法生成高分辨率的三維圖像。

CBCT在口腔醫學中幾乎覆蓋所有亞專科:
?種植牙:評估頜骨密度、神經管位置,輔助種植體定位和手術導板設計。
?正畸與阻生牙:觀察牙齒排列、埋伏牙位置及與周圍組織關系,減少拔牙風險。
?牙體牙髓治療:診斷復雜根管、根裂及根尖病變,提高治療精確性。
?頜面外科:用于腫瘤、骨折的術前評估及術后效果監測。
?顳下頜關節:清晰顯示關節結構異常,輔助診斷關節紊亂病。>

CBCT技術路線

使用第三方工具

有不少工具可以實現CBCT效果,例如:Slicer。
在這里插入圖片描述

使用Python實現

使用pydicom結合mpl_toolkits實現三維展示。

使用前端實現

使用VTK.js、Three.js及WebAssembly 實現。

方案技術適用場景優缺點
VTK.jsWebGL + VTK醫學影像可視化(CT/MRI)高質量、原生支持 Volume Rendering,但數據轉換復雜
Three.js + 3D 紋理WebGL + Shader一般 3D 可視化兼容性好,適合前端開發,但醫學精度低
WebAssembly + 醫學引擎WASM專業醫學影像專業級醫學軟件,性能強,但開發難度大

純前端實現方案優缺點

?類別?優點?缺點
?性能與成本1. 低服務器依賴,節省硬件和維護成本。
2. 實時交互,響應延遲低。
1. 瀏覽器內存限制大,可能崩潰。
2. 低端設備GPU性能不足,導致渲染卡頓。
?數據隱私1. 數據無需上傳服務器,符合隱私法規(如HIPAA)。
2. 離線緩存支持斷網使用。
1. 數據預處理依賴后端,可能需臨時暴露敏感信息。
?功能與兼容性1. 支持基礎三維操作(旋轉、縮放、剖面切割)。1. 復雜算法(如深度學習分割)難以實現。
2. 瀏覽器兼容性有限(如舊版Safari)。
?開發與部署1. 部署便捷,前端靜態資源可托管至CDN。
2. 適合輕量級應用(教育、預覽)。
1. 大規模數據加載耗時(如全頭顱CBCT)。
2. 需額外優化壓縮和分塊加載邏輯。

使用VolView實現CBCT

VolView是一款基于VTK.js的開源醫學影像瀏覽器,支持在網頁端直接拖拽加載DICOM數據并生成2D切片及3D電影級體渲染視圖,提供標注、測量等工具,所有數據均在本地處理,確保隱私安全。無需安裝軟件,可跨平臺使用,適用于臨床診斷與科研教育。
在這里插入圖片描述

官網:https://volview.kitware.com/

關于VolView的介紹可以參考視頻:

VolView的使用

1.克隆代碼

GitHub地址:https://github.com/Kitware/VolView

2.配置依賴

npm i

3.運行

npm run dev

4.效果

在這里插入圖片描述
注:點擊左側可以在線加載演示數據,也可以點擊右側上傳本地dicom影像文件。

在這里插入圖片描述

進階:VolView配合Python解決卡頓

上文我們提到,基于vtk.js的純前端CBCT解決方案,雖然能不依賴其他第三方軟件的情況下顯示出我們需要的效果,但它對性能的高要求導致打開前端的電腦必須有較高的GPU配置,否則將異常卡頓。

此處給出思路:

由于卡頓主要是三維顯示導致,其代碼需實時計算得出三維效果,導致瀏覽器卡頓。要解決卡頓,我們就需要解決三維顯示問題。

我們可以將VolView的三維渲染部分替換為server端Python生成stl文件。

1.修改VtkThreeView.vue

在這里插入圖片描述
去除原有渲染三維的組件,改為我們自定義的新組件: <Custom3DView />

全部代碼如下:

<template><div class="vtk-container-wrapper vtk-three-container"><div class="vtk-container" :class="active ? 'active' : ''"><!-- 此處是繪制3D重建的地方 start--><div class="vtk-sub-container"><!-- <divclass="vtk-view"ref="vtkContainerRef"data-testid="vtk-view vtk-three-view"></div> --><Custom3DView /></div><!-- 此處是繪制3D重建的地方 end --><div class="overlay-no-events tool-layer"><crop-tool :view-id="viewID" /><pan-tool :viewId="viewID" /></div><view-overlay-grid class="overlay-no-events view-annotations"><template v-slot:top-left><div class="annotation-cell"><v-btnclass="pointer-events-all"darkiconsize="medium"variant="text"@click="resetCamera"><v-icon size="medium" class="py-1">mdi-camera-flip-outline</v-icon><v-tooltiplocation="right"activator="parent"transition="slide-x-transition">Reset Camera</v-tooltip></v-btn><span class="ml-3">{{ topLeftLabel }}</span></div></template></view-overlay-grid><transition name="loading"><div v-if="isImageLoading" class="overlay-no-events loading"><div>Loading the image</div><div><v-progress-circular indeterminate color="blue" /></div></div></transition></div></div>
</template><script lang="ts">
import {computed,defineComponent,onBeforeUnmount,onMounted,PropType,provide,ref,toRefs,watch,Ref,nextTick,
} from 'vue';
import { computedWithControl } from '@vueuse/core';
import { vec3 } from 'gl-matrix';import vtkVolumeRepresentationProxy from '@kitware/vtk.js/Proxy/Representations/VolumeRepresentationProxy';
import { Mode as LookupTableProxyMode } from '@kitware/vtk.js/Proxy/Core/LookupTableProxy';
import vtkPiecewiseFunctionProxy from '@kitware/vtk.js/Proxy/Core/PiecewiseFunctionProxy';
import vtkVolumeMapper from '@kitware/vtk.js/Rendering/Core/VolumeMapper';
import vtkImageData from '@kitware/vtk.js/Common/DataModel/ImageData';
import { getDiagonalLength } from '@kitware/vtk.js/Common/DataModel/BoundingBox';
import type { Vector3 } from '@kitware/vtk.js/types';import { useProxyManager } from '@/src/composables/useProxyManager';
import ViewOverlayGrid from '@/src/components/ViewOverlayGrid.vue';
import { useResizeObserver } from '../composables/useResizeObserver';
import { useCurrentImage } from '../composables/useCurrentImage';
import { useCameraOrientation } from '../composables/useCameraOrientation';
import vtkLPSView3DProxy from '../vtk/LPSView3DProxy';
import { useSceneBuilder } from '../composables/useSceneBuilder';
import { usePersistCameraConfig } from '../composables/usePersistCameraConfig';
import { useModelStore } from '../store/datasets-models';
import { LPSAxisDir } from '../types/lps';
import { useViewProxy } from '../composables/useViewProxy';
import { ViewProxyType } from '../core/proxies';
import { VolumeColorConfig } from '../store/view-configs/types';
import useVolumeColoringStore, {DEFAULT_AMBIENT,DEFAULT_DIFFUSE,DEFAULT_SPECULAR,
} from '../store/view-configs/volume-coloring';
import { getShiftedOpacityFromPreset } from '../utils/vtk-helpers';
import CropTool from './tools/crop/CropTool.vue';
import PanTool from './tools/PanTool.vue';
import { useWidgetManager } from '../composables/useWidgetManager';
import { VTKThreeViewWidgetManager } from '../constants';
import { useCropStore, croppingPlanesEqual } from '../store/tools/crop';
import { isViewAnimating } from '../composables/isViewAnimating';
import { ColoringConfig } from '../types/views';
import useViewCameraStore from '../store/view-configs/camera';
import { Maybe } from '../types';
import { useResetViewsEvents } from './tools/ResetViews.vue';
import Custom3DView from '@/src/components/Custom3DView.vue';function useCvrEffect(config: Ref<Maybe<VolumeColorConfig>>,imageRep: Ref<vtkVolumeRepresentationProxy | null>,viewProxy: Ref<vtkLPSView3DProxy>
) {const cvrParams = computed(() => config.value?.cvr);const repMapper = computedWithControl(imageRep,() => imageRep.value?.getMapper() as vtkVolumeMapper | undefined);const image = computedWithControl(imageRep,() => imageRep.value?.getInputDataSet() as vtkImageData | null | undefined);const volume = computedWithControl(imageRep,() => imageRep.value?.getVolumes()[0]);const renderer = computed(() => viewProxy.value.getRenderer());const isAnimating = isViewAnimating(viewProxy);const cvrEnabled = computed(() => {const enabled = !!cvrParams.value?.enabled;const animating = isAnimating.value;return enabled && !animating;});const requestRender = () => {if (!isAnimating.value) {viewProxy.value.renderLater();}};// lightsconst volumeCenter = computed(() => {if (!volume.value) return null;const volumeBounds = volume.value.getBounds();return [(volumeBounds[0] + volumeBounds[1]) / 2,(volumeBounds[2] + volumeBounds[3]) / 2,(volumeBounds[4] + volumeBounds[5]) / 2,] as Vector3;});const lightFollowsCamera = computed(() => cvrParams.value?.lightFollowsCamera ?? true);watch([volumeCenter, renderer, cvrEnabled, lightFollowsCamera],([center, ren, enabled, lightFollowsCamera_]) => {if (!center) return;if (ren.getLights().length === 0) {ren.createLight();}const light = ren.getLights()[0];if (enabled) {light.setFocalPoint(...center);light.setColor(1, 1, 1);light.setIntensity(1);light.setConeAngle(90);light.setPositional(true);ren.setTwoSidedLighting(false);if (lightFollowsCamera_) {light.setLightTypeToHeadLight();ren.updateLightsGeometryToFollowCamera();} else {light.setLightTypeToSceneLight();}} else {light.setPositional(false);}requestRender();},{ immediate: true });// sampling distanceconst volumeQuality = computed(() => cvrParams.value?.volumeQuality);watch([volume, image, repMapper, volumeQuality, cvrEnabled, isAnimating],([volume_, image_, mapper, volumeQuality_, enabled, animating]) => {if (!volume_ || !mapper || volumeQuality_ == null || !image_) return;if (animating) {mapper.setSampleDistance(0.75);mapper.setMaximumSamplesPerRay(1000);mapper.setGlobalIlluminationReach(0);mapper.setComputeNormalFromOpacity(false);} else {const dims = image_.getDimensions();const spacing = image_.getSpacing();const spatialDiagonal = vec3.length(vec3.fromValues(dims[0] * spacing[0],dims[1] * spacing[1],dims[2] * spacing[2]));// Use the average spacing for sampling by defaultlet sampleDistance = spacing.reduce((a, b) => a + b) / 3.0;// Adjust the volume sampling by the quality slider valuesampleDistance /= volumeQuality_ > 1 ? 0.5 * volumeQuality_ ** 2 : 1.0;const samplesPerRay = spatialDiagonal / sampleDistance + 1;mapper.setMaximumSamplesPerRay(samplesPerRay);mapper.setSampleDistance(sampleDistance);// Adjust the global illumination reach by volume quality slidermapper.setGlobalIlluminationReach(enabled ? 0.25 * volumeQuality_ : 0);mapper.setComputeNormalFromOpacity(!enabled && volumeQuality_ > 2);}requestRender();},{ immediate: true });// volume propertiesconst ambient = computed(() => cvrParams.value?.ambient ?? 0);const diffuse = computed(() => cvrParams.value?.diffuse ?? 0);const specular = computed(() => cvrParams.value?.specular ?? 0);watch([volume, image, ambient, diffuse, specular, cvrEnabled],([volume_, image_, ambient_, diffuse_, specular_, enabled]) => {if (!volume_ || !image_) return;const property = volume_.getProperty();property.setScalarOpacityUnitDistance(0,(0.5 * getDiagonalLength(image_.getBounds())) /Math.max(...image_.getDimensions()));property.setShade(true);property.setUseGradientOpacity(0, !enabled);property.setGradientOpacityMinimumValue(0, 0.0);const dataRange = image_.getPointData().getScalars().getRange();property.setGradientOpacityMaximumValue(0,(dataRange[1] - dataRange[0]) * 0.01);property.setGradientOpacityMinimumOpacity(0, 0.0);property.setGradientOpacityMaximumOpacity(0, 1.0);// do not toggle these parameters when animatingproperty.setAmbient(enabled ? ambient_ : DEFAULT_AMBIENT);property.setDiffuse(enabled ? diffuse_ : DEFAULT_DIFFUSE);property.setSpecular(enabled ? specular_ : DEFAULT_SPECULAR);requestRender();},{ immediate: true });// volumetric scattering blendingconst useVolumetricScatteringBlending = computed(() => cvrParams.value?.useVolumetricScatteringBlending ?? false);const volumetricScatteringBlending = computed(() => cvrParams.value?.volumetricScatteringBlending ?? 0);watch([useVolumetricScatteringBlending,volumetricScatteringBlending,repMapper,cvrEnabled,],([useVsb, vsb, mapper, enabled]) => {if (!mapper) return;if (enabled && useVsb) {mapper.setVolumetricScatteringBlending(vsb);} else {mapper.setVolumetricScatteringBlending(0);}requestRender();},{ immediate: true });// local ambient occlusionconst useLocalAmbientOcclusion = computed(() => cvrParams.value?.useLocalAmbientOcclusion ?? false);const laoKernelSize = computed(() => cvrParams.value?.laoKernelSize ?? 0);const laoKernelRadius = computed(() => cvrParams.value?.laoKernelRadius ?? 0);watch([useLocalAmbientOcclusion,laoKernelSize,laoKernelRadius,repMapper,cvrEnabled,],([useLao, kernelSize, kernelRadius, mapper, enabled]) => {if (!mapper) return;if (enabled && useLao) {mapper.setLocalAmbientOcclusion(true);mapper.setLAOKernelSize(kernelSize);mapper.setLAOKernelRadius(kernelRadius);} else {mapper.setLocalAmbientOcclusion(false);mapper.setLAOKernelSize(0);mapper.setLAOKernelRadius(0);}requestRender();},{ immediate: true });
}function useColoringEffect(config: Ref<Maybe<ColoringConfig>>,imageRep: Ref<vtkVolumeRepresentationProxy | null>,viewProxy: Ref<vtkLPSView3DProxy>
) {const colorBy = computed(() => config.value?.colorBy);const colorTransferFunction = computed(() => config.value?.transferFunction);const opacityFunction = computed(() => config.value?.opacityFunction);const proxyManager = useProxyManager();watch([imageRep, colorBy, colorTransferFunction, opacityFunction],([rep, colorBy_, colorFunc, opacityFunc]) => {if (!rep || !colorBy_ || !colorFunc || !opacityFunc || !proxyManager) {return;}const { arrayName, location } = colorBy_;const lut = proxyManager.getLookupTable(arrayName);lut.setMode(LookupTableProxyMode.Preset);lut.setPresetName(colorFunc.preset);lut.setDataRange(...colorFunc.mappingRange);const pwf = proxyManager.getPiecewiseFunction(arrayName);pwf.setMode(opacityFunc.mode);pwf.setDataRange(...opacityFunc.mappingRange);switch (opacityFunc.mode) {case vtkPiecewiseFunctionProxy.Mode.Gaussians:pwf.setGaussians(opacityFunc.gaussians);break;case vtkPiecewiseFunctionProxy.Mode.Points: {const opacityPoints = getShiftedOpacityFromPreset(opacityFunc.preset,opacityFunc.mappingRange,opacityFunc.shift,opacityFunc.shiftAlpha);if (opacityPoints) {pwf.setPoints(opacityPoints);}break;}case vtkPiecewiseFunctionProxy.Mode.Nodes:pwf.setNodes(opacityFunc.nodes);break;default:}if (rep) {// control color range manuallyrep.setRescaleOnColorBy(false);rep.setColorBy(arrayName, location);}// Need to trigger a render for when we are restoring from a state fileviewProxy.value.renderLater();},{ immediate: true });
}export default defineComponent({props: {id: {type: String,required: true,},viewDirection: {type: String as PropType<LPSAxisDir>,required: true,},viewUp: {type: String as PropType<LPSAxisDir>,required: true,},},components: {ViewOverlayGrid,CropTool,PanTool,Custom3DView,},setup(props) {const modelStore = useModelStore();const volumeColoringStore = useVolumeColoringStore();const viewCameraStore = useViewCameraStore();const { id: viewID, viewDirection, viewUp } = toRefs(props);const vtkContainerRef = ref<HTMLElement>();// --- computed vars --- //const {currentImageID: curImageID,currentImageMetadata: curImageMetadata,currentImageData,isImageLoading,} = useCurrentImage();// --- view proxy setup --- //const { viewProxy, setContainer: setViewProxyContainer } =useViewProxy<vtkLPSView3DProxy>(viewID, ViewProxyType.Volume);onMounted(() => {viewProxy.value.setOrientationAxesVisibility(true);viewProxy.value.setOrientationAxesType('cube');viewProxy.value.setBackground([0, 0, 0, 0]);setViewProxyContainer(vtkContainerRef.value);});onBeforeUnmount(() => {setViewProxyContainer(null);viewProxy.value.setContainer(null);});useResizeObserver(vtkContainerRef, () => viewProxy.value.resize());// --- scene setup --- //const { baseImageRep } = useSceneBuilder<vtkVolumeRepresentationProxy>(viewID,{baseImage: curImageID,models: computed(() => modelStore.idList),});// --- picking --- //// disables picking for crop control and morewatch(baseImageRep,(rep) => {if (rep) {rep.getVolumes().forEach((volume) => volume.setPickable(false));}},{ immediate: true });// --- widget manager --- //const { widgetManager } = useWidgetManager(viewProxy);provide(VTKThreeViewWidgetManager, widgetManager);// --- camera setup --- //const { cameraUpVec, cameraDirVec } = useCameraOrientation(viewDirection,viewUp,curImageMetadata);const resetCamera = () => {const bounds = curImageMetadata.value.worldBounds;const center = [(bounds[0] + bounds[1]) / 2,(bounds[2] + bounds[3]) / 2,(bounds[4] + bounds[5]) / 2,] as vec3;viewProxy.value.updateCamera(cameraDirVec.value,cameraUpVec.value,center);viewProxy.value.resetCamera();viewProxy.value.renderLater();};watch([baseImageRep, cameraDirVec, cameraUpVec],() => {const cameraConfig = viewCameraStore.getConfig(viewID.value,curImageID.value);// We don't want to reset the camera if we have a config we are restoringif (!cameraConfig) {// nextTick ensures resetCamera gets called after// useSceneBuilder refreshes the scene.nextTick(resetCamera);}},{immediate: true,});const { restoreCameraConfig } = usePersistCameraConfig(viewID,curImageID,viewProxy,'position','focalPoint','directionOfProjection','viewUp');watch(curImageID, () => {// See if we have a camera configuration to restoreconst cameraConfig = viewCameraStore.getConfig(viewID.value,curImageID.value);if (cameraConfig) {restoreCameraConfig(cameraConfig);viewProxy.value.getRenderer().resetCameraClippingRange();viewProxy.value.renderLater();}});// --- coloring setup --- //const volumeColorConfig = computed(() =>volumeColoringStore.getConfig(viewID.value, curImageID.value));watch([viewID, curImageID],() => {if (curImageID.value &&currentImageData.value &&!volumeColorConfig.value) {volumeColoringStore.resetToDefaultColoring(viewID.value,curImageID.value,currentImageData.value);}},{ immediate: true });// --- CVR parameters --- //useCvrEffect(volumeColorConfig, baseImageRep, viewProxy);// --- coloring --- //useColoringEffect(volumeColorConfig, baseImageRep, viewProxy);// --- cropping planes --- //const cropStore = useCropStore();const croppingPlanes = cropStore.getComputedVTKPlanes(curImageID);watch(croppingPlanes,(planes, oldPlanes) => {const mapper = baseImageRep.value?.getMapper();if (!mapper ||!planes ||(oldPlanes && croppingPlanesEqual(planes, oldPlanes)))return;mapper.removeAllClippingPlanes();planes.forEach((plane) => mapper.addClippingPlane(plane));mapper.modified();viewProxy.value.renderLater();},{ immediate: true });// --- Listen to ResetViews event --- //const events = useResetViewsEvents();events.onClick(() => resetCamera());// --- template vars --- //return {vtkContainerRef,viewID,active: false,topLeftLabel: computed(() =>volumeColorConfig.value?.transferFunction.preset.replace(/-/g, ' ') ??''),isImageLoading,resetCamera,};},
});
</script><style scoped>
.model-container {width: 100%;height: 600px;position: relative;
}
</style><style scoped src="@/src/components/styles/vtk-view.css"></style>
<style scoped src="@/src/components/styles/utils.css"></style><style scoped>
.vtk-three-container {background-color: black;grid-template-columns: auto;
}
</style>

2.新增Custom3DView.vue

在這里插入圖片描述

src/components目錄下新增Custom3DView.vue。用來顯示后端Python生成的stl。

全部代碼如下:

<template><div ref="container" class="model-container"></div>
</template><script>
import * as THREE from 'three';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls';
import { STLLoader } from 'three/examples/jsm/loaders/STLLoader';
import { toRaw } from 'vue';export default {data() {return {loadingProgress: 0,loadError: null,animateId: null};},mounted() {this.initThreeContext();this.loadSTLModel();this.setupAnimation();},beforeDestroy() {this.cleanupResources();},methods: {initThreeContext() {const container = this.$refs.container;// 場景配置this._scene = new THREE.Scene();this._scene.background = new THREE.Color(0x000000);// 相機配置this._camera = new THREE.PerspectiveCamera(45, // 縮小視角增加近景效果container.clientWidth / container.clientHeight,0.1,500 // 縮小可視范圍提升渲染性能); this._camera.position.set(30, 30, 30); // 初始位置更靠近模型// 渲染器配置(網頁7的黑色背景方案)this._renderer = new THREE.WebGLRenderer({ antialias: true,alpha: true // 保留alpha通道以備后續擴展});this._renderer.setClearColor(0x000000, 1); // 雙重確保背景顏色this._renderer.setSize(container.clientWidth, container.clientHeight);container.appendChild(this._renderer.domElement);// 光源優化const ambientLight = new THREE.AmbientLight(0x404040);const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);directionalLight.position.set(15, 15, 15);this._scene.add(ambientLight, directionalLight);// 控制器配置this._controls = new OrbitControls(toRaw(this._camera), this._renderer.domElement);this._controls.enableDamping = true;this._controls.dampingFactor = 0.05;},loadSTLModel() {const objSTLLoader=new STLLoader()objSTLLoader.crossOrigin='Anonymous'objSTLLoader.load( 'https://stl所在路徑.stl', geometry => {// 添加模型前清空舊模型this.clearExistingModel();// 材質配置(淺灰色方案)const material  = new THREE.MeshPhongMaterial({color: 0xcccccc, // 淺灰色specular: 0x222222, shininess: 150, side: THREE.DoubleSide});const mesh = new THREE.Mesh(geometry, material);geometry.center();mesh.scale.set(0.1, 0.1, 0.1);// 自動聚焦模型const box = new THREE.Box3().setFromObject(mesh);const center = box.getCenter(new THREE.Vector3());toRaw(this._camera).lookAt(center);toRaw(this._scene).add(mesh); },progress => {this.loadingProgress = (progress.loaded / progress.total) * 100},error => {this.loadError = '模型加載失敗,請檢查網絡或文件路徑'});},setupAnimation() {const animate = () => {this.animateId = requestAnimationFrame(animate);toRaw(this._controls).update();this._renderer.render(toRaw(this._scene), toRaw(this._camera));};animate();},cleanupResources() {cancelAnimationFrame(this.animateId);toRaw(this._controls).dispose();this._renderer.dispose();toRaw(this._scene).traverse(obj => {if (obj.isMesh) {obj.geometry.dispose();obj.material.dispose();}});}}
};
</script><style scoped>
.model-container {width: 100%;height: 600px;position: relative;background: #000; /* 備用黑色背景 */
}
</style>

3.Python生成stl三維文件

在服務端用Python生成stl:

from pydicom import dcmread
import pylibjpegimport numpy as np
import pydicom
import pydicom.pixel_data_handlers.gdcm_handler as gdcm_handlerimport os
import matplotlib.pyplot as plt
from glob import glob
from mpl_toolkits.mplot3d.art3d import Poly3DCollection
import scipy.ndimage
from skimage import measure
from mpl_toolkits import mplot3d
from stl import mesh
import trimesh
pydicom.config.image_handlers = [None, gdcm_handler]
pydicom.config.image_handlers = ['gdcm_handler']def load_scan(path):slices = []# count = 0for s in os.listdir(path):ds = pydicom.dcmread(path + '/' + s, force=True)ds.PhotometricInterpretation = 'YBR_FULL'if s != '.DS_Store':  # This is for AttributeError: 'FileDataset' object has no attribute 'InstanceNumber'slices.append(ds)slices.sort(key=lambda x: int(x.InstanceNumber))try:slice_thickness = np.abs(slices[0].ImagePositionPatient[2] - slices[1].ImagePositionPatient[2])except:slice_thickness = np.abs(slices[0].SliceLocation - slices[1].SliceLocation)for s in slices:s.SliceThickness = slice_thicknessreturn slicesdef get_pixels_hu(scans):image = np.stack([s.pixel_array for s in scans])image = image.astype(np.int16)image[image == -2000] = 0# Convert to Hounsfield units (HU)intercept = scans[0].RescaleInterceptslope = scans[0].RescaleSlopeif slope != 1:image = slope * image.astype(np.float64)image = image.astype(np.int16)image += np.int16(intercept)return np.array(image, dtype=np.int16)def make_mesh(image, threshold=-300, step_size=1):print("Transposing surface")p = image.transpose(2, 1, 0)print("Calculating surface")verts, faces, norm, val = measure.marching_cubes(p, threshold, step_size=step_size, allow_degenerate=True)return verts, facesdef resample(image, scan, new_spacing=[1, 1, 1]):# Determine current pixel spacing, change this function to get better resultspacing = [float(scan[0].SliceThickness)] + [float(i) for i in scan[0].PixelSpacing]spacing = np.array(spacing)resize_factor = [spacing[0] / new_spacing[0], spacing[1] / new_spacing[1], spacing[2] / new_spacing[2]]new_real_shape = np.multiply(image.shape, resize_factor)new_shape = np.round(new_real_shape)real_resize_factor = new_shape / image.shapenew_spacing = spacing / real_resize_factorimage = scipy.ndimage.interpolation.zoom(image, real_resize_factor)return image, new_spacingif __name__ == "__main__":from matplotlib.cm import get_cmapimport matplotlib.colors as mcolorsdata_path = "/mnt/data_18T/data/口腔/CBCT及三維重建/dicom"output_path = "/mnt/data_18T/data/口腔/CBCT及三維重建/stl_path/"if not os.path.exists(output_path):  # create the output pathos.mkdir(output_path)patient = load_scan(data_path)images = get_pixels_hu(patient)imgs_after_resamp, spacing = resample(images.astype(np.float64), patient, [1, 0.5, 1])v, f = make_mesh(imgs_after_resamp, 350, 1)# save the stl filevertices = vfaces = f# 創建顏色列表colors = get_cmap('Greens')(np.linspace(0, 1, len(vertices)))colors = mcolors.to_rgba_array(colors)mesh = trimesh.Trimesh(vertices=vertices, faces=faces)mesh.export(output_path + 'cube2.stl', file_type="stl")

4.最終效果

在這里插入圖片描述
在這里插入圖片描述
注:我的是集顯,配置不算高,在使用stl顯示三維的情況下,很流暢。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/bicheng/74342.shtml
繁體地址,請注明出處:http://hk.pswp.cn/bicheng/74342.shtml
英文地址,請注明出處:http://en.pswp.cn/bicheng/74342.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

debug - 安裝.msi時,為所有用戶安裝程序

文章目錄 debug - 安裝.msi時&#xff0c;為所有用戶安裝程序概述筆記試試在目標.msi后面直接加參數的測試 備注備注END debug - 安裝.msi時&#xff0c;為所有用戶安裝程序 概述 為了測試&#xff0c;裝了一個test.msi. 安裝時&#xff0c;只有安裝路徑的選擇&#xff0c;沒…

Java Stream兩種list判斷字符串是否存在方案

這里寫自定義目錄標題 背景初始化方法一、filter過濾方法二、anyMatch匹配 背景 在項目開發中&#xff0c;經常遇到篩選list中是否包含某個子字符串&#xff0c;有多種方式&#xff0c;本篇主要介紹stream流的filter和anyMatch兩種方案&#xff0c;記錄下來&#xff0c;方便備…

DeepSeek vs 通義大模型:誰將主導中國AI的未來戰場?

當你在深夜調試代碼時,是否幻想過AI伙伴能真正理解你的需求?當企業面對海量數據時,是否期待一個真正智能的決策大腦? 這場由DeepSeek和通義領銜的大模型之爭,正在重塑中國AI產業的競爭格局。本文將為你揭開兩大技術巨頭的終極對決! 一、顛覆認知的技術突破 1.1 改變游戲…

3. 軸指令(omron 機器自動化控制器)——>MC_SetOverride

機器自動化控制器——第三章 軸指令 12 MC_SetOverride變量?輸入變量?輸出變量?輸入輸出變量 功能說明?時序圖?重啟運動指令?多重啟動運動指令?異常 MC_SetOverride 變更軸的目標速度。 指令名稱FB/FUN圖形表現ST表現MC_SetOverride超調值設定FBMC_SetOverride_instan…

從像素到世界:自動駕駛視覺感知的坐標變換體系

接著上一篇 如何讓自動駕駛汽車“看清”世界?坐標映射與數據融合詳解的概述,這一篇詳細講解自動駕駛多目視覺系統設計原理,并給出應用示例。 摘要 在自動駕駛系統中,準確的環境感知是實現路徑規劃與決策控制的基礎。本文系統性地解析圖像坐標系、像素坐標系、相機坐標系與…

附錄B ISO15118-20測試命令

本章節給出ISO15118-20協議集的V2G命令&#xff0c;包含json、xml&#xff0c;并且根據exiCodec.jar編碼得到exi內容&#xff0c; 讀者可以參考使用&#xff0c;測試編解碼庫是否能正確編解碼。 B.1 supportedAppProtocolReq json: {"supportedAppProtocolReq": {…

VLAN章節學習

為什么會有vlan這個技術&#xff1f; 1.通過劃分廣播域來降低廣播風暴導致的設備性能下降&#xff1b; 2.提高網絡管理的靈活性和通過隔離網絡帶來的安全性&#xff1b; 3.在成本不變的情況下增加更多的功能性&#xff1b; VLAN又稱虛擬局域網&#xff08;再此擴展&#xf…

FPGA時鐘約束

提示&#xff1a;文章寫完后&#xff0c;目錄可以自動生成&#xff0c;如何生成可參考右邊的幫助文檔 目錄 前言 一、Create_clock 前言 時鐘周期約束&#xff0c;就是對時鐘進行約束。 一、Create_clock create_clock -name <name> -period <period> -waveform …

機房布局和布線的最佳實踐:如何打造高效、安全的機房環境

機房布局和布線的最佳實踐:如何打造高效、安全的機房環境 大家好,我是Echo_Wish。今天我們來聊聊機房布局和布線的問題,這可是數據中心和IT運維中的一個非常重要的環節。不管是剛剛接觸運維的新人,還是已經摸爬滾打多年的老兵,都應該對機房的布局和布線有一個清晰的認識。…

spring-security原理與應用系列:建造者

目錄 1.構建過程 AbstractSecurityBuilder AbstractConfiguredSecurityBuilder WebSecurity 2.建造者類圖 SecurityBuilder ???????AbstractSecurityBuilder ???????AbstractConfiguredSecurityBuilder ???????WebSecurity 3.小結 緊接上一篇文…

OpenHarmony子系統開發 - 電池管理(二)

OpenHarmony子系統開發 - 電池管理&#xff08;二&#xff09; 五、充電限流限壓定制開發指導 概述 簡介 OpenHarmony默認提供了充電限流限壓的特性。在對終端設備進行充電時&#xff0c;由于環境影響&#xff0c;可能會導致電池溫度過高&#xff0c;因此需要對充電電流或電…

xy軸不等比縮放問題——AUTOCAD c#二次開發

在 AutoCAD .net api里&#xff0c;部分實體&#xff0c;像文字、屬性、插入塊等&#xff0c;是不支持非等比縮放的。 如需對AutoCAD中圖形進行xyz方向不等比縮放&#xff0c;則需進行額外的函數封裝。 選擇圖元&#xff0c;指定縮放基準點&#xff0c;scaleX 0.5, scaleY …

如何在 HTML 中創建一個有序列表和無序列表,它們的語義有何不同?

大白話如何在 HTML 中創建一個有序列表和無序列表&#xff0c;它們的語義有何不同&#xff1f; 1. HTML 中有序列表和無序列表的基本概念 在 HTML 里&#xff0c;列表是一種用來組織信息的方式。有序列表就是帶有編號的列表&#xff0c;它可以讓內容按照一定的順序呈現&#…

kafka的文章

1.面試的問題 要點 至多一次、恰好一次數據一致性超時重試、冪等消息順序消息擠壓延時消息 1.1 kafaka 生產消息的過程。 在消息發送的過程中&#xff0c;涉及到了兩個線程&#xff0c;一個是main 線程&#xff0c;一個是sender 線程。在main 線程中創建了一個雙端隊列 Reco…

以mysql 為例,增刪改查語法及其他高級特性

以下是 MySQL 的 增刪改查語法及 高級特性的詳細整理&#xff0c;結合示例說明&#xff1a; 1. 基礎操作&#xff08;CRUD&#xff09; (1) 創建數據&#xff08;INSERT&#xff09; -- 單條插入 INSERT INTO users (id, name, email) VALUES (1, Alice, aliceexample.com);…

Postman最新詳細安裝及使用教程【附安裝包】

一、Postman介紹 ?Postman是一個功能強大的API測試工具&#xff0c;主要用于模擬和測試各種HTTP請求&#xff0c;支持GET、POST、PUT、DELETE等多種請求方法。?通過Postman&#xff0c;用戶可以發送請求并查看返回的響應&#xff0c;檢查響應的內容和狀態&#xff0c;從而驗…

第十三章 : Names in Templates_《C++ Templates》notes

Names in Templates 重難點多選題設計題 重難點 1. 名稱分類與基本概念 知識點&#xff1a; 限定名&#xff08;Qualified Name&#xff09;&#xff1a;使用::或.顯式指定作用域的名稱&#xff08;如std::vector&#xff09;非限定名&#xff08;Unqualified Name&#xff0…

整合vue+Element UI 開發管理系統

1、 安裝 Node.js 和 npm 確保安裝了 Node.js 和 npm。可以通過 Node.js 官網 下載。 2、 創建 Vue 項目 安裝cli npm install -g vue/cli 使用 Vue CLI 創建一個新的 Vue 項目。 vue create admin-system cd admin-system npm run serve 出現這個頁面表示vue創建成功 安…

3. 軸指令(omron 機器自動化控制器)——>MC_Stop

機器自動化控制器——第三章 軸指令 9 MC_Stop變量?輸入變量?輸出變量?輸入輸出變量 功能說明?指令詳情?時序圖?重啟運動指令?多重啟動運動指令?異常 MC_Stop 使軸減速停止。 指令名稱FB/FUN圖形表現ST表現MC_Stop強制停止FBMC_Stop_instance (Axis :《參數》 ,Execu…

C#中修飾符——abstract、virtual

一、多態簡介 在面向對象編程的過程中&#xff0c;多態體現出來的是【一個接口&#xff0c;多個功能】&#xff1b;多態性體現在2個方面&#xff1a; 1、程序運行時&#xff0c;在方法參數、集合或數組等位置&#xff0c;派生類對象可以作為基類的對象處理&#xff1b;這樣該對…