vlambda博客
学习文章列表

跨平台桌面软件开发实践——在基于Qt(C++)框架下的硬件上位机开发中调用OpenCV进行图像分析处理

本次项目的背景是一个自定义大型LED显示屏,在这个LED屏幕上有很多很多像素点,数量大概是五千多个。我们需要编写上位机软件来对下位机进行操作,本质上是通过串口对下位机即LED的主控发送我们要上传的图片中的每个像素点的颜色代码,下位机将发来的每个点的颜色代码映射到其对应的点上显示出来。LED所能显示的色深有限, 每个像素点只能显示 有限个数种类 的颜色。并且像素点众多,对于硬件系统的IO要求较高,这五千多个像素点会被分为一百多个像素单元。对于上位机到下位机的串口通信,以像素单元为基本单位,每一行串口报文包含了单元内所有像素的颜色代码,并规定一定的格式以便下位机判读。
一、安装环境
在一切的开始,我们需要安装我们所需要的软件环境。之前的文章讲过,软件开发具有通性,也就是说我们要使用什么语言某种框架开发某种处理器架构某个操作系统上运行某个应用程序。在此我们要使用C++语言使用Qt框架开发x86平台Windows系统上运行的应用程序,根据项目需要,我们需要引入一些Qt框架的原生库,比如Qt的图形库、串口库、UI组件库等等,以及一些第三方库,比如OpenCV图形库。基于Qt的跨平台特性,同一套桌面项目的代码可能在支持Qt的所有架构的所有系统中运行, 我能够 在搭载 arm架 M1芯片的 macOS上通过Apple Rosetta 2 转译运行 x8 6 台上的Qt 开发工具 进行跨平台开发 并将我 在M ac上完成 的整个项目在 Windows平台上用 Qt 开发工具打开 能够完美 编译运行,这也说明Qt对各个平台的底层支持具有一定程度的优越性。
在macOS环境下 凭借优秀的软件包管理器homebrew 能够完美进行自动化安装,我们也可以 直接进Qt的官网qt.io来寻找Qt在macOS上运行的release版本。在Window上安装Qt套件如果进行c++开发的话,还会自带cmake构建工具、minGW提供编译环境等等。

【在macOS上通过homebrew安装OpenCV】

OpenCV是一个基于BSD许可(开源)发行的跨平台计算机视觉和机器学习软件库,可以运行在Linux、Windows、Android和Mac OS操作系统上。 它轻量级而且高效——由一系列 C 函数和少量 C++ 类构成,同时提供了Python、Ruby、MATLAB等语言的接口,实现了图像处理和计算机视觉方面的很多通用算法。
                                                                                        ——度娘百科

我们已经知道这个东西很牛逼了,那么要怎么安装它呢?

在M1 Mac上通过homebrew来自动化安装OpenCV之后在Qt项目的.pro配置文件中引入安装的OpenCV之后会无法进行编译。原因是homebrew上OpenCV的安装脚本是arm架构的OpenCV,我们知道M1芯片是arm架构这本身没什么问题,但是我们前面有提到,Qt在M1 Mac上只有x86版本,是通过Rosetta 2转译运行的,当然无法过编译。关于一个软件是否能在arm架构的Mac上运行可以上doesitarm.com查询。

解决办法是编译安装OpenCV,先安装cmake

brew install cmake

然后从Github上把OpenCV的源代码clone到电脑上。

git clone https://github.com/opencv/opencv.git

进入源码目录,新建一个文件夹build

mkdir build

进入并构建项目

cd buildcmake \-DCMAKE_SYSTEM_PROCESSOR=x86_64 \-DCMAKE_OSX_ARCHITECTURES=x86_64 \-DCMAKE_BUILD_TYPE=RELEASE\-DCMAKE_INSTALL_PREFIX=/usr/local ..

编译(M1是八核心处理器,j之后跟的是线程数,越大编译越快)

make -j8
跨平台桌面软件开发实践——在基于Qt(C++)框架下的硬件上位机开发中调用OpenCV进行图像分析处理
【8线程编译OpenCV】
跨平台桌面软件开发实践——在基于Qt(C++)框架下的硬件上位机开发中调用OpenCV进行图像分析处理
【实测M1芯片在Java后端和C++项目编译上丝般顺滑,甚至能够吊打i9】

编译完成之后,进行安装

sudo make install
安装完成之后,将安装目录引入qt项目
INCLUDEPATH += /usr/local/include/opencv4 /usr/local/include/opencv4/opencv2
LIBS += /usr/local/lib/libopencv_*

这样我们就可以调用OpenCV图形库了

示例:

#include <opencv2/opencv.hpp>using namespace cv//在主要生命周期函数中调用void main(){Mat img = imread("/Users/wuruihao/Desktop/学习资料.jpg");imshow("91吴先生",img);}

二、图像中的像素三通道值近似

我们前面提到:每个像素点只能显示有限个数种类的颜色,我们将这有限的颜色的RGB值保存在Vec3b三通道向量数组里。然后遍历我们要上传的图片,将遍历的每一个像素的RGB值用Vec3b表示,将其与之前的颜色数组里每一个元素用norm取向量范数,与哪一个元素的范数最小,我们就将该颜色近似于该颜色的色值。

代码实现如下(近似之后的图像预览):

void MainWindow::preview_img(const cv::Mat& mat){ Mat imgzero(cv::Mat::zeros(56, 98, CV_8UC3)); cv::resize(mat,mat,Size(98,56)); for(int unit =0; unit< wall.size(); unit++) { for(int element = 0; element < wall[unit].size(); element++) { for(int colornum=0;colornum<sizeof(color)/sizeof(color[0]);colornum++){ imgzero.at<Vec3b>(wall[unit][element])=colorreload(mat.at<Vec3b>(wall[unit][element])); } }            } namedWindow("imgpreview",0);    imshow("imgpreview",imgzero);}
Vec3b MainWindow::colorreload(Vec3b &a){ double similarity[22]; int min=0; for(int i=0;i<sizeof(color)/sizeof(color[0]);i++){ similarity[i]=norm(color[i], a); if (similarity[i]<similarity[min]){ min=i; } } a=color[min]; return a;

三、图像单元像素级细分

这个程序的本质是将要上传的图片进行编码按照图像单元一条一条发给下位机,图像的大小、单元和像素的位置是根据项目工程图来定的,定下来很容易,这个工程问题最后进代码落地就没有那么容易了。我们前面讲的算法里一个是遍历图像中所有的像素点,这个遍历有两种方法,一种是按照图像的rows、cols(排与列),一种是通过单元——元素的动态二维数组。后面我们发送串口报文时就必须通过遍历单元元素这种二维数组来按照规定拼接串口报文。所以我们必须将工程图中一百多个单元每个单元及其包含的元素坐标全部进入对象。

这是一个浩大的工程,老吴预计怎么也得花一天时间,于是把小秦抓来一起数格子。然后小秦同志“老吴我又想出了一个好idea”,于是趴在我旁边写了一个相同形状单元的拖动函数,于是我俩搞了三个小时就搞完了。

【LED屏幕内的单元排布】

代码实现如下:

Point WALL::trans(Point input, char mark, int distance){ if(mark == 'x'|| mark == 'X') { return input+Point(distance,0); } else if (mark == 'y'|| mark == 'Y') { return input+Point(0,distance); }
}vector<Point2d> WALL::shift(vector<Point2d> input, char mark, int distance){ for(int i =0; i < input.size(); i++) { input[i] = trans(input[i],mark,distance); } return input;}

四、串口报文发送。

这个本质上就是文本拼接,前面有提到那个单元和元素的二维数组,遍历单元内所有元素的颜色代码,与起始位、间隔符、停止位一起拼接,然后换行换下一单元。

 for(int picdiv=0;picdiv<uploadnum;picdiv++){ int num=picdiv+1; QString picnum="PIC"+QString::number(num);; for(int unit =0; unit< wall.size(); unit++) { QString unitnum=picnum+"UNIT"+QString::number(unit); unitnum+='B'; for(int element = 0; element < wall[unit].size(); element++) { unitnum+=senddata[picdiv][unit][element]; unitnum+='/'; }                unitnum+='E'; unitnum+='\n'; mainport.write(unitnum.toLatin1()); ui->sendTextEdit->insertPlainText(unitnum); } }

之前在公司的时候,帮老板写一个试卷系统,老板是做培训机构的,想搞个在线考试。我就把试题全部按条进数据库,然后组卷子把这些条数据按照一定条件select出来,然后两层套娃组JSON进试卷数据库。你没听错,组成的JSON试卷直接就是一个参数值,两层JSON一层是题型,比如选择填空,一层是题号和问题答案之类的。然后试卷的前端显示实现就是上面这种方式,从后端接口拿一大段JSON,一层一层遍历渲染加控件。

【上位机主界面(其实就这一个界面)】

五、项目运行(Windows下)

这个也叫做项目打包,就是我们写的软件最后应该是一个.exe文件。

是这样,我们在Windows上用Qt Creater打开项目,以release方式编译运行一次项目,然后关闭项目。找到项目以release方式build的目录,找到那个.exe文件,单独(没错只有这个exe文件)把它拖到你想拖到的那个位置,比如“D:\学习资料\”。然后打开你的版本的Qt(命令行),输入

windeployqt D:\学习资料\guluguluscreen.exe

然后该工具会自动补全项目所需要的其他库。

这里特别注意,如果开发过程中引入了第三方库,比如把第三方库的dll文件全部复制粘贴进刚才的exe所在的目录下,这样你的exe才能够运行。

最后通过安装包制作工具将这个目录下所有的内容制作成安装程序向导即可。