Bootstrap

11月5日 OpenCV 实战基础学习笔记——图像金字塔、轮廓、模板匹配


前言

本文为11月5日 OpenCV 实战基础学习笔记——图像金字塔、轮廓、模板匹配,分为六个章节:

  • 图像金字塔;
  • 图像轮廓;
  • 模板匹配;
  • 直方图;
  • 傅里叶变换;
  • 信用卡识别实战。

一、图像金字塔

1、高斯金字塔

  • 向下采样(缩小):

    • 与高斯内核卷积;
    • 去除偶数行和列。
  • 向上采样(放大):

    • 行和列扩大为原来的两倍,新增的行和列用 0 填充;
    • 与之前的内核(乘以 4)卷积,获得近似值。
img = cv.imread('AM.png')
img = cv.cvtColor(img, cv.COLOR_BGR2RGB)
plt.imshow(img)
print(img.shape)
>>> (442, 340, 3)

1

img_up = cv.pyrUp(img)
plt.imshow(img_up)
print(img_up.shape)
>>> (884, 680, 3)

2

img_down = cv.pyrDown(img)
plt.imshow(img_down)
print(img_down.shape)
>>> (221, 170, 3)

3

# 先上采样再下采样,图像会损失信息,变得模糊
img_up_down = cv.pyrDown(img_up)
res = np.hstack((img, img_up_down))
plt.imshow(res)

4

2、拉普拉斯金字塔

L i = G i − P y r U p ( P y r D o w n ( G I ) ) L_i = G_i - PyrUp(PyrDown(G_I)) Li=GiPyrUp(PyrDown(GI))

5

img_down_up = cv.pyrUp(img_down)
L1 = img - img_down_up
plt.imshow(L1)

5

二、图像轮廓

cv2.findContours(img,mode,method):

  • mode: 轮廓检索模式

    • RETR_EXTERNAL :只检索最外面的轮廓;
    • RETR_LIST:检索所有的轮廓,并将其保存到一条链表当中;
    • RETR_CCOMP:检索所有的轮廓,并将他们组织为两层:顶层是各部分的外部边界,第二层是空洞的边界;
    • RETR_TREE:检索所有的轮廓,并重构嵌套轮廓的整个层次;
  • method: 轮廓逼近方法

    • CHAIN_APPROX_NONE:以Freeman链码的方式输出轮廓,所有其他方法输出多边形(顶点的序列);
    • CHAIN_APPROX_SIMPLE:压缩水平的、垂直的和斜的部分,也就是,函数只保留他们的终点部分。
contours = cv.imread('contours.png')
contours_gray = cv.cvtColor(contours, cv.COLOR_BGR2GRAY)
ret, thresh = cv.threshold(contours_gray, 127, 255, cv.THRESH_BINARY)

plt.imshow(thresh, cmap='gray')

6

contours, hierarchy = cv.findContours(thresh, cv.RETR_TREE, cv.CHAIN_APPROX_NONE)

np.array(contours).shape
>>> (11,)

1、绘制轮廓

# 传入图像、轮廓、索引、颜色模式、线条厚度
# 需要备份图片
img_copy = img.copy()
res = cv.drawContours(img_copy, contours, -1, (255, 0, 0), 2) # -1: 所有轮廓;(0, 0, 255) B G R; 2:线条宽度
plt.imshow(res)

7

2、轮廓特征

c1 = contours[0]

# 面积
cv.contourArea(c1)
>>> 8500.5

# 周长,True 表示闭合的
cv.arcLength(c1, True)
>>> 437.9482651948929

3、轮廓近似

contours2 = cv.imread('contours2.png')
contours2_RGB = cv.cvtColor(contours2, cv.COLOR_BGR2RGB)
contours2_gray = cv.cvtColor(contours2, cv.COLOR_BGR2GRAY)

plt.imshow(contours2_RGB)

8

ret, thresh = cv.threshold(contours2_gray, 127, 255, cv.THRESH_BINARY)

contours, hierarchy = cv.findContours(thresh, cv.RETR_TREE, cv.CHAIN_APPROX_NONE)
cnt = contours[0]

draw_contours2 = contours2_RGB.copy()
res = cv.drawContours(draw_contours2, [cnt], -1, (255, 0, 0), 2)
plt.imshow(res)

9

# 近似
epsilon = 0.05 * cv.arcLength(cnt, True)
approx = cv.approxPolyDP(cnt, epsilon, True)

draw_contours2 = contours2_RGB.copy()
res = cv.drawContours(draw_contours2, [approx], -1, (255, 0, 0), 2)
plt.imshow(res)

10

4、外接矩形

contours = cv.imread('contours.png')
contours_RGB = cv.cvtColor(contours, cv.COLOR_BGR2RGB)
contours_gray = cv.cvtColor(contours, cv.COLOR_BGR2GRAY)
ret, thresh = cv.threshold(contours_gray, 127, 255, cv.THRESH_BINARY)
contours, hierarchy = cv.findContours(thresh, cv.RETR_TREE, cv.CHAIN_APPROX_NONE)
cnt = contours[0]

x, y, w, h = cv.boundingRect(cnt)
img = cv.rectangle(contours_RGB, (x, y), (x+w, y+h), (0, 255, 0), 2)
plt.imshow(img)

11

area = cv.contourArea(cnt)
x, y, w, h = cv.boundingRect(cnt)
rect_area = w * h
extent = float(area) / rect_area
print("轮廓面积与边界矩形的面积之比 = ", extent)
>>> 轮廓面积与边界矩形的面积之比 =  0.5154317244724715

5、外接圆

(x, y), radius = cv.minEnclosingCircle(cnt)
centre = (int(x), int(y))
radius = int(radius)
img = cv.circle(img, centre, radius, (0, 255, 0), 2)
plt.imshow(img)

12

三、模板匹配

模板在原图像上从原点开始滑动,计算模板与图像被模板覆盖的差别,然后将每次计算的结果放入一个矩阵里,作为结果输出。假如原图形是A x B,而模板是 a x b,则输出结果的矩阵是(A-a+1) x (B-b+1)。

1、单个模板匹配

  • TM_SQDIFF:计算平方不同,计算出来的值越小,越相关;
  • TM_CCORR:计算相关性,计算出来的值越大,越相关;
  • TM_CCOEFF:计算相关系数,计算出来的值越大,越相关;
  • TM_SQDIFF_NORMED:计算归一化平方不同,计算出来的值越接近0,越相关;
  • TM_CCORR_NORMED:计算归一化相关性,计算出来的值越接近1,越相关;
  • TM_CCOEFF_NORMED:计算归一化相关系数,计算出来的值越接近1,越相关。
img = cv.imread('lena.jpg', 0) # 0: 以灰度图读入
template = cv.imread('face.jpg', 0)

h, w = template.shape[:2]
template.shape[:2]
>>> (110, 85)

img.shape
>>> (263, 263)

methods = ['cv.TM_CCOEFF', 'cv.TM_CCOEFF_NORMED', 'cv.TM_CCORR',
           'cv.TM_CCORR_NORMED', 'cv.TM_SQDIFF', 'cv.TM_SQDIFF_NORMED']

res = cv.matchTemplate(img, template, cv.TM_SQDIFF)
res.shape
>>> (154, 179)

min_val, max_val, min_loc, max_loc = cv.minMaxLoc(res)
min_val
>>> 39168.0

max_val
>>> 74403584.0

min_loc # 最小值所在位置,关注
>>> (107, 89)

max_loc
>>> (159, 62)

for meth in methods:
    img2 = img.copy()

    # 匹配方法的真值
    method = eval(meth)
    # print(method)
    res = cv.matchTemplate(img, template, method)
    min_val, max_val, min_loc, max_loc = cv.minMaxLoc(res)

    # 如果是平方差匹配TM_SQDIFF或归一化平方差匹配TM_SQDIFF_NORMED,取最小值
    if method in [cv.TM_SQDIFF, cv.TM_SQDIFF_NORMED]:
        top_left = min_loc
    else:
        top_left = max_loc
    bottom_right = (top_left[0] + w, top_left[1] + h)

    # 画矩形
    cv.rectangle(img2, top_left, bottom_right, 255, 2)

    plt.subplot(121), plt.imshow(res, cmap='gray')
    plt.xticks([]), plt.yticks([])  # 隐藏坐标轴
    plt.subplot(122), plt.imshow(img2, cmap='gray')
    plt.xticks([]), plt.yticks([])
    plt.suptitle(meth)
    plt.show()

13
14
15
16
17
17

2、多模板匹配

img = cv.imread('mario.jpg')
img_rgb = cv.cvtColor(img, cv.COLOR_BGR2RGB)
img_gray = cv.cvtColor(img_rgb, cv.COLOR_BGR2GRAY)
template = cv.imread('mario_coin.jpg', 0)
h, w = template.shape[:2]

res = cv.matchTemplate(img_gray, template, cv.TM_CCOEFF_NORMED)
threshold = 0.8 # 取匹配程度大于 80% 的坐标

loc = np.where(res >= threshold)
for pt in zip(*loc[: : -1]):  # *: 可选参数
    bottom_right = (pt[0] + w, pt[1] + h)
    cv.rectangle(img_rgb, pt, bottom_right, (255, 0, 0), 2)
    
plt.imshow(img_rgb)

18

四、直方图

统计图片的灰度值。
cv.calcHist(images, channels, mask, histSize, ranges).

img = cv.imread('cat.jpg', 0)
hist = cv.calcHist([img], [0], None, [256], [0, 256])
hist.shape
>>> (256, 1)

plt.hist(img.ravel(), 256); # img.ravel(): 

19

img = cv.imread('cat.jpg')
color = ['b', 'g', 'r']
for i, col in enumerate(color):
    histr = cv.calcHist([img], [i], None, [256], [0, 256])
    plt.plot(histr, color=col)
    plt.xlim([0, 256])

20

1、mask 操作

mask = np.zeros(img.shape[:2], np.uint8)
print(mask.shape)
mask[100:300, 100:400] = 255
plt.imshow(mask, cmap='gray')
>>> (414, 500)

21

img = cv.imread('cat.jpg', 0)

img_masked = cv.bitwise_and(img, img, mask=mask) # 与操作
plt.imshow(img_masked, cmap='gray')

22

hist_full = cv.calcHist([img], [0], None, [256], [0, 256])
hist_mask = cv.calcHist([img], [0], mask, [256], [0, 256])

plt.figure(figsize=(20, 10))

plt.subplot(231), plt.imshow(img, 'gray')
plt.subplot(232), plt.imshow(mask, 'gray')
plt.subplot(233), plt.imshow(img_masked, 'gray')
plt.subplot(234), plt.plot(hist_full)
plt.subplot(235), plt.plot(hist_mask)

plt.xlim([0, 256])
plt.show()

23

2、直方图均衡化

img = cv.imread('cat.jpg', 0)
plt.hist(img.ravel(), 256);

img_equ = cv.equalizeHist(img)
plt.hist(img_equ.ravel(), 256);

24
25

res = np.hstack((img, img_equ))
plt.imshow(res, cmap='gray')

26

3、自适应直方图均衡化

clahe = cv.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8))

res_clahe = clahe.apply(img)
res = np.hstack((img, img_equ, res_clahe))

plt.figure(figsize=(30, 10))
plt.imshow(res, cmap='gray')

27

五、傅里叶变换

步骤:

  1. cv.dft() 和 cv.idft(),输入图像需先转换成 np.float32 格式;
  2. 结果中,频率为 0 的部分会在左上角,需转换到中心位置,可通过 shift 实现;
  3. cv.dft() 返回的结果是双通道的(实部+虚部),需转换成图像格式才能展示(0, 255)。
img = cv.imread('lena.jpg', 0)
img_float32 = np.float32(img)

dft = cv.dft(img_float32, flags=cv.DFT_COMPLEX_OUTPUT)
dft_shift = np.fft.fftshift(dft)

# 得到灰度图像表示的形式
magnitude_spectrum = 20 * np.log(cv.magnitude(dft_shift[:, :, 0], dft_shift[:, :, 1]))

plt.subplot(121)
plt.imshow(img, cmap='gray')
plt.title("Input")
plt.xticks([])
plt.yticks([])

plt.subplot(122)
plt.imshow(magnitude_spectrum, cmap='gray')
plt.title("Magnitude Spectrum")
plt.xticks([])
plt.yticks([])

plt.show()

28

  • 低通滤波和高通滤波:
    • 低通:只保留低频,使图像模糊;
dft = cv.dft(img_float32, flags=cv.DFT_COMPLEX_OUTPUT)
dft_shift = np.fft.fftshift(dft)

rows, cols = img.shape
crow, ccol = int(rows/2), int(cols/2)

# 低通滤波
mask = np.zeros((rows, cols, 2), np.uint8)
mask[crow-30:crow+30, ccol-30:ccol+30] = 1

# IDFT
fshift = dft_shift * mask
f_shift = np.fft.ifftshift(fshift)
img_back = cv.idft(f_shift)
img_back = cv.magnitude(img_back[:, :, 0], img_back[:, :, 1])

plt.subplot(121)
plt.imshow(img, cmap='gray')
plt.title("Input")
plt.xticks([])
plt.yticks([])

plt.subplot(122)
plt.imshow(img_back, cmap='gray')
plt.title("Result")
plt.xticks([])
plt.yticks([])

plt.figure(figsize=(20, 10))

plt.show()

29

  • 高通:只保留高频,使图像的细节增强。
# 高通滤波
mask = np.ones((rows, cols, 2), np.uint8)
mask[crow-30:crow+30, ccol-30:ccol+30] = 0

# IDFT
fshift = dft_shift * mask
f_shift = np.fft.ifftshift(fshift)
img_back = cv.idft(f_shift)
img_back = cv.magnitude(img_back[:, :, 0], img_back[:, :, 1])

plt.subplot(121)
plt.imshow(img, cmap='gray')
plt.title("Input")
plt.xticks([])
plt.yticks([])

plt.subplot(122)
plt.imshow(img_back, cmap='gray')
plt.title("Result")
plt.xticks([])
plt.yticks([])

plt.figure(figsize=(20, 10))

plt.show()

30

六、信用卡识别实战

import cv2 as cv
import numpy as np
import myutils
import matplotlib.pyplot as plt

# 绘图展示
def cv_show(name,img):
	cv.imshow(name, img)
	cv.waitKey(0)
	cv.destroyAllWindows()

# 指定信用卡类型
FIRST_NUMBER = {
	"3": "American Express",
	"4": "Visa",
	"5": "MasterCard",
	"6": "Discover Card"
}
# 读取 template
temp = cv.imread('./images/ocr_a_reference.png')
# img = cv.cvtColor(img, cv.COLOR_BGR2RGB)
cv_show('template', temp)

# 预处理
# 灰度
temp_gray = cv.cvtColor(temp, cv.COLOR_BGR2GRAY)
cv_show('temp_gray', temp_gray)

31

# 阈值
ret, ref = cv.threshold(temp_gray, 10, 255, cv.THRESH_BINARY_INV)
print(ret)
>>> 10.0

cv_show('ref', ref)

32

# 计算轮廓
# cv2.findContours() 只接受二值图,即黑白的(不是灰度图)
refCnts, hierarchy = cv.findContours(ref.copy(), cv.RETR_EXTERNAL, cv.CHAIN_APPROX_SIMPLE)

cv.drawContours(temp, refCnts, -1, (0, 0, 255), 2)
cv_show('temp_Contours', temp)

print(np.array(refCnts).shape)

refCnts = myutils.sort_contours(refCnts, method="left-to-right")[0]
digits = {}

33

# 遍历每一个轮廓
for (i, c) in enumerate(refCnts):
    
    # 计算外接矩形并且resize成合适大小
    x, y, w, h = cv.boundingRect(c)
    roi = ref[y:y+h, x:x+w]
    roi = cv.resize(roi, (57, 88))
    
    # 每一个数字对应每一个模板
    digits[i] = roi

# 初始化卷积核
rectKernel = cv.getStructuringElement(cv.MORPH_RECT, (9, 3))
sqKernel = cv.getStructuringElement(cv.MORPH_RECT, (5, 5))

# 读取输入图像,预处理
img = cv.imread('./images/credit_card_01.png')
img = myutils.resize(img, width=300)
img_gray = cv.cvtColor(img, cv.COLOR_BGR2GRAY)

cv_show('img_gray', img_gray)

34

# 礼帽:突出更明亮的区域

img_tophat = cv.morphologyEx(img_gray, cv.MORPH_TOPHAT, rectKernel)

cv_show('tophat', img_tophat)

35

# Sobel 卷积
# ksize=-1相当于用 3*3 的卷积核
gradX = cv.Sobel(img_tophat, ddepth=cv.CV_32F, dx=1, dy=0, ksize=-1)
gradX = np.absolute(gradX)
(minVal, maxVal) = ((np.min(gradX)), np.max(gradX))

gradX = 255 * ((gradX - minVal) / (maxVal - minVal))
gradX = gradX.astype("uint8")

print(np.array(gradX).shape)
>>> (189, 300)

cv_show('gradX', gradX)

36

# 闭操作
# 先膨胀,再腐蚀,将数字连在一起
gradX = cv.morphologyEx(gradX, cv.MORPH_CLOSE, rectKernel)

cv_show('gradX', gradX)

37

# THRESH_OTSU 区分数字和背景
# 自动寻找合适的阈值,适合双峰,需把阈值参数设置为 0
thresh = cv.threshold(gradX, 0, 255, cv.THRESH_BINARY | cv.THRESH_OTSU)[1]

cv_show('thresh', thresh)

38

# 再来一个闭操作
thresh = cv.morphologyEx(thresh, cv.MORPH_CLOSE, sqKernel)

cv_show('thresh', thresh)

39

# 计算轮廓
threshCnts, hierarchy = cv.findContours(thresh.copy(), cv.RETR_EXTERNAL, cv.CHAIN_APPROX_SIMPLE)

cnts = threshCnts
img_cur = img.copy()
cv.drawContours(img_cur, cnts, -1, (0, 0, 255), 3)

cv_show('img', img_cur)

locs = []

40

# 遍历轮廓
for (i, c) in enumerate(cnts):
    
    # 计算矩形
    (x, y, w, h) = cv.boundingRect(c)
    ar = w / float(h)
    
    # 选择合适的区域,根据实际任务来,这里的基本都是四个数字一组
    if ar > 2.5 and ar < 4.0:
        if (w > 40 and w < 55) and (h > 10 and h < 20):
            # 符合的留下来
            locs.append((x, y, w, h))
            
# 将符合的轮廓从左到右排序
locs = sorted(locs, key=lambda x:x[0])
output = []

# 遍历每一个轮廓中的数字
for (i, (gX, gY, gW, gH)) in enumerate(locs):
    groupOutput = []
    
    # 根据坐标提取每一个组
    group = img_gray[gY-5 : gY+gH+5, gX-5 : gX+gW+5]
    cv_show('group', group)
    
    # 预处理
    group = cv.threshold(group, 0, 255, cv.THRESH_BINARY | cv.THRESH_OTSU)[1]
    cv_show('group', group)
    
    # 计算每一组的轮廓
    digitCnts, hierarchy = cv.findContours(group.copy(), cv.RETR_EXTERNAL, cv.CHAIN_APPROX_SIMPLE)
    digitCnts = myutils.sort_contours(digitCnts, method="left-to-right")[0]
    
    # 计算每一组中的每一个数值
    for c in digitCnts:
        # 找到当前数值的轮廓,resize成合适的的大小
        (x, y, w, h) = cv.boundingRect(c)
        roi = group[y:y + h, x:x + w]
        roi = cv.resize(roi, (57, 88))
        cv_show('roi',roi)
        
        # 计算匹配得分
        scores = []
        
        # 在模板中计算每一个得分
        for (digit, digitROI) in digits.items():
            # 模板匹配
            result = cv.matchTemplate(roi, digitROI, cv.TM_CCOEFF)
            (_, score, _, _) =cv.minMaxLoc(result)
            scores.append(score)
            
        # 得到最合适的数字
        groupOutput.append(str(np.argmax(scores)))
        
        # 画出来
        cv.rectangle(img, (gX-5, gY-5), (gX+gW+5, gY+gH+5), (0, 0, 255), 1)
        cv.putText(img, "".join(groupOutput), (gX, gY-15), cv.FONT_HERSHEY_SIMPLEX, 0.65, (0, 0, 255), 2)
        
        # 得到结果
        output.extend(groupOutput)
        
# 打印结果
print("Credit Card Type: {}".format(FIRST_NUMBER[output[0]]))
print("Credit Card #: {}".format(" ".join(output)))
cv.imshow("Image", img)
cv.waitKey(0)

41
42
43
44


;