OpenCV-Python 通过边缘检测识别物体并批量提取(大米识别为例)——minAreaRect批量生成物体的最小外接矩形(旋转矩形)并批量裁剪

OpenCV版本:4.0.0.21(已兼容4.5.2.X版本)

算法实现思路如下:

  1. 对图像做降噪滤波处理
  2. 提取边缘
  3. 检测轮廓
  4. 检测轮廓最小外接矩形(旋转矩形)
  5. 旋转图像
  6. 裁剪

代码如下:

import cv2
import numpy as np

image = cv2.imread("rice.jpg")
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)  # 转为灰度图
blurred = cv2.GaussianBlur(gray, (11, 11), 0)
edge = cv2.Canny(blurred, 30, 150)  # 用Canny算子提取边缘
cv2.imwrite("./results/edge.jpg", edge)

contour = image.copy()
(cnts, _) = cv2.findContours(edge, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)  # 轮廓检测
cv2.drawContours(contour, cnts, -1, (0, 255, 0), 2)  # 绘制轮廓
cv2.imwrite("./results/contour.jpg", contour)

count = 0  # 米粒个数
margin = 5  # 裁剪边距
draw_rect = image.copy()
for i, contour in enumerate(cnts):
    area = cv2.contourArea(contour)  # 计算包围形状的面积
    if area < 15:  # 过滤面积小于15的形状
        continue
    count += 1
    rect = cv2.minAreaRect(contour)  # 检测轮廓最小外接矩形,得到最小外接矩形的(中心(x,y), (宽,高), 旋转角度)
    box = np.int0(cv2.boxPoints(rect))  # 获取最小外接矩形的4个顶点坐标
    cv2.drawContours(draw_rect, [box], 0, (255, 0, 0), 2)  # 绘制轮廓最小外接矩形

    h, w = image.shape[:2]  # 原图像的高和宽
    rect_w, rect_h = int(rect[1][0]) + 1, int(rect[1][1]) + 1  # 最小外接矩形的宽和高
    if rect_w <= rect_h:
        x, y = int(box[1][0]), int(box[1][1])  # 旋转中心
        M2 = cv2.getRotationMatrix2D((x, y), rect[2], 1)
        rotated_image = cv2.warpAffine(image, M2, (w * 2, h * 2))
        y1, y2 = y - margin if y - margin > 0 else 0, y + rect_h + margin + 1
        x1, x2 = x - margin if x - margin > 0 else 0, x + rect_w + margin + 1
        rotated_canvas = rotated_image[y1: y2, x1: x2]
    else:
        x, y = int(box[2][0]), int(box[2][1])  # 旋转中心
        M2 = cv2.getRotationMatrix2D((x, y), rect[2] + 90, 1)
        rotated_image = cv2.warpAffine(image, M2, (w * 2, h * 2))
        y1, y2 = y - margin if y - margin > 0 else 0, y + rect_w + margin + 1
        x1, x2 = x - margin if x - margin > 0 else 0, x + rect_h + margin + 1
        rotated_canvas = rotated_image[y1: y2, x1: x2]
    print("rice #{}".format(count))
    # cv2.imshow("rotated_canvas", rotated_canvas)
    cv2.imwrite("./rotation-results/{}.jpg".format(count), rotated_canvas)
    cv2.waitKey(0)
cv2.imwrite("./results/rect.jpg", draw_rect)

        原图像如下:

        裁剪出来的部分效果如下:

gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)  # 转为灰度图
blurred = cv2.GaussianBlur(gray, (11, 11), 0)
edge = cv2.Canny(blurred, 30, 150)  # 用Canny算子提取边缘
cv2.imwrite("./results/edge.jpg", edge)

        将读取的图像转为灰度图,然后做高斯降噪滤波处理,再用Canny算子检测边缘转换为二值图,效果如下:

contour = image.copy()
(cnts, _) = cv2.findContours(edge, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)  # 轮廓检测
cv2.drawContours(contour, cnts, -1, (0, 255, 0), 2)  # 绘制轮廓
cv2.imwrite("./results/contour.jpg", contour)

        cv2.findContours()函数可以用来检测轮廓。它的第一个参数是要检测的图像,必须是二值图,即黑白的(不是灰度图),所以读取的图像要先转成灰度的,再转成二值图。第二个参数有四种,表示轮廓的检索模式:

  • cv2.RETR_EXTERNAL表示只检测外轮廓
  • cv2.RETR_LIST检测的轮廓不建立等级关系
  • cv2.RETR_CCOMP建立两个等级的轮廓,上面的一层为外边界,里面的一层为内孔的边界信息。如果内孔内还有一个连通物体,这个物体的边界也在顶层
  • cv2.RETR_TREE建立一个等级树结构的轮廓

        第三个参数为轮廓的近似办法:

  • cv2.CHAIN_APPROX_NONE存储所有的轮廓点,相邻的两个点的像素位置差不超过1,即max(abs(x1-x2),abs(y2-y1))==1
  • cv2.CHAIN_APPROX_SIMPLE压缩水平方向,垂直方向,对角线方向的元素,只保留该方向的终点坐标,例如一个矩形轮廓只需4个点来保存轮廓信息

        cv2.findContours()函数有两个返回值,一个是轮廓本身,还有一个是每条轮廓对应的属性

  1. contour返回值:第一个值返回的是list,list中每个元素都是图像中的一个轮廓,用numpy中的ndarray表示。每一个ndarray里保存的是轮廓上的各个点的坐标。
  2. hierarchy返回值:此外,该函数还可返回一个可选的hiararchy结果,这是一个ndarray,其中的元素个数和轮廓个数相同,每个轮廓contours[i]对应4个hierarchy元素hierarchy[i][0]~hierarchy[i][3],分别表示后一个轮廓、前一个轮廓、父轮廓、内嵌轮廓的索引编号,如果没有对应项,则该值为负数。

        cv2.drawContours()函数可以在图像上绘制轮廓:

  1. 第一个参数是指明在哪幅图像上绘制轮廓
  2. 第二个参数是轮廓本身,在Python中是一个list
  3. 第三个参数指定绘制轮廓list中的哪条轮廓,如果是-1,则绘制其中的所有轮廓
  4. 第四个参数是轮廓线条的颜色
  5. 第五个参数是轮廓线条的粗细

        绘制轮廓效果如下:

for i, contour in enumerate(cnts):
    ares = cv2.contourArea(contour)  # 计算包围形状的面积
    print(ares)
    if ares < 15:  # 过滤面积小于15的形状
        continue
    count += 1

        因为cv2.findContours()返回的第一个值是listlist中每个元素都是图像中的一个轮廓,所以就可以用for循环对返回值进行遍历(批量处理)。cv2.contourArea()可以计算包围形状的面积,当检测的轮廓是个很小的点或者不规则时,就可以设置包围形状面积的大小过滤掉(这个函数计算的面积有时候不太准确,使用须谨慎)。

rect = cv2.minAreaRect(contour)  # 检测轮廓最小外接矩形,得到最小外接矩形的(中心(x,y), (宽,高), 旋转角度)
box = np.int0(cv2.boxPoints(rect))  # 获取最小外接矩形的4个顶点坐标
cv2.drawContours(image, [box], 0, (255, 0, 0), 2)  # 绘制轮廓最小外接矩形

        cv2.minAreaRect()可以得到最小面积的矩形,这个矩形是可以有偏转角度的,可以与图像的边界不平行。返回一个Box2D结构rect:(最小外接矩形的中心(x,y),(宽,高),旋转角度θ),但是要绘制这个矩形,需要通过函数cv2.boxPoints()获得矩形的顶点坐标box,返回形式[ [x0,y0], [x1,y1], [x2,y2], [x3,y3] ]。用cv2.drawContours()绘制出来的效果如下:

        这里说一下cv2.minAreaRect()cv2.boundingRect()的区别。cv2.boundingRect()得到的是包覆轮廓的最小正矩形,cv2.minAreaRect()得到的是包覆轮廓的最小外接矩形。下图蓝色框是最小外接矩形,红色框是最小正矩形

        裁剪最小外接矩形(旋转矩形)通常的做法是通过旋转,将矩形的四个坐标点做映射,然后求出被旋转后图像的四个点的坐标再裁剪,这个方法需要通过坐标变换求出一系列的参数似乎有点麻烦。现在来看看旋转函数cv2.getRotationMatrix2D()的参数:

  1. 旋转时固定的点

  2. 旋转角度

  3. 图片缩放尺度

        那么,根据cv2.getRotationMatrix2D()的第一个参数旋转时固定的点(即旋转中心)还有cv2.boxPoints()返回矩形的四个顶点坐标,就可以以矩形的其中一个顶点坐标作为旋转中心进行旋转,然后再根据cv2.minAreaRect()函数返回的参数((宽,高),旋转角度θ)在旋转中心的坐标上加上矩形的宽和高就可以进行裁剪。(注意:这里裁剪是以矩形的左上顶点进行裁剪)

h, w = image.shape[:2]  # 原图像的长和宽
rect_w, rect_h = int(rect[1][0]) + 1, int(rect[1][1]) + 1  # 最小外接矩形的宽和高
if rect_w <= rect_h:
    x, y = int(box[1][0]), int(box[1][1])  # 旋转中心
    M2 = cv2.getRotationMatrix2D((x, y), rect[2], 1)
    rotated_image = cv2.warpAffine(image, M2, (w * 2, h * 2))
    y1, y2 = y - margin if y - margin > 0 else 0, y + rect_h + margin + 1
    x1, x2 = x - margin if x - margin > 0 else 0, x + rect_w + margin + 1
    rotated_canvas = rotated_image[y1: y2, x1: x2]

        因为裁剪需要的参数是整数,所以rect_wrect_h这里设置为int类型,而int是向下取整,所以需要加1,不然裁剪的时候会损失精度。矩形旋转为竖直的时候,需要分两种情况来讨论,一是当矩形的宽<=矩形的高时,旋转矩形顺时针旋转旋转角度θ为负),示意图如下:

        cv2.getRotationMatrix2D()返回的是个矩阵,通过cv2.warpAffine()仿射变换完成旋转,其中cv2.warpAffine()的第三个参数设置为原图的2倍,是为了防止旋转之后目标区域变换到图像外面。裁剪的时候再加1是因为切片符号取不到右边的值,所以加1

 else:
    x, y = int(box[2][0]), int(box[2][1])  # 旋转中心
    M2 = cv2.getRotationMatrix2D((x, y), rect[2] + 90, 1)
    rotated_image = cv2.warpAffine(image, M2, (w * 2, h * 2))
    y1, y2 = y - margin if y - margin > 0 else 0, y + rect_w + margin + 1
    x1, x2 = x - margin if x - margin > 0 else 0, x + rect_h + margin + 1
    rotated_canvas = rotated_image[y1: y2, x1: x2]

        矩形旋转为竖直的第二种情况是,当矩形的宽>矩形的高时,旋转矩形逆时针旋转旋转角度θ为负),示意图如下:

        当矩形的宽>矩形的高时,旋转矩形按旋转角度θ顺时针旋转为图中的绿色矩形,因为这里裁剪是以矩形的左上顶点进行裁剪,所以还需要加一个90°进行逆时针旋转红色矩形,最后旋转矩形蓝色)到正矩形红色)整体看起来就是逆时针旋转

        注意:

  • 旋转角度θ是水平轴(x轴)逆时针旋转到矩形的第一条边的夹角,这个边是width,另一条边是height。这里的width与height是相对的。(特殊情况:正矩形的旋转角度θ为-0)

参考链接:

OpenCV 识别图片中的米粒个数,并计算米粒的平均面积和长度_逗创科技——逗创创的博客-CSDN博客

对硬币、销钉、大米进行图像分割(附代码)_小小何先生的学习之旅-CSDN博客

使用OpenCV-python提取图片中的硬币_凯哥大数据——刘凯的博客-CSDN博客

python利用四个坐标点对图片目标区域最小外接矩形进行裁剪_u014662865的博客-CSDN博客

使用Python和OpenCV检测图像中的物体并将物体裁剪下来_liqiancao的专栏-CSDN博客

python opencv minAreaRect 生成最小外接矩形_lanyuelvyun的博客-CSDN博客_cv2.minarearect

OpenCV 中boundingRect、minAreaRect、minEnclosingCircle用法_线上幽灵-CSDN博客_cv2.minenclosingcircle

猜你喜欢

转载自blog.csdn.net/weixin_41611054/article/details/106324887