作为 Write for DOnations 计划的一部分,作者选择了 Girls Who Code 来接受捐赠。
介绍
计算机视觉是计算机科学的一个子领域,旨在从图像和视频中提取更高层次的理解。 该领域包括目标检测、图像恢复(矩阵补全)和光流等任务。 计算机视觉为自动驾驶汽车原型、无员工杂货店、有趣的 Snapchat 过滤器和移动设备的面部验证器等技术提供支持。
在本教程中,您将在使用预训练模型构建 Snapchat 式狗过滤器时探索计算机视觉。 对于那些不熟悉 Snapchat 的人来说,这个过滤器会检测到你的脸,然后在上面叠加一个狗面具。 然后,您将训练一个面部情绪分类器,以便过滤器可以根据情绪挑选狗面具,例如表示快乐的柯基犬或表示悲伤的哈巴狗。 在此过程中,您还将探索普通最小二乘法和计算机视觉中的相关概念,这将使您了解机器学习的基础知识。
在学习本教程时,您将使用计算机视觉库 OpenCV
,线性代数实用程序使用 numpy
,绘图使用 matplotlib
。 在构建计算机视觉应用程序时,您还将应用以下概念:
- 普通最小二乘法作为回归和分类技术。
- 随机梯度神经网络的基础知识。
虽然没有必要完成本教程,但如果您熟悉这些数学概念,您会发现更容易理解一些更详细的解释:
- 基本线性代数概念:标量、向量和矩阵。
- 基础微积分:如何进行导数。
您可以在 https://github.com/do-community/emotion-based-dog-filter 找到本教程的完整代码。
让我们开始吧。
先决条件
要完成本教程,您将需要以下内容:
- 具有至少 1GB RAM 的 Python 3 本地开发环境。 您可以按照如何为Python 3安装和设置本地编程环境来配置您需要的一切。
- 用于实时图像检测的工作网络摄像头。
第 1 步 - 创建项目并安装依赖项
让我们为这个项目创建一个工作区并安装我们需要的依赖项。 我们将调用我们的工作区 DogFilter
:
mkdir ~/DogFilter
导航到 DogFilter
目录:
cd ~/DogFilter
然后为项目创建一个新的 Python 虚拟环境:
python3 -m venv dogfilter
激活您的环境。
source dogfilter/bin/activate
提示发生变化,表明环境处于活动状态。 现在安装 PyTorch,这是我们将在本教程中使用的 Python 深度学习框架。 安装过程取决于您使用的操作系统。
在 macOS 上,使用以下命令安装 Pytorch:
python -m pip install torch==0.4.1 torchvision==0.2.1
在 Linux 上,使用以下命令:
pip install http://download.pytorch.org/whl/cpu/torch-0.4.1-cp35-cp35m-linux_x86_64.whl pip install torchvision
对于 Windows,请使用以下命令安装 Pytorch:
pip install http://download.pytorch.org/whl/cpu/torch-0.4.1-cp35-cp35m-win_amd64.whl pip install torchvision
现在为 OpenCV
和 numpy
安装预打包的二进制文件,它们分别是计算机视觉和线性代数库。 前者提供图像旋转等实用程序,后者提供矩阵求逆等线性代数实用程序。
python -m pip install opencv-python==3.4.3.18 numpy==1.14.5
最后,为我们的资产创建一个目录,该目录将保存我们将在本教程中使用的图像:
mkdir assets
安装依赖项后,让我们构建过滤器的第一个版本:面部检测器。
第 2 步 — 构建人脸检测器
我们的第一个目标是检测图像中的所有面孔。 我们将创建一个脚本,该脚本接受单个图像并输出带有框的人脸的带注释图像。
幸运的是,我们可以使用 预训练模型 ,而不是编写自己的人脸检测逻辑。 我们将建立一个模型,然后加载预训练的参数。 OpenCV 通过提供这两者使这变得容易。
OpenCV 在其源代码中提供了模型参数。 但我们需要本地安装的 OpenCV 的绝对路径才能使用这些参数。 由于该绝对路径可能会有所不同,因此我们将下载自己的副本并将其放在 assets
文件夹中:
wget -O assets/haarcascade_frontalface_default.xml https://github.com/opencv/opencv/raw/master/data/haarcascades/haarcascade_frontalface_default.xml
-O
选项将目的地指定为 assets/haarcascade_frontalface_default.xml
。 第二个参数是源 URL。
我们将从Pexels(CC0,链接到原始图像)中检测以下图像中的所有人脸。
首先,下载图像。 以下命令将下载的图像保存为 assets
文件夹中的 children.png
:
wget -O assets/children.png https://assets.digitalocean.com/articles/python3_dogfilter/CfoBWbF.png
为了检查检测算法是否有效,我们将在单个图像上运行它并将生成的带注释图像保存到磁盘。 为这些带注释的结果创建一个 outputs
文件夹。
mkdir outputs
现在为面部检测器创建一个 Python 脚本。 使用 nano
或您喜欢的文本编辑器创建文件 step_1_face_detect
:
nano step_2_face_detect.py
将以下代码添加到文件中。 此代码导入 OpenCV,其中包含图像实用程序和人脸分类器。 其余代码是典型的 Python 程序样板。
step_2_face_detect.py
"""Test for face detection""" import cv2 def main(): pass if __name__ == '__main__': main()
现在将 main
函数中的 pass
替换为以下代码,该代码使用您下载到 assets
文件夹的 OpenCV 参数初始化人脸分类器:
step_2_face_detect.py
def main(): # initialize front face classifier cascade = cv2.CascadeClassifier("assets/haarcascade_frontalface_default.xml")
接下来,添加此行以加载图像 children.png
。
step_2_face_detect.py
frame = cv2.imread('assets/children.png')
然后添加此代码以将图像转换为黑白图像,因为分类器是在黑白图像上训练的。 为此,我们转换为灰度,然后离散化直方图:
step_2_face_detect.py
# Convert to black-and-white gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) blackwhite = cv2.equalizeHist(gray)
然后使用 OpenCV 的 detectMultiScale 函数检测图像中的所有人脸。
step_2_face_detect.py
rects = cascade.detectMultiScale( blackwhite, scaleFactor=1.3, minNeighbors=4, minSize=(30, 30), flags=cv2.CASCADE_SCALE_IMAGE)
scaleFactor
指定图像沿每个维度缩小的程度。minNeighbors
表示一个候选矩形需要保留多少个相邻矩形。minSize
是允许检测到的最小物体尺寸。 小于此的对象将被丢弃。
返回类型是 tuples 的列表,其中每个元组有四个数字,分别表示矩形的最小 x、最小 y、宽度和高度。
遍历所有检测到的对象并使用 cv2.rectangle 将它们绘制在绿色图像上:
step_2_face_detect.py
for x, y, w, h in rects: cv2.rectangle(frame, (x, y), (x + w, y + h), (0, 255, 0), 2)
- 第二个和第三个参数是矩形的对角。
- 第四个参数是要使用的颜色。
(0, 255, 0)
对应于我们的 RGB 颜色空间的绿色。 - 最后一个参数表示我们线的宽度。
最后,将带有边界框的图像写入 outputs/children_detected.png
处的新文件:
step_2_face_detect.py
cv2.imwrite('outputs/children_detected.png', frame)
您完成的脚本应如下所示:
step_2_face_detect.py
"""Tests face detection for a static image.""" import cv2 def main(): # initialize front face classifier cascade = cv2.CascadeClassifier( "assets/haarcascade_frontalface_default.xml") frame = cv2.imread('assets/children.png') # Convert to black-and-white gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) blackwhite = cv2.equalizeHist(gray) rects = cascade.detectMultiScale( blackwhite, scaleFactor=1.3, minNeighbors=4, minSize=(30, 30), flags=cv2.CASCADE_SCALE_IMAGE) for x, y, w, h in rects: cv2.rectangle(frame, (x, y), (x + w, y + h), (0, 255, 0), 2) cv2.imwrite('outputs/children_detected.png', frame) if __name__ == '__main__': main()
保存文件并退出编辑器。 然后运行脚本:
python step_2_face_detect.py
打开 outputs/children_detected.png
。 您将看到以下图像,其中显示了用框勾勒出的面:
在这一点上,你有一个工作面检测器。 它接受图像作为输入,并在图像中的所有面部周围绘制边界框,输出带注释的图像。 现在让我们将同样的检测应用到实时摄像机源。
第 3 步 - 链接相机源
下一个目标是将计算机的摄像头连接到面部检测器。 您无需在静态图像中检测人脸,而是从计算机摄像头检测所有人脸。 您将收集相机输入,检测并注释所有面部,然后将注释图像显示给用户。 您将从步骤 2 中的脚本继续,因此首先复制该脚本:
cp step_2_face_detect.py step_3_camera_face_detect.py
然后在编辑器中打开新脚本:
nano step_3_camera_face_detect.py
您将使用 OpenCV 官方文档中的 测试脚本 中的一些元素来更新 main
函数。 首先初始化一个 VideoCapture
对象,该对象设置为从您的计算机摄像头捕获实时信息。 将其放在 main
函数的开头,在函数中的其他代码之前:
step_3_camera_face_detect.py
def main(): cap = cv2.VideoCapture(0) ...
从定义 frame
的行开始,缩进所有现有代码,将所有代码放在 while
循环中。
step_3_camera_face_detect.py
while True: frame = cv2.imread('assets/children.png') ... for x, y, w, h in rects: cv2.rectangle(frame, (x, y), (x + w, y + h), (0, 255, 0), 2) cv2.imwrite('outputs/children_detected.png', frame)
替换在 while
循环开始处定义 frame
的行。 您现在不是从磁盘上的图像读取,而是从相机读取:
step_3_camera_face_detect.py
while True: # frame = cv2.imread('assets/children.png') # DELETE ME # Capture frame-by-frame ret, frame = cap.read()
替换 while
循环末尾的 cv2.imwrite(...)
行。 您无需将图像写入磁盘,而是将带注释的图像显示回用户屏幕:
step_3_camera_face_detect.py
cv2.imwrite('outputs/children_detected.png', frame) # DELETE ME # Display the resulting frame cv2.imshow('frame', frame)
另外,添加一些代码来监视键盘输入,以便您可以停止程序。 检查用户是否点击了 q
字符,如果是,则退出应用程序。 在 cv2.imshow(...)
之后添加以下内容:
step_3_camera_face_detect.py
... cv2.imshow('frame', frame) if cv2.waitKey(1) & 0xFF == ord('q'): break ...
cv2.waitkey(1)
行将程序暂停 1 毫秒,以便将捕获的图像显示回给用户。
最后,释放捕获并关闭所有窗口。 将它放在 while
循环之外以结束 main
函数。
step_3_camera_face_detect.py
... while True: ... cap.release() cv2.destroyAllWindows()
您的脚本应如下所示:
step_3_camera_face_detect.py
"""Test for face detection on video camera. Move your face around and a green box will identify your face. With the test frame in focus, hit `q` to exit. Note that typing `q` into your terminal will do nothing. """ import cv2 def main(): cap = cv2.VideoCapture(0) # initialize front face classifier cascade = cv2.CascadeClassifier( "assets/haarcascade_frontalface_default.xml") while True: # Capture frame-by-frame ret, frame = cap.read() # Convert to black-and-white gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) blackwhite = cv2.equalizeHist(gray) # Detect faces rects = cascade.detectMultiScale( blackwhite, scaleFactor=1.3, minNeighbors=4, minSize=(30, 30), flags=cv2.CASCADE_SCALE_IMAGE) # Add all bounding boxes to the image for x, y, w, h in rects: cv2.rectangle(frame, (x, y), (x + w, y + h), (0, 255, 0), 2) # Display the resulting frame cv2.imshow('frame', frame) if cv2.waitKey(1) & 0xFF == ord('q'): break # When everything done, release the capture cap.release() cv2.destroyAllWindows() if __name__ == '__main__': main()
保存文件并退出编辑器。
现在运行测试脚本。
python step_3_camera_face_detect.py
这将激活您的相机并打开一个显示您的相机源的窗口。 你的脸会实时被一个绿色方块包围:
注意:如果你发现你必须保持非常静止才能工作,房间里的照明可能不够充足。 试着搬到一个明亮的房间,那里你和你的背景有很高的对比度。 另外,避免靠近头部的强光。 例如,如果您背对着太阳,则此过程可能效果不佳。
我们的下一个目标是获取检测到的人脸并在每个人脸上叠加狗面具。
第 4 步 - 构建狗过滤器
在我们构建过滤器本身之前,让我们探索一下图像是如何用数字表示的。 这将为您提供修改图像并最终应用狗过滤器所需的背景。
让我们看一个例子。 我们可以用数字构造一个黑白图像,其中0
对应黑色,1
对应白色。
关注 1 和 0 之间的分界线。 你看到什么形状?
0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 1 1 1 0 0 0 0 0 1 1 1 1 1 0 0 0 0 0 1 1 1 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0
图像是钻石。 如果将此值的 矩阵 保存为图像。 这为我们提供了以下图片:
我们可以使用 0 到 1 之间的任何值,例如 0.1、0.26 或 0.74391。 接近 0 的数字更暗,接近 1 的数字更亮。 这使我们能够表示白色、黑色和任何灰色阴影。 这对我们来说是个好消息,因为我们现在可以使用 0、1 以及介于两者之间的任何值来构造任何灰度图像。 例如,考虑以下内容。 你能说出它是什么吗? 同样,每个数字对应一个像素的颜色。
1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 1 1 1 1 1 1 0 0 .4 .4 .4 .4 0 0 1 1 1 0 .4 .4 .5 .4 .4 .4 .4 .4 0 1 1 0 .4 .5 .5 .5 .4 .4 .4 .4 0 1 0 .4 .4 .4 .5 .4 .4 .4 .4 .4 .4 0 0 .4 .4 .4 .4 0 0 .4 .4 .4 .4 0 0 0 .4 .4 0 1 .7 0 .4 .4 0 0 0 1 0 0 0 .7 .7 0 0 0 1 0 1 0 1 1 1 0 0 .7 .7 .4 0 1 1 0 .7 1 1 1 .7 .7 .7 .7 0 1 1 1 0 0 .7 .7 .7 .7 0 0 1 1 1 1 1 1 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
重新渲染为图像,您现在可以看出这实际上是一个精灵球:
您现在已经了解了如何用数字表示黑白和灰度图像。 为了引入颜色,我们需要一种编码更多信息的方法。 图像的高度和宽度表示为 h x w
。
在当前的灰度表示中,每个像素是一个介于 0 和 1 之间的值。 我们可以等效地说我们的图像具有 h x w x 1
的尺寸。 换句话说,我们图像中的每个 (x, y)
位置只有一个值。
对于颜色表示,我们使用 0 到 1 之间的三个值来表示每个像素的颜色。 一个数字对应“红色度数”,一个数字对应“绿色度数”,最后一个数字对应“蓝色度数”。 我们称之为 RGB 颜色空间 。 这意味着对于我们图像中的每个 (x, y)
位置,我们有三个值 (r, g, b)
。 结果,我们的图像现在是 h x w x 3
:
这里,每个数字的范围是 0 到 255,而不是 0 到 1,但思路是一样的。 不同的数字组合对应不同的颜色,如深紫色(102, 0, 204)
或亮橙色(255, 153, 51)
。 要点如下:
- 每个图像将表示为一个具有三个维度的数字框:高度、宽度和颜色通道。 直接操作这个数字框就相当于操作图像。
- 我们也可以将这个盒子展平,变成一个数字列表。 这样,我们的图像就变成了一个向量。 稍后,我们会将图像称为矢量。
既然您了解了图像是如何以数字方式表示的,那么您就可以开始将狗面具应用于面部了。 要应用狗遮罩,您将用非白色狗遮罩像素替换子图像中的值。 首先,您将使用单个图像。 从您在第 2 步中使用的图像下载这张脸的裁剪图。
wget -O assets/child.png https://assets.digitalocean.com/articles/python3_dogfilter/alXjNK1.png
此外,下载以下狗面具。 本教程中使用的狗面具是我自己的图纸,现在根据 CC0 许可证发布到公共领域。
用 wget
下载这个:
wget -O assets/dog.png https://assets.digitalocean.com/articles/python3_dogfilter/ED32BCs.png
创建一个名为 step_4_dog_mask_simple.py
的新文件,它将保存将狗面具应用于面部的脚本的代码:
nano step_4_dog_mask_simple.py
为 Python 脚本添加以下样板并导入 OpenCV 和 numpy
库:
step_4_dog_mask_simple.py
"""Test for adding dog mask""" import cv2 import numpy as np def main(): pass if __name__ == '__main__': main()
将 main
函数中的 pass
替换为将原始图像和狗蒙版加载到内存中的这两行代码。
step_4_dog_mask_simple.py
... def main(): face = cv2.imread('assets/child.png') mask = cv2.imread('assets/dog.png')
接下来,给孩子戴上狗面具。 逻辑比我们之前所做的更复杂,所以我们将创建一个名为 apply_mask
的新函数来模块化我们的代码。 在加载图像的两行之后,添加调用 apply_mask
函数的这一行:
step_4_dog_mask_simple.py
... face_with_mask = apply_mask(face, mask)
创建一个名为 apply_mask
的新函数并将其放在 main
函数上方:
step_4_dog_mask_simple.py
... def apply_mask(face: np.array, mask: np.array) -> np.array: """Add the mask to the provided face, and return the face with mask.""" pass def main(): ...
此时,您的文件应如下所示:
step_4_dog_mask_simple.py
"""Test for adding dog mask""" import cv2 import numpy as np def apply_mask(face: np.array, mask: np.array) -> np.array: """Add the mask to the provided face, and return the face with mask.""" pass def main(): face = cv2.imread('assets/child.png') mask = cv2.imread('assets/dog.png') face_with_mask = apply_mask(face, mask) if __name__ == '__main__': main()
让我们构建 apply_mask
函数。 我们的目标是将面膜敷在孩子的脸上。 但是,我们需要保持狗面具的纵横比。 为此,我们需要明确计算我们的狗面具的最终尺寸。 在 apply_mask
函数中,将 pass
替换为提取两个图像的高度和宽度的这两行代码:
step_4_dog_mask_simple.py
... mask_h, mask_w, _ = mask.shape face_h, face_w, _ = face.shape
接下来,确定需要“进一步缩小”的维度。 准确地说,我们需要两个约束中更严格的一个。 将此行添加到 apply_mask
函数:
step_4_dog_mask_simple.py
... # Resize the mask to fit on face factor = min(face_h / mask_h, face_w / mask_w)
然后通过将此代码添加到函数来计算新形状:
step_4_dog_mask_simple.py
... new_mask_w = int(factor * mask_w) new_mask_h = int(factor * mask_h) new_mask_shape = (new_mask_w, new_mask_h)
这里我们将数字转换为整数,因为 resize
函数需要整数维度。
现在添加此代码以将狗面具调整为新形状:
step_4_dog_mask_simple.py
... # Add mask to face - ensure mask is centered resized_mask = cv2.resize(mask, new_mask_shape)
最后,将图像写入磁盘,以便在运行脚本后仔细检查调整大小的狗面具是否正确:
step_4_dog_mask_simple.py
cv2.imwrite('outputs/resized_dog.png', resized_mask)
完成的脚本应如下所示:
step_4_dog_mask_simple.py
"""Test for adding dog mask""" import cv2 import numpy as np def apply_mask(face: np.array, mask: np.array) -> np.array: """Add the mask to the provided face, and return the face with mask.""" mask_h, mask_w, _ = mask.shape face_h, face_w, _ = face.shape # Resize the mask to fit on face factor = min(face_h / mask_h, face_w / mask_w) new_mask_w = int(factor * mask_w) new_mask_h = int(factor * mask_h) new_mask_shape = (new_mask_w, new_mask_h) # Add mask to face - ensure mask is centered resized_mask = cv2.resize(mask, new_mask_shape) cv2.imwrite('outputs/resized_dog.png', resized_mask) def main(): face = cv2.imread('assets/child.png') mask = cv2.imread('assets/dog.png') face_with_mask = apply_mask(face, mask) if __name__ == '__main__': main()
保存文件并退出编辑器。 运行新脚本:
python step_4_dog_mask_simple.py
在 outputs/resized_dog.png
处打开图像以仔细检查蒙版是否已正确调整大小。 它将与本节前面显示的狗面具相匹配。
现在将狗面具添加到孩子身上。 再次打开step_4_dog_mask_simple.py
文件,返回apply_mask
函数:
nano step_4_dog_mask_simple.py
首先,从 apply_mask
函数中删除写入调整大小掩码的代码行,因为您不再需要它:
cv2.imwrite('outputs/resized_dog.png', resized_mask) # delete this line ...
取而代之的是,应用您从本节开始的图像表示知识来修改图像。 首先制作子图像的副本。 将此行添加到 apply_mask
函数:
step_4_dog_mask_simple.py
... face_with_mask = face.copy()
接下来,找到狗面具不是白色或接近白色的所有位置。 为此,请检查所有颜色通道中的像素值是否小于 250,因为我们预计接近白色的像素位于 [255, 255, 255]
附近。 添加此代码:
step_4_dog_mask_simple.py
... non_white_pixels = (resized_mask < 250).all(axis=2)
此时,狗图像最多与子图像一样大。 我们想将狗图像居中在脸上,因此通过将以下代码添加到 apply_mask
来计算使狗图像居中所需的偏移量:
step_4_dog_mask_simple.py
... off_h = int((face_h - new_mask_h) / 2) off_w = int((face_w - new_mask_w) / 2)
将狗图像中的所有非白色像素复制到子图像中。 由于子图像可能比狗图像大,我们需要取子图像的一个子集:
step_4_dog_mask_simple.py
face_with_mask[off_h: off_h+new_mask_h, off_w: off_w+new_mask_w][non_white_pixels] = \ resized_mask[non_white_pixels]
然后返回结果:
step_4_dog_mask_simple.py
return face_with_mask
在 main
函数中,添加此代码以将 apply_mask
函数的结果写入输出图像,以便您可以手动仔细检查结果:
step_4_dog_mask_simple.py
... face_with_mask = apply_mask(face, mask) cv2.imwrite('outputs/child_with_dog_mask.png', face_with_mask)
您完成的脚本将如下所示:
step_4_dog_mask_simple.py
"""Test for adding dog mask""" import cv2 import numpy as np def apply_mask(face: np.array, mask: np.array) -> np.array: """Add the mask to the provided face, and return the face with mask.""" mask_h, mask_w, _ = mask.shape face_h, face_w, _ = face.shape # Resize the mask to fit on face factor = min(face_h / mask_h, face_w / mask_w) new_mask_w = int(factor * mask_w) new_mask_h = int(factor * mask_h) new_mask_shape = (new_mask_w, new_mask_h) resized_mask = cv2.resize(mask, new_mask_shape) # Add mask to face - ensure mask is centered face_with_mask = face.copy() non_white_pixels = (resized_mask < 250).all(axis=2) off_h = int((face_h - new_mask_h) / 2) off_w = int((face_w - new_mask_w) / 2) face_with_mask[off_h: off_h+new_mask_h, off_w: off_w+new_mask_w][non_white_pixels] = \ resized_mask[non_white_pixels] return face_with_mask def main(): face = cv2.imread('assets/child.png') mask = cv2.imread('assets/dog.png') face_with_mask = apply_mask(face, mask) cv2.imwrite('outputs/child_with_dog_mask.png', face_with_mask) if __name__ == '__main__': main()
保存脚本并运行它:
python step_4_dog_mask_simple.py
您将在 outputs/child_with_dog_mask.png
中看到以下儿童戴着狗面具的照片:
您现在有一个实用程序可以将狗面具应用于面部。 现在让我们使用您构建的内容实时添加狗面具。
我们将从第 3 步中中断的地方继续。 将 step_3_camera_face_detect.py
复制到 step_4_dog_mask.py
。
cp step_3_camera_face_detect.py step_4_dog_mask.py
打开你的新脚本。
nano step_4_dog_mask.py
首先,在脚本顶部导入 NumPy 库:
step_4_dog_mask.py
import numpy as np ...
然后将您之前工作中的 apply_mask
函数添加到 main
函数上方的这个新文件中:
step_4_dog_mask.py
def apply_mask(face: np.array, mask: np.array) -> np.array: """Add the mask to the provided face, and return the face with mask.""" mask_h, mask_w, _ = mask.shape face_h, face_w, _ = face.shape # Resize the mask to fit on face factor = min(face_h / mask_h, face_w / mask_w) new_mask_w = int(factor * mask_w) new_mask_h = int(factor * mask_h) new_mask_shape = (new_mask_w, new_mask_h) resized_mask = cv2.resize(mask, new_mask_shape) # Add mask to face - ensure mask is centered face_with_mask = face.copy() non_white_pixels = (resized_mask < 250).all(axis=2) off_h = int((face_h - new_mask_h) / 2) off_w = int((face_w - new_mask_w) / 2) face_with_mask[off_h: off_h+new_mask_h, off_w: off_w+new_mask_w][non_white_pixels] = \ resized_mask[non_white_pixels] return face_with_mask ...
其次,在main
函数中定位到这一行:
step_4_dog_mask.py
cap = cv2.VideoCapture(0)
在该行之后添加此代码以加载狗面具:
step_4_dog_mask.py
cap = cv2.VideoCapture(0) # load mask mask = cv2.imread('assets/dog.png') ...
接下来,在 while
循环中,找到这一行:
step_4_dog_mask.py
ret, frame = cap.read()
在其后添加此行以提取图像的高度和宽度:
step_4_dog_mask.py
ret, frame = cap.read() frame_h, frame_w, _ = frame.shape ...
接下来,删除 main
中绘制边界框的行。 您会在 for
循环中找到这一行,该循环遍历检测到的人脸:
step_4_dog_mask.py
for x, y, w, h in rects: ... cv2.rectangle(frame, (x, y), (x + w, y + h), (0, 255, 0), 2) # DELETE ME ...
取而代之的是添加裁剪框架的代码。 出于美学目的,我们裁剪了一个比脸部稍大的区域。
step_4_dog_mask.py
for x, y, w, h in rects: # crop a frame slightly larger than the face y0, y1 = int(y - 0.25*h), int(y + 0.75*h) x0, x1 = x, x + w
如果检测到的人脸离边缘太近,请进行检查。
step_4_dog_mask.py
# give up if the cropped frame would be out-of-bounds if x0 < 0 or y0 < 0 or x1 > frame_w or y1 > frame_h: continue
最后,将带有蒙版的人脸插入图像中。
step_4_dog_mask.py
# apply mask frame[y0: y1, x0: x1] = apply_mask(frame[y0: y1, x0: x1], mask)
验证您的脚本是否如下所示:
step_4_dog_mask.py
"""Real-time dog filter Move your face around and a dog filter will be applied to your face if it is not out-of-bounds. With the test frame in focus, hit `q` to exit. Note that typing `q` into your terminal will do nothing. """ import numpy as np import cv2 def apply_mask(face: np.array, mask: np.array) -> np.array: """Add the mask to the provided face, and return the face with mask.""" mask_h, mask_w, _ = mask.shape face_h, face_w, _ = face.shape # Resize the mask to fit on face factor = min(face_h / mask_h, face_w / mask_w) new_mask_w = int(factor * mask_w) new_mask_h = int(factor * mask_h) new_mask_shape = (new_mask_w, new_mask_h) resized_mask = cv2.resize(mask, new_mask_shape) # Add mask to face - ensure mask is centered face_with_mask = face.copy() non_white_pixels = (resized_mask < 250).all(axis=2) off_h = int((face_h - new_mask_h) / 2) off_w = int((face_w - new_mask_w) / 2) face_with_mask[off_h: off_h+new_mask_h, off_w: off_w+new_mask_w][non_white_pixels] = \ resized_mask[non_white_pixels] return face_with_mask def main(): cap = cv2.VideoCapture(0) # load mask mask = cv2.imread('assets/dog.png') # initialize front face classifier cascade = cv2.CascadeClassifier("assets/haarcascade_frontalface_default.xml") while(True): # Capture frame-by-frame ret, frame = cap.read() frame_h, frame_w, _ = frame.shape # Convert to black-and-white gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) blackwhite = cv2.equalizeHist(gray) # Detect faces rects = cascade.detectMultiScale( blackwhite, scaleFactor=1.3, minNeighbors=4, minSize=(30, 30), flags=cv2.CASCADE_SCALE_IMAGE) # Add mask to faces for x, y, w, h in rects: # crop a frame slightly larger than the face y0, y1 = int(y - 0.25*h), int(y + 0.75*h) x0, x1 = x, x + w # give up if the cropped frame would be out-of-bounds if x0 < 0 or y0 < 0 or x1 > frame_w or y1 > frame_h: continue # apply mask frame[y0: y1, x0: x1] = apply_mask(frame[y0: y1, x0: x1], mask) # Display the resulting frame cv2.imshow('frame', frame) if cv2.waitKey(1) & 0xFF == ord('q'): break # When everything done, release the capture cap.release() cv2.destroyAllWindows() if __name__ == '__main__': main()
保存文件并退出编辑器。 然后运行脚本。
python step_4_dog_mask.py
您现在有一个实时狗过滤器正在运行。 该脚本还适用于图片中的多张面孔,因此您可以将您的朋友聚在一起进行一些自动狗化。
我们在本教程中的第一个主要目标到此结束,即创建一个 Snapchat 式的狗过滤器。 现在让我们使用面部表情来确定应用于面部的狗面具。
第 5 步 — 使用最小二乘法构建基本的人脸情绪分类器
在本节中,您将创建一个情绪分类器,以根据显示的情绪应用不同的蒙版。 如果您微笑,过滤器将应用柯基犬面具。 如果您皱眉,它将使用哈巴狗面具。 在此过程中,您将探索 最小二乘 框架,这是理解和讨论机器学习概念的基础。
为了了解如何处理我们的数据并产生预测,我们将首先简要探索机器学习模型。
我们需要为我们考虑的每个模型提出两个问题。 目前,这两个问题足以区分模型:
- 输入:模型给出了什么信息?
- 输出:模型试图预测什么?
在高层次上,目标是开发情绪分类模型。 型号为:
- 输入:给定的人脸图像。
- 输出:预测相应的情绪。
model: face -> emotion
我们将使用的方法是最小二乘法; 我们取一组点,然后找到一条最佳拟合线。 下图所示的最佳拟合线是我们的模型。
考虑我们线路的输入和输出:
- 输入:给定
x
坐标。 - 输出:预测相应的 $y$ 坐标。
least squares line: x -> y
我们的输入 x
必须代表人脸,我们的输出 y
必须代表情绪,以便我们使用最小二乘法进行情绪分类:
x -> face
:我们将使用x
的值的 vector,而不是使用 one 数字来表示x
。 因此,x
可以表示人脸图像。 文章 Ordinary Least Squares 解释了为什么可以为x
使用值向量。y -> emotion
:每一种情绪都会对应一个数字。 例如,“愤怒”为 0,“悲伤”为 1,“快乐”为 2。 这样,y
就可以表示情绪了。 但是,我们的行是 not 被限制为输出y
值 0、1 和 2。 它有无数个可能的 y 值——可以是 1.2、3.5 或 10003.42。 我们如何将这些y
值转换为与类对应的整数? 有关详细信息和说明,请参阅文章 One-Hot Encoding。
有了这些背景知识,您将使用矢量化图像和 one-hot 编码标签构建一个简单的最小二乘分类器。 您将分三个步骤完成此操作:
- 预处理数据:如本节开头所述,我们的样本是向量,其中每个向量编码一张人脸图像。 我们的标签是对应于情绪的整数,我们将对这些标签应用 one-hot 编码。
- 指定和训练模型:使用封闭形式的最小二乘解
w^*
。 - 使用模型运行预测:取
Xw^*
的 argmax 以获得预测的情绪。
让我们开始吧。
首先,设置一个目录来包含数据:
mkdir data
然后下载由 Pierre-Luc Carrier 和 Aaron Courville 策划的数据,这些数据来自 2013 年 Kaggle 上的面部情绪分类 竞赛。
wget -O data/fer2013.tar https://bitbucket.org/alvinwan/adversarial-examples-in-computer-vision-building-then-fooling/raw/babfe4651f89a398c4b3fdbdd6d7a697c5104cff/fer2013.tar
导航到 data
目录并解压缩数据。
cd data tar -xzf fer2013.tar
现在我们将创建一个脚本来运行最小二乘模型。 导航到项目的根目录:
cd ~/DogFilter
为脚本创建一个新文件:
nano step_5_ls_simple.py
添加 Python 样板文件并导入您需要的包:
step_5_ls_simple.py
"""Train emotion classifier using least squares.""" import numpy as np def main(): pass if __name__ == '__main__': main()
接下来,将数据加载到内存中。 将 main
函数中的 pass
替换为以下代码:
step_5_ls_simple.py
# load data with np.load('data/fer2013_train.npz') as data: X_train, Y_train = data['X'], data['Y'] with np.load('data/fer2013_test.npz') as data: X_test, Y_test = data['X'], data['Y']
现在 one-hot 对标签进行编码。 为此,使用 numpy
构造单位矩阵,然后使用我们的标签列表索引到该矩阵:
step_5_ls_simple.py
# one-hot labels I = np.eye(6) Y_oh_train, Y_oh_test = I[Y_train], I[Y_test]
在这里,我们使用单位矩阵中第 i
行全为零的事实,除了第 i
项。 因此,第 i 行是类 i
的标签的 one-hot 编码。 此外,我们使用 numpy
的高级索引,其中 [a, b, c, d]1, 3 = [b, d]
。
在商用硬件上计算 (X^TX)^{-1}
会花费太长时间,因为 X^TX
是一个具有超过 400 万个值的 2304x2304
矩阵,因此我们将通过仅选择前 100 个来减少此时间特征。 添加此代码:
step_5_ls_simple.py
... # select first 100 dimensions A_train, A_test = X_train[:, :100], X_test[:, :100]
接下来,添加此代码以评估封闭形式的最小二乘解:
step_5_ls_simple.py
... # train model w = np.linalg.inv(A_train.T.dot(A_train)).dot(A_train.T.dot(Y_oh_train))
然后为训练和验证集定义一个评估函数。 把它放在你的 main
函数之前:
step_5_ls_simple.py
def evaluate(A, Y, w): Yhat = np.argmax(A.dot(w), axis=1) return np.sum(Yhat == Y) / Y.shape[0]
为了估计标签,我们对每个样本取内积,并使用 np.argmax
获得最大值的索引。 然后我们计算正确分类的平均数量。 这个最终数字是您的准确性。
最后,将此代码添加到 main
函数的末尾,以使用您刚刚编写的 evaluate
函数计算训练和验证准确度:
step_5_ls_simple.py
# evaluate model ols_train_accuracy = evaluate(A_train, Y_train, w) print('(ols) Train Accuracy:', ols_train_accuracy) ols_test_accuracy = evaluate(A_test, Y_test, w) print('(ols) Test Accuracy:', ols_test_accuracy)
仔细检查您的脚本是否与以下内容匹配:
step_5_ls_simple.py
"""Train emotion classifier using least squares.""" import numpy as np def evaluate(A, Y, w): Yhat = np.argmax(A.dot(w), axis=1) return np.sum(Yhat == Y) / Y.shape[0] def main(): # load data with np.load('data/fer2013_train.npz') as data: X_train, Y_train = data['X'], data['Y'] with np.load('data/fer2013_test.npz') as data: X_test, Y_test = data['X'], data['Y'] # one-hot labels I = np.eye(6) Y_oh_train, Y_oh_test = I[Y_train], I[Y_test] # select first 100 dimensions A_train, A_test = X_train[:, :100], X_test[:, :100] # train model w = np.linalg.inv(A_train.T.dot(A_train)).dot(A_train.T.dot(Y_oh_train)) # evaluate model ols_train_accuracy = evaluate(A_train, Y_train, w) print('(ols) Train Accuracy:', ols_train_accuracy) ols_test_accuracy = evaluate(A_test, Y_test, w) print('(ols) Test Accuracy:', ols_test_accuracy) if __name__ == '__main__': main()
保存文件,退出编辑器,然后运行 Python 脚本。
python step_5_ls_simple.py
您将看到以下输出:
Output(ols) Train Accuracy: 0.4748918316507146 (ols) Test Accuracy: 0.45280545359202934
我们的模型给出了 47.5% 的训练准确率。 我们在验证集上重复此操作以获得 45.3% 的准确率。 对于三向分类问题,45.3% 合理地高于猜测,即 33%。 这是我们用于情绪检测的起始分类器,在下一步中,您将构建此最小二乘模型以提高准确性。 准确度越高,基于情绪的狗过滤器就越可靠地为每个检测到的情绪找到合适的狗过滤器。
第 6 步——通过对输入进行特征化来提高准确性
我们可以使用更具表现力的模型来提高准确性。 为了实现这一点,我们 特征化 我们的输入。
原始图像告诉我们位置 (0, 0
) 为红色,(1, 0
) 为棕色,依此类推。 一个特征化的图像可能会告诉我们图像的左上角有一条狗,中间有一个人,等等。 特征化很强大,但它的精确定义超出了本教程的范围。
我们将对径向基函数 (RBF) 内核使用 近似,使用随机高斯矩阵 。 我们不会在本教程中详细介绍。 相反,我们将其视为为我们计算高阶特征的黑盒。
我们将继续我们在上一步中中断的地方。 复制前面的脚本,这样你就有了一个好的起点:
cp step_5_ls_simple.py step_6_ls_simple.py
在编辑器中打开新文件:
nano step_6_ls_simple.py
我们将从创建特征化随机矩阵开始。 同样,我们将在新特征空间中仅使用 100 个特征。
找到以下行,定义 A_train
和 A_test
:
step_6_ls_simple.py
# select first 100 dimensions A_train, A_test = X_train[:, :100], X_test[:, :100]
在 A_train
和 A_test
的定义正上方,添加一个随机特征矩阵:
step_6_ls_simple.py
d = 100 W = np.random.normal(size=(X_train.shape[1], d)) # select first 100 dimensions A_train, A_test = X_train[:, :100], X_test[:, :100] ...
然后替换 A_train
和 A_test
的定义。 我们使用这种随机特征化重新定义了我们的矩阵,称为 design 矩阵。
step_6_ls_simple.py
A_train, A_test = X_train.dot(W), X_test.dot(W)
保存文件并运行脚本。
python step_6_ls_simple.py
您将看到以下输出:
Output(ols) Train Accuracy: 0.584174642717 (ols) Test Accuracy: 0.584425799685
这种特征化现在提供了 58.4% 的训练准确率和 58.4% 的验证准确率,验证结果提高了 13.1%。 我们将 X 矩阵修剪为 100 x 100
,但 100 的选择是任意的。 我们还可以将 X
矩阵修剪为 1000 x 1000
或 50 x 50
。 假设 x
的尺寸是 d x d
。 我们可以通过将 X 重新修剪为 d x d
并重新计算新模型来测试 d
的更多值。
尝试更多的 d
值,我们发现测试准确度额外提高了 4.3%,达到 61.7%。 在下图中,我们考虑了新分类器的性能,因为我们改变了 d
。 直观地说,随着 d
的增加,随着我们使用越来越多的原始数据,准确度也应该提高。 然而,该图并没有描绘出一幅美好的图画,而是呈现出负面趋势:
随着我们保留更多数据,训练和验证准确性之间的差距也会增加。 这是 过度拟合 的明显证据,我们的模型正在学习不再适用于所有数据的表示。 为了对抗过度拟合,我们将通过惩罚复杂模型来 正则化 我们的模型。
我们用正则化项修改我们的普通最小二乘目标函数,给我们一个新的目标。 我们的新目标函数称为岭回归,它看起来像这样:
min_w |Aw- y|^2 + lambda |w|^2
在这个等式中,lambda
是一个可调的超参数。 将 lambda = 0
代入方程,岭回归变为最小二乘。 将 lambda = infinity
代入方程,你会发现最好的 w
现在必须为零,因为任何非零 w
都会导致无限损失。 事实证明,这个目标也产生了一个封闭形式的解决方案:
w^* = (A^TA + lambda I)^{-1}A^Ty
仍然使用特征化样本,再次重新训练和重新评估模型。
在编辑器中再次打开 step_6_ls_simple.py
:
nano step_6_ls_simple.py
这一次,将新特征空间的维数增加到d=1000
。 将 d
的值从 100
更改为 1000
,如以下代码块所示:
step_6_ls_simple.py
... d = 1000 W = np.random.normal(size=(X_train.shape[1], d)) ...
然后使用 lambda = 10^{10}
的正则化应用岭回归。 将定义 w
的行替换为以下两行:
step_6_ls_simple.py
... # train model I = np.eye(A_train.shape[1]) w = np.linalg.inv(A_train.T.dot(A_train) + 1e10 * I).dot(A_train.T.dot(Y_oh_train))
然后找到这个块:
step_6_ls_simple.py
... ols_train_accuracy = evaluate(A_train, Y_train, w) print('(ols) Train Accuracy:', ols_train_accuracy) ols_test_accuracy = evaluate(A_test, Y_test, w) print('(ols) Test Accuracy:', ols_test_accuracy)
将其替换为以下内容:
step_6_ls_simple.py
... print('(ridge) Train Accuracy:', evaluate(A_train, Y_train, w)) print('(ridge) Test Accuracy:', evaluate(A_test, Y_test, w))
完成的脚本应如下所示:
step_6_ls_simple.py
"""Train emotion classifier using least squares.""" import numpy as np def evaluate(A, Y, w): Yhat = np.argmax(A.dot(w), axis=1) return np.sum(Yhat == Y) / Y.shape[0] def main(): # load data with np.load('data/fer2013_train.npz') as data: X_train, Y_train = data['X'], data['Y'] with np.load('data/fer2013_test.npz') as data: X_test, Y_test = data['X'], data['Y'] # one-hot labels I = np.eye(6) Y_oh_train, Y_oh_test = I[Y_train], I[Y_test] d = 1000 W = np.random.normal(size=(X_train.shape[1], d)) # select first 100 dimensions A_train, A_test = X_train.dot(W), X_test.dot(W) # train model I = np.eye(A_train.shape[1]) w = np.linalg.inv(A_train.T.dot(A_train) + 1e10 * I).dot(A_train.T.dot(Y_oh_train)) # evaluate model print('(ridge) Train Accuracy:', evaluate(A_train, Y_train, w)) print('(ridge) Test Accuracy:', evaluate(A_test, Y_test, w)) if __name__ == '__main__': main()
保存文件,退出编辑器,然后运行脚本:
python step_6_ls_simple.py
您将看到以下输出:
Output(ridge) Train Accuracy: 0.651173462698 (ridge) Test Accuracy: 0.622181436812
随着训练准确度下降到 65.1%,验证准确度进一步提高了 0.4% 至 62.2%。 再次重新评估许多不同的 d
,我们发现岭回归的训练和验证精度之间的差距较小。 换句话说,岭回归受到的过度拟合较少。
具有这些额外增强功能的最小二乘的基线性能表现相当不错。 即使是最好的结果,训练和推理时间加起来也不超过 20 秒。 在下一节中,您将探索更复杂的模型。
第 7 步 — 在 PyTorch 中使用卷积神经网络构建面部情绪分类器
在本节中,您将使用神经网络而不是最小二乘法构建第二个情感分类器。 同样,我们的目标是生成一个接受面部作为输入并输出情感的模型。 最终,该分类器将确定应用哪个狗面具。
简要的神经网络可视化和介绍,参见文章Understanding Neural Networks。 在这里,我们将使用一个名为 PyTorch 的深度学习库。 有许多广泛使用的深度学习库,每个库都各有利弊。 PyTorch 是一个特别好的起点。 为了实现这个神经网络分类器,我们再次采取三个步骤,就像我们对最小二乘分类器所做的那样:
- 预处理数据:应用 one-hot 编码,然后应用 PyTorch 抽象。
- 指定和训练模型:使用 PyTorch 层设置神经网络。 定义优化超参数并运行随机梯度下降。
- 使用模型运行预测:评估神经网络。
创建一个新文件,命名为 step_7_fer_simple.py
nano step_7_fer_simple.py
导入必要的实用程序并创建一个 Python 类 来保存您的数据。 对于此处的数据处理,您将创建训练和测试数据集。 为此,请实现 PyTorch 的 Dataset
接口,它允许您加载和使用 PyTorch 的内置数据管道用于面部表情识别数据集:
step_7_fer_simple.py
from torch.utils.data import Dataset from torch.autograd import Variable import torch.nn as nn import torch.nn.functional as F import torch.optim as optim import numpy as np import torch import cv2 import argparse class Fer2013Dataset(Dataset): """Face Emotion Recognition dataset. Utility for loading FER into PyTorch. Dataset curated by Pierre-Luc Carrier and Aaron Courville in 2013. Each sample is 1 x 1 x 48 x 48, and each label is a scalar. """ pass
删除 Fer2013Dataset
类中的 pass
占位符。 取而代之的是,添加一个初始化我们的数据持有者的函数:
step_7_fer_simple.py
def __init__(self, path: str): """ Args: path: Path to `.np` file containing sample nxd and label nx1 """ with np.load(path) as data: self._samples = data['X'] self._labels = data['Y'] self._samples = self._samples.reshape((-1, 1, 48, 48)) self.X = Variable(torch.from_numpy(self._samples)).float() self.Y = Variable(torch.from_numpy(self._labels)).float() ...
此功能首先加载样本和标签。 然后它将数据包装在 PyTorch 数据结构中。
在 __init__
函数之后直接添加一个 __len__
函数,因为这是实现 PyTorch 期望的 Dataset
接口所必需的:
step_7_fer_simple.py
... def __len__(self): return len(self._labels)
最后,添加一个 __getitem__
方法,它返回一个包含样本和标签的 dictionary:
step_7_fer_simple.py
def __getitem__(self, idx): return {'image': self._samples[idx], 'label': self._labels[idx]}
仔细检查您的文件是否如下所示:
step_7_fer_simple.py
from torch.utils.data import Dataset from torch.autograd import Variable import torch.nn as nn import torch.nn.functional as F import torch.optim as optim import numpy as np import torch import cv2 import argparse class Fer2013Dataset(Dataset): """Face Emotion Recognition dataset. Utility for loading FER into PyTorch. Dataset curated by Pierre-Luc Carrier and Aaron Courville in 2013. Each sample is 1 x 1 x 48 x 48, and each label is a scalar. """ def __init__(self, path: str): """ Args: path: Path to `.np` file containing sample nxd and label nx1 """ with np.load(path) as data: self._samples = data['X'] self._labels = data['Y'] self._samples = self._samples.reshape((-1, 1, 48, 48)) self.X = Variable(torch.from_numpy(self._samples)).float() self.Y = Variable(torch.from_numpy(self._labels)).float() def __len__(self): return len(self._labels) def __getitem__(self, idx): return {'image': self._samples[idx], 'label': self._labels[idx]}
接下来,加载 Fer2013Dataset
数据集。 在 Fer2013Dataset
类之后将以下代码添加到文件末尾:
step_7_fer_simple.py
trainset = Fer2013Dataset('data/fer2013_train.npz') trainloader = torch.utils.data.DataLoader(trainset, batch_size=32, shuffle=True) testset = Fer2013Dataset('data/fer2013_test.npz') testloader = torch.utils.data.DataLoader(testset, batch_size=32, shuffle=False)
此代码使用您创建的 Fer2013Dataset
类初始化数据集。 然后对于训练集和验证集,它将数据集包装在 DataLoader
中。 这会将数据集转换为可迭代以供以后使用。
作为健全性检查,验证数据集实用程序是否正常运行。 使用 DataLoader
创建示例数据集加载器并打印该加载器的第一个元素。 将以下内容添加到文件末尾:
step_7_fer_simple.py
if __name__ == '__main__': loader = torch.utils.data.DataLoader(trainset, batch_size=2, shuffle=False) print(next(iter(loader)))
验证您完成的脚本是否如下所示:
step_7_fer_simple.py
from torch.utils.data import Dataset from torch.autograd import Variable import torch.nn as nn import torch.nn.functional as F import torch.optim as optim import numpy as np import torch import cv2 import argparse class Fer2013Dataset(Dataset): """Face Emotion Recognition dataset. Utility for loading FER into PyTorch. Dataset curated by Pierre-Luc Carrier and Aaron Courville in 2013. Each sample is 1 x 1 x 48 x 48, and each label is a scalar. """ def __init__(self, path: str): """ Args: path: Path to `.np` file containing sample nxd and label nx1 """ with np.load(path) as data: self._samples = data['X'] self._labels = data['Y'] self._samples = self._samples.reshape((-1, 1, 48, 48)) self.X = Variable(torch.from_numpy(self._samples)).float() self.Y = Variable(torch.from_numpy(self._labels)).float() def __len__(self): return len(self._labels) def __getitem__(self, idx): return {'image': self._samples[idx], 'label': self._labels[idx]} trainset = Fer2013Dataset('data/fer2013_train.npz') trainloader = torch.utils.data.DataLoader(trainset, batch_size=32, shuffle=True) testset = Fer2013Dataset('data/fer2013_test.npz') testloader = torch.utils.data.DataLoader(testset, batch_size=32, shuffle=False) if __name__ == '__main__': loader = torch.utils.data.DataLoader(trainset, batch_size=2, shuffle=False) print(next(iter(loader)))
退出编辑器并运行脚本。
python step_7_fer_simple.py
这将输出以下一对 张量 。 我们的数据管道输出两个样本和两个标签。 这表明我们的数据管道已启动并准备就绪:
Output{'image': (0 ,0 ,.,.) = 24 32 36 ... 173 172 173 25 34 29 ... 173 172 173 26 29 25 ... 172 172 174 ... ⋱ ... 159 185 157 ... 157 156 153 136 157 187 ... 152 152 150 145 130 161 ... 142 143 142 ⋮ (1 ,0 ,.,.) = 20 17 19 ... 187 176 162 22 17 17 ... 195 180 171 17 17 18 ... 203 193 175 ... ⋱ ... 1 1 1 ... 106 115 119 2 2 1 ... 103 111 119 2 2 2 ... 99 107 118 [torch.LongTensor of size 2x1x48x48] , 'label': 1 1 [torch.LongTensor of size 2] }
现在您已验证数据管道工作正常,返回 step_7_fer_simple.py
以添加神经网络和优化器。 打开 step_7_fer_simple.py
。
nano step_7_fer_simple.py
首先,删除您在上一次迭代中添加的最后三行:
step_7_fer_simple.py
# Delete all three lines if __name__ == '__main__': loader = torch.utils.data.DataLoader(trainset, batch_size=2, shuffle=False) print(next(iter(loader)))
取而代之的是,定义一个 PyTorch 神经网络,其中包括三个卷积层,然后是三个全连接层。 将此添加到现有脚本的末尾:
step_7_fer_simple.py
class Net(nn.Module): def __init__(self): super(Net, self).__init__() self.conv1 = nn.Conv2d(1, 6, 5) self.pool = nn.MaxPool2d(2, 2) self.conv2 = nn.Conv2d(6, 6, 3) self.conv3 = nn.Conv2d(6, 16, 3) self.fc1 = nn.Linear(16 * 4 * 4, 120) self.fc2 = nn.Linear(120, 48) self.fc3 = nn.Linear(48, 3) def forward(self, x): x = self.pool(F.relu(self.conv1(x))) x = self.pool(F.relu(self.conv2(x))) x = self.pool(F.relu(self.conv3(x))) x = x.view(-1, 16 * 4 * 4) x = F.relu(self.fc1(x)) x = F.relu(self.fc2(x)) x = self.fc3(x) return x
现在初始化神经网络,定义损失函数,并通过在脚本末尾添加以下代码来定义优化超参数:
step_7_fer_simple.py
net = Net().float() criterion = nn.CrossEntropyLoss() optimizer = optim.SGD(net.parameters(), lr=0.001, momentum=0.9)
我们将训练两个 epochs。 现在,我们将 epoch 定义为训练的迭代,其中每个训练样本都只使用了一次。
首先,从数据集加载器中提取 image
和 label
,然后将它们分别包装在 PyTorch Variable
中。 其次,运行前向传播,然后通过损失和神经网络进行反向传播。 将以下代码添加到脚本的末尾以执行此操作:
step_7_fer_simple.py
for epoch in range(2): # loop over the dataset multiple times running_loss = 0.0 for i, data in enumerate(trainloader, 0): inputs = Variable(data['image'].float()) labels = Variable(data['label'].long()) optimizer.zero_grad() # forward + backward + optimize outputs = net(inputs) loss = criterion(outputs, labels) loss.backward() optimizer.step() # print statistics running_loss += loss.data[0] if i % 100 == 0: print('[%d, %5d] loss: %.3f' % (epoch, i, running_loss / (i + 1)))
您的脚本现在应该如下所示:
step_7_fer_simple.py
from torch.utils.data import Dataset from torch.autograd import Variable import torch.nn as nn import torch.nn.functional as F import torch.optim as optim import numpy as np import torch import cv2 import argparse class Fer2013Dataset(Dataset): """Face Emotion Recognition dataset. Utility for loading FER into PyTorch. Dataset curated by Pierre-Luc Carrier and Aaron Courville in 2013. Each sample is 1 x 1 x 48 x 48, and each label is a scalar. """ def __init__(self, path: str): """ Args: path: Path to `.np` file containing sample nxd and label nx1 """ with np.load(path) as data: self._samples = data['X'] self._labels = data['Y'] self._samples = self._samples.reshape((-1, 1, 48, 48)) self.X = Variable(torch.from_numpy(self._samples)).float() self.Y = Variable(torch.from_numpy(self._labels)).float() def __len__(self): return len(self._labels) def __getitem__(self, idx): return {'image': self._samples[idx], 'label': self._labels[idx]} trainset = Fer2013Dataset('data/fer2013_train.npz') trainloader = torch.utils.data.DataLoader(trainset, batch_size=32, shuffle=True) testset = Fer2013Dataset('data/fer2013_test.npz') testloader = torch.utils.data.DataLoader(testset, batch_size=32, shuffle=False) class Net(nn.Module): def __init__(self): super(Net, self).__init__() self.conv1 = nn.Conv2d(1, 6, 5) self.pool = nn.MaxPool2d(2, 2) self.conv2 = nn.Conv2d(6, 6, 3) self.conv3 = nn.Conv2d(6, 16, 3) self.fc1 = nn.Linear(16 * 4 * 4, 120) self.fc2 = nn.Linear(120, 48) self.fc3 = nn.Linear(48, 3) def forward(self, x): x = self.pool(F.relu(self.conv1(x))) x = self.pool(F.relu(self.conv2(x))) x = self.pool(F.relu(self.conv3(x))) x = x.view(-1, 16 * 4 * 4) x = F.relu(self.fc1(x)) x = F.relu(self.fc2(x)) x = self.fc3(x) return x net = Net().float() criterion = nn.CrossEntropyLoss() optimizer = optim.SGD(net.parameters(), lr=0.001, momentum=0.9) for epoch in range(2): # loop over the dataset multiple times running_loss = 0.0 for i, data in enumerate(trainloader, 0): inputs = Variable(data['image'].float()) labels = Variable(data['label'].long()) optimizer.zero_grad() # forward + backward + optimize outputs = net(inputs) loss = criterion(outputs, labels) loss.backward() optimizer.step() # print statistics running_loss += loss.data[0] if i % 100 == 0: print('[%d, %5d] loss: %.3f' % (epoch, i, running_loss / (i + 1)))
验证代码后,保存文件并退出编辑器。 然后,启动此概念验证培训:
python step_7_fer_simple.py
随着神经网络的训练,您将看到类似于以下内容的输出:
Output[0, 0] loss: 1.094 [0, 100] loss: 1.049 [0, 200] loss: 1.009 [0, 300] loss: 0.963 [0, 400] loss: 0.935 [1, 0] loss: 0.760 [1, 100] loss: 0.768 [1, 200] loss: 0.775 [1, 300] loss: 0.776 [1, 400] loss: 0.767
然后,您可以使用许多其他 PyTorch 实用程序来扩充此脚本,以保存和加载模型、输出训练和验证准确度、微调学习率计划等。 在以 0.01 的学习率和 0.9 的动量训练 20 个 epoch 后,我们的神经网络达到了 87.9% 的训练准确率和 75.5% 的验证准确率,比迄今为止最成功的最小二乘方法的 66.6% 进一步提高了 6.8% . 我们将在新脚本中包含这些额外的花里胡哨。
创建一个新文件来保存您的实时摄像头将使用的最终面部情绪检测器。 这个脚本包含上面的代码以及一个命令行界面和一个易于导入的代码版本,稍后将使用它。 此外,它包含预先调整的超参数,用于具有更高准确性的模型。
nano step_7_fer.py
从以下导入开始。 这与我们之前的文件相匹配,但还包括 OpenCV 作为 import cv2.
step_7_fer.py
from torch.utils.data import Dataset from torch.autograd import Variable import torch.nn as nn import torch.nn.functional as F import torch.optim as optim import numpy as np import torch import cv2 import argparse
在这些导入的正下方,重用 step_7_fer_simple.py
中的代码来定义神经网络:
step_7_fer.py
class Net(nn.Module): def __init__(self): super(Net, self).__init__() self.conv1 = nn.Conv2d(1, 6, 5) self.pool = nn.MaxPool2d(2, 2) self.conv2 = nn.Conv2d(6, 6, 3) self.conv3 = nn.Conv2d(6, 16, 3) self.fc1 = nn.Linear(16 * 4 * 4, 120) self.fc2 = nn.Linear(120, 48) self.fc3 = nn.Linear(48, 3) def forward(self, x): x = self.pool(F.relu(self.conv1(x))) x = self.pool(F.relu(self.conv2(x))) x = self.pool(F.relu(self.conv3(x))) x = x.view(-1, 16 * 4 * 4) x = F.relu(self.fc1(x)) x = F.relu(self.fc2(x)) x = self.fc3(x) return x
再次,重用来自 step_7_fer_simple.py
的人脸情绪识别数据集的代码并将其添加到此文件中:
step_7_fer.py
class Fer2013Dataset(Dataset): """Face Emotion Recognition dataset. Utility for loading FER into PyTorch. Dataset curated by Pierre-Luc Carrier and Aaron Courville in 2013. Each sample is 1 x 1 x 48 x 48, and each label is a scalar. """ def __init__(self, path: str): """ Args: path: Path to `.np` file containing sample nxd and label nx1 """ with np.load(path) as data: self._samples = data['X'] self._labels = data['Y'] self._samples = self._samples.reshape((-1, 1, 48, 48)) self.X = Variable(torch.from_numpy(self._samples)).float() self.Y = Variable(torch.from_numpy(self._labels)).float() def __len__(self): return len(self._labels) def __getitem__(self, idx): return {'image': self._samples[idx], 'label': self._labels[idx]}
接下来,定义一些实用程序来评估神经网络的性能。 首先,添加一个 evaluate
函数,它将神经网络的预测情绪与单个图像的真实情绪进行比较:
step_7_fer.py
def evaluate(outputs: Variable, labels: Variable, normalized: bool=True) -> float: """Evaluate neural network outputs against non-one-hotted labels.""" Y = labels.data.numpy() Yhat = np.argmax(outputs.data.numpy(), axis=1) denom = Y.shape[0] if normalized else 1 return float(np.sum(Yhat == Y) / denom)
然后添加一个名为 batch_evaluate
的函数,它将第一个函数应用于所有图像:
step_7_fer.py
def batch_evaluate(net: Net, dataset: Dataset, batch_size: int=500) -> float: """Evaluate neural network in batches, if dataset is too large.""" score = 0.0 n = dataset.X.shape[0] for i in range(0, n, batch_size): x = dataset.X[i: i + batch_size] y = dataset.Y[i: i + batch_size] score += evaluate(net(x), y, False) return score / n
现在,使用预训练模型定义一个名为 get_image_to_emotion_predictor
的函数,它接收图像并输出预测的情绪:
step_7_fer.py
def get_image_to_emotion_predictor(model_path='assets/model_best.pth'): """Returns predictor, from image to emotion index.""" net = Net().float() pretrained_model = torch.load(model_path) net.load_state_dict(pretrained_model['state_dict']) def predictor(image: np.array): """Translates images into emotion indices.""" if image.shape[2] > 1: image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) frame = cv2.resize(image, (48, 48)).reshape((1, 1, 48, 48)) X = Variable(torch.from_numpy(frame)).float() return np.argmax(net(X).data.numpy(), axis=1)[0] return predictor
最后,添加以下代码来定义 main
函数以利用其他实用程序:
step_7_fer.py
def main(): trainset = Fer2013Dataset('data/fer2013_train.npz') testset = Fer2013Dataset('data/fer2013_test.npz') net = Net().float() pretrained_model = torch.load("assets/model_best.pth") net.load_state_dict(pretrained_model['state_dict']) train_acc = batch_evaluate(net, trainset, batch_size=500) print('Training accuracy: %.3f' % train_acc) test_acc = batch_evaluate(net, testset, batch_size=500) print('Validation accuracy: %.3f' % test_acc) if __name__ == '__main__': main()
这会加载一个预训练的神经网络,并在提供的人脸情绪识别数据集上评估其性能。 具体来说,脚本输出我们用于训练的图像的准确性,以及我们为测试目的而放置的一组单独的图像。
仔细检查您的文件是否与以下内容匹配:
step_7_fer.py
from torch.utils.data import Dataset from torch.autograd import Variable import torch.nn as nn import torch.nn.functional as F import torch.optim as optim import numpy as np import torch import cv2 import argparse class Net(nn.Module): def __init__(self): super(Net, self).__init__() self.conv1 = nn.Conv2d(1, 6, 5) self.pool = nn.MaxPool2d(2, 2) self.conv2 = nn.Conv2d(6, 6, 3) self.conv3 = nn.Conv2d(6, 16, 3) self.fc1 = nn.Linear(16 * 4 * 4, 120) self.fc2 = nn.Linear(120, 48) self.fc3 = nn.Linear(48, 3) def forward(self, x): x = self.pool(F.relu(self.conv1(x))) x = self.pool(F.relu(self.conv2(x))) x = self.pool(F.relu(self.conv3(x))) x = x.view(-1, 16 * 4 * 4) x = F.relu(self.fc1(x)) x = F.relu(self.fc2(x)) x = self.fc3(x) return x class Fer2013Dataset(Dataset): """Face Emotion Recognition dataset. Utility for loading FER into PyTorch. Dataset curated by Pierre-Luc Carrier and Aaron Courville in 2013. Each sample is 1 x 1 x 48 x 48, and each label is a scalar. """ def __init__(self, path: str): """ Args: path: Path to `.np` file containing sample nxd and label nx1 """ with np.load(path) as data: self._samples = data['X'] self._labels = data['Y'] self._samples = self._samples.reshape((-1, 1, 48, 48)) self.X = Variable(torch.from_numpy(self._samples)).float() self.Y = Variable(torch.from_numpy(self._labels)).float() def __len__(self): return len(self._labels) def __getitem__(self, idx): return {'image': self._samples[idx], 'label': self._labels[idx]} def evaluate(outputs: Variable, labels: Variable, normalized: bool=True) -> float: """Evaluate neural network outputs against non-one-hotted labels.""" Y = labels.data.numpy() Yhat = np.argmax(outputs.data.numpy(), axis=1) denom = Y.shape[0] if normalized else 1 return float(np.sum(Yhat == Y) / denom) def batch_evaluate(net: Net, dataset: Dataset, batch_size: int=500) -> float: """Evaluate neural network in batches, if dataset is too large.""" score = 0.0 n = dataset.X.shape[0] for i in range(0, n, batch_size): x = dataset.X[i: i + batch_size] y = dataset.Y[i: i + batch_size] score += evaluate(net(x), y, False) return score / n def get_image_to_emotion_predictor(model_path='assets/model_best.pth'): """Returns predictor, from image to emotion index.""" net = Net().float() pretrained_model = torch.load(model_path) net.load_state_dict(pretrained_model['state_dict']) def predictor(image: np.array): """Translates images into emotion indices.""" if image.shape[2] > 1: image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) frame = cv2.resize(image, (48, 48)).reshape((1, 1, 48, 48)) X = Variable(torch.from_numpy(frame)).float() return np.argmax(net(X).data.numpy(), axis=1)[0] return predictor def main(): trainset = Fer2013Dataset('data/fer2013_train.npz') testset = Fer2013Dataset('data/fer2013_test.npz') net = Net().float() pretrained_model = torch.load("assets/model_best.pth") net.load_state_dict(pretrained_model['state_dict']) train_acc = batch_evaluate(net, trainset, batch_size=500) print('Training accuracy: %.3f' % train_acc) test_acc = batch_evaluate(net, testset, batch_size=500) print('Validation accuracy: %.3f' % test_acc) if __name__ == '__main__': main(
保存文件并退出编辑器。
和以前一样,使用面部检测器,下载预训练的模型参数并使用以下命令将它们保存到您的 assets
文件夹中:
wget -O assets/model_best.pth https://github.com/alvinwan/emotion-based-dog-filter/raw/master/src/assets/model_best.pth
运行脚本以使用和评估预训练模型:
python step_7_fer.py
这将输出以下内容:
OutputTraining accuracy: 0.879 Validation accuracy: 0.755
至此,您已经构建了一个非常准确的面部表情分类器。 从本质上讲,我们的模型可以正确区分快乐、悲伤和惊讶的面孔(十分之八)。 这是一个相当不错的模型,因此您现在可以继续使用这个面部情绪分类器来确定将哪个狗面具应用于面部。
第 8 步 - 完成基于情绪的狗过滤器
在集成我们全新的面部情绪分类器之前,我们需要从动物面具中进行挑选。 我们将使用 Dalmation 面具和 Sheepdog 面具:
执行以下命令将两个掩码下载到您的 assets
文件夹:
wget -O assets/dalmation.png https://assets.digitalocean.com/articles/python3_dogfilter/E9ax7PI.png # dalmation wget -O assets/sheepdog.png https://assets.digitalocean.com/articles/python3_dogfilter/HveFdkg.png # sheepdog
现在让我们在过滤器中使用遮罩。 首先复制 step_4_dog_mask.py
文件:
cp step_4_dog_mask.py step_8_dog_emotion_mask.py
打开新的 Python 脚本。
nano step_8_dog_emotion_mask.py
在脚本顶部插入新行以导入情绪预测器:
step_8_dog_emotion_mask.py
from step_7_fer import get_image_to_emotion_predictor ...
然后,在 main()
函数中,找到这一行:
step_8_dog_emotion_mask.py
mask = cv2.imread('assets/dog.png')
将其替换为以下内容以加载新掩码并将所有掩码聚合到一个元组中:
step_8_dog_emotion_mask.py
mask0 = cv2.imread('assets/dog.png') mask1 = cv2.imread('assets/dalmation.png') mask2 = cv2.imread('assets/sheepdog.png') masks = (mask0, mask1, mask2)
添加换行符,然后添加此代码以创建情绪预测器。
step_8_dog_emotion_mask.py
# get emotion predictor predictor = get_image_to_emotion_predictor()
您的 main
函数现在应该与以下内容匹配:
step_8_dog_emotion_mask.py
def main(): cap = cv2.VideoCapture(0) # load mask mask0 = cv2.imread('assets/dog.png') mask1 = cv2.imread('assets/dalmation.png') mask2 = cv2.imread('assets/sheepdog.png') masks = (mask0, mask1, mask2) # get emotion predictor predictor = get_image_to_emotion_predictor() # initialize front face classifier ...
接下来,找到这些行:
step_8_dog_emotion_mask.py
# apply mask frame[y0: y1, x0: x1] = apply_mask(frame[y0: y1, x0: x1], mask)
在 # apply mask
行下方插入以下行,以使用预测器选择适当的掩码:
step_8_dog_emotion_mask.py
# apply mask mask = masks[predictor(frame[y:y+h, x: x+w])] frame[y0: y1, x0: x1] = apply_mask(frame[y0: y1, x0: x1], mask)
完成的文件应如下所示:
step_8_dog_emotion_mask.py
"""Test for face detection""" from step_7_fer import get_image_to_emotion_predictor import numpy as np import cv2 def apply_mask(face: np.array, mask: np.array) -> np.array: """Add the mask to the provided face, and return the face with mask.""" mask_h, mask_w, _ = mask.shape face_h, face_w, _ = face.shape # Resize the mask to fit on face factor = min(face_h / mask_h, face_w / mask_w) new_mask_w = int(factor * mask_w) new_mask_h = int(factor * mask_h) new_mask_shape = (new_mask_w, new_mask_h) resized_mask = cv2.resize(mask, new_mask_shape) # Add mask to face - ensure mask is centered face_with_mask = face.copy() non_white_pixels = (resized_mask < 250).all(axis=2) off_h = int((face_h - new_mask_h) / 2) off_w = int((face_w - new_mask_w) / 2) face_with_mask[off_h: off_h+new_mask_h, off_w: off_w+new_mask_w][non_white_pixels] = \ resized_mask[non_white_pixels] return face_with_mask def main(): cap = cv2.VideoCapture(0) # load mask mask0 = cv2.imread('assets/dog.png') mask1 = cv2.imread('assets/dalmation.png') mask2 = cv2.imread('assets/sheepdog.png') masks = (mask0, mask1, mask2) # get emotion predictor predictor = get_image_to_emotion_predictor() # initialize front face classifier cascade = cv2.CascadeClassifier("assets/haarcascade_frontalface_default.xml") while True: # Capture frame-by-frame ret, frame = cap.read() frame_h, frame_w, _ = frame.shape # Convert to black-and-white gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) blackwhite = cv2.equalizeHist(gray) rects = cascade.detectMultiScale( blackwhite, scaleFactor=1.3, minNeighbors=4, minSize=(30, 30), flags=cv2.CASCADE_SCALE_IMAGE) for x, y, w, h in rects: # crop a frame slightly larger than the face y0, y1 = int(y - 0.25*h), int(y + 0.75*h) x0, x1 = x, x + w # give up if the cropped frame would be out-of-bounds if x0 < 0 or y0 < 0 or x1 > frame_w or y1 > frame_h: continue # apply mask mask = masks[predictor(frame[y:y+h, x: x+w])] frame[y0: y1, x0: x1] = apply_mask(frame[y0: y1, x0: x1], mask) # Display the resulting frame cv2.imshow('frame', frame) if cv2.waitKey(1) & 0xFF == ord('q'): break cap.release() cv2.destroyAllWindows() if __name__ == '__main__': main()
保存并退出您的编辑器。 现在启动脚本:
python step_8_dog_emotion_mask.py
现在试试看! 微笑将注册为“快乐”并显示原来的狗。 中性的脸或皱眉将注册为“悲伤”并产生dalmation。 一张“惊讶”的脸,下巴很大,会让牧羊犬屈服。
这结束了我们基于情感的狗过滤器并涉足计算机视觉。
结论
在本教程中,您使用计算机视觉构建了面部检测器和狗过滤器,并使用机器学习模型根据检测到的情绪应用蒙版。
机器学习应用广泛。 但是,在应用机器学习时,由从业者考虑每个应用程序的伦理影响。 您在本教程中构建的应用程序是一个有趣的练习,但请记住,您依赖 OpenCV 和现有数据集来识别人脸,而不是提供自己的数据来训练模型。 使用的数据和模型对程序的工作方式有重大影响。
例如,想象一个工作搜索引擎,其中模型使用候选人数据进行训练。 例如种族、性别、年龄、文化、第一语言或其他因素。 也许开发人员训练了一个强制稀疏性的模型,最终将特征空间减少到性别解释大部分差异的子空间。 因此,该模型会影响主要基于性别的候选人求职甚至公司选择过程。 现在考虑更复杂的情况,其中模型难以解释并且您不知道特定特征对应于什么。 您可以在加州大学伯克利分校的 Moritz Hardt 教授的 机器学习中的机会平等 中了解更多信息。
机器学习中可能存在巨大的不确定性。 要理解这种随机性和复杂性,您必须同时培养数学直觉和概率思维技能。 作为从业者,您有责任深入挖掘机器学习的理论基础。