Matlab和OpenCV混合编程小结

本文所用的 Matlab 版本为 Matlab R2017b,OpenCV 版本为 opencv-3.4.3,C++ IDE 为 Visual Studio 2017,系统环境为 Windows 10_x64。

前言

  秋招告一段落了,又要回到最初的起点,继续搞(qu)科(hua)研(shui)了,由于前人的代码主要是用 C++ 和 Matlab 混编实现的,而 Shaun 比较熟悉的是 C++ 和 OpenCV,而用 OpenCV 完全重写前人的代码工作量又太大了而且有些 API 不是很好互换,为了方便站在巨人的肩膀上继续前进,所以只能学习一下 OpenCV 和 Matlab 的混合编程了,这样在前人的基础上实现 Shaun 自己的想法相对来说更容易一些。

准备篇

  由于目前主要用 C++ 实现的是一些小功能,也不需要调试,所以就直接使用 VSCode 进行编程了(或许以后还是会用 VS 进行一些简单的调试),而在没有配置相关环境的前提下,VSCode 无法实现自动补全,所以需要在 VSCode 中配置相应环境。具体添加方法如下:在 VSCode 中点击菜单栏 “查看” ==》“命令面板...” ==》选择 “C/Cpp: Edit Configurations...”==》在出现的 c_cpp_properties.json 文件中 "includePath" 对应的值中添加 OpenCV 的 include 目录和 Matlab 的 include 目录,添加之后的 "includePath" 如下:

1
2
3
4
5
6
7
{
"includePath": [
"${workspaceFolder}/**"
, "D:/ProgramFiles/OpenCV/3.4.3/build/include/**"
, "C:/Program Files/MATLAB/R2017b/extern/include/**"
]
}

如此就能在 VSCode 中写 OpenCV 和 Matlab 相关函数时实现自动补全了。

Matlab 和 C++ 混编篇

  由于 Shaun 使用的是 OpenCV 的 C++ 接口,所以需要先知道 Matlab 和 C++ 混合编程如何进行。以实现两个数的加法为例,首先创建一个 mexAdd.cpp 文件,其中 C++ 代码具体如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#include <mex.h>    // 必须包含头文件 mex.h
#include <iostream>

// 检查输入是否合法
void checkInputs(int nrhs, const mxArray *prhs[])
{
if (nrhs != 2)
{
mexErrMsgTxt("Incorrect number of inputs. Function expects 2 inputs.");
}

if (!mxIsDouble(prhs[0]))
{
mexErrMsgTxt("Input number must be double.");
}
}

double add(double x, double y)
{
return x + y;
}

/**
* nlhs:matlab 函数左边变量个数,即返回值参数个数
* plhs: matlab 函数左边变量,即返回值参数
* nrhs: matlab 右边变量个数,即函数输入参数个数
* prhs: matlab 函数右边变量,即函数输入参数
*/
void mexFunction(int nlhs, mxArray *plhs[], int nrhs, const mxArray *prhs[])
{
checkInputs(nrhs, prhs);

// 输入参数可以不使用指针,但输出参数必须使用指针
double *a = nullptr; // 输出参数
double b = 0.0, c = 0.0; // 两个输入参数
plhs[0] = mxCreateDoubleMatrix(1, 1, mxREAL); // 创建1x1的实数矩阵用作输出第一个参数
a = mxGetPr(plhs[0]); // 用指针a指向第一个输出
b = *(mxGetPr(prhs[0])); // b作为第一个输入
c = *(mxGetPr(prhs[1])); // c作为第二个输入
*a = add(b, c); // 计算b、c之和得到a
}

  若要使用 Matlab 混合编译 C++,必须要添加头文件 mex.h,使用 void mexFunction(int nlhs, mxArray *plhs[], int nrhs, const mxArray *prhs[]) 函数接收输入输出参数,如此编译完成之后,就和使用普通的 Matlab 函数一样了。具体编译调用方法如下,新建 addTest.m 文件,其中 Matlab 代码如下:

1
2
3
4
5
6
7
8
9
clc, clear, close all;  % 清空变量和关闭所有打开窗口

current_folder = pwd; % 获取当前文件路径
addpath(genpath(current_folder)); % 添加matlab临时搜索路径,并包含子文件夹(matlab退出后该路径不存在)

mex mexAdd.cpp; % 混合编译C++,得到matlab可识别的函数

a = 3.1; b = 2.6;
c = mexAdd(a, b);

其中 mex mexAdd.cpp 可以直接在 Matlab 命令行窗口下预先执行编译动作,编译成功后会输出一个 mexAdd.mexw64 文件,若是 32 位系统则后缀为 mexw32,之后直接执行 ans = mexAdd(3.1, 2.6); 即可在 Matlab 中调用该函数。在 Matlab 首次执行 mex 命令时,Matlab 会自动选择 VS 编译器作为默认 C++ 编译器,也可以执行 mex -setup 初始化或更换默认编译器。

BTW: 最好在安装 Matlab 之前安装 Visual Studio,否则使用 mex 编译时,可能会出现找不到编译器的情况。

Matlab 和 OpenCV 混编篇

  Matlab 和 OpenCV 混编大体上和 C++ 混编差不多,最大的区别在于如何利用 OpenCV 的 cv::Mat 对象和相关的库函数,Matlab 良心的提供了 OpenCV 接口以实现 mexArray 和 cv::Mat 格式之间的互相转化,使用这些接口需要包含头文件 opencvmex.hpp 。如果不使用 Matlab 提供的这些接口而是自己写转换过程的话有点麻烦,因为 Matlab 的数据是以列优先方式存储的,而 OpenCV 的数据是以行优先方式存储的。至于如何进行混编,主要有以下三种方式:

  1. 第一种是自己写 make.m 文件,相当于 gcc 编译时需要的 Makefile 文件,需要手动拼接各种编译命令和添加相应的附加依赖库;
  2. 第二种是通过 Matlab 官方提供的 Computer Vision System Toolbox OpenCV Interface 功能,Matlab 没有默认安装该功能,这个功能需要另外安装,具体安装方法为:在 Matlab 命令行窗口输入 visionSupportPackages,即可弹出“附加功能资源管理器”窗口选择对应附加功能安装即可,安装完之后可通过 mexOpenCV 命令对包含 OpenCV 库函数的 .cpp 文件进行编译,查看 mexOpenCV.m 的源码可知,mexOpenCV 其实是对 mex 命令进行了封装,其调用的 OpenCV 库也是其工具箱自带的 OpenCV,而且有些库还没有包含,有一定的局限性,不过该附加功能自带了些示例程序,可以参考学习一下;
  3. 第三种是使用第三方的 mexopencv,不过需要安装与该工具对应的 OpenCV 版本,并需要进行一定的配置工作,略显麻烦。

  Shaun 这里直接使用的是第一种方式,自己写 make.m 文件,比较灵活,想怎么配置就怎么配置。下面具体以 RGB 转 GRAY 为例,首先新建 mexRGB2GRAY.cpp,其中 C++ 代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
#include <opencvmex.hpp>

#define _DO_NOT_EXPORT
#if defined(_DO_NOT_EXPORT)
#define DllExport
#else
#define DllExport __declspec(dllexport)
#endif

/**
* Usage: [img_matrix] = mexRGB2GRAY('img.jpg');
* Input: a image file;
* Output: a matrix of image which can be read by Matlab
**/

// 检查输入是否合法
void checkInputs(int nrhs, const mxArray *prhs[])
{
if (nrhs != 1)
{
mexErrMsgTxt("Incorrect number of inputs. Function expects 1 inputs.");
}

if (mxGetNumberOfDimensions(prhs[0]) != 3) // 获取Matlab图像总的维度个数(灰度图为2维,RGB彩色图为3维)
{
mexErrMsgTxt("Incorrect number of dimensions. First input must be a RGB image.");
}

// 检查图像数据类型
if (!mxIsUint8(prhs[0]))
{
mexErrMsgTxt("Template and image must be UINT8.");
}
}

void exit_with_help()
{
mexPrintf("Uasge: [image_matrix] = mexRGB2GRAY('image_file.jpg');\n");
}

static void fakeAnswer(mxArray *plhs[])
{
plhs[0] = mxCreateNumericMatrix(0, 0, mxDOUBLE_CLASS, mxREAL); // 创建一个0x0的空双精度matlab矩阵
}

cv::Mat RGB2GRAY(const mxArray *prhs[])
{
cv::Ptr<cv::Mat> img_cv = ocvMxArrayToMat_uint8(prhs[0], true); // 将unit8数据类型的matlab矩阵转换为OpenCV的mat对象智能指针
if (img_cv.empty())
{
return cv::Mat_<double>(0, 0);
}

// 将RGB转化为GRAY图
cv::Mat gray((*img_cv).size(), CV_8UC1);
if ((*img_cv).channels() == 3)
{
cv::cvtColor(*img_cv, gray, CV_RGB2GRAY);
}
else if((*img_cv).channels() == 4)
{
cv::cvtColor(*img_cv, gray, CV_RGBA2GRAY);
}
else
{
(*img_cv).copyTo(gray);
}

return gray;
}

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

if (nrhs == 1)
{
cv::Mat gray = RGB2GRAY(prhs);
plhs[0] = ocvMxArrayFromMat_uint8(gray); // 将unit8数据类型的OpenCV的mat对象转换为matlab矩阵
}
else
{
exit_with_help();
fakeAnswer(plhs);
return ;
}
}

然后新建 makefile.m 文件,自己配置相关编译环境, Shaun 这里具体如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
function makefile()

% 选择相应计算机系统版本
is_64bit = strcmp(computer, 'MACI64') || strcmp(computer, 'GLNXA64') || strcmp(computer, 'PCWIN64');

% 配置OpenCV编译环境,如果系统是64位的,则OpenCV也需要是64位的
out_dir = '.'; % 输出目录,这里为当前目录
CPPFLAGS = ' -O -DNDEBUG -I./ -ID:/ProgramFiles/OpenCV/3.4.3/build/include'; % OpenCV “include” 目录
LDFLAGS = ' -LD:/ProgramFiles/OpenCV/3.4.3/build/x64/vc15/lib -LC:/PROGRA~1/MATLAB/R2017b/extern/lib/win64/microsoft'; % OpenCV “lib” 目录 和 MatLab 附加库目录
LIBS = ' -lopencv_world343 -lmwocvmex'; % 添加OpenCV相关库和Matlab libmwocvmex.lib库

if is_64bit
CPPFLAGS = [CPPFLAGS ' -largeArrayDims'];
end

% 需要编译的 cpp 文件
compile_files = {
'mexRGB2GRAY.cpp'
'mexAdd.cpp'
};

% 开始编译
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

end

其中 Matlab 配置路径中的 PROGRA~1 是指 Windows 下的 C 盘中的 Program Files 文件夹,为了使用 Matlab 提供的转换接口,libmwocvmex.lib 是必须要添加的一个库。最后具体使用示例 Matlab 代码如下:

1
2
3
4
5
6
7
8
9
10
11
clc, clear, close all;  % 清空变量和关闭所有打开窗口

current_folder = pwd; % 获取当前文件路径
addpath(genpath(current_folder)); % 添加matlab临时搜索路径,并包含子文件夹(matlab退出后该路径不存在)

makefile();

image = imread('lena.jpg');

I = mexRGB2GRAY(image);
figure, imshow(I);

也可以将 makefile(); 函数预先执行。※注: 这里如果出现编译报错 “缺少依赖共享库” 的情况可能还需要把 OpenCV 的 bin 目录加到系统环境变量 Path 中,Shaun 这里是路径 D:\ProgramFiles\OpenCV\3.4.3\build\x64\vc15\bin,然后重启 Matlab

调试篇

  若要对写的 mexAdd.cpp 文件进行调试,则需要

  1. 先使用 mex -g mexAdd.cpp 编译该文件,由于添加了 -g 参数,此时除了会生成 mexAdd.mexw64 文件之外,还会生成一个 mexAdd.mexw64.pdb 文件,该文件即包含调试信息;

  2. 然后在相应 .m 文件中调用 mexAdd 函数的位置设置断点,运行该相关文件,matlab 程序会在调用 mexAdd 函数之前停下;

  3. 此时使用 VS2017 打开 mexAdd.cpp 文件,在需要调试的地方设置好断点,选中菜单栏中的“调试” ==》“附加到进程 ”,或者直接点击菜单栏上的绿色三角 附加...,选中 MATLAB.exe ,点击附加,即可看到 VS2017 调试程序已启动;

  4. 最后回到 matlab 中继续运行相关文件,即可看到程序跳转到 VS2017,并执行到设置断点的地方,此时即可在 VS2017 中按调试 C++ 程序一样对其进行调试。

  完成调试并执行完 mexAdd.cpp 之后,程序将回到 matlab 界面继续执行,直到整个 matlab 程序执行完成。

后记

  这次主要是记录 Matlab 如何调用 C++ 编写的函数,其实还可以用 C++ 调用 Matlab 编写的函数,不过那是另一种混编方式了,以后有机会碰到的话再继续记录吧。

参考资料

[1] Matlab与C++混合编程(依赖OpenCV)

[2] 更改默认编译器

[3] OpenCV Interface Support

[4] Matlab OpenCV混合编程

[5] vc与matlab连接的实用函数简介