OpenCV学习——直方图、边缘检测、模板匹配以及霍夫变化
OpenCV学习——直方图、边缘检测、模板匹配以及霍夫变化
直方图
概念:又称质量分布图,是一种统计报告图,由一系列高度不等的纵向条纹或线段表示数据分布的情况。 一般用横轴表示数据类型,纵轴表示分布情况。(百度百科)
图像直方图
概念:数字图像中亮度分布得直方图,标绘了图像中每个灰度值[0 , 255]的像素个数。横轴表示灰度值,纵轴表示分布情况。
直方图的术语和意义
一些术语:
dims:需要统计的特征数目(=1表示统计灰度值)。
bins:每个特征空间子区段的数目,直条或组距(可以理解成一段区间)。
range:要统计特征的取值范围。
意义:
- 图像中像素强度分布的图形表达式。
- 每一个灰度值的像素个数。
- 不同图像的直方图可能是相同的。
# 绘制img的直方图
cv2.calcHist(img, channels, mask, histSize, ranges[, hist, accumulate])
# img:图像,传入函数时要用[]括起来,例如[img]。
# channels:灰度图:[0],彩色图像则传入参数可以为[0]、[1]、[2]分别表示R、G、B通道
# mask:掩膜图像。统计整幅图像的直方图则设为None,否则需要制作掩模图像
# histSize:bin数目,要用[]括起来
# ranges:像素值范围,一般为[0,256],python是前包后不包的,理论上还是[0,255]灰度值。
代码测试:
import cv2 as cv
import matplotlib.pyplot as plt
# 以灰度图的方式读取图像
img = cv.imread('img/18.jpg',0)
histr = cv.calcHist([img], [0], None, [256], [0, 256])
cv.imshow("original", img)
cv.waitKey(0)
# 创建画布
plt.figure(figsize=(10, 6), dpi=100)
plt.plot(histr)
plt.show()
掩膜的应用
什么是掩膜?
掩膜是用选定的图像、图形或物体,对要处理的图像进行遮挡,来控制图像处理的区域。
在数字图像处理中,利用二位矩阵进行掩膜(0,1组成的二进制图像),利用掩膜对图像进行遮掩,其中值为1的区域进行处理,值为0的区域进行遮掩操作。
主要用途:
- 提取感兴趣区域。
- 屏蔽不需要的图形区域
- 结构特征提取:用相似性变量或图像匹配方法检测和提取图像中与掩膜相似的结构特征。
- 特殊形状图像的制作
总之:就是感兴趣的我对我掩膜矩阵进行赋值为1进行显示,不感兴趣,我赋值为0,我不看你。
import cv2 as cv
import numpy as np
import matplotlib.pyplot as plt
img = cv.imread('img/11.jpg', 0)
# 创建掩膜
mask = np.zeros(img.shape[:2], np.uint8)
mask[100:350, 100:300] = 1
# 自己与自己进行与操作保持不变,在取mask区域
mask_img = cv.bitwise_and(img, img, mask = mask)
plt.imshow(mask_img,cmap=plt.cm.gray)
# 计算掩膜区域的直方图
mask_histr = cv.calcHist([img], [0], mask, [256], [0, 256])
cv.imshow("original", img)
cv.waitKey(0)
plt.figure(figsize=(10, 6), dpi=100)
plt.plot(mask_histr)
plt.show()
别问为啥不用🐖了,问就是艾斯比较帅!
直方图均衡化
原理:把原始图像的灰度直方图从比较集中的某个灰度区间变成更为广泛灰度区间。
作用:扩大图像像素值的分布范围,提高图像整体的对比度。(一般用于解决曝光过度/不足的图片)
dst = cv.equalizeHist(img)
# img:灰度图像
# dst:均衡化后的图像
代码测试:
import cv2 as cv
img = cv.imread('img/11.jpg', 0)
dst = cv.equalizeHist(img)
cv.imshow("original", img)
cv.imshow('dst', dst)
cv.waitKey(0)
自适应的直方图均衡化
原理:将一幅图像分成多块,对每一小块进行直方图均衡化。如果有噪声,为了避免噪声会放大,需要时用对比度限制。若直方图中额bin超过了对比度上线们需要把超出的限速均匀分散到其他bin中,在进行均衡化。
一定程度上解决了图像过亮或过暗导致的细节信息的部分丢失。
# 创建一个自适应均衡化对象
cv.createCLAHE(clipLimit, tileGridSize)
# clipLimit: 对比度限制/阈值(默认为40)
# tileGridSize: 分块大小(默认8*8)
代码实现:
import cv2 as cv
img = cv.imread('img/11.jpg', 0)
# 创建一个自适应均衡化对象
clahe = cv.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8))
cl1 = clahe.apply(img)
cv.imshow("original", img)
cv.imshow('cl1', cl1)
cv.waitKey(0)
效果看起来比直接进行直方图均衡化好很多。
边缘检测
目的:标识数字图像中亮度变化明显的点。
方法:
- 基于搜索:寻找图像一阶导数的最大值检测边界,利用计算结果估计边缘局部方向,一般采用梯度方向,并利用此方向找到局部梯度模的最大值。(Sober算子和Scharr算子)
- 基于零穿越:寻找图像二阶导数零穿越来寻找边界。(Laplacian算子)
Sober检测算子
原理:对传进来的图像像素做卷积(卷积的实质是在求梯度值,或者说给了一个加权平均,其中权值就是所谓的卷积核)然后对生成的新像素灰度值做阈值运算,以此来确定边缘信息。
首先先介绍一下不连续的函数怎么求导,其一阶导数可以写成以下形式(中间差分):
f’(x) = f(x) - f(x - 1)
或
f’(x) = f(x + 1) - f(x)
所以有f’(x) = 1/2(f(x + 1) - f(x - 1))
这里说一下sober的卷积怎么算:
下面是z5的8近邻像素
获得四个差分方向:
两个对角:(z1, z9)–>(-1,1),(z3,z7) -->(1,1);
垂直方向:(z2, z8)–>(0,1);
水平方向:(z6, z4)(1,0);
定义梯度矢量:
|g| = <像素灰度差分>/<相邻像素的距离>
灰度差分:(前一个像素 - 后一个像素)* 差分方向
例子:(z1 -z9)*(-1,1)
距离:曼哈顿距离(不是欧式距离)
然后对四个像素的|g|求平均,并转换成整数(去掉分母,扩大了16倍),然后拆分成x、y方向
Gx:
Gy:
对Img进行Sober算子检测边界:
第一:需要在两个方向进行求导:
水平变化:将图像Img与奇数大小的模板进行卷积,结果为Gx;
垂直变化:将图像Img与偶数大小的模板进行卷积,结果为Gy;
第二、整合
统计极大值所在位置就是图像的边缘。
Schar算子
与Sober算子基本差不多,区别是卷积核不同,但是比Sober更加精确
Gx:
Gy:
API
# sobel检测的api
Sobe_x_or_y = cv.Sobel(img, ddepth, dx, dy, dst, ksize, scale, delta, borderType)
# img:传入的图像
# ddepth:图像深度(原图是unit8,但是太小了,要转成16位的,原因见下)
# dx,dy:求导的阶数(0表示没有在这个方向求导)
# ksize:Sobel算子的卷积核大小,必须为奇数,默认是3
# scale:缩放倒数的比例常数,默认无
# borderType:图像边界模式(cv.BORDER_DEFAULT)
# 注:会将求导计算出来的负值、以及大于255的值进行截断
cv.CV_16S # 16位有符号的数据类型
cv.convertScaleAbs() # 转换测成unit8格式
# 图像混合(可以看前面的博客回一下),以为是两个方向的,所以要混合以下
cv.addWeighted(img1, weight1, img2, weight2, gamma) #gamma是偏移量,一般设置为0就好啦,这个是之前没讲到的。
代码测试:
import cv2 as cv
img = cv.imread('img/11.jpg', 0)
# 计算sobel的卷积结果
x = cv.Sobel(img, cv.CV_16S, 1, 0)
y = cv.Sobel(img, cv.CV_16S, 0, 1)
# 数据转换
scalex = cv.convertScaleAbs(x)
scaley = cv.convertScaleAbs(y)
# 图像合成
img1 = cv.addWeighted(scalex, 0.5, scaley, 0.5, 0)
cv.imshow("original", img)
cv.imshow('img1', img1)
cv.waitKey(0)
结果:
Schar算子,将sober算子的ksize设置成-1即可
x = cv.Sobel(img, cv.CV_16S, 1, 0, ksize = -1)
y = cv.Sobel(img, cv.CV_16S, 0, 1, ksize = -1)
结果:(代码就不重复了)
Laplacian算子
利用二阶导数来检测边缘。
f’‘(x) = f’(x + 1) - f’(x) = f(x + 1) + f(x - 1) - 2f(x)
使用的卷积核:
kernel = [[ 0 , 1 , 0], [ 1 , -4 , 1], [ 0 , 1 , 0]]
API
laplacian = cv.Laplacian(img, ddepth, ksize, scale, delta, borderType)
代码测试:
import cv2 as cv
img = cv.imread('img/11.jpg', 0)
# 计算laplacian的结果
laplacian = cv.Laplacian(img, cv.CV_16S)
result = cv.convertScaleAbs(laplacian)
cv.imshow("original", img)
cv.imshow('laplacian', result)
cv.waitKey(0)
Canny边缘检测算法
算法构成:
第一步:噪声去除
第二步:计算图像梯度(Sobel算子,梯度:G;方向:1/(tan(Gy/Gx))有垂直、水平、对角线四个方向)
第三步:非极大值抑制:对图像中的每一个像素进行检查,看这个点的梯度是不是周围具有相同梯度方向的点中最大的。
第四步:滞后阈值:用于确定真正的边界,设置两个阈值(maxVal和minVal,大于manVal的看为真正的阈值,低于minVal的抛弃),介于之间的判断是否与边界相连,相连则认为是边界,否则抛弃。
API
canny = cv.Canny(img, threshold1, threshold2)
# img:图像
# threshold1:minVal
# threshold2:maxVal
代码测试
import cv2 as cv
img = cv.imread('img/11.jpg', 0)
# 计算canny的结果
maxVal = 100
minVal = 0
canny = cv.Canny(img, minVal, maxVal)
cv.imshow("original", img)
cv.imshow('canny', canny)
cv.waitKey(0)
模板匹配和霍夫变化
模板匹配
在给定图片中查找和模板最相似的区域。
实现流程:
- 准备两幅图像
- 原图像
- 模板:与原图像对比的图像块
- 滑动模板图像和原图像进行比对(从左到右,从上到下,依次计算与模板相似程度)
API
# 获取匹配的相关矩阵
res = cv.matchTemplate(img, template, method)
# img:要进行模板匹配的图像
# template:模板
# method:模板匹配算法(如下)
# 平方差匹配(cv.TM_SQDIFF):利用模板与图像见的平方差匹配,最好的匹配情况值为0,匹配越差,值越大
# 相关匹配(cv.TM_CCORR):利用模板与图像间的乘法,数值越大,匹配程度越高
# 利用相关系数匹配(cv.TM_CCOEFF):利用模板与图像间的相关系数匹配,1表示完美匹配,-1表示最差
# 定位最匹配位置,返回四个值,最小/最大值,最小/最大位置
min_val, max_val, min_loc, max_loc = cv.minMaxLoc(res)
代码测试
import cv2 as cv
img = cv.imread('img/11.jpg')
template = cv.imread('img/20.png')
# 获取template的shape信息
h, w= template.shape[:2]
# 模式匹配
res = cv.matchTemplate(img, template, cv.TM_SQDIFF)
min_val, max_val, min_loc, max_loc = cv.minMaxLoc(res)
top_left = min_loc
bottom_right = (top_left[0] + w, top_left[1] + h)
# 下面绘制检测框的color是bgr格式
cv.rectangle(img, top_left, bottom_right, (0, 255, 0), 2)
cv.imshow("template", template)
cv.imshow("detection", img)
cv.waitKey(0)
霍夫变化
运用两个坐标空间之间的变换将在一个空间中具有相同形状的曲线或直线映射到另一个坐标空间的一个点上形成峰值,从而把检测任意形状的问题转化为统计峰值问题。(来源)
霍夫空间
我们已知直线表达式: y = kx + b
我们将其转换成 k 、 b为变量的函数表达式(x,y为已知量,q,b为未知量):**b = -kx + y **
我们将由直角坐标系转化后的空间(右侧的)成为霍夫空间。笛卡尔坐标系中的一条直线对应霍夫空间中的一个点,反之,霍夫空间中的一条直线,对应着笛卡尔空间坐标系中的一个点。(就是换了斜率和截距)
笛卡尔中的两个点映射到霍夫空间的情况:
我们看到霍夫空间会有一个交点,交点的意义:两个点确定直线的斜率和截距
同理,多个点(笛卡尔坐标系)在霍夫变化中映射直线的交点是笛卡尔坐标系中对应点共线的斜率和截距。
问题:当笛卡尔坐标系中,对应点的x都相同,那么就会出现截距、斜率无限大的情况,那怎么解决呢?
解决方法:由直角坐标系映射成极坐标(y = ax +b ==> r = xcosα + ysinα)(α是与x轴的夹角)
实现流程
原有图像:100*100的图片;
要求:用霍夫变化检测图片中的直线
第一步:创建一个2D数组(累加器),初始化所有值为0,行用ρ表示(最大值为图片对角线距离),列用α表示
第二步:取直线上一个点(x,y),将其带入直线极坐标公式,便利阿尔法(0-180),求出ρ值,如果数值在上述累加器中存在相应位置,则在该位置+1.
第三步:在直线上取第二个点,重复操作,更新累加器的值。
第四步:搜索累加器中的最大值,找出(ρ,α),将图像中的直线表示出来。
霍夫线检测
API
cv.HougLines(img, rho, theta, threshold)
# img:二值化图像
# rho、theta:ρ和α的精度
# threshold:阈值,只有累加器中的值高于阈值才能被认为是直线,越小,直线越多
error:TypeError: 'NoneType' object is not iterable
如果出现上面的变化,可以看看是不是图片格式出错了
代码测试:
import cv2 as cv
import numpy as np
img = cv.imread('img/21.png', 0)
# Canny进行边缘检测,获取边缘信息
edges = cv.Canny(img, 50, 150)
# 霍夫直线变化
lines = cv.HoughLines(edges, 0.8, np.pi/180, 100)
print(lines)
# 将检测的线绘制到图像中
for line in lines:
rho, theta = line[0]
a = np.cos(theta)
b = np.sin(theta)
x0 = a * rho
y0 = b * rho
x1 = int(x0 + 1000 * (-b))
y1 = int(y0 + 1000 * (a))
x2 = int(x0 - 1000 * (-b))
y2 = int(y0 - 1000 * (a))
cv.line(img, (x1, y1), (x2, y2), (0, 255, 0))
cv.imshow("edges", edges)
cv.imshow("img", img)
cv.waitKey(0)
霍夫圆检测
霍夫圆检测阶段——霍夫梯度法
第一:检测圆心
圆心到圆周法线的交汇处,设置一个阈值,在某个点的相交的直线的条数大于这个阈值就认为该交汇点为圆心。
第二:圆半径确定
圆心到圆周的距离是相同的,确定一个阈值,只要相同距离的数量大于阈值,就认为该距离就是圆的半径。
API
ircles = cv.HoughCircles(img, method, dp, minDist, param1 = 100, param2 = 100, minRadius = 0, maxRadius =0)
# img:灰度图像
# method:霍夫变化圆检测算法,cv.HOUGH_GRADIENT
# dp:霍夫空间分辨率,=1表示霍夫空间与输入图像空间大小一致,=2表示霍夫空间是输入图像空间的一半
# minDist:圆心的最小距离,如果检测的两个圆心之间距离小于该值,则认为他们是同一个圆心
# param1:边缘检测是使用Canny算子的高阈值,低阈值是高阈值的一半
# param2:检测圆心和确定半径时所共有的阈值
# minRadius和maxRadius是所检测的圆半径的最小值和最大值
代码测试:
import cv2 as cv
import numpy as np
import matplotlib.pyplot as plt
img = cv.imread('img/23.png', 0)
img1 = cv.imread('img/23.png', 0)
# 去噪
img1 = cv.medianBlur(img, 7)
# 霍夫圆检测
circles = cv.HoughCircles(img, cv.HOUGH_GRADIENT, 1, 100, param1=100, param2=50, minRadius= 0, maxRadius= 100)
print(circles)
for i in circles[0,:]:
cv.circle(img, (int(i[0]), int(i[1])), int(i[2]), (0, 255, 0), 5)
cv.circle(img, (int(i[0]), int(i[1])), 2, (0, 0, 255), 5)
cv.imshow("original", img1)
cv.imshow("img", img)
cv.waitKey(0)