轮廓计算需要先将图像二值化,然后从黑色背景上找到白色前景对象(一个个的闭环区域),OpenCV 使用 cv.findContours
计算轮廓:
# 在二值图上计算 contour
cv.findContours(
image, # 二值图
mode, # 检索模式,常用的模式是cv.RETR_TREE
method, # 近似方法,是记录轮廓上所有点还是只返回关键点,常见的方法cv.CHAIN_APPROX_SIMPLE
offset # 偏移量
)
检索模式的其他选择见这里,数据近似方法的其他选择见这里。它的返回参数有两个:contours, hierarchy
,第一个是包含所有轮廓点的一个列表,列表中每一个轮廓以 numpy.array 形式给出,形状为 (N, 1, 2);第二个是轮廓的拓扑信息,就是轮廓之间的层次关系,直接以 numpy.array 形式给出。
可以使用 cv.drawContours
将找到的轮廓绘制在图像上,也可以使用该函数基于轮廓绘制 mask。
# 将 contour 画在图像上
cv.drawContours(
image, # BGR 图即可
contours, # 轮廓列表
contourIdx, # 需要绘制的轮廓索引,如果是 -1 表示绘制所有的轮廓
color, # 绘制轮廓颜色
thickness, # 绘制轮廓线宽,如果是 -1 表示完全填充
lineType, # 绘制轮廓线型
hierarchy, # 轮廓层次,只在绘制多个轮廓时用到
maxLevel, # 绘制轮廓的最大水平(详见下方说明)
offset # 偏移量
)
maxLevel
如果为0,则只绘制指定的轮廓线;如果是1,则绘制轮廓线和所有嵌套轮廓线;如果是2,则绘制轮廓、所有嵌套的轮廓、所有嵌套到嵌套的轮廓,以此类推;该参数只有在层次结构参数 hierarchy
可用时才考虑。
# 示例:
gray = cv.cvtColor(image, cv.COLOR_BGR2GRAY)
_, _bin = cv.threshold(gray, 128, 255, cv.THRESH_BINARY)
contours, hierarchy = cv.findContours(_bin, cv.RETR_TREE, cv.CHAIN_APPROX_SIMPLE)
image = cv.drawContours(image, contours, -1, (255, 255, 255), 2)
第一个特征:图像不变矩(Invariant Moments),图像特征的一种,不受光照、噪点、几何形变的影响,代表特征有Hu矩、ZerNike矩,OpenCV 中计算的是图像Hu矩。OpenCV 可以从等高线 contour 中计算不变矩特征,以此来提取图像某区域的一些属性,例如面积、形心等。
cnt = contours[180] # 第180个 contour 的数据信息
M = cv.moments(cnt) # 计算不变矩,返回的值:m[ij],mu[ij],nu[ij]
cx = int(M['m10']/M['m00']) # 形心横坐标
cy = int(M['m01']/M['m00']) # 形心纵坐标
area = M['m00'] # 面积
area = cv.contourArea(cnt) # contour 的面积,也可以通过不变矩的 m[00] 获取
perimeter = cv.arcLength(cnt, True) # contour 的周长
第二个特征:轮廓近似度,也叫轮廓逼近、多边形拟合。轮廓由多个点来刻画,点的精度或者说密集程度代表了 contour 的精度,不同的精度得到的近似 contour 不同。如果一个矩形方块的边缘有锯齿,要想得到它的矩形轮廓就需要将 contour 的精度设置的低一些,如下第二幅图所示:
contour 的近似度设置方式如下,参数 epsilon 代表到近似轮廓的最大距离,该距离越大精度就越低,图二是10%的弧长,图三是1%弧长:
epsilon = 0.1*cv.arcLength(cnt,True)
approx = cv.approxPolyDP(cnt,epsilon,True)
第三个特征:凸包。平面上多个点就能形成一个凸包问题,OpenCV 可以快速计算一堆点所能构成的最大凸包,并返回构成该最大凸包的点坐标。
hull = cv.convexHull(points, clockwise, returnPoints)
参数:
-- points:所有的点坐标,也就是一个 contour
-- clockwise:返回点的顺序,默认是True,也就是顺时针;如果设置为False,就按逆时针返回
-- returnPoints:默认是True;如果设置为False,就返回点在 contour 中的索引
实例:
k = cv.isContourConvex(cnt) # 判断 contour 是不是凸包
hull = cv.convexHull(cnt) # 返回最大凸包点
第四个特征:最小外接边界框。目标检测中只需要一个边界框来做物体定位,而 contour 给出物体的外围轮廓,OpenCV 可以从这个轮廓中提取物体的边框信息完成物体定位。contour 的边界框有两种形式,一种是原始边界框,一种是旋转边界框。OpenCV 还能够给出 contour 的最小面积外接圆,以及旋转椭圆框,这也可以作为物体的定位信息。
# 1. 原始边界框,左上角到右下角定位,xywh 形式
x,y,w,h = cv.boundingRect(cnt)
cv.rectangle(img,(x,y),(x+w,y+h),(0,255,0),2)
# 2. 旋转边界框,在 contour 的所有边界框中拥有最小面积,(center, w, h, rotation angle)形式
rect = cv.minAreaRect(cnt)
box = cv.boxPoints(rect)
box = np.int0(box)
cv.drawContours(img,[box],0,(0,0,255),2)
# 3. 外接圆
(x,y),radius = cv.minEnclosingCircle(cnt)
center = (int(x),int(y))
radius = int(radius)
cv.circle(img,center,radius,(0,255,0),2)
# 4. 旋转椭圆框
ellipse = cv.fitEllipse(cnt)
cv.ellipse(img,ellipse,(0,255,0),2)
下图的前三个即是原始边界框、旋转边界框、外接圆、旋转椭圆的示例。
第五个特征:拟合线。用一条直线来拟合所有的点,有点类似于最小二乘法,具体表现如上图第四个所示。
rows,cols = img.shape[:2]
[vx,vy,x,y] = cv.fitLine(cnt, cv.DIST_L2,0,0.01,0.01)
lefty = int((-x*vy/vx) + y)
righty = int(((cols-x)*vy/vx)+y)
cv.line(img,(cols-1,righty),(0,lefty),(0,255,0),2)
# 纵横比
x,y,w,h = cv.boundingRect(cnt)
aspect_ratio = float(w)/h
# extent:contour 面积占边界框的百分比
area = cv.contourArea(cnt)
x,y,w,h = cv.boundingRect(cnt)
rect_area = w*h
extent = float(area)/rect_area
# 固体度solidity:contour 面积占凸包的百分比
area = cv.contourArea(cnt)
hull = cv.convexHull(cnt)
hull_area = cv.contourArea(hull)
solidity = float(area)/hull_area
# 当量直径
area = cv.contourArea(cnt)
equi_diameter = np.sqrt(4*area/np.pi)
# 倾斜角
(x,y),(MA,ma),angle = cv.fitEllipse(cnt)
# 掩码 mask
mask = np.zeros(imgray.shape,np.uint8)
cv.drawContours(mask,[cnt],0,255,-1)
pixelpoints = np.transpose(np.nonzero(mask))
# 通过掩码 mask 获取像素点的最值以及位置,均值
min_val, max_val, min_loc, max_loc = cv.minMaxLoc(imgray,mask = mask)
mean_val = cv.mean(im,mask = mask)
# 图形的端点
leftmost = tuple(cnt[cnt[:,:,0].argmin()][0])
rightmost = tuple(cnt[cnt[:,:,0].argmax()][0])
topmost = tuple(cnt[cnt[:,:,1].argmin()][0])
bottommost = tuple(cnt[cnt[:,:,1].argmax()][0])
第一个:计算凸包以及最远点。上面提到过凸包,contour 的最大凸包并不一定会用到 contour 里面的所有点,相反有些点可能会成为凸包的缺陷点,也就是说它是在凸包内部的。OpenCV 可以根据 contour 和它所形成的的凸包来找到这些缺陷点。
hull = cv.convexHull(cnt,returnPoints = False) # 这里必须设置成 False
defects = cv.convexityDefects(cnt,hull) # 返回凸包中某一段的起点、终点、最远点、最远点距离
for i in range(defects.shape[0]):
s,e,f,d = defects[i,0]
start = tuple(cnt[s][0]) # 凸包中某条线的起点
end = tuple(cnt[e][0]) # 凸包中某条线的终点
far = tuple(cnt[f][0]) # 轮廓点集中距离该直线的最远点
cv.line(img,start,end,[0,255,0],2)
cv.circle(img,far,5,[0,0,255],-1)
第二个:判断点和图形的关系。contour 是一个轮廓,它可以围成一个多边形,OpenCV 能够计算一个点与多边形的相对关系:在内部(负数),在外部(正数),在边界上(0)。
dist = cv.pointPolygonTest(cnt,(50,50),True) # 最后一个参数,如果是True就返回具体的距离,如果是False就只会返回+1/-1/0
第三个:相似度匹配。contour 的不变矩代表了物体的特征,该特征不受平移、旋转、尺寸的影响,所以它可以用来做物体的相似度匹配。OpenCV 利用 Hu 矩来计算相似度,返回两个 contour 的相似性度量,返回结果越小表示匹配度越好、相似度越高。这个可以用来做比较基础的 OCR 识别。
import cv2 as cv
import numpy as np
img1 = cv.imread('star.jpg',0)
img2 = cv.imread('star2.jpg',0)
ret, thresh = cv.threshold(img1, 127, 255,0)
ret, thresh2 = cv.threshold(img2, 127, 255,0)
contours,hierarchy = cv.findContours(thresh,2,1)
cnt1 = contours[0]
contours,hierarchy = cv.findContours(thresh2,2,1)
cnt2 = contours[0]
ret = cv.matchShapes(cnt1,cnt2,1,0.0)
print( ret )
计算 contour 时返回的第二个参数 hierarchy 的意义是什么?怎么用?一张图可能包含多个 contour,有的 contour 位于其它 contour 的内部,这时候就需要定义它们之间的位置关系,类似于二叉树中的子节点、父节点、兄弟节点,OpenCV 按照层级关系来定义 contour 的拓扑信息。
具体来说,hierarchy 是一个二维列表,每一行定义一个 contour 的拓扑信息: [ N e x t , P r e v i o u s , F i r s t _ C h i l d , P a r e n t ] \bold {[Next, Previous, First\_Child, Parent]} [Next,Previous,First_Child,Parent]Next、Previous:表示与它同一层级的下一个、前一个 contour,同一层级表示彼此之间不存在交集,第0层级就是图像中最外围的等高线集合。
First_Child:指的是下一层级中第一个 contour,下一层级指的是当前等高线内部的。
Parent:表示上一层级的 contour,就是它的外围、它属于哪一个 contour。
以上关系如果不存在对应的contour,参数值以 -1 表示。基于以上层级关系,OpenCV 中定义了4种不同的轮廓检索模式:
关于层次关系的详细、完整介绍参见官网。
直方图直接作用对象是灰度图,它给出图中 0~255 像素值的数量统计,能够形象地描述图像的亮度、对比度等属性。
OpenCV 和 numpy 都有自己的直方图计算函数,但是前者的计算速度是要比后者快大约40倍(官方说的),所以建议最好是用OpenCV来计算。绘图的话可以用 OpenCV 的直线绘图,更好一点的方法是直接用 matplotlib 的绘图模块。
cv.calcHist(images, channels, mask, histSize, ranges[, hist[, accumulate]])
参数:
-- images:输入图像,需要放在一个列表中
-- channels:指定通道,如果是灰度图那就直接用[0],如果是彩色图那就可以指定不同的通道[0]或[1]或[2]
-- mask:掩码,用来指定计算直方图的区域,一般设置为 None
-- histSize:需要统计的像素值的数量,一般是[256]
-- ranges:需要统计的像素值的范围,一般是 [0,256]
实例:
hist = cv.calcHist([image], [1], None, [256], [0, 256]) # 返回 (256,1) 的numpy
img = cv.imread('test.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])
plt.show()
当图像的像素集中于某一区域时,会造成图像亮度不均衡、对比度差的问题,这时可以用直方图均衡化来解决,让图像像素均匀分布在 0~255 空间上,提高对比度,这在很多地方是很有用的,比如人脸识别中,我们对训练集的图像做直方图均衡化来让每张图拥有相同的光照条件。
直方图均衡化的算法原理可以参照维基百科的解释。
cv.equalizeHist(image)
参数:
-- image:输入灰度图像;
img = cv.imread('wiki.jpg',0) # 只能是灰度图
equ = cv.equalizeHist(img) # 直方图均衡化
res = np.hstack((img,equ)) # 将原始图和均衡化之后的图做对比
上述全局直方图均衡化适用于明暗分布变化不大的图像,或者说统一、整体变化的图像;当图像中有的地方亮、有的地方暗的时候,全局均衡化就不再适用,需要使用自适应均衡化。例如下图所示,左边是原始图像,部分区域背景很暗,前景的雕像人脸则较亮;使用全局均衡化后,图像对比度的确增加,但却丢失了雕像人脸的大部分特征。
自适应直方图均衡化的原理:将图像分割成多个不同的小块区域,分别对每个小块区域做直方图均衡化。在每一个小块内部,直方图会被限制在一个小区域内(除非有噪音);如果有噪音,该区域会被放大。为了避免这种情况,自适应直方图均衡化还添加了一个对比度限制参数。
img = cv.imread('tsukuba_l.png',0)
# create a CLAHE object (Arguments are optional).
clahe = cv.createCLAHE(clipLimit=2.0, tileGridSize=(8,8))
cl1 = clahe.apply(img)
cv.imwrite('clahe_2.jpg',cl1)
上述直方图都是将像素点展平到一维空间再做统计,它只展现了像素点的灰度强度值,不能体现垂直和水平方向上的分布特点。这里的 2D 直方图也叫颜色直方图,它包含两个特征:每个像素的色调和饱和度值。
颜色直方图的计算函数同普通直方图一样,cv.calcHist
,但此处输入的 HSV 图像(H 是色调,S 是饱和度,V 是明亮程度)。
cv.calcHist(
image, # 输入图像,HSV 格式
channels, # 通道,颜色直方图使用 [0,1] 表示 H、S
mask, # 掩码,一般为 None
histSize, # 每个维度的尺寸大小,H 是 180,S 是 256
ranges # 每个维度的范围,H是0到180,S是0到256
)
img = cv.imread('home.jpg')
hsv = cv.cvtColor(img,cv.COLOR_BGR2HSV) # 转换为HSV
hist = cv.calcHist([hsv], [0, 1], None, [180, 256], [0, 180, 0, 256]) # 2D 直方图
plt.imshow(hist, interpolation = 'nearest') # 绘制结果
plt.show()
虽然画出来了颜色直方图,但是仍然不直观,如果对 2D 直方图不熟悉可能还是不知道它代表着什么意义。OpenCV 官方源码 sample/python/color_histogram.py 给出一段示例代码,如何将 2D 直方图直观的可视化出来,以便更好地分析图像信息。[代码传送]
在该代码中,作者在 HSV 中创建了一个颜色映射,然后转换成BGR,将得到的直方图图像与这个颜色映射相乘。他还使用了一些预处理步骤来去除小的孤立像素,从而得到一个良好的直方图。
可以在柱状图中清楚地看到有什么颜色:蓝色,黄色,还有一些由于棋盘而产生的白色。
何谓直方图反向投影?对于一幅输入图像,它返回的是一个和输入同尺寸的单通道图,图中每一个像素点值表示原图像中该位置属于我们感兴趣物体的概率!直方图反向投影如何实现?
M
,创建原始图像的直方图 I
;R
作为调色板创建一个新的概率图像 B
;B
应用卷积查找目标特征,B=D*B
;B
中强度较大的位置,即是所需结果。下面将基于上述步骤给出 OpenCV 的实现,需要注意几点的是:反向投影函数 cv.calcBackProject
接收的参数跟计算直方图的函数 cv.calcHist
相同,只是需要先对目标图的直方图做归一化,然后将其当做 mask 传入,最后一个参数 1
是缩放比例。
import numpy as np
import cv2 as cv
# 目标图像、原图像转换成 HSV
roi = cv.imread('rose_red.png')
hsv = cv.cvtColor(roi,cv.COLOR_BGR2HSV)
target = cv.imread('rose.png')
hsvt = cv.cvtColor(target,cv.COLOR_BGR2HSV)
# 第一步:计算目标的直方图
roihist = cv.calcHist([hsv],[0, 1], None, [180, 256], [0, 180, 0, 256] )
# 第二步:计算概率图像,也就是直方图反向投影操作
cv.normalize(roihist,roihist,0,255,cv.NORM_MINMAX)
dst = cv.calcBackProject([hsvt], [0,1], roihist, [0,180,0,256], 1)
# 第三步:卷积计算特征
disc = cv.getStructuringElement(cv.MORPH_ELLIPSE,(5,5))
cv.filter2D(dst,-1,disc,dst)
# 第四步:阈值操作
ret,thresh = cv.threshold(dst,50,255,0)
thresh = cv.merge((thresh,thresh,thresh))
res = cv.bitwise_and(target,thresh)
res = np.vstack((target,thresh,res))
cv.imwrite('res.jpg',res)