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

推荐订阅源

T
Threatpost
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
T
The Blog of Author Tim Ferriss
S
SegmentFault 最新的问题
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
博客园 - 司徒正美
T
Tailwind CSS Blog
The Cloudflare Blog
The Last Watchdog
The Last Watchdog
PCI Perspectives
PCI Perspectives
博客园 - 聂微东
Stack Overflow Blog
Stack Overflow Blog
TaoSecurity Blog
TaoSecurity Blog
云风的 BLOG
云风的 BLOG
C
Cybersecurity and Infrastructure Security Agency CISA
O
OpenAI News
Recorded Future
Recorded Future
GbyAI
GbyAI
www.infosecurity-magazine.com
www.infosecurity-magazine.com
Y
Y Combinator Blog
D
Darknet – Hacking Tools, Hacker News & Cyber Security
量子位
博客园 - 叶小钗
V
Vulnerabilities – Threatpost
F
Full Disclosure
Recent Announcements
Recent Announcements
Vercel News
Vercel News
S
Schneier on Security
H
Heimdal Security Blog
Cisco Talos Blog
Cisco Talos Blog
V2EX - 技术
V2EX - 技术
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
B
Blog RSS Feed
宝玉的分享
宝玉的分享
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
P
Privacy & Cybersecurity Law Blog
T
Threat Research - Cisco Blogs
G
Google Developers Blog
C
Cyber Attacks, Cyber Crime and Cyber Security
爱范儿
爱范儿
IT之家
IT之家
大猫的无限游戏
大猫的无限游戏
C
Check Point Blog
N
Netflix TechBlog - Medium
S
Security @ Cisco Blogs
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
Microsoft Azure Blog
Microsoft Azure Blog
H
Hackread – Cybersecurity News, Data Breaches, AI and More
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
Cyberwarzone
Cyberwarzone

元视角

.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 个特征点,而基于特征点我们可以对人脸检测/识别做进一步的探索,比如驾驶过程中司机的疲劳检测、表情分析、活体检测等等,而这篇文章主要利用了其中的轮廓点来生成多边形掩膜,来达到抠取人脸的目的。在这个基础上,我们实现了通过照片制作表情包的想法,虽然目前实用性并不强,可我觉得处理图像的这个过程还是特别有意思。以前,有人讲过程序员的三大浪漫,即操作系统、编译原理 和 图像学,这种浪漫大概只有程序员能理解。好了,以上就是这篇博客的全部内容啦,如果大家对人脸的处理还有更好的方案,欢迎大家在评论区积极留言。