B06 - 图像处理 - 形态转换

形态学的概念与应用,由膨胀腐蚀到开运算闭运算,再到顶帽黑帽运算,最终介绍这些运算在提取边界、校准光照的作用

什么是形态学?

  • 形态学是生物学的概念,主要研究动植物的形态和结构。数学形态学是建立在集合论和拓扑学基础之上的图像分析学科
  • 图像处理中的形态学:指基于形状的图像处理操作,以数学形态学为工具从图像中提取表达和描绘区域形状的图像结构信息,如边界、骨架、凸壳等
  • 形态学运算:包括:二值腐蚀和膨胀,二值开闭运算,骨架抽取,极限腐蚀,击中击不中变换,形态学梯度,顶帽变换,颗粒分析,流域变换,灰值腐蚀和膨胀,灰值开闭运算,灰值形态学梯度等
  • 形态学基本思想:利用结构元素测量或提取输入图像中的形状或特征,以便进行图像分析和目标识别。形态学操作都是基于各种形状的结构元,结构元对输入图像进行操作得到输出图像。腐蚀和膨胀是图像处理中最基本的形态学操作

图像处理上,什么是腐蚀 (erode)?

  • 腐蚀类似于 “领域被蚕食”,将图像中的高亮区域或白色部分进行缩减细化,其运行结果图比原图的高亮区域更小
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    imgGray = cv2.imread("../images/Fig0905a.tif", flags=0)  # flags=0 读取为灰度图像
    ret, imgBin = cv2.threshold(imgGray, 0, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU) # 二值化处理
    # 图像腐蚀
    kSize = (3, 3) # 卷积核的尺寸
    kernel = np.ones(kSize, dtype=np.uint8) # 生成盒式卷积核
    imgErode1 = cv2.erode(imgBin, kernel=kernel) # 图像腐蚀
    kSize = (9, 9)
    kernel = np.ones(kSize, dtype=np.uint8)
    imgErode2 = cv2.erode(imgBin, kernel=kernel)
    kSize = (25, 25)
    kernel = np.ones(kSize, dtype=np.uint8)
    imgErode3 = cv2.erode(imgBin, kernel=kernel)
    show_images([imgGray,imgErode1,imgErode2,imgErode3])

图像处理上,什么是膨胀 (dilate)?

  • 膨胀类似于 “领域扩张”,将图像中的高亮区域或白色部分进行扩张,其运行结果图比原图的高亮区域更大
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    imgGray = cv2.imread("../images/handwriting01.png", flags=0)  # flags=0 读取为灰度图像
    ret, imgBin = cv2.threshold(imgGray, 0, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU) # 二值化处理
    # 图像膨胀
    kSize = (3, 3) # 卷积核的尺寸
    kernel = np.ones(kSize, dtype=np.uint8) # 生成盒式卷积核
    imgDilate1 = cv2.dilate(imgBin, kernel=kernel) # 图像膨胀
    kSize = (5, 5)
    kernel = np.ones(kSize, dtype=np.uint8)
    imgDilate2 = cv2.dilate(imgBin, kernel=kernel) # 图像膨胀
    kSize = (7, 7)
    kernel = np.ones(kSize, dtype=np.uint8)
    imgDilate3 = cv2.dilate(imgBin, kernel=kernel) # 图像膨胀

图像腐蚀的原理?

  • 使用 cv: :getStructuringElement 定义核矩阵,将核矩阵与源矩阵对齐后,核矩阵开始遍历扫描源矩阵,核矩阵上锚点位置对应的像素值为该核矩阵对应源矩阵像素值的最小值
  • 从左到右,依次是源图像素矩阵,核矩阵,腐蚀后结果矩阵,膨胀后结果矩阵,分别记为 A,B,C,D
  • 如图 A (2,2)=1,A (8,9)=64,

C(3,3)=min(A(2,2),A(2,3),A(2,4),A(3,2),A(3,3),A(3,4),A(4,2),A(4,3),A(4,4))=min(1,2,3,2,4,6,3,6,9)=1C(3,3)=min(A(2,2),A(2,3),A(2,4),A(3,2),A(3,3),A(3,4),A(4,2),A(4,3),A(4,4))=min(1,2,3,2,4,6,3,6,9)=1

图像膨胀的原理?

  • 使用 cv: :getStructuringElement 定义核矩阵,将核矩阵与源矩阵对齐后,核矩阵开始遍历扫描源矩阵,核矩阵上锚点位置对应的像素值为该核矩阵对应源矩阵像素值的最大值
  • 从左到右,依次是源图像素矩阵,核矩阵,腐蚀后结果矩阵,膨胀后结果矩阵,分别记为 A,B,C,D
  • 如图 A (2,2)=1,A (8,9)=64,

D(5,5)=max(A(4,4),A(4,5),A(4,6),A(5,4),A(5,5),A(5,6),A(6,4),A(6,5),A(6,6))=min(9,12,15,12,16,20,15,20,25)=25D(5,5)=max(A(4,4),A(4,5),A(4,6),A(5,4),A(5,5),A(5,6),A(6,4),A(6,5),A(6,6))=min(9,12,15,12,16,20,15,20,25)=25

膨胀、腐蚀与滤波的异同?

  • 都是使用卷积核对源矩阵进行扫描,并通过卷积核与对应的源矩阵的运算决定目标矩阵在锚点位置的值
  • 滤波是卷积核与源矩阵进行乘积和得到锚点位置的值,而腐蚀、膨胀则是卷积核对应区域的最小值或最大值,填充锚点位置的值

什么是开运算?

  • 先腐蚀后膨胀的过程,通常用于去除噪点、断开狭窄的狭颈、消除细长的突出、平滑物体边界但不改变面积
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    imgGray = cv2.imread("../images/Fig0905a.tif", flags=0)  # flags=0 读取为灰度图像
    ret, imgBin = cv2.threshold(imgGray, 0, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU) # 二值化处理
    # 图像腐蚀
    kSize = (3, 3) # 卷积核的尺寸
    kernel = np.ones(kSize, dtype=np.uint8) # 生成盒式卷积核
    imgErode = cv2.erode(imgBin, kernel=kernel) # 图像腐蚀
    # 图像的开运算
    kSize = (3, 3) # 卷积核的尺寸
    kernel = np.ones(kSize, dtype=np.uint8) # 生成盒式卷积核
    imgOpen = cv2.morphologyEx(imgGray, cv2.MORPH_OPEN, kernel)

什么是闭运算?

  • 先膨胀后腐蚀的过程,通常用于弥合狭窄的断裂和细长的沟壑,消除小孔,填补轮廓中的缝隙,消除噪点,连接相邻的部分
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    imgGray = cv2.imread("../images/handwriting01.png", flags=0)  # flags=0 读取为灰度图像
    mu, sigma = 0.0, 10.0
    noiseGause = np.random.normal(mu, sigma, imgGray.shape)
    imgNoisy = imgGray + noiseGause
    imgNoisy = np.uint8(cv2.normalize(imgNoisy, None, 0, 255, cv2.NORM_MINMAX)) # 归一化为 [0,255]
    ret, imgBin = cv2.threshold(imgNoisy, 125, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU) # 二值化处理
    # 图像的闭运算
    kSize = (2, 2) # 卷积核的尺寸
    kernel = np.ones(kSize, dtype=np.uint8) # 生成盒式卷积核
    imgClose1 = cv2.morphologyEx(imgBin, cv2.MORPH_CLOSE, kernel)
    kSize = (3, 3) # 卷积核的尺寸
    kernel = np.ones(kSize, dtype=np.uint8) # 生成盒式卷积核
    imgClose2 = cv2.morphologyEx(imgBin, cv2.MORPH_CLOSE, kernel)
    kSize = (5, 5) # 卷积核的尺寸
    kernel = np.ones(kSize, dtype=np.uint8) # 生成盒式卷积核
    imgClose3 = cv2.morphologyEx(imgBin, cv2.MORPH_CLOSE, kernel)

什么是顶帽变换?

  • 顶帽变换就是用源图像减去开运算 图像。因为开运算带来的结果是放大了裂缝或者局部低亮度的区域。因此,从原图中减去开运算后的图,得到的效果图突出了比原图轮廓周围区域更明亮的区域。顶帽一般用于校正不均匀光照的影响
    1
    2
    3
    4
    5
    imgGray = cv2.imread("../images/Fig0726a.tif", flags=0)  # flags=0 读取为灰度图像
    ret, imgBin = cv2.threshold(imgGray, 205, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU) # 二值化处理
    kernel = np.ones((5, 5), np.uint8) # 卷积核
    imgOpen = cv2.morphologyEx(imgBin, cv2.MORPH_OPEN, kernel) # 开运算
    imgThat = cv2.morphologyEx(imgBin, cv2.MORPH_TOPHAT, kernel) # 顶帽运算

什么是黑帽变换?

  • 黑帽变换就是用闭运算减去源图像。黑帽运算后的效果图突出了比原图轮廓周围的区域更暗的区域。黑帽运算一般用来分离比邻近点暗一些的斑块
    1
    2
    3
    4
    5
    imgGray = cv2.imread("../images/Fig0338a.tif", flags=0)  # flags=0 读取为灰度图像
    ret, imgBin = cv2.threshold(imgGray, 127, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU) # 二值化处理
    kernel = np.ones((5, 5), np.uint8) # 卷积核
    imgClose = cv2.morphologyEx(imgBin, cv2.MORPH_CLOSE, kernel) # 闭运算
    imgBhat = cv2.morphologyEx(imgBin, cv2.MORPH_BLACKHAT, kernel) # 底帽运算

什么是形态学梯度?

  • 图像的形态学梯度运算,是膨胀图像与腐蚀图像之差,可以得到图像的轮廓,通常用于提取物体边缘
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    # 读取原始图像
    imgGray = cv2.imread("../images/handwriting03.png", flags=0) # flags=0 读取为灰度图像
    ret, imgBin = cv2.threshold(imgGray, 15, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU) # 二值化处理
    # 图像的形态学梯度
    kSize = (3, 3) # 卷积核的尺寸
    kernel = np.ones(kSize, dtype=np.uint8) # 生成盒式卷积核
    imgGrad1 = cv2.morphologyEx(imgBin, cv2.MORPH_GRADIENT, kernel) # 形态学梯度
    kSize = (5, 5) # 卷积核的尺寸
    kernel = np.ones(kSize, dtype=np.uint8) # 生成盒式卷积核
    imgGrad2 = cv2.morphologyEx(imgBin, cv2.MORPH_GRADIENT, kernel) # 形态学梯度
    kSize = (3, 3) # 卷积核的尺寸
    kernel = np.ones(kSize, dtype=np.uint8) # 生成盒式卷积核
    imgOpen = cv2.morphologyEx(imgBin, cv2.MORPH_OPEN, kernel) # 开运算
    imgOpenGrad = cv2.morphologyEx(imgOpen, cv2.MORPH_GRADIENT, kernel) # 形态学梯度

击中 - 击不中变换?

  • 击中 - 击不中是形态检测的基本工具,可以实现对象的细化和剪枝操作,常用于物体识别、图像细化
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    # 读取原始图像
    imgGray = cv2.imread("../images/handwriting03.png", flags=0) # flags=0 读取为灰度图像
    ret, imgBin = cv2.threshold(imgGray, 25, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU) # 二值化处理
    # 闭运算
    kernel = np.ones((3, 3), dtype=np.uint8) # 生成盒式卷积核
    imgClose = cv2.morphologyEx(imgBin, cv2.MORPH_CLOSE, kernel) # 闭运算
    # 击中击不中变换
    kernB1 = np.array([[0, 0, 0],[0, -1, 1],[0, 0, 0]], dtype=np.int32) # B1
    kernB2 = np.array([[0, 0, 0],[1, -1, 0],[0, 0, 0]], dtype=np.int32) # B2
    imgH1 = cv2.morphologyEx(imgClose, cv2.MORPH_HITMISS, kernB1)
    imgH2 = cv2.morphologyEx(imgClose, cv2.MORPH_HITMISS, kernB2)
    imgHMT = cv2.add(imgH1, imgH2) # 击中击不中

击中 - 击不中用于特征识别?

  • 本例使用击中 - 击不中变换进行特征识别,提取绳结特征
    1
    2
    3
    4
    5
    6
    7
    8
    imgGray = cv2.imread("../images/imgNetrope.png", flags=0)  # flags=0 读取为灰度图像
    ret, imgBin = cv2.threshold(imgGray, 25, 255, cv2.THRESH_BINARY_INV | cv2.THRESH_OTSU) # 二值化处理
    # 击中击不中变换
    kernal1 = cv2.getStructuringElement(cv2.MORPH_CROSS, (5, 5), (-1, -1))
    kernal2 = cv2.getStructuringElement(cv2.MORPH_CROSS, (9,9), (-1, -1))
    imgHMT1 = cv2.morphologyEx(imgBin, cv2.MORPH_HITMISS, kernal1)
    imgHMT2 = cv2.morphologyEx(imgBin, cv2.MORPH_HITMISS, kernal2)
    show_images([imgGray,imgHMT1,imgHMT2])

形态算法之边界提取?

  • 形态学处理的主要应用是提取图像中用来表示和描述形状的元素和成分,例如提取边界、连通分量、凸壳和区域骨架
  • 边界提取:通过对目标图像进行腐蚀和膨胀处理,比较结果图像与原图像的差别来实现
    1
    2
    3
    4
    5
    6
    imgGray = cv2.imread("../images/imgNetrope.png", flags=0)  # flags=0 读取为灰度图像
    ret, imgBin = cv2.threshold(imgGray, 25, 255, cv2.THRESH_BINARY_INV | cv2.THRESH_OTSU) # 二值化处理
    kSize = (3, 3) # 卷积核的尺寸
    kernel = np.ones(kSize, dtype=np.uint8) # 生成盒式卷积核
    imgErode1 = cv2.erode(imgBin, kernel=kernel) # 图像腐蚀
    imgBound1 = imgBin - imgErode1 # 图像边界提取

形态算法之空洞填充?

  • 孔洞是被前景像素连成的边框包围的背景区域。书法作品图像中存在孔洞,在图像分割后也经常会有一些孔洞
  • 闭运算孔洞填充: 形态学闭运算可以用来实现孔洞填充,闭运算先膨胀后腐蚀操作,膨胀使白色高亮区域增加,孔洞会被填充,但需要准确设置核大小,因此不是通用的方法
  • 约束膨胀孔洞填充: 先找到孔洞中的一个点,用结构元进行膨胀,然后用原始图像的补集进行约束(交集运算),不断迭代重复这一操作直到算法收敛,就得到孔洞填充图
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    imgGray = cv2.imread("../images/imgBloodCell.png", flags=0)
    ret, imgBin = cv2.threshold(imgGray, 127, 255, cv2.THRESH_BINARY_INV | cv2.THRESH_OTSU) # 二值化处理
    imgBinInv = cv2.bitwise_not(imgBin) # 二值图像的补集
    kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3, 3)) # 构造 3×3 十字形结构元
    F = np.zeros(imgBin.shape, np.uint8) # 构建阵列 F,并写入 BinInv 的边界值
    F[:, 0] = imgBinInv[:, 0]
    F[:, -1] = imgBinInv[:, -1]
    F[0, :] = imgBinInv[0, :]
    F[-1, :] = imgBinInv[-1, :]
    # 循环迭代:对 F 进行膨胀,膨胀结果与 BinInv 进行 AND 操作
    Flast = F.copy()
    for i in range(1000):
    F_dilation = cv2.dilate(F, kernel)
    F = cv2.bitwise_and(F_dilation, imgBinInv)
    if (F==Flast).all():
    break # 结束迭代算法
    else:
    Flast = F.copy()
    if i==100: imgF100 = F # 中间结果
  • 泛洪算法孔洞填充: 也成为 “漫水填充法 “。其原理是将像素点的灰度值视为高度,整个图像就像一张高低起伏的地形图,向洼地注水将会淹没低洼区域,从而实现孔洞填充
    1
    2
    3
    4
    5
    6
    7
    8
    9
    imgGray = cv2.imread("../images/imgBloodCell.png", flags=0)  # flags=0 读取为灰度图像
    ret, imgBin = cv2.threshold(imgGray, 127, 255, cv2.THRESH_BINARY_INV | cv2.THRESH_OTSU) # 二值化处理
    h, w = imgBin.shape[:2]
    mask = np.zeros((h+2, w+2), np.uint8) # 掩模图像比原始图像宽 2 个像素、高 2 个像素
    imgFloodfill = imgBin.copy()
    cv2.floodFill(imgFloodfill, mask, (0, 0), newVal=225) # 算法从背景像素原点 (0, 0) 开始
    imgFloodfillInv = cv2.bitwise_not(imgFloodfill) # 计算补集
    imgHoleFilled = imgBin | imgFloodfillInv # 计算交集
    imgRebuild = cv2.bitwise_not(imgHoleFilled) # 计算补集

形态算法之提取联通分量?

  • 提取连通分量的过程也是对连通分量的标注,通常给图像中的每个连通区分配编号,在输出图像中该连通区内的所有的像素值赋值为对应的区域编号,这样的输出图像被称为标注图像
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    imgGray = cv2.imread("../images/Fig0918a.tif", flags=0)  # flags=0 读取为灰度图像
    # 预处理
    ret, imgThresh = cv2.threshold(imgGray, 200, 255, cv2.THRESH_BINARY_INV) # 二值化处理
    kernel = np.ones((3, 3), dtype=np.uint8) # 生成盒式卷积核
    imgClose = cv2.morphologyEx(imgThresh, cv2.MORPH_CLOSE, kernel) # 闭运算,消除噪点
    imgErode = cv2.erode(imgClose, kernel=kernel) # 腐蚀运算,腐蚀亮点
    imgBin = imgErode
    imgBinCopy = imgBin.copy() # 复制 imgBin
    xBinary = np.zeros(imgBin.shape, np.uint8) # 大小与 img 相同,像素值为 0
    kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3, 3)) # 3×3结构元
    count = [] # 为了记录连通分量中的像素个数
    while imgBinCopy.any(): # 直到 imgBinCopy 中的像素值全部为0
    Xa_copy, Ya_copy = np.where(imgBinCopy > 0) # imgBinCopy 中值为255的像素的坐标
    xBinary[Xa_copy[0]][Ya_copy[0]] = 255 # 选取第一个点,并将 xBinary 中对应像素值改为255
    # 约束膨胀,先对 xBinary 膨胀,再与 imgBin 执行与操作(取交集)
    for i in range(100):
    dilation_B = cv2.dilate(xBinary, kernel)
    xBinary = cv2.bitwise_and(imgBin, dilation_B)
    # 取 xBinary 值为255的像素坐标,并将 imgBinCopy 中对应坐标像素值变为0
    Xb, Yb = np.where(xBinary > 0)
    imgBinCopy[Xb, Yb] = 0
    # 显示连通分量及其包含像素数量
    count.append(len(Xb))
    lenCount = len(count)
    if lenCount == 0:
    print("无连通分量")
    elif lenCount == 1:
    print("第1个连通分量为{}".format(count[0]))
    else:
    print("第{}个连通分量为{}".format(len(count), count[-1]-count[-2]))

形态算法之凸壳?

  • 几何中,如果连接物体 A 内任意两点的直线段都在 A 的内部,则称 A 是凸的。数字图像处理中,凸集简化为二维平面,且以离散坐标形式表达。数字集合 A 是凸的,当且仅当它的欧氏凸壳只包含属于 A 的数字点
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    img = cv2.imread("../images/imgDemo1.png", flags=1)
    imgGray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 灰度图像
    imgBlur = cv2.blur(imgGray, (3, 3)) # 去除噪点
    ret, imgBin = cv2.threshold(imgBlur, 225, 255, cv2.THRESH_BINARY) # 二值化处理
    img2, contours, hierarchy = cv2.findContours(imgBin, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) # 寻找所有的轮廓
    hullAll = [] # 所有的凸包
    for i in range(len(contours)):
    hull = cv2.convexHull(contours[i], False) # 计算轮廓的凸包
    hullAll.append(hull)
    colorContours = (0, 255, 0) # 设置轮廓的颜色
    colorConvexHull = (255, 255, 255) # 设置凸包的颜色
    imgContours = np.zeros(img.shape, np.uint8)
    for i in range(len(contours)): # 绘制轮廓线
    cv2.drawContours(imgContours, contours, i, colorContours, 2, 8, hierarchy)
    imgDrawing = imgContours.copy()
    for i in range(len(contours)): # 绘制凸包线
    cv2.drawContours(imgDrawing, hullAll, i, colorConvexHull, 2, 8)

形态算法之细化?

  • 细化是将图像的线条从多像素宽度减少到单位像素宽度的过程,也称为 "骨架化"、“中轴转换 "和" 对称轴转换”
  • 形态骨架是一种细化的结构,指图像的骨骼部分,用于描述物体的几何形状和拓扑结构,是目标物体重要的拓扑描述。图像的细化是对二值图像进行骨架提取,删除不需要的轮廓点,只保留其骨架点
  • 细化过程就是对图像不断重复地逐层边界像素的过程,目标物体随着细化的进行有规律地缩小,但是目标图像边界线的连接性,方向性和特征点不变,最终使变换图像成为单像素宽的图像骨架
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    def thinning(image):
    array = [0, 0, 1, 1, 0, 0, 1, 1, 1, 1, 0, 1, 1, 1, 0, 1, \
    1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, \
    0, 0, 1, 1, 0, 0, 1, 1, 1, 1, 0, 1, 1, 1, 0, 1, \
    1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, \
    1, 1, 0, 0, 1, 1, 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, \
    1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 1, 1, 1, 0, 1, \
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, \
    0, 0, 1, 1, 0, 0, 1, 1, 1, 1, 0, 1, 1, 1, 0, 1, \
    1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, \
    0, 0, 1, 1, 0, 0, 1, 1, 1, 1, 0, 1, 1, 1, 0, 1, \
    1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, \
    1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, \
    1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, \
    1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 1, 1, 1, 0, 0, \
    1, 1, 0, 0, 1, 1, 1, 0, 1, 1, 0, 0, 1, 0, 0, 0]
    h, w = image.shape[0], image.shape[1]
    imgThin = image.copy()
    for i in range(h):
    for j in range(w):
    if image[i, j] == 0:
    a = np.ones((9,), dtype=np.int)
    for k in range(3):
    for l in range(3):
    if -1<(i-1+k)<h and -1<(j-1+l)<w and imgThin[i-1+k,j-1+l]==0:
    a[k*3+l] = 0
    sum = a[0]*1 + a[1]*2 + a[2]*4 + a[3]*8 + a[5]*16 + a[6]*32 + a[7]*64 + a[8]*128
    imgThin[i, j] = array[sum] * 255
    return imgThin
    # 图像为灰度图像,背景为白色(255),被细化物体为黑色(0)
    image = cv2.imread("../images/imgNetrope.png", flags=0) # flags=0 灰度图像
    ret, binary = cv2.threshold(image, 205, 255, cv2.THRESH_BINARY) # 二值化处理
    imgThin = thinning(binary) # 细化算法

形态算法之骨架?

  • 形态骨架是一种细化的结构,指图像的骨骼部分,用于描述物体的几何形状和拓扑结构,是目标物体重要的拓扑描述。图像的细化是对二值图像进行骨架提取,删除不需要的轮廓点,只保留其骨架点
  • 步骤:1)对图像进行腐蚀,腐蚀后的物体变得更窄细;2)对腐蚀后图像做开运算,开运算处理时被删除的像素就是骨骼的一部分,将其加入骨骼图像;3)重复以上过程,直到图像被完全腐蚀
    1
    2
    3
    4
    5
    6
    from skimage import morphology
    imgGray = cv2.imread("../images/handwriting01.png", flags=0) # flags=0 灰度图像
    ret, imgBin = cv2.threshold(imgGray, 127, 255, cv2.THRESH_BINARY) # 二值化处理
    imgBin[imgBin==255] = 1
    skeleton01 = morphology.skeletonize(imgBin)
    skeleton = skeleton01.astype(np.uint8) * 255

形态学之提取水平和垂直线?

  • 通过自定义的结构元素,使结构元对输入图像的一些对象敏感,而对另一些对象不敏感,就会滤去敏感对象、保留不敏感对象。对于水平或垂直线,可以通过定义水平线或垂直线的结构元素去除水平线或垂直线的干扰,也可以提取水平或垂直线
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    imgGray = cv2.imread("../images/imgLine2.png", flags=0)  # flags=0 灰度图像
    ret, imgBin = cv2.threshold(imgGray, 205, 255, cv2.THRESH_BINARY_INV) # 二值化处理,反白
    h, w = imgBin.shape[0], imgBin.shape[1]
    # 提取水平线
    hline = cv2.getStructuringElement(cv2.MORPH_RECT, ((w//16),1), (-1,-1)) # 水平结构元
    imgOpenHline = cv2.morphologyEx(imgBin, cv2.MORPH_OPEN, hline) # 开运算提取水平结构
    imgHline = cv2.bitwise_not(imgOpenHline) # 恢复白色背景
    # 提取垂直线
    vline = cv2.getStructuringElement(cv2.MORPH_RECT, (1,(h//16)), (-1,-1)) # 垂直结构元
    imgOpenVline = cv2.morphologyEx(imgBin, cv2.MORPH_OPEN, vline) # 开运算提取垂直结构
    imgVline = cv2.bitwise_not(imgOpenVline)
    lineRemoved = imgBin - imgOpenHline # 删除水平线 (白底为 0)
    lineRemoved = lineRemoved - imgOpenVline # 删除垂直线
    imgRebuild = cv2.bitwise_not(lineRemoved) # 恢复白色背景

形态学之灰度顶帽变换校正阴影?

  • 均匀光照对于从背景中提取目标十分重要,顶帽变换的重要用途就是校正不均匀光照的影响
  • 本例图像是在非均匀光照条件下拍摄的,图像底部和右侧的暗色区域比较明显。如果直接用 Otsu 最优阈值处理方法对灰度图像进行二值化处理,在暗区域的分割出现错误,一些米粒未能从背景中提取出来,而在左上角的亮区域则把部分背景理解为米粒
    1
    2
    3
    4
    5
    6
    7
    imgGray = cv2.imread("../images/Fig0940a.tif", flags=0)  # flags=0 灰度图像
    # 直接用 Otsu 最优阈值处理方法进行二值化处理
    ret, imgBin = cv2.threshold(imgGray, 127, 255, cv2.THRESH_OTSU) # 二值化处理,黑色背景
    # 顶帽运算后再用 Otsu 最优阈值处理方法进行二值化处理
    element = cv2.getStructuringElement(cv2.MORPH_CROSS, (50, 50)) # 十字交叉结构元
    imgThat = cv2.morphologyEx(imgGray, cv2.MORPH_TOPHAT, element) # 顶帽运算
    ret, imgThatBin = cv2.threshold(imgThat, 127, 255, cv2.THRESH_OTSU) # 二值化处理,黑色背景

形态学之灰度底帽变换校正光照?

  • 图像相减结合开运算和闭运算,就得到顶帽变换和底帽变换
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    imgGray = cv2.imread("../images/imgHat01.png", flags=0)  # flags=0 灰度图像
    # 直接用 Otsu 最优阈值处理方法进行二值化处理
    ret, imgBin = cv2.threshold(imgGray, 127, 255, cv2.THRESH_BINARY_INV | cv2.THRESH_OTSU) # 二值化处理,白色背景
    # 底帽变换后再用 Otsu 最优阈值处理方法进行二值化处理
    element = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (55, 55)) # 圆形结构元
    imgBhat = cv2.morphologyEx(imgGray, cv2.MORPH_BLACKHAT, element) # 底帽运算
    ret, imgBhatBin = cv2.threshold(imgBhat, 50, 255, cv2.THRESH_BINARY |cv2.THRESH_OTSU) # 二值化处理,白色背景
    # 圆环表面有很多黑色噪点,通过闭操作去除
    element = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5)) # 圆形结构元
    imgSegment = cv2.morphologyEx(imgBhatBin, cv2.MORPH_CLOSE, element) # 闭运算
    show_iamges([imgGray,imgBin,imgBhat,imgSegment])

形态学之提取水平图像平滑?

  • 由于开运算和闭运算对亮细节和暗细节的抑制作用,对图像先后做开运算和闭运算可以有效地抑制细节,实现平滑图像和去除噪声
  • 使用开运算和闭运算做形态平滑,就是循环交替地执行开运算和闭运算的操作,把图像像素点的灰度值视为高度,整个图像就像一张高低起伏的地形图。形态学平滑就是不断地挖掉高峰、填平低谷,因此起到了平整地形的功效
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    imgGray = cv2.imread("../images/Fig0938a.tif", flags=0)  # flags=0 灰度图像
    # 用不同半径圆形结构元依次交替进行开运算-闭运算实现图像平滑
    kSize = (2, 2)
    element = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, kSize) # 圆形结构元
    imgOpen1 = cv2.morphologyEx(imgGray, cv2.MORPH_OPEN, element) # 灰度开运算
    imgClose1 = cv2.morphologyEx(imgOpen1, cv2.MORPH_CLOSE, element) # 灰度闭运算
    imgIter = imgGray.copy() # 循环交替进行开运算-闭运算
    for i in range(10):
    imgIter = cv2.morphologyEx(imgIter, cv2.MORPH_OPEN, element)
    imgIter = cv2.morphologyEx(imgIter, cv2.MORPH_CLOSE, element)

形态学之纹理分割?

  • 形态学的纹理分割是以纹理内容为基础,找到两个区域的边界,将图像分割为不同的区域
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    imgGray = cv2.imread("../images/Fig0943a.tif", flags=0) 
    # 根据小斑点直径(25) 设计圆形结构元,用闭运算删除小斑点
    kSize = (20, 20) # 结构元半径 10,小于小斑点半径
    element = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, kSize) # 圆形结构元
    imgClose1 = cv2.morphologyEx(imgGray, cv2.MORPH_CLOSE, element) # 灰度闭运算
    kSize = (60, 60) # 结构元半径 30,大于小斑点半径
    element = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, kSize) # 圆形结构元
    imgClose2 = cv2.morphologyEx(imgGray, cv2.MORPH_CLOSE, element) # 灰度闭运算
    kSize = (120, 120) # 结构元半径 60,大于大斑点半径
    element = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, kSize) # 圆形结构元
    imgOpen = cv2.morphologyEx(imgClose2, cv2.MORPH_OPEN, element) # 灰度开运算
    # 形态学梯度得到区域分割的边界
    kSize = (5, 5) # 结构元的尺寸
    element = cv2.getStructuringElement(cv2.MORPH_RECT, kSize) # 矩形结构元
    imgGrad = cv2.morphologyEx(imgOpen, cv2.MORPH_GRADIENT, element) # 形态学梯度
    # 纹理分割图像重建
    imgRebuild = cv2.bitwise_or(imgGray, imgGrad) # 计算交集

形态学之边缘角点检测?

  • 形态学边缘检测的原理是,图像中的物体在膨胀时向周围扩张,在腐蚀时会发生收缩,变化的区域都只发生在物体的边缘。图像的形态学梯度运算,是膨胀图像与腐蚀图像之差,可以得到图像的轮廓,通常用于提取物体边缘
  • 形态学角点检测的原理是,通过十字形、菱形、方形、X 型等不同形状结构元的膨胀腐蚀,使原图像的边缘不发生变化,仅有焦点被腐蚀
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    img = cv2.imread("../images/imgBuilding1.png", flags=1)
    imgSign = img.copy()
    imgGray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 图片格式转换:BGR(OpenCV) -> Gray
    # ret, imgBin = cv2.threshold(imgGray, 127, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU) # 二值化处理
    # 边缘检测
    element = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))
    imgEdge = cv2.morphologyEx(imgGray, cv2.MORPH_GRADIENT, element) # 形态学梯度
    # 构造 5×5 结构元素,十字形、菱形、方形、X 型
    cross = cv2.getStructuringElement(cv2.MORPH_CROSS, (5, 5)) # 十字型结构元
    square = cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5)) # 矩形结构元
    xShape = cv2.getStructuringElement(cv2.MORPH_CROSS, (5, 5)) # X 形结构元
    diamond = cv2.getStructuringElement(cv2.MORPH_CROSS, (5, 5)) # 构造菱形结构元
    diamond[1, 1] = diamond[3, 3] = 1
    diamond[1, 3] = diamond[3, 1] = 1
    print(diamond)
    imgDilate1 = cv2.dilate(imgGray, cross) # 用十字型结构元膨胀原图像
    imgErode1 = cv2.erode(imgDilate1, diamond) # 用菱形结构元腐蚀图像
    imgDilate2 = cv2.dilate(imgGray, xShape) # 使用 X 形结构元膨胀原图像
    imgErode2 = cv2.erode(imgDilate2, square) # 使用方形结构元腐蚀图像
    imgDiff = cv2.absdiff(imgErode2, imgErode1) # 将两幅闭运算的图像相减获得角点
    retval, thresh = cv2.threshold(imgDiff, 40, 255, cv2.THRESH_BINARY) # # 二值化处理
    # 在原图上用半径为 5 的圆圈标记角点
    for j in range(thresh.size):
    y = int(j / thresh.shape[0])
    x = int(j % thresh.shape[0])
    if (thresh[x, y] == 255):
    cv2.circle(imgSign, (y, x), 5, (255, 0, 255))

参考:

  1. 【youcans 的 OpenCV 例程 200 篇】113. 形态学操作之腐蚀_opencv 可以单通道腐蚀操作吗_youcans_的博客 - CSDN 博客
  2. 【youcans 的 OpenCV 例程 200 篇】114. 形态学操作之膨胀_youcans_的博客 - CSDN 博客
  3. 【youcans 的 OpenCV 例程 200 篇】115. 形态学操作之开运算_youcans_的博客 - CSDN 博客
  4. 【youcans 的 OpenCV 例程 200 篇】116. 形态学操作之闭运算_youcans_的博客 - CSDN 博客
  5. 【youcans 的 OpenCV 例程 200 篇】117. 形态学操作之顶帽运算_youcans_的博客 - CSDN 博客
  6. 【youcans 的 OpenCV 例程 200 篇】118. 形态学操作之底帽运算_youcans_的博客 - CSDN 博客
  7. 【youcans 的 OpenCV 例程 200 篇】119. 图像的形态学梯度_图像形态学梯度 opencv linux_youcans_的博客 - CSDN 博客
  8. Site Unreachable
  9. 【youcans 的 OpenCV 例程 200 篇】121. 击中 - 击不中用于特征识别_opencv 击中击不中_youcans_的博客 - CSDN 博客
  10. 【youcans 的 OpenCV 例程 200 篇】122. 形态算法之边界提取_youcans_的博客 - CSDN 博客
  11. 【youcans 的 OpenCV 例程 200 篇】123. 形态算法之孔洞填充_数字图像处理孔洞填充的算法_youcans_的博客 - CSDN 博客
  12. 【youcans 的 OpenCV 例程 200 篇】124. 孔洞填充的泛洪算法_youcans_的博客 - CSDN 博客
  13. 【youcans 的 OpenCV 例程 200 篇】127. 形态算法之细化_deutch 细化算法_youcans_的博客 - CSDN 博客
  14. 【youcans 的 OpenCV 例程 200 篇】126. 形态算法之凸壳(Convex hull)_youcans_的博客 - CSDN 博客
  15. 【youcans 的 OpenCV 例程 200 篇】125. 形态算法之提取连通分量_youcans_的博客 - CSDN 博客
  16. 【youcans 的 OpenCV 例程 200 篇】128. 形态算法之骨架 (skimage)_youcans_的博客 - CSDN 博客
  17. 【youcans 的 OpenCV 例程 200 篇】130. 形态学之提取水平和垂直线_youcans_的博客 - CSDN 博客
  18. 【youcans 的 OpenCV 例程 200 篇】139. 灰度顶帽变换校正阴影_youcans_的博客 - CSDN 博客
  19. 【youcans 的 OpenCV 例程 200 篇】140. 灰度底帽变换校正光照_youcans_的博客 - CSDN 博客
  20. 【youcans 的 OpenCV 例程 200 篇】142. 基于灰度形态学的图像平滑_youcans_的博客 - CSDN 博客
  21. 【youcans 的 OpenCV 例程 200 篇】144. 基于灰度形态学的纹理分割_opencv 纹理分割_youcans_的博客 - CSDN 博客
  22. 【youcans 的 OpenCV 例程 200 篇】145. 形态学之边缘和角点检测_youcans_的博客 - CSDN 博客