计算机视觉系列:边缘检测
——————————————————————————
边缘检测介绍
什么是边缘检测呢?顾名思义,你的三围,你的身材,你身体的轮廓,就是要做检测的边缘。比如脑补一下超人,是不是在脑子里能够想出他的轮廓?也就是边缘,反过来,也能通过边缘检测判断这个人是不是超人。
本小节结束
—————————————————————————————————————————————————————–
常见边缘检测函数
opencv提供了很多的边缘检测的滤波函数,包括Laplacian(),Sobel(),Scharr()。这些滤波函数都会将非边缘区域转换成黑色,将边缘区域转换为白色或者其他饱和颜色。但是,这些函数都会将噪声错误的识别为边缘,缓解这个问题的有效思路是在边缘检测前对图像进行模糊处理,模糊滤波函数可以用blur(),medianBlur(),GaussianBlur()等,比如blur()是均值滤波,是最简单的一种滤波操作,输出图像的每一个像素是核窗口内输入图像对应像素的像素的平均值( 所有像素加权系数相等),其实说白了它就是归一化后的方框滤波。边缘检测函数和模糊滤波函数的参数都有很多,但是总会有一个ksize参数,是一个奇数,用来描述滤波核的宽和高,单位为像素。
本小节结束
—————————————————————————————————————————————————————–
边缘描绘并模拟肖像彩色卷
medianBlur作为模糊函数对于去除彩色图像的噪声十分有效;而Laplacian函数作为边缘检测函数,会产生明显的边缘线条。在得到Laplacian函数的结果之后,需要将其转化为黑色边缘和白色背景的图像,将其归一化后乘以原图像使其边缘变黑,先have a look of result。
import cv2
import numpy as np
def strokeEdges(src, blurKsize = 7, edgeKsize = 5):
if blurKsize >= 3:
blurredSrc = cv2.medianBlur(src, blurKsize)
graySrc = cv2.cvtColor(blurredSrc, cv2.COLOR_BGR2GRAY)
else:
graySrc = cv2.cvtColor(src, cv2.COLOR_BGR2GRAY)
cv2.Laplacian(graySrc, cv2.CV_8U, graySrc, ksize=edgeKsize)
normal = (1.0 / 255) * (255 - graySrc)
normal = np.array(normal)
cv2.imshow("stroke", normal)
cv2.waitKey()
img = cv2.imread('test2.jpg')
img = np.array(img)
strokeEdges(img)
blurKsize作为medianBlur的ksize参数,edgeKsize作为Laplacian的ksize参数,对于较大的ksize,medianBlur的代价会很大。blueKsize的值小于3会关闭模糊效果。
原图如下:
处理后的图像如下:
本小节结束
—————————————————————————————————————————————————————–
canny边缘检测
opencv提供了一个十分方便的边缘检测函数Canny,以算法的发明者(Jhop.F.Canny)命名,算法的实现也十分便捷。首先,由于Canny只能处理灰度图,所以将读取的图像转成灰度图。先看一下效果
import cv2
import numpy as np
import matplotlib.pyplot as plt
img = cv2.imread('timg.jpg', cv2.IMREAD_GRAYSCALE)
img = np.array(img)
img1 = cv2.Canny(img, 100, 100)#Second the third parameters are two thresholds.
#The smaller they are, the more details are detected.
cv2.imshow("original", img)
cv2.imshow("canny", img1)
cv2.waitKey()
下面关心一下这个算法是怎么实现的。
高斯模糊去燥
这一步很简单,主要作用就是去除噪声,也就是前文提到的低通滤波器。因为噪声也集中于高频信号,很容易被识别为伪边缘。应用高斯模糊去除噪声,降低伪边缘的识别。但是由于图像边缘信息也是高频信号,高斯模糊的半径选择很重要,过大的半径很容易让一些弱边缘检测不到,也就是核设置的小一点。
计算梯度和方向
图像的边缘可以指向不同方向,因此经典Canny算法用了四个梯度算子来分别计算水平、垂直、对角线方向的梯度。但是常用的边缘差分算子(如Rober,Prewitt,Sobel)只计算水平和垂直方向的差分 和 。这样就可以如下计算梯度模和方向:
其中
为梯度强度,
表示梯度方向,arctan为反正切函数。以Sobel算子为例讲述如何计算梯度强度和方向。
号是卷积运算的意思,在卷积的运算结果中, 会保留下与 轴方向有关的边缘, 会保留下与 轴方向有关的边缘。求得的 角即为边缘的方向。
非最大抑制NMS
将当前像素的梯度强度与沿正负梯度方向上的两个像素进行比较。
如果当前像素的梯度强度与另外两个像素相比最大,则该像素点保留为边缘点,否则该像素点将被抑制。
为什么进行以上的操作呢?因为对图像进行梯度计算后,仅仅基于梯度值提取的边缘仍然很模糊,上面两部会保留下最清晰的边界。
双阈值去除假阳性
在施加非极大值抑制之后,剩余的像素可以更准确地表示图像中的实际边缘。然而,仍然存在由于噪声和颜色变化引起的一些边缘像素。为了解决这些杂散响应,必须用弱梯度值过滤边缘像素,并保留具有高梯度值的边缘像素,可以通过选择高低阈值来实现。如果边缘像素的梯度值高于高阈值,则将其标记为强边缘像素;如果边缘像素的梯度值小于高阈值并且大于低阈值,则将其标记为弱边缘像素;如果边缘像素的梯度值小于低阈值,则会被抑制。阈值的选择取决于给定输入图像的内容。
分析边缘及其连接
到目前为止,被划分为强边缘的像素点已经被确定为边缘,因为它们是从图像中的真实边缘中提取出来的。然而,对于弱边缘像素,将会有一些争论,因为这些像素可以从真实边缘提取也可以是因噪声或颜色变化引起的。为了获得准确的结果,应该抑制由后者引起的弱边缘。通常,由真实边缘引起的弱边缘像素将连接到强边缘像素,而噪声响应未连接。为了跟踪边缘连接,通过查看弱边缘像素及其8个邻域像素,只要其中一个为强边缘像素,则该弱边缘点就可以保留为真实的边缘。
本小节结束
—————————————————————————————————————————————————————–
轮廓检测
MWE(minimum work example)
在计算机视觉中,轮廓检测是另一个比较重要的任务,不单是用来检测图像或者视频中物体的轮廓,而且计算多边形边界、形状逼近和计算感兴趣区域也会用到轮廓检测,是与图像数据交互时的常规操作,先通过下面的例子熟悉下Numpy和opencv提供的API。
import numpy as np
import cv2
img = np.zeros((200, 200))
img.dtype = 'uint8'
img[50:150, 50:150] = 255
#binaryzation
ret, thresh = cv2.threshold(img, 1, 255, cv2.THRESH_BINARY)
image, contours, hierarchy = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
color = cv2.cvtColor(img, cv2.COLOR_GRAY2BGR)
img = cv2.drawContours(color, contours, -1, (0, 255, 0), 2)
cv2.imshow("contours", img)
cv2.waitKey()
首先创建了一个 的黑色空白图像,接着在图像中央放了一个白色方块,接着对图像做了二值化操作。重点来了,最后是findCcontours()函数,第一个参数为输入图像,第二个参数会得到轮廓的整体层次,如果只想得到最外围的轮廓,第二个参数为cv2.RETR_EXTERNAL。findContours有三个返回值,修改后的图像,图像的轮廓和他们的层次。最后把图像画出来,-1表示目标文件和原文件的通道数一样,(0,255,0)表示用绿色画,2表示画线的宽度。
结果如下:
实战
找到一个正方形的轮廓很简单,但是要寻找不规则的轮廓呢?同样是要调用findContours函数。先看下面的水壶:
这是一个纯种的不规则图形,比如说我要提取它的边界框、最小矩形面积、最小包围圆,又该怎么操作呢?
import numpy as np
import cv2
# read the image
img = cv2.pyrDown(cv2.imread("hammer.png"), cv2.IMREAD_UNCHANGED)
#change to numpy array
img = np.array(img)
#binaryzation
ret, thresh = cv2.threshold(cv2.cvtColor(img.copy(), cv2.COLOR_BGR2GRAY),\
127, 255, cv2.THRESH_BINARY)
#find outline
image, contours, hierarchy = cv2.findContours(thresh, cv2.RETR_EXTERNAL\
, cv2.CHAIN_APPROX_SIMPLE)
for c in contours:
#find bounding box coordinates
x, y, w, h = cv2.boundingRect(c)
cv2.rectangle(img, (x, y), (x + w, y + h), (0, 255, 0), 2)
# find minimum area
rect = cv2.minAreaRect(c)
#calculate coordinates of the minimum area rectangle
box = cv2.boxPoints(rect)
#normalize coordinates to integers
box = np.int0(box)
#draw contours
cv2.drawContours(img, [box], 0, (0, 0, 255), 3)
#calculate center and radius of minimum enclosing circle
(x, y), radius = cv2.minEnclosingCircle(c)
#cast to integers
center = (int(x), int(y))
radius = int(radius)
#draw the circle
img = cv2.circle(img, center, radius, (255, 0, 0), 2)
cv2.drawContours(img, contours, -1, (255, 0, 0), 1)
cv2.imshow("contours", img)
cv2.waitKey()
看一下结果:
应该明白这段代码是什么意思了吧。
首先画出简单的边界框,将轮廓信息转换为 坐标,并加上矩形的高度 和宽度 ,画出矩阵信息,也就是绿色的边框。
x, y, w, h = cv2.boundingRect(c)
cv2.rectangle(img, (x, y), (x + w, y + h), (0, 255, 0), 2)
下一步是计算出包围目标区域的最小矩形区域。首先,计算最小矩形区域,然后标记这个矩形的定点,计算出的顶点是浮点型,像素的坐标值是整数,因此需要numpy.int0进行转化,然后画出这个矩形,也就是红色边框。
rect = cv2.minAreaRect(c)
box = cv2.boxPoints(rect)
box = np.int0(box)
此函数会修改原图像img,其次,该函数的第二个参数接收一个保存数组轮廓,从而可以绘制一系列的轮廓,因此如果只有一组点来保存轮廓,需要把这些点放到一个数组里。第三个参数是绘制轮廓数组的索引,-1表示绘制所有的轮廓,否则绘制指定的轮廓。
cv2.drawContours(img, [box], 0, (0, 0, 255), 3)
最后是画最小闭圆。
(x, y), radius = cv2.minEnclosingCircle(c)
center = (int(x), int(y))
radius = int(radius)
img = cv2.circle(img, center, radius, (255, 0, 0), 2)
本小节结束
—————————————————————————————————————————————————————–
用定制核做卷积
引入一个小插曲,介绍不同的核会卷积出什么效果,基于python的类实现。
import numpy as np
import cv2
class VconvolutionFilter(object):
def __init__(self, kernel):
self._kernel = kernel
def apply(self, img1):
img1 = cv2.filter2D(img1, -1, self._kernel)
cv2.imshow("test", np.array(img1))
cv2.waitKey()
class SharpenFilter(VconvolutionFilter):
def __init__(self):
kernel = np.array([[-1, -1, -1],
[-1, 8, -1],
[-1, -1, -1]])
VconvolutionFilter.__init__(self, kernel)
class BlurFilter(VconvolutionFilter):
def __init__(self):
kernel = np.array([[0.04, 0.04, 0.04, 0.04, 0.04],
[0.04, 0.04, 0.04, 0.04, 0.04],
[0.04, 0.04, 0.04, 0.04, 0.04],
[0.04, 0.04, 0.04, 0.04, 0.04],
[0.04, 0.04, 0.04, 0.04, 0.04]])
VconvolutionFilter.__init__(self, kernel)
class EmbossFilter(VconvolutionFilter):
def __init__(self):
kernel = np.array([[2, -1, 0],
[-1, 1, 1],
[0, 1, 2]])
VconvolutionFilter.__init__(self, kernel)
img = cv2.imread('test2.jpg', 0)
img = np.array(img)
ef = EmbossFilter()
ef.apply(img)
此处不在放图显示效果,就简单的介绍一下吧。
1. 对于SharpenFilter的核,感兴趣区域的像素权重为8,邻近的像素权重为-1,对于感兴趣的像素而言,新像素值是用当前像素乘以8,在减去附近的8个像素值,如果感兴趣的区域与周围区域已经有一点差别,那么在卷积之后差别会放大,让图像锐化。
2.对于BlurFilter的核,为了达到模糊效果,权重的和通常为1,临近像素的值加权后取均值达到模糊的效果。
3.对于EmbossFilter的核,他是非对称的,因为权重的特殊设置,使其具有模糊和锐化的效果,会产生一种浮雕和脊状的效果。
本小节结束
—————————————————————————————————————————————————————–
来个小插曲,凸轮廓、直线检测、圆检测也属于边缘检测的内容,上次忘写了,这次补上吧。也许你好奇为什么要做边缘检测啊,最小闭圆这种东西,个人的理解是标记感兴趣区域,方便处理。假设一张图有一万个像素点,你只想处理两百个像素点,那就不如先把这两百个像素点先提取出来,然后再处理,节省计算资源。
凸轮廓
大多数处理轮廓的时候,物体的形状都是变化多样的。凸形状内部的任意连点的连线都在该形状里面,如果没有直观的印象,那么先放段程序和实例,先对凸轮廓有所了解。
import cv2
img = cv2.pyrDown(cv2.imread("hammer.png", cv2.IMREAD_UNCHANGED))
ret, thresh = cv2.threshold(cv2.cvtColor(img.copy(), cv2.COLOR_BGR2GRAY), 127, 255, cv2.THRESH_BINARY)
image, contours, hier = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
for cnt in contours:
epsilon = 0.01 * cv2.arcLength(cnt, True)
approx = cv2.approxPolyDP(cnt, epsilon, True)
hull = cv2.convexHull(cnt)
cv2.drawContours(img, [hull], -1, (0, 0, 255), 2)
cv2.imshow("hull ", img)
cv2.waitKey()
结果如下图所示:
大概对凸轮廓有所了解了吧,分析一下这段代码。cv2.approxPloyDP是用来计算近似多边形的边框,第一个参数是“轮廓”,第二个参数是 ,表示源轮廓与近似多边形的最大差值,(这个值越小,近似多边形与源轮廓越接近)。第三个参数是布尔标记,表示多边形是否闭合。
已经有精确表示的轮廓,为毛还要一个近似多边形呢?because近似多边形由一组直线组成,能够在区域里面定义多边形,以便于后期的操作。cv2.arcLength用于计算轮廓的周长,计算完毕后,乘以参数 来计算 。为了计算凸形状,需要用cv2.convexHull来获取处理的轮廓信息cnt,然后把凸轮廓画出来就好。
本小节结束
—————————————————————————————————————————————————————–
直线检测与圆检测
边缘检测和轮廓十分重要,是其他复杂操作的基础,而直线检测和圆检测是边缘检测的基础,介绍一下opencv是如何检测边缘和轮廓的。
直线检测
Hough变换是直线和形状检测背后的理论基础,它由Riched Duda和Peter Hart发明,是对Paul Hough大佬所做工作的扩展,首先介绍直线检测。
直线检测可以通过HoughLines和HoughLinesP函数完成,差别是,第一个函数用了标准的Hough变换,而第二个函数用了概率Hough变换。但是概率版本的性能是基于标准版本优化后的,计算代价会少一些。
下面分析一下这两个变换:
Hough变换
设已知一黑白图像上画了一条直线,要求出这条直线所在的位置。我们知道,直线的方程可以用 来表示,其中 和 是参数,分别是斜率和截距。过某一点 的所有直线的参数都会满足方程 ,即点(x0,y0)确定了一族直线。
方程 在参数 平面上是一条直线。这样,图像 平面上的一个前景像素点就对应到参数平面上的一条直线。我们举个例子说明一下,设图像上的直线是 , 我们先取上面的三个点: 。可以求出,过 点的直线的参数要满足方程 , 过 点的直线的参数要满足方程 , 过 点的直线的参数要满足方程 , 这三个方程就对应着参数平面上的三条直线,而这三条直线会相交于一点 。
同理,原图像上直线 上的其它点,如 等,对应参数平面上的直线也会通过点 。这个性质就为我们解决问题提供了方法,就是把图像平面上的点对应到参数平面上的线,来解决问题。
简而言之,Hough变换思想为:在原始图像坐标系下的一个点对应了参数坐标系中的一条直线,同样参数坐标系的一条直线对应了原始坐标系下的一个点,然后,原始坐标系下呈现直线的所有点,它们的斜率和截距是相同的,所以它们在参数坐标系下对应于同一个点。这样在将原始坐标系下的各个点投影到参数坐标系下之后,看参数坐标系下有没有聚集点,这样的聚集点就对应了原始坐标系下的直线。
Hough变换概率版本
概率霍夫变换(Progressive Probabilistic Hough Transform)的原理很简单,如下所述:
1.随机获取边缘图像上的前景点,映射到极坐标系画曲线;
2.当极坐标系里面有交点达到最小投票数,将该点对应 坐标系的直线 找出来;
3.搜索边缘图像上前景点,在直线 上的点(且点与点之间距离小于maxLineGap(防止出现弧)连成线段),然后这些点全部删除,并且记录该线段的参数(起始点和终止点);
4.重复1. 2. 3.。
显而易见,这玩意的计算量要小的多。
开始处理
原始图像
处理后的图像:
import cv2
import numpy as np
img = cv2.imread('lines.jpg')
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
edges = cv2.Canny(gray, 50, 120)
minlinelength = 20
maxlinegap = 5
lines = cv2.HoughLinesP(edges, 1, np.pi/180, minlinelength, maxlinegap)
for x1, y1, x2, y2 in lines[0]:
cv2.line(img, (x1, y1), (x2, y2), (0, 255, 0), 2)
cv2.imshow("edges", edges)
cv2.imshow("lines", img)
cv2.waitKey()
设置直线的最小长度(更短的直线会被消除),最大线段间隙也很重要,一条线段的间隙超过这个长度会被视为两条分开的线段。Houghlines函数会接受一个由Canny边缘检测滤波器处理过的单通道二值图像,不一定要Canny滤波器,但是一个经过去燥并只有边缘的图像当做Hough变换的输入的效果会比较好。介绍一下HoughlinesP的参数:
1.需要处理的图像。
2.线段的几何表示,
和
。
3.阈值,每个投票箱代表一条直线,投票数达到阈值的直线会被保留。
4.直线最小长度和最大线段间隙。
圆检测
opencv的HoughCircles可以对圆进行检测,思想与直线检测的思想类似,不断的画圆、检测、判断。
import cv2
img = cv2.imread('circle.png')
cv2.imshow('img', img)
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
circles= cv2.HoughCircles(gray, cv2.HOUGH_GRADIENT, 1, \
100, param1=100, param2=30, minRadius=5, maxRadius=300)
print(len(circles[0]))
for circle in circles[0]:
print(circle[2])
x = int(circle[0])
y = int(circle[1])
r = int(circle[2])
img = cv2.circle(img,(x,y),r,(0,0,255),-1)
cv2.imshow('res', img)
cv2.waitKey(0)
讲真,HoughCircles函数的参数调节不好的话,图像圆检测的效果特别烂。。。
cv2.HoughCircles(image,method, dp, minDist, circles, param1, param2, minRadius, maxRadius)
1. image 不用多说,输入矩阵
2. method 也就是cv2.HOUGH_GRADIENT,梯度法
3. dp 计数器的分辨率图像像素分辨率与参数空间分辨率的比值,通俗一点说,dp=1,则参数空间与图像像素空间(分辨率)一样大,dp=2,参数空间的分辨率只有像素空间的一半大。
4. minDist 圆心之间最小距离,如果距离太小,会产生很多相交的圆,如果距离太大,则会漏掉正确的圆
5. param1 canny检测的双阈值中的高阈值,低阈值是它的一半。
6. param2 最小投票数(基于圆心的投票数)。
7. minRadius 需要检测圆的最小半径
8. maxRadius 需要检测圆的最大半径
本小节结束
—————————————————————————————————————————————————————–