MFC application development based on VTK

Previously, we introduced the development of single document application based on VTK, and took image resampling as an example to realize a simple image resampling application. The multi document application is basically the same as the single document application, which will not be described here. Dialog application is a widely used framework in MFC application. This section takes the implementation of four view framework program commonly used in medical image visualization as an example to describe the development of dialog application based on VTK.

1. Use VS and CMake to establish an empty MFC dialog program framework.

Use VS to create an MFC dialog project vtkDialog, delete the project files, and complete cmakelists Txt file, and add the corresponding code file and link VTK dynamic library. After CMake configuration, open the generated project file vtkDialog SLN, compile and execute to get an empty dialog box program. Where CvtkDialogDlg is the main dialog class of the program.

2. Design the user interface and add corresponding controls

The functions of this program include (1) image reading and management; (2) Image segmentation and browsing. A common medical image visualization program, including four views, cross-sectional view, sagittal view, coronal view and three-dimensional view. Therefore, based on the above design, we add a tree Control, and the corresponding Control class in MFC is CTreeCtrl. Tree Control is the most commonly used file management Control, which can easily organize and manage files hierarchically. The implementation of four views requires four controls. Here, we select CStatic Control and add it to the dialog window. After adding, generate corresponding Control type variables for the Control.

According to the above design, the image needs to be displayed in CStatic. This requires the continuous extension of CStatic class to support VTK visual pipeline. A feasible method is to design a subclass of CStatic class and implement VTK visual pipeline and processing in this subclass.

3. Realize VTK image visualization control

3.1 first add an MFC class CvtkView

The base class is selected as CStatic and added to cmakelists Txt file.

3.2 overloaded CvtkView class, PreSubclassWindow() function and OnPaint() function

The PreSubclassWindow function is responsible for creating the VTK visualization pipeline, and the OnPaint() function is responsible for rendering the scene in the client area.

3.3 establish VTK visualization pipeline

VTK visualization pipeline has been introduced in Chapter 2, which mainly includes vtkacor, vtkrenderer, vtkrenderwindow and vtkrenderwindowinteractor. Of course, you can also set vtkRenderWindowInteractorStyle, lighting, material, color, etc. as needed. Define relevant objects in the CvtkView class header file, and instantiate and build the visualization pipeline in the PreSubclassWindow function. The code is as follows.

 1 void CvtkView::PreSubclassWindow()
 2 {
 3     // TODO: Add your specialized code here and/or call the base class
 4     CRect rect;
 5     GetClientRect(rect);
 6  
 7     m_Renderer = vtkSmartPointer<vtkRenderer>::New();
 8     
 9     m_RenderWindow = vtkSmartPointer<vtkRenderWindow>::New();
10     m_RenderWindow->SetParentId(this->m_hWnd);
11     m_RenderWindow->SetSize(rect.Width(), rect.Height());
12     m_RenderWindow->AddRenderer(m_Renderer);
13  
14     if(m_RenderWindow->GetInteractor() == NULL)
15     {
16         vtkSmartPointer<vtkRenderWindowInteractor> RenderWindowInteractor =
17             vtkSmartPointer<vtkRenderWindowInteractor>::New();
18         RenderWindowInteractor->SetRenderWindow(m_RenderWindow);
19         RenderWindowInteractor->Initialize();
20     }
21  
22     m_RenderWindow->Start();    
23     CStatic::PreSubclassWindow();
24 }

I believe that through the previous study, the process of establishing visual pipeline here has been relatively familiar. It should be noted that vtkRenderWindow needs to establish an association with the control itself through the function vtkRenderWindow::SetParentId(), so that M_ The rendered content in renderwindow is displayed on the window of the control; vtkRenderWindow::SetSize() sets the size of the rendering window to be consistent with the client area of the current control.

You may wonder, why is there no vtkActor? In a normal visualization pipeline, vtkActor represents the objects that need to be rendered or drawn. The object to be rendered here is an image, but unlike before, there is no direct definition of an image vtkActor. As for the reason, let's leave it alone. With the gradual improvement of such functions, we will explain it in detail. After the VTK rendering pipeline is established, call the Render() function of vtkRenderWindow in the OnPaint() function to realize rendering.

Here, a basic VTK display control has been implemented. When designing the interface, the four view variable types automatically added through MFC are CStatic by default. Since CvtkView inherits from CStatic, we can directly modify the four variable types defined in the CvtkDialogDlg header file of the main dialog class to CvtkView. Then compile and run the program. Has a four view prototype appeared (as shown in the figure below)? Since no render objects have been added, all four views are empty black windows.

3.4 interactive image segmentation

The control needs to realize two basic functions: one is interactive image segmentation; The second is slice image extraction. The first function adopts vtkrelicecursorwidget and vtkrelicecursor classes. Usually, the two classes are used at the same time. Each vtkrelicecursorwidget object needs to define a corresponding vtkrelicecursor object. Vtkrelicecursorwidget provides user-friendly segmentation and interaction through the defined "cross" coordinate axis, and supports the rotation and translation of the coordinate axis; When the coordinate system changes, vtkreplicecursor is called to segment the image and update it into the vtkRenderer object.

The second function is implemented by vtkImagePlaneWidget. This class internally defines a vtkimageduplicate object, which uses the segmentation plane defined in vtkresilicecursor to segment the image, draws it to a plane through texture mapping, and displays it in the vtkRenderer specified by the user.

In addition, when defining the visualization pipeline, we did not define the relevant vtkActor. We only need to automatically display the corresponding images of the vrender widget through its internal view. According to the above analysis, relevant objects are defined in the header file and instantiated in the PreSubclassWindow function.

1 vtkSmartPointer< vtkImagePlaneWidget >   m_ImagePlaneWidget;
2 vtkSmartPointer< vtkResliceCursorWidget> m_ResliceCursorWidget;
3 vtkSmartPointer< vtkResliceCursor >      m_ResliceCursor;
4 vtkSmartPointer< vtkResliceCursorThickLineRepresentation > m_ResliceCursorRep;

When instantiating, it should be noted that the view class renders the output of the vtkrelicecursorwidget object by default, so you need to specify the corresponding vtkRenderer object for the vtkrelicecursorwidget object,

1 m_ResliceCursorWidget->SetInteractor(m_RenderWindow->GetInteractor()); 
2 m_ResliceCursorWidget->SetDefaultRenderer(m_Renderer);

In this way, the slice image will be converted into a vtkActor object inside the vtkreliccursorwidget object and added to the specified vtkRenderer object for display and rendering; There is also an important function in vtkImagePlaneWidget, vtkImagePlaneWidget::SetDefaultRenderer(vtkRenderer *), which is used to set the corresponding vtkRenderer to display slices. According to the design of this program, the slice generated by vtkImagePlaneWidget needs to be displayed in the 3D scene, so it is not called here. That is, by default, this control class only displays two-dimensional slice images.

3.5 add image setting function and initialize image segmentation object

The control class needs to set the corresponding processing image from the outside, so it provides an interface function for external calls. In 3.4, only the interactive image segmentation object is defined and created, and the corresponding input data is not set. Therefore, each time a new data is passed in, it needs to be initialized.

 1 void CvtkView::SetImageData(vtkSmartPointer<vtkImageData> ImageData)
 2 {
 3     if (ImageData == NULL ) return;
 4     
 5     m_ImageData = ImageData;
 6     SetupReslice();
 7 }
 8 void CvtkView::SetupReslice()
 9 {
10     if (m_ImageData == NULL) return;
11     int dims[3];
12     m_ImageData->GetDimensions(dims);
13  
14     //
15     m_ImagePlaneWidget->SetInput(m_ImageData); 
16     m_ImagePlaneWidget->SetPlaneOrientation(m_Direction); 
17     m_ImagePlaneWidget->SetSliceIndex(dims[m_Direction]/2); 
18     m_ImagePlaneWidget->On(); 
19     m_ImagePlaneWidget->InteractionOn(); 
20     
21     //
22     m_ResliceCursor->SetCenter(m_ImageData->GetCenter()); 
23     m_ResliceCursor->SetImage(m_ImageData); 
24     m_ResliceCursor->SetThickMode(0); 
25  
26     m_ResliceCursorRep->GetResliceCursorActor()-> 
27         GetCursorAlgorithm()->SetResliceCursor(m_ResliceCursor);
28     m_ResliceCursorRep->GetResliceCursorActor()-> 
29         GetCursorAlgorithm()->SetReslicePlaneNormal(m_Direction);
30  
31     m_ResliceCursorWidget->SetEnabled(1); 
32     m_Renderer->ResetCamera(); 
33     
34     //
35     double range[2]; 
36     m_ImageData->GetScalarRange(range); 
37     m_ResliceCursorWidget->GetResliceCursorRepresentation()->
38         SetWindowLevel(range[1]-range[0], (range[0]+range[1])/2.0); 
39     m_ImagePlaneWidget->SetWindowLevel(range[1]-range[0], (range[0]+range[1])/2.0); 
40 }

In the setupresolve() function, first initialize the vtkImagePlaneWidget object,

vtkImagePlaneWidget::SetInput() sets the input image

vtkImagePlaneWidget::SetPlaneOrientation() sets the orientation of the slice

vtkImagePlaneWidget::SetSliceIndex() sets the default layer number in the current direction

Note that after setting the input image and the corresponding interactive object, start vtkImagePlaneWidget. The corresponding function is:

vtkImagePlaneWidget::On();

vtkImagePlaneWidget::InteractionOn();

 

Then set the vtkreplicecursor object,

Vtkreplicecursor:: setinput() set input image

Vtkreplicecursor:: setcenter() sets the default syncopation center point

Vtkreplicecursor:: setthickmode() sets the segmentation mode

SetThickMode(int) function when the parameter is 0, one single-layer image slice is obtained for each segmentation; When the parameter is 1, the thickness mode is turned on, and the slice thickness can be set through SetThickness(), that is, a multi-layer thickness image is obtained. Here we turn off the thickness mode.

m_ Resolicecursor rep is a vtkreicecursor thicklinerepresentation object, that is, the "cross" coordinate axis displayed on the screen during image segmentation. Use this object to set its associated vtkreicecursor object and set the segmentation direction. SetDefaultRenderer() is used to set the vtkRenderer required to display the segmentation results. We set it here as the class member variable m_Renderer, that is, the results of each mouse segmentation are displayed in the visual pipeline defined by the current window. Similarly, after setting the input image of the vtkrelicecursor object, open the vtkrelicecursorwidget object:

vtkResliceCursorWidget::SetEnabled();

m_Direction is the direction sign, with values of 0, 1 and 2 respectively, representing the X-axis, Y-axis and Z-axis directions respectively. Cross section view, sagittal view and coronal view can be realized by setting different direction values.

Such a control with image segmentation function has been completed. The control supports the user to set the slice direction and image input. When running, the corresponding cross coordinate axis is displayed in each view according to the direction set by the user. The user can drag the cross to interact. Of course, we haven't achieved synchronization between views yet.

4. Improve the CvtkDialogDlg class

4.1 four view initialization

First, initialize four view control class objects in the OnInitDialog function of the initialization function of the CvtkDialogDlg class:

 1 BOOL CvtkDialogDlg::OnInitDialog()
 2 {
 3     CDialog::OnInitDialog();
 4  
 5     // Add "About..." menu item to system menu.
 6  
 7     // IDM_ABOUTBOX must be in the system command range.
 8     ASSERT((IDM_ABOUTBOX & 0xFFF0) == IDM_ABOUTBOX);
 9     ASSERT(IDM_ABOUTBOX < 0xF000);
10  
11     CMenu* pSysMenu = GetSystemMenu(FALSE);
12     if (pSysMenu != NULL)
13     {
14         BOOL bNameValid;
15         CString strAboutMenu;
16         bNameValid = strAboutMenu.LoadString(IDS_ABOUTBOX);
17         ASSERT(bNameValid);
18         if (!strAboutMenu.IsEmpty())
19         {
20             pSysMenu->AppendMenu(MF_SEPARATOR);
21             pSysMenu->AppendMenu(MF_STRING, IDM_ABOUTBOX, strAboutMenu);
22         }
23     }
24  
25     // Set the icon for this dialog.  The framework does this automatically
26     //  when the application's main window is not a dialog
27     SetIcon(m_hIcon, TRUE);            // Set big icon
28     SetIcon(m_hIcon, FALSE);        // Set small icon
29  
30     // TODO: Add extra initialization here 
31  
32     m_AxialView.SetSliceDirection(0);
33     m_AxialView.GetImagePlaneWidget()->SetInteractor(m_3DView.GetInteractor());
34     m_AxialView.GetImagePlaneWidget()->SetDefaultRenderer(m_3DView.GetRenderer());
35     
36     m_SagittalView.SetSliceDirection(1);
37     m_SagittalView.GetImagePlaneWidget()->SetInteractor(m_3DView.GetInteractor());
38     m_SagittalView.GetImagePlaneWidget()->SetDefaultRenderer(m_3DView.GetRenderer());
39     m_SagittalView.SetResliceCursor(m_AxialView.GetResliceCursor());
40  
41     m_CoronalView.SetSliceDirection(2);
42     m_CoronalView.GetImagePlaneWidget()->SetInteractor(m_3DView.GetInteractor());
43     m_CoronalView.GetImagePlaneWidget()->SetDefaultRenderer(m_3DView.GetRenderer());
44     m_CoronalView.SetResliceCursor(m_AxialView.GetResliceCursor());
45  
46     return TRUE;  // return TRUE  unless you set the focus to a control
47 }

As can be seen from the code, we specify the slice direction for each view, and then specify the corresponding interaction object and drawing object vtkRenderer for the vtkImagePlaneWidget class object of each view. As we mentioned earlier, vtkImagePlaneWidget needs to specify the corresponding vtkRenderer class object for it to display image slices. Here, the results of vtkImagePlaneWidget need to be displayed in the 3D view. Therefore, set the vtkRenderer object of the 3D view to the vtkImagePlaneWidget object in each 2D view through the SetDefaultRenderer() function, so that the slice images in three directions can be displayed in the 3D view.

In addition, it should be noted that,

m_SagittalView.SetResliceCursor(m_AxialView.GetResliceCursor());

m_CoronalView.SetResliceCursor(m_AxialView.GetResliceCursor());

Here, m_SagittalView and M_ The vtkreplicecursor objects in the coronalview class are all set to M_ The vtkrelicecursor object of axialview, that is, one vtkrelicecursor is used in three 2D views. We know that the vtkresilicecursor widget object is defined in the two-dimensional view class. The object realizes interactive image segmentation through user interaction and vtkresilicecursor. However, while image segmentation, the three directions should be synchronized, that is, when the segmentation center of one image changes, the views of the other two directions should be updated in time to maintain synchronization. Therefore, a common vtkresilicecursor object will be used in the three views to facilitate view synchronization.

4.2 four view synchronization

Because the three 2D views use the same vtkreplicecursor object, when the image segmentation parameters (i.e. segmentation plane parameters: center point and normal direction) of any vtkreplicecursor object change, other views will also change. However, since parameter changes cannot be detected directly inside the view, we need to listen to the message of parameter changes through the observer command mode of VTK and handle it accordingly. The change of segmentation parameters is realized by dragging or rotating the "cross" axis of the view. At this time, a message vtkrelicecursorwidget:: resoliceaxechangedevent will be generated in vtkrelicecursorwidget. We only need to listen to this message. Therefore, define a vtkCommand class to implement corresponding processing operations for vtkrelicecursorwidget:: resoliceaxechangedevent.

In addition, when the user changes the coordinate axis of the image segmentation (rotating the coordinate axis or translating the coordinate system), the image segmentation plane will change accordingly. If the new segmentation plane is updated to the vtkImagePlaneWidget object of the two-dimensional view, the simultaneous update operation of the three-dimensional view can be realized. Based on the above design, a vtkCommand subclass is implemented to listen to the vtkrelicecursorwidget:: resoliceaxechangedevent message and implement the corresponding update operation.

 1 class vtkResliceCursorCallback : public vtkCommand 
 2 { 
 3 public: 
 4     static vtkResliceCursorCallback *New() 
 5     { return new vtkResliceCursorCallback; } 
 6  
 7     void Execute( vtkObject *caller, unsigned long /*ev*/, 
 8         void *callData ) 
 9     { 
10         vtkResliceCursorWidget *rcw = dynamic_cast< 
11             vtkResliceCursorWidget * >(caller); 
12         if (rcw) 
13         {  
14             for (int i = 0; i < 3; i++) 
15             { 
16                 vtkPlaneSource *ps = static_cast< vtkPlaneSource * >( 
17                     view[i]->GetImagePlaneWidget()->GetPolyDataAlgorithm()); 
18  
19                 ps->SetOrigin(view[i]->GetResliceCursorWidget()->
20                     GetResliceCursorRepresentation()->GetPlaneSource()->GetOrigin());
21                 ps->SetPoint1(view[i]->GetResliceCursorWidget()->
22                     GetResliceCursorRepresentation()->GetPlaneSource()->GetPoint1());
23                 ps->SetPoint2(view[i]->GetResliceCursorWidget()->
24                 GetResliceCursorRepresentation()->GetPlaneSource()->GetPoint2());
25  
26                 view[i]->GetImagePlaneWidget()->UpdatePlacement(); 
27                 view[i]->Render();
28             } 
29             view[3]->Render();
30         } 
31         
32     } 
33  
34     vtkResliceCursorCallback() {} 
35     CvtkView *view[4];
36 };

Every time you listen to the vtkrelicecursorwidget:: resoliceaxechangedevent message, you can execute the vtkrelicecursorcallback:: execute function to synchronize and refresh the view. This function updates the tangent plane parameters of the vtkrimageplanewidget object in three views, that is, using vtkrelicecursorthicklinerepresentation:: getplaneresource() in each view

Obtain the texture mapping plane object vtkPlaneSource in the current direction, and obtain the origin of the plane and two points defining the coordinate axis in the plane,

vtkPlaneSource::GetOrigin(),

vtkPlaneSource::GetPoint1();

vtkPlaneSource::GetPoint2();

Set the coordinates of three points into the tangent plane object of vtkImagePlaneWidget by calling the function

vtkImagePlaneWidget::UpdatePlacement()

Update the spatial position and pose of the segmentation plane to get a new image section. Finally, refresh the four views to complete the synchronization and update of the four views.

After defining this class, in the initialization function BOOL CvtkDialogDlg::OnInitDialog(), define this class object and set the corresponding listening message to realize view synchronization.

 1 vtkSmartPointer<vtkResliceCursorCallback> cbk = 
 2 vtkSmartPointer<vtkResliceCursorCallback>::New(); 
 3  
 4 cbk->view[0] = &m_AxialView;
 5 m_AxialView.GetResliceCursorWidget()->AddObserver( 
 6 vtkResliceCursorWidget::ResliceAxesChangedEvent, cbk ); 
 7  
 8 cbk->view[1] = &m_SagittalView;
 9 m_SagittalView.GetResliceCursorWidget()->AddObserver( 
10 vtkResliceCursorWidget::ResliceAxesChangedEvent, cbk );
11  
12 cbk->view[2] = &m_CoronalView;
13 m_CoronalView.GetResliceCursorWidget()->AddObserver( 
14 vtkResliceCursorWidget::ResliceAxesChangedEvent, cbk );
15  
16 cbk->view[3]= &m_3DView;
4.3 data management

The tree control is used to manage image files and respond to right-click messages and left-click messages. Right click the right-click menu function of the root node of the message response tree control, which mainly defines the functions of image reading, image saving, image emptying and so on. After the image is read in, set it to three views, and call the corresponding CvtkView::Render() function to update the view (a view refresh function defined by CvtkView). The left button of the tree node presses the message to realize the image switching; Click one image node at a time to update the current image to the four views.

So far, a complete four view image display Demo has been completed. The Demo can realize two-dimensional slice display in three directions of cross-section, sagittal plane and coronal plane, as well as three-dimensional display of three slices; The synchronous display of four views is realized, that is, when the tangent plane in an image changes, other views will be updated synchronously. In the implementation process, it is implemented based on two VTK Widget classes, namely vtkresilicecursor widget and vtkImagePlaneWidget. Vtkrelicecursorwidget integrates a vtkrelicecursorthicklinerepresentation object to adjust the segmentation plane interactively, and a vtkrelicecursor object is used for image segmentation according to the segmentation plane set by the user; The vtkImagePlaneWidget also calculates and displays the segmentation image using the internally defined vtkimageresolice according to the vtkresilicecursor object and the segmentation plane set by the user. This example only implements a four view Demo program, and there are still many places that need to be improved. If you are interested, you can modify and improve it on this basis.

Posted by frikus on Mon, 09 May 2022 09:15:24 +0300