OpenCV 入门02
图像的基本操作:
访问像素值并修改它们 - 访问图像属性 - 设置感兴趣区域(ROI) - 分割和合并图像
本节中的几乎所有操作都主要与Numpy相关,而不是与OpenCV相关。要使用OpenCV编写更好的优化代码,需要Numpy的丰富知识
访问和修改像素值
import numpy as np import cv2 as cv if __name__ == '__main__': img = cv.imread('images/1.jpg') # img 的数据是bgr 的 # print(type(img)) # numpy.ndarray 类型 # print(img.shape) # (1080, 1920, 3) # 1 访问(100,100) 位置的像素颜色值 print(img[100, 100]) # 2 访问(100,100) 位置的像素颜色值 中的 b的值 print(img[100, 100, 0]) # 3 修改(100,100)-> (200,200) 区域为红色 img[100:200, 100:200] = [0, 0, 255] cv.imshow('hh', img) cv.waitKey(0) cv.destroyAllWindows()
访问图像属性
图像属性包括行数,列数和通道数,图像数据类型,像素数等
import numpy as np import cv2 as cv if __name__ == '__main__': img = cv.imread('images/1.jpg') # img 的数据是bgr 的 # 1 图像的 shape print(img.shape) # (1080, 1920, 3) 如果图像是灰度的,则返回的元组仅包含行数和列数,因此这是检查加载的图像是灰度还是彩色的好方法 # 2 像素总数 print(img.size) # 1080*1920 # 3 图形数据类型 print(img.dtype) # uint8 cv.imshow('hh', img) cv.waitKey(0) cv.destroyAllWindows()
拆分和合并图像通道
import numpy as np import cv2 as cv if __name__ == '__main__': img = cv.imread('images/1.jpg') # img 的数据是bgr 的 # 1 split to channel b, g, r = cv.split(img) # print(b.shape,g.shape,r.shape) # (1080, 1920) (1080, 1920) (1080, 1920) # 2 merge channel img2 = cv.merge((b, g, r)) # print(img2.shape) (1080, 1920,3) # ================================ # 使用Numpy 索引代替 split,因为split耗时更长 b = img[:, :, 0] g = img[:, :, 1] r = img[:, :, 2] # print(b.shape,g.shape,r.shape) # (1080, 1920) (1080, 1920) (1080, 1920) cv.imshow('hh', img) cv.waitKey(0) cv.destroyAllWindows()
为图像设置边框(填充)
import numpy as np import cv2 as cv from matplotlib import pyplot as plt if __name__ == '__main__': # 如果要在图像周围创建边框(如相框),则可以使用cv.copyMakeBorder()。但是它在卷积运算,零填充等方面有更多应用 img = cv.imread('images/1.jpg') img = img[...,::-1] # 将OpenCV 的bgr 变为rgb replicate = cv.copyMakeBorder(img, 200, 200, 200, 200, cv.BORDER_REPLICATE) reflect = cv.copyMakeBorder(img, 200, 200, 200, 200, cv.BORDER_REFLECT) reflect101 = cv.copyMakeBorder(img, 200, 200, 200, 200, cv.BORDER_REFLECT_101) wrap = cv.copyMakeBorder(img, 200, 200, 200, 200, cv.BORDER_WRAP) constant = cv.copyMakeBorder(img, 200, 200, 200, 200, cv.BORDER_CONSTANT, value=(255,0,0)) plt.subplot(231), plt.imshow(img, 'gray'), plt.title('ORIGINAL') plt.subplot(232), plt.imshow(replicate, 'gray'), plt.title('REPLICATE') plt.subplot(233), plt.imshow(reflect, 'gray'), plt.title('REFLECT') plt.subplot(234), plt.imshow(reflect101, 'gray'), plt.title('REFLECT_101') plt.subplot(235), plt.imshow(wrap, 'gray'), plt.title('WRAP') plt.subplot(236), plt.imshow(constant, 'gray'), plt.title('CONSTANT') plt.show()
图像上的算术运算
图像的几种算术运算,例如加法,减法,按位运算 cv.add,cv.addWeighted
图像加法
默认使用cv.add ,它是饱和运算,当数值大于饱和值的时候,就是饱和值了。
import numpy as np import cv2 as cv from matplotlib import pyplot as plt if __name__ == '__main__': # 通过OpenCV函数cv.add()或仅通过numpy操作res = img1 + img2添加两个图像。两个图像应具有相同的深度和类型,或者第二个图像可以只是一个标量值 x = np.array([ [ [1, 2, 3], [1, 2, 3], ], [ [1, 2, 3], [1, 2, 3], ]],dtype="uint8") y = np.array([ [ [1, 2, 3], [1, 2, 3], ], [ [1, 2, 3], [1, 2, 3], ]],dtype="uint8") print(x, y) print("#================================") print(cv.add(x, y)) print("#================================") print(x + y) # Numpy 加法 # 注意 OpenCV加法和Numpy加法之间有区别。OpenCV加法是饱和运算,而Numpy加法是模运算 z = np.array([ [ [255, 255, 255], [255, 255, 255], ], [ [255, 255, 255], [255, 255, 255], ],],dtype="uint8") print("#=============cv 是饱和运算===================") print(cv.add(x,z)) # cv 是饱和运算 一般使用它!!! print("#================================") print(x+z) # Numpy 的模运算
图像融合
import numpy as np import cv2 as cv from matplotlib import pyplot as plt if __name__ == '__main__': img1 = cv.imread('images/0.jpg') # (1080, 1920, 3) img2 = cv.imread('images/2.jpg') # (1080, 1920, 3) # 第一幅图像的权重为0.7,第二幅图像的权重为0.3。 dst = cv.addWeighted(img1, 0.7, img2, 0.3, 0) # 注意 img1 和 img2 的shape 要一致 cv.imshow('dst', dst) cv.waitKey(0) cv.destroyAllWindows()
按位运算
import cv2 as cv if __name__ == '__main__': img1 = cv.imread("images/1.jpg") img2 = cv.imread("images/logo.png") # 把logo放在左上角,所以我创建了ROI (region of interest) 感兴趣的区域 rows, cols, channels = img2.shape roi = img1[0:rows, 0:cols] # 现在创建logo的掩码,并同时创建其相反掩码 img2gray = cv.cvtColor(img2, cv.COLOR_BGR2GRAY) cv.imshow("logogray",img2gray) _, mask = cv.threshold(img2gray, 100, 255, cv.THRESH_BINARY) # 二值化函数 黑底图片用THRESH_BINARY, 白底用THRESH_BINARY_INV mask_inv = cv.bitwise_not(mask) cv.imshow("mask",mask) # 从logo图像中提取logo区域 img2_fg = cv.bitwise_and(img2, img2, mask=mask) # fg:foreground 前景 # 现在将ROI中的logo的区域涂黑 img1_bg = cv.bitwise_and(roi, roi, mask=mask_inv) # 位运算& # bg:background 背景 # 将logo放入ROI并修改主图像 dst = cv.add(img1_bg, img2_fg) img1[0:rows, 0:cols] = dst cv.imshow('res', img1) cv.waitKey(0) cv.destroyAllWindows()
logo 原图:
练习题:
使用cv.addWeighted
函数在文件夹中创建图像的幻灯片放映,并在图像之间进行平滑过渡
import cv2 as cv if __name__ == '__main__': # 加载文件夹的图片 l = [] for i in range(6): l.append(cv.imread(f'images/{i}.jpg')) cv.namedWindow('ppt', True) dst = cv.addWeighted(l[0], 1, l[1], 0, 0) cv.imshow('ppt', dst) for idx in range(len(l) - 1): a = 1 img1 = l[idx] img2 = l[idx + 1] cv.waitKey(0) while a >= 0: dst = cv.addWeighted(img1, a, img2, 1 - a, 0) cv.imshow('ppt', dst) cv.waitKey(100) a -= 0.1 cv.destroyAllWindows()
效果:
注:点击任意键切换图片
性能衡量和提升技术
在图像处理中,由于每秒要处理大量操作,因此必须使代码不仅提供正确的解决方案,而且还必须以最快的方式提供,
因此我们要了解:
1,衡量代码的性能
2,一些提高代码性能的技巧
cv.getTickCount,cv.getTickFrequency
衡量性能:
import cv2 as cv # OpenCv 获取程序执行时间 # img1 = cv.imread('images/1.jpg') # e1 = cv.getTickCount() # for i in range(5, 49, 2): # img1 = cv.medianBlur(img1, i) # e2 = cv.getTickCount() # t = (e2 - e1) / cv.getTickFrequency() # print(t) # 3.8622963 # time 库获取执行时间 import time img1 = cv.imread('images/1.jpg') t1 = time.time() for i in range(5, 49, 2): img1 = cv.medianBlur(img1, i) e2 = cv.getTickCount() print(time.time() - t1) # 3.5017619132995605
OpenCV 默认优化:
import cv2 as cv # 许多 OpenCV 函数都是使用 SSE2、 AVX 等进行优化的 # 检查是否启用了优化 print(cv.useOptimized()) cv.setUseOptimized(False) print(cv.useOptimized())
性能优化的措施:
- 尽量避免在Python中使用循环,尤其是双/三重循环等。它们本来就很慢。
- 由于Numpy和OpenCV已针对向量运算进行了优化,因此将算法/代码向量化到最大程度。
- 利用缓存一致性。
- 除非需要,否则切勿创建数组的副本。尝试改用视图。数组复制是一项昂贵的操作。
即使执行了所有这些操作后,如果你的代码仍然很慢,或者不可避免地需要使用大循环,请使用Cython等其他库来使其更快。
OpenCV 中的图像处理
HSV:
HSV H 色调 S 饱和度 V 明度,根据颜色的直观特性由A. R. Smith在1978年创建的一种颜色空间, 也称六角锥体模
Note:OpenCV 中的HSV的色调范围为[0,179],饱和度范围为[0,255],值范围为[0,255]。不同的软件使用不同的规模。因此,如果你要将OpenCV值和它们比较,你需要将这些范围标准化 。
改变颜色空间:
如何将图像从一个色彩空间转换到另一个,像BGR↔灰色,BGR↔HSV等
如何创建一个应用程序,以提取视频中的彩色对象
cv.cvtColor,cv.inRange
改变颜色空间:
import cv2 as cv import numpy as np # OpenCV中有超过150种颜色空间转换方法。但是我们将研究只有两个最广泛使用的,BGR↔灰色和BGR↔HSV。 if __name__ == '__main__': # 1 # cvtColor(input_image, flag),其中flag决定转换的类型 # 如何获取flag ,请参考下面代码: # flags = [i for i in dir(cv) if i.startswith('COLOR_')] # 2 # 取视频的每一帧 -> 转换从BGR到HSV颜色空间 -> 对HSV图像设置红色范围的阈值 cap = cv.VideoCapture(0) while 1: # 读取帧 _, frame = cap.read() # 转换颜色空间 BGR 到 HSV hsv = cv.cvtColor(frame, cv.COLOR_BGR2HSV) # 定义HSV中红色的范围 lower_red = np.array([150, 50, 50]) upper_red = np.array([180, 255, 255]) # 设置HSV的阈值使得只取红色 mask = cv.inRange(hsv, lower_red, upper_red) res = cv.bitwise_and(frame, frame, mask=mask) cv.imshow('frame', frame) cv.imshow('mask', mask) cv.imshow('res', res) k = cv.waitKey(10) if k == 27: break cv.destroyAllWindows()
1,捕捉相机中的红色元素:
左为原图,中为mask ,右为计算后的(图像中有一些噪点)。
OpenCV HSV 大致范围(参考):
使用程序将 bgr 转为 hsv:
import cv2 as cv import numpy as np def myBgr2Hsv(bgr): hsvColor = cv.cvtColor(bgr, cv.COLOR_BGR2HSV) print(hsvColor) if __name__ == '__main__': # 1 将bgr 颜色转化为 openCV 中的 HSV值 myBgr2Hsv(np.uint8([[[203, 192, 255]]])) # pink myBgr2Hsv(np.uint8([[[0, 0, 255]]])) # red
2,练习题:
尝试找到一种方法来提取多个彩色对象,例如,同时提取红色,蓝色,绿色对象
答案: Just add then together
import cv2 as cv import numpy as np if __name__ == '__main__': cap = cv.VideoCapture(0) while 1: _, frame = cap.read() # Convert BGR to HSV hsv = cv.cvtColor(frame, cv.COLOR_BGR2HSV) # define range of color in HSV lower_red = np.array([150, 50, 50]) upper_red = np.array([179, 255, 255]) lower_green = np.array([35, 50, 50]) upper_green = np.array([77, 255, 255]) lower_blue = np.array([100, 50, 50]) upper_blue = np.array([125, 255, 255]) red_mask = cv.inRange(hsv, lower_red, upper_red) green_mask = cv.inRange(hsv, lower_green, upper_green) blue_mask = cv.inRange(hsv, lower_blue, upper_blue) mask = red_mask + blue_mask + green_mask # Bitwise-AND mask and original image res = cv.bitwise_and(frame, frame, mask=mask) cv.imshow('frame', frame) cv.imshow('mask', mask) cv.imshow('res', res) k = cv.waitKey(5) if k == 27: break cv.destroyAllWindows()
图像的几何变换
将不同的几何变换应用到图像上,如平移、旋转、仿射变换 cv.getPerspectiveTransform
OpenCV提供了两个转换函数cv.warpAffine和cv.warpPerspective,您可以使用它们进行各种转换。cv.warpAffine采用2x3转换矩阵,而cv.warpPerspective采用3x3转换矩阵作为输入。
缩放:
import cv2 as cv import numpy as np if __name__ == '__main__': # 1 # 放大 为原来的2倍 使用插值方法为 cv.INTER_CUBIC img = cv.imread('images/1.jpg') # print(img.shape) # (1080, 1920, 3) cv.imshow("img", img) res1 = cv.resize(img, None, fx=2, fy=2, interpolation=cv.INTER_CUBIC) # print(res1.shape) # (2160, 3840, 3) cv.imshow("res1", res1) # 或 # height, width = img.shape[:2] # res2 = cv.resize(img, (2 * width, 2 * height), interpolation=cv.INTER_CUBIC) cv.waitKey(0) cv.destroyAllWindows()
平移:
平移是物体位置的移动。如果您知道在(x,y)方向上的位移,则将其设为(txtx,tyty),你可以创建转换矩阵MM,如下所示:
import cv2 as cv import numpy as np if __name__ == '__main__': # 1 # 平移 x 平移100 y 平移50 img = cv.imread('images/1.jpg', cv.IMREAD_GRAYSCALE) cv.imshow("origin",img) rows, cols = img.shape M = np.float32([[1, 0, 100], [0, 1, 50]]) dst = cv.warpAffine(img, M, (cols, rows)) # 第三个参数是输出图像的大小 cv.imshow('img', dst) cv.waitKey(0) cv.destroyAllWindows()
旋转:
图像旋转角度为θθ是通过以下形式的变换矩阵实现的:
但是OpenCV提供了可缩放的旋转以及可调整的旋转中心,因此您可以在自己喜欢的任何位置旋转。修改后的变换矩阵为
上面的 旋转矩阵 我们可以通过 cv.getRotationMatrix2D() 轻松 获得
import cv2 as cv import numpy as np if __name__ == '__main__': # 1 # 平移 x 平移100 y 平移50 img = cv.imread('images/1.jpg', cv.IMREAD_GRAYSCALE) cv.imshow("origin",img) rows, cols = img.shape # 在( (cols - 1) / 2.0, (rows - 1) / 2.0 ) 逆时针 旋转90度,不进行缩放 M = cv.getRotationMatrix2D(((cols - 1) / 2.0, (rows - 1) / 2.0), 90, 1) dst = cv.warpAffine(img, M, (cols, rows)) cv.imshow("rotated",dst) cv.waitKey(0) cv.destroyAllWindows()
仿射变换:
在仿射变换中,原始图像中的所有平行线在输出图像中仍将平行。为了找到变换矩阵,我们需要输入图像中的三个点及其在输出图像中的对应位置。然后cv.getAffineTransform将创建一个2x3矩阵,该矩阵将传递给cv.warpAffine
img = cv.imread('drawing.png') rows,cols,ch = img.shape pts1 = np.float32([[50,50],[200,50],[50,200]]) pts2 = np.float32([[10,100],[200,50],[100,250]]) M = cv.getAffineTransform(pts1,pts2) dst = cv.warpAffine(img,M,(cols,rows)) plt.subplot(121),plt.imshow(img),plt.title('Input') plt.subplot(122),plt.imshow(dst),plt.title('Output')
透视变换
对于透视变换,您需要3x3变换矩阵。即使在转换后,直线也将保持直线。要找到此变换矩阵,您需要在输入图像上有4个点,在输出图像上需要相应的点。在这四个点中,其中三个不应共线。然后可以通过函数cv.getPerspectiveTransform找到变换矩阵。然后将cv.warpPerspective应用于此3x3转换矩阵。
图像阈值
学习简单阈值,自适应阈值和Otsu阈值 cv.threshold和cv.adaptiveThreshold
简单阈值:
import cv2 as cv import numpy as np from matplotlib import pyplot as plt if __name__ == '__main__': # 1 # 对于每个像素,应用相同的阈值。如果像素值小于阈值,则将其设置为0,否则将其设置为最大值。 # 函数cv.threshold 用于应用阈值。 # 第一个参数是源图像,它应该是灰度图像 。 # 第二个参数是阈值,用于对像素值进行分类。 # 第三个参数是分配给超过阈值的像素值的最大值。 # OpenCV提供了不同类型的阈值,这由函数的第四个参数给出。 # 所有简单的阈值类型为: # cv.THRESH_BINARY # cv.THRESH_BINARY_INV # cv.THRESH_TRUNC # cv.THRESH_TOZERO # cv.THRESH_TOZERO_INV img = cv.imread('images/1.jpg', cv.IMREAD_GRAYSCALE) _, thresh1 = cv.threshold(img, 127, 255, cv.THRESH_BINARY) _, thresh2 = cv.threshold(img, 127, 255, cv.THRESH_BINARY_INV) _, thresh3 = cv.threshold(img, 127, 255, cv.THRESH_TRUNC) _, thresh4 = cv.threshold(img, 127, 255, cv.THRESH_TOZERO) _, thresh5 = cv.threshold(img, 127, 255, cv.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()
自适应阈值
import cv2 as cv import numpy as np from matplotlib import pyplot as plt if __name__ == '__main__': # 1 # 在上一节中,我们使用一个全局值作为阈值。但这可能并非在所有情况下都很好,例如,如果图像在不同区域具有不同的光照条件。在这种情况下,自适应阈值阈值化可以提供帮助。在此,算法基于像素周围的小区域确定像素的阈值。因此,对于同一图像的不同区域,我们获得了不同的阈值,这为光照度变化的图像提供了更好的结果。 # 除上述参数外,方法cv.adaptiveThreshold 还包含三个输入参数: # 1,adaptiveMethod 决定阈值是如何计算的: # cv.ADAPTIVE_THRESH_MEAN_C::阈值是邻近区域的平均值减去常数C。 # cv.ADAPTIVE_THRESH_GAUSSIAN_C: 阈值是邻域值的高斯加权总和减去常数 C。 # 2,BLOCKSIZE确定附近区域的大小, # 3,C是从邻域像素的平均或加权总和中减去的一个常数。 # 下面的代码比较了光照变化的图像的全局阈值和自适应阈值: # 对于每个像素,应用相同的阈值。如果像素值小于阈值,则将其设置为0,否则将其设置为最大值。 img = cv.imread('images/1.jpg', cv.IMREAD_GRAYSCALE) img = cv.medianBlur(img,5) # medianBlur 基本思想是用像素点邻域灰度值的中值来代替该像素点的灰度值,让周围的像素值接近真实的值从而消除孤立的噪声点。该方法在取出脉冲噪声、椒盐噪声的同时能保留图像的边缘细节。这些优良特性是线性滤波所不具备的 # 第二个参数 滤波模板的尺寸大小,必须是大于1的奇数,如3、5、7…… _, th1 = cv.threshold(img, 127, 255, cv.THRESH_BINARY) th2 = cv.adaptiveThreshold(img, 255, cv.ADAPTIVE_THRESH_MEAN_C, cv.THRESH_BINARY, 11, 2) th3 = cv.adaptiveThreshold(img, 255, cv.ADAPTIVE_THRESH_GAUSSIAN_C, cv.THRESH_BINARY, 11, 2) 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()
Otsu的二值化
它使用于灰度图呈现双峰的阈值处理,
素材图:
import cv2 as cv import numpy as np from matplotlib import pyplot as plt if __name__ == '__main__': # 1 # 考虑仅具有两个不同图像值的图像(双峰图像),其中直方图将仅包含两个峰。一个好的阈值应该在这两个值的中间 img = cv.imread('images/a.png', 0) # 全局阈值 ret1, th1 = cv.threshold(img, 127, 255, cv.THRESH_BINARY) # Otsu阈值 ret2, th2 = cv.threshold(img, 0, 255, cv.THRESH_BINARY + cv.THRESH_OTSU) # 高斯滤波后再采用Otsu阈值 blur = cv.GaussianBlur(img, (5, 5), 0) ret3, th3 = cv.threshold(blur, 0, 255, cv.THRESH_BINARY + cv.THRESH_OTSU) # 绘制所有图像及其直方图 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) # numpy.ndarray 类的一个方法 .ravel() 。Return a flattened array._ 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()
其它参见:http://www.woshicver.com/FifthSection/4_3_%E5%9B%BE%E5%83%8F%E9%98%88%E5%80%BC/#otsu
图像平滑
使用各种 低通滤镜模糊图像,将定制的滤镜应用于图像(2D卷积)
2D卷积:
LPF(低通滤波器)有助于消除噪声,使图像模糊等。HPF(高通滤波器)滤波器有助于在图像中找到边缘。
OpenCV提供了一个函数cv.filter2D来将内核与图像进行卷积。例如,我们将尝试对图像进行平均滤波。5x5平均滤波器内核如下所示:
import numpy as np import cv2 as cv from matplotlib import pyplot as plt if __name__ == '__main__': img = cv.imread('images/boat.png') kernel = np.ones((5, 5), np.float32) / 25 dst = cv.filter2D(img, -1, kernel) 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()
图像模糊:
通过将图像与低通滤波器内核进行卷积来实现图像模糊。这对于消除噪音很有用。它实际上从图像中消除了高频部分(例如噪声,边缘)。因此,在此操作中边缘有些模糊。(有一些模糊技术也可以不模糊边缘)。OpenCV主要提供四种类型的模糊技术。
1,平均模糊(滤波):
这是通过将图像与归一化框滤镜进行卷积来完成的。它仅获取内核区域下所有像素的平均值,并替换中心元素。这是通过功能cv.blur()或cv.boxFilter()完成的。我们应该指定内核的宽度和高度。3x3归一化框式过滤器如下所示:
注意 如果您不想使用标准化的框式过滤器,请使用cv.boxFilter()。将参数normalize = False
传递给函数。
import numpy as np import cv2 as cv from matplotlib import pyplot as plt if __name__ == '__main__': img = cv.imread('images/logo.png') blur = cv.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()
2,高斯模糊(滤波):
在这种情况下,代替盒式滤波器,使用了高斯核。这是通过功能cv.GaussianBlur()完成的。我们应指定内核的宽度和高度,该宽度和高度应为正数和奇数。我们还应指定X和Y方向的标准偏差,分别为sigmaX和sigmaY。如果仅指定sigmaX,则将sigmaY与sigmaX相同。如果两个都为零,则根据内核大小进行计算。高斯模糊对于从图像中去除高斯噪声非常有效。
如果需要,可以使用函数cv.getGaussianKernel()创建高斯内核。
import numpy as np import cv2 as cv from matplotlib import pyplot as plt if __name__ == '__main__': img = cv.imread('images/logo.png') blur = cv.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('GaussianBlur') plt.xticks([]), plt.yticks([]) plt.show()
3,中位模糊(滤波):
函数cv.medianBlur() 提取内核区域下所有像素的中值,并将中心元素替换为该中值。这对于消除图像中的椒盐噪声非常有效。有趣的是,在上述过滤器中,中心元素是新计算的值,该值可以是图像中的像素值或新值。但是在中值模糊中,中心元素总是被图像中的某些像素值代替。有效降低噪音。其内核大小应为正奇数整数。
import numpy as np import cv2 as cv from matplotlib import pyplot as plt if __name__ == '__main__': img = cv.imread('images/logo.png') blur = cv.medianBlur(img, 5) plt.subplot(121), plt.imshow(img), plt.title('Original') plt.xticks([]), plt.yticks([]) plt.subplot(122), plt.imshow(blur), plt.title('medianBlur') plt.xticks([]), plt.yticks([]) plt.show()
4,双边模糊(滤波):
cv.bilateralFilter() 在去除噪声的同时保持边缘清晰锐利非常有效。但是,与其他过滤器相比,该操作速度较慢。我们已经看到,高斯滤波器采用像素周围的邻域并找到其高斯加权平均值。高斯滤波器仅是空间的函数,也就是说,滤波时会考虑附近的像素。高斯滤波不考虑像素是否具有几乎相同的强度。高斯滤波不考虑像素是否是边缘像素。因此高斯滤波也模糊了边缘,这是我们不想做的。
双边滤波器在空间中也采用高斯滤波器,但是又有一个高斯滤波器,它是像素差的函数。空间的高斯函数确保仅考虑附近像素的模糊,而强度差的高斯函数确保仅考虑强度与中心像素相似的那些像素的模糊。由于边缘的像素强度变化较大,因此可以保留边缘。
import numpy as np import cv2 as cv from matplotlib import pyplot as plt if __name__ == '__main__': img = cv.imread('images/a.png') blur = cv.bilateralFilter(img, 5,75,75) plt.subplot(121), plt.imshow(img), plt.title('Original') plt.xticks([]), plt.yticks([]) plt.subplot(122), plt.imshow(blur), plt.title('medianBlur') plt.xticks([]), plt.yticks([]) plt.show()
形态学转换
将学习不同的形态学操作,例如 侵蚀,膨胀,开运算,闭运算 等。cv.erode(),cv.dilate(), cv.morphologyEx()等
形态变换是一些基于图像形状的简单操作。通常在二进制图像(mask)上执行。它需要两个输入,一个是我们的原始图像,第二个是决定操作性质的结构元素或内核。
两种基本的形态学算子是侵蚀和膨胀。然后,它的变体形式是(如“打开”,“关闭”,“渐变”等)。
图片素材:
侵蚀:
侵蚀的基本思想就像土壤侵蚀一样,它侵蚀前景物体的边界(尽量使前景保持白色)。它是做什么的呢?内核滑动通过图像(在2D卷积中)。原始图像中的一个像素(无论是1还是0)只有当内核下的所有像素都是1时才被认为是1,否则它就会被侵蚀(变成0)。
结果是,根据内核的大小,边界附近的所有像素都会被丢弃。因此,前景物体的厚度或大小减小,或只是图像中的白色区域减小。它有助于去除小的白色噪声(正如我们在颜色空间章节中看到的),分离两个连接的对象等。
import numpy as np import cv2 as cv if __name__ == '__main__': img = cv.imread('images/j.png', cv.IMREAD_GRAYSCALE) kernel = np.ones((5, 5), np.uint8) # 使用一个5x5内核 erosion = cv.erode(img, kernel, iterations=1) cv.imshow("img",img) cv.imshow("erode 侵蚀",erosion) cv.waitKey(0) cv.destroyAllWindows()
扩张:
它与侵蚀正好相反。如果内核下的至少一个像素为“ 1”,则像素元素为“ 1”。
因此,它会增加图像中的白色区域或增加前景对象的大小。
import numpy as np import cv2 as cv if __name__ == '__main__': img = cv.imread('images/j.png', cv.IMREAD_GRAYSCALE) kernel = np.ones((5, 5), np.uint8) # 使用一个5x5内核 dilation = cv.dilate(img, kernel, iterations=1) cv.imshow("img",img) cv.imshow("dilation 扩张",dilation) cv.waitKey(0) cv.destroyAllWindows()
开运算:
开运算 只是 侵蚀然后扩张 的另一个名称。它对于消除噪音很有用。在这里,我们使用函数cv.morphologyEx()
import numpy as np import cv2 as cv if __name__ == '__main__': img = cv.imread('images/j.png', cv.IMREAD_GRAYSCALE) kernel = np.ones((5, 5), np.uint8) # 使用一个5x5内核 dilation = cv.morphologyEx(img, cv.MORPH_OPEN, kernel) cv.imshow("img", img) cv.imshow("MORPH_OPEN 开运算", dilation) cv.waitKey(0) cv.destroyAllWindows()
闭运算:
闭运算与开运算相反,先扩张然后再侵蚀。在关闭前景对象内部的小孔或对象上的小黑点时很有用。
import numpy as np import cv2 as cv if __name__ == '__main__': img = cv.imread('images/j.png', cv.IMREAD_GRAYSCALE) kernel = np.ones((5, 5), np.uint8) # 使用一个5x5内核 dilation = cv.morphologyEx(img, cv.MORPH_CLOSE, kernel) cv.imshow("img", img) cv.imshow("MORPH_CLOSE 闭运算", dilation) cv.waitKey(0) cv.destroyAllWindows()
形态学梯度:
它和扩张和侵蚀是有区别的,
结果将看起来像对象的轮廓
import numpy as np import cv2 as cv if __name__ == '__main__': img = cv.imread('images/j.png', cv.IMREAD_GRAYSCALE) kernel = np.ones((5, 5), np.uint8) # 使用一个5x5内核 gradient = cv.morphologyEx(img, cv.MORPH_GRADIENT, kernel) cv.imshow("img", img) cv.imshow("MORPH_GRADIENT 形态学梯度", gradient) cv.waitKey(0) cv.destroyAllWindows()
顶帽:
输入图像和图像开运算之差
import numpy as np import cv2 as cv if __name__ == '__main__': img = cv.imread('images/open.png', cv.IMREAD_GRAYSCALE) kernel = np.ones((5, 5), np.uint8) # 使用一个5x5内核 tophat = cv.morphologyEx(img, cv.MORPH_TOPHAT, kernel) cv.imshow("img", img) cv.imshow("MORPH_TOPHAT 顶帽 ", tophat) cv.waitKey(0) cv.destroyAllWindows()
黑帽:
输入图像和图像闭运算之差
import numpy as np import cv2 as cv if __name__ == '__main__': img = cv.imread('images/close.png', cv.IMREAD_GRAYSCALE) kernel = np.ones((5, 5), np.uint8) # 使用一个5x5内核 blackhat = cv.morphologyEx(img, cv.MORPH_BLACKHAT, kernel) cv.imshow("img", img) cv.imshow("MORPH_BLACKHAT 黑帽 ", blackhat) cv.waitKey(0) cv.destroyAllWindows()
其他内核:
在Numpy的帮助下,我们在前面的示例中手动创建了一个结构元素。它是矩形。但是在某些情况下,您可能需要椭圆形/圆形的内核。因此,为此,OpenCV具有一个函数cv.getStructuringElement()。您只需传递内核的形状和大小,即可获得所需的内核。
# 矩形内核 >>> cv.getStructuringElement(cv.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) # 椭圆内核 >>> cv.getStructuringElement(cv.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) # 十字内核 >>> cv.getStructuringElement(cv.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)
图像梯度
如何查找图像梯度,边缘
cv.Sobel(),cv.Scharr(),cv.Laplacian()等
OpenCV提供三种类型的梯度滤波器,即Sobel,Scharr和Laplacian
1. Sobel 算子
边界检测,
计算近似梯度:
Sobel算子是高斯平滑加微分运算的联合运算,因此它更抗噪声。你可以指定要采用的导数方向,垂直或水平(分别通过参数yorder和xorder)。你还可以通过参数ksize指定内核的大小。
import numpy as np import cv2 as cv import matplotlib.pyplot as plt if __name__ == '__main__': img = cv.imread('images/aaa.jpg', cv.IMREAD_GRAYSCALE) sobelx = cv.Sobel(img, cv.CV_64F, 1, 0, ksize=5)# x sobelx = cv.convertScaleAbs(sobelx) # 调整为绝对值,配合cv.CV_64F 用,不然就只有右面的边界了 sobely = cv.Sobel(img, cv.CV_64F, 0, 1, ksize=5)# y sobely = cv.convertScaleAbs(sobely) sobelxy =cv.addWeighted(sobelx,0.5,sobely,0.5,0) sobelxy11 = cv.Sobel(img,cv.CV_64F,1,1,ksize=5) # x,y 不能同时设为1,1 效果不行 sobelxy11 = cv.convertScaleAbs(sobelxy11) plt.subplot(3, 2, 1), plt.imshow(img, cmap='gray') plt.title('Original'), plt.xticks([]), plt.yticks([]) plt.subplot(3, 2, 2), plt.imshow(sobelx, cmap='gray') plt.title('Sobel X'), plt.xticks([]), plt.yticks([]) plt.subplot(3, 2, 3), plt.imshow(sobely, cmap='gray') plt.title('Sobel Y'), plt.xticks([]), plt.yticks([]) plt.subplot(3, 2, 4), plt.imshow(sobelxy, cmap='gray') plt.title('Sobel XY'), plt.xticks([]), plt.yticks([]) plt.subplot(3, 2, 5), plt.imshow(sobelxy11, cmap='gray') # 效果就不行 plt.title('Sobel XY11'), plt.xticks([]), plt.yticks([]) plt.show()
2.Scharr 算子
scharr 的卷积核的系数和sobel 的不太一样,
import numpy as np import cv2 as cv import matplotlib.pyplot as plt if __name__ == '__main__': img = cv.imread('images/aaa.jpg', cv.IMREAD_GRAYSCALE) scharrx = cv.Scharr(img, cv.CV_64F, 1, 0) # x ==等价于 cv.Sobel(img, cv.CV_64F, 1, 0,ksize=-1) scharrx = cv.convertScaleAbs(scharrx) # 调整为绝对值,配合cv.CV_64F 用,不然就只有右面的边界了 scharry = cv.Scharr(img, cv.CV_64F, 0, 1) # y scharry = cv.convertScaleAbs(scharry) scharrxy = cv.addWeighted(scharrx, 0.5, scharry, 0.5, 0) plt.subplot(2, 2, 1), plt.imshow(img, cmap='gray') plt.title('Original'), plt.xticks([]), plt.yticks([]) plt.subplot(2, 2, 2), plt.imshow(scharrx, cmap='gray') plt.title('Scharr X'), plt.xticks([]), plt.yticks([]) plt.subplot(2, 2, 3), plt.imshow(scharry, cmap='gray') plt.title('Scharr Y'), plt.xticks([]), plt.yticks([]) plt.subplot(2, 2, 4), plt.imshow(scharrxy, cmap='gray') plt.title('Scharr XY'), plt.xticks([]), plt.yticks([]) plt.show()
3.Laplacian算子
Laplacian 之所以是二阶导数,是因为在同一方向上进行了两次操作,
import numpy as np import cv2 as cv import matplotlib.pyplot as plt if __name__ == '__main__': img = cv.imread('images/aaa.jpg', cv.IMREAD_GRAYSCALE) laplacian = cv.Laplacian(img, cv.CV_64F) laplacian = cv.convertScaleAbs(laplacian) plt.subplot(2, 2, 1), plt.imshow(img, cmap='gray') plt.title('Original'), plt.xticks([]), plt.yticks([]) plt.subplot(2, 2, 2), plt.imshow(laplacian, cmap='gray') plt.title('Laplacian'), plt.xticks([]), plt.yticks([]) plt.show()
Canny 边缘检测:
Canny边缘检测的概念,cv.Canny()
四个步骤:
1,去噪(一般使用图像平滑中的高斯滤波处理)
2,梯度
3,非极大值抑制
4,双阈值处理
梯度:
使用Sobel 算子 计算梯度要分别计算梯度值和方向,
Note:5x5的算子,
非极大值抑制
双阈值处理:
该阶段确定哪些边缘全部是真正的边缘,哪些不是。为此,我们需要两个阈值minVal
和maxVal
。强度梯度大于maxVal
的任何边缘必定是边缘,而小于minVal
的那些边缘必定是非边缘,因此将其丢弃。介于这两个阈值之间的对象根据其连通性被分类为边缘或非边缘。如果将它们连接到“边缘”像素,则将它们视为边缘的一部分。否则,它们也将被丢弃
代码实现:
import numpy as np import cv2 as cv import matplotlib.pyplot as plt if __name__ == '__main__': img = cv.imread('images/3.jpg', cv.IMREAD_GRAYSCALE) dst = cv.Canny(img,100,200) dst2 = cv.Canny(img,64,128) # 细节更丰富 plt.subplot(2, 2, 1), plt.imshow(img, cmap='gray') plt.title('Original'), plt.xticks([]), plt.yticks([]) plt.subplot(2, 2, 2), plt.imshow(dst, cmap='gray') plt.title('Canny'), plt.xticks([]), plt.yticks([]) plt.subplot(2, 2, 3), plt.imshow(dst2, cmap='gray') plt.title('Canny'), plt.xticks([]), plt.yticks([]) plt.show()
练习:
编写一个小应用程序以找到Canny边缘检测,该检测的阈值可以使用两个跟踪栏进行更改。这样,你可以了解阈值的影响。
import cv2 as cv if __name__ == '__main__': cv.namedWindow('origin') cv.createTrackbar('minVal', 'origin', 0, 255, lambda x: 0) cv.createTrackbar('maxVal', 'origin', 0, 255, lambda x: 0) img = cv.imread('images/3.jpg', cv.IMREAD_GRAYSCALE) while 1: if cv.waitKey(1) == 27: break minVal = cv.getTrackbarPos('minVal', 'origin') maxVal = cv.getTrackbarPos('maxVal', 'origin') dst = cv.Canny(img, minVal, maxVal) cv.imshow('origin', img) cv.imshow('canny', dst)
图像金字塔
什么是图像金字塔(pyramid),cv.pyrUp(),cv.pyrDown()
概念:
图像金字塔是同一图像的不同分辨率的子图集合,
如何生成图像金字塔:
1,向下取样:
两个步骤:
通过这样做,M×N 图像变成M/2×N/2图像。因此面积减少到原始面积的四分之一。它称为Octave。
2,向上取样:
为何乘4?
通过这样做,M×N M×N图像变成2M×2N 图像。因此面积增大到原始面积的四倍。
注:向下取样和向上取样不是互逆的,
向下取样代码:
import cv2 as cv if __name__ == '__main__': img = cv.imread("images/4.jpg") res = cv.pyrDown(img) # 向下取样 res2 = cv.pyrDown(res) # 向下取样 res3 = cv.pyrDown(res2) # 向下取样 cv.imshow("img",img) cv.imshow("res",res) cv.imshow("res2",res2) cv.imshow("res3",res3) cv.waitKey(0) cv.destroyAllWindows()
向上取样代码:
import cv2 as cv if __name__ == '__main__': img = cv.imread("images/zzz.jpg") res = cv.pyrUp(img) # 向上取样 res2 = cv.pyrUp(res) # 向上取样 res3 = cv.pyrUp(res2) # 向上取样 cv.imshow("img",img) cv.imshow("res",res) cv.imshow("res2",res2) cv.imshow("res3",res3) cv.waitKey(0) cv.destroyAllWindows()
拉普拉斯金字塔
前面的金字塔叫做高斯金字塔,因为使用的是高斯卷积核,
拉普拉斯金字塔是在高斯金字塔基础之上的新的金字塔,
import cv2 as cv if __name__ == '__main__': img = cv.imread("images/lena.jpg") down1 = cv.pyrDown(img) res = img - cv.pyrUp(down1) down2 = cv.pyrDown(down1) res2 = down1 - cv.pyrUp(down2) cv.imshow("img", img) cv.imshow("res", res) cv.imshow("res2", res2) cv.waitKey(0) cv.destroyAllWindows()
使用金字塔进行图像融合
金字塔的一种应用是图像融合。例如,在图像拼接中,您需要将两个图像堆叠在一起,但是由于图像之间的不连续性,可能看起来不太好。在这种情况下,使用金字塔混合图像可以无缝混合,而不会在图像中保留大量数据。一个经典的例子是将两种水果,橙和苹果混合在一起
import cv2 as cv import numpy as np import matplotlib.pyplot as plt if __name__ == '__main__': apple = cv.imread('images/apple.jpg') orange = cv.imread('images/orange.jpg') apple = cv.resize(apple, (320, 320), interpolation=cv.INTER_CUBIC) # 将原图300,300 调整到64 的整数倍,因为下面有6层金字塔,这样就不会有小数出现 orange = cv.resize(orange, (320, 320), interpolation=cv.INTER_CUBIC) # 生成apple的高斯金字塔 G = apple.copy()[..., ::-1] plt.subplot(3, 3, 1), plt.imshow(G, cmap='gray') plt.title('Original'), plt.xticks([]), plt.yticks([]) gaussianAppleList = [G] for i in range(6): G = cv.pyrDown(G) gaussianAppleList.append(G) plt.subplot(3, 3, i + 2), plt.imshow(G, cmap='gray') plt.title(f'Gaussian{i + 1}'), plt.xticks([]), plt.yticks([]) plt.show() # 生成orange的高斯金字塔 G = orange.copy()[..., ::-1] plt.subplot(3, 3, 1), plt.imshow(G, cmap='gray') plt.title('Original'), plt.xticks([]), plt.yticks([]) gaussianOrangeList = [G] for i in range(6): G = cv.pyrDown(G) gaussianOrangeList.append(G) plt.subplot(3, 3, i + 2), plt.imshow(G, cmap='gray') plt.title(f'Gaussian{i + 1}'), plt.xticks([]), plt.yticks([]) plt.show() # 生成apple的拉普拉斯金字塔 lpApple = [gaussianAppleList[5]] for i in range(5, 0, -1): gaussianUp = cv.pyrUp(gaussianAppleList[i]) L = cv.subtract(gaussianAppleList[i - 1], gaussianUp) lpApple.append(L) plt.subplot(3, 3, 6 - i), plt.imshow(L, cmap='gray') plt.title(f'Lp Apple{6 - i}'), plt.xticks([]), plt.yticks([]) plt.show() # 生成B的拉普拉斯金字塔 lpOrange = [gaussianOrangeList[5]] for i in range(5, 0, -1): gaussianUp = cv.pyrUp(gaussianOrangeList[i]) L = cv.subtract(gaussianOrangeList[i - 1], gaussianUp) lpOrange.append(L) plt.subplot(3, 3, 6 - i), plt.imshow(L, cmap='gray') plt.title(f'Lp Orange{6 - i}'), plt.xticks([]), plt.yticks([]) plt.show() # # 在每个拉普拉斯金字塔级别中加入苹果的左半部分和橙子的右半部分 lpNewList = [] i = 0 for lp1, lp2 in zip(lpApple, lpOrange): rows, cols, dpt = lp1.shape lp_new = np.hstack((lp1[:, 0:cols // 2], lp2[:, cols // 2:])) # 水平方向 连接 lpNewList.append(lp_new) plt.subplot(3, 3, i + 1), plt.imshow(lp_new, cmap='gray') plt.title(f'Lp New {i + 1}'), plt.xticks([]), plt.yticks([]) i += 1 plt.show() # 重建原始图像 lp_ = lpNewList[0] # 最模糊 的 lp plt.subplot(3, 3, 1), plt.imshow(lp_, cmap='gray') plt.title(f'Lp ReBuilt 1'), plt.xticks([]), plt.yticks([]) for i in range(1, 6): lp_ = cv.pyrUp(lp_) lp_ = cv.add(lp_, lpNewList[i]) plt.subplot(3, 3, i + 1), plt.imshow(lp_, cmap='gray') plt.title(f'Lp ReBuilt {i + 1}'), plt.xticks([]), plt.yticks([]) plt.show() # ================================ # 如果直接将图像 拼接 cols = apple.shape[1] sc = np.hstack((apple[:, :cols // 2, ::-1], orange[:, cols // 2:, ::-1])) plt.subplot(1, 2, 1), plt.imshow(sc, cmap='gray') plt.title(f'Simply Concat'), plt.xticks([]), plt.yticks([]) plt.subplot(1, 2, 2), plt.imshow(lp_, cmap='gray') plt.title(f'Pyramid Process'), plt.xticks([]), plt.yticks([]) plt.show()
轮廓
边缘检测虽然能够检测出边缘,但边缘是不连续的,图像轮廓是指将边缘连接起来形成一个整体,
轮廓是什么,如何查找轮廓,绘制轮廓
cv.findContours(),cv.drawContours()
什么是轮廓:
轮廓可以简单地解释为连接具有相同颜色或强度的所有连续点(沿边界)的曲线。轮廓是用于形状分析以及对象检测和识别的有用工具。
1,为了获得更高的准确性,请使用二进制图像。因此,在找到轮廓之前,请应用阈值或canny边缘检测
2,从OpenCV 3.2开始,findContours()不再修改源图像
3,在OpenCV中,找到轮廓就像从黑色背景中找到白色物体。因此请记住,要找到的对象应该是白色,背景应该是黑色
查找轮廓并画出:
findcontour()函数中有三个参数,第一个是源图像,第二个是轮廓检索模式,第三个是轮廓逼近方法。输出等高线和层次结构。轮廓是图像中所有轮廓的Python列表。每个单独的轮廓是一个(x,y)坐标的Numpy数组的边界点的对象。
import cv2 as cv import numpy as np import matplotlib.pyplot as plt if __name__ == '__main__': img = cv.imread('images/lena.jpg') cv.imshow("img",img) imgGray = cv.cvtColor(img,cv.COLOR_BGR2GRAY) _, thresh = cv.threshold(imgGray, 127, 255, 0) # contours 是个列表,包含了所有的轮廓,每个轮廓是由点组成的 contours, hierarchy = cv.findContours(thresh, cv.RETR_TREE, cv.CHAIN_APPROX_SIMPLE) # 第一个是源图像,第二个是轮廓检索模式,第三个是轮廓逼近方法 # 绘制轮廓 到 原图上 # cv.drawContours(img,contours,-1,(0,255,0),1) # 绘制所有的轮廓 # cv.drawContours(img,contours,3,(0,255,0),1) # 绘制第四个轮廓 # 但是在大多数情况下,以下方法会很有用: cnt = contours[3] cv.drawContours(img, [cnt], 0, (0, 255, 0), 1) cv.imshow("dst",img) cv.waitKey(0) cv.destroyAllWindows()
轮廓特征:
如何找到轮廓的面积,周长,质心,边界框等
cv.moment() cv.contourArea() cv.arcLenth()
1 特征矩
特征矩可以帮助您计算一些特征,例如物体的质心,物体的面积等。
函数cv.moments()提供了所有计算出的矩值的字典
import cv2 as cv import numpy as np import matplotlib.pyplot as plt if __name__ == '__main__': img = cv.imread('images/star.jpg') imgGray = cv.cvtColor(img, cv.COLOR_BGR2GRAY) _, thresh = cv.threshold(imgGray, 50, 255, 0) # cv.imshow("imgGray",imgGray) # cv.imshow("thresh",thresh) contours, hierarchy = cv.findContours(thresh, cv.RETR_TREE, cv.CHAIN_APPROX_SIMPLE) cnt = contours[0] M = cv.moments(cnt) print(M) ''' {'m00': 29596.0, 'm10': 3305242.5, 'm01': 2100078.833333333, 'm20': 489679620.8333333, 'm11': 236742605.8333333, 'm02': 198434862.5, 'm30': 81469692246.65001, 'm21': 35396123621.833336, 'm12': 22497281737.8, 'm03': 21186929032.75, 'mu20': 120554469.33291936, 'mu11': 2208553.485592097, 'mu02': 49417052.45084831, 'mu30': -143816517.0492096, 'mu21': 156043323.3139515, 'mu12': 22906353.578845024, 'mu03': 93258731.11600876, 'nu20': 0.1376313210231425, 'nu11': 0.0025214007863357023, 'nu02': 0.056417105458760115, 'nu30': -0.0009543907471199464, 'nu21': 0.0010355299027977676, 'nu12': 0.00015201043909599888, 'nu03': 0.0006188807231008864} ''' cv.waitKey(0) cv.destroyAllWindows()
质心:
import cv2 as cv import numpy as np import matplotlib.pyplot as plt if __name__ == '__main__': img = cv.imread('images/star.jpg') imgGray = cv.cvtColor(img, cv.COLOR_BGR2GRAY) _, thresh = cv.threshold(imgGray, 50, 255, 0) # cv.imshow("imgGray",imgGray) # cv.imshow("thresh",thresh) contours, hierarchy = cv.findContours(thresh, cv.RETR_TREE, cv.CHAIN_APPROX_SIMPLE) cnt = contours[0] M = cv.moments(cnt) # print(M) cx = int(M['m10'] / M['m00']) cy = int(M['m01'] / M['m00']) print(cx,cy)
2 轮廓面积
轮廓区域由函数cv.contourArea()或从矩M['m00']
中给出。
import cv2 as cv import numpy as np import matplotlib.pyplot as plt if __name__ == '__main__': img = cv.imread('images/star.jpg') imgGray = cv.cvtColor(img, cv.COLOR_BGR2GRAY) _, thresh = cv.threshold(imgGray, 50, 255, 0) # cv.imshow("imgGray",imgGray) # cv.imshow("thresh",thresh) contours, hierarchy = cv.findContours(thresh, cv.RETR_TREE, cv.CHAIN_APPROX_SIMPLE) cnt = contours[0] M = cv.moments(cnt) # print(M) # 面积 print(M['m00'])
import cv2 as cv import numpy as np import matplotlib.pyplot as plt if __name__ == '__main__': img = cv.imread('images/star.jpg') imgGray = cv.cvtColor(img, cv.COLOR_BGR2GRAY) _, thresh = cv.threshold(imgGray, 50, 255, 0) # cv.imshow("imgGray",imgGray) # cv.imshow("thresh",thresh) contours, hierarchy = cv.findContours(thresh, cv.RETR_TREE, cv.CHAIN_APPROX_SIMPLE) cnt = contours[0] # 面积 # M = cv.moments(cnt) # print(M) # print(M['m00']) print(cv.contourArea(cnt))
3 轮廓周长
也称为弧长。可以使用cv.arcLength()函数找到它。第二个参数指定形状是闭合轮廓(True
)还是曲线
import cv2 as cv import numpy as np import matplotlib.pyplot as plt if __name__ == '__main__': img = cv.imread('images/star.jpg') imgGray = cv.cvtColor(img, cv.COLOR_BGR2GRAY) _, thresh = cv.threshold(imgGray, 50, 255, 0) # cv.imshow("imgGray",imgGray) # cv.imshow("thresh",thresh) contours, hierarchy = cv.findContours(thresh, cv.RETR_TREE, cv.CHAIN_APPROX_SIMPLE) cnt = contours[0] # 周长 perimeter = cv.arcLength(cnt, True) print(perimeter)
4 轮廓近似
根据我们指定的精度,它可以将轮廓形状近似为顶点数量较少的其他形状。它是Douglas-Peucker算法的实现。
第二个参数称为epsilon,它是从轮廓到近似轮廓的最大距离。
import cv2 as cv import numpy as np import matplotlib.pyplot as plt if __name__ == '__main__': img = cv.imread('images/star.jpg') imgGray = cv.cvtColor(img, cv.COLOR_BGR2GRAY) _, thresh = cv.threshold(imgGray, 50, 255, 0) # cv.imshow("imgGray",imgGray) # cv.imshow("thresh",thresh) contours, hierarchy = cv.findContours(thresh, cv.RETR_TREE, cv.CHAIN_APPROX_SIMPLE) cnt = contours[0] dst = cv.drawContours(img, [cnt], 0, (0, 255, 0), 1) cv.imshow("dst", dst) # 轮廓近似 epsilon = 0.1 * cv.arcLength(cnt, True) approx = cv.approxPolyDP(cnt, epsilon, True) # print(approx) # 将近似曲线的点 画在图上 用蓝色标出 res1 = cv.polylines(img, [approx], True, (255, 0, 0)) cv.imshow("res1",res1) # 改变epsilon epsilon = 0.01 * cv.arcLength(cnt, True) approx = cv.approxPolyDP(cnt, epsilon, True) res2 = cv.polylines(img, [approx], True, (255, 0, 0)) cv.imshow("res2",res2) cv.waitKey(0) cv.destroyAllWindows()
在第二张图片中,蓝线显示了ε=弧长的10%时的近似曲线。第三幅图显示了ε=弧长度的1%时的情况。
5 轮廓凸包
凸包外观看起来与轮廓逼近相似,但不相似(在某些情况下两者可能提供相同的结果)。在这里,cv.convexHull()函数检查曲线是否存在凸凹缺陷并对其进行校正。一般而言,凸曲线是始终凸出或至少平坦的曲线。如果在内部凸出,则称为凸度缺陷。例如,检查下面的手的图像。红线显示手的凸包。双向箭头标记显示凸度缺陷,这是凸包与轮廓线之间的局部最大偏差
import cv2 as cv import numpy as np import matplotlib.pyplot as plt if __name__ == '__main__': img = cv.imread('images/hand.jpg') imgGray = cv.cvtColor(img, cv.COLOR_BGR2GRAY) _, thresh = cv.threshold(imgGray, 50, 255, 0) contours, hierarchy = cv.findContours(thresh, cv.RETR_TREE, cv.CHAIN_APPROX_SIMPLE) cnt = contours[0] hull = cv.convexHull(cnt) # cv.convexHull()函数检查曲线是否存在凸凹缺陷并对其进行校正 res1 = cv.polylines(img, [hull], True, (255, 0, 0)) cv.imshow("res1", res1) dst = cv.drawContours(img, [cnt], 0, (0, 255, 0), 1) cv.imshow("dst", dst) cv.waitKey(0) cv.destroyAllWindows()
6 检查凸度
cv.isContourConvex()具有检查曲线是否凸出的功能。它只是返回True还是False
ret = cv.isContourConvex(cnt)
7 边界矩形
a, 直角矩形
它不考虑物体的旋转。所以边界矩形的面积不是最小的。它是由函数cv.boundingRect()找到的。
import cv2 as cv import numpy as np import matplotlib.pyplot as plt if __name__ == '__main__': img = cv.imread('images/aaa.jpg') imgGray = cv.cvtColor(img, cv.COLOR_BGR2GRAY) _, thresh = cv.threshold(imgGray, 50, 255, 0) contours, hierarchy = cv.findContours(thresh, cv.RETR_TREE, cv.CHAIN_APPROX_SIMPLE) cnt = contours[0] x,y,w,h = cv.boundingRect(cnt) cv.rectangle(img,(x,y),(x+w,y+h),(255,0,0),2) dst = cv.drawContours(img, [cnt], 0, (0, 255, 0), 1) cv.imshow("dst", dst) cv.waitKey(0) cv.destroyAllWindows()
b, 旋转矩形
边界矩形是用最小面积绘制的,所以它也考虑了旋转。使用的函数是cv.minAreaRect()。它返回一个Box2D结构,其中包含以下细节 -(中心(x,y),(宽度,高度),旋转角度)。但要画出这个矩形,我们需要矩形的四个角。它由函数cv.boxPoints()获得
import cv2 as cv import numpy as np import matplotlib.pyplot as plt if __name__ == '__main__': img = cv.imread('images/aaa.jpg') imgGray = cv.cvtColor(img, cv.COLOR_BGR2GRAY) _, thresh = cv.threshold(imgGray, 50, 255, 0) contours, hierarchy = cv.findContours(thresh, cv.RETR_TREE, cv.CHAIN_APPROX_SIMPLE) cnt = contours[0] rect = cv.minAreaRect(cnt) box = np.int0(cv.boxPoints(rect)).reshape(-1,1,2) cv.polylines(img,[box],True,(0,0,255),2) dst = cv.drawContours(img, [cnt], 0, (0, 255, 0), 1) cv.imshow("dst", dst) cv.waitKey(0) cv.destroyAllWindows()
8 最小闭合圆
使用函数cv.minEnclosingCircle()查找对象的圆周。它是一个以最小面积完全覆盖物体的圆
import cv2 as cv import numpy as np import matplotlib.pyplot as plt if __name__ == '__main__': img = cv.imread('images/aaa.jpg') imgGray = cv.cvtColor(img, cv.COLOR_BGR2GRAY) _, thresh = cv.threshold(imgGray, 50, 255, 0) contours, hierarchy = cv.findContours(thresh, cv.RETR_TREE, cv.CHAIN_APPROX_SIMPLE) cnt = contours[0] (x, y), radius = cv.minEnclosingCircle(cnt) center = (int(x), int(y)) radius = int(radius) cv.circle(img, center, radius, (255, 0, 0), 2) dst = cv.drawContours(img, [cnt], 0, (0, 255, 0), 1) cv.imshow("dst", dst) cv.waitKey(0) cv.destroyAllWindows()
9 拟合一个椭圆
import cv2 as cv import numpy as np import matplotlib.pyplot as plt if __name__ == '__main__': img = cv.imread('images/aaa.jpg') imgGray = cv.cvtColor(img, cv.COLOR_BGR2GRAY) _, thresh = cv.threshold(imgGray, 50, 255, 0) contours, hierarchy = cv.findContours(thresh, cv.RETR_TREE, cv.CHAIN_APPROX_SIMPLE) cnt = contours[0] ellipse = cv.fitEllipse(cnt) cv.ellipse(img, ellipse, (255, 0, 0), 2) dst = cv.drawContours(img, [cnt], 0, (0, 255, 0), 1) cv.imshow("dst", dst) cv.waitKey(0) cv.destroyAllWindows()
9 拟合直线
import cv2 as cv import numpy as np import matplotlib.pyplot as plt if __name__ == '__main__': img = cv.imread('images/aaa.jpg') imgGray = cv.cvtColor(img, cv.COLOR_BGR2GRAY) _, thresh = cv.threshold(imgGray, 50, 255, 0) contours, hierarchy = cv.findContours(thresh, cv.RETR_TREE, cv.CHAIN_APPROX_SIMPLE) cnt = contours[0] rows, cols = img.shape[:2] [vx, vy, x, y] = cv.fitLine(cnt, cv.DIST_L2, 0, 0.01, 0.01) lefty = int((-x * vy / vx) + y) righty = int(((cols - x) * vy / vx) + y) cv.line(img, (cols - 1, righty), (0, lefty), (255, 0, 0), 2) dst = cv.drawContours(img, [cnt], 0, (0, 255, 0), 1) cv.imshow("dst", dst) cv.waitKey(0) cv.destroyAllWindows()
轮廓属性:
略
轮廓分层:
略
直方图
如何使用OpenCV和Numpy函数查找直方图,如何使用OpenCV和Matplotlib函数绘制直方图
cv.calcHist(),np.histogram()
什么是直方图:
您可以将直方图视为图形或绘图,从而可以总体了解图像的强度分布。它是在X轴上具有像素值(不总是从0到255的范围),在Y轴上具有图像中相应像素数的图
这只是理解图像的另一种方式。通过查看图像的直方图,您可以直观地了解该图像的对比度,亮度,强度分布等。当今几乎所有图像处理工具都提供直方图功能。
您可以看到图像及其直方图。(请记住,此直方图是针对灰度图像而非彩色图像绘制的)。直方图的左侧区域显示图像中较暗像素的数量,而右侧区域则显示明亮像素的数量。从直方图中,您可以看到暗区域多于亮区域,而中间调的数量(中间值的像素值,例如127附近)则非常少。
如何寻找直方图:
现在我们有了一个关于直方图的想法,我们可以研究如何找到它。OpenCV和Numpy都为此内置了功能。在使用这些功能之前,我们需要了解一些与直方图有关的术语。
BINS:上面的直方图显示每个像素值的像素数,即从0到255。即,您需要256个值来显示上面的直方图。但是考虑一下,如果您不需要分别找到所有像素值的像素数,而是找到像素值间隔中的像素数怎么办? 例如,您需要找到介于0到15之间的像素数,然后找到16到31之间,…,240到255之间的像素数。只需要16个值即可表示直方图。这就是在OpenCV教程中有关直方图的示例中显示的内容。
因此,您要做的就是将整个直方图分成16个子部分,每个子部分的值就是其中所有像素数的总和。 每个子部分都称为“ BIN”。在第一种情况下,bin的数量为256个(每个像素一个),而在第二种情况下,bin的数量仅为16个。BINS由OpenCV文档中的histSize术语表示。
DIMS:这是我们为其收集数据的参数的数量。在这种情况下,我们仅收集关于强度值的一件事的数据。所以这里是1。
RANGE:这是您要测量的强度值的范围。通常,它是[0,256]
,即所有强度值。
01 OpenCV 中的直方图计算:
因此,现在我们使用cv.calcHist()函数查找直方图。让我们熟悉一下该函数及其参数:cv.calcHist(images,channels,mask,histSize,ranges [,hist [,accumulate]])
- images:它是uint8或float32类型的源图像。它应该放在方括号中,即“ [img]”。
- channels:也以方括号给出。它是我们计算直方图的通道的索引。例如,如果输入为灰度图像,则其值为[0]。对于彩色图像,您可以传递[0],[1]或[2]分别计算蓝色,绿色或红色通道的直方图。
- mask:图像掩码。为了找到完整图像的直方图,将其指定为“无”。但是,如果要查找图像特定区域的直方图,则必须为此创建一个掩码图像并将其作为掩码。
- histSize:这表示我们的BIN计数。需要放在方括号中。对于全尺寸,我们通过[256]。
- ranges:这是我们的RANGE。通常为[0,256]。
因此,让我们从示例图像开始。只需以灰度模式加载图像并找到其完整直方图即可。
import cv2 as cv import numpy as np import matplotlib.pyplot as plt if __name__ == '__main__': img = cv.imread('images/0.jpg',cv.IMREAD_GRAYSCALE) hist = cv.calcHist([img], [0], None, [256], [0, 256]) # print(hist.shape) # (256, 1) # 每个值对应于该图像中具有相应像素值的像素数 # 绘制直方图 plt.plot(hist) plt.show()
02 Numpy中的直方图计算:
Numpy还为您提供了一个函数np.histogram()。因此,除了calcHist()函数外,您可以尝试下面的代码:
import cv2 as cv import numpy as np import matplotlib.pyplot as plt if __name__ == '__main__': img = cv.imread('images/0.jpg', cv.IMREAD_GRAYSCALE) # 1 # hist = cv.calcHist([img], [0], None, [256], [0, 256]) # # print(hist.shape) # (256, 1) # 每个值对应于该图像中具有相应像素值的像素数 # # 绘制直方图 # plt.plot(hist) # plt.show() # 2 # hist, bins = np.histogram(img.ravel(), 256, [0, 256]) # img.ravel() 的作用是降维 # # print(img.shape) # (1080, 1920) # # print(img.ravel().shape) # (2073600,) # plt.plot(hist) # plt.show() # 另外 Numpy还有另一个函数np.bincount(),它比np.histogram()快10倍左右,对于一维直方图,您可以更好地尝试一下 hist = np.bincount(img.ravel(), minlength=256) plt.plot(hist) plt.show()
如何绘制直方图:
两种方法:
1. 简洁有力的方法:使用Matplotlib绘图功能
2. 稍长的方法:使用OpenCV绘图功能(cv.line或cv.polyline函数,具体略)
import numpy as np import matplotlib.pyplot as plt if __name__ == '__main__': img = cv.imread('images/0.jpg', 0) plt.hist(img.ravel(), 256, [0,256]) # Matplotlib带有直方图绘图功能:matplotlib.pyplot.hist() 它直接找到直方图并将其绘制。您无需使用calcHist()或np.histogram()函数来查找直方图 plt.show()
掩码的使用:
import cv2 as cv import numpy as np import matplotlib.pyplot as plt if __name__ == '__main__': img = cv.imread('images/0.jpg', 0) # create a mask mask = np.zeros(img.shape[:2], np.uint8) mask[100:300, 100:400] = 255 masked_img = cv.bitwise_and(img, img, mask=mask) # 计算掩码区域和非掩码区域的直方图 # 检查作为掩码的第三个参数 hist_full = cv.calcHist([img], [0], None, [256], [0, 256]) hist_mask = cv.calcHist([img], [0], mask, [256], [0, 256]) plt.subplot(221), plt.imshow(img, 'gray') plt.subplot(222), plt.imshow(mask, 'gray') plt.subplot(223), plt.imshow(masked_img, 'gray') plt.subplot(224), plt.plot(hist_full), plt.plot(hist_mask) plt.xlim([0, 256]) plt.show()
直方图均衡:
将学习直方图均衡化的概念,并利用它来提高图像的对比度
直方图均衡化是图像处理领域中利用图像直方图对对比度进行调整的方法
概述:
这种方法通常用来增加许多图像的全局对比度,尤其是当图像的有用数据的对比度相当接近的时候。通过这种方法,亮度可以更好地在直方图上分布。这样就可以用于增强局部的对比度而不影响整体的对比度,直方图均衡化通过有效地扩展常用的亮度来实现这种功能。
这种方法对于背景和前景都太亮或者太暗的图像非常有用,这种方法尤其是可以带来X光图像中更好的骨骼结构显示以及曝光过度或者曝光不足照片中更好的细节。这种方法的一个主要优势是它是一个相当直观的技术并且是可逆操作,如果已知均衡化函数,那么就可以恢复原始的直方图,并且计算量也不大。这种方法的一个缺点是它对处理的数据不加选择,它可能会增加背景噪声的对比度并且降低有用信号的对比度。
均衡化前后的图像的直方图
例子:
下面的这个例子是一个8位的8×8灰度图像:
该灰度图像的灰度值出现次数如下表所示,为了简化表格,出现次数为0的值已经被省略
累积分布函数(cdf)如下所示,与上一表格类似,为了简化,累积分布函数值为0的灰度值已经被省略
代码:
import cv2 as cv import numpy as np import matplotlib.pyplot as plt if __name__ == '__main__': img = cv.imread('images/aaa.jpg', cv.IMREAD_GRAYSCALE) hist, bins = np.histogram(img.flatten(), 256, [0, 256]) cdf = hist.cumsum() # shape (256,1) cdf_normalized = cdf / cdf.max() * float(hist.max()) plt.plot(cdf_normalized, color='b') plt.hist(img.flatten(), 256, [0, 256], color='r') plt.xlim([0, 256]) plt.legend(('cdf', 'histogram'), loc='upper left') plt.show()
你可以看到直方图位于较亮的区域。我们需要全频谱。为此,我们需要一个转换函数,将亮区域的输入像素映射到整个区域的输出像素。这就是直方图均衡化的作用
import cv2 as cv import numpy as np import matplotlib.pyplot as plt if __name__ == '__main__': img = cv.imread('images/aaa.jpg', cv.IMREAD_GRAYSCALE) equ = cv.equalizeHist(img) plt.subplot(1, 2, 1), plt.imshow(img, 'gray') plt.title("img"), plt.xticks([]), plt.yticks([]) plt.subplot(1, 2, 2), plt.imshow(equ, 'gray') plt.title("equ"), plt.xticks([]), plt.yticks([]) plt.show()
当图像的直方图限制在特定区域时,直方图均衡化效果很好。在直方图覆盖较大区域(即同时存在亮像素和暗像素)的强度变化较大的地方,效果不好。
CLAHE 自适应直方图均衡
直方图均衡后,背景对比度确实得到了改善。但是在两个图像中比较雕像的脸。由于亮度过高,我们在那里丢失了大多数信息。这是因为它的直方图不像我们在前面的案例中所看到的那样局限于特定区域(尝试绘制输入图像的直方图,您将获得更多的直觉)。
因此,为了解决这个问题,使用了自适应直方图均衡。在这种情况下,图像被分成称为“tiles”的小块(在OpenCV中,tileSize默认为8x8
)。然后,像往常一样对这些块中的每一个进行直方图均衡。因此,在较小的区域中,直方图将限制在一个较小的区域中(除非存在噪声)。如果有噪音,它将被放大。为了避免这种情况,应用了对比度限制。如果任何直方图bin超出指定的对比度限制(在OpenCV中默认为40),则在应用直方图均衡之前,将这些像素裁剪并均匀地分布到其他bin。均衡后,要消除图块边界中的伪影,请应用双线性插值。
import cv2 as cv import numpy as np import matplotlib.pyplot as plt if __name__ == '__main__': img = cv.imread('images/aaa.jpg', cv.IMREAD_GRAYSCALE) equ = cv.equalizeHist(img) clahe = cv.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8)) cl1 = clahe.apply(img) plt.subplot(2, 2, 1), plt.imshow(img, 'gray') plt.title("img"), plt.xticks([]), plt.yticks([]) plt.subplot(2, 2, 2), plt.imshow(equ, 'gray') plt.title("equ"), plt.xticks([]), plt.yticks([]) plt.subplot(2, 2, 3), plt.imshow(cl1, 'gray') plt.title("cl1"), plt.xticks([]), plt.yticks([]) plt.show()
二维直方图:
学习查找和绘制2D直方图
在之前,我们计算并绘制了一维直方图。 之所以称为一维,是因为我们仅考虑一个特征,即像素的灰度强度值。
但是在二维直方图中,您要考虑两个特征是每个像素的色相(色调)和饱和度值。
import cv2 as cv import numpy as np import matplotlib.pyplot as plt if __name__ == '__main__': img = cv.imread('images/0.jpg') hsv = cv.cvtColor(img, cv.COLOR_BGR2HSV) hist = cv.calcHist([hsv], [0, 1], None, [180, 256], [0, 180, 0, 256]) # channel = [0,1],因为我们需要同时处理H和S平面 # bins = [180,256] 对于H平面为180,对于S平面为256 # channel = [0,1],因为我们需要同时处理H和S平面 # range = [0,180,0,256] 色相(调)值介于0和180之间,饱和度介于0和256之间 plt.imshow(hist, interpolation='nearest') # X轴显示S值,Y轴显示色相 plt.show()
import cv2 as cv import numpy as np import matplotlib.pyplot as plt if __name__ == '__main__': img = cv.imread('images/0.jpg') hsv = cv.cvtColor(img, cv.COLOR_BGR2HSV) # Numpy 中的二维直方图 np.histogram2d h s hist, xbins, ybins = np.histogram2d(hsv[...,0].ravel(), hsv[...,1].ravel(), [180, 256], [[0, 180], [0, 256]]) # 第一个参数是H平面,第二个是S平面,第三个是对于H平面为180,对于S平面为256,第四个是它们的范围。 plt.imshow(hist, interpolation='nearest') # X轴显示S值,Y轴显示色相 plt.show()
直方图反投影:
略
傅里叶变换
使用OpenCV查找图像的傅立叶变换
利用Numpy中可用的fft (快速傅立叶变换)函数
cv.dft(),cv.idft(),
dft: 离散傅里叶变换
它是一种线性积分变换,用于信号在时域(或空域)和频域之间的变换。
而又知:
任何连续的周期信号,都可以由一组适当的正弦曲线组合而成 ---傅里叶。
所以,任何连续的周期信号都是可以用频率来表示的,
相位:
不是同时开始的一组三角函数,在叠加时要体现开始的时间,
图像上的傅里叶变换
您可以将图像视为在两个方向上采样的信号。因此,在X和Y方向都进行傅立叶变换,可以得到图像的频率表示。更直观地说,对于正弦信号,如果幅度在短时间内变化如此之快,则可以说它是高频信号。如果变化缓慢,则为低频信号。您可以将相同的想法扩展到图像。图像中的振幅在哪里急剧变化?在边缘点或噪声。因此,可以说边缘和噪声是图像中的高频内容。如果幅度没有太大变化,则它是低频分量。
傅里叶变换得到低频和高频信息,低频针对的是图片的细节,高频针对的是图片的边界,我们对频率进行处理,最终会反映到逆变换后的图像上。
如何找到傅立叶变换:
Numpy进行傅立叶变换:
Numpy具有FFT软件包来执行此操作。np.fft.fft2()为我们提供了频率转换,它将是一个复杂的数组。
它的第一个参数是输入图像,即灰度图像。
第二个参数是可选的,它决定输出数组的大小。如果它大于输入图像的大小,则在计算FFT之前用零填充输入图像。如果小于输入图像,将裁切输入图像。如果未传递任何参数,则输出数组的大小将与输入的大小相同。
现在,一旦获得结果,零频率分量(DC分量)将位于左上角。如果要使其居中,则需要在两个方向上将结果都移动N2N2。只需通过函数np.fft.fftshift()即可完成。(它更容易分析)。找到频率变换后,就可以找到幅度谱。
import cv2 as cv import numpy as np import matplotlib.pyplot as plt if __name__ == '__main__': img = cv.imread('images/0.jpg',cv.IMREAD_GRAYSCALE) f = np.fft.fft2(img) # 傅里叶变换 # 返回值是一个复数的数组 # print(type(f),type(f[0,0])) # <class 'numpy.ndarray'> <class 'numpy.complex128'> fshift = np.fft.fftshift(f) # 将零频率分量移动到中心位置 magnitude_spectrum = 20 * np.log(np.abs(fshift)) # 调整值的范围 plt.subplot(121), plt.imshow(img, cmap='gray') plt.title('Input Image'), plt.xticks([]), plt.yticks([]) plt.subplot(122), plt.imshow(magnitude_spectrum, cmap='gray') plt.title('Magnitude Spectrum'), plt.xticks([]), plt.yticks([]) plt.show()
看,您可以在中心看到更多白色区域,这表明低频内容更多 .
Numpy 实现逆傅里叶变换
np.fft.ifft2() , np.fft.ifftshift()
import cv2 as cv import numpy as np import matplotlib.pyplot as plt if __name__ == '__main__': img = cv.imread('images/0.jpg',cv.IMREAD_GRAYSCALE) f = np.fft.fft2(img) # 傅里叶变换 # 返回值是一个复数的数组 fshift = np.fft.fftshift(f) # 将零频率分量移动到中心位置 # 逆变换回 图片 ishift = np.fft.ifftshift(fshift) iimg = np.fft.ifft2(ishift) iimg = np.abs(iimg) plt.subplot(121), plt.imshow(img, cmap='gray') plt.title('img'), plt.xticks([]), plt.yticks([]) plt.subplot(122), plt.imshow(iimg, cmap='gray') plt.title('iimg'), plt.xticks([]), plt.yticks([]) plt.show()
低频,高频
滤波
接收一定的频率
高通滤波器:允许高频通过,将会增强图片尖锐的细节
低通滤波器:允许低频通过 ,将会模糊图片
代码
1
import cv2 as cv import numpy as np import matplotlib.pyplot as plt if __name__ == '__main__': img = cv.imread('images/lena.jpg', cv.IMREAD_GRAYSCALE) row, col = img.shape crow, ccol = row // 2, col // 2 f = np.fft.fft2(img) # 傅里叶变换 # 返回值是一个复数的数组 fshift = np.fft.fftshift(f) # 将零频率分量移动到中心位置 # 去掉低频,将白色的去掉 fshift[crow - 30:crow + 30, ccol - 30: ccol + 30] = 0 # 逆变换回 图片 ishift = np.fft.ifftshift(fshift) iimg = np.fft.ifft2(ishift) iimg = np.abs(iimg) plt.subplot(121), plt.imshow(img, cmap='gray') plt.title('img'), plt.xticks([]), plt.yticks([]) plt.subplot(122), plt.imshow(iimg, cmap='gray') plt.title('iimg'), plt.xticks([]), plt.yticks([]) plt.show()
2
具体操作:
import cv2 as cv import numpy as np import matplotlib.pyplot as plt if __name__ == '__main__': img = cv.imread('images/lena.jpg', cv.IMREAD_GRAYSCALE) row, col = img.shape crow, ccol = row // 2, col // 2 mask = np.zeros((row, col), np.uint8) mask[crow - 30:crow + 30, ccol - 30:ccol + 30] = 1 f = np.fft.fft2(img) # 傅里叶变换 # 返回值是一个复数的数组 fshift = np.fft.fftshift(f) # 将零频率分量移动到中心位置 fshift = fshift * mask # 逆变换回 图片 ishift = np.fft.ifftshift(fshift) iimg = np.fft.ifft2(ishift) iimg = np.abs(iimg) plt.subplot(121), plt.imshow(img, cmap='gray') plt.title('img'), plt.xticks([]), plt.yticks([]) plt.subplot(122), plt.imshow(iimg, cmap='gray') plt.title('iimg'), plt.xticks([]), plt.yticks([]) plt.show()
模板匹配
如何使用模板匹配在图像中查找对象,cv.matchTemplate(),cv.minMaxLoc()
理论:
模板匹配是一种用于在较大图像中搜索和查找模板图像位置的方法。
为此,OpenCV带有一个函数cv.matchTemplate()。 它只是将模板图像滑动到输入图像上(就像在2D卷积中一样),然后在模板图像下比较模板和输入图像的拼图。
它返回一个灰度图像,其中每个像素表示该像素的邻域与模板匹配的程度。
如果输入图像的大小为(WxH)
,而模板图像的大小为(wxh)
,则输出图像的大小将为(W-w + 1,H-h + 1)
。得到结果后,可以使用cv.minMaxLoc()函数查找最大/最小值在哪。将其作为矩形的左上角,并以(w,h)
作为矩形的宽度和高度。该矩形是您模板的区域。
OpenCV中的模板匹配
我们在图1 中搜索车牌,
import cv2 as cv import numpy as np import matplotlib.pyplot as plt if __name__ == '__main__': img = cv.imread('images/aaa.jpg') imgGray = cv.cvtColor(img, cv.COLOR_BGR2GRAY) img2 = imgGray.copy() template = cv.imread('images/template.jpg', cv.IMREAD_GRAYSCALE) h, w = template.shape # 列表中所有的6种比较方法 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: temp = img2.copy() temp2 = img.copy() method = eval(meth) # 应用模板匹配 res = cv.matchTemplate(temp, template, method) min_val, max_val, min_loc, max_loc = cv.minMaxLoc(res) # 如果方法是TM_SQDIFF或TM_SQDIFF_NORMED,则取最小值 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(temp, top_left, bottom_right, (255, 255, 255), 2) cv.rectangle(temp2, top_left, bottom_right, (0, 255, 0), 2) plt.subplot(221), plt.imshow(res, cmap='gray') plt.title('Matching Result'), plt.xticks([]), plt.yticks([]) plt.subplot(222), plt.imshow(temp, cmap='gray') plt.title('Detected Point'), plt.xticks([]), plt.yticks([]) plt.subplot(223), plt.imshow(temp2[..., ::-1]) plt.title('Detected Point(color)'), plt.xticks([]), plt.yticks([]) plt.suptitle(meth) # 设置大标题 plt.show()
你会看到,使用cv.TM_CCORR的结果并不理想
多对象的模板匹配
在上面,我们在图像中搜索了车牌,该车牌在图像中仅出现一次。假设您正在搜索具有多次出现的对象,则cv.minMaxLoc()不会为您提供所有位置。在这种情况下,我们将使用阈值化。
因此,在此示例中,我们将使用著名游戏超级玛丽的屏幕截图,并在其中找到硬币
import cv2 as cv import numpy as np import matplotlib.pyplot as plt if __name__ == '__main__': img_rgb = cv.imread('mario.png') img_gray = cv.cvtColor(img_rgb, cv.COLOR_BGR2GRAY) template = cv.imread('mario_coin.png', 0) h, w = template.shape res = cv.matchTemplate(img_gray, template, cv.TM_CCOEFF_NORMED) threshold = 0.8 loc = np.where(res >= threshold) # 返回的是个元组,其中的元素是两个array,zip 之后就是对应的坐标了 for pt in zip(*loc[::-1]): cv.rectangle(img_rgb, pt, (pt[0] + w, pt[1] + h), (0, 0, 255), 2) cv.imwrite('res.png', img_rgb) # arr = np.array([[1,2],[3,4]]) # print(arr) # ret = np.where(arr>=2) # for point in zip(*ret): # print(point)
霍夫线变换
霍夫变换的概念。 如何使用它来检测图像中的线条。
cv.HoughLines(),cv.HoughLinesP()
理论:
霍夫变换是一种特征提取,被广泛应用在图像分析、计算机视觉以及数位影像处理。 霍夫变换是用来辨别找出物件中的特征,例如:线条。
它的演算流程大致如下,给定一个物件、要辨别的形状的种类,演算法会在参数空间中执行投票来决定物体的形状,而这是由累加空间(accumulator space)里的局部最大值来决定。
现在广泛使用的霍夫变换是由 Richard Duda 和 Peter Hart 在1972年发明,并称之为广义霍夫变换,广义霍夫变换和更早前1962年的Paul Hough 的专利有关 。 经典的霍夫变换是侦测图片中的直线,之后,霍夫变换不仅能识别直线,也能够识别任何形状,常见的有圆形、椭圆形。1981年,因为Dana H. Ballard 的一篇期刊论文 "Generalizing the Hough transform to detect arbitrary shapes",让霍夫变换开始流行于计算机视觉界。
在自动化分析数位图片的问题裡,其中一个常有的子问题是侦测某些简单的直线、圆形、椭圆形。在多数情况下,边缘侦测器(edge detector)会先用来做图片前处理,将原本的图片变成只含有边缘的图片。 因为图片的不完美或是边缘侦测的不完美,导致有些点(point)或像素(pixel)缺漏,或是有杂讯使得边缘侦测器所得的边界偏离了实际的边界。所以无法直观的将检测出的边缘分成直线、圆形、椭圆形的集合, 而霍夫变换解决上述问题,因为霍夫变换演算法中的投票步骤,在複杂的参数空间中找到图形的参数,电脑可以由参数得知该边缘(edge)是哪种形状。
推导:
最简单的霍夫变换是在图像中识别直线。在平面直角坐标系(x-y)中,一条直线可以用方程式表示, 是直线的截距,是直线的斜率。 而可以视为参数空间中的一点。当直线垂直于 x 轴时,斜率为无限大, 若用电脑数值计算时会很不方便,因此Duda 和 Hart 提出使用Hesse normal form来表示直线的参数.如下图:
是从原点到直线的距离,是和x 轴的夹角。利用参数空间 解决了原本参数空间 发散的问题, 进而能够比较每一个线段的参数,有人将平面称为二维直线的霍夫空间(Hough space)。这个表示方法让霍夫变换跟二维的雷登变换非常相似,可以说是一体两面 。给定一个点 ,通过该点的所有直线的参数的集合,会在平面上形成一个三角函数,可由下方程式证明:
因此,给定很多点,判断这些点是否共线(concurrent lines)的问题,经由霍夫变换之后,变成判断一堆曲线(每一个点在 平面上代表一条曲线)是否 在 平面上相交于同一点的问题(concurrent curves)。
注:下面的ρ和上面的r是一样的,
现在,让我们看一下霍夫变换如何处理线条。任何一条线都可以用(ρ,θ)这两个术语表示。因此,首先创建2D数组或累加器(以保存两个参数的值),并将其初始设置为0。让行表示ρ,列表示θ。阵列的大小取决于所需的精度。假设您希望角度的精度为1度,则需要180列。对于ρ,最大距离可能是图像的对角线长度。因此,以一个像素精度为准,行数可以是图像的对角线长度。
考虑一个100x100的图像,中间有一条水平线。
取直线的第一点。您知道它的(x,y)即(0,50)值。现在在线性方程式中( ρ=50sin(θ) ),将值θ= 0,1,2,..... 180放进去,然后检查得到ρ。对于每对(ρ,θ),在累加器中对应的(ρ,θ)单元格将值增加1。所以现在在累加器中,单元格(50,90)= 1以及其他一些单元格。
现在,对行的第二个点(1,50)。方程为 ρ=cos(θ) + 50sin(θ) 执行与上述相同的操作。
递增(ρ,θ)对应的单元格中的值。这次,单元格(50,90)=2。实际上,您正在对(ρ,θ)值进行投票。您对线路上的每个点都继续执行此过程。在每个点上,单元格(50,90)都会增加或投票,而其他单元格可能会或可能不会投票。这样一来,最后,单元格(50,90)的投票数将最高。因此,如果您在累加器中搜索最大票数,则将获得(50,90)值,该值表示该图像中的一条线与原点的距离为50,角度为90度。在下面的动画中很好地显示了这一投票过程:
这就是霍夫变换对线条的工作方式。它很简单,也许您可以自己使用Numpy来实现它。下图显示了累加器。某些位置的亮点表示它们是图像中可能的线条的参数。
opencv中使用霍夫线变换
上面说明的所有内容都封装在OpenCV函数cv.HoughLines()中。它只是返回一个:math:(rho,theta)
值的数组。ρ以像素为单位,θ以弧度为单位。第一个参数,输入图像应该是二进制图像,因此在应用霍夫变换之前,请应用阈值或使用Canny边缘检测。第二和第三参数分别是ρ和θ精度。
第四个参数是阈值,这意味着应该将其视为行的最低投票。请记住,票数取决于线上的点数。因此,它表示应检测到的最小线长。
import cv2 as cv import numpy as np import matplotlib.pyplot as plt if __name__ == '__main__': img = cv.imread(cv.samples.findFile('images/aaa.png')) plt.subplot(121), plt.imshow(img[..., ::-1]), plt.title("Origin"), plt.xticks([]), plt.yticks([]) gray = cv.cvtColor(img, cv.COLOR_BGR2GRAY) edges = cv.Canny(gray, 50, 150, apertureSize=3) lines = cv.HoughLines(edges, 1, np.pi / 180, 200) for line in lines: rho, theta = line[0] # ρ 和 θ a = np.cos(theta) b = np.sin(theta) x0 = a * rho y0 = b * rho x1 = int(x0 + 1000 * (-b)) # 从x0 ,y0 往两边延伸 y1 = int(y0 + 1000 * (a)) x2 = int(x0 - 1000 * (-b)) y2 = int(y0 - 1000 * (a)) cv.line(img, (x1, y1), (x2, y2), (0, 0, 255), 2) plt.subplot(122), plt.imshow(img[..., ::-1]), plt.title("HoughLines"), plt.xticks([]), plt.yticks([]) plt.show()
概率霍夫变换:
在霍夫变换中,您可以看到,即使对于带有两个参数的行,也需要大量计算。概率霍夫变换是我们看到的霍夫变换的优化。它没有考虑所有要点。取而代之的是,它仅采用随机的点子集,足以进行线检测。只是我们必须降低阈值。
OpenCV的实现基于Matas,J.和Galambos,C.和Kittler, J.V.使用渐进概率霍夫变换对行进行的稳健检测。使用的函数是cv.HoughLinesP()。
minLineLength 最小行长。小于此长度的线段将被拒绝。
maxLineGap 线段之间允许将它们视为一条线的最大间隙。
最好的是,它直接返回行的两个端点。在以前的情况下,您仅获得线的参数,并且必须找到所有点。在这里,一切都是直接而简单的。
import cv2 as cv import numpy as np import matplotlib.pyplot as plt if __name__ == '__main__': img = cv.imread(cv.samples.findFile('images/aaa.jpg')) plt.subplot(121), plt.imshow(img[..., ::-1]), plt.title("Origin"), plt.xticks([]), plt.yticks([]) gray = cv.cvtColor(img, cv.COLOR_BGR2GRAY) edges = cv.Canny(gray, 50, 150, apertureSize=3) lines = cv.HoughLinesP(edges, 1, np.pi / 180, 100, minLineLength=100, maxLineGap=10) for line in lines: x1, y1, x2, y2 = line[0] cv.line(img, (x1, y1), (x2, y2), (0, 0, 255), 2) plt.subplot(122), plt.imshow(img[..., ::-1]), plt.title("HoughLines"), plt.xticks([]), plt.yticks([]) plt.show()
import cv2 as cv import numpy as np import matplotlib.pyplot as plt if __name__ == '__main__': img = cv.imread(cv.samples.findFile('images/aaa.jpg')) plt.subplot(121), plt.imshow(img[..., ::-1]), plt.title("Origin"), plt.xticks([]), plt.yticks([]) gray = cv.cvtColor(img, cv.COLOR_BGR2GRAY) edges = cv.Canny(gray, 50, 150, apertureSize=3) lines = cv.HoughLinesP(edges, 1, np.pi / 180, 100, minLineLength=100, maxLineGap=50) for line in lines: x1, y1, x2, y2 = line[0] cv.line(img, (x1, y1), (x2, y2), (0, 0, 255), 2) plt.subplot(122), plt.imshow(img[..., ::-1]), plt.title("HoughLinesP"), plt.xticks([]), plt.yticks([]) plt.show()
霍夫圆变换
如何使用霍夫变换来查找图像中的圆
cv.HoughCircles()
圆在数学上表示为,其中是圆的中心,r 是圆的半径。从等式中,我们可以看到我们有3个参数,因此我们需要3D累加器进行霍夫变换,这将非常低效。
因此,OpenCV使用更加技巧性的方法,即使用边缘的梯度信息的Hough梯度方法。
import cv2 as cv import numpy as np import matplotlib.pyplot as plt if __name__ == '__main__': img = cv.imread('images/logo.png') grayImg = cv.cvtColor(img, cv.COLOR_BGR2GRAY) grayImg = cv.medianBlur(grayImg, 5) circles = cv.HoughCircles(grayImg, cv.HOUGH_GRADIENT, 1, 20, param1=50, param2=30, minRadius=0, maxRadius=0) circles = np.uint16(np.around(circles)) for i in circles[0, :]: # 绘制外圆 cv.circle(img, (i[0], i[1]), i[2], (0, 255, 0), 2) # 绘制圆心 cv.circle(img, (i[0], i[1]), 2, (0, 0, 255), 3) cv.imshow('detected circles', img) cv.waitKey(0) cv.destroyAllWindows()
图像分割与分水岭(Watershed)算法
如何使用分水岭算法实现基于标记的图像分割
cv.watershed()
理论:
任何灰度图像都可以看作是一个地形表面,其中高强度表示山峰,低强度表示山谷。你开始用不同颜色的水(标签)填充每个孤立的山谷(局部最小值)。随着水位的上升,根据附近的山峰(坡度),来自不同山谷的水明显会开始合并。为了避免这种情况,你要在水融合的地方建造屏障。你继续填满水,建造障碍,直到所有的山峰都在水下。然后你创建的屏障将返回你的分割结果。这就是Watershed背后的“思想”。详情参考: http://www.cmm.mines-paristech.fr/~beucher/wtshed.html
但是这种方法会由于图像中的噪声或其他不规则性而产生过度分割的结果。
如下图:
因此OpenCV实现了一个基于标记 的分水岭算法,你可以指定哪些是要合并的山谷点,哪些不是。
这是一个交互式的图像分割。我们所做的是给我们知道的对象赋予不同的标签。用一种颜色(或强度)标记我们确定为前景或对象的区域,用另一种颜色标记我们确定为背景或非对象的区域,最后用0
标记我们不确定的区域。这是我们的标记。然后应用分水岭算法。然后我们的标记将使用我们给出的标签进行更新,对象的边界值将为-1
代码:
import cv2 as cv import numpy as np import matplotlib.pyplot as plt if __name__ == '__main__': img = cv.imread('images/water_coins.jpg') gray = cv.cvtColor(img, cv.COLOR_BGR2GRAY) ret, thresh = cv.threshold(gray, 0, 255, cv.THRESH_BINARY_INV + cv.THRESH_OTSU) cv.imshow("img1",thresh) # 噪声去除 kernel = np.ones((3, 3), np.uint8) opening = cv.morphologyEx(thresh, cv.MORPH_OPEN, kernel, iterations=2) # 寻找前景区域 dist_transform = cv.distanceTransform(opening, cv.DIST_L2, 5) ret, sure_fg = cv.threshold(dist_transform, 0.7 * dist_transform.max(), 255, 0) cv.imshow("img2",sure_fg) # print(type(sure_fg[0,0])) # numpy.float32 因此下面会进行转换 # 确定背景区域 sure_bg = cv.dilate(opening, kernel, iterations=3) cv.imshow("img3",sure_bg) # 找到未知区域 sure_fg = np.uint8(sure_fg) unknown = cv.subtract(sure_bg, sure_fg) cv.imshow("img4",unknown) cv.waitKey(0) cv.destroyAllWindows()
import cv2 as cv import numpy as np import matplotlib.pyplot as plt if __name__ == '__main__': img = cv.imread('images/water_coins.jpg') gray = cv.cvtColor(img, cv.COLOR_BGR2GRAY) ret, thresh = cv.threshold(gray, 0, 255, cv.THRESH_BINARY_INV + cv.THRESH_OTSU) # cv.imshow("img1",thresh) # 噪声去除 kernel = np.ones((3, 3), np.uint8) opening = cv.morphologyEx(thresh, cv.MORPH_OPEN, kernel, iterations=2) # 寻找前景区域 dist_transform = cv.distanceTransform(opening, cv.DIST_L2, 5) ret, sure_fg = cv.threshold(dist_transform, 0.7 * dist_transform.max(), 255, 0) # cv.imshow("img2",sure_fg) # print(type(sure_fg[0,0])) # numpy.float32 因此下面会进行转换 # 确定背景区域 sure_bg = cv.dilate(opening, kernel, iterations=3) # cv.imshow("img3",sure_bg) # 找到未知区域 sure_fg = np.uint8(sure_fg) unknown = cv.subtract(sure_bg, sure_fg) # cv.imshow("img4",unknown) # 类别标记 ret, markers = cv.connectedComponents(sure_fg) # 它用0标记图像的背景,然后其他对象用从1开始的整数标记 # 现在让所有的未知区域为0,保证背景的标记是0 markers[unknown == 255] = 0 # 使用分水岭算法。然后标记图像将被修改。边界区域将标记为 - 1 markers = cv.watershed(img, markers) # print(markers[:100,:100]) img[markers == -1] = [255, 0, 0] cv.imshow("final", img) cv.waitKey(0) cv.destroyAllWindows()