学习了一下OpenCV,熟悉了一点基础概念,就寻找了一下单片机上能否支持人脸识别,用来做一些小玩意。
结果还真发现了一个模块叫ESP32-CAM。ESP32-CAM算得上是一款最便宜的支持人脸识别的单片机开发板了,性能算是单片机里相当不错的了,虽然也只是勉强支持了人脸识别。但是它的优势也是巨大的,就是价格,太便宜了!!

并且在B站上看到了这位大佬的作品:
基于esp32cam人脸识别开锁完整教程&独家教程
于是也来学一下这个模块的使用。顺便问一下
几十块钱的人脸识别门锁,你敢不敢用。

不过可以拿来给孩子做个玩具。
淘宝了一个带底座的ESP32-CAM模块,这个底座主要就是解决了供电和烧录,直接插上就能连接电脑了。
参照使用手册,搭建了arduino的开发环境,安装了最新的ESP32支持包,可以跑一下官方的范例,能够进行人脸识别。买开发板都会赠送这个教程,这里就不细说了。
不过官方的范例,代码有些混乱,还是大佬开源的这版代码看着逻辑比较清晰。

视频中的大佬将他的代码都奉献出来了,在此表达感谢,大家可以去支持一下。

代码可以下载下来,自己进行一下理解。
不过大佬的代码在我这里运行的时候,会报错,fr_flash的错误,这个我还没找到问题原因,可能就是无法将识别出来的人脸数据,存储起来,导致,每次重启都需要重新录入人脸。

先讲一下人脸识别的关键点。
首先是获取视频,使用的是下面的接口
fb = esp_camera_fb_get();
然后将视频转化为RGB888,使用的是下面的接口
fmt2rgb888(fb->buf, fb->len, fb->format, out_res.image);
人脸的操作,分为了检测(查看有没有人脸),录入(将人脸的特征值存储下来),和识别(识别摄像头拍摄到的人脸是不是在库中)。三种操作都需要进行人脸检测。使用到的接口是
out_res.net_boxes = face_detect(image_matrix, &mtmn_config);
然后将人脸对齐
align_face(out_res.net_boxes, image_matrix, aligned_face)
再提取特征值
out_res.face_id = get_face_id(aligned_face);
注意,这个face_id就是最最核心的数据,是你人脸的特征值。有了这个数据,你就可以进行人脸的判断了。
如果是录入,就需要将这个数据进行存储;
如果是识别,就需要将这个数据与已经存储的特征进行对比,符合就说明拍摄到了对的人。
特征对比的核心函数如下
face_id_node *f = recognize_face_with_name(&st_face_list, out_res.face_id);
其中st_face_list就是一个链表,里面的数据就是已经存储好的face_id。
整个代码思路理清之后,就容易修改了。我们来看一下这个face_id的数据是什么结构。
typedef struct tag_face_id_node
{
struct tag_face_id_node *next; /*!< next face id node */
char id_name[ENROLL_NAME_LEN]; /*!< name corresponding to the face id */
dl_matrix3d_t *id_vec; /*!< face id */
} face_id_node;
typedef struct
{
int w; /*!< Width */
int h; /*!< Height */
int c; /*!< Channel */
int n; /*!< Number of filter, input and output must be 1 */
int stride; /*!< Step between lines */
fptp_t *item; /*!< Data */
} dl_matrix3d_t;
可以看出来这个特征值,就是下面那个结构。
我尝试着打印了一个组数据,发现item为一个512长度的float数组。这就是你人脸的特征,应该也算是你的生物指纹之一吧,这么轻松就能获取到了。

那么就来解决一下flash中没有成功存储face_id的问题吧,
启动的时候,系统调用了这个函数
read_face_id_from_flash_with_name(&st_face_list);
应该就是报错的原因,那么我们不用这个flash存储了,改用数组或者文件直接存储即可。
正好有内存卡可以使用。参考的代码就是SD_MMC这个范例代码。
大概原理就是将用户数据建成目录,每个用户录入一组特征值,即faceID,录入的时候,分别存储到对应目录中。
开机启动的时候,通过读取每个目录,形成核心对比数据,放在st_face_list变量中。
两个核心函数,保存ID
void SD_save_face_id(char* name,int number,dl_matrix3d_t *face_id)
{
int num=0;
char filename[32]={0};
unsigned char* data=NULL;
num=(face_id->w)*(face_id->h)*(face_id->c)*(face_id->n);
sprintf(filename,"/%s",name);
SD_createDir(SD_MMC,filename);
sprintf(filename,"/%s/1.txt",name);
data=(unsigned char*)face_id->item;
SD_writeFile(SD_MMC, filename, data,num*4);
}
读出ID
void SD_read_all_face_id(fs::FS &fs)
{
st_face_list.count=0;
st_face_list.head=NULL;
st_face_list.tail=NULL;
st_face_list.confirm_times=5;
face_id_node* faceidnode_now=NULL;
File root = fs.open("/");
if(!root)
{
Serial.println("Failed to open directory");
return;
}
if(!root.isDirectory())
{
Serial.println("Not a directory");
return;
}
File file = root.openNextFile();
while(file)
{
if(file.isDirectory())
{
face_id_node* faceidnode=NULL;
char fullname[64]={0};
Serial.printf("find user:[%s]\n",file.name());
sprintf(fullname,"%s/1.txt",file.name());
File tryopen = fs.open(fullname);
if(!tryopen)
{
Serial.printf("user has no face id[%s]\n",fullname);
file = root.openNextFile();
continue;
}
else
{
faceidnode = (face_id_node*)dl_lib_calloc(1, sizeof(face_id_node), 0);
if(faceidnode)
{
dl_matrix3d_t* id_vec;
strcpy(faceidnode->id_name,file.name()+1);
id_vec= (dl_matrix3d_t*)dl_lib_calloc(1, sizeof(dl_matrix3d_t), 0);
if(id_vec)
{
float* face=NULL;
faceidnode->id_vec=id_vec;
faceidnode->next=NULL;
id_vec->w=1;
id_vec->h=1;
id_vec->c=512;
id_vec->n=1;
id_vec->stride=512;
face= (float*)dl_lib_calloc(512, sizeof(float), 0);
tryopen.read((unsigned char*)face,512*4);
id_vec->item=face;
if(st_face_list.head==NULL)
{
st_face_list.head=faceidnode;
st_face_list.tail=faceidnode;
faceidnode_now = faceidnode;
}
else
{
st_face_list.tail=faceidnode;
faceidnode_now->next=faceidnode;
faceidnode_now=faceidnode;
}
st_face_list.count++;
}
}
}
}
file = root.openNextFile();
}
}
其他操作函数最终见下载,都放上来就太多了。

烧录好之后,访问web。

可以先录入人脸
输入名字,点击ADD USER,我这里用一个图片代替一下。

然后点击ACCESS CONTROL,进行人脸识别

识别成功,会提示DOOR OPEN FOR test。
重启之后,信息不会再丢失。
有人问,这个美女是谁啊,那有点不好意思了,这是个男的。

这个模块的信号可能不太好,距离路由器远了之后,就会很卡,很奇怪的是,我家用了一个路由器进行了信号的增强覆盖,这个模块接到扩展的路由器上,一样也会变的卡顿。
所以我就单独用了一个路由器,将摄像头模块和电脑组成了一个局域网,而且得把模块放在路由器附近,这样就不再卡顿了。
后续考虑是不是改一下用外置天线,看看是不是天线的问题导致的。
这个模块RST按钮旁边有一个板载红色LED。该LED内部连接到GPIO 33。您可以使用此LED指示正在发生的事情。例如,如果连接了Wi-Fi,则LED为红色,反之亦然。
该LED具有反向逻辑,因此您发送了一个 LOW 信号打开它 HIGH 信号将其关闭。

所以可能模拟红灯亮起,表示开锁,同理这个GPIO可以让你用来触发一些别的操作而不仅限于开锁,开灯,开水龙头,想开啥开啥。

大佬的代码,还请去B站观看一下,然后在评论区即可看到下载链接。
我的代码下载
改这个存储用了一天的时间,所以还是用了点小心机。

你们知道人生的意义是什么吗?分享给你们一张图吧

其实我觉得人生有意义,我们的意义,存在于我们对别人的意义。
我正在编写一个包含C扩展的gem。通常当我写一个gem时,我会遵循TDD的过程,我会写一个失败的规范,然后处理代码直到它通过,等等......在“ext/mygem/mygem.c”中我的C扩展和在gemspec的“扩展”中配置的有效extconf.rb,如何运行我的规范并仍然加载我的C扩展?当我更改C代码时,我需要采取哪些步骤来重新编译代码?这可能是个愚蠢的问题,但是从我的gem的开发源代码树中输入“bundleinstall”不会构建任何native扩展。当我手动运行rubyext/mygem/extconf.rb时,我确实得到了一个Makefile(在整个项目的根目录中),然后当
我已经在Sinatra上创建了应用程序,它代表了一个简单的API。我想在生产和开发上进行部署。我想在部署时选择,是开发还是生产,一些方法的逻辑应该改变,这取决于部署类型。是否有任何想法,如何完成以及解决此问题的一些示例。例子:我有代码get'/api/test'doreturn"Itisdev"end但是在部署到生产环境之后我想在运行/api/test之后看到ItisPROD如何实现? 最佳答案 根据SinatraDocumentation:EnvironmentscanbesetthroughtheRACK_ENVenvironm
我们的git存储库中目前有一个Gemfile。但是,有一个gem我只在我的环境中本地使用(我的团队不使用它)。为了使用它,我必须将它添加到我们的Gemfile中,但每次我checkout到我们的master/dev主分支时,由于与跟踪的gemfile冲突,我必须删除它。我想要的是类似Gemfile.local的东西,它将继承从Gemfile导入的gems,但也允许在那里导入新的gems以供使用只有我的机器。此文件将在.gitignore中被忽略。这可能吗? 最佳答案 设置BUNDLE_GEMFILE环境变量:BUNDLE_GEMFI
这似乎非常适得其反,因为太多的gem会在window上破裂。我一直在处理很多mysql和ruby-mysqlgem问题(gem本身发生段错误,一个名为UnixSocket的类显然在Windows机器上不能正常工作,等等)。我只是在浪费时间吗?我应该转向不同的脚本语言吗? 最佳答案 我在Windows上使用Ruby的经验很少,但是当我开始使用Ruby时,我是在Windows上,我的总体印象是它不是Windows原生系统。因此,在主要使用Windows多年之后,开始使用Ruby促使我切换回原来的系统Unix,这次是Linux。Rub
我正在玩HTML5视频并且在ERB中有以下片段:mp4视频从在我的开发环境中运行的服务器很好地流式传输到chrome。然而firefox显示带有海报图像的视频播放器,但带有一个大X。问题似乎是mongrel不确定ogv扩展的mime类型,并且只返回text/plain,如curl所示:$curl-Ihttp://0.0.0.0:3000/pr6.ogvHTTP/1.1200OKConnection:closeDate:Mon,19Apr201012:33:50GMTLast-Modified:Sun,18Apr201012:46:07GMTContent-Type:text/plain
无论您是想搭建桌面端、WEB端或者移动端APP应用,HOOPSPlatform组件都可以为您提供弹性的3D集成架构,同时,由工业领域3D技术专家组成的HOOPS技术团队也能为您提供技术支持服务。如果您的客户期望有一种在多个平台(桌面/WEB/APP,而且某些客户端是“瘦”客户端)快速、方便地将数据接入到3D应用系统的解决方案,并且当访问数据时,在各个平台上的性能和用户体验保持一致,HOOPSPlatform将帮助您完成。利用HOOPSPlatform,您可以开发在任何环境下的3D基础应用架构。HOOPSPlatform可以帮您打造3D创新型产品,HOOPSSDK包含的技术有:快速且准确的CAD
在应用开发中,有时候我们需要获取系统的设备信息,用于数据上报和行为分析。那在鸿蒙系统中,我们应该怎么去获取设备的系统信息呢,比如说获取手机的系统版本号、手机的制造商、手机型号等数据。1、获取方式这里分为两种情况,一种是设备信息的获取,一种是系统信息的获取。1.1、获取设备信息获取设备信息,鸿蒙的SDK包为我们提供了DeviceInfo类,通过该类的一些静态方法,可以获取设备信息,DeviceInfo类的包路径为:ohos.system.DeviceInfo.具体的方法如下:ModifierandTypeMethodDescriptionstatic StringgetAbiList()Obt
文章目录1.开发板选择*用到的资源2.串口通信(个人理解)3.代码分析(注释比较详细)1.主函数2.串口1配置3.串口2配置以及中断函数4.注意问题5.源码链接1.开发板选择我用的是STM32F103RCT6的板子,不过代码大概在F103系列的板子上都可以运行,我试过在野火103的霸道板上也可以,主要看一下串口对应的引脚一不一样就行了,不一样的就更改一下。*用到的资源keil5软件这里用到了两个串口资源,采集数据一个,串口通信一个,板子对应引脚如下:串口1,TX:PA9,RX:PA10串口2,TX:PA2,RX:PA32.串口通信(个人理解)我就从串口采集传感器数据这个过程说一下我自己的理解,
说在前面这部分我本来是合为一篇来写的,因为目的是一样的,都是通过独立按键来控制LED闪灭本质上是起到开关的作用,即调用函数和中断函数。但是写一篇太累了,我还是决定分为两篇写,这篇是调用函数篇。在本篇中你主要看到这些东西!!!1.调用函数的方法(主要讲语法和格式)2.独立按键如何控制LED亮灭3.程序中的一些细节(软件消抖等)1.调用函数的方法思路还是比较清晰地,就是通过按下按键来控制LED闪灭,即每按下一次,LED取反一次。重要的是,把按键与LED联系在一起。我打算用K1来作为开关,看了一下开发板原理图,K1连接的是单片机的P31口,当按下K1时,P31是与GND相连的,也就是说,当我按下去时
@作者:SYFStrive @博客首页:HomePage📜:微信小程序📌:个人社区(欢迎大佬们加入)👉:社区链接🔗📌:觉得文章不错可以点点关注👉:专栏连接🔗💃:感谢支持,学累了可以先看小段由小胖给大家带来的街舞👉微信小程序(🔥)目录自定义组件-behaviors 1、什么是behaviors 2、behaviors的工作方式 3、创建behavior 4、导入并使用behavior 5、behavior中所有可用的节点 6、同名字段的覆盖和组合规则总结最后自定义组件-behaviors 1、什么是behaviorsbehaviors是小程序中,用于实现