一、前言
最近在项目需要将C++版本的opencv集成到原本的代码中从而进行一些简单的图像处理。但是在这其中遇到了一些问题,首先就是原本的opencv我们需要在x86的架构上进行编译然后将其集成到我们的项目中,这里我们到底应该将opencv编译为x86架构的还是编译成ARM架构,其次就是,编译完成以后,我们得到的都是.so的二进制文件,我们应该怎么将其链接到我们自己的项目中呢?带着这些问题,所以才有了这一篇教程。所以本次教程就是为了教大家如何将一个开源的项目交叉编译,并且将其链接到自己的项目中,如果你准备好了,就让我们开始吧!
二、谁适合本次教程
因为本次教程已经涉及到Linux C应用开发了所以并不适用小白,所以请具备一定的Linux基础以后,再阅读本次教程。在教程中很多关于Linux的基础操作我并不会细讲,甚至有的简单的步骤我会直接省略。让我们开始吧!
三、动态链接的概念
首先我们来讲讲什么是动态链接,以及什么是动态链接库。首先来讲讲什么是动态链接,动态链接是一种在程序运行时解析外部函数和变量引用的机制。简单来说,我们在编译程序时,并不需要将这部分程序编译到二进制文件中而是在程序运行时进行调用。然后就是动态连接库,动态链接库是包含可被多个程序共享的代码和数据的文件。动态连接库往往被我们编译成二进制文件(.dll或.so)然后在外部通过.h或者其它接口进行调用,这也是目前最主流的编程方式。总的来说动态连接库解决了代码重复依赖的问题,我们将一部分公共的代码编译到一个二进制文件中并且对外开放接口,外部程序通过预留接口访问动态连接库从而实现一套代码被多个程序使用。其次,如果我们的代码涉及机密,可以将其编译成动态链接库并且开放接口给用户使用,用户在可以使用完整函数功能的同时又不能获取源代码。了解了这些,下面就来带大家看看如何交叉编译动态链接库并且将其链接到自己的项目中!
四、动态链接库的编译与链接
这里大家需要注意,我们后面所说的编译都是指交叉编译,也就是说我们会在x86的设备上交叉编译动态链接库,并且将其链接到自己的代码中再将其放到开发板端运行。如果你只想在x86设备上完成此操作其实也可以直接看本次教程,因为不管需不需要交叉编译,道理都是一样的,唯一不一样的就是交叉编译后的程序需要放到开发板端运行。
这里我们首先需要安装交叉编译环境,交叉编译器的安装与环境变量的添加在之前交叉调试的文章中已经讲过了,大家可以直接参考:
vs code交叉调试教程:[Linux]从零开始的vs code交叉调试arm Linux程序教程-CSDN博客
这里我就默认大家已经安装好了Ubuntu并且已经安装好了交叉编译器,需要像这样能够输出版本号,如下图所示:
输入命令以后,能够有上面的输出就表示交叉编译环境没有问题,就可以进行下一步了。
这里我们同样使用图形化中的vs code进行操作,首先我们需要新建一个文件夹,直接使用下面的命令即可:
mkdir Project
然后我们再用vs code打开这个文件夹,这个文件夹后面也会作为我们的工程文件夹:
然后我们新建一个用于编译动态链接库的.c文件,这里我就直接叫“lib.c”了:
下面我们可以将下方的测试代码拷贝到这个.c的文件中:
#include "stdio.h"
void Hello_World()
{
printf("Hello World\n");
}
void Hello_Gcc()
{
printf("Hello Gcc\n");
}
void Hello_Arm()
{
printf("Hello Arm\n");
}
因为我们使用这个.c的文件编译动态链接库,所以并不需要写主函数。又因为是用于测试,所以写的函数非常简单,写好以后,如图所示:
大家将代码写入文件以后,记得保存。这里我们还需要一个.h文件来调用动态链接库中的函数,因为在动态链接库中只包含了函数的定义,没有包含声明,我们需要在.h文件中声明这些函数,这也是为了给外部一个接口供外部调用。这里我直接新建了了一个名为“lib.h”的文件:
我们在.h文件中写入下面的代码声明我们在.c文件中定义的函数:
#ifndef __LIB_H__
#define __LIB_H__
void Hello_World();
void Hello_Gcc();
void Hello_Arm();
#endif
写入以后,如图所示:
这里同样的,写入以后记得保存,然后我们在.c文件中引用这个.h文件:
这里我们修改完以后,我们将.c和.h文件都保存好,然后我们在项目目录下使用下面的命令将我们刚刚的代码编译成动态链接库:
aarch64-linux-gnu-gcc -fPIC -shared -o lib.so lib.c
这里还是来简单解释一下这段代码,首先是“aarch64-linux-gnu-gcc”,这就是我们编译时使用的交叉编译器,这里就不多说了,然后“-fPIC”是为了生成位置无关代码,这是动态库的要求。然后是“-shared
”是为了告诉编译器要将这个文件编译成动态链接库,“-o lib.so”是为了指定生成的二进制文件的名字,这里生成的二进制名字就叫“lib.so”,最后“lib.c”就是我们输入的源文件了。
编译完成以后,就可以看到我们的项目目录下多了一个.so的文件,这个就是我们通过.c文件编译出的动态链接库:
这里我们可以使用“nm”工具来查看这个动态链接库中是否包含了我们写的函数,先使用下面的命令安装一个“nm”的工具库:
sudo apt install binutils
安装完成以后,我们使用下面的命令来检查我们的.so文件,这里lib.so就是我们编译出来的动态链接文件:
nm -D lib.so
输入命令以后,可以看到许多关于这个动态链接库的信息,看不懂没关系,我们只需要找输出的信息中有没有我们刚刚写的函数,这里可以看到,我们写的函数已经成功的编译到动态链接库中了:
这一步一般不会出错,就不多说了。
下面我们来使用一下这个被我们编译出来的动态链接库,这里我们直接在原本的项目文件夹中直接新建一个名为“main.c”的文件:
下面我们在main.c中直接输入下面的代码:
#include "lib.h"
int main()
{
Hello_World();
Hello_Gcc();
Hello_Arm();
}
这里我们只需要引用我们动态链接库的头文件即可。
写入完成以后,如图所示:
然后我们使用下面的命令来编译这个main.c文件:
aarch64-linux-gnu-gcc main.c -o main -L./ lib.so
这里还是来简单解释一下命令,首先就是“aarch64-linux-gnu-gcc”这就不多说了,然后是“main.c”这是我们编译时输入的源文件,同样不多说了,“-o main”表示我们要输出的二进制文件名为“main,”
这里的“-L”表示自己指定.so文件路径,这里我写的“./”表示在当前目录下搜索.so文件,最后“lib.so”表示要链接的库的名称。
比那一完成以后,就可以看到项目目录下多了一个名为“main”的可执行文件:
我们下面再使用“nm”工具来查看一下我们编译出来的可执行文件:
这里可以看到,我们的函数已经被编译到这个可执行文件中了,但是可以看到这些函数的前面都有一个U,这里的U表示“Undefined”,这也证实了这些函数未在我们的可执行文件中定义,需要从别的库链接。
因为这个可执行文件我们是使用交叉编译器编译的,所以肯定是不能在X86的主机上运行的:
下面我们就来测试一下这个可执行文件是否可以正常运行,这里我们需要将可执行文件和动态链接库文件发送到开发板端,这里我直接使用sftp发送,大家可以选择自己熟悉的方式去传输文件:
在开发板端,我们有一个可执行文件和一个动态链接库文件,如下:
这里我们需要指定一个环境变量LD_LIBRARY_PATH,这个环境变量会指定除了在标准路径以外的路径中寻找链接库文件,我们直接使用下面的命令将这个环境变量设置为当前目录,表示在当前目录寻找动态链接库:
export LD_LIBRARY_PATH=./
路径设置完成以后,我们直接运行可执行文件即可:
这里我们可以看到,我们的函数可以正常打印。
如果我们不指定LD_LIBRARY_PATH路径的话,运行可执行文件时就会提示库找不到:
至此,我们的动态链接库已经正常的编译并且正常的链接到了我们的可执行文件中。
五、编译与链接opencv
有了上面的经验以后,我们就可以来实战一下,这里就来教大家如何交叉编译opencv库并且链接到自己的项目中。
这里我们首先使用下面的命令来下载一下opencv的源码:
wget https://github.com/opencv/opencv/archive/refs/tags/4.5.5.tar.gz
如果这里下载卡住的话,就使用下面的命令配置一下代理,大家根据自己的情况自行配置即可:
export http_proxy=http://192.168.112.10:7890
export https_proxy=http://192.168.112.10:7890
拉取到opencv的源码压缩包以后,如图所示:
我们使用下面的命令解压opencv的源码压缩包:
tar -xvf 4.5.5.tar.gz
解压以后得到了下面的文件夹:
我们进入这个文件夹可以看到下面的文件夹:
下面我们准备编译,首先在opencv项目目录下新建一个名为“build”的目录,并且进入:
mkdir build
然后我们在build目录下新建一个名为build的构建文件:
touch build
下面我们在build构建文件中写入下面的构建脚本:
cmake
-DCMAKE_SYSTEM_NAME=Linux \
-DCMAKE_SYSTEM_PROCESSOR=aarch64 \
-DCMAKE_C_COMPILER=aarch64-linux-gnu-gcc \
-DCMAKE_CXX_COMPILER=aarch64-linux-gnu-g++ \
-DCMAKE_INSTALL_PREFIX="/home/chulingxiao/Opencv/install" \
-DBUILD_LIST=core,imgproc,highgui \
-DBUILD_EXAMPLES=OFF \
-DBUILD_TESTS=OFF \
-DWITH_JPEG=ON \
-DWITH_PNG=ON \
..
make -j4
make install
因为我们已经将交叉编译器的可执行文件的路径添加到环境变量了,所以这里直接写交叉编译器的名字即可。写入以后我们保存退出即可。
DCMAKE_INSTALL_PREFIX变量可以设置我们编译后安装的路径。这里大家自己写安装的路径即可。
我们再使用下面的命令给这个构建脚本可执行权限:
chmod +x build
然后我们使用下面的命令安装一下cmake:
sudo apt install cmake
然后我们直接执行这个可执行文件即可:
./build
随后就开始编译了:
我们等待makefile生成完成即可。生成完成makefile以后编译就开始了:
编译完成以后,可以看到我们的文件被安装到了如下目录:
我们打开安装的目录,可以看到以下文件夹:
这里的“include”文件夹里面放了所有的头文件:
在lib目录下放了所有的动态链接库文件:
下面来教大家如何将我们编译出来的内容链接到我们自己的项目中,这里我们首先回到项目文件夹中,然后将下面的内容写入main.c中用于测试我们opencv的功能,这是一个使用opencv将图片二值化的程序,可以将传入的图片二值化:
#include <opencv2/opencv.hpp>
#include <iostream>
int main()
{
cv::Mat image = cv::imread("test.jpg", cv::IMREAD_GRAYSCALE);
if (image.empty()) {
std::cerr << "无法读取图片!" << std::endl;
return -1;
}
// 二值化处理
cv::Mat binary_image;
double thresh_value = 128; // 设定阈值
cv::threshold(image, binary_image, thresh_value, 255, cv::THRESH_BINARY);
// 保存结果
cv::imwrite("test_output.jpg", binary_image);
std::cout << "二值化完成,结果保存在 test_output.jpg" << std::endl;
}
这里因为头文件以及库的引用比较复杂,我们写一个makefile来帮助我们编译文件,在项目目录下新建一个makefile文件,将下面的内容复制到文件中:
# 编译器设置
CXX = aarch64-linux-gnu-g++
CXXFLAGS = -std=c++11
# OpenCV 路径(改成你的安装路径)
OPENCV_PATH = /home/chulingxiao/Opencv/install
OPENCV_INC = /home/chulingxiao/Opencv/install/include/opencv4/
OPENCV_LIB = /home/chulingxiao/Opencv/install/lib
# 程序名称
TARGET = main
SRC = main.c
OPENCV_LIBS = -lopencv_core -lopencv_imgcodecs -lopencv_imgproc
$(TARGET): $(SRC)
$(CXX) $(CXXFLAGS) -I$(OPENCV_INC) $< -o $@ -L$(OPENCV_LIB) $(OPENCV_LIBS)
复制以后如图所示:
这里大家只需要修改几个地方即可,首先就是OPENCV_PATH,这里大家将路径改为我们一开始构建opencv时DCMAKE_INSTALL_PREFIX变量的路径,也就是一开始设置的opencv的安装路径。
然后OPENCV_INC 路径大家写到opencv安装路径下的头文件路径,这里可以参考我写的。
最后OPENCV_LIB 路径大家写到opencv安装路径下的动态链接库路径,这里同样参考我写的。
修改完以上内容以后,就没有什么需要改了,我们直接在项目目录下输入“make”即可开始编译:
这里没有输出别的错误并且生成可执行文件就表示编译没有问题。
下面我们将这个可执行文件通过sftp传输到开发板端:
然后我们在开发板端运行这个可执行文件,发现缺少库:
我们在opencv安装目录中,将这个库拷贝到开发板:
然后再在开发板端指定一下寻找库的路径:
export LD_LIBRARY_PATH=./
然后再次执行可执行文件,发现还缺少了一个名为“libopencv_imgcodecs.so.405”的库:
我们再次使用sftp传输这个库到开发板:
我们再次运行可执行文件,发现还缺少了一个名为“libopencv_imgproc.so.405”的库:
我们再次使用sftp将这个库传输到开发板:
我们再次运行可执行文件,发现,可执行文件已经不提示找不到库了,提示的是找不到文件:
大家还记得我们的程序是做什么的吗?是的,这是一个将图像二值化的程序,要求我们传入一个图像,然而我们的目录下没有图像,这里我们传输一张图片到当前目录下并且将名字改为“test.jpg”这也和我们程序中的名称一样:
我们再次执行可执行文件,可以看到,图像已经正常被处理了,并且输出为了“test_output.jpg”:
我们将其传输到可视化界面中,可以看到图像被正常二值化:
这也证明了我们的opencv在正常工作,表示我们的交叉编译以及so文件的链接都是成功的。
六、结语
尽管我们在这个过程中遇到了很多问题,但我教给大家的是解决问题的方法,这些方法也包括了如果我们在运行可执行文件缺少库我们应该怎么办编译时怎样链接库不会出错。当然,做完上面的步骤,相信大家对嵌入式Linux开发多少有一定的了解了,但这也只是学习嵌入式Linux开发的一个开始。那么最后,感谢大家的观看!