【Lecture 5.3】Opencv 面部识别

Comparing Image Data Structures

OpenCV支持读取大多数文件格式的图像,例如JPEG,PNG和TIFF。 大多数图像和视频分析需要先将图像转换为grayscale 图。 这样可以简化图像并减少噪声,从而改善分析效果。 让我们编写一些代码,读取人像Floyd Mayweather 的图像并将其转换为灰度图。

# 首先我们导入cv2包 并加载图片
import cv2 as cv
img = cv.imread('readonly/floyd.jpg')
# 使用cv.cvtColor 方法将图片转换成 grayscale
gray = cv.cvtColor(img, cv.COLOR_BGR2GRY)

在我们查看图片处理结果之前,我们看一下 opencv 的文档,和 tesseract 一样,opencv也是用c++写的包,因此文档的内容也很少。但幸运的是网页版文档很全面,因此可以 在 docs.opencv.org 网站上学习特殊的函数用法。这里 函数 cvtColor 将图片从一个色彩空间转换成两一个色彩空间,这里我们把图片转换成灰度图。

we already know at least two different ways of doing this, using binarization and PIL color spaces conversions

# 让我们 inspect 返回的灰度图对象
import inspect
inspect.getmro(type(gray))

返回的:

(numpy.ndarray, object)

我们可以看到返回的图像的类型是:ndarray , 这与前面都不一样,前面我们处理的图片的类型都是 PIL.Image object 对象。但是 OpenCV 把一张图表示成二维比特序列(a two dimensional sequence of bytes),即 ndarray 所定义的类型。

# Lets look at the array contents.
gray
array([[ 40,  39,  39, ...,  77,  76,  75],
       [ 43,  42,  42, ...,  76,  75,  75],
       [ 39,  39,  39, ...,  76,  75,  74],
       ...,
       [ 21,  22,  24, ..., 219, 223, 209],
       [ 18,  20,  22, ..., 196, 206, 196],
       [ 16,  18,  20, ..., 168, 182, 176]], dtype=uint8)

对于这种类型的图片,并不能使用display的方法进行展示,我们还是使用PIL包对图像进行类型的转换a PIL object(take an array of data with a given color format and convert this into a PIL object),再 display

from PIL import Image
image = Image.fromarray(gray, 'L') # "L" is just an array of luminance values in unsigned integers
display(image)

扩展内容:关于opencv中的图片类型:多维数组。因此,array也可以定义为1维的。

import numpy as np
single_dim = np.array([25, 50, 25, 10, 10])

在图像中,这类似于在一个 grayscale 的每一行中的5个像素。 但是实际上,所有image库都倾向于至少包含两个维度,即a width and a height,,用一个矩阵表示。 因此,如果将single_dim放置在另一个数组中,则它将是一个二维数组,其中元素在高度方向上,而元素在高度方向上为5

# So if we put the single_dim inside of another array, this would be a two dimensional array with element in the height direction, and five in the width direction
double_dim = np.array([single_dim])
# 执行
double_dim
# -- 输出:二维数组
array([[25, 50, 25, 10, 10]])

不太理解:这个array第一维度有1个元素,这个元素包含一行5个值;

这看起来应该很熟悉,很像一个列表列表! 让我们看看如果显示新的二维数组是什么样子

display(Image.fromarray(double_dim,"L")) # display 一个数组图
# 输出
-  # 注实际:输出是一个图片

确切地说,实际上是连续的五个具有不同灰度的像素。 numpy库有一个很好的属性,称为shape,它使我们能够查看数组的维数。 shape属性返回一个元组 (height,width)

double_dim.shape
#-- 输出
(1, 5)

于是我们可以查看一下刚才的图片的数组维度:

img.shape
# --输出
(416, 416, 3)

可以看到这个图片有3个维度(height,width,color depth)在这种情况下,像素的颜色表示为三个值的数组。 让我们看一下第一个像素的颜色

first_pixel = img[0][0]  # 坐标(0,0)的元素的值
first_pixel

# -- 输出
array([33,35,53], dtype=uint8)

在这里,我们看到使用整数以RGB模式提供颜色值。 这意味着每种颜色通道都可以具有256个值之一,并且此数据可以表示的唯一颜色总数为256 * 256 * 256,大约为1600万种颜色。 我们将其称为24位颜色,即8 + 8 + 8。 如果您发现自己在买电视,可能会注意到一些昂贵的型号广告上标有10位甚至12位面板。 在这些电视中,红色,绿色和蓝色频道中的每个频道都由10或12位而不是8位表示。对于十位面板,这意味着有10亿种色彩,而12位面板的能力超过680亿 颜色!

对于数组的类型的数据还有一些别的常见操作,比如 reshape – change the number of rows and columns that are represented

print("Original image")
print(gray)

如果我们想把它重新表示为 one dimensional image 我们可以简单的使用 reshape方法

print("New image")
# reshape的参数
image1d = np.reshape(1, gray.shape[0]*gray.shape[1])
print(image1d)
# -- 输出
Original image
[[ 40  39  39 ...  77  76  75]
 [ 43  42  42 ...  76  75  75]
 [ 39  39  39 ...  76  75  74]
 ...
 [ 21  22  24 ... 219 223 209]
 [ 18  20  22 ... 196 206 196]
 [ 16  18  20 ... 168 182 176]]
New image
[[ 40  39  39 ... 168 182 176]]

因此,为什么我们要讨论这些嵌套的字节数组(nested arrays of bytes),我们本应该将OpenCV库。

还记得在上一堂课中我们想要查找图像中的 gaps 以便绘制线条以输入到 kranken吗? 我们使用了 PIL 模块,使用 getpixel() 查看单个像素并查看 luminosity,然后使用 ImageDraw.rectangle 实际填充黑条分隔符。 这是一个很好的高级API,让我们编写例程即可完成所需的工作,而不必过多地了解图像的存储方式。 但这在计算上非常慢。

但我们同样可以通过操作数组的值来完成它:

import cv2 as cv
img = cv.imread('readonly/two_col.png')
# 转换成灰度图
gray = cv.cvtColor(img, cv.COLOR_BGR2GRAY)

回忆列表切片的工作原理,如果您有一个数字列表,例如 a = [0,1,2,3,4,5],则 a[2:4] 将返回位置 at position 2 through 4 - 不要忘记列表从0开始索引!

如果我们有一个二维数组,则可以使用 a[2:4, 1:3] 格式将其切成较小的一部分。 您可以将其想象为先沿行维切片,然后沿列维切片。 因此,在此示例中,它将是第2行和第3行以及第1列和第2列的矩阵。这是我们的图像。

gray[2:4, 1:3]
# 输出
array([[255, 255],
       [255, 255]], dtype=uint8)

因此,我们看到它这个区域全是白色的。 我们可以将其用作“窗口”,并围绕我们的大图片进行移动。

最后,ndarray 库具有许多矩阵函数,这些函数通常运行速度非常快。 在这种情况下,我们要考虑的一个是count_nonzer(),它仅返回矩阵中不为零的条目数。

np.count_nonzero(gray[2:4, 1:3])
# 输出
4

好的,采用这种低级方法处理图像的最后一个好处是,我们也可以非常快速地更改像素。 前面我们使用的方法是绘制矩形并设置填充和线宽。 如果您要执行以下操作(例如,更改线条的填充颜色或绘制复杂形状),这将非常好。 但是我们真的只想在这里打线。 这真的很容易-我们只想将亮度值从255更改为0。

举个例子,我们创建一个 白色 矩阵

white_matrix = np.full((12,12), 255, dtype=np.uint8) #numpy创建数组
display(Image.fromarray(white_matrix, "L"))
white_matrix  # 忽略黑色部分截图残留

image-20200608234036958

array([[255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255],
    [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255],
    [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255],
    [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255],
    [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255],
    [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255],
    [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255],
    [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255],
    [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255],
    [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255],
    [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255],
    [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255]],
   dtype=uint8)

我们可以简单的把这个白色方块中的一列变成黑色

white_matrix[:, 6] = np.full((1,12), 0, dtype=np.uint8)

display(Image.fromarray(white_matrix, "L"))
white_matrix 

image-20200608234057152

array([[255, 255, 255, 255, 255, 255,   0, 255, 255, 255, 255, 255],
    [255, 255, 255, 255, 255, 255,   0, 255, 255, 255, 255, 255],
    [255, 255, 255, 255, 255, 255,   0, 255, 255, 255, 255, 255],
    [255, 255, 255, 255, 255, 255,   0, 255, 255, 255, 255, 255],
    [255, 255, 255, 255, 255, 255,   0, 255, 255, 255, 255, 255],
    [255, 255, 255, 255, 255, 255,   0, 255, 255, 255, 255, 255],
    [255, 255, 255, 255, 255, 255,   0, 255, 255, 255, 255, 255],
    [255, 255, 255, 255, 255, 255,   0, 255, 255, 255, 255, 255],
    [255, 255, 255, 255, 255, 255,   0, 255, 255, 255, 255, 255],
    [255, 255, 255, 255, 255, 255,   0, 255, 255, 255, 255, 255],
    [255, 255, 255, 255, 255, 255,   0, 255, 255, 255, 255, 255],
    [255, 255, 255, 255, 255, 255,   0, 255, 255, 255, 255, 255]],
   dtype=uint8)

这正是我们想要做的。 那么,当它看起来低得多时,为什么要用这种方式呢? 真的,答案就是速度。 使用矩阵存储和处理图像数据字节的这种方式与低级API和硬件开发人员如何考虑将文件和字节存储在内存中的方式非常接近。

它快多少? 好吧,这取决于您发现; 本周有一项可选任务,将我们的旧代码转换为这种新格式,以比较两种不同方法的可读性和速度。

OpenCV

好的,我们马上要做本课程的项目。如果您整体上对本系列课程进行反思,是一开始对Python 可能根本不了解,在数字教科书的帮助下逐步学习了python包含的基本控制结构和库,使用对象(object)对数据和函数的表示,现在开始探索存在于python的第三方库,该库允许您操纵和显示图像。这是一个很大的成就!

但是,您一直坚持不懈(persisted),在我们进入项目之前,我想与您分享另外一组 feature。 OpenCV 库包含对图像进行人脸检测(face detection on images)的机制。使用的技术基于Haar级联(Haar cascades),这是一种机器学习方法。现在,我们将不涉及机器学习的知识,我们还专门研究了使用Python的应用数据科学,如果您对该主题感兴趣的话,可以在此基础上进行学习。但是在这里,我们将OpenCV视为black box。

OpenCV附带(come with)了已经训练好的模型,用于检测面部,眼睛和微笑。

你也可以训练模型来检测其他事物(例如热狗或长笛flutes),如果对此感兴趣,我建议您查看有关如何训练级联分类器的Open CV文档:https://docs.opencv.org/3.4/dc/d88/tutorial_traincascade.html,但是,在本讲座中,我们只想使用当前的分类器,看看是否可以检测到图像中我们感兴趣的部分。

代码如下:

import cv2 as cv
# 读入已经训练好的模型
face_cascade = cv.CascadeClassifier('readonly/haarcascade_frontalface_default.xml')
eye_cascade = cv.CascadeClassifier('readonly/haarcascade_eye.xml')

# 检测face
# 读入图片 - 转换成灰度图
img = cv.imread('readonly/floyd.jpg')
gray = cv.cvtColor(img, cv.COLOR_BGR2GRAY)
# 使用 face_cascade classifier, 可以查看文档,但一般使用 detectMultiScale() 函数
# 此函数以矩形形式返回对象列表。 第一个参数是图像的ndarray。
faces = face_cascade.detectMultiScale(gray)
# And lets just print those faces out to the screen
faces

输出:

array([[158,  75, 176, 176]], dtype=int32)

这个函数?

face.tolist()[0]
[158, 75, 176, 176]

产生的矩形框的格式是(x,y,w,h) ,其中 x 和 y 代表的是左上角点的坐标,w和h代表bounding box 的宽和高。

有了这个参数之后,我们可以使用前面我们在PIL库中的方法

from PIL import Image
# 创建一个图片的 PIL 对象
pil_img = Image.fromarray(gray,mode='L')
# 创建绘制对象
from PIL import ImageDraw
drawing = ImageDraw.Draw(pil_img)
# 尺寸参数
rec = faces.tolist()[0]
drawing.rectangle(rec, outline='white')
# 显示效果
display(pil_img)
image-20200612092943902

显然这个结果不是我们想要的,哪里出错了呢?

Well, a quick double check of the docs and it is apparent that OpenCV is return the coordinatesas (x,y,w,h), while PIL.ImageDraw is looking for (x1,y1,x2,y2).

我们重新写一下代码:

pil_img=Image.fromarray(gray,mode="L")
# Setup our drawing context
drawing=ImageDraw.Draw(pil_img)
# And draw the new box
drawing.rectangle((rec[0],rec[1],rec[0]+rec[2],rec[1]+rec[3]), outline="white")
# And display
display(pil_img)
image-20200612093222441

可以看到检测结果相当不错,请注意,这显然不是头部检测,但是我们使用的haarcascades文件寻找的是眼睛和嘴巴。 让我们尝试一些更复杂的事情,让我们读取一个动态图试试

img = cv.imread('readonly/msi_recruitment.gif')
display(Image.fromarray(img))

输出:

---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-36-a6d9e3885cc1> in <module>
      4 img = cv.imread('readonly/msi_recruitment.gif')
      5 # And lets take a look at that image
----> 6 display(Image.fromarray(img))

/opt/conda/lib/python3.7/site-packages/PIL/Image.py in fromarray(obj, mode)
   2506     .. versionadded:: 1.1.6
   2507     """
-> 2508     arr = obj.__array_interface__
   2509     shape = arr['shape']
   2510     ndim = len(shape)

AttributeError: 'NoneType' object has no attribute '__array_interface__'

事实证明,此错误的根源是OpenCV无法处理 Gif 图像。 这是一种痛苦和不幸。 但是我们知道如何解决这个问题? 一个是我们可以在PIL中打开它,然后将其另存为png,然后在open cv中打开它。

# 让我们使用 PIL 打开gif图片
pil_img = Image.open('readonly/msi_recruitment.gif')

# 现在opencv可以处理这个 PIL 对象格式的图片了
# 转化成灰度图像并保存
open_cv_version = pil_img.convert('L')
open_cv_version.save('msi_recruitment.png')

现在图像已经处理为 opencv 可以读取的png文件,我们来尝试检测这个图里的人脸:

cv_img = cv.imread('msi_recruitment.png')
# 不需要转灰度图,已经是灰度图了
# 注意这幅图里不止一个人脸
faces = face_cascade.detectMultiScale(cv_img)
# 我们使用PIL绘制face的坐标
pil_img = Image.open('readonly/msi_recruitment.gif')
drawing = ImageDraw.Draw(pil_img)
for x,y,w,h in faces:
    '''这可能是您的新语法! 回想一下,faces是(x,y,w,h)格式的矩形列表,即列表列表。 不必进行迭代然后手动提取每个项目,我们可以使用元组拆包将子列表中的单个项目直接提取到变量中。 一个非常好的python feature'''
    drawing.rectangle((x,y,x+w,y+h), outline='white')

display(pil_img)
image-20200612101214765

这里发生了什么!? 我们看到已经检测到脸部,并且在图像上的那些脸部周围绘制了框,但是颜色变得很奇怪了! 事实上,这与gif格式的图像的颜色限制有关。 简而言之,gif图像的颜色数量非常有限。a gif image:在调色板艺术家(pallette artists)用来混合颜料之后,这称为调色板(a color pallette)。For gifs the pallette can only be 256 colors-但它们可以是 any 256色。 引入新颜色时,必须占用旧颜色的空间。 在这种情况下,PIL将白色添加(绘制)到调色板(gif图像中)中,但不知道要替换哪种颜色,从而使图像混乱。

谁知道有太多关于图像格式的知识? 我们可以使用.mode属性查看图像处于哪种模式?

pil_img.mode
# 经过绘制的PIL图片的格式输出-----
'p'

在PIL库的文档中我们能法线很多的图片的mode,他们对应于不同的颜色空间,在这里我们把这个图片的mode转换到RGB色彩空间试试

pil_img = Image.open('readonly/msi_recruiment.gif')
pil_img = pil_image.covert('RGB')
pil_img.mode
'RGB'

好了,现在我们再尝试绘制正方形(faces)

drawing = ImageDraw(pil_img)
for x,y,w,h in faces:
    drawing.rectangle((x,y,x+w,y+h), outline='white')
    
display(pil_img)
image-20200612102637246

太棒了! 我们设法在该图像中检测出一堆人脸。 好像我们错过了四张脸。 在机器学习世界中,我们将这些称为假阴性:false negatives – 机器认为不是面孔(so a negative)的东西,但是在上面是不正确的。 因此,我们将检测到的真实面孔称为真阳性(true positives)-- 机器认为是正确的面孔。

此外还有误报(假阳性:false positives)-机器认为这是一张面孔,但事实上不是。 我们在图像中看到其中两个假阳性的结果,拾取衬衫中的阴影图案或纹理,并将其与 haarcascades匹配。

最后,我们有真阴性(true negatives),或者机器学习分类器可以考虑的所有可能矩形的集合,它正确地表明结果不是人脸。

我们还是可以尝试提高这个检测结果,要为给定图像找到良好的价值,需要进行大量的实验。 首先,让我们创建一个函数,该函数将在图像上为我们绘制矩形

# 创建在图片上画矩形的函数
def show_rects(faces):
    # 读取图片-创建绘制对象-绘制矩形(参数)
    pil_img = Image.open('readonly/msi_recruitment.gif').convert('RGB')
    drawing = ImageDraw.Draw(pil_img)
    for x,y,w,h in faces:    # 输入 faces 的坐标
        drawing.rectangle((x,y,x+w,y+h), outline='white')
    display(pil_img)

首先,我们可以尝试对图片做二值化(binarize)处理。 opencv库内置有二值化函数 threshold(). 根据函数的文档,是要输入参数:

  • the image,

  • the midpoint, and

  • the maximum value

  • a flag which indicates whether the threshold should bebinary or something else.

cv_img_bin = cv.threshold(img, 120, 255, cv.THRESH_BINARY)[1] # 将返回一个list,我们需要第二个值
# 为什么要第二个值?
# 对这张图片进行面部识别,获取面部坐标list
faces = face_cascade.detectMultiScale(cv_img_bin) 
# 在原图上绘制得到的face坐标的方框
show_rects(faces)
image-20200612151031548

这个运行结果图好像有问题?

很有意思。 不,但是我们确实看到底部有一个误报,分类器在其中检测到太阳镜是眼睛,下面是深色阴影线是嘴。
如果您在笔记本中观看此视频,为什么不暂停一下并尝试一些不同的阈值参数?

直接调整 xml模型参数

OpenCV的detectMultiScale()函数还具有几个参数。 首先是比例因子(scale factor)。 比例因子更改了针对模型(即haarcascades XML文件)考虑的矩形的大小。 您可以认为它好像在改变屏幕上矩形的大小。

# Lets experiment with the scale factor. Usually it's a small value, lets try 1.05
faces = face_cascade.detectMultiScale(cv_img,1.05)
# Show those results
show_rects(faces)
# Now lets also try 1.15
faces = face_cascade.detectMultiScale(cv_img,1.15)
# Show those results
show_rects(faces)
# Finally lets also try 1.25
faces = face_cascade.detectMultiScale(cv_img,1.25)
# Show those results
show_rects(faces)
image-20200612151551395 image-20200612151608049 image-20200612151622883

我们可以看到,随着我们改变比例因子,我们改变了 true and flase positives and negatives。 将 scale 设置为1.05时,我们有7个 true positives(正确识别的面孔)和3个 false negatives(存在但未检测到的面孔)和3个 false positives(其中opencv认为是面孔的非面孔)。 当我们将scale 更改为1.15时,我们会丢失误报(false positives),但也会丢失其中一个 true positives,即右边戴着帽子的女士。 当我们将其更改为1.25时,我们也会损失更多的 true positives

在机器学习和人工智能中,这实际上是一个非常有趣的现象。 不仅要在模型的精确度与不精确度之间进行权衡。 您认为这三种模式中哪一种最好?

好吧,这个问题的答案实际上是“取决于”。这取决于您为什么要检测面部,以及如何处理面部。如果您认为这些问题很有趣,则可能需要查看Applied Data Science with Python specialization Michigan offers on Coursera.。

好的,除了做广告的机会,您还注意到我们更改比例因子时发生的其他事情吗?这很微妙,但是在较小的比例因子下,处理运行的速度会花费更长的时间。这是因为小的比例因此将处理更多的subimage。这也可能会影响我们可能使用的方法。

Jupyter 对计时命令(timing commands)有很好的支持。您可能以前已经知道这一点,**在jupyter中以百分号开头的行称为“魔术函数”。**这不是普通的python-实际上是编写Jupyter预定义的函数的简便方法。看起来很像我们在上一讲中讨论过的装饰器,但是魔术功能早在装饰器成为python语言的一部分之前就已经存在了。虚拟机中的一个内置魔术功能称为 timeit ,它会重复一段python十次(默认情况下),并告诉您完成该程序所需的平均速度。

# Lets time the speed of detectmultiscale when using a scale of 1.05
%timeit face_cascade.detectMultiScale(cv_img,1.05)

您会看到这是一个巨大的差异,使用较小的比例尺时,速度大约慢两倍半!

到此结束了我们在opencv中检测面部的讨论。 您会看到,就像OCR一样,这不是一个简单(foolproof)的过程。 但是我们可以在他人在机器学习中所做的工作的基础上,并利用强大的库使我们更接近构建一揽子基于python的解决方案。 请记住,检测机制并不特定于面部,而只是我们使用的 haarcascades 训练数据。 在网络上,您将能够找到其他训练数据来检测其他物体,包括眼睛,动物等。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值