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

推荐订阅源

T
Troy Hunt's Blog
人人都是产品经理
人人都是产品经理
MongoDB | Blog
MongoDB | Blog
Stack Overflow Blog
Stack Overflow Blog
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
Microsoft Security Blog
Microsoft Security Blog
F
Fortinet All Blogs
博客园 - 三生石上(FineUI控件)
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
P
Proofpoint News Feed
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
博客园 - Franky
H
Hackread – Cybersecurity News, Data Breaches, AI and More
Martin Fowler
Martin Fowler
V
Visual Studio Blog
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Microsoft Azure Blog
Microsoft Azure Blog
云风的 BLOG
云风的 BLOG
G
Google Developers Blog
WordPress大学
WordPress大学
腾讯CDC
J
Java Code Geeks
T
The Blog of Author Tim Ferriss
博客园 - 司徒正美
博客园 - 【当耐特】
小众软件
小众软件
Attack and Defense Labs
Attack and Defense Labs
宝玉的分享
宝玉的分享
Google DeepMind News
Google DeepMind News
罗磊的独立博客
Schneier on Security
Schneier on Security
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
量子位
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
O
OpenAI News
TaoSecurity Blog
TaoSecurity Blog
H
Heimdal Security Blog
Blog — PlanetScale
Blog — PlanetScale
Forbes - Security
Forbes - Security
B
Blog
I
InfoQ
D
DataBreaches.Net
月光博客
月光博客
T
Tenable Blog
S
Security Affairs
C
Check Point Blog
T
Threatpost
C
Cybersecurity and Infrastructure Security Agency CISA
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
www.infosecurity-magazine.com
www.infosecurity-magazine.com

元视角

.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 认证 - 元视角 似花还似非花 - 元视角 视频是不能 P 的系列:使用 Dlib 实现人脸识别 - 元视角 浅议分布式链路追踪与日志的整合 - 元视角 关于 Git 大文件上传这件小事 - 元视角 .NET 进程内队列 Channel 的入门与应用 - 元视角 使用 Fody 实现 .NET 的静态编织 - 元视角 .NET Core + ELK 搭建可视化日志分析平台(下) - 元视角 聊一聊前端图片懒加载背后的故事 - 元视角 支持外部链接跳转的 Vue Router 扩展实现 - 元视角 不得不说的 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 的系列:OpenCV 和 Dlib 实现表情包 - 元视角
飞鸿踏雪 · 2022-07-02 · via 元视角

2020 年年底的时候,博主曾心血来潮地开启过一个系列:视频是不能 P 的,其灵感则是来源于互联网上的一个梗,即:视频不能 P 所以是真的。不过,在一个美颜盛行的时代,辨别真伪实在是一件奢侈的事情,在各种深度学习框架光环的加持下,在视频中实现“改头换面”已然不再是新鲜事儿,AI 换脸风靡一时的背后,带来是关乎隐私和伦理的一系列问题,你越来越难以确认,屏幕对面的那个到底是不是真实的人类。古典小说《红楼梦》里的太虚幻境,其牌坊上有幅对联写道,“假作真时真亦假,无为有处有还无”。果然,在这个亦真亦幻的世界里,哪里还有什么东西是不能 PS 的呢?在“鸽”了很久很久之后,博主决定要来更新这个系列啦,让我们继续以 OpenCV 作为起点,来探索那些好玩、有趣的视频/图像处理思路,这一次呢,我们来聊聊 OpenCV、Dlib 和 表情包,希望寓教于乐的方式能让大家感受到编程的快乐!

环境准备

python -m pip install opencv-python
python -m pip install opencv-contrib-python
python -m pip install Pillow
python -m pip install numpy
python -m pip install imutils
python -m pip install dlib

请注意,如果通过 pip 安装 dlib 不大顺利,你可以到 https://github.com/sachadee/Dlib 这个仓库中下载对应的 .whl 文件。例如,博主使用的是 64 位的 Windows 系统,而我的 Python 版本是 3.7,因此,我下载的是 dlib-19.22.99-cp37-cp37m-win_amd64.whl 这个文件。此时,我们可以用下面的方式来安装 dlib

python -m pip install dlib-19.22.99-cp37-cp37m-win_amd64.whl

除此以外,我们还需要下载 dlib 所需的模型文件,下载地址为:http://dlib.net/files/shape_predictor_68_face_landmarks.dat.bz2。下载该文件并解压后,可以得到一个 .dat 格式的文件,这就是我们用来做人脸识别的模型文件,即人脸的 68 个特征点检测,这个我们会在下面做更进一步的说明。

初探 Dlib

其实,如果是简单的人脸检测,OpenCV 完全足矣。如果你读过我这个系列的第一篇文章,就会知道 OpenCV 人脸检测的实际效果如何。事实上,它会使用一个矩形来表示检测到的人脸范围,这里,我还是用堺雅人主演的电视剧《半泽直树》来作为说明,可以注意到每一个矩形对应着一张人脸:

OpenCV人脸检测效果展示 OpenCV人脸检测效果展示

可是,如果我们希望对这些“人脸”进行比对以期望达到人脸识别的目的,这个精度对我们来说就显得捉襟见肘啦!为了解决这个问题, dlib 中使用的是一种被称之为 68 face landmarks 的方法,简单来说,就是用 68 个特性点来定位一个人的面目五官,为了方便大家理解,我们一起来看下面的例子:

# 初始化 detector 和 predictor
detector = dlib.get_frontal_face_detector()
predictor = dlib.shape_predictor('shape_predictor_68_face_landmarks.dat')

# 载入人脸图片
image = cv2.imread("./faces/face1.jpg")

# 检测人脸 & 绘制特征点
face_rects = detector(image, 0)
for index, face in enumerate(face_rects):
    shape = predictor(image, face_rects[index])
    shape = face_utils.shape_to_np(shape)
    for idx, (x,y) in enumerate():
        cv2.circle(img, (x, y), 1, (0, 0, 255), -1)
        cv2.putText(img, str(idx + 1), (x,y), cv2.FONT_HERSHEY_SIMPLEX, 0.2, (255, 255, 255), 1, cv2.LINE_AA)

cv2.imshow("image",image)

此时,我们可以得到下面的结果:

使用 dlib 进行人脸检测 使用 dlib 进行人脸检测

可以注意到, dlib 可以进一步识别出人脸中的特征点,譬如眼睛、眉毛、鼻子、嘴巴等等,这让我们有了更多可以探索的乐趣。举个例子,司机在道路上疲劳驾驶,有可能会引发交通事故,如果我们能实时分析司机的面目表情,就可以为这个世界做出一点小小的努力。一个常见的思路是计算“眼睛”部分的宽高比,因为当人眼睛闭合的时候,相当于“眼睛”部分纵向的特征点间距变小。再比如,我们可以通过嘴角和眉毛的弧度来“揣测”一个人的喜怒哀乐,让计算机不再是冷冰冰地一台机器。也许,你听说过“三庭五眼”这套理论,所以,从某种意义上来讲,这个思路还可以扩展到“看相”这个方向。果然,你可以永远相信神仙姐姐的颜值呢…

移花接木

OK,在对 dlib 有一个初步印象以后,我们来说说,在今天这篇博客里,博主到底想做一件什么样的事情。如下图所示,我们希望借助 dlib 逐步地“抠取”出人物表情,最终再和这个经典的“熊猫人”融合在一起,从而达到从某任意图片生成表情包的目的。当然啦,这个想法并不容易实现,因为你从这个图片中就可以看到它的最终效果。图中使用的素材出自半泽直树第二季,当半泽直树遇上对手黑崎骏一,瞬间碰撞出一种惺惺相惜的 CP 感,作为雅人叔的忠实粉丝,还有什么比做成表情包更直抒胸臆的表达方式呢?

制作表情包的过程说明 制作表情包的过程说明

68 个特征点的绘制,在初探 dlib 的环节已经讲过,这里就不再赘述啦!这里,我们先来说说人脸的矩形范围如何获得,可能有读者朋友会问,这个矩形的作用是什么?其实,不管我们用怎样不规则的一个多边形来裁切图片,我们最终得到的一定是具备长和宽的矩形。因此,这个矩形就是帮我们定位整个脸的范围。下面是对应的代码片段:

def get_facemark_rect(shape):

    shape2np = face_utils.shape_to_np(shape)
    (x0, y0) = shape2np[0]

    minX = maxX = x0
    minY = maxY = y0

    for (x,y) in shape2np:
        if x < minX:
            minX = x
        if x >= maxX:
            maxX = x
        if y < minY:
            minY = y
        if y >= maxY:
            maxY = y

    return (minX, minY, maxX - minX, maxY - minY)

可以注意到,代码非常地朴实无华,只需要分别找到 xy 的最小值/最大值,就可以确定人脸的矩形范围。如下图所示,红色线条呈现出的即为人脸的矩形范围。作为对比,我们同时绘制了 dlib 本身自带的一个矩形范围,这个矩形范围大致等同于 OpenCV 人脸检测的效果:

获得人脸的矩形范围 获得人脸的矩形范围

事实上,如果你回头去看我绘制特征点这一部分的代码,会发现这里有一个变量 face 一直没有用到,它实际上是 dlib里定义的一种类型 rectangle,从命名上我们就可以知道,这是一个表示矩形的数据结构。在 OpenCV 中,我们可以使用下面的代码片段来绘制一个矩形:

# 使用自己计算出的矩形范围
(x, y, w, h) = get_facemark_rect(shape)
cv2.rectangle(img, (x, y), (x + w, y + h), (0, 0, 255), 1)

# 使用 dlib 自带的矩形范围
x, y, w, h = face.left(), face.top(), face.right() - face.left(), face.bottom() - face.top()
cv2.rectangle(img, (x, y), (x + w, y + h), (0, 255, 0), 1)

考虑到,博主这里需要的是一个相对精确的面部的范围,大家可以结合自己的需要,选择其中一种即可。接下来,我们来考虑如何根据这 68 个特征点进行“抠脸”,为此,我们只需要找到那些表示面部轮廓的点即可,通过上面的图形,我们可以看出,脸的下半部分轮廓点为 1 到 17,上半部分轮廓点为 18 到 27。因此,我们只需要用这些点构造一个多边形即可:

def create_face_mask(image, facemark):
    shape, face = facemark
    shape2np = face_utils.shape_to_np(shape)
    mask = np.zeros(image.shape, dtype=np.uint8)
    points = np.concatenate([shape2np[0:16], shape2np[26:17:-1]])
    cv2.fillPoly(img=mask, pts=[points], color=(255,) * image.shape[2])
    return mask

这里,我们首先创建了一个和原图片同样大小的图片 mask , 然后调用 fillPoly() 对多边形进行填充。这样,我们就得到了一个和人脸轮廓完全一致的掩膜:

利用人脸轮廓创建掩膜 利用人脸轮廓创建掩膜

OpenCV 中,可以通过 bitwise_and() 函数来对两张图片进行“叠加”, 而在这个示例中,原图片为 image,掩膜图片为 mask,因此,对应的代码片段如下:

image = cv2.bitwise_and(image, mask)

此时,我们可以得到下面的结果,可以注意到,雅人叔的脸已经被我“抠取”出来了,哈哈!

成功抠取出雅人叔的脸 成功抠取出雅人叔的脸

当然啦,这个图片对我们来说太大了!毕竟,我们需要的是雅人叔的这张脸,而不是这个黑乎乎的背景。此时此刻,前面我们计算出来的矩形范围就派上用场啦,因此,我们对图片做一次裁切,OpenCV中裁切图片是非常容易的,我们只需要像对待数组一样指定一个范围:

(x, y, w, h) = get_facemark_rect(shape)
image = image[y:y + h, x:x + w]

当然,这里你会发现一个问题,裁切出来的图片带着黑色背景,这显然不利于我们和“熊猫人”进行融合。这里,博主提供的解决方案是:创建一张同样大小的图片,再把除了黑色以外的颜色全部复制过来,因为黑色再在 NumPy 中使用一个元素全部为 0 的数组来表示。那么,这样就简单多啦:

def create_white_image(image):
    white = np.zeros(image.shape, dtype=np.uint8)
    for i in range(0, image.shape[0]):
        for j in range(0, image.shape[1]):
            white[ i, j ] = np.uint8(255)

    return white

# 创建一张背景色为白色的图片,复制除黑色以外的每一个像素
white = create_white_image(image)
for i in range(0, image.shape[0]):
    for j in range(0, image.shape[1]):
        if ((image[i, j] != 0).all()):
            white[i, j] = image[i, j]

接下来,为了让雅人叔的脸更贴近“熊猫人”的气质,我们对图片运用一次 threshold() 函数。事实上,到这一步为止,博主一直没有找到特别好的方法,因为人脸上可能会有阴影造成的深浅变化,这样就没有办法 100% 的转化为黑白图片。除了这种固定阈值的方案,博主同样尝试自适应阈值的方案,即 adaptiveThreshold() 函数。不过,从最终效果来看,自适应阈值的方案表现并不好,下面是这两种方案的效果对比:

固定阈值和自适应阈值效果对比 固定阈值和自适应阈值效果对比

代码层面没什么悬念,转灰度图再调用相应的函数即可:

# 转灰度图
image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)

# 固定阈值 + THRESH_BINARY
_, image1 = cv2.threshold(image, 45, 255, cv2.THRESH_BINARY)

# 自适应阈值 + ADAPTIVE_THRESH_GAUSSIAN_C
image2 = cv2.adaptiveThreshold(image, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY,9,7)

# 自适应阈值 + ADAPTIVE_THRESH_MEAN_C
image3 = cv2.adaptiveThreshold(image, 255, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY,9,7)

如果抛开最终呈现出来的效果的好坏不谈,到这里我们勉强算是达到了预期目的,我们将这个结果以图片的形式保存下来。接下来,我们只需要把雅人叔的脸和“熊猫人”组合在一起即可:

# 读取熊猫人和人脸图片
panda = Image.open("PandaMan.jpg")
face = Image.open("face.jpg")

w_g, h_g = bg.size
w_f, h_f = face.size
ratio = h_f / w_f

# 根据熊猫人的大小对人脸进行缩放
w_new = int(w_g * 0.4)
h_new = int(ratio * w_new)

resized = face.resize((w_new, h_new),Image.ANTIALIAS)

# 计算左上角坐标
left = int((w_g - w_new) / 2)
top = int((h_g / 2 - h_new) / 2)

# 把人脸贴到指定位置
panda.paste(resized, (left, top, left + w_new, top + h_new))
panda.save('output.jpg')

那么,对不起了,雅人叔,我实在是太喜欢你在不同的剧里呈现出来的形象,半泽直树、古美门、德川家定、山南敬助、真田信繁…,每一个角色都能让人惊呼这居然是同一个人,更不用说,有那种根植于文学和话剧的清澈感,能在演绎角色的同时加入个人的理解。雅人叔,请变成我的表情包吧,哈哈!

最终合成的表情包效果 最终合成的表情包效果

本文小结

其实,一开始在规划这篇文章的时候,我原本是打算写一写“换脸”这个话题的。可是后来发现,换脸涉及的那些知识,对现在的我而言,实在是一座难以逾越的高山,所以,最后不得不退而求其次,写一个相对简单的话题,可即使这样,写这篇文章还是花费了我挺长时间,因为一直没有找到更好的方法来处理人脸。所以,这篇文章写到这种程度,其实是因为我不想一直拖延下去。简单总结一下,这篇文章介绍了 dlib 的用法,相对于 OpenCV 而言,它可以拿到人脸的 68 个特征点,而基于特征点我们可以对人脸检测/识别做进一步的探索,比如驾驶过程中司机的疲劳检测、表情分析、活体检测等等,而这篇文章主要利用了其中的轮廓点来生成多边形掩膜,来达到抠取人脸的目的。在这个基础上,我们实现了通过照片制作表情包的想法,虽然目前实用性并不强,可我觉得处理图像的这个过程还是特别有意思。以前,有人讲过程序员的三大浪漫,即操作系统、编译原理 和 图像学,这种浪漫大概只有程序员能理解。好了,以上就是这篇博客的全部内容啦,如果大家对人脸的处理还有更好的方案,欢迎大家在评论区积极留言。