物联网综合项目:基于树莓派4B的人脸识别智能门锁(全栈开发) 您所在的位置:网站首页 基于树莓派的项目 物联网综合项目:基于树莓派4B的人脸识别智能门锁(全栈开发)

物联网综合项目:基于树莓派4B的人脸识别智能门锁(全栈开发)

2024-07-09 19:02| 来源: 网络整理| 查看: 265

这是我第一次写博客,简单的为所做过的项目做个记录。

这个项目是我们专业要求我们所做的综合课设,项目具体功能和实现都是我们自己构思的,然后上网查资料勉强做出来了一个简单的智能门锁系统。整个架构比较简单。

话不多说,接下来开始介绍项目如何实现。

一、概要设计 1、功能

经过我们小组初步设计,智能门锁的主要功能如下:

按键开门功能:用户可以通过输入密码打开门锁,适用于家庭成员和访客。人脸识别开门功能:利用人脸识别技术,识别授权用户并自动开门,提高安全性和便利性。手机微信小程序开门功能:通过手机微信小程序远程控制开门,支持一键开锁、修改密码等功能,方便用户在任何地点控制门锁。实时监控与报警功能:当检测到非法闯入或门锁被破坏时,系统会立即向用户发送警报信息,并触发本地报警。历史记录查询功能:用户可以通过手机微信小程序查看门锁的开锁记录和报警记录,了解门锁使用情况。微信小程序上传人脸功能:用户可以在微信小程序上上传人脸照片到服务器,然后由系统从服务器获取人脸照片进行人脸识别开锁。 2、系统架构

由于老师要求我们全栈开发,所以系统包含三层:应用层、网络层和感知层。

 感知层:由门锁硬件设备组成,包括锁体、控制器、摄像头等,负责数据采集和执行控制。网络层:提供门锁与手机、服务器之间的通信支持,采用Wi-Fi、移动网络等技术实现本地和远程的数据传输。应用层:包括手机微信小程序和云端服务平台,为用户提供门锁控制、数据查询等服务。

感知层:我们使用了树莓派4B作为开发环境,一个USB摄像头、一个4x4矩阵键盘、一个数字舵机和一个电位器。

其中,数字舵机用来模拟门锁的开关,而电位器则用来记录门锁的状态。说实话,一开始我们设计的时候忽略了记录门锁状态的传感器,我天真的以为舵机能够返回它的状态,但实际上舵机只负责输出,因此我们还需要电位器来记录门锁状态。至于如何记录,后面会详细介绍。

网络层:我不知道我们这算不算是网络层,我们使用了阿里云的物联网平台作为服务器,用来接收和传输数据。一开始我们是打算搭建云服务器,然后实现图片和其他数据的上传以及接收的,但是在搭建服务器的时候遇到了困难。主要是国内的域名备案比较麻烦,然后我租了个香港节点的服务器,又买了个域名,搞了几天给它绑定了域名,进行了SSL认证,我以为这样就可以了,结果在搭建完一个简单的Node项目(返回Hello World)后,仍然不能通过域名绑定的URL访问,而微信小程序和Android Stdio都不能直接使用IP地址访问(好像在同一个网络下自建服务器可以实现)。

然后由于时间的原因我们不能继续在服务器上浪费时间了,所以我们就直接使用了阿里云的物联网平台,可以简单的创建数据模型进行传输。

应用层:我们的应用层是微信小程序,用户可以通过微信小程序进行开关锁,查看门锁状态以及查询开锁记录。

二、详细设计

由于我们小组分工合作,我负责树莓派编程,因此我的详细设计主要包括读取矩阵键盘、控制数字舵机以及发送数据到阿里云平台。不过我会将整个系统的所有设计都写一遍,并且后面会附上源码,感兴趣的朋友可以下载。

1、矩阵键盘

矩阵键盘我是在淘宝买的树莓派矩阵键盘,然后通过杜邦线和树莓派4B的端口连接起来。

矩阵键盘的读取可以参考这篇文章:

http://t.csdnimg.cn/XLRY0

代码复用性很高,我拿来直接就用了。需要注意的是先测试矩阵键值对应的值,看输出的值是否是自己想要的。

源码文件名:keyvalue.py

2、数字舵机SG90

数字舵机的控制代码有点难读懂,我参考了这篇文章:

http://t.csdnimg.cn/RCgqv

下面说说我的个人理解:数字舵机是根据改变其占空比来控制舵机角度的,舵机的输入频率是50KHZ,周期就是20ms。而其占空比与对应角度的关系如下图:

如果只需要输出图示角度,就不用关心公式了,直接输出相应的占空比如: 

p2.ChangeDutyCycle(2.5)#或者其他占空比

如果要输出对应的角度,可以参考上面那篇文章的公式,也可以自己根据角度和占空比对应的关系,来得到某个角度所对应占空比的公式,封装成函数就可以了。

注意,输出到对应角度后,记得将占空比置为0:

p2.ChangeDutyCycle(0)#占空比置0消抖

我的舵机只需要旋转180度,也就是直接输出12.5%的占空比就可以了。

然而在实际测试中,我却发现,舵机正转180度,相当于开锁,那我如果要它关锁,我该怎么输出呢,输出负角度让舵机反转180度?

这样当然是不行的,如果按照如下代码:

p2.ChangeDutyCycle(-12.5)

这样拿去运行的话会直接报错,占空比没有负数的情况。那么到底怎么让它反转呢?

其实,数字舵机SG90的转动角度,是基于当前角度来转动的。

由于我们一开始让它转了180度,那么想要让它反转180度,其实只要输出0度对应的占空比,舵机就能反转180,也就是相当于回到原位。

源码文件名:SG90_concrol.py

3、摄像头、人脸识别

由于树莓派人脸识别网上都有很多教程,整个流程走下来篇幅较长,所以请读者上网自行搜索。注意的是树莓派要调用摄像头进行人脸识别,需要下载opencv库及其他库,读者可以先创建python测试脚本,然后封装成方法调用就可以了。我的源码中有人脸识别的代码,文件名“face.py”,是基于每一帧读取然后和图像比对的,小于阈值则人脸识别通过。只是一个简单的人脸识别,读者可以自行查阅资料使用更高级的深度学习算法模型等进行人脸识别。

4、树莓派连接阿里云IOT平台 ·以下是避坑指南,想直接知道怎么实现的跳过不看就行

一开始我查阅资料,网上有的是使用mqtt协议连接阿里云并且发送数据,当然数据发送丝毫没有问题,只是那篇资料没有提到如何接收阿里云的数据。然后我不断上网查资料,又查阅到一篇用linkkit协议连接阿里云的,可以成功接收到阿里云的数据,但是调用发布数据的方法时,阿里云平台却收不到所发送的数据。

这让我非常苦恼。一方面是mqtt只能发送数据,另一方面linkkit又只能接收数据。

这可让我如何是好啊!

于是,我想到,将两者结合起来用。

这个看起来可行的方法,实际上用代码跑起来,非常的差劲。首先就是mqtt和主程序是同步运行,要么放在循环里连接一次就发一次数据,要么放在循环外连接,但里面就只能发一次数据了。

我选择了前者。 而linkkit让我感觉使用起来更方便,因为linkkit提供了异步连接的方法:

lk.connect_async()#linkkit异步连接

所以,linkkit在我的心中好感度倍增。

接着我将两者放在主程序中运行,结果……

我当时都忘记录屏了,所以你们没办法看到效果。

总之就是一个字,屎!

由于将mqtt连接放在了主函数里,每一次循环都要发送一次数据,导致整个程序运行缓慢,而且键盘读取也非常卡顿,几乎让矩阵键盘没办法正常使用。

而且,程序不仅要发送数据,还要接收数据,在接收数据的时候linkkit又要和阿里云连接,通过两个协议连接阿里云,结果就是其中一个连接超时,整个程序要么只能发要么只能收。

因此,程序在运行时,网络协议连接必须异步进行,在后台等待调用接收或者发送的方法,才能让整个程序高效运转。

所以,我去咨询了阿里云客服,详细的阐明了我现在遇到的问题。客服和我说,无论是mqtt还是linkkit都有发布数据和接收数据的方法,在阿里云的官方文档能找到(具体在哪里我也忘了,到时候你们可以问问阿里云客服,也就是通过提交工单反馈问题),我们只需要选择其中一个连接协议就可以。

linkkit是阿里云自己提供的一个轻量级可靠的设备连接通信协议,相比于mqtt,复用性肯定不高。但是他自己提供了异步连接的方法,所以我选择了linkkit。

当然,mqtt也可以进行异步连接,读者自行查找资料。

·连接阿里云平台实现方法及代码

无论是用linkkit还是mqtt,都需要先下载其python库,读者自行找资料下载。

因为我是用了linkkit,如果想知道mqtt怎么连接阿里云平台,并且如何发布数据和接收数据的,可以查询阿里云官方文档,有个下载文档里面介绍了mqtt的所有方法。记得使用异步连接!

接下来是linkkit连接阿里云的代码,调用代码如下:

lk = linkkit.LinkKit( host_name="cn-shanghai", product_key=shared.ProductKey, device_name=shared.DeviceName, device_secret=shared.DeviceSecret) lk.thing_setup("lock.json") lk.on_thing_enable = receive.on_thing_enable lk.on_thing_disable = receive.on_thing_disable lk.on_connect = receive.on_connect lk.on_thing_prop_post = receive.on_thing_prop_post #发布数据方法 lk.on_subscribe_topic = receive.on_subscribe_topic lk.on_topic_message = receive.on_topic_message #接收数据方法 lk.on_publish_topic = receive.on_publish_topic lk.on_unsubscribe_topic = receive.on_unsubscribe_topic ''' 上述过程的目的是当调用到其中如“lk.on_topic_message”函数时会执行“receive.on_topic_message”,这样我们就可以对接收到的数据进一步处理 ''' lk.connect_async() #linkkit异步连接 time.sleep(2) #延时,给一点连接时间

使用linkkit的话,需要去阿里云IOT平台下载数据物理模型才能发布数据,在我的源码中是lock.json文件。

这样,我们就可以使用如下方法发布数据:

prop_data = { "mark":shared.status } lk.thing_post_property(prop_data)

调用的方法在“receive.py”文件中。

5、微信小程序连接阿里云平台

微信小程序如何连接阿里云平台呢?

用的最多的协议还是mqtt协议,因此我也是使用了mqtt协议连接。

附上微信小程序的.js文件,其他界面设计和逻辑就看你们自己的需求了,后面我会放上微信小程序的源码,仅供参考。

var mqtt = require('../../utils/mqtt.min.js'); //根据自己存放的路径修改 const crypto = require('../../utils/hex_hmac_sha1.js'); //根据自己存放的路径修改 const app = getApp(); Page({ data: { lockStatus: '关', records: [], isConnecting: false }, deviceConfig: { productKey: "k1h41jOvnlr", deviceName: "wechat", deviceSecret: "bac19ea7cc134d775ecb886155da6519", regionId: "cn-shanghai" }, onLoad: function () { this.connectMqttClient(); // 初始化MQTT连接 }, pass: function() { const password = app.globalData.lockPassword; // 从全局数据中获取lockPassword console.log(password); // 打印密码 }, connectMqttClient: function () { if (this.data.isConnecting || (app.globalData.mqttClient && app.globalData.mqttClient.connected)) { console.log('已经在连接中或已连接,无需重复连接'); return; } this.setData({ isConnecting: true }); const options = this.initMqttOptions(this.deviceConfig); const mqttClient = mqtt.connect(`wxs://${this.deviceConfig.productKey}.iot-as-mqtt.${this.deviceConfig.regionId}.aliyuncs.com`, options); mqttClient.on('connect', () => { console.log('连接服务器成功'); this.setData({ isConnecting: false }); app.globalData.mqttClient = mqttClient; // 保存到全局变量 mqttClient.subscribe('/k1h41jOvnlr/wechat/user/get', (err) => { if (!err) { console.log('订阅成功'); } else { console.error('订阅失败:', err); } }); }); mqttClient.on('message', (topic, message) => { console.log('收到消息:', topic, message.toString()); if (topic === '/k1h41jOvnlr/wechat/user/get') { this.handleLockMessage(message.toString()); } }); mqttClient.on('close', () => { console.log('MQTT连接已关闭,尝试重连'); this.setData({ isConnecting: false }); this.retryConnection(); }); mqttClient.on('error', (error) => { console.error('连接错误:', error); this.setData({ isConnecting: false }); this.retryConnection(); }); mqttClient.on('offline', () => { console.log('MQTT客户端离线,尝试重连'); this.setData({ isConnecting: false }); this.retryConnection(); }); this.setData({ mqttClient: mqttClient }); }, retryConnection: function() { // 在尝试重新连接之前等待一段时间,防止频繁重连 setTimeout(() => { this.connectMqttClient(); }, 5000); // 5秒后重试 }, handleLockMessage: function (message) { try { const parsedMessage = JSON.parse(message); const value = parsedMessage.items.mark.value; let newStatus = ''; if (value === 1) { newStatus = '开'; } else if (value === 0) { newStatus = '关'; } else { console.warn('未知的 mark 值:', value); return; } const previousStatus = this.data.lockStatus; // 仅当状态变化时才更新状态和记录 if (newStatus !== previousStatus) { const newRecord = `锁已${newStatus} - ${new Date().toLocaleString()}`; this.setData({ lockStatus: newStatus, records: [newRecord, ...this.data.records] }); console.log(`添加${newStatus === '开' ? '开' : '关'}锁记录:`, newRecord); } } catch (e) { console.error('解析消息失败:', e); } }, toggleLock: function() { // 计算将要发送到阿里云的状态 const requestedStatus = this.data.lockStatus === '开' ? 0 : 1; // 发送相反的状态请求 this.publishUnlockStatus(requestedStatus); // 发送请求到阿里云 wx.showToast({ title: '发送请求中...', icon: 'loading', duration: 1000 }); }, publishUnlockStatus: function(isLocked) { const mqttClient = app.globalData.mqttClient; if (mqttClient && mqttClient.connected) { const topic = `/sys/${this.deviceConfig.productKey}/${this.deviceConfig.deviceName}/thing/event/property/post`; const message = JSON.stringify({ id: Date.now(), version: '1.0', params: { Lock: isLocked ? 1 : 0, }, method: "thing.event.property.post" }); mqttClient.publish(topic, message, function(err) { if (err) { console.error('发布消息失败:', err); } else { console.log('发布消息成功'); } }); } else { console.error('MQTT客户端未连接,尝试重连'); this.connectMqttClient(); } }, showRecords: function() { const records = this.data.records; wx.navigateTo({ url: '/pages/records/records', success: function(res) { res.eventChannel.emit('sendRecords', { records: records }); res.eventChannel.on('clearRecords', () => { this.setData({ records: [] }); }); }.bind(this), fail: function(err) { console.error('导航到记录页面失败', err); } }); }, initMqttOptions: function(deviceConfig) { const params = { productKey: deviceConfig.productKey, deviceName: deviceConfig.deviceName, timestamp: Date.now(), clientId: Math.random().toString(36).substr(2) }; const options = { keepalive: 60, clean: true, protocolVersion: 4 }; options.password = this.signHmacSha1(params, deviceConfig.deviceSecret); options.clientId = `${params.clientId}|securemode=2,signmethod=hmacsha1,timestamp=${params.timestamp}|`; options.username = `${params.deviceName}&${params.productKey}`; return options; }, signHmacSha1: function(params, deviceSecret) { let keys = Object.keys(params).sort(); const list = []; keys.map((key) => { list.push(`${key}${params[key]}`); }); const contentStr = list.join(''); return crypto.hex_hmac_sha1(deviceSecret, contentStr); } });

值得注意的是,为了防止连接到的mqtt只能在一个页面内使用,我们可以创建一个全局变量存放获取到的mqtt变量,在其他页面需要接收或发送数据时只需要调用即可。

6、程序的数据处理

看到这的小伙伴可能会有疑惑,接收的数据如何传到主程序中呢?

当然可以使用方法中的return来返回数据,但我选择了创建共享变量这个方法。

使用创建共享变量的方法能够避免主程序中拥有太多的变量定义,使代码看起来美观,也能方便调用其他方法直接给变量复制。

创建共享变量的方法很简单,首先创建一个文件,如“shared.py”文件,在文件中初始化变量,然后在其他文件中调用这个变量给变量赋值,如:

shared.pre_status=shared.status

记得在文件中引入我们的“shared”文件。

至此,树莓派硬件编程到这里也算是基本完成。

值得注意的是,在实际的系统测试过程中,我发现了bug。

7、bug的触发及处理

程序基本运行后,会出现逻辑上的bug。

主程序的代码如下(改进bug后的代码):

while 1: shared.status=B1K.B1Kstatus()#读取电位器的状态——0 or 1 if shared.status==0 and shared.lock_value==1 and operation==1:#使用硬件开锁后手动关锁判定 shared.lock_value=0 operation=0 #shared.pre_lock_value=shared.status if shared.status==0 and shared.lock_value==1 and shared.pre_lock_value==1 and operation==0:#使用远程开锁后手动关锁判定 shared.lock_value=0 shared.pre_lock_value=0 if shared.status!=shared.pre_status:#锁状态改变,则发送数据 print('lock_status:',shared.status) shared.pre_status=shared.status prop_data = { "mark":shared.status } lk.thing_post_property(prop_data) if shared.lock_value!=shared.pre_lock_value:#接收到的开锁命令发生改变,则记录上一次开锁命令 print('lock_value:',shared.lock_value) shared.pre_lock_value=shared.lock_value shared.swap = 1 if shared.swap==1:#开锁命令改变则控制舵机 if shared.lock_value==1: SG90_concrol.control(1)#开锁 shared.swap=0 else : SG90_concrol.control(0)#关锁 shared.swap=0 key=keyvalue.getkey()#读取键盘输入 if key!=None and key=='A':#按下按键‘A’,则进入密码开锁 print('input password') key=None password='' while 1: key1 = keyvalue.getkey() if not key1==None: password = password + key1 if password==shared.true_password:#密码正确与否判定 SG90_concrol.control(1)#开锁 shared.status=B1K.B1Kstatus() print(shared.status) prop_data = { "mark":shared.status } lk.thing_post_property(prop_data)#开锁后发送锁状态 shared.lock_value=1 operation=1 break if len(password)>4:#只能输入四位密码 password='' if key1=='#': print('exit') break print('password:',password) if key!=None and key=='B':#按下B,则进入人脸识别开锁 face.recognize_faces(known_face_encodings,known_face_names)#人脸识别算法,返回开锁人姓名 print(shared.name) SG90_concrol.control(1) shared.lock_value=1 operation=1 time.sleep(0.2) bug1:使用密码开锁或者人脸识别开锁后,无法使用微信小程序关锁。

这个bug产生的原因是,当我们使用密码开锁或人脸开锁后,锁状态status置为1,状态改变后,会发送数据给阿里云。但是由于我控制锁,是通过前一个锁指令(也就是接收到的lock_value值)是否发生改变来判定的。至于为什么要通过锁指令发生改变来判定,后面再解释。

所以,因为我是用密码或人脸开锁,锁状态status改变,但是lock_value值和lock_prevalue的值都没有改变,都为0(因为使用密码开锁前使用了云端关锁,所以value值存储为0)。

而这个时候锁状态为1,我们想要远程关锁,那么发送的指令为0。由于接收的指令仍然为0,和前一个状态一致,所以锁指令不执行。

这个时候,我们就只需要将lock_value赋值为1(用密码开锁status为1,那么lock_value值也应该为1,很合理嘛)。

于是,又出现了问题。由于将lock_value值置为了1,回到循环中后检测到了value值改变,所以pre_value值也改变,两个都为1。并且还会执行一次开锁。这个时候虽然可以通过远程关锁,但是使用手动关锁的时候,程序检测到锁指令为1,又给你打开了。

也就是说,无法手动关锁了。这在实际应用中非常不合常理,所以我又加了一个变量——operation。

它作为记录使用非远程开锁(密码或者人脸识别)的变量,当使用非远程开锁时,operation值为1,那么接下来,我只要在主程序中检测到锁的状态为0而value值为1且operation值为1(说明手动关锁),就将operation赋值为0,同时将lock_value值赋值为0,就可以解决bug。

那么为什么要使用锁指令是否改变来判定指令是否生效呢?因为如果不这么做,锁指令一直为1,且又在循环里,就会一直执行开锁,导致舵机抖动,而且频率很快,有可能会使舵机损坏。

bug2:使用非远程开锁后,手动关锁再使用远程开锁失效。

这其实是bug1衍生出来的bug2,如果没有解决bug1,也就不会有bug2。这可能跟我最开始的逻辑判定有关,如果修改整个锁的逻辑判定,或许就不会产生这两个bug。但我做这个课设的时候离结课没几天了,根本没有时间去修改整个逻辑判定。这让我想到公司运行的代码出现bug一样,你总不可能去为了一个bug而动整个底层逻辑,同样都是收益低于付出。只能硬着头皮改bug。

这个bug其实很简单,使用非远程开锁后,lock_value值为1,这个时候手动关锁,锁状态status为0,而开锁指令lock_value为1,所以开锁指令和前一个指令相同,指令无法生效。但是不能进行简单的判定手动关锁判定,如(伪代码):if 锁状态为0 且 锁指令为1 ->让锁指令为0

这样判定会出现很多问题,第一个就是这个判定在bug1的解决方案的判定之上了,会产生更多的bug。

因此,我们要确保手动关锁之前是用非远程开锁还是远程开锁。正好用到了bug1中的operation。

如果operation为0,则用的是非远程开锁,为1则是远程开锁。

那么解决方案来了:if 锁状态为0 且 锁指令为1 且 operation为0 ->让锁指令为0

又错辣!

这样会导致正常远程开锁也开不了,感兴趣的读者测试一下就知道了。

因此,正确的判定是: if 锁状态为0 且 锁指令为1 且 锁prevalue为1 且 operation为0

->锁指令为0,锁prevalue为0

到这里,总算是解决了这个bug了。因为系统比较简单,运行起来只发现了这两个bug。

三、源码以及系统的使用说明 1、智能门锁系统使用说明:

智能门锁是通过4x4按键来决定开锁方式的,按下S11(对应键值为‘A’)进入密码输入,密码长度只有四位,超过四位清空,读者可以自行修改;按下S12(对应键值为‘B’)进入人脸识别,键值如何设置读者可以自行决定,不用和我的一样。

修改密码功能:密码在阿里云平台设置为一个四位的字符串,微信小程序只需要给阿里云发送四位的字符串,树莓派就收到后在receive.py中的接收消息方法进行处理,将新密码的值赋值给shared.true_password中。

注意:读者在使用时需在树莓派对应源码里的photo里加上自己的图片以作为人脸识别的资源!

def on_topic_message(topic, payload, qos, userdata): print("Received message from topic:", topic) data = json.loads(payload) if "items" in data and "Lock" in data["items"]:#接收锁指令 shared.lock_value = data["items"]["Lock"]["value"] #print("Lock value:", shared.lock_value) if "items" in data and "password" in data["items"]:#修改密码 shared.true_password = data["items"]["password"]["value"] print("password:", shared.true_password) 2、系统源码

github网站源码,树莓派4B:https://github.com/fake-gd/ZNMS_raspberry.git

微信小程序:GitHub - fake-gd/ZNMS-wx

四、系统改进方向及实物展示 1、实物展示:

用手上的材料做了一个简易模型,看起来很寒碜哈哈。

2、改进方向

我觉得我们这个项目还是有很多可以改进的地方的,当时因为课程时间短,学校板子等到课程开始后五天才到,留给我们做整体开发也就只有八天左右,所以很多功能都没有实现。

比如将人脸识别开锁者的姓名显示到微信小程序端;在检测到开锁5s后无人关锁则在微信小程序端提醒用户关锁;如果门被强行打开,则在微信小程序端提示并且触发报警。

还有服务端的搭建,如果可以正常访问url的话,我可以先构建一个服务端的数据库用来存放数据,然后从微信小程序拍照上传到服务器,树莓派通过服务器获取并下载照片,这样就可以通过手机添加新的人脸。

以及后续用户成员访问功能等等,这是这个项目还可以改进的地方!



【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有