1.按短边筛选
原始轮廓图:
import cv2
import numpy as np
# 读取轮廓图
contour_image = cv2.imread('..\\IMGS\\pp_edge.png', cv2.IMREAD_GRAYSCALE)
# 使用cv2.findContours()函数获取所有轮廓
contours, _ = cv2.findContours(contour_image, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
# 设定的直径阈值
threshold_diameter = 25.0
# 遍历每个轮廓
new_image = np.zeros_like(contour_image)
for contour in contours:
rect = cv2.minAreaRect(contour) # 获取最小外接矩形
diameter = rect[1][0] if rect[1][0] <= rect[1][1] else rect[1][1] # 计算轮廓的最短尺寸,并获取直径
# 如果轮廓的直径小于设定的阈值,则忽略该轮廓
if diameter >= threshold_diameter:
cv2.drawContours(new_image, [contour], -1, (255, 255, 255))
print(diameter, end=', ')
cv2.imshow('Filtered Contours', new_image) # 绘制保留的轮廓
cv2.waitKey(0)
cv2.destroyAllWindows()
输出结果以及与原图对比:
2.按长边筛选
import cv2
import numpy as np
# 读取轮廓图
contour_image = cv2.imread('..\\IMGS\\pp_edge2.png', cv2.IMREAD_GRAYSCALE)
# 使用cv2.findContours()函数获取所有轮廓
contours, _ = cv2.findContours(contour_image, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
# 设定的直径阈值
threshold_diameter = 25.0
# 遍历每个轮廓
new_image = np.zeros_like(contour_image)
for contour in contours:
diameter = cv2.minEnclosingCircle(contour)[1] * 2 # cv2.minEnclosingCircle():计算一个轮廓的最小包围圆,返回值是圆心坐标和半径
# 如果轮廓的直径小于设定的阈值,则忽略该轮廓
if diameter >= threshold_diameter:
cv2.drawContours(new_image, [contour], -1, (255, 255, 255))
print(diameter, end=', ')
cv2.imshow('Filtered Contours', new_image) # 绘制保留的轮廓
cv2.waitKey(0)
cv2.destroyAllWindows()
结果图:
3.将粘连的轮廓分开
有经过灰度、二值化处理后的图如下,需要将图中的所有轮廓读出,用以进行尺寸测量和数量统计等后续操作。
看得出,图中有两个轮廓 粘连到了一起,如果直接进行查找和统计就会出错。
例如用下面代码将轮廓找出并画出:
import cv2
import numpy as np
input_img = cv2.imread('..\\IMGS\\pp_binary.png', cv2.IMREAD_GRAYSCALE) # 读取已转换过的轮廓图像
contours = cv2.findContours(input_img, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)[0] # 查找轮廓
tmp_img = np.zeros_like(input_img) # 创建空白临时图像
cv2.drawContours(tmp_img, contours, -1, 255) # 将轮廓绘制在临时图像上
cv2.imshow('OutPut', tmp_img) # 显示轮廓图
cv2.waitKey(0)
cv2.destroyAllWindows()
输出图如下:
看得出,轮廓是粘到一起的。为了将轮廓分开,初步的设想是,先用腐蚀的方法,如果存在轮廓粘连,那么腐蚀操作以后,图像就会被分成多个独立的部分,然后将每个独立的部分再进行膨胀操作恢复尺寸,并分别读取每个独立部分的轮廓,这样就实现了粘连轮廓的分离。
代码:
import cv2
import numpy as np
input_img = cv2.imread('..\\IMGS\\pp_binary.png', cv2.IMREAD_GRAYSCALE) # 读取已转换过的轮廓图像
contours = cv2.findContours(input_img, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)[0] # 查找轮廓
out_contours = [] # 创建用以输出的轮廓线列表
for contour in contours: # 针对每一个轮廓进行操作
rect = cv2.minAreaRect(contour) # 获取最小外接矩形
diameter = rect[1][0] if rect[1][0] <= rect[1][1] else rect[1][1] # 计算轮廓的最短尺寸,并获取直径
num_kernel = int(diameter / 2.5) # 定义核的大小
if num_kernel % 2 == 0:
num_kernel += 1
kernel = np.ones((num_kernel, num_kernel), np.uint8) # 定义核
tmp_img = np.zeros_like(input_img) # 创建空白临时图像
cv2.drawContours(tmp_img, [contour], -1, (255, 255, 255), thickness=cv2.FILLED) # 将当前的轮廓绘制在临时图像上
erode_img = cv2.erode(tmp_img, kernel, iterations=1) # 腐蚀操作,打破连接
tmp_contours = cv2.findContours(erode_img, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)[0] # 读取腐蚀操作后的临时图像的轮廓
if len(tmp_contours) > 1: # 如果存在轮廓粘连,那么腐蚀操作以后,图像就会被分成多个独立的部分
for tmp in tmp_contours:
tmp_img = np.zeros_like(input_img) # 创建空白临时图像
cv2.drawContours(tmp_img, [tmp], -1, (255, 255, 255), thickness=cv2.FILLED) # 将轮廓绘制在临时图像上
dilate_img = cv2.dilate(tmp_img, kernel, iterations=1) # 膨胀操作,恢复轮廓尺寸
_ = cv2.findContours(dilate_img, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)[0] # 读取膨胀操作后的临时图像的轮廓
out_contours.extend(_) # 将膨胀操作后的轮廓加入输出用的轮廓列表
else:
out_contours.extend([contour]) # 不存在轮廓粘连,直接将当前轮廓加入输出用的轮廓列表
# 在原图上绘制轮廓
tmp_img = np.zeros_like(input_img) # 创建空白临时图像,用以输出
cv2.drawContours(tmp_img, out_contours, -1, 255) # 在临时图像上画出所有轮廓
# 显示结果
mask_img = cv2.cvtColor(tmp_img, cv2.COLOR_GRAY2BGR) # 掩模图,用以将轮廓叠加在原图上
mask_img[tmp_img == 255] = [0, 255, 0] # 按照临时图中的白色位置,将掩模图中的对应位置变为绿色
gray_img_3ch = cv2.cvtColor(input_img, cv2.COLOR_GRAY2BGR) # 转换创建RGB图,用以显示
show_img = cv2.addWeighted(gray_img_3ch, 0.5, mask_img, 1, 0) # 将掩模图与灰色3通道图叠加输出
cv2.imshow('OutPut', show_img)
cv2.waitKey(0)
cv2.destroyAllWindows()
输出结果:
粘连的轮廓已经被分开,成为两个相邻的独立轮廓。当然了,由于使用了先腐蚀再膨胀的操作,轮廓尺寸会有一些精度上的损失,但是比起将两个轮廓误统计为一个,误差还是小多了。
题外话,先腐蚀再膨胀看上去很熟悉是吧,不就是开操作吗?直接使用开操作行不行?我的实验结果是不行,会得到类似下面这样的结果: