在VTK开发中高效做“可视化调试”¶
在 VTK 开发过程中,经常会遇到这样的问题:
- 算法中间结果不确定对不对
- mesh 某些 cell 是否异常?
- vtkImageData 是否方向、spacing 正确?
- 中间 pipeline 数据是否损坏?
如果每一步都写文件再用 ParaView 打开,效率极低,整理了一套 VTK 调试工具封装,实现:
- ✅ 任意步骤弹窗显示 vtkPolyData
- ✅ 支持 cell 级别高亮
- ✅ 支持 vtkImageData 直接显示
- ✅ 支持中间结果保存 vtp
- ✅ 支持 Qt 下医学图像比例尺显示
1 调试核心思想¶
核心目标:
在算法任意一步插入一行代码,自动弹出当前结果窗口,关闭后继续执行。
类似这样:
程序会:
- 阻塞进程
- 弹出 vtk 窗口
- 允许旋转缩放查看
- 关闭窗口后继续执行
2 模型调试(PolyData)¶
2.1 显示模型¶
void VtkUtil::ShowVtkDebugPolydata(vtkSmartPointer<vtkPolyData> surface) {
#if ISDEBUGGING
vtkNew<vtkPolyDataMapper> polydatamapper ;
polydatamapper->SetInputData(surface);
vtkNew<vtkActor> actor ;
actor->SetMapper(polydatamapper);
// actor->GetProperty()->SetOpacity(0.1);
vtkNew<vtkRenderer> renderer;
renderer->AddActor(actor);
renderer->SetBackground(0.2, 0.2, 0.2);
vtkNew<vtkRenderWindow> renwin ;
renwin->AddRenderer(renderer);
renwin->SetSize(800, 800);
vtkNew<vtkInteractorStyleTrackballCamera>style ;
vtkNew<vtkRenderWindowInteractor> rendererwindowinteracrot ;
rendererwindowinteracrot->SetInteractorStyle(style);
rendererwindowinteracrot->SetRenderWindow(renwin);
rendererwindowinteracrot->Start();
#else
Q_UNUSED(surface);
#endif
}
2.2 标记异常 Cell¶
调试 mesh 问题时,经常需要:标记异常三角形。
void VtkUtil::ShowVtkDebugPolydata(vtkSmartPointer<vtkPolyData> surface,
QList<quint32> self_intersected_list) {
#if ISDEBUGGING
qSort(self_intersected_list.begin(), self_intersected_list.end());
self_intersected_list = self_intersected_list.toSet().toList();
unsigned char color1[3] = { 255, 0, 0 };
unsigned char color2[3] = { 0, 0, 0 };
vtkNew<vtkUnsignedCharArray> cellColor;
cellColor->SetNumberOfComponents(3);
for(quint32 id = 0; id < surface->GetNumberOfCells(); id++) {
if(self_intersected_list.contains(id)) {
cellColor->InsertNextTypedTuple(color1);
} else {
cellColor->InsertNextTypedTuple(color2);
}
}
surface->GetCellData()->SetScalars(cellColor);
vtkNew<vtkPolyDataMapper> polydatamapper ;
polydatamapper->SetInputData(surface);
vtkNew<vtkActor> actor ;
actor->SetMapper(polydatamapper);
// actor->GetProperty()->SetOpacity(0.1);
vtkNew<vtkRenderer> renderer;
renderer->AddActor(actor);
renderer->SetBackground(0.2, 0.2, 0.2);
vtkNew<vtkRenderWindow> renwin ;
renwin->AddRenderer(renderer);
renwin->SetSize(800, 800);
vtkNew<vtkInteractorStyleTrackballCamera>style ;
vtkNew<vtkRenderWindowInteractor> rendererwindowinteracrot ;
rendererwindowinteracrot->SetInteractorStyle(style);
rendererwindowinteracrot->SetRenderWindow(renwin);
rendererwindowinteracrot->Start();
#else
Q_UNUSED(surface);
Q_UNUSED(self_intersected_list);
#endif
}
效果:
- 红色 → 异常 cell
- 黑色 → 正常 cell
调试 mesh 拓扑问题非常高效。
2.3 保存模型¶
void VtkUtil::WriteVTP(
vtkSmartPointer<vtkPolyData> surface, QString filename) {
#if ISDEBUGGING
vtkNew<vtkXMLPolyDataWriter> writer;
writer->SetFileName(filename.toLocal8Bit().data());
writer->SetInputData(surface);
writer->Write();
#else
Q_UNUSED(surface);
Q_UNUSED(filename);
#endif
}
3 图像调试¶
3.1 显示 vtkImageData¶
图像调试直接使用:
- vtkImageViewer2
void VtkUtil::ShowVtkDebugPolydata(
vtkSmartPointer<vtkImageData> surface) {
#if ISDEBUGGING
vtkNew<vtkImageViewer2> imageViewer ;
imageViewer->SetInputData(surface);
vtkNew<vtkRenderWindowInteractor> renderWindowInteractor ;
imageViewer->SetupInteractor(renderWindowInteractor);
imageViewer->Render();
imageViewer->GetRenderer()->ResetCamera();
imageViewer->Render();
renderWindowInteractor->Start();
#else
Q_UNUSED(surface);
#endif
}
3.2 Qt 下标准医学图像显示¶
关键:平行投影 + 正确 ParallelScale
if (this->vmtk_renderer_ == nullptr) {
qWarning() << "renderer is null";
return false;
}
if (this->actor_ == nullptr) {
this->actor_ = vtkSmartPointer<vtkImageActor>::New();
}
this->actor_->SetInputData(this->image_);
this->vmtk_renderer_->GetRenderer()->AddActor(this->actor_);
if (this->scale_actor_ == nullptr) {
this->scale_actor_ = vtkSmartPointer<vtkLegendScaleActor>::New();
}
this->scale_actor_->SetLabelMode(1);
this->scale_actor_->TopAxisVisibilityOff();
this->scale_actor_->LeftAxisVisibilityOn();
this->scale_actor_->BottomAxisVisibilityOff();
this->scale_actor_->RightAxisVisibilityOff();
this->scale_actor_->LegendVisibilityOff();
this->scale_actor_->SetLeftBorderOffset(90);
this->scale_actor_->SetBottomBorderOffset(35);
this->scale_actor_->SetTopBorderOffset(35);
this->scale_actor_->GetLeftAxis()->SetNumberOfLabels(5);
this->scale_actor_->GetLeftAxis()->SetAdjustLabels(true);
this->scale_actor_->GetLeftAxis()->SetFontFactor(1);
this->vmtk_renderer_->GetRenderer()->AddActor(this->scale_actor_);
double origin[3];
double spacing[3];
int extent[6];
this->image_->GetOrigin(origin);
this->image_->GetSpacing(spacing);
this->image_->GetExtent(extent);
vtkSmartPointer<vtkCamera> camera =
this->vmtk_renderer_->GetRenderer()->GetActiveCamera();
camera->ParallelProjectionOn();
double xc = origin[0] + 0.5 * (extent[0] + extent[1]) * spacing[0];
double yc = origin[1] + 0.5 * (extent[2] + extent[3]) * spacing[1];
double xd = (extent[1] - extent[0] + 1) * spacing[0];
double yd = (extent[3] - extent[2] + 1) * spacing[1];
double d = camera->GetDistance();
Q_UNUSED(xd)
camera->SetFocalPoint(xc, yc, 0.0);
camera->SetPosition(xc, yc, d);
camera->SetParallelScale(0.5 * (yd - 1));
this->vmtk_renderer_->Render();
作用:
- 保证图像不透视变形
- 像素与 spacing 对应
- 比例尺准确显示
scale 使用 vtkLegendScaleActor ,用于医学影像的真实物理尺寸标注。
3.3 VTI转为QImage¶
QImage VtkUtil::VtkImageDataToQImage(
vtkSmartPointer<vtkImageData> imageData, const qint32 value) {
if (!imageData) {
qWarning() << "image data is null";
return QImage();
}
/// \todo retrieve just the UpdateExtent
qint32 width = imageData->GetDimensions()[0];
qint32 height = imageData->GetDimensions()[1];
QImage image(width, height, QImage::Format_RGB32);
QRgb *rgbPtr = reinterpret_cast<QRgb *>(image.bits()) + width * (height - 1);
unsigned char *colorsPtr =
reinterpret_cast<unsigned char *>(imageData->GetScalarPointer());
// Loop over the vtkImageData contents.
for (qint32 row = 0; row < height; row++) {
for (qint32 col = 0; col < width; col++) {
// Swap the vtkImageData RGB values with an equivalent QColor
*(rgbPtr++) = QColor(colorsPtr[0], colorsPtr[1], colorsPtr[2]).rgb();
colorsPtr += imageData->GetNumberOfScalarComponents();
}
rgbPtr -= width * 2;
}
if(1 == value) {
image = image.copy((image.width() - image.height()) / 2, 0,
image.height(), image.height());
} else if(2 == value) {
image = image.scaled(image.width(), image.width());
}
return image;
}