文章目录
前言
本文为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)
img_up = cv.pyrUp(img)
plt.imshow(img_up)
print(img_up.shape)
>>> (884, 680, 3)
img_down = cv.pyrDown(img)
plt.imshow(img_down)
print(img_down.shape)
>>> (221, 170, 3)
# 先上采样再下采样,图像会损失信息,变得模糊
img_up_down = cv.pyrDown(img_up)
res = np.hstack((img, img_up_down))
plt.imshow(res)
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=Gi−PyrUp(PyrDown(GI))
img_down_up = cv.pyrUp(img_down)
L1 = img - img_down_up
plt.imshow(L1)
二、图像轮廓
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')
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)
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)
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)
# 近似
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)
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)
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)
三、模板匹配
模板在原图像上从原点开始滑动,计算模板与图像被模板覆盖的差别,然后将每次计算的结果放入一个矩阵里,作为结果输出。假如原图形是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()
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)
四、直方图
统计图片的灰度值。
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():
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])
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)
img = cv.imread('cat.jpg', 0)
img_masked = cv.bitwise_and(img, img, mask=mask) # 与操作
plt.imshow(img_masked, cmap='gray')
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()
2、直方图均衡化
img = cv.imread('cat.jpg', 0)
plt.hist(img.ravel(), 256);
img_equ = cv.equalizeHist(img)
plt.hist(img_equ.ravel(), 256);
res = np.hstack((img, img_equ))
plt.imshow(res, cmap='gray')
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')
五、傅里叶变换
步骤:
- cv.dft() 和 cv.idft(),输入图像需先转换成 np.float32 格式;
- 结果中,频率为 0 的部分会在左上角,需转换到中心位置,可通过 shift 实现;
- 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()
- 低通滤波和高通滤波:
- 低通:只保留低频,使图像模糊;
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()
- 高通:只保留高频,使图像的细节增强。
# 高通滤波
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()
六、信用卡识别实战
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)
# 阈值
ret, ref = cv.threshold(temp_gray, 10, 255, cv.THRESH_BINARY_INV)
print(ret)
>>> 10.0
cv_show('ref', ref)
# 计算轮廓
# 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 = {}
# 遍历每一个轮廓
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)
# 礼帽:突出更明亮的区域
img_tophat = cv.morphologyEx(img_gray, cv.MORPH_TOPHAT, rectKernel)
cv_show('tophat', img_tophat)
# 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)
# 闭操作
# 先膨胀,再腐蚀,将数字连在一起
gradX = cv.morphologyEx(gradX, cv.MORPH_CLOSE, rectKernel)
cv_show('gradX', gradX)
# THRESH_OTSU 区分数字和背景
# 自动寻找合适的阈值,适合双峰,需把阈值参数设置为 0
thresh = cv.threshold(gradX, 0, 255, cv.THRESH_BINARY | cv.THRESH_OTSU)[1]
cv_show('thresh', thresh)
# 再来一个闭操作
thresh = cv.morphologyEx(thresh, cv.MORPH_CLOSE, sqKernel)
cv_show('thresh', thresh)
# 计算轮廓
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 = []
# 遍历轮廓
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)