图像形态学操作是图像处理中的一种重要技术,主要用于处理二值图像(即黑白图像)。

        OpenCV 中的图像形态学操作是图像处理中的重要工具,通过腐蚀、膨胀、开运算、闭运算和形态学梯度等操作,可以实现对图像的噪声去除、对象分离、边缘检测等效果。掌握这些操作有助于更好地处理和分析图像数据。

        形态学操作是图像处理中基于形状的一系列操作,它们通常应用于二值图像或灰度图像,并且主要用于提取图像中的特定形状。这些操作依赖于结构元素(也称为核),它是一个较小的矩阵,用于探测和改变图像的局部区域。下面介绍腐蚀、膨胀、开运算、闭运算以及形态学梯度等操作的简单应用和底层原理。

原图像如下所示

本文所参考的其他作者的博客将附在文末!!! 

以下是 OpenCV 中常用的形态学操作及其函数:

操作 函数 说明 应用场景
腐蚀 cv2.erode() 用结构元素扫描图像,如果结构元素覆盖的区域全是前景,则保留中心像素。 去除噪声、分离物体。
膨胀 cv2.dilate() 用结构元素扫描图像,如果结构元素覆盖的区域存在前景,则保留中心像素。 连接断裂的物体、填充空洞。
开运算 cv2.morphologyEx() 先腐蚀后膨胀。 去除小物体、平滑物体边界。
闭运算 cv2.morphologyEx() 先膨胀后腐蚀。 填充小孔洞、连接邻近物体。
形态学梯度 cv2.morphologyEx() 膨胀图减去腐蚀图。 提取物体边缘。
顶帽运算 cv2.morphologyEx() 原图减去开运算结果。 提取比背景亮的细小物体。
黑帽运算 cv2.morphologyEx() 闭运算结果减去原图。 提取比背景暗的细小物体。

腐蚀和膨胀

侵蚀和膨胀是最基本的形态学作是。它们具有广泛的用途,即:

1.去除噪声

2.隔离图像中的单个元素并连接不同的元素。

3.在图像中发现强度凸起或孔洞

腐蚀 (Erosion)

        腐蚀操作通过减少图像中的前景区域(白色部分)来缩小物体。具体来说,腐蚀操作会遍历整个图像,并使用一个结构元素与图像中的每个像素进行比较。如果结构元素覆盖的所有像素都属于前景,则该位置的像素保留为前景;否则,该位置的像素将被设置为背景(通常是黑色)。因此,腐蚀可以去除小的对象、断开连接的物体以及平滑物体边界

数学上,腐蚀可以用以下公式表示:

(A⊖B)(x,y)=min⁡{A(x+i,y+j)−B(i,j)}(A⊖B)(x,y)=min{A(x+i,y+j)−B(i,j)}

其中 A 是输入图像,B 是结构元素,i,j 表示结构元素中的坐标。

函数原型: cv2.erode(src, kernel, iterations=1)

src: 输入图像,通常是二值图像。

kernel: 结构元素,可以自定义或使用 cv2.getStructuringElement() 生成。

iterations: 腐蚀操作的次数,默认为1。

名词解释

        “结构元素是形态学变换中的基本元素,是为了探测图像的某种结构信息而设计特定形状和尺寸的图像,称为收集图像结构信息的探针。结构元素有多种类型,如圆形、方形、线形等,可携带知识(形态、大小、灰度和色度信息)来探测、研究图像的结构特点。结构元素是膨胀和腐蚀操作的最基本组成部分,用于测试输出图像,通常要比待处理的图像小很多,二维平面结构元素由一个数值为0或1的矩阵组成。结构元素的原点指定了图像中需要处理的像素范围,结构元素中数值为1的点决定结构元素的领域像素进行膨胀或腐蚀操作时是否需要参与计算。 ”它可以具有任何形状或大小,通常是正方形或圆形。

import cv2
import numpy as np

image = cv2.imread("D:\WPS Office\cartoon_nezha.jpg", 0)        # 读取图像
kernel = np.ones((5,5), np.uint8)             # 定义结构元素
eroded_image = cv2.erode(image, kernel, iterations=1)       # 腐蚀操作
# 显示结果
cv2.imshow('Eroded Image', eroded_image)
cv2.waitKey(0)
cv2.destroyAllWindows()

腐蚀操作会使图像中的前景对象变小,边缘被腐蚀掉,常用于去除噪声或分离连接的对象。 

膨胀 (Dilation)

        膨胀与腐蚀相反,它通过增加图像中的前景区域来扩大物体。膨胀操作同样遍历整个图像,但这次如果结构元素覆盖的区域内有任何像素属于前景,则该位置的像素将被设置为前景。这意味着膨胀可以填充孔洞、连接邻近的物体以及扩大物体。

膨胀的数学定义如下:

(A⊕B)(x,y)=max⁡{A(x−i,y−j)+B(i,j)}(A⊕B)(x,y)=max{A(x−i,y−j)+B(i,j)}

其中 A 是输入图像,B 是结构元素,i,j 表示结构元素中的坐标。

函数原型: cv2.dilate(src, kernel, iterations=1)

src: 输入图像,通常是二值图像。

kernel: 结构元素,可以自定义或使用 cv2.getStructuringElement() 生成。

iterations: 膨胀操作的次数,默认为1。

import cv2
import numpy as np

# 读取图像
image = cv2.imread("D:\WPS Office\cartoon_nezha.jpg", 0)

# 定义结构元素
kernel = np.ones((5,5), np.uint8)

# 膨胀操作
dilated_image = cv2.dilate(image, kernel, iterations=1)

# 显示结果
cv2.imshow('Dilated Image', dilated_image)
cv2.waitKey(0)
cv2.destroyAllWindows()

膨胀操作会使图像中的前景对象变大,边缘被扩展,常用于填补前景对象中的空洞或连接断裂的对象。 

进一步理解腐蚀与膨胀

Python代码示例 
from __future__ import print_function
import cv2 as cv
import numpy as np
import argparse


src = None
erosion_size = 0
max_elem = 2
max_kernel_size = 21
title_trackbar_element_shape = 'Element:\n 0: Rect \n 1: Cross \n 2: Ellipse'
title_trackbar_kernel_size = 'Kernel size:\n 2n +1'
title_erosion_window = 'Erosion Demo'
title_dilation_window = 'Dilation Demo'


def main(image):
    global src
    src = cv.imread(cv.samples.findFile(image))
    if src is None:
        print('Could not open or find the image: ', image)
        exit(0)


    cv.namedWindow(title_erosion_window)
    cv.createTrackbar(title_trackbar_element_shape, title_erosion_window, 0, max_elem, erosion)
    cv.createTrackbar(title_trackbar_kernel_size, title_erosion_window, 0, max_kernel_size, erosion)


    cv.namedWindow(title_dilation_window)
    cv.createTrackbar(title_trackbar_element_shape, title_dilation_window, 0, max_elem, dilatation)
    cv.createTrackbar(title_trackbar_kernel_size, title_dilation_window, 0, max_kernel_size, dilatation)


    erosion(0)
    dilatation(0)
    cv.waitKey()


# 值与形态形状的可选映射
def morph_shape(val):
    if val == 0:
        return cv.MORPH_RECT
    elif val == 1:
        return cv.MORPH_CROSS
    elif val == 2:
        return cv.MORPH_ELLIPSE


def erosion(val):
    erosion_size = cv.getTrackbarPos(title_trackbar_kernel_size, title_erosion_window)
    erosion_shape = morph_shape(cv.getTrackbarPos(title_trackbar_element_shape, title_erosion_window))
    

    element = cv.getStructuringElement(erosion_shape, (2 * erosion_size + 1, 2 * erosion_size + 1),(erosion_size, erosion_size))
    

    erosion_dst = cv.erode(src, element)
    cv.imshow(title_erosion_window, erosion_dst)


def dilatation(val):
    dilatation_size = cv.getTrackbarPos(title_trackbar_kernel_size, title_dilation_window)
    dilation_shape = morph_shape(cv.getTrackbarPos(title_trackbar_element_shape, title_dilation_window))


    element = cv.getStructuringElement(dilation_shape, (2 * dilatation_size + 1, 2 * dilatation_size + 1),(dilatation_size, dilatation_size))

    dilatation_dst = cv.dilate(src, element)
    cv.imshow(title_dilation_window, dilatation_dst)


if __name__ == "__main__":
    parser = argparse.ArgumentParser(description='Code for Eroding and Dilating tutorial.')
    parser.add_argument('--input', help='Path to input image.', default='LinuxLogo.jpg')
    args = parser.parse_args()
    main(args.input)

运行效果图: 

       

源代码解析如下: 

def main(image):
    global src
    src = cv.imread(cv.samples.findFile(image))
    if src is None:
        print('Could not open or find the image: ', image)
        exit(0)


    cv.namedWindow(title_erosion_window)
    cv.createTrackbar(title_trackbar_element_shape, title_erosion_window, 0, max_elem, erosion)
    cv.createTrackbar(title_trackbar_kernel_size, title_erosion_window, 0, max_kernel_size, erosion)


    cv.namedWindow(title_dilation_window)
    cv.createTrackbar(title_trackbar_element_shape, title_dilation_window, 0, max_elem, dilatation)
    cv.createTrackbar(title_trackbar_kernel_size, title_dilation_window, 0, max_kernel_size, dilatation)


    erosion(0)
    dilatation(0)
    cv.waitKey()
主程序的执行逻辑 
  1. 加载图像(可以是 BGR 或灰度)
  2. 创建两个窗口(一个用于侵蚀输出,另一个用于膨胀),每个窗口都有一组跟踪条
    • 第一个跟踪条 “Element” 返回将要映射的形态类型的值(1 = 矩形,2 = 十字,3 = 椭圆)
    • 第二个跟踪栏 “Kernel size” 返回相应作的元素的大小
  3. 调用 once 侵蚀和扩张以显示初始图像

每次我们移动任何滑块时,用户的函数 erosion 或 dilation 都会被调用,并且它将根据当前的跟踪条值更新输出图像。

现在我们来分析一下这两个函数:

侵蚀函数
def erosion(val):
    erosion_size = cv.getTrackbarPos(title_trackbar_kernel_size, title_erosion_window)
    erosion_shape = morph_shape(cv.getTrackbarPos(title_trackbar_element_shape, title_erosion_window))
    
    element = cv.getStructuringElement(erosion_shape, (2 * erosion_size + 1, 2 * erosion_size + 1),(erosion_size, erosion_size))
    
    erosion_dst = cv.erode(src, element)
    cv.imshow(title_erosion_window, erosion_dst)

执行 erosion作的函数是cv::erode。正如我们所看到的,它接收两个参数并返回处理后的图像:

src:源图像

element:我们将用于执行作的内核。我们可以使用函数 cv::getStructuringElement来指定它的形状:

    element = cv.getStructuringElement(erosion_shape, (2 * erosion_size + 1, 2 * erosion_size + 1),(erosion_size, erosion_size))

我们可以为内核选择三种形状中的任何一种:

矩形框:MORPH_RECT

十字架:MORPH_CROSS

椭圆:MORPH_ELLIPSE

然后,我们只需要指定内核的大小和锚点。如果未指定锚点,则假定它位于中心。

就这样。我们已经准备好对我们的形象进行侵蚀。

膨胀函数

如你所见,它与 erosion 的代码片段完全相似。在这里,我们还可以选择定义我们的内核、它的锚点和要使用的运算符的大小。

def dilatation(val):
    dilatation_size = cv.getTrackbarPos(title_trackbar_kernel_size, title_dilation_window)
    dilation_shape = morph_shape(cv.getTrackbarPos(title_trackbar_element_shape, title_dilation_window))
    element = cv.getStructuringElement(dilation_shape, (2 * dilatation_size + 1, 2 * dilatation_size + 1),(dilatation_size, dilatation_size))

    dilatation_dst = cv.dilate(src, element)
    cv.imshow(title_dilation_window, dilatation_dst)

注意:此外,还有一些参数允许您一次执行多个侵蚀/膨胀(迭代),还可以设置边界类型和值。

开运算 (Opening)

        开运算是先对图像进行腐蚀然后进行膨胀的操作。它的主要作用是可以去除细小的噪声点、分离粘连在一起的小对象以及平滑较大物体的边界而不明显改变其面积。开运算对于消除孤立的小点和毛刺特别有效。

开运算的表达式为:

Opening(A,B)=(A⊖B)⊕B

函数原型:cv2.morphologyEx(src, op, kernel)

src: 输入图像,通常是二值图像。

op: 形态学操作类型,开运算使用 cv2.MORPH_OPEN

kernel: 结构元素,可以自定义或使用 cv2.getStructuringElement() 生成。

import cv2
import numpy as np

# 读取图像
image = cv2.imread("D:\WPS Office\cartoon_nezha.jpg", 0)

# 定义结构元素
kernel = np.ones((5,5), np.uint8)

# 开运算
opened_image = cv2.morphologyEx(image, cv2.MORPH_OPEN, kernel)

# 显示结果
cv2.imshow('Opened Image', opened_image)
cv2.waitKey(0)
cv2.destroyAllWindows()

 运行效果如下

开运算可以去除图像中的小噪声,同时保留图像中的主要前景对象。 

闭运算 (Closing)

        闭运算是先对图像进行膨胀后进行腐蚀的过程。闭运算可以帮助填补物体内部的小空洞、连接断裂的部分和平滑物体的边界。它能够帮助修复因噪声造成的裂缝或不连续性。

闭运算的数学形式是:

Closing(A,B)=(A⊕B)⊖B

函数原型:cv2.morphologyEx(src, op, kernel)

src: 输入图像,通常是二值图像。

op: 形态学操作类型,开运算使用 cv2.MORPH_OPEN

kernel: 结构元素,可以自定义或使用 cv2.getStructuringElement() 生成。

import cv2
import numpy as np

# 读取图像
image = cv2.imread("D:\WPS Office\cartoon_nezha.jpg", 0)

# 定义结构元素
kernel = np.ones((5,5), np.uint8)

# 开运算
opened_image = cv2.morphologyEx(image, cv2.MORPH_OPEN, kernel)

# 显示结果
cv2.imshow('Opened Image', opened_image)
cv2.waitKey(0)
cv2.destroyAllWindows()

闭运算可以填补前景对象中的小孔,同时保留图像中的主要前景对象。 

形态学梯度 (Morphological Gradient)

        形态学梯度是膨胀与腐蚀的结果之间的差值,主要用于提取图像中前景对象的边缘。。这种操作强调了图像中的边界信息,因为膨胀倾向于扩展物体而腐蚀则倾向于收缩物体,两者之差突出了物体边缘的变化。

形态学梯度的计算方式如下:

Gradient(A,B)=(A⊕B)−(A⊖B)

函数原型:cv2.morphologyEx(src, op, kernel)

src: 输入图像,通常是二值图像。

op: 形态学操作类型,形态学梯度使用 cv2.MORPH_GRADIENT

kernel: 结构元素,可以自定义或使用 cv2.getStructuringElement() 生成。

import cv2
import numpy as np

# 读取图像
image = cv2.imread("D:\WPS Office\cartoon_nezha.jpg", 0)

# 定义结构元素
kernel = np.ones((5,5), np.uint8)

# 形态学梯度
gradient_image = cv2.morphologyEx(image, cv2.MORPH_GRADIENT, kernel)

# 显示结果
cv2.imshow('Gradient Image', gradient_image)
cv2.waitKey(0)
cv2.destroyAllWindows()

 运行效果如下图所示:

形态学梯度可以提取图像中前景对象的边缘,常用于边缘检测。

顶帽运算和黑帽运算

顶帽运算(Top Hat Transformation)和黑帽运算(Black Hat Transformation)是形态学图像处理中的两种高级操作,它们基于基本的腐蚀、膨胀、开运算和闭运算。这两种运算主要用于增强图像中特定的特征,如阴影部分或暗区域。

顶帽运算

顶帽运算是原始图像与经过开运算后的图像之间的差值。开运算是先腐蚀后膨胀的过程,它能够去除图像中的小对象,并平滑较大物体的边界而不明显改变其面积。顶帽运算则通过这种方式来突出那些比周围环境更亮的局部区域或特征,即那些在开运算后被削弱的细节。具体来说,顶帽运算可以用来校正不均匀的光照条件,并突出显示原本可能被忽略的小亮点或斑块。

应用场景

1.增强细节:顶帽运算可以用来增强图像中比结构元素小的亮区域,适用于需要强调细微差异的情况。

2.背景提取:当一幅图像具有大幅背景时,顶帽运算可以帮助分离出前景中的微小、规则分布的对象 。

3.光照校正:由于顶帽运算能够凸显局部亮度变化,因此它可以用于解决因光照不均导致的图像分割问题。

数学表达式为:

TopHat(L)=L−Opening(L)

其中L 是原始图像,而 Opening(L) 表示对L 执行开运算的结果。

在实际应用中,顶帽运算通常用于分离比邻近点亮一些的斑块,在一幅具有大幅背景的图像中,当微小物品比较有规律的情况下,可以使用顶帽运算进行背景提取。

import cv2
import numpy as np

# 读取输入图像
img = cv2.imread("D:\WPS Office\cartoon_nezha.jpg", cv2.IMREAD_GRAYSCALE)

# 定义结构元素
kernel = np.ones((5,5), np.uint8)

# 应用顶帽运算
tophat = cv2.morphologyEx(img, cv2.MORPH_TOPHAT, kernel)

# 显示结果
cv2.imshow('Original Image', img)
cv2.imshow('Top Hat', tophat)
cv2.waitKey(0)
cv2.destroyAllWindows()

黑帽运算

黑帽运算是原图与其闭运算结果之间的差值。闭运算是先膨胀后腐蚀的过程,它能填充目标内部的小孔洞和平滑边界。黑帽运算通过这种方式来突出显示图像内部的暗点或较暗的局部区域或特征,这些可能是由于闭运算过程中未能完全填充的小孔洞或者是前景中的黑色斑点。黑帽变换对于消除光照不均的影响非常有用,特别是在需要强调图像中较暗部分的时候。

应用场景

1.检测阴影:黑帽运算对于识别图像中的深陷部分或阴影特别有用,因为它能识别那些比周围背景更暗的区域 。

2.填补空洞:在处理二值图像时,黑帽运算可以帮助填补那些未被完全填充的小孔洞 。

3.改善对比度:在某些情况下,黑帽运算可以用来改善图像中特定区域的对比度,从而使得这些区域更加清晰可见 。

数学表达式如下:

BlackHat(L)=Closing(L)−L

这里 L同样表示原始图像,Closing(L)则是对 L 进行闭运算后的结果。

import cv2
import numpy as np

# 读取输入图像
img = cv2.imread("D:\WPS Office\cartoon_nezha.jpg", cv2.IMREAD_GRAYSCALE)

# 定义结构元素
kernel = np.ones((5,5), np.uint8)

# 应用黑帽运算
blackhat = cv2.morphologyEx(img, cv2.MORPH_BLACKHAT, kernel)

# 显示结果
cv2.imshow('Original Image', img)
cv2.imshow('Black Hat', blackhat)
cv2.waitKey(0)
cv2.destroyAllWindows()

总结

顶帽运算主要用于提取比周围环境更亮的局部特征,适合于增强细节和校正光照不均的问题。

黑帽运算则是为了找出比周围环境更暗的局部特征,常用于检测阴影和填补图像中的小孔洞。

进一步理解后面这几种形态学操作

Python代码示例
from __future__ import print_function
import cv2 as cv
import numpy as np
import argparse

morph_size = 0
max_operator = 4
max_elem = 2
max_kernel_size = 21

title_trackbar_operator_type = 'Operator:\n 0: Opening - 1: Closing  \n 2: Gradient - 3: Top Hat \n 4: Black Hat'
title_trackbar_element_type = 'Element:\n 0: Rect - 1: Cross - 2: Ellipse'
title_trackbar_kernel_size = 'Kernel size:\n 2n + 1'
title_window = 'Morphology Transformations Demo'

morph_op_dic = {0: cv.MORPH_OPEN, 1: cv.MORPH_CLOSE, 2: cv.MORPH_GRADIENT, 3: cv.MORPH_TOPHAT, 4: cv.MORPH_BLACKHAT}


def morphology_operations(val):
    morph_operator = cv.getTrackbarPos(title_trackbar_operator_type, title_window)
    morph_size = cv.getTrackbarPos(title_trackbar_kernel_size, title_window)
    morph_elem = 0
    val_type = cv.getTrackbarPos(title_trackbar_element_type, title_window)
    if val_type == 0:
        morph_elem = cv.MORPH_RECT
    elif val_type == 1:
        morph_elem = cv.MORPH_CROSS
    elif val_type == 2:
        morph_elem = cv.MORPH_ELLIPSE


    element = cv.getStructuringElement(morph_elem, (2*morph_size + 1, 2*morph_size+1), (morph_size, morph_size))
    operation = morph_op_dic[morph_operator]
    dst = cv.morphologyEx(src, operation, element)
    cv.imshow(title_window, dst)


parser = argparse.ArgumentParser(description='Code for More Morphology Transformations tutorial.')
parser.add_argument('--input', help='Path to input image.', default='LinuxLogo.jpg')
args = parser.parse_args()


src = cv.imread(cv.samples.findFile(args.input))
if src is None:
    print('Could not open or find the image: ', args.input)
    exit(0)

cv.namedWindow(title_window)
cv.createTrackbar(title_trackbar_operator_type, title_window , 0, max_operator, morphology_operations)
cv.createTrackbar(title_trackbar_element_type, title_window , 0, max_elem, morphology_operations)
cv.createTrackbar(title_trackbar_kernel_size, title_window , 0, max_kernel_size, morphology_operations)

morphology_operations(0)
cv.waitKey()

未完待续……

特此鸣谢 

图像处理学习笔记(二十三)——形态处理学基本概念(理论篇) - 知乎

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐