如何用OpenCV结合分水岭算法区分聚集木屑并测量尺寸
基于OpenCV分水岭算法分割聚集木屑并测量尺寸
当前使用OpenCV结合硬币参考物测量图像中木屑尺寸的方案,在木屑聚集时会被findContours识别为单个轮廓。以下是原实现代码,以及针对该问题的分水岭算法优化方案,实现聚集物体的精准分割并计算每个物体的相对尺寸。
原实现代码
# 读取图像并预处理(转为灰度图) image = cv2.imread(img_path) gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) thresh = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU)[1] # 使用高斯核模糊去除不必要的边缘 # blur = cv2.GaussianBlur(gray, (9, 9), 0) # blur = cv2.medianBlur(gray, 7) blur = cv2.bilateralFilter(thresh,11, 125, 125) # nbw = cv2.fastNlMeansDenoising(image, None, 10, 7, 21) # Canny边缘检测 edged = cv2.Canny(blur, 50, 100) edged = cv2.dilate(edged, None, iterations=2) edged = cv2.erode(edged, None, iterations=2) kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (6,6)) closing = cv2.morphologyEx(edged, cv2.MORPH_CLOSE, kernel) # show_images([blur, edged]) # 查找轮廓 cnts = cv2.findContours(closing.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) cnts = imutils.grab_contours(cnts) # 从左到右排序轮廓(最左侧为参考物) (cnts, _) = contours.sort_contours(cnts) # 过滤过小的轮廓,去除噪声 cnts = [x for x in cnts if cv2.contourArea(x) > 400] # 参考物尺寸设置 # 此处使用2cm x 2cm的正方形作为参考(实际应为硬币,需调整对应尺寸) ref_object = cnts[0] box = cv2.minAreaRect(ref_object) box = cv2.boxPoints(box) box = np.array(box, dtype="int") box = perspective.order_points(box) (tl, tr, br, bl) = box dist_in_pixel = euclidean(tl, tr) dist_in_cm = 2 pixel_per_cm = dist_in_pixel/dist_in_cm # 绘制轮廓并标注尺寸 measurement=[] for cnt in cnts: box = cv2.minAreaRect(cnt) box = cv2.boxPoints(box) box = np.array(box, dtype="int") box = perspective.order_points(box) (tl, tr, br, bl) = box cv2.drawContours(image, [box.astype("int")], -1, (0, 0, 255), 2) mid_pt_horizontal = (tl[0] + int(abs(tr[0] - tl[0])/2), tl[1] + int(abs(tr[1] - tl[1])/2)) mid_pt_verticle = (tr[0] + int(abs(tr[0] - br[0])/2), tr[1] + int(abs(tr[1] - br[1])/2)) wid = euclidean(tl, tr)/pixel_per_cm ht = euclidean(tr, br)/pixel_per_cm measurement.append((wid,ht)) cv2.putText(image, "{:.1f}cm".format(wid), (int(mid_pt_horizontal[0] - 15), int(mid_pt_horizontal[1] - 10)), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 255, 0), 2) cv2.putText(image, "{:.1f}cm".format(ht), (int(mid_pt_verticle[0] + 10), int(mid_pt_verticle[1])), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 255, 0), 2) cv2_imshow(image) cv2.waitKey(0) cv2.destroyAllWindows()
分水岭算法优化实现
分水岭算法通过标记确定前景、确定背景和未知边界区域,实现粘连物体的精准分割。核心步骤及完整代码如下:
核心步骤
- 预处理调整:先对灰度图去噪,再进行二值化,用形态学开运算去除小噪声点
- 标记确定前景:通过距离变换获取物体中心区域,再阈值处理得到精准前景标记
- 标记确定背景:对二值图进行膨胀操作,确保包裹所有物体的区域为背景
- 计算未知区域:用背景区域减去前景区域,得到需要分割的边界区域
- 标记连通组件:用
cv2.connectedComponents标记前景连通区域,调整标记规则适配分水岭算法 - 应用分水岭:调用
cv2.watershed完成分割,提取每个物体的轮廓并计算尺寸
完整优化代码
import cv2 import numpy as np import imutils from scipy.spatial import distance as dist from imutils import perspective from imutils import contours def euclidean(a, b): return dist.euclidean(a, b) # 读取图像(替换为你的图像路径) img_path = "your_image_path.jpg" image = cv2.imread(img_path) original = image.copy() gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) # 预处理:去噪 + 二值化(反向二值化让物体为白色) blur = cv2.medianBlur(gray, 5) thresh = cv2.threshold(blur, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)[1] # 形态学开运算去除小噪声 kernel = np.ones((3,3), np.uint8) opening = cv2.morphologyEx(thresh, cv2.MORPH_OPEN, kernel, iterations=2) # 确定背景区域:膨胀操作 sure_bg = cv2.dilate(opening, kernel, iterations=3) # 确定前景区域:距离变换 + 阈值(0.2可根据图像调整) dist_transform = cv2.distanceTransform(opening, cv2.DIST_L2, 5) ret, sure_fg = cv2.threshold(dist_transform, 0.2*dist_transform.max(), 255, 0) sure_fg = np.uint8(sure_fg) # 计算未知边界区域 unknown = cv2.subtract(sure_bg, sure_fg) # 标记连通组件 ret, markers = cv2.connectedComponents(sure_fg) # 所有标记加1,背景设为1(避免0被分水岭视为未知区域) markers = markers + 1 # 未知区域标记为0 markers[unknown == 255] = 0 # 应用分水岭算法,分割边界标记为-1 markers = cv2.watershed(image, markers) image[markers == -1] = [255, 0, 0] # 分割边界设为红色 # 提取所有分割后的物体轮廓(排除背景和边界) cnts = [] for label in np.unique(markers): if label == 1 or label == -1: continue # 创建掩码提取当前物体 mask = np.zeros(gray.shape, dtype="uint8") mask[markers == label] = 255 # 查找当前物体的轮廓 cnt = cv2.findContours(mask.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) cnt = imutils.grab_contours(cnt) if len(cnt) > 0: cnts.append(cnt[0]) # 过滤过小的轮廓 cnts = [x for x in cnts if cv2.contourArea(x) > 400] # 排序轮廓,确定参考物(假设最左侧为硬币) (cnts, _) = contours.sort_contours(cnts) ref_object = cnts[0] # 计算像素与厘米的比例(替换为你的硬币实际直径,如1元硬币为2.5cm) box = cv2.minAreaRect(ref_object) box = cv2.boxPoints(box) box = np.array(box, dtype="int") box = perspective.order_points(box) (tl, tr, br, bl) = box dist_in_pixel = euclidean(tl, tr) dist_in_cm = 2.5 # 硬币实际直径(单位:cm) pixel_per_cm = dist_in_pixel / dist_in_cm # 绘制轮廓并标注尺寸 measurement = [] for cnt in cnts: # 计算最小外接矩形 box = cv2.minAreaRect(cnt) box = cv2.boxPoints(box) box = np.array(box, dtype="int") box = perspective.order_points(box) (tl, tr, br, bl) = box # 绘制轮廓 cv2.drawContours(original, [box.astype("int")], -1, (0, 0, 255), 2) # 计算宽度和高度 wid = euclidean(tl, tr) / pixel_per_cm ht = euclidean(tr, br) / pixel_per_cm measurement.append((wid, ht)) # 标注尺寸 mid_pt_horizontal = (tl[0] + int(abs(tr[0]-tl[0])/2), tl[1] + int(abs(tr[1]-tl[1])/2)) mid_pt_vertical = (tr[0] + int(abs(tr[0]-br[0])/2), tr[1] + int(abs(tr[1]-br[1])/2)) cv2.putText(original, "{:.1f}cm".format(wid), (int(mid_pt_horizontal[0]-15), int(mid_pt_horizontal[1]-10)), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255,255,0), 2) cv2.putText(original, "{:.1f}cm".format(ht), (int(mid_pt_vertical[0]+10), int(mid_pt_vertical[1])), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255,255,0), 2) # 显示结果 cv2.imshow("Segmented and Measured", original) cv2.waitKey(0) cv2.destroyAllWindows()
关键调整说明
- 参考物适配:代码默认硬币直径为2.5cm(1元硬币),请根据实际使用的硬币修改
dist_in_cm值 - 前景阈值:
0.2*dist_transform.max()可根据图像调整,值越高前景标记越靠近物体中心,分割越保守;值越低则前景范围越大,分割更彻底 - 噪声过滤:通过形态学开运算和轮廓面积过滤,去除图像中的小噪点干扰
内容的提问来源于stack exchange,提问作者Marten Meckelburg
相关产品推荐
相关产品推荐

