跳转至

在VTK开发中高效做“可视化调试”

在 VTK 开发过程中,经常会遇到这样的问题:

  • 算法中间结果不确定对不对
  • mesh 某些 cell 是否异常?
  • vtkImageData 是否方向、spacing 正确?
  • 中间 pipeline 数据是否损坏?

如果每一步都写文件再用 ParaView 打开,效率极低,整理了一套 VTK 调试工具封装,实现:

  • ✅ 任意步骤弹窗显示 vtkPolyData
  • ✅ 支持 cell 级别高亮
  • ✅ 支持 vtkImageData 直接显示
  • ✅ 支持中间结果保存 vtp
  • ✅ 支持 Qt 下医学图像比例尺显示

1 调试核心思想

核心目标:

在算法任意一步插入一行代码,自动弹出当前结果窗口,关闭后继续执行。

类似这样:

VtkUtil::ShowVtkDebugPolydata(surface);

程序会:

  1. 阻塞进程
  2. 弹出 vtk 窗口
  3. 允许旋转缩放查看
  4. 关闭窗口后继续执行

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;
}