C++改写Matlab源码实践一之【显示图片】——————附带详细讲解和示例

时间:2022-10-05 19:53:21

0 背景

这里以显示Matlab变量struct中的图片程序代码为例,示范如何将其转换为C++代码,并在C++代码中显示图片。由于在Matlab的代码转换中是不支持转换输出图片文件的函数(例如imshow函数),于是只能返回uint8格式的图片数据((8位无符号整数,即1个字节,以此方式存储的图像称作8位图像,相比较默认matlab数据类型双精度浮点double(64位,8个字节),可以节省很大一部分存储空间),然后自己写图片显示函数。

难点就在于显示转换后的C++代码中返回的uint8格式的图片。

下图为Matlab变量struct中的信息(包含多个cell):
C++改写Matlab源码实践一之【显示图片】——————附带详细讲解和示例
每个cell中包含400个112923维度的矩阵(即400张三通道的图片数据【高112(行),宽92(列)】):

C++改写Matlab源码实践一之【显示图片】——————附带详细讲解和示例

下图为uint8格式的图片数据:
C++改写Matlab源码实践一之【显示图片】——————附带详细讲解和示例
下图为在Matlab中显示的图片数据:

C++改写Matlab源码实践一之【显示图片】——————附带详细讲解和示例

C++改写Matlab源码实践一之【显示图片】——————附带详细讲解和示例

下图为C++改写后显示彩色图片(也就是我们最终实现的结果):

C++改写Matlab源码实践一之【显示图片】——————附带详细讲解和示例

⚠️:这里预先转换的前提是,需要在转换机器上安装Matlab和opencv(用于图片显示),具体的安装方法在以前的博文中有详细讲解。

1 Matlab有用代码提取

下面的代码为Mtalab的原始代码:

function varargout = untitled(varargin)

gui_Singleton = 1;
gui_State = struct('gui_Name',       mfilename, ...
                   'gui_Singleton',  gui_Singleton, ...
                   'gui_OpeningFcn', @untitled_OpeningFcn, ...
                   'gui_OutputFcn',  @untitled_OutputFcn, ...
                   'gui_LayoutFcn',  [] , ...
                   'gui_Callback',   []);
if nargin && ischar(varargin{1})
    gui_State.gui_Callback = str2func(varargin{1});
end

if nargout
    [varargout{1:nargout}] = gui_mainfcn(gui_State, varargin{:});
else
    gui_mainfcn(gui_State, varargin{:});
end

function untitled_OpeningFcn(hObject, eventdata, handles, varargin)

set(handles.Dimension,'String',''); % 属性纬度
set(handles.Numbers,'String',''); % 对象个数
set(handles.Clusters,'String',''); % 训练准确率
set(handles.RecNum,'String',''); % 识别对象

handles.output = hObject;

guidata(hObject, handles);

function varargout = untitled_OutputFcn(hObject, eventdata, handles) 

varargout{1} = handles.output;
% 载入数据
function Load_Callback(hObject, eventdata, handles)
% 数据载入控制
global Myfig;
global Convfig;
global TrFig;
load('Fig.mat');
load('Oliver.mat');
bar = waitbar(0,'读取数据中...');
for i=1:100
    str=['训练中...',num2str(100*i/100),'%'];    % 百分比形式显示处理进程,不需要删掉这行代码就行
    waitbar(i/100,bar,str)                       % 更新进度条bar,配合bar使用
end
close(bar);  
Myfig = gRGB;
Convfig = gRGB1;
TrFig = gRGB2;
% data1 = Data1;
set(handles.Dimension,'String',num2str(size(FigData,2))); % 属性维度
set(handles.Numbers,'String',num2str(size(FigData,1))); % 对象个数
set(handles.Clusters,'String',[num2str(85.50),'%']);

function Exit_Callback(hObject, eventdata, handles)
delete(handles.figure1);
close all;

function Iteration_Callback(hObject, eventdata, handles)

function Dimension_Callback(hObject, eventdata, handles)

function Dimension_CreateFcn(hObject, eventdata, handles)

if ispc && isequal(get(hObject,'BackgroundColor'), get(0,'defaultUicontrolBackgroundColor'))
    set(hObject,'BackgroundColor','white');
end

function Numbers_Callback(hObject, eventdata, handles)

function Numbers_CreateFcn(hObject, eventdata, handles)

if ispc && isequal(get(hObject,'BackgroundColor'), get(0,'defaultUicontrolBackgroundColor'))
    set(hObject,'BackgroundColor','white');
end

function Clusters_Callback(hObject, eventdata, handles)

function Clusters_CreateFcn(hObject, eventdata, handles)

if ispc && isequal(get(hObject,'BackgroundColor'), get(0,'defaultUicontrolBackgroundColor'))
    set(hObject,'BackgroundColor','white');
end
% 展示图片
function Perform_Callback(hObject, eventdata, handles)

cla reset
global Myfig;
global Convfig;
global TrFig;
Cope = get(handles.RecNum,'String');
   if isempty(Cope)
    pause(0.25);
    h=msgbox('请先选择识别对象(1-400)');
    uiwait(h,5);
   else
    TotG=uint8(Convfig{str2num(Cope)}); % 原始模型
    axes(handles.Net);
    imshow(TotG)
    hold on
    axes(handles.ObjFunction);
    TotG1=uint8(Myfig{str2num(Cope)}); % 改进模型
    imshow(TotG1)
    axes(handles.axes4);
    TotG2=uint8(TrFig{str2num(Cope)}); % 待检测模型
    imshow(TotG2)
    hold off
   end

function RecNum_Callback(hObject, eventdata, handles)

function RecNum_CreateFcn(hObject, eventdata, handles)

if ispc && isequal(get(hObject,'BackgroundColor'), get(0,'defaultUicontrolBackgroundColor'))
    set(hObject,'BackgroundColor','white');
end

提取有用的代码并写可以转换的函数后的结果如下:

function [TotG, TotG1, TotG2]=t2(Cope)

oliver = coder.load('Oliver.mat'); 

Myfig = oliver.gRGB;
Convfig = oliver.gRGB1;
TrFig = oliver.gRGB2;

TotG=uint8(Convfig{Cope});
TotG1=uint8(Myfig{Cope});
TotG2=uint8(TrFig{Cope});

2 转换为C++

使用上一篇博文中提到的转换方法转换后,得到如下代码C++代码(由于我用CLion运行C++程序,而CLion使用CMake进行C++编译运行,所以添加了cmake-build-debug文件夹):

C++改写Matlab源码实践一之【显示图片】——————附带详细讲解和示例

Matlab转换的成C++代码是无法直接运行,我们需要添加一些额外的Matlab库文件(例如mat.hmatrix.hmex.htmwtypes.hrtwtypes.h),这些文件是从Matlab的库中得到的,在Mac上的地址为/Applications/MATLAB_R2022a.app/extern/include/):

C++改写Matlab源码实践一之【显示图片】——————附带详细讲解和示例

我们进入example/main.cpp文件中,这里是Matlab转换时自动写好的示例代码。其中最有用的函数部分的代码如下:

static double argInit_real_T()
{
  return 0.0;
}

static void main_dealImage()
{
  static unsigned char TotG[30912];
  static unsigned char TotG1[30912];
  static unsigned char TotG2[30912];
  // Initialize function 'dealImage' input arguments.
  // Call the entry-point 'dealImage'.
  dealImage(argInit_real_T(), TotG, TotG1, TotG2);
}

如果我们使用open cv 中的Mat构造函数接收unchar参数,那么显示出来的图像将只会是丢掉颜色信息的灰度图像,不会有彩色的图像(CV_8UC3:带有3个通道的8位无符号整数矩阵/图像;CV_8UC1:带有1个通道的8位无符号整数矩阵/图像)。

  uchar* data = TotG;
  //matlab中原矩阵为:112行,92列
  cv::Mat src(92, 112, CV_8UC1, data);//rect_height, rect_width
  imshow("image", src);
  cv::waitKey(0);

⚠️注意:这里传入Mat中的行、列信息和Matlab原数据相反的原因是因为,在MATLAB的二位数组(矩阵)的数据存放顺序默认为列优先(从第一列自上向下存放和访问,再第二列…),而c/c++数据存放顺序默认为行优先。

我们使用如下代码打印输入的数据:

  uchar* data = TotG2;
  int row = 112;//行
  int col = 92;//列
  for (int k = 0; k < 3; ++k) {
    for (int i = 0; i < col; ++i) {
      for (int j = 0; j < row; ++j) {
        std::cout<<(double)data[k*col*row + i*row + j]<<" ";
      }std::cout<<std::endl;
    }std::cout<<std::endl<<std::endl;
  }

数据对比:

matlab(1通道):
C++改写Matlab源码实践一之【显示图片】——————附带详细讲解和示例

C++(打印出来的1通道):
C++改写Matlab源码实践一之【显示图片】——————附带详细讲解和示例

matlab(3通道):
C++改写Matlab源码实践一之【显示图片】——————附带详细讲解和示例

C++(打印出来的3通道):
C++改写Matlab源码实践一之【显示图片】——————附带详细讲解和示例

CV_8UC1格式的图片显示:
C++改写Matlab源码实践一之【显示图片】——————附带详细讲解和示例
CV_8UC3格式的图片显示:

C++改写Matlab源码实践一之【显示图片】——————附带详细讲解和示例

出现这样问题的原因是因为:

  • 在opencv中图像的三通道是BGR(历史原因:早期在相机制造商和软件提供商中很流行),而在Matlab中图像的三通道是RGB,也就是调整接收到的三通道数据的顺序,才能使用CV_8UC3格式来显示图片。不然直接使用就会导致图像信息错乱,而使用CV_8UC1格式,虽然规避了图像信息错位的问题,但是丢失了另外两个通道的颜色信息,矩阵的行列信息也是相反,导致只能显示灰度图像。

我们使用如下方法进行三通道数据的处理(行列转置,三通道顺序调整,思路类似于下图):

C++改写Matlab源码实践一之【显示图片】——————附带详细讲解和示例

cv::Mat array2cv(uchar* array){
  //int D = mxGetNumberOfDimensions(pic);		// 维度  mxArray* pic
  int M = 112;// mxGetM(pic);						// 行(高)
  int N = 92;//mxGetN(pic) / D;					// 列(宽)
  uchar* picReal = array;
  cv::Mat cvPic(M, N, CV_8UC3);

  for (int i = 0; i < M; i++)//行
  {
    uchar* pData = cvPic.ptr<uchar>(i);

    for (int j = 0; j < N; j++)//列
    {
      pData[3 * j + 0] = picReal[2 * M * N + j * M + i];
      pData[3 * j + 1] = picReal[1 * M * N + j * M + i];
      pData[3 * j + 2] = picReal[0 * M * N + j * M + i];
    }
  }
  return cvPic;
}

然后使用如下代码进行数据显示:

  cv::Mat src(112, 92, CV_8UC3, data);//rect_height, rect_width
  cv::imshow("image", array2cv(data));
  cv::waitKey(0);

3 使用纯C++实现mat数据中的图片显示

g1.cpp文件内容如下:

#include<opencv2/opencv.hpp>
#include <opencv2/core/core.hpp>        //核心模块
#include <opencv2/highgui/highgui.hpp>  //GUI用户界面
#include <opencv2/imgproc/imgproc.hpp> // 图像处理
//using namespace cv;

#include "mat.h"//Matlab中的库

cv::Mat array2cv(mxArray* pic){
    int D = mxGetNumberOfDimensions(pic);		// 维度  mxArray* pic
    int M = mxGetM(pic);//112						// 行(高)
    int N = mxGetN(pic) / D;//92					// 列(宽)
    uchar* picReal = (uchar*)mxGetPr(pic);;
    cv::Mat cvPic(M, N, CV_8UC3);//rect_height, rect_width

    for (int i = 0; i < M; i++)
    {
        uchar* pData = cvPic.ptr<uchar>(i);

        for (int j = 0; j < N; j++)
        {
            pData[3 * j + 0] = picReal[2 * M * N + j * M + i];
            pData[3 * j + 1] = picReal[1 * M * N + j * M + i];
            pData[3 * j + 2] = picReal[0 * M * N + j * M + i];
        }
    }
    return cvPic;
}


void getMatInfo(const char* path, const char* variableName){
//    const char* path = "/Users/mac/CLionProjects/getMatInfo/Fig.mat";

    MATFile* pmatFile = matOpen(path, "r");

    if (pmatFile == NULL)
    {
        printf("mat数据读取失败!");
        return;
    }

    int ndir;
    const char** dir = (const char**)matGetDir(pmatFile, &ndir);

    mxArray* array = matGetVariable(pmatFile, dir[0]);//得到cell
    matClose(pmatFile);

    if (array == NULL)
    {
        printf("结构体中cell数据不存在!");
        return;
    }

    mxArray* pic1 = mxGetCell(array, 5);//得到图片
    imshow("image", array2cv(pic1));
    cv::waitKey(0);
    
}


int main() {
    const char *path = "/Users/mac/CLionProjects/getMatInfo/Oliver.mat";
    const char* variableName = "Myfig"; //FigData

    getMatInfo(path, variableName);
    
    return 0;
}

编译的CMakeLists.txt文件如下:

cmake_minimum_required(VERSION 3.15)
project(getMatInfo)

set(CMAKE_CXX_STANDARD 11)

include_directories(${PROJECT_SOURCE_DIR})
include_directories(.)

# Matlab的动态链接库
LINK_LIBRARIES("/Applications/MATLAB_R2022a.app/bin/maci64/libeng.dylib"
        "/Applications/MATLAB_R2022a.app/bin/maci64/libmx.dylib"
        "/Applications/MATLAB_R2022a.app/bin/maci64/libmat.dylib"
        "/Applications/MATLAB_R2022a.app/bin/maci64/libmex.dylib")

#找到opencv的包
find_package(OpenCV REQUIRED)
include_directories( ${OpenCV_INCLUDE_DIRS})

# 根据的自己文件路径进行修改
add_executable(getMatInfo
        g1.cpp)
        
#动态链接
target_link_libraries(getMatInfo ${OpenCV_LIBS} )

4 代码封装

这里我们对方法进行封装成动态链接库(供Java或C++调用,主要是供Java调用),让其可以通过传入图片编号和文件存储路径,然后返回保存的图片路径。

头文件dealImage.h为(让编译器以处理 C 语言代码的方式来处理修饰的 C++ 代码。):

#ifndef DEALIMAGE_DEALMATLABDATA_H
#define DEALIMAGE_DEALMATLABDATA_H

#include "rtwtypes.h"
#include <cstddef>
#include <cstdlib>


extern "C"{
void main_dealImage(int index, const char* imgPath, char** saveImgPath);
};

#endif // DEALIMAGE_DEALMATLABDATA_H

源文件dealImage.cpp为(为了可以供Java调用,我们使用JNA来进行C++动态链接库的调用,我们返回的数据结构类型均为C的数据结构):

注意⚠️:
这里为了返回一个Java中的String[]的数据结构,我们C语句中的char**来存储返回的三张图片的保存路径。这里我们在方法中,申请了堆上的空间来初始化char**,如果直接使用传入的行参,而不自己申请空间,在Java调用动态链接库的时候,就会爆Java的垃圾回收异常。

#include "dealMatlabData.h"

#include "dealImage.h"

#include<opencv2/opencv.hpp>
#include <opencv2/core/core.hpp>        //核心模块
#include <opencv2/highgui/highgui.hpp>  //GUI用户界面
#include <opencv2/imgproc/imgproc.hpp> // 图像处理

const int filePathLengthMax =  256;

char convfigStr[filePathLengthMax], myfigStr[filePathLengthMax], trFigStr[filePathLengthMax];

cv::Mat array2cv(uchar*);

cv::Mat array2cv(uchar* array){
  //int D = mxGetNumberOfDimensions(pic);		// 维度  mxArray* pic
  int M = 112;// mxGetM(pic);						// 行(高)
  int N = 92;//mxGetN(pic) / D;					// 列(宽)
  uchar* picReal = array;
  cv::Mat cvPic(M, N, CV_8UC3);

  for (int i = 0; i < M; i++)
  {
    uchar* pData = cvPic.ptr<uchar>(i);

    for (int j = 0; j < N; j++)
    {
      pData[3 * j + 0] = picReal[2 * M * N + j * M + i];
      pData[3 * j + 1] = picReal[1 * M * N + j * M + i];
      pData[3 * j + 2] = picReal[0 * M * N + j * M + i];
    }
  }
  return cvPic;
}


//
// Arguments    : void
// Return Type  : void
//
void main_dealImage(int index, const char* imgPath, char** saveImgPath)
{
  static unsigned char TotG[30912];
  static unsigned char TotG1[30912];
  static unsigned char TotG2[30912];
  //TotG=uint8(Convfig{Cope});% 原始模型
  //TotG1=uint8(Myfig{Cope}); % 改进模型
  //TotG2=uint8(TrFig{Cope}); %待检测模型


  // Initialize function 'dealImage' input arguments.
  // Call the entry-point 'dealImage'.
  dealImage(index, TotG, TotG1, TotG2);


  const char* convfigName = "convfig.jpg";
  const char* myfigName = "myfigfig.jpg";
  const char* trFigName = "trFig.jpg";

  sprintf(convfigStr,"%s%s",imgPath, convfigName);
  sprintf(myfigStr,"%s%s",imgPath, myfigName);
  sprintf(trFigStr,"%s%s",imgPath, trFigName);

  std::string convfigPath = convfigStr, myfigPath =myfigStr, trFigPath = trFigStr;

  cv::imwrite(convfigPath, array2cv(TotG));
  cv::imwrite(myfigPath, array2cv(TotG1));
  cv::imwrite(trFigPath, array2cv(TotG2));

  //char* saveImgPath[3] = {convfigStr, myfigStr, trFigStr};

//  char** saveImgPath  = (char **) malloc(sizeof(char**));
//  saveImgPath[0] = convfigStr;
//  saveImgPath[1] = myfigStr;
//  saveImgPath[2] = trFigStr;

//  char** saveImgPath = new char*[3];
//  char* saveImgPath[3];

  //封装成动态链接库时使用
  saveImgPath = new char *[3];
  for(int i = 0;i < 3;i++){
    saveImgPath[i] = new char[filePathLengthMax];
  }

    saveImgPath[0] = convfigStr;//% 原始模型
    saveImgPath[1] = myfigStr;//% 改进模型
    saveImgPath[2] = trFigStr;// %待检测模型
}

使用cmake动态链接库的方法如下:

(base) mac@macbook dealImage % rm -rf build
(base) mac@macbook dealImage % mkdir build
(base) mac@macbook dealImage % cd build
(base) mac@macbook build % cmake ..
(base) mac@macbook build % make
Scanning dependencies of target dealImage
[ 33%] Building CXX object CMakeFiles/dealImage.dir/dealImage.cpp.o
[ 66%] Building CXX object CMakeFiles/dealImage.dir/dealMatlabData.cpp.o
[100%] Linking CXX shared library libdealImage.dylib
[100%] Built target dealImage

5 福利

matlab原码、c++转换后代码的资源链接,提取码: riig

其他

由opencv的BGR引起的问题,可以让人联想到一些其他有趣的东西:
C++改写Matlab源码实践一之【显示图片】——————附带详细讲解和示例
想学习C++或者Java调用动态链接库的,可以查看本博客的其他博文。