zoukankan      html  css  js  c++  java
  • 基于面部识别的日志系统的设计与实现

    基于面部识别的日志系统的设计与实现

    @(GUI程序开发)[PyQt, 信号, 面部识别, 多线程, 媒体播放, opencv]


    [TOC]

    需求与设计

    使用面部识别技术,识别进出重要通道的人员,并对人员进出动作进行记录。在人员进出时,在摄像机前采集画面,使用采集到的画面与历史记录对比,如果人员已经存在出入记录,追加一条记录即可;如果不存在则新建记录。

    技术与实现

    核心技术

    使用Python来完成整个程序的编写,面部识别采用python开源库face_recognition, 基于dlib(C++实现)。
    使用opencv来捕获视频流,支持本地摄像头和网络摄像头。
    使用QThread创建线程更新每一帧图片到PyQt界面。

    数据存储

    使用Sqlite3作为单机版数据库,同时支持MySQL数据库。
    使用sqlite3进行数据存储。

    前端界面

    使用PyQt4作为前端界面,使用绝对布局。

    显示监控画面

    opencv捕获摄像机视频流

    Opencv捕获视频流,主要使用opencv的videoCapture方法, 传入摄像头物理地址(0-99)或者网络视频流地址。

    • 安装(Ubuntu): sudo apt install
    • 使用(Python2.7 代码示例):
    import cv2
    #以下的代码片段应当被放在单独的线程中
    #使用videoCapture捕获视频
    #address可以是usb摄像头的地址(0-99)
    #address也可以是视频流的网络地址 如海康摄像机rtsp地址
    address = "rtsp://admin:12345@192.0.0.64:554/h264/ch1/main/av_stream"
    cap = cv2.videoCapture(address)
    #当视频流捕获到以后,开始获取捕获的每一帧
    #拿到的frame实际上是以矩阵的形式存储的,具体的数据结构是numpy的ndarray
    #并转换为PyQt可以显示的QPixmap(图片)
    while cap.isOpened():
        ret, frame = cap.read()
            #拿到frame的信息,并从cv2默认的BGR模式转成RGB模式
            height, width, fps = frame.shape
            bytesPerLine = 3 * width
            cv2.cvtColor(frame, cv2.COLOR_BGR2RGB, frame)
            image = QtGui.QImage(frame.data, width, height,bytesPerLine, QtGui.QImage.Format_RGB888)
            #然后在主线程中更新image,Pyqt4发送信号并传送image到主线程即可
    

    PyQt4子线程控制UI线程更新

    使用场景一:子线程获取视频流的frame,主线程更新frame

    使用QT提供的线程类:QThread

    使用opencv获取视频流之后,应在UI线程中更新获得的每一帧。
    可以自定义一个控件,集成自QLabel,然后自定义信号,这个信号将会在自线程里被触发,在主线程里执行。
    具体实现方法:

    class VideoPlayer(QtGui.QLabel):
        def __init__(self, parent, address):
            QtGui.QLabel.__init__(self, parent, address)
            #这里创建子线程,传递流媒体地址
            self.video_provider = videoThread(address)
            self.video_provider.start()
            #这里自定义一个信号,以便于在子线程里面触发更新帧的方法
            self.connect(self.video_provider, QtCore.SIGNAL('newImage(PyQt_PyObject)'), self.setFrame)
            #这里创建一个当前帧,初始化为None,方便以后截图
            self.current_frame = None
        
        #这个方法将在newImage信号触发之后执行
        def setFrame(self):
            pixmap = QtGui.QPixmap.fromImage(frame)
            self.current_frame = pixmap
            self.setPixmap(pixmap)
    

    这样主线程(UI线程已经做好了所有的准备工作,接下来就是在子线程内获取视频的每一帧,然后发送信号过来)
    创建一个线程类,继承QtCore.QThread

    #其实就是上面opencv的方法放在一个线程内
    class videoThread(QtCore.QThread):
        '''线程类,负责更新视频的每一帧'''
        def __init__(self, address):
            '''初始化的时候传入视频流的地址'''
            super(videoThread, self).__init__()
            self.address = address
    
        def run(self):
            '''重写run函数,读取到视频流之后不断更新frame到主线程'''
            self.cap = cv2.VideoCapture(self.address)
            while self.cap.isOpened():
                _, frame = self.cap.read()
                if frame is not None:
                    #这里进行frame的转换
                    height, width, fps = frame.shape
                    bytesPerLine = 3 * width
                    cv2.cvtColor(frame, cv2.COLOR_BGR2RGB, frame)
                    image = QtGui.QImage(frame.data, width, 
                    height,bytesPerLine, QtGui.QImage.Format_RGB888)
                else:
                    #如果没有拿到frame,则给一个摄像头连接失败的图片,递归,直到摄像头重新连接
                    image = QtGui.QImage("./icons/no_video.png")
                    self.run()
                #在这里发送信号的主线程,主线程会自动执行之前定义的setFrame方法,更新图片
                self.emit(QtCore.SIGNAL('newImage(PyQt_PyObject)'), image)
            #如果摄像头没有获取到,给链接失败的图片,然后递归
            image = QtGui.QImage("./icons/no_video.png")
            self.emit(QtCore.SIGNAL('newImage(PyQt_PyObject)'), image)
            self.run()
    

    这样就可以在前端播放画面了。

    使用场景二:视频的截屏,在子线程里截屏并保存到本地

    使用Python原声的threading.thread创建线程,只想简单工作

    截屏的时候应当在界面显示:准备,3, 2, 1 的提示,这样的提示每一次间隔1秒钟,以便于给用户3秒的调整时间。 这样的工作必须在子线程内执行,否则在主线程内执行会阻断UI线程更新视频流的业务,造成视频卡顿。
    实现方法: 首先创建一个用于显示提示消息的Label,然后在子线程里更新Label的text即可。

    from PtQt4 import QtGui, QtCore
    from thread import threading
    
    class MainWindow(QtGui.QWidget):
        def __init__(self):
            #...
            #...
            self.notify_label = QtGui.QLabel(self)
            self.take_photo_button = QtGui.QPushButton(self)
            self.take_photo_button.setText(u"拍照")
            self.connect(self.take_photo_button, QtCore.SIGNAL("clicked()"),
                self, QtCore.SOLT("takePhoto()"))
        
        #自定义的槽,在class内应加上 装饰器
        @QtCore.pyqtSlot()
        def takePhoto(self):
            thread = threading(target=self.updateNotifyAndTakePhoto)
            thread.start()
        
        #这里是真正进行提示和截图的方法
        def updateNotifyAndTakePhoto(self):
            #先进性一轮循环,显示四个提示,每一次间隔一秒,持续四秒
            for text in [u"准备", '3', '2', '1']:
                self.notify_label.setText(text)
                time.sleep(1)
            #然后设置提示消息为空
            self.notify_label.setText("")
            #开始截图
            img = self.video_player.current_frame
            img.save("./tmp/face.jpg")
    

    PyQt4信号的另一种定义方法

    在子线程内打开新的窗口

    定义信号,初始化的时候绑定,在子线程内调用

    class MainWindow(QtGui.QMainWindow):
        '''程序主窗口'''
        #自定义的信号一定要作为类的成员变量
        record_window_signal = QtCore.pyqtSignal()
        def __init__(self):
            #... other code
            self.record_window_signal.connect(self.showRecordWindow)
            self.history_window_signal.connect(self.showHistoryWindow)
            #这里直接调用启动一个线程
            thread = threading(tearget=self.backgroundWork)
            thread.start()
        
        #一个函数,做后台工作
        def backgroundWork(self):
            #...做一些后台工作,比如人脸识别
            #需要打开新的窗口的时候,触发这个信号即可
            self.record_window_signal.emit()
    
        #自定义的信号被触发时打开一个自定义的Dialog
        def showRecordWindow(self):
            #CSInfoWindow是一个自定义的Didlog
            self.add_dialog = CSInfoWindow()
            self.add_dialog.exec_()
    

    人脸检测和比对

    使用face_recognition检测图片内有没有人脸

    在进行人脸识别的时候,首先要从图片内找到人脸,有时候图片内根本没有人脸,有时候由很多张脸,都需要进行判断

    安装face_recognition模块

    • 安装依赖
      cmake 负责编译dlib sudo apt install cmake
      libboost dlib的依赖 sudo apt install libboost1.61-dev
      dlib 机器学习类库 sudo apt install libdlib-dev
    • 安装模块
      pil python的图像处理模块 pip install pillow
      numpy 机器学习核心模块,无需多言 pip install numpy face_recognition

    使用face_recognition检测照片内的人脸

    主要是一些API的使用,后台的方法都在dlib中实现,python只是调用而已
    上一张效果图:

    检测出来两个人脸

    这张图片直接检测出来,两个人脸!!! 但是,其实我们只需要一张人脸。

    #face_locations 就是对一张照片中人脸的定义,本质上是一个数组
    face_locations = face_recognition.face_locations(image)
    #如果有多张人脸,则不作处理,因为不符合我们的使用场景
    if len(face_locations) > 1:
        print u"检测出了多张人脸,请确保镜头中只有一个人"
    #如果没有任何人脸,也不做处理
    if len(face_locations) == 0:
        print u"没有检测出来人脸,请重新识别"
    #有且只有一张人脸的时候才开始进行人脸的定位
    else:
        #face_locations[0]就是惟一的一张人脸
        top, right, bottom, left = face_locations[0]
        #按照人脸的尺寸,裁剪出来人脸
        face_image = image[top:bottom, left:right]
        pil_image = Image.fromarray(face_image)
        #显示截取出来的人脸
        pil_image.show()
        print u"人脸检测成功"
        filename = FACE_DIR + str(datetime.now()) + ".jpg"
        image = Image.fromarray(image)
        image.save(filename)
    

    使用face_recognition进行人脸的比对

    在人员数据库中存储了很多的人脸,当用户刷脸之后,要对人脸进行比对,确定用户身份。
    人脸比对的API:
    face_recognition.api.compare_faces(known_face_encodings, face_encoding_to_check, tolerance=0.6)

    Compare a list of face encodings against a candidate encoding to see if they match.
    Parameters:
    known_face_encodings – A list of known face encodings
    face_encoding_to_check – A single face encoding to compare against the list
    tolerance– How much distance between faces to consider it a match. Lower is more strict. 0.6 is typical best performance.
    Returns:
    A list of True/False values indicating which known_face_encodings match the face encoding to check

    • 参数1: 需要数据库内的所有人脸数据组成的数组(know_face_encodings
    • 参数2: 需要未知人脸的数据(矩阵): (face_encoding_to_check)
    • 严格程度: tolerance 严格程度从0.1 到1 越来越宽松,数值设置越大约有可能识别错误,数据太小由难以识别到。

    具体实现的代码:

    def recognite(unknow_img):
        #传入一张未知图片的编码,转换成矩阵
        unknow_face_encoding = face_recognition.face_encodings(unknow_img)[0]
        know_faces = []
        know_labels = []
        # 从本地的文件夹内加载所有已经存在的人脸并转为矩阵,放在一个数组内
        for filename in os.listdir(FACE_DIR):
            path = os.path.join(FACE_DIR,filename)
            image = face_recognition.load_image_file(path)
            know_face_encoding = face_recognition.face_encodings(image)[0]
            know_faces.append(know_face_encoding)
            know_labels.append(filename)
        # 进行人脸的比对,这里精准度设置为0.4 具体需要根据摄像头的分辨率 拍摄距离等进行调试
        result = face_recognition.compare_faces(know_faces, unknow_face_encoding, tolerance=0.4)
        # 返回检测的结果级和对应的标签(姓名或者其他唯一性标识)
        return result, know_labels
    

    总结

    接到任务的第一反映

    在刚开始接到这个任务的时候,其实是很震惊的,因为作为机器学期或者人工智能的内行人来讲,我很清楚人脸识别意味着什么。(....意味着算法,预测模型,训练模型,计算机图形学),总之这就是一个经典的课题,要从头实现绝非一个人一年两年就能出成果的。但是好在前人有很多经验!

    最初的计划

    额!最初的想法是自己实现人脸比对的算法, 因为之前使用knn算法随手写数字进行过识别,是一个典型的监督学分类案例,具体的方法就是将图片转换为矩阵,再对矩阵进行归一化,使用欧式距离公式来计算出来n维空间中这些点的距离,然后根据距离它最近的几个点进行概率判断,从而进行分类。但是人脸识别这样做根本不可行,人脸识别中既有回归分析又有分类,还是很棘手的。

    第三方SDK

    经过查阅资料发现,使用第三方SDK还是比较靠谱的,目前主流的由opencv和dlib,这里选择了dlib,因为dlib的面部识别的密度比较大,所以精准度会高一点,但是项目中还是使用了Opencv来进行视频流的获取。

    最终结果

    本来打算在windows环境下使用c#实现,但是考虑到开发效率, 决定还是用python开发比较靠谱。但在此之前并没有python GUI程序的开发经验,所以从头学习了python的QT开发, 开发环境选择在linux下,但是QT跨平台,今后也可在windows平台下部署,不过一想到windows下复杂的软件和类库安装过程,我就呵呵了,决定运行环境依旧在Linux下,避免电脑中毒导致系统崩溃(这个以前有血的教训,所以强烈建议使用Linux来运行重要系统)。
    经过三天两夜的开发, 从零开始实现了人脸识别的程序, 测试之后还比较稳定,关于精准度的问题,这个还是需要根据实际环境来调试,并且也需要在拍照的时候做好用户引导。因为,天知道以后有多少人会被录在数据库内,而且精准度又不能太低,所以必须做好使用人员的培训,确保不会认错人,或者认不出来人。 如果单位的人比较少,大可以慢慢采集原始数据,正脸,侧脸,各种角度表情都可以存在数据库进行比对,但是这个程序要检测的对象是随机的,不是固定的那么几个人,但从目前测试的结果来看,正常使用,距离摄像头2到4米的距离拍照,基本上不会再误报,不遮挡眼睛,不吐舌头,微笑,大笑都可以正确识别。

    
    
  • 相关阅读:
    golang plugin插件的使用
    UE4 是如何渲染每一帧画面的
    虚幻4 Gbuffer
    学习一下虚幻4
    关于 Kapacitor 和 TickScript 的笔记
    SpringOne2020
    TICK Telegraf InfluxDB Chronograf Kapacitor 参考资料
    【转】高考英语常考短语100条
    「面试」拿到B站的意向书
    使用ABAP操作office Word文档
  • 原文地址:https://www.cnblogs.com/wanghuaijun/p/8185355.html
Copyright © 2011-2022 走看看