Tags

, , ,

After all learning and practicing about DLL, LIB, CLR, P/Invoke in previous blogs, finally I got back to my main mission — to use VC++ OpenCV codes (with barely no UIs) in a VC# project (rich UIs). No concern about converting IplImage or CvMat images of OpenCV to formats supported by C# in this blog.

There were a few errors before I could get this done but it went well anyway. Mostly, I followed the same steps as written in Use C++ codes in a C# project — solution of wrapping native C++ with a managed CLR wrapper. Hence, there will be 3 projects involved: one VC++ OpenCV project for main computing, one VC++ CLR project for wrapping (creating DLL), and one VC# project for testing DLL call.

::::::: STEP 1: Create a VC++ OpenCV project for main computing :::::::

1 ) Create a new project, named cppOpencv, by choosing File > New > Project > Visual C++ >Win32 Console Application > Application Settings > Application type: Console Application, Additional options: Empty project > click Finish button.

2 ) Add a header file of your computing class to the project. For example, cppOpencv.h containing:

#pragma once
#include <opencv2/opencv.hpp>

class MYcppGui {
public:
    MYcppGui();
    ~MYcppGui();

    int myCppLoadAndShowRGB(char* url, int maxWidth, int maxHeight, int nPicPerRow);

private:
    bool win0; // status of an OpenCV window
};

3 ) Add an OpenCV C++ source file of your computing class to the project. For example, cppOpencv.cpp containing:

#pragma once
#include "cppOpencv.h"

MYcppGui::MYcppGui()
{
    win0 = false;
}

MYcppGui::~MYcppGui()
{
    cvDestroyAllWindows();
}

int MYcppGui::myCppLoadAndShowRGB( char* url, int maxWidth, int maxHeight, int nPicPerRow )
{       
    // my OpenCV C++ codes uses only cxcore, highgui and imgproc 
}

Note: I wrote all OpenCV codes in conventional unmanaged C++ styles. 

4 ) Add another C++ source file to the project. This file is just for testing the created class and will not be used in the further steps. For example, main.cpp containing:

#include <stdio.h>
#include "cppOpencv.h"

int main() {
    MYcppGui *myGui = new MYcppGui();
    myGui->myCppLoadAndShowRGB("C:\\Users\\Public\\Pictures\\Sample Pictures\\Desert.jpg", 800, 700, 3);
    free(myGui);

    printf("End of the test program\n");
    system("pause");
    return 0;
}

5 ) Before building the project, make sure to set Project Properties following instructions in Install OpenCV 2.4.5 and use it with Microsoft Visual C++ 2010. Otherwise, a number of errors will be reported. I recommend creating Property Sheets as suggested in the instructions as it can be reused in STEP 2.

6 ) Build and run the project to ensure that no error exists and your class works as desired.

::::::: STEP 2: Create a VC++ CLR project (DLL) for wrapping :::::::

1 ) Create a new CLR project, named clrOpencv, by choosing File > New > Project > Visual C++ >CLR > Class Library. And you will find many files created, including clrOpencv.h and clrOpencv.cpp in Solution Explorer panel.

2 ) In clrOpencv.h, add the following codes: (the codes printed in blue, red and orange are those added or edited by me; the orange ones refer to conversion between managed and unmanaged format)

    • Alternative 1: Create a wrapper straightforward. The problem for this alternative is the parameter char* url (required by cvLoadImage command used inside myLoadAndShowRGB) which is an unmanaged format and not supported in C#. In order to convert System.String used in C# to char* and pass it to this wrapper function, unsafe directive is required in C# codes (will be explained later in STEP 3.3).
#pragma once

#include "C:\Projects\cppOpencv\cppOpencv\cppOpencv.h"
#include "C:\Projects\cppOpencv\cppOpencv\cppOpencv.cpp"

using namespace System;

namespace MYopencv {
    public ref class MYGui
    {
    public:
       MYGui();
       int myLoadAndShowRGB(char* url, int maxWidth, int maxHeight, int nPicPerRow);

    private:
       MYcppGui *myGui;
    };
}
    • Alternative 2: Do the conversion (from unmanaged char* to managed String^) inside the wrapper. This way, the wrapper acts as a complete bridge between unmanaged C++ codes (STEP 1) and managed C# codes (STEP 3). No need to add codes for interop (inter-operate) in those two projects.
#pragma once

#include "C:\Projects\cppOpencv\cppOpencv\cppOpencv.h"
#include "C:\Projects\cppOpencv\cppOpencv\cppOpencv.cpp"

using namespace System;
using namespace System::Runtime::InteropServices;

namespace MYopencv {
    public ref class MYConversion
    {
    public:
       MYConversion();
       char* myStringToChar ( String^ str );
    };

    public ref class MYGui
    {
    public:
       MYGui();
       int myLoadAndShowRGB(String^ url, int maxWidth, int maxHeight, int nPicPerRow);

    private:
       MYcppGui *myGui;
    };
}

In the above example, I created a separated class named MYConversion as I planed to reuse it many times. However, you can just put the conversion codes directly in the body of myLoadAndShowRGB (STEP 2.3).

3 ) Continue from the previous step, in clrOpencv.cpp, add the following codes: (the codes printed in blue and orange are those added or edited by me; the orange ones refer to conversion between unmanaged and managed format)

    • Alternative 1: Create a wrapping function by simply calling the instance of MYcppGui *myGui.
// This is the main DLL file.
#include "stdafx.h"
#include "clrOpencv.h"

#include "C:\Projects\cppOpencv\cppOpencv\cppOpencv.h"
#include "C:\Projects\cppOpencv\cppOpencv\cppOpencv.cpp"

using namespace MYopencv;

MYGui::MYGui()
{
    myGui = new MYcppGui();
}

int MYGui::myLoadAndShowRGB( char *url, int maxWidth, int maxHeight, int nPicPerRow)
{
    return myGui->myCppLoadAndShowRGB( url, maxWidth, maxHeight, nPicPerRow);
}
    • Alternative 2: Create a wrapping function which includes conversion between unmanaged and managed format.
// This is the main DLL file.
#include "stdafx.h"
#include "clrOpencv.h"

#include "C:\Projects\cppOpencv\cppOpencv\cppOpencv.h"
#include "C:\Projects\cppOpencv\cppOpencv\cppOpencv.cpp"

using namespace MYopencv;

// ++++++++++++ MYGui Class ++++++++++++

MYGui::MYGui()
{
    myGui = new MYcppGui();
}

int MYGui::myLoadAndShowRGB( String^ url, int maxWidth, int maxHeight, int nPicPerRow)
{
    MYConversion ^convert = gcnew MYConversion();
    char* str = convert->myStringToChar(url);

    int result = myGui->myCppLoadAndShowRGB( str, maxWidth, maxHeight, nPicPerRow);

    // Add the line below will cause runtime assertion error (sth about heap error) in C#
    //free(str);   

    return result; 
} 

// ++++++++++++ MYConversion Class ++++++++++++ 

MYConversion::MYConversion() {} 

char* MYConversion::myStringToChar ( String^ str ) 
{ 
   IntPtr ptr = Marshal::StringToHGlobalAnsi(str); 
   return static_cast<char*>(ptr.ToPointer()) ; 
}

4 ) Your codes are ready for building DLL now. But don’t forget to set Project Properties regarding OpenCV. Otherwise, there are errors saying fatal error C1083: Cannot open include file: ‘opencv2/opencv.hpp’: No such file or directory. The easiest way to set Project Properties is to use Add Existing Property Sheet and choose the property files (*.props) created in STEP 1.

5 ) Build either Debug or Release version, or both, of the project and you should get the DLL(s) ready to be used in other .NET environment.

Note: Although I properly added Property Sheets to my CLR project, I still got many link errors saying something like

error LNK2028: unresolved token (0A0004C6) "extern "C" void __cdecl cvDestroyWindow(char const *)" ....

error LNK2019 unresolved external symbol "extern "C" void __cdecl cvDestroyWindow ....

error LNK2001 unresolved external symbol "public: virtual void __thiscall cv::HOGDescriptor::setSVMDetector ....

I double checked the Property Sheets and they already included $(OPENCV_BUILD)\x86\vc10\lib as an additional library directory; there was not supposed to have link errors at this point. Finally, in addition to including Property Sheets, I had to manually add $(OPENCV_BUILD)\x86\vc10\lib to Project PropertiesCommon Properties > VC++ directories > Library Directories again and those link errors just disappeared.

::::::: STEP 3: Create a VC# project for testing DLL call :::::::

1 ) Create a new VC# project and add your preferred UIs. Or create a simple console application.

2 ) Solution Explorer panel > References > right click and choose Add Reference > in Browse tab, browse for the DLL created in STEP 2 > click OK button. Now the name clrOpencv should be listed under References section in Solution Explorer panel.

3 ) Add your namespace to the C# project (by using directive) and try calling your wrapper functions. In the example below, I used OpenFileDialog to get System.String of image’s url from a user and pass it to my wrapper function: (codes related to this article are written in either blue or orange; the orange ones refer to managed-to-unmanaged conversion and vice versa)

    • Alternative 1: As the wrapper does not perform any conversion from System.String to char*, C# must do it.
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using MYopencv;

namespace WindowsFormsApplication1
{
   public partial class Form1 : Form
   {
      MYGui cvGui;

      public Form1()
      {
         InitializeComponent();
         cvGui = new MYGui();
      }

      private void Form1_Load(object sender, EventArgs e)
      { }

      private void button2_Click(object sender, EventArgs e)
      {
         OpenFileDialog fdlg = new OpenFileDialog();
         fdlg.Title = "Choose image";
         fdlg.InitialDirectory = @"C:\Users\Public\Pictures\Sample Pictures";
         fdlg.Filter = "Image files (*.*)|*.*";
         fdlg.FilterIndex = 2;
         fdlg.RestoreDirectory = true;

         if (fdlg.ShowDialog() == DialogResult.OK)
            textBox1.Text = fdlg.FileName;
      }

      private void button1_Click(object sender, EventArgs e)
      {
          string url = textBox1.Text.Trim();
          byte[] urlByte = Encoding.ASCII.GetBytes(url);
          unsafe
          {
             fixed (byte* p =urlByte)
             {
                sbyte* sp = (sbyte*)p;
                if (cvGui.myLoadAndShowRGB( sp, 750, 750, 3) < 0)
                    MessageBox.Show("Invalid image's directory.", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
             }
          } 

      } // end of button1_click

   } // end of class Form1

} // end of namespace WindowsFormsApplication1

Note: To allow unsafe codes in C#, go to Solution Explorer panel > right click at your project name (e.g., WindowsFormsApplication1) > choose Properties > choose All Configurations in Configuration drop-down-list > under General section, check Allow unsafe code.

    • Alternative 2: All conversions were done inside the wrapper so no need to do anything special in C# codes.
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using MYopencv;

namespace WindowsFormsApplication1
{
   public partial class Form1 : Form
   {
      MYGui cvGui;

      public Form1()
      {
         InitializeComponent();
         cvGui = new MYGui();
      }

      private void Form1_Load(object sender, EventArgs e)
      { }

      private void button2_Click(object sender, EventArgs e)
      {
         OpenFileDialog fdlg = new OpenFileDialog();
         fdlg.Title = "Choose image";
         fdlg.InitialDirectory = @"C:\Users\Public\Pictures\Sample Pictures";
         fdlg.Filter = "Image files (*.*)|*.*";
         fdlg.FilterIndex = 2;
         fdlg.RestoreDirectory = true;

         if (fdlg.ShowDialog() == DialogResult.OK)
             textBox1.Text = fdlg.FileName;
      }

      private void button1_Click(object sender, EventArgs e)
      {
         int result = cvGui.myLoadAndShowRGB(textBox1.Text.Trim(), 750, 750, 3);
         if ( result < 0)
             MessageBox.Show("Invalid image's directory.", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);

      } // end of button1_click

   } // end of class Form1

} // end of namespace WindowsFormsApplication1

4 ) Build and run the project to ensure that no errors exist and everything works as expected.

::::::: Conclusion :::::::

For now, I think that wrapping OpenCV C++ codes with CLR is not much different from wrapping non-OpenCV C++ codes. Personally, I prefer doing all conversions in the wrapper class; do the conversion just once (in the wrapper) and it will keep all my future C# projects involving that DLL nice and clean.