前两篇已经实现了基于QT的人脸识别考勤打卡系统 ,当时我把这个系统分为用户考勤打卡和管理员管理员系统,并且都在window上运行,但是如果要运用到实际当中,还是存在很多问题,虽然我们自己做的系统几乎是很难运用在实际当中,但是我们做的系统要努力贴近实际。
基于QT的人脸识别考勤管理系统【一】 https://blog.csdn.net/qq_42449351/article/details/99716413
基于QT的人脸识别考勤管理系统【二】 https://blog.csdn.net/qq_42449351/article/details/99753675
前面实现的系统,最大的问题就是用户考勤打卡这一块,当用户打卡时,必须打开电脑运行用户考勤打卡系统,很明显这样是不可取的,所以我想着将用户考勤系统移植到Arm gec6818板上,这样打卡只需要在门前安装一个arn板就可以。
一、移植主要的问题
1、在Arm板上无法用opencv打开摄像头 (只是个人运行会这样)。所以我采用的是专门为 linux 设备设计的一套视频框架V4L2。
2、需要将pro文件的opencv路径改成linux下opencv的路径。
3、Arm板的处理速度肯定没有电脑快,所以要减少运算量。
二、移植前期准备
1、交叉编译QT程序
如果在linux下没有通过交叉编译让QT程序生成可执行文件,那么一切都是空谈。下面我就跟大家分享如何交叉编译QT程序。 首先准备好使用到的工具
1、ubuntu16.04 --64位
2、arm-linux-gnueabi-5.4.0.tar.xz --交叉编译工具
3、Qt-Embedded-5.7.0.tar.xz ---arm版本Qt
一、安装交叉编译的工具的步骤:
1、将arm-linux-gnueabi-5.4.0.tar.xz 拷贝到linux系统 /usr/local/arm目录下(如果没有这个目录就创建)解压
2、解压命令为:sudo tar -xf arm-linux-gnueabi-5.4.0.tar.xz ,解压成功后会得到arm-linux-gnueabi-5.4.0.tar.xz , usr
3、为了方便操作把usr下面的路径移到当前目录 执行:sudo mv usr/local/arm/5.4.0 ./
4、设置环境变量,打开 ~/.bashrc 在最后面添加 export PATH=/usr/local/arm/5.4.0/usr/bin:$PATH
二、安装arm版本Qt库
1、 把Qt-Embedded-5.7.0.tar.xz拷贝到/usr/local目录下(必须是这个目录)解压,这个过程解压完成即可
三、交叉编译Qt程序
1、 进入到qt程序工程目录下
2、 执行/usr/local/Qt-Embedded-5.7.0/bin/qmake --生makefile文件
3、 make ---编译 把生成的可执行文件下载到arm开发板运行
如果ARM板能运行该可执行文件,那么恭喜你成功交叉编译QT程序
2、将运行时需要使用的文件拷贝到Arm板
1、opencv官方训练好的人脸检测分类器(haarcascade_frontalface_alt2.xml)。
2、自己训练成功的人脸识别文件(MyFaceFisherModel.xml)。
3、 数据库文件(stuface.db)
三、源代码改写
配置文件 (将window的opencv的路径改成linux的路径)
INCLUDEPATH +=/opt/opencv/include
INCLUDEPATH +=/opt/opencv/include/opencv
INCLUDEPATH +=/opt/opencv/include/opencv2
LIBS += -L/opt/opencv/lib/ -lopencv_core -lopencv_highgui -lopencv_imgcodecs \
-lopencv_calib3d -lopencv_highgui -lopencv_shape\
-lopencv_core -lopencv_imgcodecs -lopencv_stitching\
-lopencv_dnn -lopencv_imgproc -lopencv_superres\
-lopencv_face -lopencv_ml -lopencv_videoio\
-lopencv_features2d -lopencv_objdetect -lopencv_video\
-lopencv_flann -lopencv_photo -lopencv_videostab\
1、V4L2打开摄像头和采集图片的程序
将V4L2的程序设计为一个线程类,通过信号将Mat型的图片传给主线程,V4L2采集图片的步骤有:1、打开设备 2、配置设备(采集的频率,图像宽高, 图像格式(jpeg, yuv--422,420))3、在内核空间申请缓冲区队列(2-4个)4、把申请好的缓冲区对象一一映射到用户空间 5、开始采集
1、打开设备(摄像头)
//1.打开设备
this->vfd = ::open(deviceName.c_str(), O_RDWR);
if(this->vfd < 0)
{
perror("open fail");
VideoException vexp("open fail");//创建异常对象
//抛异常
throw vexp;
}
2、配置设备
//2.配置采集属性
struct v4l2_format vfmt;
vfmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; //
vfmt.fmt.pix.width = WIDTH;
vfmt.fmt.pix.height = HEIGHT;
vfmt.fmt.pix.pixelformat = V4L2_PIX_FMT_YUYV;//(设置视频输出格式,但是要摄像头支持4:2:2)
//通过ioctl把属性写入设备
int ret = ioctl(this->vfd, VIDIOC_S_FMT, &vfmt);
if(ret < 0)
{
VideoException vexp("set fail");//创建异常对象
throw vexp;
}
3、申请缓冲区队列
//1申请缓冲区队列
struct v4l2_requestbuffers reqbuffer;
reqbuffer.count = this->count;//申请缓冲区队列长度
reqbuffer.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
reqbuffer.memory = V4L2_MEMORY_MMAP;
int ret = ioctl(this->vfd, VIDIOC_REQBUFS, &reqbuffer);
if(ret < 0)
{
VideoException vexp("req buffer fail");//创建异常对象
throw vexp;
}
4、申请好的缓冲区对象一一映射到用户空间
for(int i=0; i<this->count; i++)
{
struct VideoFrame frame;
struct v4l2_buffer mapbuffer;
mapbuffer.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
mapbuffer.index = i;
mapbuffer.memory = V4L2_MEMORY_MMAP;
//从队列中拿到内核空间
ret = ioctl(this->vfd, VIDIOC_QUERYBUF, &mapbuffer);
if(ret < 0)
{
perror("query fail");
}
//映射
frame.length = mapbuffer.length;
frame.start = (char *)mmap(NULL, mapbuffer.length, PROT_READ|PROT_WRITE, MAP_SHARED, this->vfd, mapbuffer.m.offset);
//空间放回队列中(内核空间)
ret = ioctl(this->vfd, VIDIOC_QBUF, &mapbuffer);
//把frame添加到容器framebuffers
framebuffers.push_back(frame);
}
5、开始采集图片
void V4l2Api::grapImage(char *imageBuffer, int *length)
{
//select (rfds, wfds, efds, time)
struct v4l2_buffer readbuf;
readbuf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
readbuf.memory = V4L2_MEMORY_MMAP;
//perror("read");
if(ioctl(this->vfd, VIDIOC_DQBUF, &readbuf)<0)//取一针数据
{
perror("read image fail");
}
//printf("%ld\n", readbuf.length);
*length = readbuf.length;
memcpy(imageBuffer,framebuffers[readbuf.index].start, framebuffers[readbuf.index].length);
//把用完的队列空间放回队列中重复使用
if(ioctl(vfd, VIDIOC_QBUF, &readbuf)<0)
{
perror("destroy fail");
exit(1);
}
}
现在V4L2的采集工作已做完,现在我们在这个类的run函数里一直采集图片,并通过opencv将采集的数据变成RGB型,每采集一次,就通过信号发送给主线程
void V4l2Api::run()
{
Mat yuvImage(HEIGHT,WIDTH,CV_8UC2);
uchar *buffer = yuvImage.data;
int len;
while(1)
{
grapImage((char *)buffer, &len); //采集图片
//把yuv转rgb
Mat rgbImage;
cvtColor(yuvImage, rgbImage, CV_YUV2RGB_YUYV);
emit sendImage(rgbImage);
msleep(50);
}
2、人脸识别的程序
以前的程序,是通过opencv进行打开摄像头,现在只需要改成用V4L2线程类打开即可,创建V4L2线程类对象,当点击按钮时,启动线程,这样就会去执行run函数,再将人脸识别的函数与线程类的发送信号相连接,如果有信号来,就会调用人脸识别函数
1、启动线程,绑定信号
//在构造函数绑定信号
connect(&vapi, &V4l2Api::sendImage, this, &ArmFace::recvMat);
void ArmFace::on_openbt_clicked()
{
/* if(capture.open(1)<0) // 以前的写法
{
qDebug()<<"open fail";
return ;
}*/
//训练好的文件名称,放置在可执行文件同目录下
cascade.load("haarcascade_frontalface_alt2.xml");
model = face::FisherFaceRecognizer::create();
//1.加载训练好的分类器
model->read("MyFaceFisherModel.xml");
ui->openbt->setStyleSheet("border-image:url(:/open.png)");
vapi.start(); //开启线程
//mtimer.start(20);
}
2、人脸识别函数
因为要考虑Arm板的处理速度,所以这里将接收到的图片变小,并且在进行人脸检测的时候,改变检测次数,以减少运算量
void ArmFace::recvMat(Mat cap)
{
//capture>>cap;
std::vector<Rect> faces(0);
cv::resize(cap, cap, Size(460, 340)); /*原图片为640*480 变成460*340 */
cvtColor(cap, gray, CV_RGB2GRAY);
equalizeHist(gray, gray);
//检测人脸
cascade.detectMultiScale(gray, faces, /*减少匹配次数*/
1.1, 3,
//|CV_HAAR_FIND_BIGGEST_OBJECT, /*只检测最大的物体*/
CV_HAAR_DO_ROUGH_SEARCH, /*初略的检测*/
//CV_HAAR_SCALE_IMAGE, /*正常比例检测*/
Size(200, 200));
Mat* pImage_roi = new Mat[faces.size()];
Mat face;
.......
下面的操作不变
}
至此我们的代码就已经改好了,现在只需要将程序进行交叉编译,再将可执行文件下载到Arm板运行即可,效果如下
总结:这次移植虽然能将用户打卡系统运行,但是还是存在着很问题。还记得当时我是把数据库文件拷贝到开发板里,然后用户打卡系统直接操作Arm板的数据库文件,然而我的管理员系统在window上运行。所以这两个系统的数据是不能相通的。
解决方案:将数据库放到服务器上,在服务器上创建一个用C++写的服务端,用户打卡系统和管理员管理系统作为客户端,服务端的功能:接收客户端发送的命令,并通过接收到的命令去执行相应的操作(操作数据库),再将操作的结果返回给客户端,这样两个系统的数据就相通了。这样做的话,程序要改很多,现在没时间,以后有时间再进行完善。