opencv进阶一#

人脸识别#

在Opencv中人脸识别是基于Haar特征+Adaboost级联分类器来实现人脸识别的!

要理解这节内容,我们首先要明白什么是特征?

特征其实就是某个区域的像素点经过运算之后得到的结果! 例如haar特征其实就是用下图列出的模板在图像中滑动,计算白色区域覆盖的像素之和减去黑色区域覆盖的像素之和,运算出来的结果就是haar特征值!

Haar特征一般和Adaboost分类器结合在一起进行目标识别!

这里需要运动机器学习的知识! 不过值得庆幸的是Opencv已经为我们训练好了数据,并且已经提取出了人脸的特征,在opencv的源码中有相应的xml特征文件. 并且我们只需要调用opencv提供好的API即可快速完成人脸识别的功能!

核心api为:

1
2
3
4
# 加载已经训练好的特征文件
faces_xml = cv.CascadeClassifier("assets/haarcascade_frontalface_default.xml")
# 根据特征文件去查找人脸
faces_xml.detectMultiScale(图像, 缩放系数, 至少检验次数)

实现步骤:

  1. 加载特征xml文件
  2. 加载图片
  3. 灰度处理
  4. 判决
  5. 绘制出检测出来的人脸

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import cv2 as cv
# 第1步:加载xml文件
faces_xml = cv.CascadeClassifier("assets/haarcascade_frontalface_default.xml")
eyes_xml = cv.CascadeClassifier("assets/haarcascade_eye.xml")

# 第2步:加载图片
img = cv.imread("img/lena.jpg", cv.IMREAD_COLOR)

# 第3步:将图片转成灰色图片
gray = cv.cvtColor(img, cv.COLOR_BGR2GRAY)

# 第4步:使用api进行人脸识别 参数2:缩放系数  参数3:至少要检测几次才算正确
faces = faces_xml.detectMultiScale(gray, 1.3, 5)
print("找到人脸的数量:",len(faces))

# 在人脸上绘制矩形
for (x,y,w,h) in faces:
    # 在找到人脸上画矩形
    cv.rectangle(img,(x,y),(x+w,y+h),(0,255,0),5)

    # 从灰色图片中找到人脸
    grayFace = gray[y:y+h,x:x+w]
    colorFace = img[y:y+h,x:x+w]
    # 在当前人脸上找到眼睛的位置
    eyes = eyes_xml.detectMultiScale(grayFace,1.3,5)
    print("当前人脸上眼睛数量:",len(eyes))
    # 在眼睛上绘制矩形
    for (e_x,e_y,e_w,e_h) in eyes:
        cv.rectangle(colorFace,(e_x,e_y),(e_x+e_w,e_y+e_h),(0,0,255),3)

cv.imshow('result',img)
cv.waitKey(0)
cv.destroyAllWindows()

HSV颜色模型#

HSV(Hue, Saturation, Value)是根据颜色的直观特性由A. R. Smith在1978年创建的一种颜色空间, 也称六角锥体模型(Hexcone Model)。

这个模型中颜色的参数分别是:色调(H),饱和度(S),明度(V)

色调H#

用角度度量,取值范围为0°~360°,从红色开始按逆时针方向计算,红色为0°,绿色为120°,蓝色为240°。它们的补色是:黄色为60°,青色为180°,品红为300°;

饱和度S#

饱和度S表示颜色接近光谱色的程度。一种颜色,可以看成是某种光谱色与白色混合的结果。其中光谱色所占的比例愈大,颜色接近光谱色的程度就愈高,颜色的饱和度也就愈高。饱和度高,颜色则深而艳。光谱色的白光成分为0,饱和度达到最高。通常取值范围为0%~100%,值越大,颜色越饱和。

明度V#

明度表示颜色明亮的程度,对于光源色,明度值与发光体的光亮度有关;对于物体色,此值和物体的透射比或反射比有关。通常取值范围为0%(黑)到100%(白)。

结论:

  1. 当S=1 V=1时,H所代表的任何颜色被称为纯色;

  2. 当S=0时,即饱和度为0,颜色最浅,最浅被描述为灰色(灰色也有亮度,黑色和白色也属于灰色),灰色的亮度由V决定,此时H无意义;

  3. 当V=0时,颜色最暗,最暗被描述为黑色,因此此时H(无论什么颜色最暗都为黑色)和S(无论什么深浅的颜色最暗都为黑色)均无意义。

注意: 在opencv中,H、S、V值范围分别是[0,180],[0,255],[0,255],而非[0,360],[0,1],[0,1];

这里我们列出部分hsv空间的颜色值, 表中将部分紫色归为红色

判断当前是白天还是晚上#

实现步骤#

  1. 将图片从BGR颜色空间,转变成HSV颜色空间
  2. 获取图片的宽高信息
  3. 统计每个颜色点的亮度
  4. 计算整张图片的亮度平均值

注意,这仅仅只能做一个比较粗糙的判定,按照我们人的正常思维,在傍晚临界点我们也无法判定当前是属于晚上还是白天!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import cv2 as cv
import numpy as np

def average_brightness(img):
    """封装一个计算图片平均亮度的函数"""
    imgInfo = img.shape
    height = imgInfo[0]
    width = imgInfo[1]

    hsv_img = cv.cvtColor(img, cv.COLOR_BGR2HSV)
    # 提取出v通道信息
    v_day = cv.split(hsv_img)[2]
    # 计算亮度之和
    result = np.sum(v_day)
    # 返回亮度的平均值
    return result/(height*width)

# 计算白天的亮度平均值
day_img = cv.imread("assets/day.jpg", cv.IMREAD_COLOR)
brightness1 = average_brightness(day_img)
print("day brightness1:",brightness1);

# 计算晚上的亮度平均值
night_img = cv.imread("assets/night.jpg", cv.IMREAD_COLOR)
brightness2 = average_brightness(night_img)
print("night brightness2:",brightness2)


cv.waitKey(0)
cv.destroyAllWindows()

颜色过滤#

在一张图片中,如果某个物体的颜色为纯色,那么我们就可以使用颜色过滤inRange的方式很方便的来提取这个物体.

下面我们有一张网球的图片,并且网球的颜色为一定范围内的绿色,在这张图片中我们找不到其它颜色也为绿色的图片,所以我们可以考虑使用绿色来提取它!

图片的颜色空间默认为BGR颜色空间,如果我们想找到提取纯绿色的话,我们可能需要写(0,255,0)这样的内容,假设我们想表示一定范围的绿色就会很麻烦!

所以我们考虑将它转成HSV颜色空间,绿色的色调H的范围我们很容易知道,剩下的就是框定颜色的饱和度H和亮度V就可以啦!

实现步骤:

  1. 读取一张彩色图片
  2. 将RGB转成HSV图片
  3. 定义颜色的范围,下限位(30,120,130),上限为(60,255,255)
  4. 根据颜色的范围创建一个mask
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
import cv2 as cv
# 读取图片
rgb_img = cv.imread("assets/tenis1.jpg", cv.IMREAD_COLOR)
cv.imshow("rgb_img",rgb_img)
# 将BGR颜色空间转成HSV空间
hsv_img = cv.cvtColor(rgb_img, cv.COLOR_BGR2HSV)

# 定义范围 网球颜色范围
lower_color = (30,120,130)
upper_color = (60,255,255)

# 查找颜色
mask_img = cv.inRange(hsv_img, lower_color, upper_color)
# 在颜色范围内的内容是白色, 其它为黑色
cv.imshow("mask_img",mask_img)

cv.waitKey(0)
cv.destroyAllWindows()

替换背景案例#

实现步骤#

  1. 从绿幕图片中过滤出绿幕
  2. 将狮子从绿幕中抠出来
  3. 在itheima图片上抠出狮子的位置
  4. 将狮子和黑马图片进行相加得到最终的图片
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import cv2 as cv
# 1.读取绿幕图片
green_img = cv.imread("assets/lion.jpg", cv.IMREAD_COLOR)

hsv_img = cv.cvtColor(green_img,cv.COLOR_BGR2HSV)
# 2. 定义绿幕的颜色范围
lower_green = (35,43,60)
upper_green = (77,255,255)
# 3. 使用inrange找出所有的背景区域
mask_green = cv.inRange(hsv_img, lower_green, upper_green)

# 复制狮子绿幕图片
mask_img = green_img.copy()
# 将绿幕图片,对应蒙板图片中所有不为0的地方全部改成0
mask_img[mask_green!=0]=(0,0,0)
cv.imshow("dst",mask_img)

# itheima图片 对应蒙板图片为0的地方全都改成0,抠出狮子要存放的位置
itheima_img = cv.imread("assets/itheima.jpg", cv.IMREAD_COLOR)
itheima_img[mask_green==0]=(0,0,0)
cv.imshow("itheima",itheima_img)

# 将抠出来的狮子与处理过的itheima图片加载一起
result = itheima_img+mask_img
cv.imshow("result",result)

cv.waitKey(0)
cv.destroyAllWindows()

图像的二值化#

图像二值化( Image Binarization)就是将图像上的像素点的灰度值设置为0255,也就是将整个图像呈现出明显的黑白效果的过程。

在数字图像处理中,二值图像占有非常重要的地位,图像的二值化使图像中数据量大为减少,从而能凸显出目标的轮廓。

1
所使用的阈值,结果图片 = cv.threshold(img,阈值,最大值,类型)

THRESH_BINARY 高于阈值改为255,低于阈值改为0
THRESH_BINARY_INV 高于阈值改为0,低于阈值改为255
THRESH_TRUNC 截断,高于阈值改为阈值,最大值失效
THRESH_TOZERO 高于阈值不改变,低于阈值改为0
THRESH_TOZERO_INV 高于阈值该为0,低于阈值不改变

简单阈值#

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import cv2 as cv

# 读取图像
img = cv.imread("assets/car.jpg",cv.IMREAD_GRAYSCALE)
# 显示图片
cv.imshow("gray",img)
# 获取图片信息
imgInfo = img.shape
height = imgInfo[0]
width = imgInfo[1]

# 定义阈值
thresh = 60

for row in range(height):
    for col in range(width):
        # 获取当前灰度值
        grayValue = img[row,col]
        if grayValue>thresh:
            img[row,col]=255
        else:
            img[row,col]=0

# 直接调用api处理 返回值1:使用的阈值, 返回值2:处理之后的图像
# ret,thresh_img = cv.threshold(img, thresh, 255, cv.THRESH_BINARY)

# 显示修改之后的图片
cv.imshow("thresh",img);

cv.waitKey(0)
cv.destroyAllWindows()

自适应阈值#

我们使用一个全局值作为阈值。但是在所有情况下这可能都不太好,例如,如果图像在不同区域具有不同的照明条件。在这种情况下,自适应阈值阈值可以帮助。这里,算法基于其周围的小区域确定像素的阈值。因此,我们为同一图像的不同区域获得不同的阈值,这为具有不同照明的图像提供了更好的结果。

除上述参数外,方法cv.adaptiveThreshold还有三个输入参数:

adaptiveMethod决定阈值是如何计算的:

  • cv.ADAPTIVE_THRESH_MEAN_C:该阈值是该附近区域减去恒定的平均Ç
  • cv.ADAPTIVE_THRESH_GAUSSIAN_C:阈值是邻域值减去常数C的高斯加权和。

BLOCKSIZE确定附近区域的大小和Ç是从平均值或附近的像素的加权和中减去一个常数。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
import cv2 as cv

# 读取图像
img = cv.imread("assets/thresh1.jpg",cv.IMREAD_GRAYSCALE)
# 显示图片
cv.imshow("gray",img)
# 获取图片信息
imgInfo = img.shape

# 直接调用api处理 参数1:图像数据 参数2:最大值  参数3:计算阈值的方法, 参数4:阈值类型 参数5:处理块大小  参数6:算法需要的常量C
thresh_img = cv.adaptiveThreshold(img,255,cv.ADAPTIVE_THRESH_GAUSSIAN_C,cv.THRESH_BINARY,11,5)

# 显示修改之后的图片
cv.imshow("thresh",thresh_img);

cv.waitKey(0)
cv.destroyAllWindows()

THRESH_OTSU#

采用日本人大津提出的算法,又称作最大类间方差法,被认为是图像分割中阈值选取的最佳算法,采用这种算法的好处是执行效率高!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
import cv2 as cv

# 读取图像
img = cv.imread("assets/otsu_test.png",cv.IMREAD_GRAYSCALE)
cv.imshow("src",img)

ret,thresh_img = cv.threshold(img, 225, 255, cv.THRESH_BINARY_INV)
cv.imshow("normal", thresh_img);

gaussian_img = cv.GaussianBlur(img,(5,5),0)
cv.imshow("g",gaussian_img)

ret,thresh_img = cv.threshold(gaussian_img, 0, 255, cv.THRESH_BINARY|cv.THRESH_OTSU)
cv.imshow("otsu", thresh_img);

print("阈值:",ret)
cv.waitKey(0)
cv.destroyAllWindows()

图像的噪声#

如果我们把图像看作信号,那么噪声就是干扰信号。我们在采集图像时可能因为各种各样的干扰而引入图像噪声。在计算机中,图像就是一个矩阵, 给原始图像增加噪声, 我们只需要让像素点加上一定灰度即可. f(x, y) = I(x, y) + noise

常见的噪声有椒盐噪声(salt and pepper noise),为什么叫椒盐噪声?因为图像的像素点由于噪声影响随机变成了黑点(dark spot)或白点(white spot)。这里的“椒”不是我们常见的红辣椒或青辣椒,而是外国的“胡椒”(香料的一种)。我们知道,胡椒是黑色的,盐是白色的,所以才取了这么个形象的名字.

接下来我们来生成10%的椒噪声和盐噪声:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# 创建和原图同大小随机矩阵 胡椒噪声
pepper_noise = np.random.randint(0,256,(height,width))
# 创建和原图同大小随机矩阵 盐噪声
salt_noise = np.random.randint(0,256,(height,width))
# 定义10%的噪声 256×10%=25.6
ratio = 0.1
# 若值小于25.6 则置为-255,否则为0
pepper_noise = np.where(pepper_noise < ratio*256,-255,0)
# 若值大于25.6 则置为255,否则为0
salt_noise = np.where(salt_noise < ratio*256,255,0)

我们还要注意,opencv的图像矩阵类型是uint8,低于0和高于255的值并不截断,而是使用了模操作。即200+60=260 % 256 = 4。所以我们需要先将原始图像矩阵和噪声图像矩阵都转成浮点数类型进行相加操作,然后再转回来。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# 将uint8类型转成浮点类型
img.astype("float")
pepper_noise.astype("float")
salt_noise.astype("float")

# 将 胡椒噪声 添加到原图中
dst_img = img + pepper_noise
# 校验越界问题
dst_img = np.where(dst_img > 255,255,dst_img)
dst_img = np.where(dst_img < 0,0,dst_img)

cv.imshow("pepper img",dst_img.astype("uint8"))

# 将 盐噪声 添加到原图中
dst_img = img + salt_noise
# 校验越界问题
dst_img = np.where(dst_img > 255,255,dst_img)
dst_img = np.where(dst_img < 0,0,dst_img)

cv.imshow("salt img",dst_img.astype("uint8"))


# 将 椒盐噪声 添加到原图中
dst_img = img + pepper_noise + salt_noise
# 校验越界问题
dst_img = np.where(dst_img > 255,255,dst_img)
dst_img = np.where(dst_img < 0,0,dst_img)

cv.imshow("pepper salt img",dst_img.astype("uint8"))