前言:本文为手把手教学嵌入式经典项目——智能门禁项目,本次项目采用 树莓派4B 与 STM32F103C8T6 进行联合开发。项目充分发挥各自 CPU 的优势与长处,将人脸识别的大计算量任务给树莓派4B,将门禁系统的控制部分交给 STM32 进行处理。该项目算是嵌入式人工智能(Embedded Artificial Intelligence)的入门项目,该代码框架可以在 STM32 部分可以根据自己的需求增加:密码锁,指纹锁等进行丰富。希望这篇博客能帮助大家助力自己的毕设亦或是项目!(文末有代码开源!)
硬件实物图:
效果图:
一、项目概述
1.1 智能门禁系统
智能门禁系统是一种现代化的安全管理系统,采用微机自动识别技术和现代安全管理措施,实现对各种门禁设备的集中管理和安全控制。该系统可以管理各种门禁设备,如密码门禁系统、非接触卡门禁系统等,并可以提供事后的查询报表。智能门禁系统广泛应用于各种场所,如住宅小区、办公室、实验室、仓库、银行等,旨在提高安全性和管理效率。该系统不仅可以保障人们的安全和财产安全,还可以提高场所的管理水平和效率。总之,智能门禁系统是一种高效、安全、灵活的安全管理系统,在现代化社会中扮演着越来越重要的角色。
市面上智能门禁系统分为 2 大类,分别是:(1)本地部署;(2)服务商运营
(1)本地部署:顾名思义,智能门禁系统的所有识别与判断都在本地机器上完成。优势:综合成本较低;劣势:性能过于依赖本地机器的性能
(2)服务商运营:该方案则是借助网络技术将本地检测到的目标数据发送至云端,由云端进行识别判断,再将结果返回至本地机器。优势:运营维护方便,整体性能很强;劣势:需要长期缴纳服务费用
1.2 项目概述
本项目的智能门禁系统目前仅为大家提供人脸识别判断开门的功能,至于密码锁,指纹锁等部分功能的实现就交给感兴趣的读者朋友了(这部分仅需要在下位机STM32进行改动即可)。
本项目通过树莓派4B的摄像头进行视频读取,使用传统的人脸识别技术(LBP人脸特征进行识别),将识别到的人脸数据结果通过树莓派4B的 UART 串口发送至 STM32。STM32 则通过 I2C 协议将树莓派4B发送过来的数据解码后显示在 OLED 屏幕上,并根据实际条件判断是否驱动 SG90 舵机进行开门操作!
二、人脸识别
2.1 人脸识别概述
人脸识别指识别并理解一张脸。人脸识别是一项热门的计算机技术研究领域, 它属于生物特征识别技术,是对生物体(一般特指人)本身的生物特征来区分生物体个体。人脸识别技术在安防监控、身份认证等众多领域都有着重要的作用。
人脸识别技术可以分为以下几类:
1、3D人脸识别:这种技术主要处理3D数据,利用了人体的立体特征。然而,这种技术的开发难度较大。
2、2D人脸识别:这种技术以2D图像为基础,虽然数据模糊,但可以使用图片和视频进行破解,因此存在一定的安全隐患。
3、2D+人脸识别:这种技术处理方式相对简单,只需要将3D数据分为RGB和深度数据。这种方法的实现速度较快。
4、基于几何特征的方法:这种方法利用人脸的各个部件(如眼睛、鼻子、嘴巴、下巴等)的形状、大小和结构关系作为识别的特征。
5、卷积神经网络(CNN):这是一种深度学习算法,通过学习直接对图像进行分类。CNN通常是一个包含多个卷积层和池化层的神经网络。
6、局部特征分析方法(Local Face Analysis):这种方法通过寻找具有局部性和拓扑性的表达方式,对模式分析和分割非常有效。★本项目采用的就是比较传统的局部特征分析的方式,借助的特征模型为BLP人脸特征。
2.2 BLP人脸特征
BLP(基于局部二值模式)是一种图像局部特征描述符,可用于人脸识别。它属于图像处理和计算机视觉领域。简单来说,LBP 是一种在纹理识别以及人脸识别中经常使用的特征,其提取过程如图所示。
首先将输入图像灰度化之后,对于每一个像素点,在 3 × 3 的窗口内,比较其与周围 8 个像 素点的大小。根据大小比较情况对周围的 8 个像素点进行二值化编码,比中心点小的编码为 0,比中心点大的编码为 1。这样中心点的像素就可以用一个 8 位 2 进制数来进行描述。
对图像中的每一个点都进行上述操作后,就可以将一张灰度图重新编码为一张 LBP 特征图。
将这个特征图分成小块(通常是 8 × 8 的小块),计算每个小块中特征点的统计直方图,然 后将所有小块的直方图拼接起来,就可以形成一个能够描述输入人脸图像的特征向量。
特征维度:8*8*256 = 16384
作者概述:BLP 方法就是将人脸图像通过BLP法则转换为BLP专属的特征向量,然后将待识别的人脸特征向量与已知的人脸特征向量进行匹配(BLP类型的),计算它们之间的距离或相似度。根据匹配结果进行识别决策,如设定阈值判断是否为同一人脸等。
三、树莓派4B的人脸识别
作者将通过BLP方法实现人脸识别技术,读者朋友可以结合着下方 BLP 方法的人脸识别技术代码进行学习!
3.1 人脸收集
智能门禁系统的第一步肯定是需要对可以通行用户的人脸数据进行采集,为了方便起见,作者直接采用使用以下 Python 代码进行收集人脸信息;
import cv2
import os
if __name__ == "__main__":
str_face_id = ""
index_photo=0
# 加载训练好的人脸检测器
faceCascade = cv2.CascadeClassifier('haarcascade_frontalface_alt.xml')
# 打开摄像头
cap = cv2.VideoCapture(0)
while True:
# 判断人脸id 是否为空,空的话创建face_id
if str_face_id.strip()=="":
str_face_id = input('Enter your face ID:')
index_photo=0
if not os.path.exists(str_face_id):
os.makedirs(str_face_id)
# 读取一帧图像
success, img = cap.read()
if not success:
continue
# 转换为灰度
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 进行人脸检测
faces = faceCascade.detectMultiScale(gray,scaleFactor=1.1,minNeighbors=5,minSize=(50, 50),flags=cv2.CASCADE_SCALE_IMAGE)
# 画框
for (x, y, w, h) in faces:
cv2.rectangle(img, (x, y), (x+w, y+h), (255, 0, 0), 3)
# 显示检测结果
cv2.imshow("FACE",img)
# 读取按键键值
key = cv2.waitKey(1) & 0xFF
# 按键"c" 进行人脸采集
if key == ord('c'):
# 保存人脸
for (x, y, w, h) in faces:
roi = img[y:y+h,x:x+w]
cv2.imwrite("%s/%d.jpg"%(str_face_id,index_photo),roi)
index_photo = index_photo+1
key = 0
# 按键"x" 切换 人脸_id
elif key == ord('x'):
str_face_id = ""
key = 0
# 按键 "q" 退出
elif key == ord('q'):
break
cap.release()
上述代码主要借助 OpneCV 自带的人脸检测器 haarcascade_frontalface_alt.xml 进行检测视频流中出现的人脸信息!
在终端输入:python3 face_collect.py
随后在 Enter your face ID: 之后输入你即将收集人脸ID(字母格式),输入完毕回车后将成功打开摄像头,进行人脸收集。通过按 “c” 按键可以进行保存图片;
通过按键 “x” 可以切换ID进行收集下一个目标人脸的有效信息,按 Ctrl+c 即可退出收集程序!
如上图所示为作者本人收集到的刘德华先生与李连杰先生的人脸数据信息
3.2 构建人脸特征库
拥有收集好的人脸数据信息接下来我们就可以进行制作基于 BLP 的人脸特征库!
通过已有的人脸数据集库制作基于 BLP 的人脸特征库的 Python 代码如下:
在终端输入代码:python3 train_model_LBP.py
import cv2
import os
import numpy as np
# 获取所有文件(人脸id)
def get_face_list(path):
for root,dirs,files in os.walk(path):
if root == path:
return dirs
if __name__ == "__main__":
# 创建人脸识别器
recognizer = cv2.face.LBPHFaceRecognizer_create()
# 用来存放人脸id的字典
# 构建人脸编号 和 人脸id 的关系
dic_face = {}
# 人脸存储路径
base_path = "../face-collect/"
# 获取人脸id
face_ids = get_face_list(base_path)
print(face_ids)
# 用来存放人脸数据与id号的列表
faceSamples=[]
ids = []
# 遍历人脸id命名的文件夹
for i, face_id in enumerate(face_ids):
# 人脸字典更新
dic_face[i] = face_id
# 获取人脸图片存放路径
path_img_face = os.path.join(base_path,face_id)
for face_img in os.listdir(path_img_face):
# 读取以.jpg为后缀的文件
if face_img.endswith(".jpg"):
file_face_img = os.path.join(path_img_face,face_img)
# 读取图像并转换为灰度图
img = cv2.imread(file_face_img)
img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 保存图像和人脸ID
faceSamples.append(img)
ids.append(i)
print(dic_face)
# 进行模型训练
recognizer.train(faceSamples, np.array(ids))
# 模型保存
recognizer.save('trainer.yml')
# 进行字典保存
with open("face_list.txt",'w') as f:
for face_id in dic_face:
f.write("%d %s\n"%(face_id,dic_face[face_id]))
代码运行成功之后将在当前文件夹下生成 trainer.yml 的文件,该文件存放了人脸库数据集转换成的 LBP 人脸特征!
3.3 人脸识别
通过上述步骤,我们成功得到了收集到人脸数据的 LBP 人脸特征,接下来我们就可以通过摄像头采集到的人脸 LBP 特征向量与数据库中存在的人脸 LBP 特征向量进行对比,计算距离得分来判断目标是否为可进入人员!
我们需要先制作目标人脸ID的字典,在 face_list.txt 文件中进行如下标注:
在终端输入代码:python3 face_recognize_LBP.py
face_recognize_LBP.py 代码:
import cv2
import os
import numpy as np
import serial
ser = serial.Serial('/dev/ttyAMA0',115200)
def read_dic_face(file_list):
data = np.loadtxt(file_list,dtype='str')
dic_face = {}
for i in range(len(data)):
dic_face[int(data[i][0])] = data[i][1]
return dic_face
if __name__ == "__main__":
# 加载人脸字典
dic_face = read_dic_face("face_list.txt")
print(dic_face)
# 加载Opencv人脸检测器
faceCascade = cv2.CascadeClassifier('../face-collect/haarcascade_frontalface_alt.xml')
# 加载训练好的人脸识别器
recognizer = cv2.face.LBPHFaceRecognizer_create()
recognizer.read('trainer.yml')
# 打开摄像头
cap = cv2.VideoCapture(0)
while True:
# 读取一帧图像
success, img = cap.read()
if not success:
continue
# 转换为灰度
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 进行人脸检测
faces = faceCascade.detectMultiScale(gray,scaleFactor=1.1,minNeighbors=5,minSize=(50, 50),flags=cv2.CASCADE_SCALE_IMAGE)
ser.write(str("idea").encode())
# 遍历检测到的人脸
for (x, y, w, h) in faces:
# 画框
cv2.rectangle(img, (x, y), (x+w, y+h), (255, 0, 0), 3)
# 进行人脸识别
id_face, confidence = recognizer.predict(gray[y:y+h,x:x+w])
print(confidence)
# 检测可信度,这里是通过计算距离来计算可信度,confidence越小说明越近似
if (confidence < 100):
str_face = dic_face[id_face]
str_confidence = " %.2f"%(confidence)
else:
str_face = "unknown"
id_face = 2
str_confidence = " %.2f"%(confidence)
ser.write(str(id_face).encode())
# 检测结果文字输出
cv2.putText(img, str_face+str_confidence, (x+5,y-5), cv2.FONT_HERSHEY_SIMPLEX, 1, (0,0,255), 2)
# 显示检测结果
cv2.imshow("FACE",img)
# 按键 "q" 退出
if cv2.waitKey(1) & 0xFF == ord('q'):
break
cap.release()
上述代码通过 OpenCV 的人脸检测器在视频流截取到人脸信息,随后进行人脸识别(与已存的人脸数据特征进行可信度计算,距离越小说明两者越相似)
★上述代码涉及到树莓派4B与STM32之间的UART串口通信,这方面知识薄弱或者不太懂的朋友可以借鉴作者这篇博客:http://t.csdn.cn/mcMra
上述代码中主要用2个串口通信信息进行发送,分别为:ser.write(str("idea").encode()) 和 ser.write(str(id_face).encode())。
其中,ser.write(str("idea").encode()) 代码是在视频中未检测到人脸,即发送 “idea” 字符串给STM32(这部分很重要,否则STM32可能会出现延迟反复检测人脸的现象)。
ser.write(str(id_face).encode()) 则是正常根据检测到的人脸ID数字进行发送数据,STM32那边正常解码即可!
四、树莓派4B与STM32的智能门禁
4.1 CubeMX配置
1、RCC配置外部高速晶振(精度更高)——HSE;
2、SYS配置:Debug设置成Serial Wire(否则可能导致芯片自锁);
3、TIM2配置:使用TIM2的Channel1产生PWM信号(控制SG90);
数据参数意义:
此时产生PWM波形频率:72M / (719 +1)/ (1999+1) = 50Hz
定时器周期:1/50 = 20ms
4、USART1配置:设置UART1串口;波特率:115200;开启UART串口中断;
5、I2C配置:
6、时钟树配置
7、工程配置
4.2 STM32代码
4.2.1 OLED代码
OLED部分的详解可以参考博客:http://t.csdn.cn/GCWyc
OLED模块主要是方便显示树莓派4B发送给STM32的数据信息!根据树莓派4B发送过来数据在OLED屏幕上显示出当前智能门禁系统的运行状态(即当前识别人物信息ID等)!主要涉及:字符串显示函数与汉字显示函数如下:
字符串显示函数:
// Parameters : x,y -- 起始点坐标(x:0~127, y:0~7); ch[] -- 要显示的字符串; TextSize -- 字符大小(1:6*8 ; 2:8*16)
// Description : 显示codetab.h中的ASCII字符,有6*8和8*16可选择
void OLED_ShowStr(unsigned char x, unsigned char y, unsigned char ch[], unsigned char TextSize)
{
unsigned char c = 0,i = 0,j = 0;
switch(TextSize)
{
case 1:
{
while(ch[j] != '\0')
{
c = ch[j] - 32;
if(x > 126)
{
x = 0;
y++;
}
OLED_SetPos(x,y);
for(i=0;i<6;i++)
WriteDat(F6x8[c][i]);
x += 6;
j++;
}
}break;
case 2:
{
while(ch[j] != '\0')
{
c = ch[j] - 32;
if(x > 120)
{
x = 0;
y++;
}
OLED_SetPos(x,y);
for(i=0;i<8;i++)
WriteDat(F8X16[c*16+i]);
OLED_SetPos(x,y+1);
for(i=0;i<8;i++)
WriteDat(F8X16[c*16+i+8]);
x += 8;
j++;
}
}break;
}
}
汉字显示函数:
// Parameters : x,y -- 起始点坐标(x:0~127, y:0~7); N:汉字在.h中的索引
// Description : 显示ASCII_8x16.h中的汉字,16*16点阵
void OLED_ShowCN(unsigned char x, unsigned char y, unsigned char N)
{
unsigned char wm=0;
unsigned int adder=32*N;
OLED_SetPos(x , y);
for(wm = 0;wm < 16;wm++)
{
WriteDat(F16x16[adder]);
adder += 1;
}
OLED_SetPos(x,y + 1);
for(wm = 0;wm < 16;wm++)
{
WriteDat(F16x16[adder]);
adder += 1;
}
}
// 这是自己写的显示中文字符串的函数,要先把中文字符串“共阴——列行式——逆向输出”取字模后存入asc.h相应的位置(连续存入)
//传入参数分别为:x:起始横坐标
// y:纵坐标(填入0-7)
// begin:填入的中文字符串的第一个字在我们asc.c字库里面的序号
// num:我们要填写几个字
// 比如要填“测试”,取完字模存入后这两个字在字库中序号为0,1,横坐标0,纵坐标第二行,就填:x:0,y:2,begin:0,num:2
void OLED_ShowCN_STR(u8 x , u8 y , u8 begin , u8 num)
{
u8 i;
for(i=0;i<num;i++){OLED_ShowCN(i*16+x,y,i+begin);} //OLED显示标题
}
4.2.2 UART代码
这部分代码是比较核心的,上述博客作者已经说明了,其实树莓派4B发送给STM32的数据都是以字符串流的格式发送来得。所以,即使发送过来的是数字数据也会变成字符,这就需要我们进行解码!
uart.h:
#ifndef __UART_H
#define __UART_H
#include "stm32f1xx_hal.h"
extern UART_HandleTypeDef huart1;
#define USART1_REC_LEN 600
extern int USART1_RX_BUF[USART1_REC_LEN];
extern uint16_t USART1_RX_STA;
extern int USART1_NewData;
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart);
#endif
uart.c:
#include "uart.h"
#include "oled.h"
int USART1_RX_BUF[USART1_REC_LEN]; //目标数据
uint16_t USART1_RX_STA=0;
int USART1_NewData;
extern int num; //标志
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
if(huart ==&huart1)
{
USART1_RX_BUF[USART1_RX_STA&0X7FFF]=USART1_NewData;
USART1_RX_STA++;
if(USART1_RX_STA>(USART1_REC_LEN-1))USART1_RX_STA=0;
num = USART1_RX_BUF[USART1_RX_STA-1];
HAL_UART_Receive_IT(&huart1,(uint8_t *)&USART1_NewData,1);
}
}
上述UART代码,利用 UART 中断函数进行读取 USART1_NewData 的数值,因为我们传输过来的数据分别为“0”(刘德华)“1”(李连杰)“2”(unknown),所以我们仅需对解码后的数据进行处理即可!
4.2.3 Control代码
contorl 部分的代码主要是负责解码树莓派4B发送过来的数据,根据解码的索引值在OLED屏幕上显示相关信息,同时驱动 SG90舵机(门)打开!需要注意的是防止某一用户多次重复进入室内,所以需要实验一个变量 i 去延迟一下判断,这样智能门禁的效果可能更好些!
control.c:
#include "control.h"
#include "uart.h"
#include "tim.h"
#include "oled.h"
int num;
int value;
int i = -1; //防止叠影通信
void SmartAccess()
{
value = num-48; //将树莓派4B发送的ASCII数值转变为字典索引号
if(value == 0 && i != 0) //刘德华
{
OLED_ShowCN_STR(40,4,0,3);
__HAL_TIM_SET_COMPARE(&htim2,TIM_CHANNEL_1,150);
HAL_Delay(3000);
__HAL_TIM_SET_COMPARE(&htim2,TIM_CHANNEL_1,50);
HAL_Delay(3000);
i = 0;
value = 4;
}
if(value == 1 && i != 1) //李连杰
{
OLED_ShowCN_STR(40,4,3,3);
__HAL_TIM_SET_COMPARE(&htim2,TIM_CHANNEL_1,150);
HAL_Delay(3000);
__HAL_TIM_SET_COMPARE(&htim2,TIM_CHANNEL_1,50);
HAL_Delay(3000);
i = 1;
value = 4;
}
if(value == 2) //unknown的索引值
{
OLED_ShowStr(40,4,"unknow",2);
}
if(value == 4)
{
OLED_ShowStr(40,4,"FreeTm",2);
HAL_Delay(3000);
i = -1;
}
}
4.2.4 main函数
/**
* @brief The application entry point.
* @retval int
*/
int main(void)
{
/* USER CODE BEGIN 1 */
int num = 0;
/* USER CODE END 1 */
/* MCU Configuration--------------------------------------------------------*/
/* Reset of all peripherals, Initializes the Flash interface and the Systick. */
HAL_Init();
/* USER CODE BEGIN Init */
/* USER CODE END Init */
/* Configure the system clock */
SystemClock_Config();
/* USER CODE BEGIN SysInit */
/* USER CODE END SysInit */
/* Initialize all configured peripherals */
MX_GPIO_Init();
MX_TIM2_Init();
MX_USART1_UART_Init();
MX_I2C2_Init();
/* USER CODE BEGIN 2 */
OLED_Init();
OLED_CLS();
HAL_UART_Receive_IT(&huart1,(uint8_t *)&USART1_RX_BUF,1);
HAL_TIM_PWM_Start(&htim2,TIM_CHANNEL_1);
OLED_ShowStr(10,2,"Target Person",2);
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
SmartAccess();
}
/* USER CODE END 3 */
}
五、项目效果
5.1 实验视频
基于树莓派4B与STM32的智能门禁系统
5.2 作者有话
本项目的智能门禁系统仅为雏形版本,不足之处非常多!例如:树莓派4B使用的人脸识别方法为传统的 BLP 方法过于老旧!感兴趣的读者朋友可以试试如今大火的深度学习网络模型进行人脸识别,效果可能会好上很多!而且部分读者朋友可能发现我们使用手机照片也可以打开门禁系统,这个是非常危险的,所以常规的门禁系现在都是配备活体目标检测了!总之,该项目优化和拓展的点有很多,丰富一下作为本科毕设完全没有问题!
六、项目代码
代码地址:基于树莓派4B与STM32的智能门禁项目代码资源-CSDN文库
如果积分不够的朋友,点波关注,评论区留下邮箱,作者无偿提供源码和后续问题解答。求求啦关注一波吧 !!!