matlab与c++混编(cpp文件调用opencv库)

一、前言

传统算法有太多都用到了 matlab 与 c++ 混编。之前设置断点调试不出来,可是换一个新的代码来跑还是需要调试。总也避不过去,只能整整这个烂活。

二、解决方案

我的环境:matlabR2021a + opencv2.4.12 + visual studio2022

(一)基础部分

先上几位大佬的连接:
link1
link2
以及 apiref.pdf 文件。
这里我按照大佬们的介绍,先不调用 opencv 库,而是先利用一个简单的 mexFunction 函数进行测试.

1. 测试1

(1)新建 hello.cpp 文件,并不需要建什么工程:

/*hello.cpp*/
#include "mex.h" 

void mexFunction(int nlhs, mxArray* plhs[], int nrhs, const mxArray* prhs[])
{
	mexPrintf("hello,world!/n");
}

这里我将 hello.cpp 存储在下面这个位置:

E:\project\practice\matlab and C++ mixd coding

打开 matlab,将路径更改为上述位置,在命令行输入:

mex -setup

选择合适的编译器,这里我选择的是 VS2022,选择成功后显示如下:
在这里插入图片描述

(2)然后继续在命令行输入:

 mex hello.cpp

编译成功后显示如下:
在这里插入图片描述
此时会在左侧的目录栏生成一个 mexw64 文件(如果是 32 位系统则会生成 mexw32):
在这里插入图片描述
此时再在命令行输入 hello,便会输出 hello world!
在这里插入图片描述
贴一下大佬的介绍:
在使用MATLAB编译C/C++代码时,我们需要修改C/C++代码,在里面添加Matlab能支持的函数接口。这样Matlab才能调用它。然后再通过Matlab的Mex工具来编译它。mexFunction() 就是接口函数。mexFunction() 定义如下:

void mexFunction(int nlhs, mxArray *plhs[], int nrhs, const mxArray *prhs[]) 

首先,这个函数是没有返回值的。它不是通过返回值把 c++ 代码的计算结果传回 Matlab 的,而是通过对参数 plhs 赋值实现的。

nlhs:输出参数数目。感觉是number of left hand size parameters,也就是Matlab调用语句左边的变量个数,实际上就是需要返回给Matlab的返回值变量有多少个。
plhs:指向输出参数的指针。感觉是pointer of left hand size parameters,也就是函数返回参数的指针。它是一个指针数组。换句话说,它是一个数组,每个元素是个指针,每个指针指向一个数据类型为mxArray的返回参数。
nrhs:输入参数数目。number of right hand size parameters,也就是Matlab调用语句右边的变量个数。
prhs:这个是pointer of right hand size parameters,和plhs类似。要注意prhs是const的指针数组,即不能改变其指向内容。


举个例子,使用
[a,b]=test(c,d,e)
调用mex函数test时,传给test的这四个参数分别是
      2,plhs,3,prhs
其中: 
prhs[0]=c 
prhs[1]=d 
prhs[2]=e 
当函数返回时,将会把你放在plhs[0],plhs[1]里的地址赋给a和b,达到返回数据的目的。

可以看到,prhs[i] 和 plhs[i] 都是指向类型 mxArray 类型数据的指针。 这个类型是在 mex.h 中定义的。事实上,Matlab 最基本的单元为 array,无论是什么类型也好,如有 doublearray、 cell array、struct array…… 即便是 b = 1.1 也是一个 1x1 的 double array。而在 C 语言中,Matlab 的 array 使用 mxArray 类型来表示。因此,plhs 和 prhs 都是指向 mxArray 类型的指针数组。当然还有其他的数据类型,可以参考 Apiguide.pdf 里的介绍。

2. 测试2

/*hello.cpp*/
#include "mex.h" 
void mexFunction(int nlhs, mxArray* plhs[], int nrhs, const mxArray* prhs[])
{
	int i;
	i = mxGetScalar(prhs[0]);
	if (i == 1)
		mexPrintf("hello,world!/n");
	else
		mexPrintf("大家好!/n");
}

编译成功后,调用结果如下:
在这里插入图片描述
这里用到了一个函数 mxGetScalar(),调用方式如下:

   i=mxGetScalar(prhs[0]); 

“Scalar” 就是标量的意思。在 Matlab 里数据都是以数组的形式存在的,mxGetScalar 的作用就是把通过 prhs[0] 传递进来的 mxArray 类型的指针指向的数据(标量)赋给 C 程序里的变量。这个变量本来应该是 double 类型的,通过强制类型转换赋给了整形变量 i。

3. 测试3

/*hello.cpp*/
#include "mex.h" 
void mexFunction(int nlhs, mxArray* plhs[], int nrhs, const mxArray* prhs[])
{
	int* i;
	i = mxGetPr(prhs[0]);
	if (i[0] == 1)
		mexPrintf("hello,world!/n");
	else
		mexPrintf("大家好!/n");
}

这里,通过 mxGetPr 函数从指向 mxArray 类型数据的 prhs[0] 获得了指向 double 类型的指针。
问题来了。我在这一步运行时出现了错误:
在这里插入图片描述
检查后发现,i 的类型应该为 double*。更改后运行结果正确:
在这里插入图片描述
但是,如果输入的不是单个的数据,而是向量或矩阵,那该怎么处理呢 ?通过 mxGetPr 只能得到指向这个矩阵的指针,如果我们不知道这个矩阵的确切大小,就没法对它进行计算。
为了解决这个问题,Matlab 提供了两个函数 mxGetM 和 mxGetN 来获得传进来参数的行数和列数。测试 4 的功能很简单,就是获得输入的矩阵,把它在屏幕上显示出来.

4. 测试4

/*show.cpp*/
#include "mex.h" 
void mexFunction(int nlhs, mxArray* plhs[], int nrhs, const mxArray* prhs[])
{
    double* data;
    int M, N;
    int i, j;
    data = mxGetPr(prhs[0]); //获得指向矩阵的指针 
    M = mxGetM(prhs[0]); //获得矩阵的行数 
    N = mxGetN(prhs[0]); //获得矩阵的列数 
    for (i = 0; i < M; i++)
    {
        for (j = 0; j < N; j++)
            mexPrintf("%4.3f  ", data[j * M + i]);
        mexPrintf("\n");
    }
}

需要注意的是,在Matlab里,矩阵第一行是从 1 开始的,而在 C 语言中,第一行的序数为零,Matlab 里的矩阵元素 b(i, j) 在传递到 C 中的一维数组大 data 后对应于 data[j*M+i] 。

测试结果:
在这里插入图片描述
这里我不是在命令行进行测试的,而是在 matlab 中新建了一个脚本 main.m:

mex -g hello.cpp
a=1:10; 
b=[a;a+1]; 
hello(a) ;
hello(b) ;

(注意:这里我是直接在最开始的测试 1 中建立的 hello.cpp 文件中进行改写的,因此调用函数还是用的 hello(a) 和 hello(b)。若这个测试又另外新建了一个 cpp 文件 show,则调用函数时应使用 hello(a) 和 hello(b),总之,调用函数名应与文件名保持一致)

这里最开始的命令写为 -g 是为了进行调试,详情请见这位大佬的帖子。即:

这里的 -g 选项是调试时必须的。这里很重要的一点是生成的 .pdb 文件是路径依赖的,即使是同一个 .cpp 文件生成的 mex 文件,它们在不同路径下的得到的 .pdb 文件也不能互用。在某一个路径下调试 mex 程序(.cpp 文件),必须用这个路径下的的 .pdb 文件。
运行后,果然生成了一个 .pdb 文件:
在这里插入图片描述
随后,在 matlab 调用函数的那一行打好断点:
在这里插入图片描述

在 VS 中点击附加到进程,选择 matlab.exe :
在这里插入图片描述

在这里插入图片描述
显示如下图:
在这里插入图片描述

在需要调试的地方打好断点,例如:
在这里插入图片描述
此时断点为空心,但不影响。然后在 matlab 中调试:
在这里插入图片描述
点击步进或 F10 自动跳转至 cpp 文件:
在这里插入图片描述
在 VS 中可以正常调试,且进来后断点变为实心:
在这里插入图片描述

终于成功了一次。

5. 测试5

输入数据是在函数调用之前已经在 Matlab 里申请了内存的,由于 mex 函数与 Matlab 共用同一个地址空间,因而在 prhs[] 里传递指针就可以达到参数传递的目的。但是,输出参数却需要在 mex 函数内申请到内存空间,才能将指针放在 plhs[] 中传递出去。由于返回指针类型必须是 mxArray,所以 Matlab 专门提供了一个函数 mxCreateDoubleMatrix 来实现内存的申请,函数原型如下:

mxArray *mxCreateDoubleMatrix(int m, int n, mxComplexity ComplexFlag) 
   m:待申请矩阵的行数 
   n:待申请矩阵的列数 

为矩阵申请内存后,得到的是 mxArray 类型的指针,就可以放在 plhs[] 里传递回去了。但是对这个新矩阵的处理,却要在函数内完成,这时就需要用到前面介绍的 mxGetPr。使用 mxGetPr 获得指向这个矩阵中数据区的指针(double类型)后,就可以对这个矩阵进行各种操作和运算了。
下面的程序是在上面的测试 4 的基础上稍作改变得到的:

/*reverse.cpp*/
#include "mex.h" 
void mexFunction(int nlhs, mxArray* plhs[], int nrhs, const mxArray* prhs[])
{
    double* inData;
    double* outData;
    int M, N;
    int i, j;
    inData = mxGetPr(prhs[0]);
    M = mxGetM(prhs[0]); 
    N = mxGetN(prhs[0]); 
    plhs[0] = mxCreateDoubleMatrix(M, N, mxREAL); 
    outData = mxGetPr(plhs[0]); 
    for (i = 0; i < M; i++)
        for (j = 0; j < N; j++)
            outData[j * M + i] = inData[(N - 1 - j) * M + i];
}

这样得到的输出矩阵是每一行得到了翻转。在 matlab 中输入:

mex -g hello.cpp
a=1:10; 
b=[a;a+1]; 
c = hello(b) ;

b 和 c 矩阵如下所示:
在这里插入图片描述
在这里插入图片描述
整个矩阵翻转:

/*reverse.cpp*/
#include "mex.h" 
void mexFunction(int nlhs, mxArray* plhs[], int nrhs, const mxArray* prhs[])
{
    double* inData;
    double* outData;
    int M, N;
    int i, j;
    inData = mxGetPr(prhs[0]);
    M = mxGetM(prhs[0]); 
    N = mxGetN(prhs[0]); 
    plhs[0] = mxCreateDoubleMatrix(M, N, mxREAL); 
    outData = mxGetPr(plhs[0]); 
    for (i = 0; i < M; i++)
        for (j = 0; j < N; j++)
            //outData[j * M + i] = inData[(N - 1 - j) * M + i];  // 每行作翻转
            outData[j * M + i] = inData[(N - 1 - j) * M + M - i - 1];  // 矩阵作180度翻转
}

在这里插入图片描述

6. 测试6

当然,Matlab 里使用到的并不是只有 double 类型这一种矩阵,还有字符串类型、稀疏矩阵、结构类型矩阵等等,并提供了相应的处理函数。本文用到编制 mex 程序中最经常遇到的一些函数,其余的详细情况清参考 Apiref.pdf
通过前面两部分的介绍,大家对参数的输入和输出方法应该有了基本的了解。具备了这些知识,就能够满足一般的编程需要了。但这些程序还有些小的缺陷,以前面介绍的 re 由于前面的例程中没有对输入、输出参数的数目及类型进行检查,导致程序的容错性很差,以下程序则容错性较好:

/*reverse.cpp*/
#include "mex.h" 
void mexFunction(int nlhs, mxArray* plhs[], int nrhs, const mxArray* prhs[])
{
    double* inData;
    double* outData;
    int M, N;
    int i, j;
    //异常处理 
    //异常处理 
    if (nrhs != 1)
        mexErrMsgTxt("USAGE: b=reverse(a)\n");
    if (!mxIsDouble(prhs[0]))
        mexErrMsgTxt("the Input Matrix must be double!\n");
    inData = mxGetPr(prhs[0]);
    M = mxGetM(prhs[0]); 
    N = mxGetN(prhs[0]); 
    plhs[0] = mxCreateDoubleMatrix(M, N, mxREAL); 
    outData = mxGetPr(plhs[0]); 
    for (i = 0; i < M; i++)
        for (j = 0; j < N; j++)
            //outData[j * M + i] = inData[(N - 1 - j) * M + i];  // 每行作翻转
            outData[j * M + i] = inData[(N - 1 - j) * M + M - i - 1];  // 矩阵作180度翻转
}

在上面的异常处理中,使用了两个新的函数:mexErrMsgTxt 和 mxIsDouble。MexErrMsgTxt 在给出出错提示的同时退出当前程序的运行。MxIsDouble 则用于判断 mxArray 中的数据是否double 类型。当然 Matlab 还提供了许多用于判断其他数据类型的函数,这里不加详述。
需要说明的是,Matlab 提供的 API 中,函数前缀有 mex- 和 mx- 两种。带 mx- 前缀的大多是对 mxArray 数据进行操作的函数,如 mxIsDouble, mxCreateDoubleMatrix 等等。而带 mex 前缀的则大多是与 Matlab 环境进行交互的函数,如 mexPrintf,mxErrMsgTxt 等等。

(二)进阶部分

下面开始看 cpp 函数调用了 opencv 库的情况。举一个简单的例子,将彩色图像转化为灰度图。即将一个图像文件名通过 matlab 传递给 c++,然后 c++ 将这个图像读入,再转成灰度图,然后返回给 Matlab。而 c++ 的图像读入和灰度转换的操作通过调用OpenCV的库函数来实现:

#include "opencv2/opencv.hpp"  
#include "mex.h"  

using namespace cv;

/*******************************************************
Usage: [imageMatrix] = RGB2Gray('imageFile.jpeg');
Input:
    a image file
OutPut:
    a matrix of image which can be read by Matlab

**********************************************************/


void exit_with_help()
{
    mexPrintf(
        "Usage: [imageMatrix] = DenseTrack('imageFile.jpg');\n"
    );
}

static void fake_answer(mxArray* plhs[])
{
    plhs[0] = mxCreateDoubleMatrix(0, 0, mxREAL);
}

void RGB2Gray(char* filename, mxArray* plhs[])
{
    // read the image  
    Mat image = imread(filename);
    if (image.empty()) {
        mexPrintf("can't open input file %s\n", filename);
        fake_answer(plhs);
        return;
    }

    // convert it to gray format  
    Mat gray;
    if (image.channels() == 3)
        cvtColor(image, gray, CV_RGB2GRAY);
    else
        image.copyTo(gray);

    // convert the result to Matlab-supported format for returning  
    int rows = gray.rows;
    int cols = gray.cols;
    plhs[0] = mxCreateDoubleMatrix(rows, cols, mxREAL);
    double* imgMat;
    imgMat = mxGetPr(plhs[0]);
    for (int i = 0; i < rows; i++)
        for (int j = 0; j < cols; j++)
            *(imgMat + i + j * rows) = (double)gray.at<uchar>(i, j);

    return;
}

void mexFunction(int nlhs, mxArray* plhs[], int nrhs, const mxArray* prhs[])
{
    if (nrhs == 1)
    {
        char filename[256];
        mxGetString(prhs[0], filename, mxGetN(prhs[0]) + 1);
        if (filename == NULL)
        {
            mexPrintf("Error: filename is NULL\n");
            exit_with_help();
            return;
        }

        RGB2Gray(filename, plhs);
    }
    else
    {
        exit_with_help();
        fake_answer(plhs);
        return;
    }
}

代码主要分三部分,第一个就是传入参数的测试,看看 Matlab 传入的参数是否存在错误,还包括了些异常处理。第二个就是帮助信息。第三个就是主要的实现函数了。只有 OpenCV 的读图像和灰度转换这里就不讲了,就是两个函数的调用。关键的地方还是如果把一个图像,也就是二维数组,传递给 mexFunction 的参数,让它返回给 Matlab。实际上,我们只要清楚一点:

    plhs[0] = mxCreateDoubleMatrix(2, 3,mxREAL);

这个函数建立的矩阵的指针 plhs[0] 是按照列的方式来存储的。假设 imgMat 是它的指针,那么 (imgMat+1) 就是矩阵元素 [1, 0],(imgMat+2) 就是矩阵元素 [0, 1],*(imgMat+4) 就是矩阵元素[0, 2]。上面的代码就是按照这个方式,将图像 gray 中像素值赋值给参数 plhs[0] 相应的位置(实际上也许可以直接内存拷贝,但因为里面是指针操作,涉及到局部变量 gray 的销毁问题,所以就简单的用上面的笨但稳当的方式来实现了)。

接下来是 make.m 文件。里面需要获取你的电脑的系统版本是 32 还是 64 位的,来选择编译选项。然后添加 OpenCV 的相关配置。如果您需要使用使用,请修改成您的 OpenCV 的相关目录。然后给出一个需要编译的文件的列表。最后分析这个列表,加上编译选项,用 mex 来编译列表里面的所有文件:

%// This make.m is for MATLAB  
%// Function: compile c++ files which rely on OpenCV for Matlab using mex  
%// Author : zouxy  
%// Date   : 2014-03-05  
%// HomePage : http://blog.csdn.net/zouxy09  
%// Email  : zouxy09@qq.com  
  
%% Please modify your path of OpenCV  
%% If your have any question, please contact Zou Xiaoyi  
  
% Notice: first use "mex -setup" to choose your c/c++ compiler  
clear all;  
  
%-------------------------------------------------------------------  
%% get the architecture of this computer  
is_64bit = strcmp(computer,'MACI64') || strcmp(computer,'GLNXA64') || strcmp(computer,'PCWIN64');  
  
  
%-------------------------------------------------------------------  
%% the configuration of compiler  
% You need to modify this configuration according to your own path of OpenCV  
% Notice: if your system is 64bit, your OpenCV must be 64bit!  
out_dir='./';  
CPPFLAGS = ' -O -DNDEBUG -I.\ -ID:\OpenCV_64\include'; % your OpenCV "include" path  
LDFLAGS = ' -LD:\OpenCV_64\lib';                       % your OpenCV "lib" path  
LIBS = ' -lopencv_core240 -lopencv_highgui240 -lopencv_video240 -lopencv_imgproc240';  
if is_64bit  
    CPPFLAGS = [CPPFLAGS ' -largeArrayDims'];  
end  
%% add your files here!  
compile_files = {   
    % the list of your code files which need to be compiled  
    'RGB2Gray.cpp'  
};  
  
  
%-------------------------------------------------------------------  
%% compiling...  
for k = 1 : length(compile_files)  
    str = compile_files{k};  
    fprintf('compilation of: %s\n', str);  
    str = [str ' -outdir ' out_dir CPPFLAGS LDFLAGS LIBS];  
    args = regexp(str, '\s+', 'split');  
    mex(args{:});  
end  
  
fprintf('Congratulations, compilation successful!!!\n'); 

感谢大佬的分享。
运行完这个函数后,编译成功,生成 mexw64 文件:
在这里插入图片描述
在这里插入图片描述
现在在命令行或新的脚本中输入:

img = picture('G:\vot2016\ball1\00000001.jpg');
imshow(uint8(img));

成功显示图片:
在这里插入图片描述
然而这样只是调用成功,并不能调试,还是不能够达到我们的目的。我想之前一样运行 main.m:

%// 调用cpp函数
mex -g picture.cpp
img = picture('G:\dataset\vot2016\ball1\00000001.jpg');
imshow(uint8(img));

结果却失败了:
在这里插入图片描述
将 -g 去掉之后也一样。为什么呢?
仔细看一下这个代码,可以看到最关键的就是最后一部分:

%-------------------------------------------------------------------  
%% compiling...  
for k = 1 : length(compile_files)  
    str = compile_files{k};  
    fprintf('compilation of: %s\n', str);  
    str = [str ' -outdir ' out_dir CPPFLAGS LDFLAGS LIBS];  
    args = regexp(str, '\s+', 'split');  
    mex(args{:});  
end  

这里 mex 了 args{:} 这个数组。可以看到这里 picture.cpp 文件只执行了 mex,而后面的 out_dir CPPFLAGS LDFLAGS LIBS 中的内容执行的却是 mex -outdir。仿照这个过程,我们只需要将 cpp 文件改成我们需要的 mex -g 便好了(注意在适当的地方加空格):

%% compiling...  
for k = 1 : length(compile_files)  
    str = compile_files{k};  
    fprintf('compilation of: %s\n', str);  
    str = [' -g ' str ' -outdir ' out_dir CPPFLAGS LDFLAGS LIBS];  
    args = regexp(str, '\s+', 'split');  %% regexp对字符串进行查找,大小写敏感
    mex(args{:});  
end  

执行完后,成功生成 pdb 文件:
在这里插入图片描述
在 cpp 文件中设置断点,成功进入。
终于大功告成了。从第一次设置断点失败到现在,历时六个半月。泪目。

  • 24
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值