import os
import sys
import numpy as np
import pydicom
import dicom_numpy
import vtk
from vtk.util import numpy_support
from PyQt5.QtWidgets import (
QApplication, QMainWindow, QFileDialog,
QVBoxLayout, QHBoxLayout, QWidget,
QSlider, QLabel, QPushButton, QMessageBox, QProgressDialog
)
from PyQt5.QtCore import Qt, QThread, pyqtSignal
import matplotlib.pyplot as plt
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
from vtk.qt.QVTKRenderWindowInteractor import QVTKRenderWindowInteractor
def fix_qt_plugin_path():
"""解决 Qt 平台插件无法初始化的问题"""
try:
from PyQt5.QtCore import QLibraryInfo
plugin_path = QLibraryInfo.location(QLibraryInfo.PluginsPath)
if os.path.exists(plugin_path):
os.environ['QT_QPA_PLATFORM_PLUGIN_PATH'] = plugin_path
return
except ImportError:
pass
paths_to_try = [
os.path.join(sys.prefix, 'Lib', 'site-packages', 'PyQt5', 'Qt5', 'plugins'),
os.path.join(sys.prefix, 'Library', 'plugins'),
os.path.join(os.path.dirname(os.path.abspath(__file__)), 'Qt', 'plugins')
]
for path in paths_to_try:
if os.path.exists(path):
os.environ['QT_QPA_PLATFORM_PLUGIN_PATH'] = path
break
fix_qt_plugin_path()
class DICOMLoader(QThread):
progress_updated = pyqtSignal(int)
loading_complete = pyqtSignal(object, object, object, object) # volume_array, spacing, origin, vtk_image
loading_failed = pyqtSignal(str)
def __init__(self, directory):
super().__init__()
self.directory = directory
def run(self):
try:
# 获取所有DICOM文件
dicom_files = self.get_all_dicom_files(self.directory)
if not dicom_files:
self.loading_failed.emit("未找到DICOM文件")
return
# 读取并分组DICOM文件
series_dict = self.read_and_group_dicom_files(dicom_files)
if not series_dict:
self.loading_failed.emit("没有有效的DICOM图像")
return
# 选择第一个系列进行处理
series_uid = next(iter(series_dict))
datasets = series_dict[series_uid]
# 处理DICOM数据集
volume_array, spacing, origin = self.process_dicom_datasets(datasets)
# 转换为VTK图像
vtk_image = self.numpy_to_vtk(volume_array, spacing, origin)
self.loading_complete.emit(volume_array, spacing, origin, vtk_image)
except Exception as e:
self.loading_failed.emit(f"加载DICOM文件失败: {str(e)}")
def get_all_dicom_files(self, directory):
"""获取目录下所有DICOM文件"""
dicom_files = []
for root, _, files in os.walk(directory):
for file in files:
if file.lower().endswith(('.dcm', '.dicm', '.dicom')):
dicom_files.append(os.path.join(root, file))
return dicom_files
def read_and_group_dicom_files(self, file_paths):
"""读取并分组DICOM文件"""
series_dict = {}
for i, file_path in enumerate(file_paths):
try:
ds = pydicom.dcmread(file_path)
# 检查是否包含像素数据
if not hasattr(ds, 'pixel_array'):
continue
# 检查必要的定位信息
required_tags = ['ImagePositionPatient', 'ImageOrientationPatient', 'PixelSpacing']
if not all(hasattr(ds, tag) for tag in required_tags):
continue
# 按系列实例UID分组
series_uid = ds.SeriesInstanceUID
if series_uid not in series_dict:
series_dict[series_uid] = []
series_dict[series_uid].append(ds)
# 更新进度
self.progress_updated.emit(int((i + 1) / len(file_paths) * 100))
except Exception as e:
print(f"无法读取文件 {file_path}: {str(e)}")
continue
# 对每个系列按切片位置排序
for series_uid in series_dict:
try:
series_dict[series_uid].sort(key=lambda ds: float(ds.ImagePositionPatient[2]))
except:
pass # 如果排序失败,保持原顺序
return series_dict
def process_dicom_datasets(self, datasets):
"""处理DICOM数据集并返回体积数据"""
try:
# 使用dicom-numpy组合体积数据
volume_array, ijk_to_xyz = dicom_numpy.combine_slices(datasets)
# 获取间距和原点
spacing = np.array([
np.linalg.norm(ijk_to_xyz[:3, 0]), # X spacing
np.linalg.norm(ijk_to_xyz[:3, 1]), # Y spacing
np.linalg.norm(ijk_to_xyz[:3, 2]) # Z spacing
])
origin = ijk_to_xyz[:3, 3]
# 调整数组方向以匹配VTK坐标系
volume_array = np.transpose(volume_array, (2, 1, 0))
return volume_array, spacing, origin
except dicom_numpy.DicomImportException as e:
raise Exception(f"DICOM导入错误: {str(e)}")
except Exception as e:
raise Exception(f"处理DICOM数据时出错: {str(e)}")
def numpy_to_vtk(self, volume_array, spacing, origin):
"""将numpy数组转换为VTK图像"""
# 确保数组是连续的
volume_array = np.ascontiguousarray(volume_array)
# 根据数据类型选择合适的VTK类型
if volume_array.dtype == np.uint8:
vtk_type = vtk.VTK_UNSIGNED_CHAR
elif volume_array.dtype == np.int16:
vtk_type = vtk.VTK_SHORT
elif volume_array.dtype == np.uint16:
vtk_type = vtk.VTK_UNSIGNED_SHORT
elif volume_array.dtype == np.float32:
vtk_type = vtk.VTK_FLOAT
else:
# 不支持的格式转换为float32
volume_array = volume_array.astype(np.float32)
vtk_type = vtk.VTK_FLOAT
# 转换为VTK数组
vtk_data = numpy_support.numpy_to_vtk(
volume_array.ravel(),
deep=True,
array_type=vtk_type
)
# 创建VTK图像
image = vtk.vtkImageData()
image.SetDimensions(volume_array.shape)
image.SetSpacing(spacing)
image.SetOrigin(origin)
image.GetPointData().SetScalars(vtk_data)
return image
class MedicalViewer(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("DICOM 三维可视化工具")
self.setGeometry(100, 100, 1200, 800)
self.current_path_points = []
self.path_planning_mode = False
self.slice_views = {}
self.init_ui()
def init_ui(self):
central_widget = QWidget()
self.setCentralWidget(central_widget)
main_layout = QHBoxLayout(central_widget)
# 左侧控制面板
control_panel = QWidget()
control_layout = QVBoxLayout(control_panel)
control_layout.setContentsMargins(5, 5, 5, 5)
self.load_button = QPushButton("加载 DICOM 文件夹")
self.load_button.clicked.connect(self.load_dicom)
control_layout.addWidget(self.load_button)
# 切片控制滑块
self.axial_slider = self.create_slice_control("轴向切片:")
self.coronal_slider = self.create_slice_control("冠状切片:")
self.sagittal_slider = self.create_slice_control("矢状切片:")
control_layout.addWidget(self.axial_slider['container'])
control_layout.addWidget(self.coronal_slider['container'])
control_layout.addWidget(self.sagittal_slider['container'])
# 窗宽窗位控制
self.ww_slider = self.create_window_control("窗宽:")
self.wl_slider = self.create_window_control("窗位:")
control_layout.addWidget(self.ww_slider['container'])
control_layout.addWidget(self.wl_slider['container'])
# 等值面阈值控制
self.threshold_slider = self.create_threshold_control("等值面阈值:")
control_layout.addWidget(self.threshold_slider['container'])
control_layout.addStretch()
# 路径规划按钮
self.path_button = QPushButton("开始路径规划")
self.path_button.clicked.connect(self.toggle_path_planning)
self.path_button.setEnabled(False)
control_layout.addWidget(self.path_button)
self.clear_path_button = QPushButton("清除路径")
self.clear_path_button.clicked.connect(self.clear_path)
self.clear_path_button.setEnabled(False)
control_layout.addWidget(self.clear_path_button)
# 导出按钮
self.export_mesh_button = QPushButton("导出网格为 DAE")
self.export_mesh_button.clicked.connect(self.export_mesh_to_dae)
self.export_mesh_button.setEnabled(False)
control_layout.addWidget(self.export_mesh_button)
self.export_path_button = QPushButton("导出路径为 DAE")
self.export_path_button.clicked.connect(self.export_path_to_dae)
self.export_path_button.setEnabled(False)
control_layout.addWidget(self.export_path_button)
# 右侧显示区域
display_panel = QWidget()
display_layout = QVBoxLayout(display_panel)
# 2D 切片显示
self.figure, self.axes = plt.subplots(1, 3, figsize=(12, 4))
self.figure.subplots_adjust(left=0.02, right=0.98, bottom=0.02, top=0.95, wspace=0.05, hspace=0)
self.canvas = FigureCanvas(self.figure)
display_layout.addWidget(self.canvas)
# 初始化2D视图
self.slice_views["axial"] = {
"axis": self.axes[0],
"slider": self.axial_slider['slider']
}
self.slice_views["coronal"] = {
"axis": self.axes[1],
"slider": self.coronal_slider['slider']
}
self.slice_views["sagittal"] = {
"axis": self.axes[2],
"slider": self.sagittal_slider['slider']
}
# 连接信号
for view in self.slice_views.values():
view["axis"].axis("off")
view["image"] = None
view["slider"].valueChanged.connect(self.update_slice_views)
# 3D VTK 渲染窗口
self.vtk_widget = QVTKRenderWindowInteractor()
display_layout.addWidget(self.vtk_widget)
main_layout.addWidget(control_panel, stretch=1)
main_layout.addWidget(display_panel, stretch=4)
def create_slice_control(self, label_text):
"""创建切片控制滑块"""
container = QWidget()
layout = QVBoxLayout(container)
layout.setContentsMargins(0, 0, 0, 0)
label = QLabel(label_text)
layout.addWidget(label)
slider = QSlider(Qt.Horizontal)
slider.setEnabled(False)
layout.addWidget(slider)
return {'container': container, 'slider': slider}
def create_window_control(self, label_text):
"""创建窗宽窗位控制滑块"""
container = QWidget()
layout = QVBoxLayout(container)
layout.setContentsMargins(0, 0, 0, 0)
label = QLabel(label_text)
layout.addWidget(label)
slider = QSlider(Qt.Horizontal)
slider.setRange(0, 4000)
slider.setValue(2000)
slider.setEnabled(False)
slider.valueChanged.connect(self.apply_window_level)
layout.addWidget(slider)
return {'container': container, 'slider': slider}
def create_threshold_control(self, label_text):
"""创建等值面阈值控制滑块"""
container = QWidget()
layout = QVBoxLayout(container)
layout.setContentsMargins(0, 0, 0, 0)
label = QLabel(label_text)
layout.addWidget(label)
slider = QSlider(Qt.Horizontal)
slider.setRange(-1000, 1000)
slider.setValue(500)
slider.setEnabled(False)
slider.valueChanged.connect(self.update_3d_renderer)
layout.addWidget(slider)
return {'container': container, 'slider': slider}
def load_dicom(self):
"""加载DICOM文件夹"""
directory = QFileDialog.getExistingDirectory(self, "选择 DICOM 文件夹")
if not directory:
return
# 创建进度对话框
progress_dialog = QProgressDialog("正在加载DICOM文件...", "取消", 0, 100, self)
progress_dialog.setWindowTitle("加载中")
progress_dialog.setWindowModality(Qt.WindowModal)
progress_dialog.setAutoClose(True)
# 创建并启动加载线程
self.loader = DICOMLoader(directory)
self.loader.progress_updated.connect(progress_dialog.setValue)
self.loader.loading_complete.connect(self.on_dicom_loaded)
self.loader.loading_failed.connect(lambda msg: (
progress_dialog.cancel(),
QMessageBox.critical(self, "错误", msg)
))
self.loader.finished.connect(progress_dialog.deleteLater)
self.loader.start()
def on_dicom_loaded(self, volume_array, spacing, origin, vtk_image):
"""DICOM加载完成后的处理"""
self.volume_array = volume_array
self.vtk_image = vtk_image
self.spacing = spacing
self.origin = origin
# 设置滑块范围
self.axial_slider['slider'].setRange(0, volume_array.shape[0] - 1)
self.coronal_slider['slider'].setRange(0, volume_array.shape[1] - 1)
self.sagittal_slider['slider'].setRange(0, volume_array.shape[2] - 1)
# 启用滑块
self.axial_slider['slider'].setEnabled(True)
self.coronal_slider['slider'].setEnabled(True)
self.sagittal_slider['slider'].setEnabled(True)
self.threshold_slider['slider'].setEnabled(True)
# 设置初始位置
self.axial_slider['slider'].setValue(volume_array.shape[0] // 2)
self.coronal_slider['slider'].setValue(volume_array.shape[1] // 2)
self.sagittal_slider['slider'].setValue(volume_array.shape[2] // 2)
# 启用窗宽窗位控制
self.ww_slider['slider'].setEnabled(True)
self.wl_slider['slider'].setEnabled(True)
# 初始化3D视图
self.setup_3d_renderer()
# 更新2D视图
self.update_slice_views()
# 启用其他按钮
self.path_button.setEnabled(True)
self.export_mesh_button.setEnabled(True)
def setup_3d_renderer(self):
"""初始化3D渲染器"""
self.renderer = vtk.vtkRenderer()
self.vtk_widget.GetRenderWindow().AddRenderer(self.renderer)
self.interactor = self.vtk_widget.GetRenderWindow().GetInteractor()
# 初始3D重建
self.update_3d_renderer()
# 设置背景和相机
self.renderer.SetBackground(0.2, 0.3, 0.4)
self.renderer.ResetCamera()
# 添加光源
light1 = vtk.vtkLight()
light1.SetPosition(0, 0, 1)
light1.SetFocalPoint(self.renderer.GetActiveCamera().GetFocalPoint())
self.renderer.AddLight(light1)
light2 = vtk.vtkLight()
light2.SetPosition(0, 1, 0)
light2.SetFocalPoint(self.renderer.GetActiveCamera().GetFocalPoint())
self.renderer.AddLight(light2)
# 初始化交互器
self.interactor.Initialize()
self.interactor.Start()
def update_3d_renderer(self):
"""更新3D重建"""
if not hasattr(self, "vtk_image"):
return
# 移除旧的actor
if hasattr(self, "mesh_actor"):
self.renderer.RemoveActor(self.mesh_actor)
# 获取当前阈值
threshold = self.threshold_slider['slider'].value()
# Marching Cubes表面重建
marching_cubes = vtk.vtkMarchingCubes()
marching_cubes.SetInputData(self.vtk_image)
marching_cubes.SetValue(0, threshold)
# 平滑滤波器
smoother = vtk.vtkWindowedSincPolyDataFilter()
smoother.SetInputConnection(marching_cubes.GetOutputPort())
smoother.SetNumberOfIterations(20)
smoother.BoundarySmoothingOn()
smoother.FeatureEdgeSmoothingOff()
smoother.SetPassBand(0.1)
smoother.NonManifoldSmoothingOn()
smoother.NormalizeCoordinatesOn()
smoother.Update()
# 创建mapper和actor
mapper = vtk.vtkPolyDataMapper()
mapper.SetInputConnection(smoother.GetOutputPort())
mapper.ScalarVisibilityOff()
self.mesh_actor = vtk.vtkActor()
self.mesh_actor.SetMapper(mapper)
self.mesh_actor.GetProperty().SetColor(0.9, 0.75, 0.6)
self.mesh_actor.GetProperty().SetOpacity(0.8)
self.mesh_actor.GetProperty().SetSpecular(0.3)
self.mesh_actor.GetProperty().SetSpecularPower(20)
self.renderer.AddActor(self.mesh_actor)
self.vtk_widget.GetRenderWindow().Render()
# 保存平滑后的网格用于导出
self.smoothed_mesh = smoother.GetOutput()
def update_slice_views(self):
"""更新所有切片视图"""
if not hasattr(self, "volume_array"):
return
# 获取当前切片位置
axial_pos = self.axial_slider['slider'].value()
coronal_pos = self.coronal_slider['slider'].value()
sagittal_pos = self.sagittal_slider['slider'].value()
# 更新轴向视图
axial_slice = self.volume_array[axial_pos, :, :]
self.slice_views["axial"]["image"] = axial_slice
self.slice_views["axial"]["axis"].clear()
self.slice_views["axial"]["axis"].imshow(axial_slice.T, cmap="gray", origin="lower")
self.slice_views["axial"]["axis"].set_title(f"轴向: {axial_pos}/{self.volume_array.shape[0]-1}")
self.slice_views["axial"]["axis"].axis("off")
# 更新冠状视图
coronal_slice = self.volume_array[:, coronal_pos, :]
self.slice_views["coronal"]["image"] = coronal_slice
self.slice_views["coronal"]["axis"].clear()
self.slice_views["coronal"]["axis"].imshow(coronal_slice.T, cmap="gray", origin="lower")
self.slice_views["coronal"]["axis"].set_title(f"冠状: {coronal_pos}/{self.volume_array.shape[1]-1}")
self.slice_views["coronal"]["axis"].axis("off")
# 更新矢状视图
sagittal_slice = self.volume_array[:, :, sagittal_pos]
self.slice_views["sagittal"]["image"] = sagittal_slice
self.slice_views["sagittal"]["axis"].clear()
self.slice_views["sagittal"]["axis"].imshow(sagittal_slice.T, cmap="gray", origin="lower")
self.slice_views["sagittal"]["axis"].set_title(f"矢状: {sagittal_pos}/{self.volume_array.shape[2]-1}")
self.slice_views["sagittal"]["axis"].axis("off")
# 应用窗宽窗位
self.apply_window_level()
# 如果有路径点,在2D视图中显示
if hasattr(self, "current_path_points") and self.current_path_points:
self.draw_path_on_slices()
self.canvas.draw()
def apply_window_level(self):
"""应用窗宽窗位设置"""
if not hasattr(self, "volume_array"):
return
ww = self.ww_slider['slider'].value()
wl = self.wl_slider['slider'].value()
for view in self.slice_views.values():
if view["image"] is not None:
for img in view["axis"].get_images():
img.set_clim(wl - ww/2, wl + ww/2)
self.canvas.draw()
def draw_path_on_slices(self):
"""在切片上绘制路径点"""
if not self.current_path_points:
return
# 将世界坐标转换为图像坐标
for view_name, view in self.slice_views.items():
view["axis"].clear()
# 重新绘制图像
if view["image"] is not None:
view["axis"].imshow(view["image"].T, cmap="gray", origin="lower")
view["axis"].axis("off")
# 绘制路径点
for i, point in enumerate(self.current_path_points):
# 转换为图像坐标
img_coord = (np.array(point) - self.origin) / self.spacing
# 根据视图类型确定要显示的坐标
if view_name == "axial":
x, y = img_coord[1], img_coord[2] # 注意坐标顺序
current_slice = self.axial_slider['slider'].value()
if abs(img_coord[0] - current_slice) < 1.0:
view["axis"].plot(x, y, "r+", markersize=10)
view["axis"].text(x, y, str(i), color="red")
elif view_name == "coronal":
x, y = img_coord[0], img_coord[2]
current_slice = self.coronal_slider['slider'].value()
if abs(img_coord[1] - current_slice) < 1.0:
view["axis"].plot(x, y, "r+", markersize=10)
view["axis"].text(x, y, str(i), color="red")
elif view_name == "sagittal":
x, y = img_coord[0], img_coord[1]
current_slice = self.sagittal_slider['slider'].value()
if abs(img_coord[2] - current_slice) < 1.0:
view["axis"].plot(x, y, "r+", markersize=10)
view["axis"].text(x, y, str(i), color="red")
def toggle_path_planning(self):
"""切换路径规划模式"""
self.path_planning_mode = not self.path_planning_mode
if self.path_planning_mode:
self.path_button.setText("完成路径规划")
self.clear_path_button.setEnabled(False)
self.current_path_points = []
self.export_path_button.setEnabled(False)
# 设置交互回调
self.interactor.AddObserver(vtk.vtkCommand.LeftButtonPressEvent, self.add_path_point)
else:
self.path_button.setText("开始路径规划")
self.clear_path_button.setEnabled(len(self.current_path_points) > 0)
# 移除交互回调
self.interactor.RemoveObservers(vtk.vtkCommand.LeftButtonPressEvent)
if len(self.current_path_points) > 1:
self.draw_3d_path()
self.export_path_button.setEnabled(True)
def add_path_point(self, obj, event):
"""添加路径点"""
click_pos = self.interactor.GetEventPosition()
# 使用拾取器获取3D坐标
picker = vtk.vtkCellPicker()
picker.SetTolerance(0.005)
picker.Pick(click_pos[0], click_pos[1], 0, self.renderer)
if picker.GetCellId() != -1:
world_pos = picker.GetPickPosition()
self.current_path_points.append(world_pos)
# 在2D视图中显示标记
self.draw_path_on_slices()
self.canvas.draw()
def draw_3d_path(self):
"""绘制3D路径"""
if len(self.current_path_points) < 2:
return
# 如果已有路径,先移除
if hasattr(self, "path_actor"):
self.renderer.RemoveActor(self.path_actor)
# 创建路径线条
points = vtk.vtkPoints()
lines = vtk.vtkCellArray()
lines.InsertNextCell(len(self.current_path_points))
for i, point in enumerate(self.current_path_points):
points.InsertNextPoint(point)
lines.InsertCellPoint(i)
poly_data = vtk.vtkPolyData()
poly_data.SetPoints(points)
poly_data.SetLines(lines)
# 创建顶点(用于显示点)
vertices = vtk.vtkCellArray()
for i in range(len(self.current_path_points)):
vert = vtk.vtkVertex()
vert.GetPointIds().SetId(0, i)
vertices.InsertNextCell(vert)
poly_data.SetVerts(vertices)
# 创建mapper和actor
mapper = vtk.vtkPolyDataMapper()
mapper.SetInputData(poly_data)
self.path_actor = vtk.vtkActor()
self.path_actor.SetMapper(mapper)
self.path_actor.GetProperty().SetColor(1, 0, 0)
self.path_actor.GetProperty().SetLineWidth(3)
self.path_actor.GetProperty().SetPointSize(8)
self.renderer.AddActor(self.path_actor)
self.vtk_widget.GetRenderWindow().Render()
# 保存路径数据用于导出
self.path_data = poly_data
def clear_path(self):
"""清除路径"""
if hasattr(self, "path_actor"):
self.renderer.RemoveActor(self.path_actor)
del self.path_actor
self.vtk_widget.GetRenderWindow().Render()
self.current_path_points = []
self.clear_path_button.setEnabled(False)
self.export_path_button.setEnabled(False)
self.update_slice_views()
def export_mesh_to_dae(self):
"""导出网格为DAE格式"""
if not hasattr(self, "smoothed_mesh"):
QMessageBox.warning(self, "警告", "没有可导出的网格")
return
options = QFileDialog.Options()
file_path, _ = QFileDialog.getSaveFileName(
self,
"保存网格为 DAE 文件",
"",
"Collada 文件 (*.dae);;所有文件 (*)",
options=options,
)
if file_path:
try:
if not file_path.lower().endswith(".dae"):
file_path += ".dae"
# 创建导出器
exporter = vtk.vtkGLTFExporter()
exporter.SetFileName(file_path)
exporter.InlineDataOn()
# 创建一个临时渲染窗口用于导出
render_window = vtk.vtkRenderWindow()
renderer = vtk.vtkRenderer()
render_window.AddRenderer(renderer)
# 只添加网格actor
renderer.AddActor(self.mesh_actor)
renderer.SetBackground(0, 0, 0)
exporter.SetRenderWindow(render_window)
exporter.Write()
QMessageBox.information(self, "成功", f"网格已保存到 {file_path}")
except Exception as e:
QMessageBox.critical(self, "错误", f"导出网格失败:\n{str(e)}")
def export_path_to_dae(self):
"""导出路径为DAE格式"""
if not hasattr(self, "path_data"):
QMessageBox.warning(self, "警告", "没有可导出的路径")
return
options = QFileDialog.Options()
file_path, _ = QFileDialog.getSaveFileName(
self,
"保存路径为 DAE 文件",
"",
"Collada 文件 (*.dae);;所有文件 (*)",
options=options,
)
if file_path:
try:
if not file_path.lower().endswith(".dae"):
file_path += ".dae"
# 创建路径的actor
mapper = vtk.vtkPolyDataMapper()
mapper.SetInputData(self.path_data)
path_actor = vtk.vtkActor()
path_actor.SetMapper(mapper)
path_actor.GetProperty().SetColor(1, 0, 0)
path_actor.GetProperty().SetLineWidth(3)
path_actor.GetProperty().SetPointSize(8)
# 创建导出器
exporter = vtk.vtkGLTFExporter()
exporter.SetFileName(file_path)
exporter.InlineDataOn()
# 创建一个临时渲染窗口用于导出
render_window = vtk.vtkRenderWindow()
renderer = vtk.vtkRenderer()
render_window.AddRenderer(renderer)
renderer.AddActor(path_actor)
renderer.SetBackground(0, 0, 0)
exporter.SetRenderWindow(render_window)
exporter.Write()
QMessageBox.information(self, "成功", f"路径已保存到 {file_path}")
except Exception as e:
QMessageBox.critical(self, "错误", f"导出路径失败:\n{str(e)}")
def main():
app = QApplication(sys.argv)
if hasattr(Qt, 'AA_EnableHighDpiScaling'):
QApplication.setAttribute(Qt.AA_EnableHighDpiScaling, True)
if hasattr(Qt, 'AA_UseHighDpiPixmaps'):
QApplication.setAttribute(Qt.AA_UseHighDpiPixmaps, True)
viewer = MedicalViewer()
viewer.show()
sys.exit(app.exec_())
if __name__ == "__main__":
if sys.platform == 'win32' and sys.executable.endswith('python.exe'):
try:
import subprocess
subprocess.Popen([sys.executable.replace('python.exe', 'pythonw.exe')] + sys.argv)
sys.exit(0)
except:
pass
main()
该代码是通过读取DICOM文件并进行处理来实现CT图像的三维建模,但在使用过程中发现无法正确读取DICOM文件,所以我想要nii格式文件来进行图像处理实现CT图像的三维重建
最新发布