书本中接下来介绍了一些基本更加高级概念和操作: 直方图,归一化,平滑化,模糊化,边缘检测等.
4.Histogram(直方图):
直方图:描述了在图像中像素值大小的分布状态.
计算函数:cv2.calcHist(images,channels,mask,histSize,ranges)
channels:是一个列表,表示通道索引.如要计算灰度图的直方图,则该参数为[0],如果要计算三个通道的则为[0,1,2]
mask:若给定了mask,则计算在mask中的直方图,若为None则计算全图的
histSize:也是一个列表,表示每一个通道我们需要使用的区间大小,比如灰度图的,可以使用[32],则以32以及倍数划分了256.
ranges:像素值大小的范围,如RGB的为[0,255]
灰度图像的直方图
image = cv2.imread(args["image"])
image_gray = cv2.cvtColor(image,cv2.COLOR_BGR2GRAY)
hist = cv2.calcHist([image], [0], None, [256], [0,255])
plt.figure()
plt.title("the histogram of gray image")
plt.xlabel("the bins")
plt.ylabel("number of Pixels")
plt.xlim([0,256])
plt.plot(hist)
plt.show()
颜色图的直方图
channels = cv2.split(image)
colors = ("b", "g", "r")
for (channel, color) in zip(channels, colors):
hist = cv2.calcHist([channel], [0], None, [256], [0, 256])
plt.plot(hist, color = color)
plt.xlim([0, 256])
plt.show()
同时计算多通道的直方图,举个例子说明一下它的意义,我们需要知道一张图里面有多少个像素点是B=20,G=30,这个时候就需要计算B和G通道的直方图.
channels = cv2.split(image)
fig = plt.figure()
ax = fig.add_subplot(131)
hist = cv2.calcHist((channels[1], channels[0]), (0, 1), None,
[32, 32], [0, 256, 0, 256])
p = ax.imshow(hist, interpolation = "nearest")
ax.set_title("2D Color Histogram for G and B")
plt.colorbar(p)
ax = fig.add_subplot(132)
hist = cv2.calcHist((channels[1], channels[2]), [0, 1], None,
[32, 32], [0, 256, 0, 256])
p = ax.imshow(hist, interpolation = "nearest")
ax.set_title("2D Color Histogram for G and R")
plt.colorbar(p)
ax = fig.add_subplot(133)
hist = cv2.calcHist((channels[0], channels[2]), [0, 1], None,
[32, 32], [0, 256, 0, 256])
p = ax.imshow(hist, interpolation = "nearest")
ax.set_title("2D Color Histogram for B and R")
plt.colorbar(p)
对于计算三通道的直方图也是类似的,意义也是类似于计算二通道的.
直方图均衡化:提高全图的对比度,直方图均衡化用于在灰度图之中,
实现该过程使用cv2.equalizeHist(image)
image = cv2.imread(args["image"])
image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
eq = cv2.equalizeHist(image)
cv2.imshow("Histogram Equalization", np.hstack([image, eq]))
可以看出均衡化之后,亮的更加亮了,暗的更加暗了.
直方图加掩模:计算在感兴趣的mask之中的直方图
#define a function to draw the histogram graph
def plot_histogram(image, title, mask = None):
chans = cv2.split(image)
colors = ("b", "g", "r")
plt.figure()
plt.title(title)
plt.xlabel("Bins")
plt.ylabel("# of Pixels")
for (chan, color) in zip(chans, colors):
hist = cv2.calcHist([chan], [0], mask, [256], [0, 256])
plt.plot(hist, color = color)
plt.xlim([0, 256])
mask = np.zeros(image.shape[0:2], dtype = "uint8")
cv2.rectangle(mask, (15, 15), (130, 100), 255, -1)
cv2.imshow("Mask", mask)
masked = cv2.bitwise_and(image, image, mask = mask)
cv2.imshow("Applying the Mask", masked)
cv2.waitKey(0)
plot_histogram(image, "Histogram for Masked Image", mask = mask)
这是天空部分的三个通道的直方图
5.平滑化和模糊化
模糊化(blurring):可以用来去噪等.
average blur:采用函数cv2.blur(image, (3, 3)),第一个参数为需要操作的图像,第二个参数为卷积核,即要取平均的区域.
blurred = np.hstack([
cv2.blur(image, (3, 3)),
cv2.blur(image, (5, 5)),
cv2.blur(image, (7, 7))])
cv2.imshow("Averaged", blurred)
cv2.waitKey(0)
可以看出,随着卷积核的增大,图片越来越模糊了.
Gaussian blur:采用函数cv2.GaussianBlur(image, (3, 3), 0), 第二个参数为卷积核大小,第三个参数为高斯分布的标准差.高斯模糊化给每个卷积核中的值附加了一个权重,来计算需要替代的值.
blurred = np.hstack([
cv2.GaussianBlur(image, (3, 3), 0),
cv2.GaussianBlur(image, (5, 5), 0),
cv2.GaussianBlur(image, (7, 7), 0)])
cv2.imshow("Gaussian", blurred)
cv2.waitKey(0)
高斯模糊比均值模糊看起来更加的自然一些.
median blur:采用函数cv2.medianBlur(image, 3),第二个参数是卷积核的大小.该方法用来去除椒盐噪声,让图片失去细节.
bilateral blur:采用函数cv2.bilateralFilter(image, 5, 21, 21),第二参数是卷积核的大小;第三个参数为第一个高斯分布的标准差,用来确定空间的;第四个参数为第二个高斯分布的标准差,用于与(x,y)强度相近似的点确定像素点的强度。两者的结合既能够保存了图像的边缘,也能够去除噪声。
blurred = np.hstack([
cv2.bilateralFilter(image, 5, 21, 21),
cv2.bilateralFilter(image, 7, 31, 31),
cv2.bilateralFilter(image, 9, 41, 41)])
cv2.imshow("Bilateral", blurred)
6.门限操作(thresholding)
simple thresholding(简单的门限操作):让灰度图像变成二值化的图像。
利用函数(T, thresh) = cv2.threshold(blurred, 155, 255, cv2.THRESH_BINARY),第一个参数是需要二值化的图像;第二个参数是门限;第三个参数是超过门限之后赋予的新值;第四个参数是代表正常二值化还是需要反转二值化,cv2.THRESH_BINARY表示正常二值化,即小于门限则输出的为0,而cv2.THRESH_BINARY_INV表示反转的二值化,即小于门限则输出为1。返回的参数T为使用的门限,thresh为返回处理后的图像。
image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
#before thresholding, blurring the image by GaussianBlur
blurred = cv2.GaussianBlur(image, (5, 5), 0)
cv2.imshow("Image", image)
(T, thresh) = cv2.threshold(blurred, 155, 255, cv2.THRESH_BINARY)
cv2.imshow("Threshold Binary", thresh)
(T, threshInv) = cv2.threshold(blurred, 155, 255, cv2.THRESH_BINARY_INV)
cv2.imshow("Threshold Binary Inv", threshInv)
cv2.imshow("Coins", cv2.bitwise_and(image, image, mask =
threshInv))
右上角为正常二值化,左下角为反转二值化,右下角为采用反转二值化作为mask作用在原图上来画出硬币。
adaptive thresholding(自适应门限操作):自动挑选门限值让灰度图像变成二值化的图像,相比于简单的自己挑选门限而言,它的灵活性更加强;自适应的门限是根据像素点周围的几个值来来确定操作该像素的门限,所以在一张图处理中可以存在很多的门限值;可以很好的解决图像像素值差异太大,单个门限很难操作的问题。
使用函数,thresh = cv2.adaptiveThreshold(blurred, 255,cv2.ADAPTIVE_THRESH_MEAN_C,
cv2.THRESH_BINARY_INV, 11, 4)
第二个参数为:超过门限后赋予的新值
第三个参数为:采用的自适应的门限计算方式,cv2.ADAPTIVE_THRESH_MEAN_C代表取均值操作,cv2.ADAPTIVE_THRESH_GAUSSIAN_C代表高斯操作,与blur中说到的高斯操作类似。
第四个参数为:正常二值化,还是反转二值化,与simple threshold中的用法一样。
第五个参数为:卷积核的大小
第六个参数为:用来微调(fine_tune)计算出来的门限值,减去这个常数C得到新的门限值。
image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
#before adaptive thresholding, blurring by Gaussian
blurred = cv2.GaussianBlur(image, (5, 5), 0)
cv2.imshow("Image", image)
thresh = cv2.adaptiveThreshold(blurred, 255,
cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 11, 4)
cv2.imshow("Mean Thresh", thresh)
thresh = cv2.adaptiveThreshold(blurred, 255,
cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY_INV, 15, 3)
cv2.imshow("Gaussian Thresh", thresh)
在实际的操作中,可以多调节调节卷积核的大小和用来微调的那个常数C,有些时候改变一些会得到意想不到的结果。
otsu and riddler-calvard:这也是一个自动计算门限值的方法,他们的原理是假设图像的灰度直方图中有两个峰值,而生成的这个门限用来区分出这两个峰值。
采用的函数是:mahotas库中的函数,T = mahotas.thresholding.otsu(blurred)返回的值也是计算出的门限;还有另外一个方法T = mahotas.thresholding.rc(blurred),返回的值也是计算出的门限。
image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
blurred = cv2.GaussianBlur(image, (5, 5), 0)
cv2.imshow("Image", image)
T = mahotas.thresholding.otsu(blurred)
print("Otsu's threshold: {}".format(T))
thresh = image.copy()
thresh[thresh > T] = 255
thresh[thresh < T] = 0
thresh =cv2.bitwise_not(thresh)
cv2.imshow("Otsu", thresh)
T = mahotas.thresholding.rc(blurred)
print("Riddler-Calvard: {}".format(T))
thresh = image.copy()
thresh[thresh > T] = 255
thresh[thresh < T] = 0
thresh =cv2.bitwise_not(thresh)
cv2.imshow("Riddler - Calvard", thresh)
因为mahotas计算出来的都是门限值,所以需要自己操作一下图片的二值化,在示例中两个函数计算出来的门限都是137。
7.梯度和边缘检测(gradient and edge detection)
梯度计算:利用Laplacian和Sober算子来计算梯度。
Laplacian算子计算使用的函数:lap = cv2.Laplacian(image, cv2.CV_64F),第二个参数是计算后输出数据的类型,注意采用的是CV_64F浮点类型,而不是使用UINT8,是因为在opencv中uint8不存在负数,当计算到的梯度强度为负数时,它是自动取为零,这不是我们想看到的结果。利用浮点类型计算完之后,再取绝对值转化为uint8:lap = np.uint8(np.absolute(lap))。
sobel算子计算使用的函数:cv2.Sobel(image, cv2.CV_64F, 1, 0),第三个和第四个参数取值可以是:(1,0)和(0,1),其中(1,0)代表计算x轴上的梯度,即垂直梯度;(0,1)代表计算y轴上的梯度,即水平梯度。数据类型也和Laplacian算子一样。
image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
cv2.imshow("Image", image)
##需要把计算梯度的数据类型改为64F,因为uint8在cv中会截取,
##在numpy中会取模运算
lap = cv2.Laplacian(image, cv2.CV_64F)
lap = np.uint8(np.absolute(lap))
cv2.imshow("Laplacian", lap)
cv2.waitKey(0)
sobelX = cv2.Sobel(image, cv2.CV_64F, 1, 0)
sobleY = cv2.Sobel(image, cv2.CV_64F, 0, 1)
sobelX = np.uint8(np.absolute(sobelX))
sobelY = np.uint8(np.absolute(sobleY))
sobelCombined = cv2.bitwise_or(sobelX, sobelY)
cv2.imshow("soble X", sobelX)
cv2.imshow("sobel Y", sobelY)
cv2.imshow("sobel combined", sobelCombined)
在使用Sobel算子时,分别计算了x轴和y轴上的梯度之后,再利用cv2.bitwise_or()函数进行融合。
Canny edge detector:用来检测边缘的检测器,是一个多步骤的检测器,包括了,去噪、计算Sobel算子梯度、抑制边缘(极大值抑制)、后置的一个门限。
采用的函数:cv2.Canny(image, 30, 150),第二三个参数分别为两个门限值,小于门限值一则认为不是边缘,大于门限值二则认为是一个边缘,介于两者之中的值根据强度的某种联系来确定。
image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
blurred = cv2.GaussianBlur(image, (5, 5), 0)
cv2.imshow("Blurrde", blurred)
canny = cv2.Canny(image, 30, 150)
cv2.imshow("Canny", canny)
可以看出检测出了硬币的边缘。
8.contour(轮廓)
找出在图像中的轮廓,一般就是用来找出图像中的物体。
使用的函数:(_, cnts, _) = cv2.findContours(edged.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
第一个参数:需要寻找出轮廓的图像,这是一个已经检测出边缘的图像,最好进行复制使用,因为该函数会破坏原图
第二个参数:需要寻找的轮廓类型,cv2.RETR_EXTERNAL代表最外层的轮廓,cv2.RETR_LIST代表所有的轮廓
第三个参数: cv2.CHAIN_APPROX_SIMPLE一般采用来减小资源的使用。
返回的第一个值:处理完之后的图片,被损坏的很严重
返回的第二个值:返回检测到的轮廓
返回的第三个值:轮廓的层次
我们只需要用到返回检测到的轮廓即可。
检测到轮廓cnts之后,我们可以把它画在原图之上,采用的函数是cv2.drawContours(coins, cnts, -1, (0, 255, 0), 2),
第一个参数:原图
第二个参数:检测到的轮廓数组
第三个参数:想要画出轮廓对于的索引,若为-1,则画出所有的轮廓
第四个参数:画轮廓的颜色
第五个参数:画轮廓线的粗细
image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
blurred = cv2.GaussianBlur(image, (11, 11), 0)
cv2.imshow("image", image)
edged = cv2.Canny(blurred, 40, 275)
cv2.imshow("Edges", edged)
(_, cnts, _) = cv2.findContours(edged.copy(), cv2.RETR_EXTERNAL,
cv2.CHAIN_APPROX_SIMPLE)
print("I count {} coin in this image". format(len(cnts)))
coins = image.copy()
cv2.drawContours(coins, cnts, -1, (0, 0, 255), 2)
cv2.imshow("Coins", coins)
控制台输出了:I count 9 coin in this image
相比于之前采用的Candy边缘检测器而言,这个边缘检测出来的效果更加的好,是因为在用GaussianBlur时使用了更大的卷积核,丢失了硬币的更多细节,从而只剩下了外层的边缘。并且画出了这些硬币。
为了把每个硬币都切出来,可以遍历cnts这个列表,并且利用cv2.boundingRect(c)找出包含轮廓最小的矩形的左上角坐标以及长宽,从而可以利用切片找出硬币,接着利用cnts找出可以包围它的最小圆,找出对应大小的mask,得到最终的硬币图。
for (i, c) in enumerate(cnts):
(x, y, w, h) = cv2.boundingRect(c)
print("Coin #{}".format(i +1 ))
coin = image[y:y + h, x:x + w]
cv2.imshow("coin", coin)
mask = np.zeros(image.shape[0:2], dtype = "uint8")
((centerX, centerY), radius) = cv2.minEnclosingCircle(c)
cv2.circle(mask, (int(centerX), int(centerY)), int(radius),
255, -1)
mask = mask[y:y + h, x:x + w]
cv2.imshow("Masked Coin", cv2.bitwise_and(coin, coin, mask =
mask))
cv2.waitKey(0)
最终找出了所有的coin。
9 conclusion
以上就是在《Practical Python and OpenCV》的所有内容了,确实都是一些简单的入门操作,也是作为一个记录吧,方便之后对这些基本函数的一些查询和记忆的加深,之后再对case study进行补充。
ps:一个正躺在床上睡觉的舍友,看着我和另外一个舍友都在学习,竟然跑下来复习今天学习的功课了。。。。。为了舍友能早点睡,去睡了去睡了。。。。。。