您的当前位置:首页正文

【python】OpenCV—Blur, Threshold, Gradient, Morphology(2)

2024-11-08 来源:个人技术集锦

系列文章



0 import

import cv2
import numpy as np
import matplotlib.pyplot as plt

%matplotlib inline

1 滤波

学习整理

对于2D图像可以进行低通或者高通滤波操作,低通滤波(LPF)有利于去噪,模糊图像,高通滤波(HPF)有利于找到图像边界。

1.1 统一的滤波器

cv2.filter2D

1.2 模糊(Blurring)

  • 均值模糊(Averaging blurring):cv2.blur
  • 高斯模糊(Gaussian blurring):cv2.GaussianBlur
  • 中值模糊(median blurring):cv2.medianBlur
  • 双边滤波(bilateral filtering):cv2.bilateralFilter,是高斯模糊的一个高级版本。模糊化不仅可以溶解噪声,而且还会平滑边缘。而双边滤波器能在去除噪声的同时保持边缘锐化。这是由于它不仅使用高斯分布值,还同时考虑了距离和像素值的差异。因此,需要指定 sigmaSpace 和 sigmaColor 这两个参数。
img = cv2.imread('C://Users/13663//Desktop/1.jpg')
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
# Plot the image with different kernel sizes
kernels = [3, 5, 11, 17]
fig, axs = plt.subplots(nrows = 1, ncols = 4, figsize = (20, 20))
for ind, s in enumerate(kernels):
    img_blurred = cv2.blur(img, ksize = (s, s))
    ax = axs[ind]
    ax.set_title('kernel {i}'.format(i=s),
             fontsize =24, color = 'white')
    ax.imshow(img_blurred)
    ax.axis('off')
plt.show()

img = cv2.imread('C://Users/13663//Desktop/1.jpg')
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)

# Plot the image with different kernel sizes
kernels = [3, 5, 11, 17]
fig, axs = plt.subplots(nrows = 2, ncols = 2, figsize = (15, 9))
for ind, s in enumerate(kernels):
    img_blurred = cv2.blur(img, ksize = (s, s))
    ax = axs[ind//2][ind%2]
    ax.set_title('kernel {i}'.format(i=s),
             fontsize =24, color = 'white')
    ax.imshow(img_blurred)
    ax.axis('off')
plt.show()

img = cv2.imread('C://Users/13663//Desktop/1.jpg')
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)

# Blur the image 
img_0 = cv2.blur(img, ksize = (7, 7))
img_1 = cv2.GaussianBlur(img, ksize = (7, 7), sigmaX = 0)   
img_2 = cv2.medianBlur(img, 7)
img_3 = cv2.bilateralFilter(img, 7, sigmaSpace = 75, sigmaColor =75)

# Plot the images
images = [img_0, img_1, img_2, img_3]
names = ['Mean Blur','Gaussian Blur','Median Blur','Bilateral Filter']
fig, axs = plt.subplots(nrows = 2, ncols = 2, figsize = (15, 9))
num = 0
for ind, p in enumerate(images):
    ax = axs[ind//2][ind%2]
    ax.set_title('{x}'.format(x=names[num]),
             fontsize =20, color = 'white')
    ax.imshow(p)
    ax.axis('off')
    num+=1
plt.show()

2 阈值化(Thresholding)

2.1 固定的 Threshold

图像的阈值化就是利用图像像素点分布规律,设定阈值进行像素点分割,进而得到图像的二值图像。我们需要设置阈值和最大值,然后据此相应地进行像素值转换。核心函数如下:

ret, dst = cv2.threshold(src, thresh, maxval, type)

  • src: 输入图,只能输入单通道图像,通常来说为灰度图
  • dst: 输出图
  • thresh: 阈值
  • maxval: 当像素值超过了阈值(或者小于阈值,根据type来决定),所赋予的值
  • type:二值化操作的类型,包含以下5种类型:
    • 二进制阈值化:cv2.THRESH_BINARY
    • 反二进制阈值化:cv2.THRESH_BINARY_INV
    • 阈值化到零:cv2.THRESH_TOZERO
    • 反阈值化到零:cv2.THRESH_TOZERO_INV
    • 阈值截断:cv2.THRESH_TRUNC

也可以通过 BIF 查看,eg:help(cv2.threshold),返回的 ret 经测试发现同 thresh

img = cv2.imread('C://Users/13663//Desktop/5.png')
#img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# Thresholding 
_, thresh_0 = cv2.threshold(img, 127, 255, cv2.THRESH_BINARY)
_, thresh_1 = cv2.threshold(img, 127, 255, cv2.THRESH_TOZERO)
_, thresh_2 = cv2.threshold(img, 127, _, cv2.THRESH_TRUNC)
_, thresh_3 = cv2.threshold(img, 127, 255, cv2.THRESH_BINARY_INV)
_, thresh_4 = cv2.threshold(img, 127, 255, cv2.THRESH_TOZERO_INV)

names = ['original image','binary T','zero T','trunc T','binary inv T','zero inv T']  
# Plot the images
images = [img, thresh_0, thresh_1, thresh_2, thresh_3, thresh_4]
fig, axs = plt.subplots(nrows = 2, ncols = 3, figsize = (13, 6))
for ind, p in enumerate(images):
    ax = axs[ind//3, ind%3]
    ax.set_title(names[ind],fontsize=14, color='white')
    ax.imshow(p,cmap='gray')
    ax.axis("off")
plt.show()

下面看看比较轻松的例子

img = cv2.imread('C://Users/13663//Desktop/3.png')
img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

# Thresholding 
_, thresh_0 = cv2.threshold(img, 127, 255, cv2.THRESH_BINARY)
_, thresh_1 = cv2.threshold(img, 127, 255, cv2.THRESH_TOZERO)
_, thresh_2 = cv2.threshold(img, 127, 255, cv2.THRESH_TRUNC)
_, thresh_3 = cv2.threshold(img, 127, 255, cv2.THRESH_BINARY_INV)
_, thresh_4 = cv2.threshold(img, 127, 255, cv2.THRESH_TOZERO_INV)

names = ['original image','binary T','zero T','trunc T','binary inv T','zero inv T']  

# Plot the images
images = [img, thresh_0, thresh_1, thresh_2, thresh_3, thresh_4]
fig, axs = plt.subplots(nrows = 2, ncols = 3, figsize = (13, 6))
for ind, p in enumerate(images):
    ax = axs[ind//3, ind%3]
    ax.set_title(names[ind],fontsize=14, color='white')
    ax.imshow(p,cmap='gray')
    ax.axis("off")
plt.show()

2.2 Adaptive thresholding

只取一个阈值并将其应用于图像的所有部分并不能满足我们的全部需求。如果我们有一张在多个不同区域亮度差异较多的图片这种情况,将一个值应用于整个图像一般不利于我们的图像处理任务。其对应更好的方法是对图像的每个部分使用不同的阈值。对应这种情况还有另外一种阈值化技术称为自适应阈值化(Adaptive thresholding)。通过对图像邻域内阈值的计算,可以得到不同光照条件下的较好结果。

核心函数 dst = cv2.adaptiveThreshold(src, maxval, thresh_type, type, Block Size, C)

  • src: 输入图,只能输入单通道图像,通常来说为灰度图
  • dst: 输出图
  • maxval: 当像素值超过了阈值(或者小于阈值,根据type来决定),所赋予的值
  • thresh_type: 阈值的计算方法,包含以下2种类型:
    • cv2.ADAPTIVE_THRESH_MEAN_C 领域内均值
    • cv2.ADAPTIVE_THRESH_GAUSSIAN_C 领域内像素点加权和,权重为一个高斯窗口
  • type:二值化操作的类型,与固定阈值函数相同,包含以下5种类型:
    • cv2.THRESH_BINARY
    • cv2.THRESH_BINARY_INV
    • cv2.THRESH_TRUNC
    • cv2.THRESH_TOZERO
    • cv2.THRESH_TOZERO_INV
  • Block Size: 图片中分块的大小
  • C :阈值计算方法中的常数项,代表从均值或加权均值中减去值的大小

for example

# Convert the image to grayscale
img = cv2.imread('C://Users/13663//Desktop/3.png')
img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# Adaptive Thresholding
_, thresh_binary = cv2.threshold(img, thresh = 127, maxval = 255, type = cv2.THRESH_BINARY)
adap_mean_2 = cv2.adaptiveThreshold(img, 255, 
                                    cv2.ADAPTIVE_THRESH_MEAN_C, 
                                    cv2.THRESH_BINARY, 7, 2)
adap_mean_2_inv = cv2.adaptiveThreshold(img, 255, 
                                        cv2.ADAPTIVE_THRESH_MEAN_C, 
                                        cv2.THRESH_BINARY_INV, 7, 2)
adap_mean_8 = cv2.adaptiveThreshold(img, 255, 
                                    cv2.ADAPTIVE_THRESH_MEAN_C, 
                                    cv2.THRESH_BINARY, 7, 8)
adap_gaussian_8 = cv2.adaptiveThreshold(img, 255, 
                                    cv2.ADAPTIVE_THRESH_GAUSSIAN_C, 
                                    cv2.THRESH_BINARY, 7, 8)
# Plot the images
images = [img, thresh_binary, adap_mean_2, adap_mean_2_inv, 
          adap_mean_8, adap_gaussian_8]
names = ['img', 'thresh_binary', 'adap_mean_2', 'adap_mean_2_inv', 
         'adap_mean_8', 'adap_gaussian_8']
fig, axs = plt.subplots(nrows = 2, ncols = 3, figsize = (15, 8))
for ind, p in enumerate(images):
    ax = axs[ind%2, ind//2]
    ax.set_title(names[ind],fontsize=18,color='white')
    ax.imshow(p, cmap = 'gray')
    ax.axis('off')
plt.show()

3 梯度(Gradient)

sobellaplace 为例

# Convert the image to grayscale
img = cv2.imread('C://Users/13663//Desktop/1.jpg')
img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# Apply gradient filtering
sobel_x = cv2.Sobel(img, cv2.CV_64F, dx = 1, dy = 0, ksize = 5)
sobel_y = cv2.Sobel(img, cv2.CV_64F, dx = 0, dy = 1, ksize = 5)
blended = cv2.addWeighted(src1=sobel_x, alpha=0.5, src2=sobel_y,
                          beta=0.5, gamma=0)
laplacian = cv2.Laplacian(img, cv2.CV_64F)
# Plot the images
images = [sobel_x, sobel_y, blended, laplacian]
names = ['sobel_x', 'sobel_y', 'blended', 'laplacian']
plt.figure(figsize = (14, 10))
for i in range(4):
    plt.subplot(2, 2, i+1)
    plt.imshow(images[i], cmap = 'gray')
    plt.title(names[i],fontsize=20,color='white')
    plt.axis('off')
plt.show()

看看更多的例子



4 形态转换(Morpgological transformations)

涉及到腐蚀(erosion)、膨胀(dilation)、开(open)、闭(close)运算

4.1 腐蚀(Erosion)



原图

img = cv2.imread('C://Users/13663//Desktop/6.png')
img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

# Create erosion kernels 
kernel_0 = np.ones((3, 3), np.uint8)
kernel_1 = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))
kernel_2 = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3, 3))
kernel_3 = cv2.getStructuringElement(cv2.MORPH_CROSS, (3, 3))

kernels = [kernel_0, kernel_1, kernel_2, kernel_3]
names = ['ones','rect','ellipse','cross']

# Plot the images
plt.figure(figsize = (10, 9))
for i in range(4):
    img_copy = img.copy()
    img_copy = cv2.erode(img_copy, kernels[i], iterations = 1)
    plt.subplot(2, 2, i+1)
    plt.title(names[i],fontsize=18,color='white')
    plt.imshow(img_copy,cmap='gray')
    plt.axis('off')
plt.show()

cv2.getStructuringElement 有三种

  • 矩形: cv2.MORPH_RECT
  • 椭圆形:cv2.MORPH_ELLIPSE
  • 交叉形:cv2.MORPH_CROSS

迭代一次后的效果,腐蚀,可以理解为把像素腐蚀掉了(变黑了——变0了),one 和 rect 效果一样

迭代两次的结果,可以感受三者之间的不同了

迭代三次的结果,总感觉椭圆和交叉形效果差不多,可能是示例的原因!

参考文章中的例子如下,要典型一些,哈哈哈!

4.2 膨胀(dilation)

img = cv2.imread('C://Users/13663//Desktop/6.png')
img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# Apply dilation
kernel = np.ones((3, 3), np.uint8)
img_dilate = cv2.dilate(img, kernel, iterations = 1)
plt.figure(figsize = (20, 10))
plt.subplot(1, 2, 1); plt.imshow(img, cmap="gray")
plt.axis('off')
plt.subplot(1, 2, 2); plt.imshow(img_dilate, cmap="gray")
plt.axis('off')
plt.show()

4.3 开(open)、闭(close)运算

  • cv2.MORPH_OPEN:先腐蚀,后膨胀
  • cv2.MORPH_CLOSE:先膨胀,后腐蚀
  • cv2.MORPH_GRADIENT:计算膨胀结果图与腐蚀结果图之差
  • cv2.MORPH_TOPHAT:顶帽,开运算结果图与原始图像之差
  • cv2.MORPH_BLACKHAT:黑帽,闭运算结果图与原始图像之差
img = cv2.imread('C://Users/13663//Desktop/4.png')
img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# Apply the operations
kernel = np.ones((3, 3), np.uint8)
img_open = cv2.morphologyEx(img, op=cv2.MORPH_OPEN, kernel=kernel)
img_close = cv2.morphologyEx(img, op=cv2.MORPH_CLOSE, kernel=kernel)
img_grad = cv2.morphologyEx(img, op=cv2.MORPH_GRADIENT, kernel=kernel)
img_tophat = cv2.morphologyEx(img, op=cv2.MORPH_TOPHAT, kernel=kernel)
img_blackhat = cv2.morphologyEx(img, op=cv2.MORPH_BLACKHAT, kernel=kernel)
# Plot the images
images = [img, img_open, img_close, img_grad, 
          img_tophat, img_blackhat]
names = ['img', 'img_open', 'img_close', 'img_grad', 
         'img_tophat', 'img_blackhat']
fig, axs = plt.subplots(nrows = 2, ncols = 3, figsize = (15, 10))
for ind, p in enumerate(images):
    ax = axs[ind//3, ind%3]
    ax.set_title(names[ind],fontsize=18,color='white')
    ax.imshow(p, cmap = 'gray')
    ax.axis('off')
plt.show()






4.4 礼帽、黑帽

在数字图像处理中,形态学变换是一种强大的工具,用于处理和分析图像的结构。礼帽(Top Hat)和黑帽(Black Hat)运算是形态学变换中的两种重要操作,它们通常用于分离图像中的特定区域或细节。

礼帽(Top Hat)运算

  • 基本原理:
    • 礼帽运算(也称为顶帽运算)是原始图像与其开运算结果之差
    • 开运算通常用于消除小物体、在纤细点分离物体,并平滑较大物体的边界。
    • 礼帽运算的结果突出了比原图轮廓周围的区域更明亮的区域,这些区域可能是噪声、亮点或其他感兴趣的特征。
  • 应用:
    • 礼帽运算常用于分离比邻近点亮一些的斑块,可以用于提取背景或突出图像中的明亮区域

黑帽(Black Hat)运算

  • 基本原理:
    • 黑帽运算是原始图像的闭运算结果与其原始图像之差
    • 闭运算通常用于弥合较窄的间断、消除小的孔洞,并填补轮廓线中的断裂。
    • 黑帽运算的结果突出了比原图轮廓周围区域更暗的区域,这些区域可能是暗斑、阴影或其他感兴趣的特征。
  • 应用:
    • 黑帽运算常用于分离比邻近点暗一些的斑块,可以用于提取图像中的暗部细节或阴影

总结

  • 礼帽和黑帽运算都是基于图像的开运算和闭运算进行的,但它们在处理和分析图像时的侧重点不同。
  • 礼帽运算关注于图像中的明亮区域,而黑帽运算则关注于图像中的暗部细节
  • 这两种运算在图像处理中有广泛的应用,如噪声滤除、特征提取、边缘检测等。

在实际使用中,选择合适的运算方法取决于具体的图像处理需求和应用场景。

代码实现

import cv2

gray = cv2.cvtColor(cv2.imread("3.jpg"), cv2.COLOR_BGR2GRAY)

kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (35, 8))

"""
[[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 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 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 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 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 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 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]
 [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 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 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 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 1 1 1 1 1]]
"""

tophat = cv2.morphologyEx(gray.copy(), cv2.MORPH_TOPHAT, kernel)
cv2.imwrite("tophat.jpg", tophat)


kernel1 = cv2.getStructuringElement(cv2.MORPH_RECT, (23, 5))
"""
[[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 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 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 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 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1]]
"""

blackhat = cv2.morphologyEx(gray.copy(), cv2.MORPH_BLACKHAT, kernel1)
cv2.imwrite("blackhat.jpg", blackhat)

展示个比较直观的例子,素材来自

先看礼帽

再看看黑帽

下面再看一个稍微复杂点的例子

原始图片

礼帽运算

较亮的区域保留,车牌部分

再看看黑帽运算

车牌号码上的黑字被突出

5 连通区域

"""
动态规划
求图像中8连通域的个数,并计算最大连通域
"""
import cv2
import numpy as np
import sys
import threading

sys.setrecursionlimit(500000000)


def initImage(img_sz=(640,640)):
    """初始化图像
    :param img_sz:
    :return:
    """
    img = np.zeros(img_sz, dtype=np.uint8)  # 单通道
    circles = [[5, 5, 5], [10, 10, 5], [100, 100, 10], [200, 200, 50],
               [300, 300, 20], [400, 400, 120], [500, 500, 8], [450, 0, 100], [200, 0, 30]]
    for circle in circles:
        cx, cy, r = circle
        cv2.circle(img, (cx, cy), r, 255, -1)  # 白色实心圆
    show_image('org_img', img)
    return img


def show_image(window_name, img, show_bool=1):
    cv2.namedWindow(window_name, 0)
    cv2.imshow(window_name, img)
    cv2.waitKey(0)
    if show_bool:
        cv2.destroyAllWindows()


def getNumOfConnections(img, regions_total):
    """求图像的8连通域个数,并计算每个连通域的像素个数
    :param img:
    :param regions_total:
    :return:
    """
    h, w = img.shape
    label = np.zeros((h, w), dtype=np.uint8)  # 未被遍历过为 0, 遍历过了为 1
    # 遍历所有行和列
    # regions_total = []
    for i in range(h):
        for j in range(w):
            if label[i][j] > 0:
                continue
            temp_regions = []  # 记录连通的区域坐标
            get8Connections(img, label, i, j, temp_regions)
            if len(temp_regions):
                regions_total.append(temp_regions)
    # return regions_total


def get8Connections(img, label, i, j, regions):
    """递归求某一个点的 8 连通域集合,集合元素为点的坐标
    :param img:
    :param label:
    :param i:
    :param j:
    :param regions:
    :return:
    """
    h1, w1 = img.shape
    if i < 0 or i >= h1 or j < 0 or j >= w1:  # 终止条件,不能越界
        return None
    if label[i][j] > 0:  # 该像素是不能被遍历过的
        return None
    if img[i][j] == 0:  # 像素的值大于0(有效的白色区域)
        label[i][j] = 1
        return None

    label[i][j] = 1
    if img[i][j] > 0:
        regions.append([i, j])
    # 8连通域
    for r_bias in range(-1, 2):
        for l_bias in range(-1, 2):
            get8Connections(img, label, i+r_bias, j+l_bias, regions)


if __name__ == '__main__':
    img = initImage()
    regions_total = []
    threading.stack_size(200000000)
    thread1 = threading.Thread(target=getNumOfConnections, args=(img, regions_total,))
    thread1.start()
    thread1.join()
    # regions = getNumOfConnections(img)

    print('regions_len = ', len(regions_total))  # regions_len = 8
    #print(regions_total)


参考

Top