OpenCV中的图像处理
转换颜色空间
OpenCV提供了超过150 种进行颜色空间转换的方法,但是以后经常用到的其实也就两种:BGR→Gray 和BGR→HSV。进行颜色空间转换的函数是:cv2.cvtColor(input_image,flag),其中flag就是要转换到的类型。
对于BGR→Gray 的转换,我们要使用的flag 就是cv2.COLOR_ BGR2GRAY。而对于BGR→HSV 的转换,我们用的flag 就是cv2.COLOR_BGR2HSV。
可以使用下面的程序输出opencv中支持的所有用于颜色空间转换的flag:
<!–hexoPostRenderEscape:
import cv2
flags=[i for i in dir(cv2) if i.startswith('COLOR_')]
print(flags)
:hexoPostRenderEscape–>
在OpenCV 的HSV 格式中,H(色彩/色度)的取值范围是[0,179],S(饱和度)的取值范围[0,255],V(亮度)的取值范围[0,255]。但是不同软件使用的值可能不同,所以当你需要拿OpenCV 的HSV 值与其他软
件的HSV 值进行对比时,一定要记得归一化处理。
物体跟踪
我们已经知道怎样将一幅图像从BGR颜色空间转换到HSV了,这样一来,我们就可以利用这一点来提取带有某个特定颜色的物体。在HSV颜色空间中要比在BGR空间中更容易表示一个特定颜色。
在接下来的例子中我们要提取一个蓝色的物体,做下面几步操作:
- 从视频中获取每一帧图像;
- 将图像转换到HSV 空间;
- 设置HSV 阈值到蓝色范围。
import cv2
import numpy as np
cap=cv2.VideoCapture(0)
while(1):
# 获取捕获到的每一帧
ret,frame=cap.read()
# 转换到HSV颜色空间
hsv_img=cv2.cvtColor(frame,cv2.COLOR_BGR2HSV)
# 设定蓝色的阈值(颜色值达到什么样才判断为蓝色)
lower_blue=np.array([110,50,50])
upper_blue=np.array([130,255,255])
# 根据阈值构建掩模(低于lower_blue为黑色,高于upper_blue的为白色)
mask=cv2.inRange(hsv_img,lower_blue,upper_blue)
# 对原图像和掩模进行按位与运算
res=cv2.bitwise_and(frame,frame,mask=mask)
# 显示图像
cv2.imshow('frame',frame)
cv2.imshow('mask',mask)
cv2.imshow('res',res)
k=cv2.waitKey(5)
if k==27:
break
# 关闭窗口
cv2.destroyAllWindows()
上图掩膜中一些白色的点就是图像处理中的噪点。
找到跟踪对象的HSV 值
import cv2
import numpy as np
# 不能用[0,255,0],而要用[[[0,255,0]]]
# 这里的三层括号应该分别对应于cvArray,cvMat,IplImage
green=np.uint8([[[0,255,0]]])
hsv_green=cv2.cvtColor(green,cv2.COLOR_BGR2HSV) # 绿色的HSV值
print(hsv_green)
可见,BGR颜色空间中的绿色对应HSV的[60,255,255]。
几何变换
OpenCV 提供了两个变换函数,cv2.warpAffine和cv2.warpPerspective,使用这两个函数你可以实现所有类型的变换。cv2.warpAffine 接收的参数是2 3的变换矩阵,而cv2.warpPerspective 接收的参数是3 3 的变换矩阵。
缩放
扩展缩放只是改变图像的尺寸大小。OpenCV提供的函数cv2,resize(src, dst, interpolation=CV_INTER_LINEAR)可以实现这个功能,图像的尺寸可以自己手动设置,也可以指定缩放因子。我们可以选择使用不同的插值方法,在缩放时我们推荐使用cv2.INTER_ AREA,在扩展时我们推荐使用v2.INTER_ CUBIC(慢)和v2.INTER_ LINEAR。默认情况下所有改变图像尺寸大小的操作使用的插值方法都是cv2.INTER_LINEAR。
import cv2
import numpy as np
img=cv2.imread('miss.jpg')
# 下面的None所占的参数位置本应该是输出图像的尺寸,但是因为后边我们设置了缩放因子(fx,fy),因此这里直接写None就好
res=cv2.resize(img,None,fx=2,fy=2,interpolation=cv2.INTER_CUBIC)
# 这里呢,我们直接设置输出图像的尺寸,所以不用设置缩放因子
height,width=img.shape[:2] # 得到原图像的宽高
res=cv2.resize(img,(2*width,2*height),interpolation=cv2.INTER_CUBIC) # 对原图像放大两倍
while(1):
cv2.imshow('res',res)
cv2.imshow('img',img)
if cv2.waitKey(1) == 27:
break
cv2.destroyAllWindows()
平移
如果要使图像要沿(x,y)方向移动,移动的距离是(tx,ty),你可以以下面的方式构建移动矩阵:
M=\left[\begin{matrix}
1 & 0 & t_x \\[8pt]
0 & 1 & t_y
\end{matrix}\right]可以使用Numpy 数组构建这个矩阵(数据类型是np.float32),然后把它传给函数cv2.warpAffine()即可完成平移。
旋转
对一个图像绕图片左下角原点旋转θ角, 需要使用到下面形式的旋转矩阵:
M=\left[\begin{matrix}
\cos\theta & -\sin\theta \\[8pt]
\sin\theta & \cos\theta
\end{matrix}\right]OpenCV支持绕任意点进行旋转的矩阵:
M=\left[\begin{matrix}
\alpha & \beta & (1-\alpha)\cdot center\cdot x-\beta\cdot center\cdot y \\[8pt]
-\beta & \alpha & \beta\cdot center\cdot x+(1-\alpha)\cdot center\cdot x
\end{matrix}\right]其中,
<!–hexoPostRenderEscape:
\alpha = scale\cdot \cos\theta
\beta = scale\cdot \sin\theta
:hexoPostRenderEscape–>
为了构建这个旋转矩阵,OpenCV 提供了函数:cv2.getRotationMatrix2D()。
import cv2
import numpy as np
img=cv2.imread('miss.jpg',0)
rows,cols=img.shape # 得到图像的宽高
# (旋转中心,旋转角度,旋转后的缩放因子)
M=cv2.getRotationMatrix2D((cols/2,rows/2),45,0.6) # 旋转45度
# 对图像img施加仿射变换M,第三个参数是输出图像的尺寸中心
dst=cv2.warpAffine(img,M,(2*cols,2*rows))
cv2.namedWindow('image', cv2.WINDOW_NORMAL)
while(1):
cv2.imshow('image',dst)
if cv2.waitKey(1)==27:
break
cv2.destroyAllWindows()
仿射变换
在仿射变换中,原图中所有的平行线在结果图像中同样平行(这是一种线性变换)。为了创建这个矩阵,我们需要从原图像中找到三个点以及他们在输出图像中的位置,然后利用cv2.getAffineTransform()创建一个2x3 的矩阵,最后这个矩阵会被传给函数cv2.warpAffine()。
import cv2
import numpy as np
img=cv2.imread('miss.jpg')
rows,cols,channel=img.shape
# 选中原图像上的三个点
before_points=np.float32([[50,50],[200,50],[50,200]])
# 定义原图上那三个点在最终目标图片上的位置
after_points=np.float32([[10,100],[200,50],[100,250]])
# 基于before_points和after_points生成仿射变换矩阵
M=cv2.getAffineTransform(before_points,after_points)
dst=cv2.warpAffine(img,M,(cols,rows)) # 对图像img施加定义好了的仿射变换
cv2.imshow('src', img)
cv2.imshow('dst', dst)
cv2.waitKey(0)
cv2.destroyAllWindows()
透视变换
对于透视变换,我们需要一个 3x3 的变换矩阵。变换前后直线还是直线。
要构建这个变换矩阵,你需要在输入图像上找4个点,并指定它们在输出图像上对应的位置。这四个点中的任意三个都不能共线。这个变换矩阵可以用函数cv2.getPerspectiveTransform()构建。然后把这个矩阵传给函数cv2.warpPerspective()来对源图像进行透视变换。
import cv2
import numpy as np
img=cv2.imread('ball.png')
rows,cols,ch=img.shape
before_points = np.float32([[56,65],[368,52],[28,387],[389,390]])
after_points = np.float32([[0,0],[300,0],[0,300],[300,300]])
M=cv2.getPerspectiveTransform(before_points,after_points)
dst=cv2.warpPerspective(img,M,(300,300))
cv2.imshow('src', img)
cv2.imshow('dst', dst)
cv2.waitKey(0)
cv2.destroyAllWindows()
图像阈值
简单阈值
使用cv2.threshhold()进行简单阈值处理,像素值高于阈值时,我们给这个像素赋予一个新值(可能是白色),否则我们给它赋予另外一种颜色(也许是黑色)。因为这个函数接受的是灰度图,所以像素值其实就是图像的亮度。
这个函数的第一个参数就是原图像,原图像应该是灰度图(不是灰度图则转换为灰度图);第二个参数就是用来对像素值进行分类的阈值;第三个参数就是当像素值高于(有时是小于)阈值时应该被赋予的新的像素值。
OpenCV提供了多种不同的阈值方法,这是由第四个参数来决定的。这些方法包括:
- cv2.THRESH_BINARY
- cv2.THRESH_BINARY_INV
- cv2.THRESH_TRUNC
- cv2.THRESH_TOZERO
- cv2.THRESH_TOZERO_INV
cv2.threshhold()有两个返回值,第一个为retVal,第二个就是阈值化处理之后的结果图像。
import cv2
import numpy as np
from matplotlib import pyplot as plt
img=cv2.imread('gradient.png',0)
# 对源图像进行各种类型的阈值化处理
ret,thresh1=cv2.threshold(img,127,255,cv2.THRESH_BINARY) # 高于127时赋值为255(白色),低于127时赋值为0(黑色)
ret,thresh2=cv2.threshold(img,127,255,cv2.THRESH_BINARY_INV)
ret,thresh3=cv2.threshold(img,127,255,cv2.THRESH_TRUNC)
ret,thresh4=cv2.threshold(img,127,255,cv2.THRESH_TOZERO)
ret,thresh5=cv2.threshold(img,127,255,cv2.THRESH_TOZERO_INV)
titles = ['Original Image','BINARY','BINARY_INV','TRUNC','TOZERO','TOZERO_INV']
images = [img, thresh1, thresh2, thresh3, thresh4, thresh5]
for i in range(6):
plt.subplot(2,3,i+1),plt.imshow(images[i],'gray')
plt.title(titles[i])
plt.xticks([]),plt.yticks([]) # 隐藏子图的刻度
plt.show()
自适应阈值
前面所说的简单阈值其实是全局阈值,即整幅图像采用同一个数作为阈值。但这种方法显然并不适合于所有情况,尤其是当同一幅图像上的不同部分具有不同亮度时。这种情况下我们就需要采用自适应阈值。
此时的阈值是根据图像上的每一个小区域计算与其对应的阈值。因此在同一幅图像上的不同区域采用的实际上是不同的阈值,从而使我们能在图像各部分亮度差异很大的情况下得到更好的结果。
下面的程序展示了简单阈值和自适应阈值的区别:
<!–hexoPostRenderEscape:
import cv2
import numpy as np
from matplotlib import pyplot as plt
img = cv2.imread('dave.png',0)
# 中值滤波(除噪点)
img = cv2.medianBlur(img,5)
# 这里是简单阈值
ret,th1 = cv2.threshold(img,127,255,cv2.THRESH_BINARY)
# 使用cv2.adaptiveThreshold()开启自适应阈值,分别使用两种计算阈值的方法:
# ADAPTIVE_THRESH_MEAN_C阈值取自相邻区域的平均值 和
# ADAPTIVE_THRESH_GAUSSIAN_C阈值取值相邻区域的加权和,权重为一个高斯窗口
th2 = cv2.adaptiveThreshold(img,255,cv2.ADAPTIVE_THRESH_MEAN_C,cv2.THRESH_BINARY,11,2)
th3 = cv2.adaptiveThreshold(img,255,cv2.ADAPTIVE_THRESH_GAUSSIAN_C,cv2.THRESH_BINARY,11,2)
# 11 为邻域大小Block size(用来计算阈值的区域大小), 2 为C 值(一个常数,阈值就等于的平均值或者加权平均值减去这个常数)
titles = ['Original Image', 'Global Thresholding (v = 127)', 'Adaptive Mean Thresholding', 'Adaptive Gaussian Thresholding']
images = [img, th1, th2, th3]
for i in range(4):
plt.subplot(2,2,i+1),plt.imshow(images[i],'gray')
plt.title(titles[i])
plt.xticks([]),plt.yticks([])
plt.show()
:hexoPostRenderEscape–>
Otsu’s 二值化
前面提到的cv2.threshold()的第一个返回值retVal,当我们使用 Otsu’s 二值化时就会用到它。
在使用全局(简单)阈值时,我们就是随便给了一个数来做阈值,那我们怎么知道我们选取的这个数的好坏呢?答案就是不停的尝试。如果是一副双峰图像(双峰图像是指图像直方图中存在两个峰)呢?我们岂不是应该在两个峰之间的峰谷选一个值作为阈值?这就是Otsu’s二值化要做的。简单来说就是对一副双峰图像自动根据其直方图计算出一个阈值。(对于非双峰图像,这种方法得到的结果可能会不理想)。
进行Otsu’s 二值化处理用到的函数还是cv2.threshold(),但是需要多传入一个参数(flag) cv2.THRESH_OTSU。这时要把阈值设为0。然后算法会自动找到最优阈值,这个最优阈值就是返回值retVal。如果不使用Otsu’s二值化即不传入后面的那个参数,cv2.threshold()返回的retVal值与设定的阈值相等。
下面的例子中,输入图像是一副带有噪声的图像。我们将使用几种方法来对其进行阈值处理,第一种方法,我们设127为全局阈值。第二种方法,直接使用Otsu二值化。第三种方法,我们首先使用一个5x5 的高斯过滤除去噪音,然后再使用Otsu二值化。这里可以看看噪音去除后对结果的影响有多大。
import cv2
import numpy as np
from matplotlib import pyplot as plt
img = cv2.imread('fingerprint.jpg',0)
# 使用全局阈值
ret1,th1 = cv2.threshold(img,127,255,cv2.THRESH_BINARY)
# 直接使用Otsu's二值化
ret2,th2 = cv2.threshold(img,0,255,cv2.THRESH_BINARY+cv2.THRESH_OTSU)
# 高斯滤波之后再使用Otsu's二值化,(5,5)为高斯核的大小,0 为标准差
blur = cv2.GaussianBlur(img,(5,5),0)
ret3,th3 = cv2.threshold(blur,0,255,cv2.THRESH_BINARY+cv2.THRESH_OTSU) # 这里threshold()的阈值一定要设为0
# 下面画出这三种阈值处理后的目标图像和它们的直方图
images = [img, 0, th1,
img, 0, th2,
blur, 0, th3]
titles = ['Original Noisy Image','Histogram','Global Thresholding (v=127)',
'Original Noisy Image','Histogram',"Otsu's Thresholding",
'Gaussian filtered Image','Histogram',"Otsu's Thresholding"]
for i in range(3):
plt.subplot(3,3,i*3+1),plt.imshow(images[i*3],'gray')
plt.title(titles[i*3]), plt.xticks([]), plt.yticks([])
plt.subplot(3,3,i*3+2),plt.hist(images[i*3].ravel(),256) # 使用plt.hist(参数为一维数组)画直方图,使用array.ravel()将多维数组转换为一维
plt.title(titles[i*3+1]), plt.xticks([]), plt.yticks([])
plt.subplot(3,3,i*3+3),plt.imshow(images[i*3+2],'gray')
plt.title(titles[i*3+2]), plt.xticks([]), plt.yticks([])
plt.show()
Otsu’s 二值化的工作原理
因为是双峰图,Otsu 算法就是要找到一个阈值(t), 使得同一类加权方
差最小,需要满足下列关系式:
\sigma^2_w(t) = q_1(t)\sigma^2_1(t) + q_2(t)\sigma^2_2(t)
其中,
<!–hexoPostRenderEscape:
q_1(t) = \sum_{i=1}^t P(i)\hspace{2em}\&\hspace{2em} q_1(t) = \sum_{i=t+1}^I P(i)
\mu_1(t) = \sum_{i=1}^t = \frac{i P(i)}{q_1(t)}\hspace{2em} \& \hspace{2em}\mu_2(t) = \sum_{i=t+1}^I \frac{iP(i)}{q_2(t)}
\sigma_1^2(t) = \sum_{i=1}^t\left[i-\mu_1(t)\right]^2 \frac{P(i)}{q_1(t)}\hspace{2em} \& \hspace{2em}\sigma_2^2(t) = \sum_{i=t+1}^I\left[i-\mu_1(t)\right]^2 \frac{iP(i)}{q_2(t)}
:hexoPostRenderEscape–>其实就是在两个峰之间找到一个阈值t,将这两个峰分开,并且使每一个峰内的方差最小。
图像平滑
2D卷积
与数字信号一样,我们也可以对2D图像实施加低通滤波(LPF),高通滤波(HPF)等。LPF 帮助我们去除噪音,模糊图像。HPF帮助我们找到图像的边缘。OpenCV提供的函数cv.filter2D()可以让我们对一幅图像进行卷积操作。
下面我们将对一幅图像使用平均滤波器,一个5x5 的平均滤波器核如下:
K=\frac1{25}\left[\begin{matrix}
1 & 1 & 1 & 1 & 1 \\[6pt]
1 & 1 & 1 & 1 & 1 \\[6pt]
1 & 1 & 1 & 1 & 1 \\[6pt]
1 & 1 & 1 & 1 & 1 \\[6pt]
1 & 1 & 1 & 1 & 1
\end{matrix}\right]
操作过程大致是这样的:将核放在图像的一个像素A 上,求与核对应的图像上25(5x5)个像素的和,再取平均数,用这个平均数替代像素A的值。重复以上操作直到将图像的每一个像素值都更新一边。
import cv2
import numpy as np
from matplotlib import pyplot as plt
img = cv2.imread('opencv-logo.png')
kernel = np.ones((5,5),np.float32)/25
#cv.Filter2D(src, dst, kernel, anchor=(-1, -1))
#ddepth => desired depth of the destination image,期望的目标图像的深度
dst = cv2.filter2D(img,-1,kernel) # 当ddepth=-1时,则输出的目标图像与源图像有相同的深度
plt.subplot(121),plt.imshow(img),plt.title('Original')
plt.xticks([]), plt.yticks([])
plt.subplot(122),plt.imshow(dst),plt.title('Averaging')
plt.xticks([]), plt.yticks([])
plt.show()
以下src.depth()和ddepth的组合是可以使用的:
- src.depth() = CV_8U, ddepth = -1 | CV_16S | CV_32F | CV_64F
- src.depth() = CV_16U | CV_16S, ddepth = -1 | CV_32F | CV_64F
- src.depth() = CV_32F, ddepth = -1 | CV_32F | CV_64F
- src.depth() = CV_64F, ddepth = -1 | CV_64F
图像模糊
使用低通滤波器可以达到图像模糊的目的,这对于去除噪音很有帮助。这个处理其实就是去除图像中的高频成分(比如:噪音,边界),所以边界也会被模糊一点。。OpenCV提供了四种模糊技术。
平均
这是由一个归一化卷积框完成的,它通过用卷积框覆盖区域内所有像素的平均值来代替中心元素。可以使用函数cv2.blur()和cv2.boxFilter()来完这个任务。我们需要设定卷积框的宽和高。
下面是一个3x3 的归一化卷积框:
K=\frac1{9}\left[\begin{matrix}
1 & 1 & 1 \\[6pt]
1 & 1 & 1 \\[6pt]
1 & 1 & 1
\end{matrix}\right]下面这个例子只是在上面例子做了一下更改:
<!–hexoPostRenderEscape:
import cv2
import numpy as np
from matplotlib import pyplot as plt
img = cv2.imread('opencv-logo.png')
# 使用归一化卷积来模糊图像
blur = cv2.blur(img,(5,5))
plt.subplot(121),plt.imshow(img),plt.title('Original')
plt.xticks([]), plt.yticks([])
plt.subplot(122),plt.imshow(blur),plt.title('Blurred')
plt.xticks([]), plt.yticks([])
plt.show()
:hexoPostRenderEscape–>
高斯模糊
现在把卷积核换成高斯核(简单来说就是,方框不变,原来每个方框的值是相等的,现在里面的值是符合高斯分布的,方框中心的值最大,其余方框根据距离中心元素的距离递减,构成一个高斯小山包。原来是求平均数现在变成求加权平均数)。
实现的函数是cv2.GaussianBlur()。我们需要指定高斯核的宽和高(必须是奇数)以及高斯函数沿X,Y 方向的标准差。如果我们只指定了X方向的的标准差,Y方向也会取相同值。如果两个标准差都是0,那么函数会根据核函数的大小自己计算。
高斯滤波可以有效的从图像中去除高斯噪音。如果需要的话,你也可以使用函数cv2.getGaussianKernel()自己构建一个高斯核。
import cv2
import numpy as np
from matplotlib import pyplot as plt
img = cv2.imread('opencv-logo.png')
# 参数0 表示要根据窗口大小(5,5)来计算高斯函数标准差
blur = cv2.GaussianBlur(img,(5,5),0)
plt.subplot(121),plt.imshow(img),plt.title('Original')
plt.xticks([]), plt.yticks([])
plt.subplot(122),plt.imshow(blur),plt.title('Blurred')
plt.xticks([]), plt.yticks([])
plt.show()
中值模糊
所谓中值模糊,就是用与卷积框对应像素的中值来替代中心像素的值。这个滤波器经常用来去除椒盐噪声。前面的滤波器都是用计算得到的一个新值来取代中心像素的值,而中值滤波是用中心像素周围的值来取代它。它能有效的去除噪声。卷积核的大小也应该是一个奇数。
import cv2
import numpy as np
from matplotlib import pyplot as plt
img = cv2.imread('opencv-logo.png')
median = cv2.medianBlur(img,5)
plt.subplot(121),plt.imshow(img),plt.title('Original')
plt.xticks([]), plt.yticks([])
plt.subplot(122),plt.imshow(blur),plt.title('Blurred')
plt.xticks([]), plt.yticks([])
plt.show()
双边滤波
函数cv2.bilateralFilter()能在保持边界清晰的情况下有效的去除噪音,但是这种操作与其他滤波器相比会比较慢。
我们已经知道高斯滤波器是求中心点邻近区域像素的高斯加权平均值,这种高斯滤波器只考虑了像素之间的空间关系(像素分布),而没有考虑像素值之间的关系(像素的相似度),所以这种方法并不会考虑一个像素是否位于边界,因此边界也会被模糊掉,而这不是我们想要的。
双边滤波同时使用了空间高斯权重和灰度值相似性高斯权重。空间高斯函数确保只有邻近区域的像素对中心点有影响,灰度值相似性高斯函数确保只有与中心像素灰度值相近的才会被用来做模糊运算。所以这种方法会确保边界不会被模糊掉,因为边界处的灰度值变化会比较大。
import cv2
import numpy as np
from matplotlib import pyplot as plt
img = cv2.imread('wood.jpg')
# cv2.bilateralFilter(src, d, sigmaColor, sigmaSpace)
#d => 过滤过程中使用的每个像素邻域的直径。
# 参数9表示邻域直径,两个75 分别是空间高斯函数标准差,灰度值相似性高斯函数标准差
blur = cv2.bilateralFilter(img,9,75,75)
plt.subplot(121),plt.imshow(img),plt.title('Original')
plt.xticks([]), plt.yticks([])
plt.subplot(122),plt.imshow(blur),plt.title('Blurred')
plt.xticks([]), plt.yticks([])
plt.show()
可见,双边滤波不会模糊边界。
形态学转换
形态学操作是指根据图像形状进行的简单操作。一般情况下,对二值化图像进行的操作需要输入两个参数,一个是原始图像,第二个被称为结构化元素或核,它是用来决定操作的性质的。两个基本的形态学操作是腐蚀和膨胀。他们的变体则构成了开运算,闭运算,梯度等。
腐蚀
就像土壤侵蚀一样,这个操作会把前景物体的边界腐蚀掉(但是前景仍然是白色)。那这是怎么做到的呢?卷积核沿着图像滑动,如果与卷积核对应的原图像的所有像素值都是1,那么中心元素就保持原来的像素值,否则就变为零。这会产生什么影响呢?根据卷积核的大小,靠近前景的所有像素都会被腐蚀掉(变为0),所以前景物体会变小,整幅图像的白色区域会减少。这对于去除白色噪声很有用,也可以用来断开两个连在一块的物体等。
这里我们有一个例子,使用一个5x5 的卷积核。
import cv2
import numpy as np
img = cv2.imread('ball.png',0)
cv2.imshow('src', img)
# 构建5*5的卷积核
kernel = np.ones((5,5),np.uint8)
# 腐蚀操作
erosion = cv2.erode(img,kernel,iterations = 1)
cv2.imshow('dst', erosion)
cv2.waitKey(0)
cv2.destroyAllWindows()
膨胀
与腐蚀相反,与卷积核对应的原图像的像素值中只要有一个是1,中心元素的像素值就是1。所以这个操作会增加图像中的白色区域(前景)。
一般在去噪声时先用腐蚀再用膨胀。因为腐蚀在去掉白噪声的同时,也会使前景对象变小。所以我们再对它进行膨胀。这时噪声已经被去除了,不会再回来了,但是前景还在增加。膨胀也可以用来连接两个分开的物体。
import cv2
import numpy as np
img = cv2.imread('j.png',0)
cv2.namedWindow('src', cv2.WINDOW_NORMAL)
cv2.namedWindow('dst', cv2.WINDOW_NORMAL)
cv2.imshow('src', img)
# 构建5*5的卷积核
kernel = np.ones((5,5),np.uint8)
# 膨胀操作
dilation = cv2.dilate(img,kernel,iterations = 1)
cv2.imshow('dst', dilation)
cv2.waitKey(0)
cv2.destroyAllWindows()
开运算
先进行腐蚀再进行膨胀就叫做开运算。,它被用来去除噪声。这里我们用到的函数是cv2.morphologyEx()。
import cv2
import numpy as np
img = cv2.imread('j_noise.png',0)
cv2.namedWindow('src', cv2.WINDOW_NORMAL)
cv2.namedWindow('dst', cv2.WINDOW_NORMAL)
cv2.imshow('src', img)
# 构建5*5的卷积核
kernel = np.ones((5,5),np.uint8)
# 开运算
opening = cv2.morphologyEx(img, cv2.MORPH_OPEN, kernel)
cv2.imshow('dst', opening)
cv2.waitKey(0)
cv2.destroyAllWindows()
闭运算
闭运算其实就是先膨胀再腐蚀。它经常被用来填充前景物体中的小洞,或者前景物体上的小黑点。
import cv2
import numpy as np
img = cv2.imread('j_noise2.png',0)
cv2.namedWindow('src', cv2.WINDOW_NORMAL)
cv2.namedWindow('dst', cv2.WINDOW_NORMAL)
cv2.imshow('src', img)
# 构建5*5的卷积核
kernel = np.ones((5,5),np.uint8)
# 闭运算
closing = cv2.morphologyEx(img, cv2.MORPH_CLOSE, kernel)
cv2.imshow('dst', closing)
cv2.waitKey(0)
cv2.destroyAllWindows()
形态学梯度
所谓形态学梯度,其实就是一幅图像膨胀之后与腐蚀之后的差别。结果看上去就像前景物体的轮廓。
import cv2
import numpy as np
img = cv2.imread('j.png',0)
# 构建5*5的卷积核
kernel = np.ones((5,5),np.uint8)
# 形态学梯度
gradient = cv2.morphologyEx(img, cv2.MORPH_GRADIENT, kernel)
cv2.namedWindow('dst', cv2.WINDOW_NORMAL)
cv2.imshow('dst', gradient)
cv2.waitKey(0)
cv2.destroyAllWindows()
礼帽
原始图像与进行开运算之后得到的图像的差。
下面的例子是用一个9x9 的核进行礼帽操作的结果:
<!–hexoPostRenderEscape:
import cv2
import numpy as np
img = cv2.imread('j.png',0)
cv2.namedWindow('src', cv2.WINDOW_NORMAL)
cv2.namedWindow('dst', cv2.WINDOW_NORMAL)
cv2.imshow('src', img)
# 构建9*9的卷积核
kernel = np.ones((9,9),np.uint8)
# 礼帽操作
tophat = cv2.morphologyEx(img, cv2.MORPH_TOPHAT, kernel)
cv2.imshow('dst', tophat)
cv2.waitKey(0)
cv2.destroyAllWindows()
:hexoPostRenderEscape–>
黑帽
进行闭运算之后得到的图像与原始图像的差。
import cv2
import numpy as np
img = cv2.imread('j.png',0)
cv2.namedWindow('src', cv2.WINDOW_NORMAL)
cv2.namedWindow('dst', cv2.WINDOW_NORMAL)
cv2.imshow('src', img)
# 构建9*9的卷积核
kernel = np.ones((9,9),np.uint8)
# 黑帽操作
blackhat = cv2.morphologyEx(img, cv2.MORPH_BLACKHAT, kernel)
cv2.imshow('dst', blackhat)
cv2.waitKey(0)
cv2.destroyAllWindows()
形态学操作之间的关系
更多种类的结构化元素
在前面的例子中,我们使用的都是Numpy 构建的结构化元素(核(,它是正方形的,但
有时我们需要构建一个椭圆形/圆形的核。为了实现这种要求,OpenCV提供了函数cv2.getStructuringElement()。你只需要告诉他你需要的核的形状和大小它会自动帮你生成特定形状的核。
# Rectangular Kernel,正方形核
>>> cv2.getStructuringElement(cv2.MORPH_RECT,(5,5))
array([[1, 1, 1, 1, 1],
[1, 1, 1, 1, 1],
[1, 1, 1, 1, 1],
[1, 1, 1, 1, 1],
[1, 1, 1, 1, 1]], dtype=uint8)
# Elliptical Kernel,椭圆形核
>>> cv2.getStructuringElement(cv2.MORPH_ELLIPSE,(5,5))
array([[0, 0, 1, 0, 0],
[1, 1, 1, 1, 1],
[1, 1, 1, 1, 1],
[1, 1, 1, 1, 1],
[0, 0, 1, 0, 0]], dtype=uint8)
# Cross-shaped Kernel,十字形核
>>> cv2.getStructuringElement(cv2.MORPH_CROSS,(5,5))
array([[0, 0, 1, 0, 0],
[0, 0, 1, 0, 0],
[1, 1, 1, 1, 1],
[0, 0, 1, 0, 0],
[0, 0, 1, 0, 0]], dtype=uint8)
图像梯度
所谓图像梯度,简单来说,就是求导。OpenCV提供了三种不同的梯度滤波器,或者说是高通滤波器:Sobel,Scharr 和Laplacian。
Sobel,Scharr 其实就是求一阶或二阶导数,Scharr 是对Sobel(使用小的卷积核求解梯度角度时)的优化。Laplacian 则是求二阶导数。
Sobel 算子和Scharr 算子
Sobel 算子是高斯平滑与微分操作的结合体,所以它的抗噪声能力很好。你可以设定求导的方向(xorder 或yorder)。还可以设定使用的卷积核的大小(ksize)。如果ksize=-1,可以使用3x3 的Scharr 滤波器,它的的效果要比3x3 的Sobel 滤波器好(而且速度相同,所以在使用3x3 滤波器时应该尽量使用Scharr 滤波器)。
3x3 的Scharr 滤波器卷积核如下:
Laplacian 算子
拉普拉斯算子可以使用二阶导数的形式定义,可假设其离散实现类似于二阶Sobel 导数,事实上,OpenCV 在计算拉普拉斯算子时直接调用的Sobel 算子。
其计算公式如下:
\Delta src = \frac{\partial^2 src}{\partial x^2} + \frac{\partial^2 src}{\partial y^2}
拉普拉斯滤波器使用的卷积核:
kernel = \left[\begin{matrix}
0 & 1 & 0 \\[6pt]
1 & -4 & 1 \\[6pt]
0 & 1 & 0
\end{matrix}\right]
上面反复提到过,我们可以通过设置输出类型为-1来让输出图像的深度(数据类型)与原图像保持一致,但可以看到,我们在代码中使用的并不是-1,而是cv2.CV_64F。这是为什么呢?
这里可以想象一下一个从黑到白的边界的导数是整数,而一个从白到黑的边界点导数却是负数。如果原图像的深度是np.int8,那么所有的负值都会被截断变成0,换句话说就是会把边界信息丢失掉。
所以如果这两种边界都想检测到,最好的的办法就是将输出的数据类型设置的更高,比如cv2.CV_16S,cv2.CV_64F 等,再取绝对值将它转回到cv2.CV_8U类型。
import cv2
import numpy as np
from matplotlib import pyplot as plt
img = cv2.imread('boxs.png',0)
# 输出类型设为cv2.CV_8U
sobelx8u = cv2.Sobel(img,cv2.CV_8U,1,0,ksize=5)
# 输出类型设为cv2.CV_64F,然后取其绝对值转换为cv2.CV_8U类型
sobelx64f = cv2.Sobel(img,cv2.CV_64F,1,0,ksize=5)
abs_sobel64f = np.absolute(sobelx64f)
sobel_8u = np.uint8(abs_sobel64f)
plt.subplot(1,3,1),plt.imshow(img,cmap = 'gray')
plt.title('Original'), plt.xticks([]), plt.yticks([])
plt.subplot(1,3,2),plt.imshow(sobelx8u,cmap = 'gray')
plt.title('Sobel CV_8U'), plt.xticks([]), plt.yticks([])
plt.subplot(1,3,3),plt.imshow(sobel_8u,cmap = 'gray')
plt.title('Sobel abs(CV_64F)'), plt.xticks([]), plt.yticks([])
plt.show()
Canny边缘检测
Canny 边缘检测是一种非常流行的边缘检测算法,是John F.Canny 在1986 年提出的,它是一个由很多步组成的算法。
去噪
由于边缘检测很容易受到噪声影响,所以第一步是使用5x5 的高斯滤波器去除噪声。
计算图像梯度
对平滑后的图像使用Sobel 算子计算水平方向和竖直方向的一阶导数(图像梯度,Gx 和Gy)。根据得到的这两幅梯度图(Gx 和Gy)找到边界的梯度和方向,公式如下:
Edge\_Gradient(G) = \sqrt{G_x^2 + G_y^2}
Angle(\theta) = tan^{-1}\left(\frac{G_x}{G_y}\right)
梯度的方向一般总是与边界垂直。梯度方向被归为四类:垂直,水平,和两个对角线。
非极大值抑制
在获得梯度的方向和大小之后,应该对整幅图像做一个扫描,去除那些非边界上的点。对每一个像素进行检查,看这个点的梯度是不是周围具有相同梯度方向的点中最大的。
如下图所示:
滞后阈值
现在要确定的是哪些边界才是真正的边界。这时我们需要设置两个阈值:minVal 和maxVal。当图像的灰度梯度高于maxVal 时被认为是真的边界,那些低于minVal 的开始被认为是边界的像素会被抛弃。如果介于两者之间的话,就要看这个点是否与某个被确定为真正的边界点相连,如果是就认为它也是边界点,如果不是就抛弃。
如下图:
我们分析一下上图,A 高于阈值maxVal ,所以是真正的边界点,C 虽然低于maxVal 但高于minVal 并且与A 相连,所以也被认为是真正的边界点。而B 就会被抛弃,因为它不仅低于maxVal 而且不与真正的边界点相连。
可见,选择合适的maxVal和minVal对于能否得到好的结果非常重要。在这一步,一些小的噪声点也会被除去,因为我们假设边界都是一些长的线段。
OpenCV 中的Canny 边界检测
实际上,在OpenCV中使用Canny算法进行边缘检测没有我们想的那么复杂,OpenCV对这几步操作进行了封装,只需要使用一个函数:cv2.Canny(),就可以完成以上几步。
这个函数的第一个参数是输入图像。第二和第三个分别是minVal和maxVal。第四个参数设置用来计算图像梯度的Sobel卷积核的大小,默认值为3。最后一个参数是L2gradient,它可以用来设定求梯度大小的方程。如果设为True,就会使用我们上面提到过的方程,否则将使用$Edge\_Gradient(G) = |G_x^2| + |G_y^2|$作为代替,默认值为False。
import cv2
import numpy as np
from matplotlib import pyplot as plt
img = cv2.imread('ball.png',0)
# cv2.Canny(src, minVal, maxVal)
edges = cv2.Canny(img,100,200)
plt.subplot(121),plt.imshow(img,cmap = 'gray')
plt.title('Original Image'), plt.xticks([]), plt.yticks([])
plt.subplot(122),plt.imshow(edges,cmap = 'gray')
plt.title('Edge Image'), plt.xticks([]), plt.yticks([])
plt.show()
图像金字塔
一般情况下,我们要处理是一副具有固定分辨率的图像。但是有些情况下,我们需要对同一图像的不同分辨率的子图像进行处理。比如,我们要在一幅图像中查找某个目标,比如脸,我们不知道目标在图像中的尺寸大小。
这种情况下,我们需要创建一组图像,这些图像是具有不同分辨率的原始图像。我们把这组图像叫做图像金字塔(简单来说就是同一图像的不同分辨率的子图集合)。如果我们把最大的图像放在底部,最小的放在顶部,看起来像一座金字塔,图像金字塔因而得名。
有两类图像金字塔:高斯金字塔和拉普拉斯金字塔。高斯金字塔的顶部是通过将底部图像中的连续的行和列去除得到的。顶部图像中的每个像素值等于下一层图像中5 个像素的高斯加权平均值。这样,操作一次一个M N 的图像就变成了一个M/2 N/2的图像。所以这幅图像的面积就变为原来图像面积的四分之一。
连续进行这样的操作我们就会得到一个分辨率不断下降的图像金字塔。我们可以使用函数cv2.pyrDown()和cv2.pyrUp()来构建图像金字塔。
函数cv2.pyrDown()从一个高分辨率大尺寸的图像向上构建一个金子塔(尺寸变小,分辨率降低)。
函数cv2.pyrUp()则从一个低分辨率小尺寸的图像向下构建一个金子塔(尺寸变大,但分辨率不会增加)。
OpenCV中的轮廓
什么是轮廓
轮廓可以简单认为是将连续的点(连着边界)连在一起的曲线,具有相同的颜色或者灰度。轮廓在形状分析和物体的检测和识别中很有用。
- 为了更加准确,要使用二值化图像。在寻找轮廓之前,要进行阈值化处理或者Canny 边界检测。
- 查找轮廓的函数会修改原始图像。如果你在找到轮廓之后还想使用原始图像的话,你应该将原始图像存储到其他变量中。
- 在OpenCV 中,查找轮廓就像在黑色背景中超白色物体。你应该记住,要找的物体应该是白色而背景应该是黑色。
让我们看看如何在OpenCV中使用cv2.findContours()在一个二值图像中查找轮廓:
该函数有三个参数,第一个是输入图像,第二个是轮廓检索模式,第三个是轮廓近似方法。返回值有三个,第一个是图像,第二个是轮廓,第三个是(轮廓的)层析结构。轮廓(第二个返回值)是一个Python列表,其中存储着图像中的所有轮廓。每一个轮廓都是一个Numpy 数组,包含对象边界点(x,y)的坐标。
怎样绘制轮廓
函数cv2.drawContours()可以被用来绘制轮廓。它可以根据你提供的边界点绘制任何形状。它的第一个参数是原始图像,第二个参数是轮廓,一个Python列表。第三个参数是轮廓的索引(在绘制独立轮廓是很有用,当设置为-1时表示绘制所有轮廓)。接下来的参数是轮廓的颜色和厚度等。
下面的例子在一幅图像上绘制所有的轮廓:
import numpy as np
import cv2
im = cv2.imread('test.jpg')
imgray = cv2.cvtColor(im,cv2.COLOR_BGR2GRAY)
ret,thresh = cv2.threshold(imgray,127,255,0)
image, contours, hierarchy = cv2.findContours(thresh,cv2.RETR_TREE,cv2.CHAIN_APPROX_SIMPLE)
使用以下代码可以绘制单个独立的轮廓:
img = cv2.drawContour(img, contours, -1, (0,255,0), 3)
轮廓特征
矩
图像的矩可以帮助我们计算图像的质心,面积等。
import cv2
import numpy as np
img = cv2.imread('ball.png',0)
ret,thresh = cv2.threshold(img,127,255,0)
contours,hierarchy = cv2.findContours(thresh, 1, 2)
cnt = contours[0]
M = cv2.moments(cnt) # 使用cv2.moments()取得轮廓的矩,返回值是字典类型
print(M)
根据这些矩的值,我们可以计算出对象的重心: $C_x = \frac{M_{10}}{M_{100}}, C_y=\frac{M_{01}}{M_{100}}$,亦即:
cx = int(M['m10']/M['m00'])
cy = int(M['m01']/M['m00'])
轮廓面积
轮廓的面积可以使用函数cv2.contourArea()计算得到,也可以使用矩(0 阶矩),即M[‘m00’]。
import cv2
import numpy as np
img = cv2.imread('ml.png',0)
ret,thresh = cv2.threshold(img,127,255,0)
contours,hierarchy = cv2.findContours(thresh, 1, 2)
cnt = contours[0]
perimeter = cv2.arcLength(cnt,True)
print(perimeter)
轮廓周长
也被称为弧长。可以使用函数cv2.arcLength() 计算得到。这个函数的第二参数可以用来指定对象的形状是闭合的(True),还是打开的(一条曲线)。
import cv2
import numpy as np
img = cv2.imread('ball.png',0)
ret,thresh = cv2.threshold(img,127,255,0)
contours,hierarchy = cv2.findContours(thresh, 1, 2)
perimeter = cv2.arcLength(contours,True)
print(perimeter)
轮廓近似
将轮廓形状近似到另外一种由更少点组成的轮廓形状,新轮廓的点的数目由我们设定的准确度来决定。使用的是Douglas-Peucker算法。
为了帮助理解,假设我们要在一幅图像中查找一个矩形,但是由于图像的种种原因,我们不能得到一个完美的矩形,而是一个“坏形状”。现在你就可以使用这个函数来近似这个形状了。这个函数的第二个参数叫epsilon,它是从原始轮廓到近似轮廓的最大距离。它是一个准确度参数。选择一个好的epsilon 对于得到满意结果非常重要。
import cv2
import numpy as np
img = cv2.imread('ml.png',0)
ret,thresh = cv2.threshold(img,127,255,0)
contours,hierarchy = cv2.findContours(thresh, 1, 2)
cnt = contours[0]
epsilon = 0.1*cv2.arcLength(cnt,True)
approx = cv2.approxPolyDP(cnt,epsilon,True)
print(approx)
凸包
凸包与轮廓相似,但它们是不同的两个特征,虽然有些情况下它们给出的结果是一样的。函数cv2.convexHull()可以用来检测一个曲线是否具有凸性缺陷,并能纠正缺陷。
一般来说,凸性曲线总是凸出来的,至少是平的。如果有地方凹进去了就被叫做凸性缺陷。例如下图中的手。红色曲线显示了手的凸包,凸性缺陷被双箭头标出来了。
计算轮廓的凸包需要使用cv2.covexHull()函数:
hull = cv2.convexHull(points[, hull[, clockwise[, returnPoints]]它的各参数含义如下:
- points 我们要传入的轮廓
- hull 输出的像素列表,通常不需要
- clockwise 方向标志。如果设置为True,则输出的凸包是顺时针方向的,否则为逆时针方向。
- returnPoints 默认值为True,它会返回凸包上点的坐标。如果设置为False,就会返回与凸包点对应的轮廓上的点。
import cv2
import numpy as np
img = cv2.imread('ml.png',0)
ret,thresh = cv2.threshold(img,127,255,0)
contours,hierarchy = cv2.findContours(thresh, 1, 2)
cnt = contours[0]
hull = cv2.convexHull(cnt) # 计算凸包
print(hull)
凸性检测
函数cv2.isContourConvex() 可以用来检测一个曲线是不是凸的,它只能返回True 或False。
import cv2
import numpy as np
img = cv2.imread('ml.png',0)
ret,thresh = cv2.threshold(img,127,255,0)
contours,hierarchy = cv2.findContours(thresh, 1, 2)
cnt = contours[0]
k = cv2.isContourConvex(cnt)
print(k)
边界矩形
有两类边界矩形。
直边界矩形 一个直矩形(就是没有旋转的矩形)是不会考虑对象是否旋转的,所以这个边界矩形的面积不是最小的。可以使用函数cv2.boundingRect()查找得到。
import cv2
import numpy as np
img = cv2.imread('lightning.png',0)
ret,thresh = cv2.threshold(img,127,255,0)
contours,hierarchy = cv2.findContours(thresh, 1, 2)
cnt = contours[0]
# (x,y)为矩形左上角的坐标,(w,h)为矩形的宽高
x,y,w,h = cv2.boundingRect(cnt)
img = cv2.rectangle(img,(x,y),(x+w,y+h),(255,255,0),2) # 画出边界矩形
cv2.imshow('boundingRect', img)
cv2.waitKey(0)
cv2.destroyAllWindows()
旋转的边界矩形 这个边界矩形是面积最小的,因为它考虑了对象的旋转。用到的函数为cv2.minAreaRect(),返回的是一个Box2D结构,其中包含矩形左上角角点的坐标(x,y),矩形的宽和高(w,h),以及旋转角度。但是要绘制这个矩形需要矩形的4个角点,可以通过函数cv2.boxPoints() 获得。
import cv2
import numpy as np
img = cv2.imread('lightning.png',0)
ret,thresh = cv2.threshold(img,127,255,0)
contours,hierarchy = cv2.findContours(thresh, 1, 2)
cnt = contours[0]
x,y,angle = cv2.minAreaRect(cnt)
print(angle)
下图中,红色的矩形为直边界矩形,绿色的矩形为旋转的边界矩形:
最小外接圆
函数cv2.minEnclosingCircle() 可以帮我们找到一个对象的外切圆,它是所有能够包括对象的圆中面积最小的一个。
import cv2
import numpy as np
img = cv2.imread('lightning.png',0)
ret,thresh = cv2.threshold(img,127,255,0)
contours,hierarchy = cv2.findContours(thresh, 1, 2)
cnt = contours[0]
(x,y),radius = cv2.minEnclosingCircle(cnt)
center = (int(x),int(y))
radius = int(radius)
img = cv2.circle(img,center,radius,(0,255,0),2) # 画出最小外接圆
cv2.imshow('minEnclosingCircle', img)
cv2.waitKey(0)
cv2.destroyAllWindows()
椭圆拟合
使用的函数为cv2.ellipse(),返回值其实就是旋转边界矩形的内切圆。
import cv2
import numpy as np
img = cv2.imread('lightning.png',0)
ret,thresh = cv2.threshold(img,127,255,0)
contours,hierarchy = cv2.findContours(thresh, 1, 2)
cnt = contours[0]
ellipse = cv2.fitEllipse(cnt)
img = cv2.ellipse(img,ellipse,(255,255,0),2) # 在原图上画出椭圆
cv2.imshow('minEnclosingCircle', img)
cv2.waitKey(0)
cv2.destroyAllWindows()
直线拟合
我们可以根据一组点拟合出一条直线,同样我们也可以为图像中的白色点拟合出一条直线。
import cv2
import numpy as np
img = cv2.imread('lightning.png',0)
ret,thresh = cv2.threshold(img,127,255,0)
contours,hierarchy = cv2.findContours(thresh, 1, 2)
cnt = contours[0]
rows,cols = img.shape[:2]
[vx,vy,x,y] = cv2.fitLine(cnt, cv2.DIST_L2,0,0.01,0.01)
lefty = int((-x*vy/vx) + y)
righty = int(((cols-x)*vy/vx)+y)
img = cv2.line(img,(cols-1,righty),(0,lefty),(0,255,0),2)
cv2.imshow('line', img)
cv2.waitKey(0)
cv2.destroyAllWindows()
轮廓的性质
长宽比
边界矩形的宽高比。
import cv2
import numpy as np
img = cv2.imread('lightning.png',0)
ret,thresh = cv2.threshold(img,127,255,0)
contours,hierarchy = cv2.findContours(thresh, 1, 2)
cnt = contours[0]
x,y,w,h = cv2.boundingRect(cnt)
aspect_ratio = float(w)/h
print(aspect_ratio)
Extent
轮廓面积与边界矩形面积的比。
Extent = \frac{Object Area}{Bounding Rectangle Area}
import cv2
import numpy as np
img = cv2.imread('lightning.png',0)
ret,thresh = cv2.threshold(img,127,255,0)
contours,hierarchy = cv2.findContours(thresh, 1, 2)
cnt = contours[0]
area = cv2.contourArea(cnt)
x,y,w,h = cv2.boundingRect(cnt)
rect_area = w*h
extent = float(area)/rect_area
print(extent)
Solidity
轮廓面积与凸包面积的比。
Solidity = \frac{Contour Area}{ConvexHull Area}
import cv2
import numpy as np
img = cv2.imread('lightning.png',0)
ret,thresh = cv2.threshold(img,127,255,0)
contours,hierarchy = cv2.findContours(thresh, 1, 2)
cnt = contours[0]
area = cv2.contourArea(cnt)
hull = cv2.convexHull(cnt)
hull_area = cv2.contourArea(hull)
solidity = float(area)/hull_area
print(solidity)
Equivalent Diameter
Equivalent Diameter = \frac{\sqrt{4\times Contour Area}}{\pi}
import cv2
import numpy as np
img = cv2.imread('lightning.png',0)
ret,thresh = cv2.threshold(img,127,255,0)
contours,hierarchy = cv2.findContours(thresh, 1, 2)
cnt = contours[0]
area = cv2.contourArea(cnt)
equi_diameter = np.sqrt(4*area/np.pi)
print(equi_diameter)
方向
也就是对象的方向,下面的方法cv2.fitEllipse()还会返回长轴和短轴的长度:
import cv2
import numpy as np
img = cv2.imread('lightning.png',0)
ret,thresh = cv2.threshold(img,127,255,0)
contours,hierarchy = cv2.findContours(thresh, 1, 2)
cnt = contours[0]
(x,y),(MA,ma),angle = cv2.fitEllipse(cnt)
print(str(x) + ", " + str(y) + ', ' + str(angle))
print(str(MA) + ', ' + str(ma))
极点
一个对象最上面,最下面,最左边,最右边的点。
import cv2
import numpy as np
img = cv2.imread('lightning.png',0)
ret,thresh = cv2.threshold(img,127,255,0)
contours,hierarchy = cv2.findContours(thresh, 1, 2)
cnt = contours[0]
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])
print(leftmost)
print(rightmost)
print(topmost)
print(bottommost)
:hexoPostRenderEscape–>
轮廓的层次结构
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!