OpenCV-python 模板匹配-分水岭-GrabCut

一、模板匹配
1、单目标

单目标模板匹配的原理:模板图像在输入图像上做滑动操作(类似于 2D 卷积),模板图像与所在原图 patch 做比较,最终返回一个灰度图,每个像素代表该像素的邻域与模板的相似度。当输入图像尺寸为 ( W , H ) (W, H) (W,H)、模板图像尺寸为 ( w , h ) (w,h) (w,h) 时,输出图像尺寸为 ( W − w + 1 , H − h + 1 ) (W-w+1, H-h+1) (W−w+1,H−h+1)。 一旦得到结果,就可以使用cv.minMaxLoc() 函数来查找最大值/最小值的位置,取它为矩形的左上角,取 ( w , h ) (w,h) (w,h) 为矩形的宽和高,这个矩形就是找到的模板区域。

cv.matchTemplate(
    image, # 原始图像,可以是灰度图,也可以是BGR图
    templ, # 模板图,尺寸不能大于原始图像
    method, # 模板匹配方式
    result, 
    mask# 掩码
)


import cv2 as cv
import numpy as np
from matplotlib import pyplot as plt

img = cv.imread('messi5.jpg',0)
img2 = img.copy()
template = cv.imread('template.jpg',0)
w, h = template.shape[::-1]

# 六种不同的模板匹配方式,
methods = ['cv.TM_CCOEFF', 'cv.TM_CCOEFF_NORMED', 'cv.TM_CCORR',
            'cv.TM_CCORR_NORMED', 'cv.TM_SQDIFF', 'cv.TM_SQDIFF_NORMED']

for meth in methods:
    img = img2.copy()
    method = eval(meth)
    
    # 模板匹配
    res = cv.matchTemplate(img,template,method)
    min_val, max_val, min_loc, max_loc = cv.minMaxLoc(res)
    
    # 获取左上角位置,不同匹配模式获取的方式不相同
    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(img, top_left, bottom_right, 255, 2)
    cv.imshow(meth, img)
    cv.waitKey(0)

OpenCV-python 模板匹配-分水岭-GrabCut_第1张图片

推荐使用 cv.TM_SQDIFF,不推荐 cv.TM_CCORR!

2、多目标

使用 cv.minMaxLoc() 找的是最值,只能有一个返回,如果图像中有多个目标区域呢?这时可以用阈值函数来筛选匹配度较高的位置。因为,模板匹配返回值给出的是每个像素点是模板左上角的概率,只要设置一个概率阈值,就能找到所需位置。

import cv2 as cv
import numpy as np

img_rgb = cv.imread('./images/Mario.png')
img_gray = cv.cvtColor(img_rgb, cv.COLOR_BGR2GRAY)
template = cv.imread('./images/mario_c.png', 0)
w, h = template.shape[::-1]

res = cv.matchTemplate(img_gray, template, cv.TM_CCOEFF_NORMED)
threshold = 0.7
loc = np.where(res >= threshold)

for pt in zip(*loc[::-1]):
    cv.rectangle(img_rgb, pt, (pt[0] + w, pt[1] + h), (0, 0, 255), 1)
cv.imshow('res.png', img_rgb)
cv.waitKey(0)

OpenCV-python 模板匹配-分水岭-GrabCut_第2张图片

在具体使用时,可能会出现的一些问题:

  • 匹配模式不同,得到的结果可能也不一样,需要根据实际情况选择最优的模式
  • 模板图像的像素值很重要,也就是它的大小、范围会影响最终的匹配结果
  • 同一个目标区域返回的左上角的位置可能是多个相邻的点,比如上面第二排硬币的框,线宽之所以如此宽是因为多个点都接受作为左上角,此时可以适当增加阈值
二、霍夫线检测
1、直线检测

cv.HoughLines:使用标准霍夫变换,找到二值图像中的直线

lines = cv.HoughLines(
    image, # 8-bit、单通道的二值图像
    rho, # 累加器的距离分辨率,以像素为单位
    theta, # 累加器的角度分辨率,以弧度为单位
    threshold, # 累加器的阈值参数,太大会过滤大部分直线,太小则误检测会很多
    lines, # 
    srn, # 对于多尺度霍夫变换,它是距离分辨率的除数
    stn, # 对于多尺度霍夫变换,它是角度分辨率的除数
    min_theta, # 直线检查的最小角度,必须在 0 和 max_theta 之间
    max_theta# 直线检查的最大角度,必须在 min_theta 和 CV_PI 之间
)

# 实例
gray = cv.cvtColor(img, cv.COLOR_BGR2GRAY)# 灰度图
edge = cv.Canny(gray, 20, 250)# 二值图
lines = cv.HoughLines(edge, 1, np.pi/90, 100)# 直线检测

result = img.copy()
for rho,theta in lines[:, 0, :]:# 遍历每一条直线
    a = np.cos(theta)
    b = np.sin(theta)
    x0 = a*rho# 计算坐标原点到直线的垂直点:(x0, y0)
    y0 = b*rho
    x1 = int(x0 + 1000*(-b))# 垂直点沿直线方向往左延伸1000个像素点:(x1, y1)
    y1 = int(y0 + 1000*(a))
    x2 = int(x0 - 1000*(-b))# 垂直点沿直线方向往右延伸1000个像素点:(x2, y2)
    y2 = int(y0 - 1000*(a)) 
    cv.line(result,(x1,y1),(x2,y2),(255,0,0),1)
cv.imshow('11', result)
cv.waitKey(0)

注:

  • 粗糙的累加器距离分辨率为 rho,精确的累加器分辨率为 rho/srn;如果 srn=0 和 stn=0,则使用经典的霍夫变换,否则,这两个参数都应该是正的。
  • 检测结果以 ( ρ , θ ) (\rho, \theta) (ρ,θ) 形式返回, ρ \rho ρ 是坐标原点到直线的距离, θ \theta θ 是纵轴与直线的夹角,通过这两个参数就能恢复出一条直线(注意,不是线段)。
  • 从 ( ρ , θ ) (\rho, \theta) (ρ,θ) 中恢复直线的算法见上述 for 循环中的代码,其中参数1000可以修改。

OpenCV-python 模板匹配-分水岭-GrabCut_第3张图片

2、线段检测

cv.HoughLinesP:使用概率霍夫变换,找到二值图像中的线段

lines =cv.HoughLinesP(
    image, # 8-bit、单通道的二值化图像
    rho, # 累加器的距离分辨率,以像素为单位
    theta, # 累加器的角度分辨率,以弧度为单位
    threshold, # 累加器的阈值参数
    lines, 
    minLineLength, # 线段检查的最小长度,小于该长度的线段被过滤
    maxLineGap# 连接同一直线上各点之间允许的最大间隙
)

# 实例
gray = cv.cvtColor(img, cv.COLOR_BGR2GRAY)# 灰度图
edge = cv.Canny(gray, 20, 250)# 二值图
lines = cv.HoughLinesP(edge, 1, np.pi/180, 100, minLineLength=200, maxLineGap=500)

result = img.copy()
for p in lines:
    x1,y1,x2,y2 = p[0]
    cv.line(result, (x1, y1), (x2, y2), (255,0,0), 1)# 画直线
    cv.circle(result, (x1,y1), 5, (0,255,0), -1)# 画端点1
    cv.circle(result, (x2,y2), 5, (0,255,0), -1)# 画端点2
cv.imshow('111', result)
cv.waitKey(0)

注:

  • rho/theta 值越小,检测的线段越精细;值越大,被过滤掉的线段越多,只能在某一个区域内检测。
  • 检测结果以 [ x 1 , y 1 , x 2 , y 2 ] [x1,y1,x2,y2] [x1,y1,x2,y2] 形式返回,代表的是线段的两个端点坐标。

OpenCV-python 模板匹配-分水岭-GrabCut_第4张图片

三、霍夫圆检测

cv.HoughCircles:使用霍夫变换,找到灰度图中的圆

circles= cv.HoughCircles(
    image, # 8-bit、单通道的灰度图
    method, # 检测方法,可以有:霍夫梯度法HOUGH_GRADIENT,优化后的霍夫梯度法HOUGH_GRADIENT
    dp, # 累加器分辨率与图像分辨率的反比
    minDist, # 探测到的圆中心之间的最小距离
    circles, 
    param1, # 参数1,和参数2一起使用
    param2, # 参数2,和参数1一起使用,它本身是圆心累加器的阈值
    minRadius, # 最小圆半径
    maxRadius# 最大圆半径
)

# 实例
img2 = cv.imread('./circle.jpg')
gray = cv.cvtColor(img2, cv.COLOR_BGR2GRAY)
circles = cv.HoughCircles(
    gray, 
    cv.HOUGH_GRADIENT, # cv.HOUGH_GRADIENT_ALT 会报错,可能跟OpenCV版本有关
    1, # dp 值设置为 1 效果就比较好
    100, 
    param1=100, 
    param2=30, 
    minRadius=5, 
    maxRadius=300
)

result = img2.copy()
for circle in circles[0]:
    center_x, center_y, radius = circle
    cv.circle(result, (center_x, center_y), radius, (0.,255,0), 2)# 画圆的轮廓
    cv.circle(result, (center_x, center_y), 3, (255,0,0), -1)# 画圆心
cv.imshow('22', result)
cv.waitKey(0)

注:

  • dp=1 表示累加器分辨率和图像分辨率相同,dp=2 表示累加器的分辨率是图像分辨率的一半。如果检测方法用的是 HOUGH_GRADIENT_ALT,推荐使用 dp=1.5。
  • minDist 参数过小,会导致很多个相邻的小圆代替正确的大圆被检测到;参数过大,则部分圆会被漏检。
  • param1和param2存在大小比较,较大的那一个将会进入 Canny 边缘检测器;param2 越小,检测到的假圆越多。
  • 在 HOUGH_GRADIENT_ALT 中,param2 越接近 1,检测到的圆形状越好;一般情况下,param2=0.9 就行了,如果想更好的检测小圆,可以将其降低到 0.85~0.8。
  • 检测结果以 [ x , y , r ] [x, y, r] [x,y,r] 形式返回, ( x , y ) (x,y) (x,y) 是圆心坐标, r r r 是圆的半径。

OpenCV-python 模板匹配-分水岭-GrabCut_第5张图片

四、轮廓检测
1、轮廓检测

cv.findContours:找到二值图中所有图形的轮廓

contours, hierarchy= cv.findContours(
    image, # 8-bit、单通道的二值图
    mode, # 轮廓检索模式,
    method, # 轮廓近似方法,
    contours,
    hierarchy, 
    offset# 可选参数,轮廓偏移量
)

res = cv.drawContours(
    image, 
    contours, # 轮廓点
    contourIdx, # 需要绘制的轮廓索引,如果为负数表示绘制所有的轮廓
    color, # 颜色
    thickness, # 线宽
    lineType, # 线型
    hierarchy, # 可选参数,层级关系,只在需要绘制其中一些轮廓时才使用
    maxLevel, # 绘制轮廓的最大水平
    offset# 可选参数,轮廓偏移量
)

# 实例
img2 = cv.imread('./circle.jpg')
gray = cv.cvtColor(img2, cv.COLOR_BGR2GRAY)# 灰度图
edge = cv.Canny(gray, 20, 250)# 二值图
contours, hierarchy = cv.findContours(edge, cv.RETR_TREE, cv.CHAIN_APPROX_SIMPLE)
res = cv.drawContours(img2, contours, -1, (0, 255, 0), 2)# 绘制轮廓

for contour in contours:
    M = cv.moments(contour)# 计算轮廓的矩
    center_x = int(M["m10"] / M["m00"])# 计算轮廓的中心点坐标
    center_y = int(M["m01"] / M["m00"])
    cv.circle(res, (center_x, center_y), 5, (0.,255,0), -1)

cv.imshow('22', res)
cv.waitKey(0)

注:

  • mode:
    • cv.RETR_EXTERNAL:只检索极端的外部轮廓
    • cv.RETR_LIST:检索所有的轮廓,而不建立任何层次关系
    • cv.RETR_CCOMP:检索所有的轮廓,并将它们组织成一个两层的层次结构。在顶层,存在组件的外部边界;在第二层,有洞的边界。如果被连接部件的孔内有另一个轮廓,它仍然被放在顶层。
    • cv.RETR_TREE:检索所有的轮廓,并重建一个完整的层次嵌套轮廓
  • method:
    • cv.CHAIN_APPROX_NONE:绝对存储所有的轮廓点
    • cv.CHAIN_APPROX_SIMPLE:压缩水平、垂直和对角线的片段,只留下他们的端点
    • cv.CHAIN_APPROX_TC89_L1:应用 Teh-Chin 链近似算法中的一种
    • cv.CHAIN_APPROX_TC89_KCOS:应用 Teh-Chin 链近似算法中的一种
  • 检测结果返回轮廓和层级结构(如果 mode 中有的话)
  • drawContours 中 的 maxLevel:如果为0,则只绘制指定的轮廓线;如果为1,函数绘制轮廓线和所有嵌套轮廓线;如果为2,函数绘制轮廓、所有嵌套的轮廓、所有嵌套到嵌套的轮廓,以此类推。只有在有层次结构可用时才考虑此参数。

OpenCV-python 模板匹配-分水岭-GrabCut_第6张图片

2、多边形拟合

cv.approxPolyDP:对检测到的轮廓点进行多边形拟合(逼近)

approxCurve= cv.approxPolyDP(
    curve, # 轮廓
    epsilon, # 拟合精度,原始曲线和它的近似值之间的最大距离
    closed, # 拟合曲线是否封闭
    approxCurve
)

# 实例
img = cv.imread('./line.jpg')
gray = cv.cvtColor(img, cv.COLOR_BGR2GRAY)# 灰度图
edge = cv.Canny(gray, 20, 250)# 二值图
contours, hierarchy = cv.findContours(edge, cv.RETR_TREE, cv.CHAIN_APPROX_SIMPLE)

result = img.copy()
for contour in contours:
    approx = cv.approxPolyDP(contour, 50, True)# 多边形拟合
    for p in approx:# 绘制所有顶点
        x,y = p[0]
        cv.circle(result, (x, y), 5, (0.,255,0), -1)
    cv.drawContours(result, [approx], -1, (255,0,100), 1)
cv.imshow('22', result)
cv.waitKey(0)

注:

  • epsilon:可以自己指定值,也可以用 cv.arcLength 计算轮廓周长然后取一定比例
  • 检测以 [ x , y ] [x,y] [x,y] 顶点形式返回,近似后的轮廓有多少条边就有多少个顶点

OpenCV-python 模板匹配-分水岭-GrabCut_第7张图片

五、角点检测
1、Harris 角点检测

cv.cornerHarris:Harris 角点检测

cv.cornerHarris(
    src, # 8-bit、单通道图像
    blockSize, # 邻域大小
    ksize, # Sobel算子的孔径参数
    k, # 检测器自由参数
    dst, 
    borderType# 像素外推方法
)

# 实例
img = cv.imread('./line.jpg')
gray = cv.cvtColor(img, cv.COLOR_BGR2GRAY)# 灰度图
edge = cv.Canny(gray, 20, 250)# 二值图

result = img.copy()
corner_harris = cv.cornerHarris(edge, 15, 19, 0.04)# 角点检测
result[corner_harris>0.01*corner_harris.max()] = [255, 0, 0]# 绘制角点
cv.imshow('t', result)
cv.waitKey(0)

注:

  • blockSize:角点的范围大小,值越大得到的角点范围越大
  • ksize:值越大,过滤掉的噪点越多
  • 检测结果以二值图的形式返回,图片大小跟原始图像相同,角点处像素不为0

OpenCV-python 模板匹配-分水岭-GrabCut_第8张图片

2、Shi-Tomasi 角点检测

cv.goodFeaturesToTrack:找到图像中或指定图像区域中最突出的角点

corners = cv.goodFeaturesToTrack(
    image, # 8-bit、单通道图像
    maxCorners, # 返回的最大角点数量
    qualityLevel, # 角点最小可接受质量参数
    minDistance, # 角点之间可能的最小欧氏距离
    corners, 
    mask, # 可选参数,指定检测区域的掩码
    blockSize, # 用于计算每个像素邻域上的导数共变矩阵的平均块的大小
    useHarrisDetector, # 是否使用 Harris 检测器
    k# Harris 检测器自由参数
)

# 实例
img = cv.imread('./line.jpg')
gray = cv.cvtColor(img, cv.COLOR_BGR2GRAY)# 灰度图
edge = cv.Canny(gray, 20, 250)# 二值图

result = img.copy()
corner_shi = cv.goodFeaturesToTrack(edge, 4, 0.1, 20)# shi-tomasi 角点检测
for p in corner_shi:
    x, y = p[0]
    cv.circle(result, (x, y), 5, (0.,255,0), -1)
cv.imshow('t', result)
cv.waitKey(0)

注:

  • qualityLevel 指定了最小可被接受的角点质量,计算方式是:qualityLevel 乘以质量最高的角点质量,质量低于该数值的角点全被过滤掉。
  • 检测结果以 [ x , y ] [x,y] [x,y] 形式返回,每一个都是检测到的角点

OpenCV-python 模板匹配-分水岭-GrabCut_第9张图片

六、分水岭算法

任何一张灰度图都可以被视作是一个“地形图”,高强度位置是山峰,低强度位置是山谷。现在假设你在往不同的山谷里面注入不同颜色的水,当山谷被注满时水会溢出来,相邻山谷的水会汇合产生颜色的变化,为了阻止这种颜色的交融,你在汇合处建立屏障;继续注水和建屏障的操作,知道所有的山谷都被淹没。最后,你所建立的屏障就能完成对图像的分割。这就是分水岭算法的原理所在,具体可以访问CMM查看动画。

但是由于图像中存在噪声或其他不规则现象,传统的分水岭算法会导致分割过度。所以,OpenCV 实现了一个基于标记的分水岭算法,在某些被标记的区域内做分水岭,这其中最关键的内容就是计算这个标记 mark!推荐方法是使用 cv.findContours 来标记,也可以用基于距离的方法来获取标记。

1、基于距离

基本算法步骤:

  • 预处理:BGR图像变成灰度图,做自适应二值化操作,形态学开操作去除白色噪点
  • 计算背景:形态学膨胀,得到确定的背景区域
  • 计算前景:cv.distanceTransform 做距离变换,根据距离变换结果做阈值处理,得到确定的前景区域
  • 计算未知区域:确定的背景减去确定的前景,得到的就是未知区域,未知区域就包含边界区域
  • 计算 mark:对前景区域做基本标记,再融合未知区域作进一步标记
  • 分水岭算法:输入是原始图像、mark,输出是标记边界
img = cv.imread('images/coins.png')
gray = cv.cvtColor(img, cv.COLOR_BGR2GRAY)
ret, thresh = cv.threshold(gray, 0, 255, cv.THRESH_BINARY_INV + cv.THRESH_OTSU)

# noise removal
kernel = np.ones((3, 3), np.uint8)
opening = cv.morphologyEx(thresh, cv.MORPH_OPEN, kernel, iterations=2)

# sure background area
sure_bg = cv.dilate(opening, kernel, iterations=3)
cv.imshow('3-background', sure_bg)

# Finding sure foreground area
dist_transform = cv.distanceTransform(opening, cv.DIST_L2, 5)
ret, sure_fg = cv.threshold(dist_transform, 0.7 * dist_transform.max(), 255, 0)

# Finding unknown region
sure_fg = np.uint8(sure_fg)
unknown = cv.subtract(sure_bg, sure_fg)

# Marker labelling
ret, markers = cv.connectedComponents(sure_fg)
# Add one to all labels so that sure background is not 0, but 1
markers = markers + 1
# Now, mark the region of unknown with zero
markers[unknown == 255] = 0

markers = cv.watershed(img, markers)
img[markers == -1] = [255, 0, 0]
2、基于轮廓
img = cv.imread('images/coins.png')
gray = cv.cvtColor(img, cv.COLOR_BGR2GRAY)
ret, thresh = cv.threshold(gray, 0, 255, cv.THRESH_BINARY_INV + cv.THRESH_OTSU)

# 形态学开操作去白色噪点
kernel = np.ones((3, 3), np.uint8)
opening = cv.morphologyEx(thresh, cv.MORPH_OPEN, kernel, iterations=2)

# 膨胀操作,让轮廓往外扩展一些(不加的话可能效果不佳)
opening = cv.dilate(opening, kernel, iterations=3)

# 计算轮廓
contours, hierarchy = cv.findContours(opening, cv.RETR_TREE, cv.CHAIN_APPROX_SIMPLE)

# 绘制轮廓,作为 mark
h, w, _ = img.shape
marks = np.zeros((h, w))
for i in range(len(contours)):
    cv.drawContours(marks, contours, i, i+1)# 不同的轮廓使用不同的标记值
marks = marks.astype(np.int32)

# 分水岭算法
marks = cv.watershed(img, marks)
img[marks == -1] = (255, 0, 0)
七、GrabCut 算法

GrabCut 算法论文《“GrabCut”: interactive foreground extraction using iterated graph cuts》,它提出了一种利用最少的交互完成前景提取的算法。算法的基本思路:先人工在图像中用矩形框选取目标前景所在区域,然后 GrabCut 通过迭代分割找到最优解;如果最优解还是不够完美,用户再使用画笔在图像上进一步标注哪些地方是背景、哪些地方是前景,算法再一次给出最优解。具体来说:

  • 画矩形框:算法认为矩形框以外的全是背景,矩形框以内的是“未知”
  • 编码:对矩形框以内的像素点进行前景和背景编码,其依据就是每个像素点跟矩形框外面的背景相似度
  • GMM 建模:使用高斯混合模型对前景和背景进行建模
  • GMM 重编码:GMM 对象素进行重新标记,原理类似于聚类
  • 图的建立:根据 GMM 的像素分布建立一个图,以像素作为节点,再另外添加两个节点:源点,汇点。每个前景像素连接到源点,每个背景像素连接到汇点
  • 计算权重:像素到源点和汇点的权重由各自是前景/背景的概率来定义,像素到像素之间的权重由像素颜色相似度来定义(差距较大则权重较低)
  • 分割:采用 mincut 算法对图进行分割,分割的原则是如果相邻像素点分别属于前景和背景,则它们之间的连边被切断,该边的权重计入代价函数,切割方式须保证代价函数最小。剪切后,所有连接到源节点的像素都成为前景,连接到汇聚节点的像素都成为背景。
  • 迭代:反复迭代,直到得到最优解。迭代的操作就是人为指定哪些地方分错了。
# 函数解析:
cv.grabCut(
    img, # 8位3通道图像
    mask, # 掩码,大小跟原始图像一致,像素值有0/1/2/3,分别表示背景、前景、可能是背景、可能是前景
    rect, # ROI,表示其外围的像素都是背景
    bgdModel, # 算法内部使用的数组,只需要创建一个(1,65)的float64数组就行
    fgdModel, # 算法内部使用的数组,只需要创建一个(1,65)的float64数组就行
    iterCount, # 迭代次数
    mode# 选择初始化是使用上面的mask还是rect,cv.GC_INIT_WITH_RECT,cv.GC_INIT_WITH_MASK
)

看一个实例:先用一个矩形框给出我们目标物体的大概位置,然后用 GrabOut 做分割。

import numpy as np
import cv2 as cv
from matplotlib import pyplot as plt

img = cv.imread('images/messi.png')
mask = np.zeros(img.shape[:2], np.uint8)# 其实这里没用到它
bgdModel = np.zeros((1, 65), np.float64)
fgdModel = np.zeros((1, 65), np.float64)
rect = (50, 50, 700, 450)# 初始化用的是它

cv.grabCut(img, mask, rect, bgdModel, fgdModel, 5, cv.GC_INIT_WITH_RECT)# 结果直接存在 mask 中
mask2 = np.where((mask == 2) | (mask == 0), 0, 1).astype('uint8')
img = img * mask2[:, :, np.newaxis]

plt.imshow(img[:, :, ::-1]), plt.colorbar(), plt.show()

OpenCV-python 模板匹配-分水岭-GrabCut_第10张图片

图像上半部分基本都分割为背景了,但是手臂以下的部分处理却不好。而且GrabCut的耗时比较长,上述 743*435 的图片,耗时在6s左右。所以,实际上GrabCut在速度和精度上效果都不理想!

你可能感兴趣的:(python,opencv,基础,opencv,python,计算机视觉)