esp32 cam+esp8266用micropython实现人脸识别开门

Posted qq_33130395

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了esp32 cam+esp8266用micropython实现人脸识别开门相关的知识,希望对你有一定的参考价值。

前言

前面文章讲了编译micropython的编译和图传,这篇记录一下我自己DIY人脸识别门锁的经验。

为什么用esp8266,因为比esp32便宜几块钱,批发甚至只要6块,哈哈

由于micropython我也刚上手,也是学一点记录一点,当帮大家提前踩坑了~

废话不多说,直接上例子!

---------------------------------------------------------------------------------------------------------------------------------

千万不要被标题骗了,光单片机是达不到人脸识别的,性能肯定不够。那么我们需要一个服务器来对人脸进行比对,我用的是自己的一个arm服务器。80多买的n1盒子,刷的centos,1.5G 4线程处理器对于我个人还是够用。如果有同学需要,我也可以单独出一篇我的服务器搭建踩坑经历。当然你也可以用你的pc来当服务器,性能绝对够用。如何使用wsl刷入Ubuntu,我前面也有说,就不废话了。

一、服务器环境搭建

我用的是centos 7,经过实验,Ubuntu的操作差不多。由于是使用centos,gcc版本肯定不够。那么需要升级gcc版本,可以升级到7或9,我就是这个坑浪费了一个多小时。

python3安装(都使用micropython了,肯定都有py3,不废话了)

centos升级:

# yum安装gcc7

$ yum install devtoolset-7-gcc*

# 用gcc7自带的脚本添加到环境变量

$ scl enable devtoolset-7 bash

# 查看gcc版本

$ gcc -v

Ubuntu不用升级gcc,自带最新的

安装boost、cmake、git

centos:

$ yum install -y boost,cmake,git

Ubuntu

$ sudo apt-get install -y git,cmake,libboost-all-dev

编译dlib(我们用的人脸识别依赖这个库)

# 克隆dlib源代码
$ git clone https://github.com/davisking/dlib.git

$ cd dlib
$ mkdir build
$ cd build

#这一部分是使用硬件加速的,如果硬件支持,人脸识别是很快的
$ cmake .. -DDLIB_USE_CUDA=1 -DUSE_AVX_INSTRUCTIONS=1
$ cmake --build .(注意中间有个空格)
$ cd ..

#python安装dlib
$ python3 setup.py install

重点!!!!!

编译dlib库建议空闲内存4G以上,不够可以临时使用swap,不然会编译失败

最后安装face_recognition

# 安装 face_recognition
$ pip3 install face_recognition

至此,我们的人脸识别环境就搭建好了

可以用一下代码测试是否安装成功

# 准备两个文件夹,一个是参照图片,一个是要识别的图片

$ face_recognition ./file1/ ./file2/

能识别出就会显示第一个文件夹内的照片的名字。

二、创建人脸识别接口

我们先使用以下代码生成自己的人脸数组

import face_recognition

# 打开你的图片文件
img = face_recognition.load_image_file('face.jpg')
# 将人脸编码成素组
face_encodings = face_recognition.face_encodings(img)

# 打印出的就是你的人脸数组,复制到下面的代码中,也可以保存到数据库(只需要list,不要将整个tuple都复制)
print(face_encodings)

使用python 的socket库来监听端口,当文件传入的时候进行识别

import socket,threading,os
import face_recognition

# 进入指定目录执行
os.chdir('/root/face/')
# 图片格式
ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif'}

def detect_faces_in_image(file_stream):
    #人脸数组
    face_list = [[[-0.09634063,  0.12095481, -0.00436332, -0.07643753,  0.0080383,
                            0.01902981, -0.07184699, -0.09383309,  0.18518871, -0.09588896,
                            0.23951106,  0.0986533 , -0.22114635, -0.1363683 ,  0.04405268,
                            0.11574756, -0.19899382, -0.09597053, -0.11969153, -0.12277931,
                            0.03416885, -0.00267565,  0.09203379,  0.04713435, -0.12731361,
                           -0.35371891, -0.0503444 , -0.17841317, -0.00310897, -0.09844551,
                           -0.06910533, -0.00503746, -0.18466514, -0.09851682,  0.02903969,
                           -0.02174894,  0.02261871,  0.0032102 ,  0.20312519,  0.02999607,
                           -0.11646006,  0.09432904,  0.02774341,  0.22102901,  0.26725179,
                            0.06896867, -0.00490024, -0.09441824,  0.11115381, -0.22592428,
                            0.06230862,  0.16559327,  0.06232892,  0.03458837,  0.09459756,
                           -0.18777156,  0.00654241,  0.08582542, -0.13578284,  0.0150229 ,
                            0.00670836, -0.08195844, -0.04346499,  0.03347827,  0.20310158,
                            0.09987706, -0.12370517, -0.06683611,  0.12704916, -0.02160804,
                            0.00984683,  0.00766284, -0.18980607, -0.19641446, -0.22800779,
                            0.09010898,  0.39178532,  0.18818057, -0.20875394,  0.03097027,
                           -0.21300618,  0.02532415,  0.07938635,  0.01000703, -0.07719778,
                           -0.12651891, -0.04318593,  0.06219772,  0.09163868,  0.05039065,
                           -0.04922386,  0.21839413, -0.02394437,  0.06173781,  0.0292527 ,
                            0.06160797, -0.15553983, -0.02440624, -0.17509389, -0.0630486 ,
                            0.01428208, -0.03637431,  0.03971229,  0.13983178, -0.23006812,
                            0.04999552,  0.0108454 , -0.03970895,  0.02501768,  0.08157793,
                           -0.03224047, -0.04502571,  0.0556995 , -0.24374914,  0.25514284,
                            0.24795187,  0.04060191,  0.17597422,  0.07966681,  0.01920104,
                           -0.01194376, -0.02300822, -0.17204897, -0.0596558 ,  0.05307484,
                            0.07417042,  0.07126575,  0.00209804],'奥巴马']]

    #加载上传的临时图片
    img = face_recognition.load_image_file(file_stream)
    #获取上传图像中的人脸编码
    unknown_face_encodings = face_recognition.face_encodings(img)
    # 定义识别结果变量
    is_name = 'unknow'
    if len(unknown_face_encodings) > 0:
        #查看上传图像中的第一张面孔是否与已知面孔匹配
        for face_who in face_list:
            match_results = face_recognition.compare_faces([face_who[0]], unknown_face_encodings[0])
            # 识别成功
            if match_results[0]:
                is_name = face_who[1]

    #将结果返回
    return is_name

def deal_data(conn, addr):
    print('新连接 {0}'.format(addr))
    # 二进制jpg数据
    buff = b''
    while 1:
        data = conn.recv(1024)
        # print('{0} 客户端发送数据是 {1}'.format(addr, data.decode()))
        print('收到数据,正在处理...')
        # 由于会分包发送,我们接收的时候要合并数据
        buff += data
        # 我使用endsend来标记文件发送结束
        if data[-7:] == b'endsend':
            # 去掉endsend
            buff = buff.strip(b'endsend')
            # 创建临时文件用来存取收到的图片数据
            tempfile = 'temp.jpg'
            # 保存图片
            with open(tempfile,'wb') as f:
                f.write(buff)
                f.close()
            # 识别人脸图片,只要返回的不是'unknow',就代表识别成功
            ss = detect_faces_in_image(tempfile)
            # 发送识别结果
            conn.send(bytes(ss,'UTF-8'))
            # 删除临时图片文件
            os.remove(tempfile)
            # 关闭连接
            print('{0} 连接关闭'.format(addr))
            conn.close()
            break
        # 关闭连接
        elif not data:
            print('{0} 连接关闭'.format(addr))
            conn.close()
            break
    
    
if __name__ == "__main__":
    # 监听的地址
    ADDR = ('0.0.0.0',10086)
    #socket使用TCP连接
    s = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
    s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    # 绑定套接字
    s.bind(ADDR)
    # 最大连接数
    s.listen(10)
    print('等待连接...')
    while 1:
        # 开始监听端口
        conn, addr = s.accept()
        # 另开线程运行
        t = threading.Thread(target=deal_data, args=(conn, addr))
        t.start()

三、人脸识别和开锁代码

esp32cam 负责拍照和上传到服务器识别,识别成功后将开门指令发送给esp8266开门

为什么要弄两个单片机,不直接用esp32cam开门?

因为我们使用的场景一般esp32cam会放置到室外,如果用esp32cam直接控制电机会有安全问题,所以加了一块8266放置到室内来开门。

esp32cam代码:

import urequests,time,socket,sys

# 开门函数
def socket_client():
    try:
        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        s.connect(('esp8266地址', 10086))
    except socket.error as msg:
        print(msg)
        sys.exit(1)
    # 开门指令为open
    data = b'open'
    # 发送到esp8266
    s.send(data)
    while 1:
        # 阻塞接受数据
        msg1 = str(s.recv(1024),'utf-8')
        print(msg1 )
        # 收到over后关闭连接
        if msg1 == 'over':
            s.close() 
            return True


# 14号引脚中断回调函数
def handle_interrupt(pin):
    global motion
    motion = True
    global interrupt_pin
    interrupt_pin = pin

# 上传函数
def get_face(buf):
    print('打开socket')
    try:
        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        s.connect(('你的服务器地址', 10086))
    except socket.error as msg:
        print(msg)
    # 图片数据    
    data = buf
    # 发送图片
    s.send(data)
    # 发送结束字符串标志
    s.send(b'endsend')
    # 识别结果
    face = str(s.recv(1024),'utf8')
    print('识别结果->',face)
    # 关闭连接
    s.close()
    # 返回识别结果
    return face

# 中断后拍照解锁    
def do_camera(led):
    #拍照动作
    buf = camera.capture()
    try:
        print ("正在识别...")
        # 调用上传代码
        face = get_face(buf)
        # 结果不是'unknow'识别成功
        if face != 'unknow':
            print ('识别成功->',face)
            led.value(0)
            # 开门
            suo = jiesuo.socket_client()
            if suo ==True:
                print('开门成功!')
                return True
            else:
                print('开门失败!')
                return False
    except Exception as e:
        print(e)            

        
#运行   
print('开始工作...')
print('初始化相机配置...')
import camera
camera.init(0, format=camera.JPEG)
#上翻下翻
camera.flip(0)
#左/右
camera.mirror(1)
# 框架
camera.framesize(camera.FRAME_SVGA)
#特效
camera.speffect(camera.EFFECT_NONE)
#白平衡
camera.whitebalance(camera.WB_NONE)
#饱和
camera.saturation(0)
#亮度
camera.brightness(0)
#对比度
camera.contrast(0)
#质量
camera.quality(10)

print('初始化引脚...')
from machine import Pin
from time import sleep
motion = False
# 控制led
led = Pin(4, Pin.OUT)
# 信号引脚,接按钮或pir红外模块
p14 = Pin(14, Pin.IN)

print('正在监控画面移动...')
p14.irq(trigger=Pin.IRQ_RISING, handler=handle_interrupt)

# 识别次数
photoNum = 1

while True:
    # 14号引脚收到电平信号
    if motion:
        if photoNum ==1:
            # 打开单片机自带的闪光灯,esp32cam是4号引脚
            led.value(1)
            print('检测到移动:来自引脚', interrupt_pin ,',上传图片...')
            
        print('开始第{0}次识别'.format(str(photoNum)))
        # 拍照并上传
        can_camera = do_camera(led)
        # 识别成功
        if can_camera:
            motion = False
            photoNum = 1
        # 识别不成功重复执行5次,每次间隔2s,防止拍照模糊识别失败,我们用次数来弥补摄像头的不足
        else:
            photoNum = photoNum + 1
            if photoNum == 6:
                print ('识别失败')
                motion = False
                # 关闭闪光灯
                led.value(0)
                photoNum = 1
            time.sleep(2)

esp8266代码:

import machine,socket,urequests,time

# 操控引脚函数
def de_pin(oc,conn):
    global pin14
    global pin12
    global pin4
    # 收到开门指令
    if oc == 'open':
        # 开门
        pin12.value(0)
        print('pin12 : 1')

        # 引脚4的当前电压(当引脚4发生电压变化,则电机开始转动,开始检测动作是否到位)
        pin4sta = pin4.value()
        print('pin4:',pin4sta)
        # 检测电机是否开始转动
        while 1:
            # 不断检测引脚4的电压
            ss = pin4.value()
            print('pin4:',ss)
            # 引脚4发生电压变化,电机开始转动,停止当前循环
            if ss != pin4sta:
                break
            time.sleep(0.1)

        # 检测开门动作是否到位
        while 1:
            time.sleep(0.1)
            ss = pin4.value()
            print('pin4:',ss)
            # 如果触碰到限位器,则动作到位,电机停止转动
            if ss == 1:
                pin12.value(1)
                print('pin12 : 0')
                break

        # 发送开门完成的指令'isopen'
        conn.send(bytes('isopen','utf8'))
        # 等待10s,你可以推门了
        time.sleep(10)

        # 开始关门
        pin14.value(0)
        print('pin14 : 1')

        # 引脚4的当前电压(当引脚4发生电压变化,则电机开始转动,开始检测动作是否到位)
        pin4sta = pin4.value()
        print('pin4:',pin4sta)

        # 检测电机是否开始转动
        while 1:
            # 不断检测引脚4的电压
            ss = pin4.value()
            print('pin4:',ss)
            # 引脚4发生电压变化,点机开始转动,停止当前循环
            if ss != pin4sta:
                break
            time.sleep(0.1)

        # 检测关门动作是否到位
        while 1:
            time.sleep(0.1)
            ss = pin4.value()
            print('pin4:',ss)
            # 如果触碰到限位器,则动作到位,电机停止转动
            if ss == 1:
                time.sleep(0.5)
                pin14.value(1)
                print('pin14 : 0')
                break
    
# 处理套接字的函数
def deal_data(conn, addr):
    print('新连接:{0}'.format(addr))
    conn.send(bytes('连接成功!','utf8'))
    # 监听
    data = conn.recv(10240)
    # 收到的数据
    data = str(data,'utf-8')
    print(data)
    # 如果收到的为'open'指令,那么开门
    if data == 'open':
        # 开门
        de_pin(data,conn)
        # 完成后通知esp32cam
        conn.send(bytes('over','utf8'))
    # 关闭连接
    conn.close()


# 监听的地址和端口,0.0.0.0为所有的来源
ADDR = ('0.0.0.0',10086)
# 使用tcp
s = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# 绑定套接字
s.bind(ADDR)
s.listen(1)

#开门引脚
pin14 = machine.Pin(14,machine.Pin.OUT)
#关门引脚
pin12 = machine.Pin(12,machine.Pin.OUT)
#初始化,由于我使用的是低电平触发的继电器,所以要设置为高电平
pin14.value(1)
pin12.value(1)
pin4 = machine.Pin(4,machine.Pin.IN)
# led灯,我的单片机是个蓝色的,挺好看,我就打开了,不想要可以注释掉
pin2 = machine.Pin(2,machine.Pin.OUT)

while True:
    print('等待连接...')
    # led灯,我的单片机是个蓝色的,挺好看,我就打开了,不想要可以注释掉
    pin2.value(0)
    # 开始监听
    conn, addr = s.accept()
    try:
        # 处理套接字
        deal_data(conn, addr)
    except:
        pass

附:开门的材料和引脚接线

材料

开门采用两路5v低电平继电器、12v减速电机(30转/分,可以拉40斤,卖家说的,不知道真假)、限位器(自制,门把手转动到位后停止电机转动,防止电机或把手损坏,下附图)、12v电源、12v降5v降压板

引脚:

esp32cam

  • gpio 14 接触发装置,开关或红外

esp8266

  • gipo 14 开门 接继电器
  • gpio 12 关门 接继电器
  • gpio 4 限位器触发引脚 接限位器

继电器:

控制端

vcc接3.3v(这个要看你引脚的电平,因为esp系列引脚都是3.3v,那么继电器的电源一定要接3.3v,不然无法触发)

GND接GND

IN接控制引脚12、14

被控端

将12v电源线正极负极分别一分二分成两跟,共四根线接两路继电器。

常开接口(一般是第一个)接12v正,中间接电机,常闭接口(一般第三个)接12v负(两个继电器的接线一定要一样,比如第一个接线口接正,那么另一个继电器的第一个引脚也必须接正,不然无法控制,要不你就自己改代码。另外常闭和常开必须严格按前面说的接正、接负和做好绝缘,否则漏电烧坏设备和不小心短路起火概不负责)

限位器长下面这样子:

 上下两个接触片为3.3v(高电平),中间把手上的接触针为gpio4。电机收到开门指令后转动,当针离开下接触片的时候,开始监测gpio4 的电压,此时gpio4为低电平。当接触到上接触片的时候,门锁已打开,gpio4为高电平,电机停转。关门同理。

完成后测试:

diy人脸识别开门门锁,使用esp32cam和esp8266单片机

最后:

打算加个扬声器,用于播报识别结果。目前还在啃DAC的文档,先上这些经验为敬~

辛辛苦苦码字不容易,给个评论鼓励下呗~

以上是关于esp32 cam+esp8266用micropython实现人脸识别开门的主要内容,如果未能解决你的问题,请参考以下文章

esp32cam板载led是那个引脚

esp32图传只有10帧

为啥乐鑫esp32-c3管脚不够

esp32cam接错烧了

ESP32 与 ESP32-CAM 的关系

最简单DIY基于ESP32CAM的物联网相机系统①(用网页实现拍照图传)