惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

雷峰网
雷峰网
MyScale Blog
MyScale Blog
PCI Perspectives
PCI Perspectives
S
Securelist
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
V
Vulnerabilities – Threatpost
C
CXSECURITY Database RSS Feed - CXSecurity.com
D
Darknet – Hacking Tools, Hacker News & Cyber Security
L
Lohrmann on Cybersecurity
C
Cybersecurity and Infrastructure Security Agency CISA
Spread Privacy
Spread Privacy
量子位
P
Proofpoint News Feed
T
Tailwind CSS Blog
腾讯CDC
G
Google Developers Blog
V
Visual Studio Blog
The GitHub Blog
The GitHub Blog
博客园 - 司徒正美
GbyAI
GbyAI
The Register - Security
The Register - Security
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
罗磊的独立博客
A
About on SuperTechFans
有赞技术团队
有赞技术团队
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
月光博客
月光博客
Recorded Future
Recorded Future
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
aimingoo的专栏
aimingoo的专栏
美团技术团队
Engineering at Meta
Engineering at Meta
博客园 - 【当耐特】
小众软件
小众软件
F
Fortinet All Blogs
Last Week in AI
Last Week in AI
U
Unit 42
T
The Blog of Author Tim Ferriss
B
Blog RSS Feed
云风的 BLOG
云风的 BLOG
Microsoft Azure Blog
Microsoft Azure Blog
The Cloudflare Blog
Recent Announcements
Recent Announcements
S
SegmentFault 最新的问题
D
DataBreaches.Net
Stack Overflow Blog
Stack Overflow Blog
A
Arctic Wolf
C
CERT Recently Published Vulnerability Notes
Security Latest
Security Latest
L
LINUX DO - 热门话题

元视角

.NET 生态下的 Agent 框架选型:从 ReAct 到原生推理 - 元视角 从「能用」到「好用」:LLM 流式响应实现方式的探索之路 - 元视角 当我用 2000 条聊天记录,让 AI 为我画一幅自画像 - 元视角 基于 Supabase 的 AI 应用开发探索 - 元视角 微博 × MCP:社交媒体新玩法解锁 - 元视角 四点钟海棠花未眠 - 元视角 Semantic Kernel × MCP:智能体的上下文增强探索 - 元视角 基于 K-Means 聚类分析实现人脸照片的快速分类 - 元视角 容器技术驱动下的代码沙箱实践与思考 - 元视角 温故而知新:后端通用查询方案的再思考 - 元视角 浅议 CancellationToken 在前后端协同取消场景中的应用 - 元视角 Semantic Kernel 视角下的 Text2SQL 实践与思考 - 元视角 关于 ChatGPT 的流式传输,你需要知道的一切 - 元视角 RAG 的是与非、Rewrite 和 Rerank - 元视角 使用 EFCore 和 PostgreSQL 实现向量存储及检索 - 元视角 基于 LLaMA 和 LangChain 实践本地 AI 知识库 - 元视角 使用 llama.cpp 在本地部署 AI 大模型的一次尝试 - 元视角 如何为 Git 配置多个 SSH Key - 元视角 C# 使用 LibUsbDotNet 实现 USB 设备检测 - 元视角 基于 C# 实现样式与数据分离的打印方案 - 元视角 基于 SVG 的图形交互方案实践 - 元视角 前端视频播放技术概览 - 元视角 温故而知新,再话 Python 动态导入 - 元视角 后 GPT 时代,NLP 不存在了? - 元视角 视频是不能 P 的系列:使用 Milvus 实现海量人脸快速检索 - 元视角 GDI+下字体大小自适应方案初探 - 元视角 小爱音箱集成 ChatGPT 的不完全教程 - 元视角 程序员视角下的三体世界随想 - 元视角 关于 Docker 容器配置信息的渐进式思考 - 元视角 在 Docker 容器内集成 Crontab 定时任务 - 元视角 为你的服务器集成 LDAP 认证 - 元视角 似花还似非花 - 元视角 浅议分布式链路追踪与日志的整合 - 元视角 关于 Git 大文件上传这件小事 - 元视角 .NET 进程内队列 Channel 的入门与应用 - 元视角 使用 Fody 实现 .NET 的静态编织 - 元视角 .NET Core + ELK 搭建可视化日志分析平台(下) - 元视角 聊一聊前端图片懒加载背后的故事 - 元视角 支持外部链接跳转的 Vue Router 扩展实现 - 元视角 视频是不能 P 的系列:OpenCV 和 Dlib 实现表情包 - 元视角 不得不说的 ASP.NET Core 集成测试 - 元视角 再议 DDD 视角下的 EFCore 与 领域事件 - 元视角 Vue.js 前端项目容器化部署实践极简教程 - 元视角 再见,人间四月天 - 元视角 Python 图像风格化迁移助力画家梦想 - 元视角 利用 ASP.NET Core 中的标头传播实现分布式链路追踪 - 元视角 利用 gRPC 实现文件的上传与下载 - 元视角 七种武器:延迟队列的原理和实现总结 - 元视角 gRPC 流式传输极简入门指南 - 元视角 Envoy 集成 Jaeger 实现分布式链路追踪 - 元视角 浅议非典型 Web 应用场景下的身份认证 - 元视角 gRPC 借助 Any 类型实现接口的泛化调用 - 元视角 分布式丛林探险系列之 Redis 集群模式 - 元视角 分布式丛林探险系列之 Redis 主从复制模式 - 元视角 通过 Python 预测 2021 年双十一交易额 - 元视角 gRPC 搭配 Swagger 实现微服务文档化 - 元视角 SSL/TLS 加密传输与数字证书的前世今生 - 元视角 使用 Python 自动识别防疫健康码 - 元视角 你不可不知的容器编排进阶技巧 - 元视角 ASP.NET Core 搭载 Envoy 实现 gRPC 服务代理 - 元视角 再话 AOP,从简化缓存操作说起 - 元视角 ASP.NET Core 搭载 Envoy 实现微服务身份认证(JWT) - 元视角 ASP.NET Core 搭载 Envoy 实现微服务的监控预警 - 元视角 ASP.NET Core 搭载 Envoy 实现微服务的反向代理 - 元视角 ASP.NET Core gRPC 打通前端世界的尝试 - 元视角 EFCore 实体命名约定库:EFCore.NamingConventions - 元视角 ASP.NET Core gRPC 集成 Polly 实现优雅重试 - 元视角 ASP.NET Core gRPC 健康检查的探索与实现 - 元视角 ASP.NET Core gRPC 拦截器的使用技巧分享 - 元视角 SnowNLP 使用自定义语料进行模型训练 - 元视角 使用 HttpMessageHandler 实现 HttpClient 请求管道自定义 - 元视角 ABP vNext 的实体与服务扩展技巧分享 - 元视角 ABP vNext 对接 Ant Design Vue 实现分页查询 - 元视角 源代码探案系列之 .NET Core 跨域中间件 CORS - 元视角 源代码探案系列之 .NET Core 限流中间件 AspNetCoreRateLimit - 元视角 源代码探案系列之 .NET Core 并发限制中间件 ConcurrencyLimiter - 元视角 通过 EmbededFileProvider 实现 Blazor 的静态文件访问 - 元视角 低代码,想说爱你不容易 - 元视角 记一次失败的 ThoughtWorks 面试经历 - 元视角 从 C# 1.0 到 C# 9.0,历代 C# 语言特性一览 - 元视角 通过 Python 分析 2020 年全年微博热搜数据 - 元视角 基于 Python 和 Selenium 实现 CSDN 一键三连自动化 - 元视角 使用多线程为你的 Python 爬虫提速的 N 种姿势,你会几种? - 元视角 实现网页长截图的常见思路总结 - 元视角 温故而知新,由 ADO.NET 与 Dapper 所联想到的 - 元视角 视频是不能 P 的系列:OpenCV 人脸检测 - 元视角 作为技术宅的我,是这样追鬼滅の刃的 - 元视角 使用 Python 抽取《半泽直树》原著小说人物关系 - 元视角 厉害了!打工人用 Python 分析西安市职位信息 - 元视角 使用 dotTrace 对 .NET 应用进行性能分析与优化 - 元视角 一道 HashSet 面试题引发的蝴蝶效应 - 元视角 基于选项模式实现.NET Core 的配置热更新 - 元视角 Dapper.Contrib 在 Oracle 环境下引发 ORA-00928 异常问题的解决 - 元视角 .NET Core 中对象池(Object Pool)的使用 - 元视角 利用 MySQL 的 Binlog 实现数据同步与订阅(下):EventBus 篇 - 元视角 利用 MySQL 的 Binlog 实现数据同步与订阅(中):RabbitMQ 篇 - 元视角 利用 MySQL 的 Binlog 实现数据同步与订阅(上):基础篇 - 元视角 记一次从已损坏的 Git 仓库中找回代码的经历 - 元视角 .NET Core 原生 DI 扩展之属性注入实现 - 元视角 .NET Core 原生 DI 扩展之基于名称的注入实现 - 元视角
视频是不能 P 的系列:使用 Dlib 实现人脸识别 - 元视角
飞鸿踏雪 · 2022-11-02 · via 元视角

本文是 #视频是不能 P 的系列# 的第三篇。此前,我们已经可以通过 OpenCV 或者 Dlib 实现对人脸的检测,并在此基础上实现了某种相对有趣的应用。譬如,利用人脸特征点提取面部轮廓并生成表情包、将图片中的人脸批量替换为精神污染神烦狗 等等。当然,在真实的应用场景中,如果只是检测到人脸,那显然远远不够的,我们更希望识别出这张人脸是谁。此时,我们的思绪将会被再次拉回到人脸识别这个话题。在探索未知世界的过程中,博主发现 OpenCV 自带的 LBPH 方法,即局部二值模式直方图方法,识别精度完全达不到预期效果。所以,博主最终选择了 Dlib 里的特征值方法,即:对每一张人脸计算一个 128 维的向量,再通过计算两个向量间的欧式距离来判断是不是同一张人脸。在此基础上,博主尝试结合 支持向量机 来实现模型训练。因此,这篇文章其实是对整个探索过程的梳理和记录,希望能给大家带来一点启发。

原理说明

如下图所示,假设对于每一个人物 X ,我们有 N 个人脸样本,通过 Dlib 提供的 compute_face_descriptor() 方法,我们可以计算出该人脸样本的特征值,这是一个 128 维度的向量。如果我们对这些人脸样本做同样的处理,我们就可以得到人物 X 的特征值列表 feature_list_of_person_x。在此基础上,利用 MumPy 中的 mean() 方法,我们就可以计算出人物 X 的平均特征 features_mean_person_x。最终,我们把人物 X 的平均特征和名称一起写入到一个 CSV 文件里面。至此,我们已经拥有了一个简单的人脸数据库。

Dlib 人脸识别原理说明图 Dlib 人脸识别原理说明图

可以预见的是,一旦我们把人脸特征数值化,那么,人脸识别就从一个图形学问题变成了数学问题。对于图中的待检测人脸,我们只需要按同样地方式计算出特征值,然后从 CSV 文件中找一个距离它最近的特征即可。这里,博主使用的是欧式距离,并且人为规定了一个阈值 0.4, 即:当这个距离小于 0.4 时,我们认为人脸匹配成功;当这个距离大于 0.4 时,我们认为人脸匹配失败。下面的例子展示了使用 Dlib 计算人脸特征值的基本过程:

detector = dlib.get_frontal_face_detector()
predictor = dlib.shape_predictor('shape_predictor_68_face_landmarks.dat')
face_reco_model = dlib.face_recognition_model_v1("dlib_face_recognition_resnet_model_v1.dat")

# 计算特征值
image = Image.open('/faces/person/sample-0.jpg')
image = cv2.cvtColor(np.asarray(image), cv2.COLOR_RGB2BGR)
faces = detector(image, 1)
if len(faces) != 0:
    face = faces[0]
    shape = predictor(image, face)
    face_descriptor = face_reco_model.compute_face_descriptor(image, shape)

此时, face_descriptor 就是当前人脸的特征值,这是一个 128 维的向量。我们只需要对每一个人脸样本重复以上步骤,就可以获得人物 X 的特征值列表,进而计算出人物 X 的特征均值。

实现过程

如下图所示,我们为每一个人物建立了一个文件夹,文件夹的名称即为对应人物的名称:

人脸样本的目录结构 人脸样本的目录结构

显然,对于每一个人物而言,我们只需要遍历该文件夹下的所有图片,即可计算出它的特征均值:

def get_mean_features_of_face(path):
    path = os.path.abspath(path)
    subDirs = [os.path.join(path, f) for f in os.listdir(path)]
    subDirs = list(filter(lambda x:os.path.isdir(x), subDirs))
    for index in range(0, len(subDirs)):
        subDir = subDirs[index]
        person_label = os.path.split(subDir)[-1]
        image_paths = [os.path.join(subDir, f) for f in os.listdir(subDir)]
        image_paths = list(filter(lambda x:os.path.isfile(x), image_paths))
        feature_list_of_person_x = []
        for image_path in image_paths:
            # 计算每一个图片的特征
            feature = get_128d_features_of_face(image_path)
            if feature == 0:
                continue
            
            feature_list_of_person_x.append(feature)
        
        # 计算当前人脸的平均特征
        features_mean_person_x = np.zeros(128, dtype=object, order='C')
        if feature_list_of_person_x:
            features_mean_person_x = 
                np.array(feature_list_of_person_x, dtype=object).mean(axis=0)
        
        yield (features_mean_person_x, person_label)

其中,get_128d_features_of_face() 方法用来计算某个图片的特征值:

def get_128d_features_of_face(image_path):
    image = Image.open(image_path)
    image = cv2.cvtColor(np.asarray(image), cv2.COLOR_RGB2BGR)
    faces = detector(image, 1)

    if len(faces) != 0:
        shape = predictor(image, faces[0])
        face_descriptor = face_reco_model.compute_face_descriptor(image, shape)
    else:
        face_descriptor = 0
    return face_descriptor

好了,当我们计算出每一个人物的特征均值以后,我们需要把当前人物的名称、特征均值一起写入到 CSV 文件里面,这样做的目的是方便我们后面做人脸识别:

def extract_features_to_csv(faces_dir):
    mean_features_list = list(get_mean_features_of_face(faces_dir))
    with open(FACES_FEATURES_CSV_FILE, "w", newline="") as csvfile:
        writer = csv.writer(csvfile)
        for mean_features in mean_features_list:
            person_features = mean_features[0]
            person_label = mean_features[1]
            person_features = np.insert(person_features, 0, person_label, axis=0)
            writer.writerow(person_features)

此刻,我们就拥有了一个简单的人脸数据库,对于任意一个待检测的人脸,我们只需要计算其特征值,然后再从人脸数据库中找到一个距离最小的特征即可:

def compare_face_fatures_with_database(database, image_path):
    image = Image.open(image_path)
    image = cv2.cvtColor(np.asarray(image), cv2.COLOR_RGB2BGR)
    faces = detector(image, 1)
    
    campare_results = []
    if len(faces) != 0:
        for i in range(len(faces)):
            face = faces[i]
            shape = predictor(image, faces[0])
            face_descriptor = face_reco_model.compute_face_descriptor(image, shape)
            face_feature_distance_list = []
            for face_data in database:
                # 比对人脸特征,当距离小于 0.4 时认为匹配成功
                dist = get_euclidean_distance(face_descriptor, face_data[1])
                dist = round(dist, 4)

                if dist >= FACES_FATURES_DISTANCE_THRESHOLD:
                    continue

                face_feature_distance_list.append((face_data[0], dist))
            
            # 按距离排序,取最小值进行绘制
            sorted(face_feature_distance_list, key=lambda x:x[1])
            if face_feature_distance_list:
                person_dist = face_feature_distance_list[0][1]
                person_label = face_feature_distance_list[0][0]
                campare_results.append((person_label, person_dist))

    return campare_results

可以注意到,我们会遍历人脸数据库中的每一个特征值,然后计算它和当前人脸特征的欧式距离。接下来,我们会对这些距离进行排序,选取距离最小的一组作为匹配结果,下面的代码片段展示了如何去计算一个欧氏距离:

def get_euclidean_distance(feature_1, feature_2):
    feature_1 = np.array(feature_1)
    feature_2 = np.array(feature_2)
    return np.sqrt(np.sum(np.square(feature_1 - feature_2)))

如下图所示,我们可以输出每一张人脸的名称,以及当前人脸相对于特征均值的距离,经博主测试,这个方案的正确率可以达到 94.58% 左右,是一种相对比较靠谱的做法:

人脸识别效果展示 人脸识别效果展示

不过,这个方案的缺点同样非常明显,即:它必须遍历人脸数据库中的每一条数据,这意味着它的时间复杂度是 O(n)。考虑到现实生活中需要录入的人脸数据可能是成百上千的,这个方案的执行效率注定会越来越低,这迫使我们不得不换一种思路来解决这个问题。重新审视人脸识别这个问题,我们会发现,这其实是一个分类问题,就像我们人为地去给一堆照片贴上标签一样。当然,它并不是一个非此即彼的二分类问题,而是一个多分类的问题。如果你接触过 Scikit Learn,那么,你大概会想到通过某种分类器进行模型训练的思路,这里以支持向量机为例:

# 提取人脸特征和标签
mean_features_list = list(get_mean_features_of_face(faces_dir))
features = list(map(lambda x:x[0], face_encodings_mean_list))
labels = list(map(lambda x:x[1], face_encodings_mean_list))

# 通过支持向量机训练模型
clf = svm.SVC(C=1.0, kernel="linear", gamma='scale', probability=True)
clf.fit(features, labels)

在这种情况下,我们可以利用 joblib.dump()joblib.load() 两个方法对模型进行保存和加载。此时,我们该如何识别一个人脸呢?简单来说,你可以把支持向量机想象成一个线性函数,因此,我们只需要传入待检测图片的特征值,它就可以帮我们计算/预测出人脸的分类。显然,它不需要像前面的方案一样遍历每一条人脸数据,因此,支持向量机这个方案执行效率上要更好一点:

features = get_128d_features_of_face(image_path)
predict_label = clf.predict(features)

当然,世界上没有百分之百完美的方案,支持向量机如果遇到了一张陌生的人脸,它的预测结果就会变得离谱起来,譬如,孙燕姿被识别为堺雅人、堺雅人被识别为胡歌,用鲁迅先生的话讲,屏幕内外充满了快活的空气。虽然,Scikit Learn 里可以用 predict_proba() 方法引入概率来评估结果的准确性,可博主实际使用下来,发现效果并没有好多少。无独有偶,OpenCV 自带的 LBPH 方法,其模型训练和这个思路非常地相似,无非是它接收是一个 Mat 类型的参数:

recognizer = cv2.face.LBPHFaceRecognizer_create()
detector = cv2.CascadeClassifier("haarcascade_frontalface_alt2.xml")

# 获取人脸样本及标签
face_samples = get_images_and_labels(DATASETS_DIR)
faces = list(map(lambda x:x[0], face_samples))
faceIds = list(map(lambda x:x[1], face_samples))

# 模型训练
recognizer.train(faces, np.array(faceIds))
recognizer.save('/train/train_data.yml')

# 模型预测
recognizer.read('/train/train_data.yml')
faceId, confidence = recognizer.predict(image)

它会返回人脸的 Id 以及置信度,我们可以结合这个置信度去做进一步的判断,比如置信度超过 50% 就认为它匹配到了人脸。我一开始提到这个方案的精度达不到要求,就是因为它经常给出错误的预测结果,甚至比支持向量机遇到陌生人脸时还要离谱。所以,对于 OpenCV 官方的这个方案呢,我们知道它是怎么回事就可以了,我个人并不推荐在真实的场景中使用这个方法。当然,OpenCV 后来开始支持 CNN,我们值得更好的人脸识别方案,限于篇幅和精力,这个等以后有机会了再展开说说。

人脸识别运用到视频中的效果展示:献给我永远的歌晨 CP 人脸识别运用到视频中的效果展示:献给我永远的歌晨 CP

本文小结

本文分享了 OpenCVDlib 下的人脸识别的实现,由于 OpenCV 自带的 LBPH 方法识别效果不符合预期,博主不得不使用 Dlib 来实现人脸识别。这里采用的思路是,计算出每一个人物的人脸特征均值,它是一个 128 维的向量,通过计算待检测人脸特征值与已知人脸特征值的欧式距离,来判断两张脸是否为同一张脸,经过测试,该方案的识别率可以达到 94.58% ,是一种相对来说比较靠谱的方案。考虑到这个方案的时间复杂度为 O(n) ,我们不得不考虑效率更高的做法。在这个基础上,我们尝试结合支持向量机做进一步的优化,支持向量机最大的问题时,当它面对一张陌生人脸的时候,其预测结果会变得非常离谱,可是在最好的情况下,它的准确率依然接近 97.04%。作为对照组,博主捎带着介绍了一下 OpenCV 自带的 LBPH 方法如何训练模型,权当帮大家扩展思路,只有此中的成败利钝,只有靠大家自己去取舍啦!最后,听说射雕英雄传又要翻拍啦,我宣布我永远都喜欢胡歌这一版的射雕英雄传,哈哈!