测量图片中物体大小

测量图片中物体直径

    • 边缘探测
    • 旋转图像
    • 图像去除黑边
    • 测量宽度
    • 计算图形旋转角度
    • 对图像加工(核心)
    • 使用
    • 总结

昨天和今天没有学习,有个学弟找我帮忙做个小demo,说要交差应付一下作业。想法是测量一下图片中近似于圆柱的物体的直径。因为我之前写过一个雷达图像的点云目标检测,我当时想这俩差不多,我应该俩小时就能写完,毕竟网上很多东西都是现成的,一边抄一点就够了,但是还是有很多坑要踩,过程中发现自己的代码编写能力真实差的雅痞。

边缘探测

之前的雷达云图实际不存在边缘探测的问题,这也是我后来意识到的问题。雷达云图之前的探测就是转为灰度图,然后二值化,形态学操作,再找边缘就可以了。但是我在做这个的时候发现两者之间区别还是很大的,我要是想把目标图片也二值化的结果很糟糕,我就在网上找了类似的。发现他没有二值化,直接高斯滤波之后就findcounter了。结果竟然挺棒的。很简单,就是直接用canny边缘算子滤波之后得到结果再寻找边缘。
这里是参考的代码
然后我就尝试了一下之后发现确实不用很复杂:

  1. 转为灰度图
  2. 高斯滤波
  3. 边缘检测
  4. 膨胀腐蚀
  5. 寻找边界
def detect_cnts(img):
    # 读取输入图片
    image = img
    # 输入图片灰度化
    gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
    # 对灰度图片执行高斯滤波
    gray = cv2.GaussianBlur(gray, (7, 7), 0)

    # 二值化
    # 对滤波结果做边缘检测获取目标
    edged = cv2.Canny(gray, 50, 100)
    # 使用膨胀和腐蚀操作进行闭合对象边缘之间的间隙
    edged = cv2.dilate(edged, None, iterations=1)
    edged = cv2.erode(edged, None, iterations=1)

    # 在边缘图像中寻找物体轮廓(即物体)
    cnts = cv2.findContours(edged.copy(), cv2.RETR_EXTERNAL,
                            cv2.CHAIN_APPROX_SIMPLE)
    cnts = imutils.grab_contours(cnts)  # 提取轮廓
    # 对轮廓按照从左到右进行排序处理
    (cnts, _) = contours.sort_contours(cnts)

    return cnts

该函数返回的就是探测到的边界

旋转图像

探测到边界之后理论上直接对外界矩形进行测量宽度就可以了,但是我的目标不是很规则,他是有点奇形怪状的,外接矩形的宽度和他实际的直径投影差的太多了,就得想办法。我一开始想根据外界矩形,把内部切割成几个部分重新检测得到结果。但是我发现最难的是再opencv里图像旋转的函数我不知道是怎么运作的,我自己写的点旋转函数和图像旋转并不重合,很重要的原因在于如果想要旋转后的图像中心不变,是需要对M矩阵进行平移变化的,我不懂。所以我没法知道矩形旋转后的坐标,就没法切割。于是想法变成了先把原图像按照外界矩形框旋转了再说。这里仅涉及到了图片旋转之后中心变了的问题的问题。这个方法也是我在网上找的。

def rotate_bound(image, angle):
    # grab the dimensions of the image and then determine the
    # center
    (h, w) = image.shape[:2]
    (cX, cY) = (w // 2, h // 2)

    # grab the rotation matrix (applying the negative of the
    # angle to rotate clockwise), then grab the sine and cosine
    # (i.e., the rotation components of the matrix)
    M = cv2.getRotationMatrix2D((cX, cY), angle, 1.0)
    cos = np.abs(M[0, 0])
    sin = np.abs(M[0, 1])

    # compute the new bounding dimensions of the image
    nW = int((h * sin) + (w * cos))
    nH = int((h * cos) + (w * sin))

    # adjust the rotation matrix to take into account translation
    M[0, 2] += (nW / 2) - cX
    M[1, 2] += (nH / 2) - cY

    # perform the actual rotation and return the image
    return cv2.warpAffine(image, M, (nW, nH))

图像去除黑边

用上面的方法旋转的话会出现黑边。我就想怎么去除,想了很久没想到。但是我找到了一个函数cv2.fillPoly,它可以通过mask使轮廓外的内容被覆盖成黑色。这样的话,旋转之后的图像就可以处理为只有我想要内容的图像,背景是黑色的,这样的话旋转出黑边也没有关系了。我在这里真的是很意外找到的,超级感谢。

def remove_backroudn(c, image):
    """
    # 根据轮廓去除图像背景
    :param c: 轮廓
    :param image:
    :return: 去除边界意外内容的图像
    """
    # 全黑
    mask = np.zeros(image.shape).astype(image.dtype)
    # 将contours里填充白色
    color = [255, 255, 255]
    cv2.fillPoly(mask, [c], color)
    # mask与real相与
    result = cv2.bitwise_and(image, mask)
    return result

测量宽度

为了避免直接以外界矩形的宽度为结果,我想了好久。我的想法是之前的到了背景为黑色的旋转图像。把目标旋转成垂直的,那么只要求出有颜色的点的横向最大距离,不就相当于最大的y半径了吗。所以此处求出所有非零点的索引,然后求同一水平线中,最左边的索引值(x最小)和最右边的索引值(x最大)的差值作为宽度。
注意到我其实一开始是想旋转成水平的,但是np.nonzero返回的是相同y在一起的,所以就变成竖直的,在相同的y下测量边界在x的最大变化值作为半径。

def cal_height(rotate_img):
    """
    计算不为0的位置,然后横坐标相减,得出宽度
    :param rotate_img_list:
    :return:
    """
    height_list = []

    img_gray = cv2.cvtColor(rotate_img, cv2.COLOR_BGR2GRAY)
    nonzero_index = np.nonzero(img_gray)
    nonzero_index = np.array(nonzero_index)
    for i in range(nonzero_index.shape[1]):
        if i == 0:
            height_max = nonzero_index[1][0]
            height_min = nonzero_index[1][0]
            continue
        if nonzero_index[0][i] == nonzero_index[0][i-1]:
            if height_max < nonzero_index[1][i]:
                height_max = nonzero_index[1][i]
            if height_min > nonzero_index[1][i]:
                height_min = nonzero_index[1][i]
        else:
            height_list.append(height_max - height_min)
            # 这里y变换的情况下,x的最大最小要重新赋值
            height_max = nonzero_index[1][i]
            height_min = nonzero_index[1][i]
    height_result = max(height_list)

    return height_result

计算图形旋转角度

上面说到想把图像旋转到目标为垂直状态,想法就是求最长边旋转到数值的角度就可以了。box是外切矩形的四个顶点坐标。

def cal_angle(box):
    """
    根据轮廓计算外接矩形
    :param c:
    :return:
    """
    # 计算旋转角度
    (tl, tr, br, bl) = box

    dA = cal_distance(tl, tr)
    dB = cal_distance(tl, bl)

    # 算出旋转角度beta
    if dA > dB:
        pot_1 = tr
        pot_2 = br
    else:
        pot_1 = tr
        pot_2 = tl

    x_distance = pot_1[0] - pot_2[0]
    y_distance = pot_1[1] - pot_2[1]
    beta = math.atan(y_distance / x_distance)  # 弧度制
    beta = beta / math.pi * 180.0

    return beta

对图像加工(核心)

遍历所有检测到的轮廓,轮廓的面积太小的话忽略。

  1. 去除轮廓外内容
  2. 计算外切矩形
  3. 讲矩形的四个顶点有红圈画出来
  4. 计算旋转角度并旋转到目标竖直
  5. 计算x方向的最大值
  6. 将高度标到图上
def draw_outline(image, cnts):
    # 循环遍历每一个轮廓
    orig = image.copy()
    for c in cnts:
        # 如果当前轮廓的面积太少,认为可能是噪声,直接忽略掉
        if cv2.contourArea(c) < 200:
            continue


        # 根据轮廓去除图像背景
        result = remove_backroudn(c, image)


        # 根据物体轮廓计算出外切矩形框
        box = cv2.minAreaRect(c)
        box = cv2.cv.BoxPoints(box) if imutils.is_cv2() else cv2.boxPoints(box)
        box = np.array(box, dtype="int")

        # 按照top-left, top-right, bottom-right, bottom-left的顺序对轮廓点进行排序,并绘制外切的BB,用绿色的线来表示
        box = perspective.order_points(box)
        cv2.drawContours(orig, [box.astype("int")], -1, (0, 255, 0), 2)

        # 绘制BB的4个顶点,用红色的小圆圈来表示
        for (x, y) in box:
            print(x, y)
            cv2.circle(orig, (int(x), int(y)), 5, (0, 0, 255), -1)

        beta = cal_angle(box)

        # 旋转
        rotate_img = rotate_bound(result, beta)

        # 计算高度
        height_result = cal_height(rotate_img)
        # 比例尺换算

        # 计算高度书写位置
        # 分别计算top-left 和top-right的中心点和bottom-left 和bottom-right的中心点坐标
        (tl, tr, br, bl) = box
        (tltrX, tltrY) = midpoint(tl, tr)
       # 把高度写上
        cv2.putText(orig, "{:.1f}in".format(height_result),
                    (int(tltrX - 15), int(tltrY - 10)), cv2.FONT_HERSHEY_SIMPLEX,
                    0.65, (255, 255, 255), 2)

    return orig

使用

使用过程很简单,先检测出所有轮廓,然后根据轮廓进行处理就可以了

def work(image):
    cnts = detect_cnts(image)
    result = draw_outline(image, cnts)
    # result = np.concatenate((image,result), axis=1)	# 把结果和输入一起显示用的
    return result

最后在主函数调用work就可以了。结果如下,图里标注的单位是骗人的,实际就是像素,哈哈哈哈哈。

总结

以上并没有和比例尺联系起来,测出的长度就是像素长度。可以把比例尺作为参数传入进行修改,这个就看大家想怎么做就怎么做了。
1.重构代码时候发现自己写的真的和一坨屎一样
2.其实最难的轮廓检测我写的是最简单的,所有的精力都放在代码的其他错误了
3.改代码并不适用于复杂环境,结果很难受。但是单纯背景下可以同时测量多个物体的尺寸大小(只要canny算子分的开,笑)
4.有一些参考我没写出来是当时连接都丢了,我懒得再找一边,但是直接百度肯定能找到的

好累啊,感觉根本就毕不了业,学的和屎一样。所以我要去召唤师峡谷了,寻找仅存的安慰。希望这些东西能帮助到大家,如果不能就在此祝大家生活愉快,笑。

你可能感兴趣的:(数字图像处理,图像识别,cv)