This week we began to investigate how to use VTK to render DICOM MPR/VR images. See the example of VTK:
VTK-8.1.0\Examples\GUI\Qt\FourPaneViewer
This example uses QVTK OpenGL Widget control to display four view s, which show intersecting MPRA, MPRB, MPRC and 3D slices (three intersecting MPR planes).
The UI below is my modified version.
VTK's official UI is the class constructor that receives the Seres directory directly from the parameters. I can choose Series directory after I modify it.
In addition, I have accumulated some experience in learning to use this example and share it with you.
1. QVTK OpenGL Widget for QT5.9 and later
Initially, my environment was VTK8.1.0+QT5.7.1+VS2013. For example, when the mouse clicked/dragged in four Views (especially the 3D slice view in the upper right corner), it was easy to crash. The error prompted when debugging code is related to the QT mouse event. (Sorry to have forgotten the screenshot, but now the environment has been upgraded)
When investigating the problem, I saw a paragraph on VTK's official website:
https://www.vtk.org/doc/nightly/html/classQVTKOpenGLWidget.html
QVTKOpenGLWidget is targeted for Qt version 5.9 and above.
So I upgraded the environment to VTK8.1.0+QT5.11.1+VS2015, compiled the running example again, and the mouse crashed easily when clicked/dragged in four Views (especially the 3D slice view in the upper right corner). But the errors are different.
2. To investigate the above problem, the text-related code guesses to be vtkImagePlaneWidget::DisplayTextOn(), change it to DisplayTextOff(), then run the program, and the mouse will not crash when clicking/dragging within four Views (especially the 3D slice view in the upper right corner). (Actually, that's not quite clear yet)
The following is the QtVTKRenderWindows.cxx file:
#include "QtVTKRenderWindows.h" #include "vtkBoundedPlanePointPlacer.h" #include "vtkDICOMImageReader.h" #include "vtkDistanceRepresentation.h" #include "vtkDistanceRepresentation2D.h" #include "vtkDistanceWidget.h" #include "vtkHandleRepresentation.h" #include "vtkImageData.h" #include "vtkImageMapToWindowLevelColors.h" #include "vtkImageSlabReslice.h" #include "vtkInteractorStyleImage.h" #include "vtkLookupTable.h" #include "vtkPlane.h" #include "vtkPlaneSource.h" #include "vtkPointHandleRepresentation2D.h" #include "vtkPointHandleRepresentation3D.h" #include <vtkRenderWindow.h> #include "vtkRenderWindowInteractor.h" #include "vtkResliceImageViewer.h" #include "vtkResliceCursorLineRepresentation.h" #include "vtkResliceCursorThickLineRepresentation.h" #include "vtkResliceCursorWidget.h" #include "vtkResliceCursorActor.h" #include "vtkResliceCursorPolyDataAlgorithm.h" #include "vtkResliceCursor.h" #include "vtkResliceImageViewerMeasurements.h" #include "vtkCamera.h" #include <QFileDialog> #include <QMessageBox> #include "CommonFunc.h" //---------------------------------------------------------------------------- void vtkResliceCursorCallback::Execute( vtkObject *caller, unsigned long ev, void *callData ) { if (ev == vtkResliceCursorWidget::WindowLevelEvent || ev == vtkCommand::WindowLevelEvent || ev == vtkResliceCursorWidget::ResliceThicknessChangedEvent) { // Render everything for (int i = 0; i < 3; i++) { this->resliceCursorWidget[i]->Render(); } //view4 update this->imagePlabeWidget[0]->GetInteractor()->GetRenderWindow()->Render(); return; } vtkImagePlaneWidget* ipw = dynamic_cast<vtkImagePlaneWidget*>(caller); if (ipw) { double* wl = static_cast<double*>(callData); if (ipw == this->imagePlabeWidget[0]) { this->imagePlabeWidget[1]->SetWindowLevel(wl[0], wl[1], 1); this->imagePlabeWidget[2]->SetWindowLevel(wl[0], wl[1], 1); } else if (ipw == this->imagePlabeWidget[1]) { this->imagePlabeWidget[0]->SetWindowLevel(wl[0], wl[1], 1); this->imagePlabeWidget[2]->SetWindowLevel(wl[0], wl[1], 1); } else if (ipw == this->imagePlabeWidget[2]) { this->imagePlabeWidget[0]->SetWindowLevel(wl[0], wl[1], 1); this->imagePlabeWidget[1]->SetWindowLevel(wl[0], wl[1], 1); } } vtkResliceCursorWidget *rcw = dynamic_cast< vtkResliceCursorWidget *>(caller); if (rcw) { vtkResliceCursorLineRepresentation *rep = dynamic_cast< vtkResliceCursorLineRepresentation *>(rcw->GetRepresentation()); // Although the return value is not used, we keep the get calls // in case they had side-effects rep->GetResliceCursorActor()->GetCursorAlgorithm()->GetResliceCursor(); for (int i = 0; i < 3; i++) { vtkPlaneSource *ps = static_cast<vtkPlaneSource *>( this->imagePlabeWidget[i]->GetPolyDataAlgorithm()); ps->SetOrigin(this->resliceCursorWidget[i]->GetResliceCursorRepresentation()-> GetPlaneSource()->GetOrigin()); ps->SetPoint1(this->resliceCursorWidget[i]->GetResliceCursorRepresentation()-> GetPlaneSource()->GetPoint1()); ps->SetPoint2(this->resliceCursorWidget[i]->GetResliceCursorRepresentation()-> GetPlaneSource()->GetPoint2()); // If the reslice plane has modified, update it on the 3D widget this->imagePlabeWidget[i]->UpdatePlacement(); } } // Render everything for (int i = 0; i < 3; i++) { this->resliceCursorWidget[i]->Render(); } this->imagePlabeWidget[0]->GetInteractor()->GetRenderWindow()->Render(); } QtVTKRenderWindows::QtVTKRenderWindows() { ui = new Ui_QtVTKRenderWindows; ui->setupUi(this); initVTKUI(); } void QtVTKRenderWindows::initVTKUI() { ui->thickModeCheckBox->setEnabled(0); ui->blendModeGroupBox->setEnabled(0); for (int i = 0; i < 3; ++i) { mResliceViewer[i] = vtkSmartPointer<vtkResliceImageViewer>::New(); mResliceRenderWin[i] = vtkSmartPointer<vtkGenericOpenGLRenderWindow>::New(); mResliceViewer[i]->SetRenderWindow(mResliceRenderWin[i]); } ui->view1->SetRenderWindow(mResliceRenderWin[0]); mResliceViewer[0]->SetupInteractor(mResliceRenderWin[0]->GetInteractor()); ui->view2->SetRenderWindow(mResliceRenderWin[1]); mResliceViewer[1]->SetupInteractor(mResliceRenderWin[1]->GetInteractor()); ui->view3->SetRenderWindow(mResliceRenderWin[2]); mResliceViewer[2]->SetupInteractor(mResliceRenderWin[2]->GetInteractor()); for (int i = 0; i < 3; ++i) { // make them all share the same reslice cursor object. mResliceViewer[i]->SetResliceCursor(mResliceViewer[0]->GetResliceCursor()); } mPlanePicker = vtkSmartPointer<vtkCellPicker>::New(); mPlanePicker->SetTolerance(0.005); mPlaneRenderWin = vtkSmartPointer<vtkGenericOpenGLRenderWindow>::New(); ui->view4->SetRenderWindow(mPlaneRenderWin); mPlaneRender = vtkSmartPointer<vtkRenderer>::New(); ui->view4->GetRenderWindow()->AddRenderer(mPlaneRender); double planeBkColor[3] = {0.5, 0.5, 0.5}; mPlaneRender->SetBackground(planeBkColor); vtkRenderWindowInteractor *iren = ui->view4->GetInteractor(); mPlaneProperty = vtkSmartPointer<vtkProperty>::New(); for (int i = 0; i < 3; ++i) { mPlaneWidget[i] = vtkSmartPointer<vtkImagePlaneWidget>::New(); mPlaneWidget[i]->SetInteractor(iren); mPlaneWidget[i]->SetPicker(mPlanePicker); double color[3] = { 0.0, 0.0, 0.0 }; color[i] = 1.0; mPlaneWidget[i]->GetPlaneProperty()->SetColor(color); color[0] /= 4.0; color[1] /= 4.0; color[2] /= 4.0; mResliceViewer[i]->GetRenderer()->SetBackground(color); mPlaneWidget[i]->SetTexturePlaneProperty(mPlaneProperty); mPlaneWidget[i]->SetDefaultRenderer(mPlaneRender); // Make them all share the same color map. mResliceViewer[i]->SetLookupTable(mResliceViewer[0]->GetLookupTable()); mPlaneWidget[i]->GetColorMap()->SetLookupTable(mResliceViewer[0]->GetLookupTable()); mPlaneWidget[i]->SetColorMap(mResliceViewer[i]->GetResliceCursorWidget() ->GetResliceCursorRepresentation()->GetColorMap()); } mResliceCallback = vtkSmartPointer<vtkResliceCursorCallback>::New(); for (int i = 0; i < 3; ++i) { mResliceCallback->imagePlabeWidget[i] = mPlaneWidget[i]; mResliceCallback->resliceCursorWidget[i] = mResliceViewer[i]->GetResliceCursorWidget(); mResliceViewer[i]->GetResliceCursorWidget()->AddObserver( vtkResliceCursorWidget::ResliceAxesChangedEvent, mResliceCallback); mResliceViewer[i]->GetResliceCursorWidget()->AddObserver( vtkResliceCursorWidget::WindowLevelEvent, mResliceCallback); mResliceViewer[i]->GetResliceCursorWidget()->AddObserver( vtkResliceCursorWidget::ResliceThicknessChangedEvent, mResliceCallback); mResliceViewer[i]->GetResliceCursorWidget()->AddObserver( vtkResliceCursorWidget::ResetCursorEvent, mResliceCallback); mResliceViewer[i]->GetInteractorStyle()->AddObserver( vtkCommand::WindowLevelEvent, mResliceCallback); } for (int i = 0; i < 3; ++i) { mResliceRenderWin[i]->Render(); } mPlaneRenderWin->Render(); } void QtVTKRenderWindows::SetBlendMode( int m ){ for (int i = 0; i < 3; ++i) { vtkImageSlabReslice *thickSlabReslice = vtkImageSlabReslice::SafeDownCast( vtkResliceCursorThickLineRepresentation::SafeDownCast( mResliceViewer[i]->GetResliceCursorWidget()->GetRepresentation())->GetReslice()); thickSlabReslice->SetBlendMode(m); mResliceViewer[i]->Render(); } } void QtVTKRenderWindows::AddDistanceMeasurementToView( int i ){ // remove existing widgets. if (this->mDistanceWidget[i]) { this->mDistanceWidget[i]->SetEnabled(0); this->mDistanceWidget[i] = nullptr; } // add new widget this->mDistanceWidget[i] = vtkSmartPointer< vtkDistanceWidget >::New(); this->mDistanceWidget[i]->SetInteractor( this->mResliceViewer[i]->GetResliceCursorWidget()->GetInteractor()); // Set a priority higher than our reslice cursor widget this->mDistanceWidget[i]->SetPriority( this->mResliceViewer[i]->GetResliceCursorWidget()->GetPriority() + 0.01); vtkSmartPointer< vtkPointHandleRepresentation2D > handleRep = vtkSmartPointer< vtkPointHandleRepresentation2D >::New(); vtkSmartPointer< vtkDistanceRepresentation2D > distanceRep = vtkSmartPointer< vtkDistanceRepresentation2D >::New(); distanceRep->SetHandleRepresentation(handleRep); this->mDistanceWidget[i]->SetRepresentation(distanceRep); distanceRep->InstantiateHandleRepresentation(); distanceRep->GetPoint1Representation()->SetPointPlacer(mResliceViewer[i]->GetPointPlacer()); distanceRep->GetPoint2Representation()->SetPointPlacer(mResliceViewer[i]->GetPointPlacer()); // Add the distance to the list of widgets whose visibility is managed based // on the reslice plane by the ResliceImageViewerMeasurements class this->mResliceViewer[i]->GetMeasurements()->AddItem(this->mDistanceWidget[i]); this->mDistanceWidget[i]->CreateDefaultRepresentation(); this->mDistanceWidget[i]->EnabledOn(); } void QtVTKRenderWindows::on_btnSelectFolder_clicked() { QString dir_name = QFileDialog::getExistingDirectory( this, tr("Select DICOM folder") ); if (dir_name.isEmpty()){ QMessageBox::critical(this, tr("Error"), tr("Please select a DICOM folder")); return; } ui->editDicomFolder->setText(dir_name); readDicomSeries(dir_name); } void QtVTKRenderWindows::on_resliceModeCheckBox_toggled( bool checked ){ ui->thickModeCheckBox->setEnabled(checked); ui->blendModeGroupBox->setEnabled(checked); for (int i = 0; i < 3; ++i) { mResliceViewer[i]->SetResliceMode(checked ? 1 : 0); mResliceViewer[i]->GetRenderer()->ResetCamera(); mResliceViewer[i]->Render(); } } void QtVTKRenderWindows::on_thickModeCheckBox_toggled( bool checked ){ for (int i = 0; i < 3; ++i) { mResliceViewer[i]->SetThickMode(checked ? 1 : 0); mResliceViewer[i]->Render(); } } void QtVTKRenderWindows::on_radioButton_Max_toggled( bool checked ){ if(checked){ SetBlendMode(VTK_IMAGE_SLAB_MAX); } } void QtVTKRenderWindows::on_radioButton_Min_toggled( bool checked ){ if(checked){ SetBlendMode(VTK_IMAGE_SLAB_MIN); } } void QtVTKRenderWindows::on_radioButton_Mean_toggled( bool checked ){ if(checked){ SetBlendMode(VTK_IMAGE_SLAB_MEAN); } } void QtVTKRenderWindows::on_resetButton_clicked() { // Reset the reslice image views for (int i = 0; i < 3; ++i) { mResliceViewer[i]->Reset(); } // Also sync the Image plane widget on the 3D top right view with any // changes to the reslice cursor. for (int i = 0; i < 3; ++i) { vtkPlaneSource *ps = static_cast<vtkPlaneSource *>( mPlaneWidget[i]->GetPolyDataAlgorithm()); ps->SetNormal(mResliceViewer[0]->GetResliceCursor()->GetPlane(i)->GetNormal()); ps->SetCenter(mResliceViewer[0]->GetResliceCursor()->GetPlane(i)->GetOrigin()); // If the reslice plane has modified, update it on the 3D widget mPlaneWidget[i]->UpdatePlacement(); } // Render in response to changes. for (int i = 0; i < 3; ++i) { mResliceViewer[i]->Render(); } ui->view4->GetRenderWindow()->Render(); } void QtVTKRenderWindows::on_AddDistance1Button_clicked() { AddDistanceMeasurementToView(1); } void QtVTKRenderWindows::readDicomSeries( const QString &dir_name ){ std::string series_dir = CommonFunc::qs2s(dir_name); auto reader = vtkSmartPointer<vtkDICOMImageReader>::New(); reader->SetDirectoryName(series_dir.c_str()); reader->Update(); int imageDims[3]; reader->GetOutput()->GetDimensions(imageDims); for (int i = 0; i < 3; ++i) { auto rep = vtkResliceCursorLineRepresentation::SafeDownCast( mResliceViewer[i]->GetResliceCursorWidget()->GetRepresentation()); rep->GetResliceCursorActor()-> GetCursorAlgorithm()->SetReslicePlaneNormal(i); mResliceViewer[i]->SetInputData(reader->GetOutput()); mResliceViewer[i]->SetSliceOrientation(i); mResliceViewer[i]->SetResliceModeToAxisAligned(); } for (int i = 0; i < 3; ++i) { mPlaneWidget[i]->RestrictPlaneToVolumeOn(); mPlaneWidget[i]->TextureInterpolateOff(); mPlaneWidget[i]->SetResliceInterpolateToLinear(); mPlaneWidget[i]->SetInputConnection(reader->GetOutputPort()); mPlaneWidget[i]->SetPlaneOrientation(i); mPlaneWidget[i]->SetSliceIndex(imageDims[i] / 2); mPlaneWidget[i]->DisplayTextOff(); mPlaneWidget[i]->SetWindowLevel(1358, -27); mPlaneWidget[i]->On(); mPlaneWidget[i]->InteractionOn(); } for (int i = 0; i < 3; ++i) { mResliceViewer[i]->GetRenderer()->ResetCamera(); mResliceRenderWin[i]->Render(); } mPlaneRender->ResetCamera(); mPlaneRenderWin->Render(); }