首先导入所需要的库文件
import cv2 import numpy as np import matplotlib.pyplot as plt %matplotlib inline
# 设置随机种子,确保结果可复现 SEED = 42 def Set_Seed(): np.random.seed(42) cv2.setRNGSeed(42) Set_Seed()
def Img_Show(img): cv2.imshow("img", img) cv2.waitKey(0) cv2.destroyAllWindows() img = cv2.imread("./photo/img.png") Img_Show(img)
1. 图像元素访问,通道分离与合并
# 功能: 对图片随机添加噪声白点 # 参数:img:传入图像 # n:设置噪声白点的数量 def salt(img, n): for k in range(n): i = int(np.random.random() * img.shape[1]) j = int(np.random.random() * img.shape[0]) if img.ndim == 2: img[j,i] = 255 elif img.ndim == 3: img[j,i,0]= 255 img[j,i,1]= 255 img[j,i,2]= 255 return img
def Channel_Split_and_Merge(): img = cv2.imread("./photo/img.png") # shape:(495, 700, 3) print("img[200,100]:", img[200,100]) # 通道分离 b, g, r = cv2.split(img) cv2.imshow("Blue", r) cv2.imshow("Red", g) cv2.imshow("Green", b) # 随机增加噪声白点 saltImage = salt(img, 1000) cv2.imshow("saltImage",saltImage) # 通道合并 merged = cv2.merge([b,g,r]) cv2.imshow("merged", merged) print(merged.strides) cv2.waitKey(0) cv2.destroyAllWindows() Channel_Split_and_Merge()
img[200,100]: [84 16 49] (2100, 3, 1)
2. 直方图的计算与显示
help(cv2.calcHist)
Help on built-in function calcHist: calcHist(...) calcHist(images, channels, mask, histSize, ranges[, hist[, accumulate]]) -> hist . @overload
cv2.calcHist([image], [0], None, [256], [0.0,255.0])
其中:
第一个参数必须用方括号括起来。
第二个参数是用于计算直方图的通道, [0]这里使用灰度图计算直方图,所以就直接使用第一个通道。
第三个参数是Mask,这里没有使用,所以用None。
第四个参数是histSize,表示这个直方图分成多少份(即多少个直方柱)。
第五个参数是表示直方图中各个像素的值,[0.0, 256.0]表示直方图能表示像素值从0.0到256的像素。最后是两个可选参数,由于直方图作为函数结果返回了,所以第六个hist就没有意义了(待确定),最后一个accumulate是一个布尔值,用来表示直方图是否叠加。
- 利用opencv来绘制直方图
def calcAndDrawHist(image, color): hist= cv2.calcHist([image], [0], None, [256], [0.0,255.0]) minVal, maxVal, minLoc, maxLoc = cv2.minMaxLoc(hist) histImg = np.zeros([256,256,3], np.uint8) hpt = int(0.9* 256); for h in range(256): intensity = int(hist[h]*hpt/maxVal) cv2.line(histImg,(h,256), (h,256-intensity), color) return histImg
def Hist_Show_and_Cal(): img = cv2.imread("./photo/img.png") b, g, r = cv2.split(img) histImgB = calcAndDrawHist(b, [255, 0, 0]) cv2.imshow("histImgB", histImgB) cv2.waitKey(0) cv2.destroyAllWindows() Hist_Show_and_Cal()
- 利用matplotlib来绘制直方图
img = cv2.imread("./photo/img.png") HistSize = 256 histImgB = cv2.calcHist([img], [0], None, [HistSize], [0.0,255.0]) histImgG = cv2.calcHist([img], [1], None, [HistSize], [0.0,255.0]) histImgR = cv2.calcHist([img], [2], None, [HistSize], [0.0,255.0]) def Get_HistMax(hist): minVal, maxVal, minLoc, maxLoc = cv2.minMaxLoc(hist) return maxVal HistMax = max(Get_HistMax(histImgB), Get_HistMax(histImgG), Get_HistMax(histImgR)) x = np.arange(0, HistSize) plt.figure(figsize=[10, 5]) # data = np.column_stack((x, histImgB.flatten()/HistMax*HistSize)) # pos.shape: (256, 2) plt.plot(x, histImgB.flatten()/HistMax*HistSize, color='blue') plt.plot(x, histImgG.flatten()/HistMax*HistSize, color='green') plt.plot(x, histImgR.flatten()/HistMax*HistSize, color='red')
[<matplotlib.lines.Line2D at 0x190de575b88>]
3. 形态学处理
element_cv = cv2.getStructuringElement(cv2.MORPH_CROSS,(5,5)) element_np = np.uint8(np.zeros((5,5))) for i in range(len(element_np)): element_np[i][2] = 1 element_np[2][i] = 1 np.equal(element_cv, element_np)
array([[ True, True, True, True, True], [ True, True, True, True, True], [ True, True, True, True, True], [ True, True, True, True, True], [ True, True, True, True, True]])
- 腐蚀和膨胀
#原图像 cv2.imshow("Origin", img) #OpenCV定义的结构元素 kernel = cv2.getStructuringElement(cv2.MORPH_RECT,(3, 3)) # kernel: # array([[1, 1, 1], # [1, 1, 1], # [1, 1, 1]], dtype=uint8) #腐蚀图像 eroded = cv2.erode(img,kernel) #显示腐蚀后的图像 cv2.imshow("Eroded Image",eroded) #膨胀图像 dilated = cv2.dilate(img,kernel) #显示膨胀后的图像 cv2.imshow("Dilated Image",dilated) #NumPy定义的结构元素 NpKernel = np.uint8(np.ones((3,3))) Nperoded = cv2.erode(img,NpKernel) #显示腐蚀后的图像 cv2.imshow("Eroded by NumPy kernel",Nperoded); cv2.waitKey(0) cv2.destroyAllWindows()
# 说明两个方式处理是完全一样的 np.allclose(eroded, Nperoded) np.alltrue(eroded==Nperoded)
True
腐蚀和膨胀的处理很简单,只需设置好结构元素,然后分别调用cv2.erode(…)和cv2.dilate(…)函数即可,其中第一个参数是需要处理的图像,第二个是结构元素。返回处理好的图像。
- 开运算和闭运算
#定义结构元素 kernel = cv2.getStructuringElement(cv2.MORPH_RECT,(5, 5)) # kernel: # 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) #闭运算 closed = cv2.morphologyEx(img, cv2.MORPH_CLOSE, kernel) #显示腐蚀后的图像 cv2.imshow("Close",closed); #开运算 opened = cv2.morphologyEx(img, cv2.MORPH_OPEN, kernel) #显示腐蚀后的图像 cv2.imshow("Open", opened); cv2.waitKey(0) cv2.destroyAllWindows()
闭运算用来连接被误分为许多小块的对象,而开运算用于移除由图像噪音形成的斑点。因此,某些情况下可以连续运用这两种运算。如对一副二值图连续使用闭运算和开运算,将获得图像中的主要对象。同样,如果想消除图像中的噪声(即图像中的“小点”),也可以对图像先用开运算后用闭运算,不过这样也会消除一些破碎的对象。
- 用形态学运算进行边缘检测
形态学检测边缘的原理很简单,在膨胀时,图像中的物体会想周围“扩张”;腐蚀时,图像中的物体会“收缩”。比较这两幅图像,由于其变化的区域只发生在边缘。所以这时将两幅图像相减,得到的就是图像中物体的边缘。不过效果并不是太好,实际使用时请用Canny或Harris等算法。
image = cv2.imread("./photo/building.jfif",0); #构造一个3×3的结构元素 element = cv2.getStructuringElement(cv2.MORPH_RECT,(3, 3)) dilate = cv2.dilate(image, element) erode = cv2.erode(image, element) # cv2.imshow("dilate",dilate) # cv2.imshow("erode",erode) #将两幅图像相减获得边,第一个参数是膨胀后的图像,第二个参数是腐蚀后的图像 result_sub = cv2.absdiff(dilate,erode) # cv2.imshow("result_first",result) # 查看灰度阈值,发现大部分处于40-50以下 hist = cv2.calcHist([result_sub], [0], None, [HistSize], [0.0,255.0]) plt.plot(x, hist.flatten()) #上面得到的结果是灰度图,将其二值化以便更清楚的观察结果 # 50表示: 灰度值小于40置0, 大于40值最大值255 retval, result = cv2.threshold(result_sub, 50, 255, cv2.THRESH_BINARY) #反色,即对二值图每个像素取反 result = cv2.bitwise_not(result) # 或者直接使用取反色算子cv2.THRESH_BINARY_INV,则无需使用cv2.bitwise_not(result) # retval, result = cv2.threshold(result, 50, 255, cv2.THRESH_BINARY_INV) #显示图像 cv2.imshow("result",result); cv2.waitKey(0) cv2.destroyAllWindows()
处理结果,可以提取边缘信息
- 拐点检测
与边缘检测不同,拐角的检测的过程稍稍有些复杂。但原理相同,所不同的是先用十字形的结构元素膨胀像素,这种情况下只会在边缘处“扩张”,角点不发生变化。接着用菱形的结构元素腐蚀原图像,导致只有在拐角处才会“收缩”,而直线边缘都未发生变化。(我的理解是这里由于边缘线经历了先膨胀再收缩所以不变,而角点只是收缩所有会有变化)
第二步是用X形膨胀原图像,角点膨胀的比边要多。这样第二次用方块腐蚀时,角点恢复原状,而边要腐蚀的更多。所以当两幅图像相减时,只保留了拐角处。
image = cv2.imread("./photo/building.jfif", 0); # 构造5×5的结构元素,分别为十字形、菱形、方形和X型 cross = cv2.getStructuringElement(cv2.MORPH_CROSS,(5, 5)) # 构造菱形结构元素 diamond = cv2.getStructuringElement(cv2.MORPH_RECT,(5, 5)) diamond[0, 0] = 0 diamond[0, 1] = 0 diamond[1, 0] = 0 diamond[4, 4] = 0 diamond[4, 3] = 0 diamond[3, 4] = 0 diamond[4, 0] = 0 diamond[4, 1] = 0 diamond[3, 0] = 0 diamond[0, 3] = 0 diamond[0, 4] = 0 diamond[1, 4] = 0 # 构造方形和十字形结构 square = cv2.getStructuringElement(cv2.MORPH_RECT,(5, 5)) x = cv2.getStructuringElement(cv2.MORPH_CROSS,(5, 5)) #使用cross膨胀图像 result1 = cv2.dilate(image,cross) #使用菱形腐蚀图像 result1 = cv2.erode(result1, diamond) cv2.imshow("result1", result1) #使用X膨胀原图像 result2 = cv2.dilate(image, x) #使用方形腐蚀图像 result2 = cv2.erode(result2,square) cv2.imshow("result2", result2) #将两幅闭运算的图像相减获得角 result = cv2.absdiff(result2, result1) #使用阈值获得二值图 retval, result = cv2.threshold(result, 40, 255, cv2.THRESH_BINARY) #在原图上用半径为5的圆圈将点标出。 # for j in range(result.size): # y = j / result.shape[0] # x = j % result.shape[0] # if result[x, y] == 255: # cv2.circle(image, (y, x), 5, (255,0,0)) cv2.imshow("Result", result) cv2.waitKey(0) cv2.destroyAllWindows()
结果展示:
4. 初级滤波内容
当我们观察一张图片时,我们观察的是图像中有多少灰度级(或颜色)及其分布。根据灰度分布的不同来区分不同的图像。但还有其他方面可以对图像进行分析。我们可以观察图像中灰度的变化。某些图像中包含大量的强度不变的区域(如蓝天),而在其他图像中的灰度变化可能会非常快(如包含许多小物体的拥挤的图像)。因此,观察图像中这些变化的频率就构成了另一条分类图像的方法。这个观点称为频域。而通过观察图像灰度分布来分类图像称为空间域。
频域分析将图像分成从低频到高频的不同部分。低频对应图像强度变化小的区域,而高频是图像强度变化非常大的区域。目前已存在若干转换方法,如傅立叶变换或余弦变换,可以用来清晰的显示图像的频率内容。注意,由于图像是一个二维实体,所以其由水平频率(水平方向的变化)和竖直频率(竖直方向的变化)共同组成。
在频率分析领域的框架中,滤波器是一个用来增强图像中某个波段或频率并阻塞(或降低)其他频率波段的操作。低通滤波器是消除图像中高频部分,但保留低频部分。高通滤波器消除低频部分。
- 用低通滤波来平滑图像
低通滤波器的目标是降低图像的变化率。如将每个像素替换为该像素周围像素的均值。这样就可以平滑并替代那些强度变化明显的区域。
高斯模糊可以在某些情况下,需要对一个像素的周围的像素给予更多的重视。因此,可通过分配权重来重新计算这些周围点的值。这可通过高斯函数(钟形函数,即喇叭形数)的权重方案来解决。
img = cv2.imread("./photo/img.png") cv2.imshow("Origin", img) # 普通的模糊方式 # 以下两种方式是等价的,其中(5,5)是控制模糊程度 result = cv2.blur(img, (5,5)) result1 = cv2.boxFilter(img, -1, (5,5)) cv2.imshow("Blur", result) cv2.imshow("BoxFilter", result1) # 高斯模糊 gaussianResult = cv2.GaussianBlur(img,(5,5),1.5) cv2.imshow("gaussianResult", gaussianResult) cv2.waitKey(0) cv2.destroyAllWindows()
低通滤波与高斯滤波的不同之处在于:低通滤波中,滤波器中每个像素的权重是相同的,即滤波器是线性的。而高斯滤波器中像素的权重与其距中心像素的距离成比例。
- 使用中值滤波消除噪点
# 功能: 对图片随机添加噪声白点 # 参数:img:传入图像 # n:设置噪声白点的数量 def salt(img, n): for k in range(n): i = int(np.random.random() * img.shape[1]) j = int(np.random.random() * img.shape[0]) if img.ndim == 2: img[j,i] = 255 elif img.ndim == 3: img[j,i,0]= 255 img[j,i,1]= 255 img[j,i,2]= 255 return img img = cv2.imread("./photo/img.png") cv2.imshow("img", img) # 产生带噪声的图片 saltimg = salt(img, 1000) cv2.imshow("saltimg", saltimg) # 过滤噪声 medianimg = cv2.medianBlur(saltimg, 3) cv2.imshow("medianimg", medianimg) cv2.waitKey(0) cv2.destroyAllWindows()
由于中值滤波不会处理最大和最小值,所以就不会受到噪声的影响。相反,如果直接采用blur进行均值滤波,则不会区分这些噪声点,滤波后的图像会受到噪声的影响。
中值滤波器在处理边缘也有优势。但中值滤波器会清除掉某些区域的纹理(如背景中的树)。
blurimg = cv2.blur(saltimg, (5,5)) cv2.imshow("blurimg", blurimg) cv2.waitKey(0) cv2.destroyAllWindows()
使用blur来消除噪声的效果虽然也消除的噪声,但是比较模糊
5. Sobel算子
dst = cv2.Sobel(src, ddepth, dx, dy[, dst[, ksize[, scale[, delta[, borderType]]]]])
Sobel算子依然是一种过滤器,只是其是带有方向的:函数返回其处理结果。前四个是必须的参数:
第一个参数是需要处理的图像;第二个参数是图像的深度,-1表示采用的是与原图像相同的深度。目标图像的深度必须大于等于原图像的深度;dx和dy表示的是求导的阶数,0表示这个方向上没有求导,一般为0、1、2。
其后是可选的参数:dst是选择图片的位数。ksize是Sobel算子的大小,必须为1、3、5、7。scale是缩放导数的比例常数,默认情况下没有伸缩系数;delta是一个可选的增量,将会加到最终的dst中,同样,默认情况下没有额外的值加到dst中;borderType是判断图像边界的模式。这个参数默认值为cv2.BORDER_DEFAULT。
img = cv2.imread("./photo/img.png", 0) # 使用16位有符号的数据类型,即cv2.CV_16S;先后对x方向与y方向进行求导 x = cv2.Sobel(img,cv2.CV_16S,1,0) y = cv2.Sobel(img,cv2.CV_16S,0,1) # 用convertScaleAbs()函数将其转回原来的uint8形式。否则将无法显示图像,而只是一副灰色的窗口 absX = cv2.convertScaleAbs(x) absY = cv2.convertScaleAbs(y) cv2.imshow("absX", absX) cv2.imshow("absY", absY) # 由于Sobel算子是在两个方向计算的,还需组合起来 # dst = gamma+(src1*aplha + src2*beta) -> absX*0.5 + absY*0.5 dst = cv2.addWeighted(absX, 0.5, absY, 0.5, 0) cv2.imshow("Result", dst) cv2.waitKey(0) cv2.destroyAllWindows()
结果如下所示:
6. Laplacian算子
图像中的边缘区域,像素值会发生“跳跃”,对这些像素求导,在其一阶导数在边缘位置为极值,这就是Sobel算子使用的原理——极值处就是边缘。
如果对像素值求二阶导数,会发现边缘处的导数值为0。
Laplace函数实现的方法是先用Sobel 算子计算二阶x和y导数,再求和,公式如下:
Laplacian(...) Laplacian(src, ddepth[, dst[, ksize[, scale[, delta[, borderType]]]]]) -> dst
前两个是必须的参数:第一个参数是需要处理的图像;第二个参数是图像的深度,-1表示采用的是与原图像相同的深度。目标图像的深度必须大于等于原图像的深度;
其后是可选的参数:
dst不用解释了;
ksize是算子的大小,必须为1、3、5、7。默认为1。
scale是缩放导数的比例常数,默认情况下没有伸缩系数;delta是一个可选的增量,将会加到最终的dst中,同样,默认情况下没有额外的值加到dst中;
borderType是判断图像边界的模式。这个参数默认值为cv2.BORDER_DEFAULT。
- 不滤波提取边缘信息
img = cv2.imread("./photo/img.png", 0) # 为了让结果更清晰,这里的ksize设为3 gray_lap = cv2.Laplacian(img,cv2.CV_16S,ksize = 3) # 用convertScaleAbs()函数将其转回原来的uint8形式。否则将无法显示图像,而只是一副灰色的窗口 dst = cv2.convertScaleAbs(gray_lap) # cv2.imshow('gray_lap',gray_lap) cv2.imshow('laplacian',dst) cv2.waitKey(0) cv2.destroyAllWindows()
可以发现图片还是有比较多的噪声,现在通过低通去噪模糊再进行边缘处理,看看效果
- 滤波提取边缘信息
img = cv2.imread("./photo/img.png", 0) # 滤波处理 blurimg = cv2.blur(img,(3,3)) gaussianimg = cv2.GaussianBlur(img, (3,3), 1.5) cv2.imshow('blurimg',blurimg) cv2.imshow('gaussianimg',gaussianimg) # 为了让结果更清晰,这里的ksize设为3 gray_lap = cv2.Laplacian(img,cv2.CV_16S,ksize = 3) gray_lap_blur = cv2.Laplacian(blurimg,cv2.CV_16S,ksize = 3) gray_lap_gaussian = cv2.Laplacian(gaussianimg,cv2.CV_16S,ksize = 3) # 用convertScaleAbs()函数将其转回原来的uint8形式。否则将无法显示图像,而只是一副灰色的窗口 gray_lap = cv2.convertScaleAbs(gray_lap) gray_lap_blur = cv2.convertScaleAbs(gray_lap_blur) gray_lap_gaussian = cv2.convertScaleAbs(gray_lap_gaussian) cv2.imshow('gray_lap',gray_lap) cv2.imshow('gray_lap_blur',gray_lap_blur) cv2.imshow('gray_lap_gaussian',gray_lap_gaussian) cv2.waitKey(0) cv2.destroyAllWindows()
7.Canny边缘检测
OpenCV-Python中Canny函数的原型为:
edge = cv2.Canny(image, threshold1, threshold2[, edges[, apertureSize[, L2gradient ]]])
必要参数:第一个参数是需要处理的原图像,该图像必须为单通道的灰度图;第二个参数是阈值1;第三个参数是阈值2。
其中较大的阈值2用于检测图像中明显的边缘,但一般情况下检测的效果不会那么完美,边缘检测出来是断断续续的。所以这时候用较小的第一个阈值用于将这些间断的边缘连接起来。
可选参数中apertureSize就是Sobel算子的大小。而L2gradient参数是一个布尔值,如果为真,则使用更精确的L2范数进行计算(即两个方向的倒数的平方和再开放),否则使用L1范数(直接将两个方向导数的绝对值相加)。
- 静态设置阈值
# 由于Canny只能处理灰度图,所以将读取的图像转成灰度图 img = cv2.imread("./photo/img.png", 0) # 用高斯平滑处理原图像降噪后再进行边缘检测 img = cv2.GaussianBlur(img,(3,3),0) # Canny函数的使用很简单,只需指定最大和最小阈值即可 canny = cv2.Canny(img, 50, 150) cv2.imshow('Canny', canny) cv2.waitKey(0) cv2.destroyAllWindows()
- 动态设置阈值
def CannyThreshold(lowThreshold): detected_edges = cv2.GaussianBlur(gray,(3,3),0) detected_edges = cv2.Canny(detected_edges,lowThreshold,lowThreshold*ratio,apertureSize = kernel_size) dst = cv2.bitwise_and(img,img,mask = detected_edges) # just add some colours to edges from original image. cv2.imshow('canny demo',dst) lowThreshold = 0 max_lowThreshold = 100 ratio = 3 kernel_size = 3 img = cv2.imread('./photo/img.png') gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY) #创建拉伸条 cv2.namedWindow('canny demo') cv2.createTrackbar('Min threshold','canny demo',lowThreshold, max_lowThreshold, CannyThreshold) CannyThreshold(0) # initialization if cv2.waitKey(0) == 27: cv2.destroyAllWindows()
8. 霍夫变换检测直线
Hough变换是经典的检测直线的算法。其最初用来检测图像中的直线,同时也可以将其扩展,以用来检测图像中简单的结构。
OpenCV提供了两种用于直线检测的Hough变换形式。其中基本的版本是cv2.HoughLines。其输入一幅含有点集的二值图(由非0像素表示),其中一些点互相联系组成直线。通常这是通过如Canny算子获得的一幅边缘图像。cv2.HoughLines函数输出的是[float, float]形式的ndarray,其中每个值表示检测到的线(ρ , θ)中浮点点值的参数。
下面的例子首先使用Canny算子获得图像边缘,然后使用Hough变换检测直线。其中HoughLines函数的参数3和4对应直线搜索的步长。在本例中,函数将通过步长为1的半径和步长为π/180的角来搜索所有可能的直线。最后一个参数是经过某一点曲线的数量的阈值,超过这个阈值,就表示这个交点所代表的参数对(rho, theta)在原图像中为一条直线。
- 标准霍夫线变换
Help on built-in function HoughLines: HoughLines(...) HoughLines(image, rho, theta, threshold[, lines[, srn[, stn[, min_theta[, max_theta]]]]]) -> lines
img = cv2.imread("./photo/road.jpg", 0) img = cv2.GaussianBlur(img,(3,3),0) edges = cv2.Canny(img, 100, 150, apertureSize = 3) # 标准霍夫线变换 lines = cv2.HoughLines(edges,1,np.pi/180,118) # dst: 边缘检测的输出图像. 它应该是个灰度图 (但事实上是个二值化图) # rho : 参数极径 r 以像素值为单位的分辨率. 我们使用 1 像素. # theta: 参数极角 \theta 以弧度为单位的分辨率. 我们使用 1度 (即CV_PI/180) # threshold: 要”检测” 一条直线所需最少的的曲线交点(里对最后一个参数使用了经验值118) # 返回:lines: 储存着检测到的直线的参数对 (r,\theta) 的容器 # (a,1,b) -> (a, b) lines = lines.reshape(lines.shape[0],lines.shape[-1]) # 通过画出检测到的直线来显示结果 for line in lines: rho = line[0] theta = line[1] a = np.cos(theta) b = np.sin(theta) point1_x = np.round(a*rho + 1000*(-b)).astype(int) point1_y = np.round(b*rho + 1000*(a)).astype(int) point2_x = np.round(a*rho - 1000*(-b)).astype(int) point2_y = np.round(b*rho - 1000*(a)).astype(int) point1 = (point1_x, point1_y) point2 = (point2_x, point2_y) # print(point1, point2) cv2.line(img, point1, point2, (255, 0, 0)) cv2.imshow('Canny', edges ) cv2.imshow('Result', img) cv2.waitKey(0) cv2.destroyAllWindows()
- 统计概率霍夫线变换
HoughLinesP(...) HoughLinesP(image, rho, theta, threshold[, lines[, minLineLength[, maxLineGap]]]) -> lines
img = cv2.imread("./photo/road.jpg", 0) img = cv2.GaussianBlur(img,(3,3),0) edges = cv2.Canny(img, 100, 150, apertureSize = 3) # 标准霍夫线变换 lines = cv2.HoughLinesP(edges, 1, np.pi/180, 80, 100, 10) # image: 边缘检测的输出图像. 它应该是个灰度图 (但事实上是个二值化图) # rho : 参数极径 r 以像素值为单位的分辨率. 我们使用 1 像素. # theta: 参数极角 \theta 以弧度为单位的分辨率. 我们使用 1度 (即CV_PI/180) # threshold: 要”检测” 一条直线所需最少的的曲线交点(里对最后一个参数使用了经验值118) # minLinLength: 能组成一条直线的最少点的数量. 点数量不足的直线将被抛弃. # maxLineGap: 能被认为在一条直线上的亮点的最大距离. # 返回:lines: 储存着检测到的直线的参数对 (x_{start}, y_{start}, x_{end}, y_{end}) 的容器 # (a,1,b) -> (a, b) lines = lines.reshape(lines.shape[0],lines.shape[-1]) # 通过画出检测到的直线来显示结果 for line in lines: point1 = (line[0], line[1]) # x_{start}, y_{start} point2 = (line[2], line[3]) # x_{end}, y_{end} cv2.line(img, point1, point2, (0,0,255), 3) # 这里的3控制线的粗细 cv2.imshow('Canny', edges ) cv2.imshow('Result', img) cv2.waitKey(0) cv2.destroyAllWindows()
9. 直方图均衡化
在某些情况下,一副图像中大部分像素的强度都集中在某一区域,而质量较高的图像中,像素的强度应该均衡的分布。为此,可将表示像素强度的直方图进行拉伸,将其平坦化。如下:
def HistDisr_Show(image): HistSize = 256 x = np.arange(0, HistSize) histImg = cv2.calcHist([image], [0], None, [HistSize], [0.0,255.0]) plt.figure(figsize=[10, 5]) plt.plot(x, histImg.flatten(), color='blue') # 将坐标轴隐藏 fig = plt.gca() fig.axes.get_xaxis().set_visible(False) fig.axes.get_yaxis().set_visible(False) # return histImg
- 使用查找表来拉伸直方图
观察上图中原始图像的直方图,很容易发现大部分强度值范围都没有用到。因此先检测图像非0的最低(imin)强度值和最高(imax)强度值。将最低值imin设为0,最高值imax设为255。中间的按255.0*(i-imin)/(imax-imin)+0.5)的形式设置。
img = cv2.imread("./photo/outside.jpg", 0) histImgB= cv2.calcHist([img], #计算图像的直方图 [0], #使用的通道 None, #没有使用mask [256], #it is a 1D histogram [0.0,255.0]) minBinNo, maxBinNo = 0, 255 lut = np.zeros(256, dtype = img.dtype )#创建空的查找表 #计算从左起第一个不为0的直方图柱的位置 for binNo, binValue in enumerate(histImgB): if binValue != 0: minBinNo = binNo break #计算从右起第一个不为0的直方图柱的位置 for binNo, binValue in enumerate(reversed(histImgB)): if binValue != 0: maxBinNo = 255-binNo break print(minBinNo, maxBinNo) #生成查找表,方法来自参考文献1第四章第2节 for i,v in enumerate(lut): # print(i) if i < minBinNo: lut[i] = 0 elif i > maxBinNo: lut[i] = 255 else: lut[i] = int(255.0*(i-minBinNo)/(maxBinNo-minBinNo)+0.5) #计算: cv2.LUT函数只有两个参数,分别为输入图像和查找表,其返回处理的结果 result = cv2.LUT(img, lut) # HistDisr_Show(result) cv2.imshow("Orgin", img) cv2.imshow("Result", result) cv2.waitKey(0) cv2.destroyAllWindows()
86 246
可以看出,直方图结果如下,可以看到原来占的区域很小的直方图尖峰被移动了
HistDisr_Show(img)
HistDisr_Show(result)
- 直方图均衡化
有时图像的视觉上的缺陷并不在强度值集中在很窄的范围内。而是某些强度值的使用频率很大。比如第一幅图中,灰度图中间值的占了很大的比例。
在完美均衡的直方图中,每个柱的值都应该相等。即50%的像素值应该小于128,25%的像素值应该小于64。总结出的经验可定义为:在标准的直方图中p%的像素拥有的强度值一定小于或等于255×p%。将该规律用于均衡直方图中:强度i的灰度值应该在对应的像素强度值低于i的百分比的强度中。其实,按我的理解来说们这也就是一个比率的问题。
lut
array([ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 3, 5, 6, 8, 10, 11, 13, 14, 16, 18, 19, 21, 22, 24, 26, 27, 29, 30, 32, 33, 35, 37, 38, 40, 41, 43, 45, 46, 48, 49, 51, 53, 54, 56, 57, 59, 61, 62, 64, 65, 67, 69, 70, 72, 73, 75, 77, 78, 80, 81, 83, 84, 86, 88, 89, 91, 92, 94, 96, 97, 99, 100, 102, 104, 105, 107, 108, 110, 112, 113, 115, 116, 118, 120, 121, 123, 124, 126, 128, 129, 131, 132, 134, 135, 137, 139, 140, 142, 143, 145, 147, 148, 150, 151, 153, 155, 156, 158, 159, 161, 163, 164, 166, 167, 169, 171, 172, 174, 175, 177, 179, 180, 182, 183, 185, 186, 188, 190, 191, 193, 194, 196, 198, 199, 201, 202, 204, 206, 207, 209, 210, 212, 214, 215, 217, 218, 220, 222, 223, 225, 226, 228, 230, 231, 233, 234, 236, 237, 239, 241, 242, 244, 245, 247, 249, 250, 252, 253, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], dtype=uint8)
p[i]即直方图累积值,这是包含小于给点强度值的像素的直方图,以代替包含指定强度值像素的数目。比如第一幅图像的累计直方图如下图中的蓝线,而完美均衡的直方图,其累积直方图应为一条斜线,如图中均衡化之后的红线。
更专业一点,这种累积直方图应称为累积分布(cumulative distribition)。在NumPy中有一个专门的函数来计算。
通过上面的介绍,应该可以明白,直方图均衡化就是对图像使用一种特殊的查询表。在第三个例子中可以看到使用查询表来获得直方图均衡化的效果。通常来说,直方图均衡化大大增加了图像的表象。但根据图像可视内容的不同,不同图像的直方图均衡化产生的效果不尽相同.
img = cv2.imread("./photo/outside.jpg", 0) # 用OpenCV实现直方图均衡化很简单,只需调用一个函数即可 result = cv2.equalizeHist(img) cv2.imshow("Orgin Image", img) cv2.imshow("Opencv Result", result) cv2.waitKey(0) cv2.destroyAllWindows()
查看使用用OpenCV实现直方图均衡化函数的直方图分布,感觉图像好像锐化得比较严重。
HistDisr_Show(result)
- 全图像直方图均衡化处理
img = cv2.imread("./photo/road.jpg") # 通道分离 b, g, r = cv2.split(img) # 用OpenCV实现直方图均衡化很简单,只需调用一个函数即可 imgB = cv2.equalizeHist(b) imgG = cv2.equalizeHist(g) imgR = cv2.equalizeHist(r) merged = cv2.merge([imgB, imgG, imgR]) cv2.imshow("img", img) cv2.imshow("merge", merged) cv2.waitKey(0) cv2.destroyAllWindows()
10. 轮廓检测
OpenCV-Python接口中使用cv2.findContours()函数来查找检测物体的轮廓。
cv2.findContours(image, mode, method[, contours[, hierarchy[, offset ]]])
参数:
第一个参数是寻找轮廓的图像;
第二个参数表示轮廓的检索模式,有四种(本文介绍的都是新的cv2接口):
cv2.RETR_EXTERNAL表示只检测外轮廓
cv2.RETR_LIST检测的轮廓不建立等级关系
cv2.RETR_CCOMP建立两个等级的轮廓,上面的一层为外边界,里面的一层为内孔的边界信息。如果内孔内还有一个连通物体,这个物体的边界也在顶层。
cv2.RETR_TREE建立一个等级树结构的轮廓。
第三个参数method为轮廓的近似办法
cv2.CHAIN_APPROX_NONE存储所有的轮廓点,相邻的两个点的像素位置差不超过1,即max(abs(x1-x2),abs(y2-y1))==1
cv2.CHAIN_APPROX_SIMPLE压缩水平方向,垂直方向,对角线方向的元素,只保留该方向的终点坐标,例如一个矩形轮廓只需4个点来保存轮廓信息
cv2.CHAIN_APPROX_TC89_L1,CV_CHAIN_APPROX_TC89_KCOS使用teh-Chinl chain 近似算法
返回值
cv2.findContours()函数返回两个值,一个是轮廓本身,还有一个是每条轮廓对应的属性。ps:opencv2返回两个值:contours:hierarchy。注:opencv3会返回三个值,分别是img, countours, hierarchy
img = cv2.imread("./photo/conter.jpg") # 转成灰度图 gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY) # 二值图 ret, binary = cv2.threshold(gray,127,255,cv2.THRESH_BINARY) # cv2.imshow("binary", binary) # 轮廓检测 orginimg, contours, hierarchy = cv2.findContours(binary,cv2.RETR_TREE,cv2.CHAIN_APPROX_SIMPLE) # cv2.imshow("binary_after", binary) # cv2.findContours()函数首先返回一个list,list中每个元素都是图像中的一个轮廓 # hierarchy本身包含两个ndarray,每个ndarray对应一个轮廓,每个轮廓有四个属性。 # orginimg是返回了你所处理的图像 # 分别用两种颜色绘制两个轮廓 cv2.drawContours(img,contours,0,(255,0,0),2) cv2.drawContours(img,contours,1,(0,255,0),2) # 第一个参数是指明在哪幅图像上绘制轮廓; # 第二个参数是轮廓本身,在Python中是一个list。 # 第三个参数指定绘制轮廓list中的哪条轮廓,如果是-1,则绘制其中的所有轮廓。后面的参数很简单。其中thickness表明轮廓线的宽度,如果是-1(cv2.FILLED),则为填充模式。绘制参数将在以后独立详细介绍。 cv2.imshow("img", img) # cv2.imshow("orgonimg", orginimg) cv2.waitKey(0) cv2.destroyAllWindows()
拓展:若出现ValueError: too many values to unpack 类错误,多为输入或者输出参数数量不一致导致。
有关cv2.findContours函数,cv2.drawContours函数的的详细介绍见:https://blog.csdn.net/sunny2038/article/details/12889059
- 函数封装
def DrawContour(img): # img = cv2.imread("./photo/conter.jpg") # 转成灰度图 gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY) # 二值图 ret, binary = cv2.threshold(gray,127,255,cv2.THRESH_BINARY) # 根据二值图返回轮廓列表 # cv2.RETR_TREE:建立一个等级树结构的轮廓 # cv2.CHAIN_APPROX_SIMPLE:压缩水平方向,垂直方向,对角线方向的元素,只保留该方向的终点坐标 _, contours, _ = cv2.findContours(binary,cv2.RETR_TREE,cv2.CHAIN_APPROX_SIMPLE) # 绘制轮廓(参数-1表示绘制全部轮廓) cv2.drawContours(img,contours,-1,(255,0,0),2) # 轮廓已绘制在原图上,显示出来 cv2.imshow("img", img) cv2.waitKey(0) cv2.destroyAllWindows()
img = cv2.imread("./photo/building.jpg") DrawContour(img)
参考资料专栏:
OpenCV
OpenCV入门指南
拓展资料:
1. python——opencv入门
2. Python+OpenCV图像处理(一篇全)
Github参考资料:
ImageProcessing