使用OpenCV测量图像中物体的大小

本文翻译自pyimagesearch技术博客上的一篇文章,《Measuring size of objects in an image with OpenCV》,原文作者:Adrian Rosebrock 。
网址:https://www.pyimagesearch.com/2016/03/28/measuring-size-of-objects-in-an-image-with-opencv/
这里写图片描述

使用OpenCV测量图像中物体的大小

图像目标尺寸检测类似于计算从我们的相机到一个物体的距离——在这两种情况下,我们都需要事先定义一个比率来测量每个给定度量单位的像素数(pixels_per_metric)。在这里所说的这个被称为“pixels_per_metric”的比率指标,我在接下来的部分中对其更正式的定义。

pixels_per_metric

为了确定图像中物体的大小,我们首先需要使用一个参照物作为“校准”点。我们的参照物应该有两个重要的属性:

  1. 我们应该知道这个物体的真实尺寸(在宽度或高度上的毫米或英寸等值的大小)。
  2. 我们应该能够轻松地在图片中找到这个参照物,要么基于参照物的位置(如,参照物可以是一副图像中左上角的物体)或基于参照物的外表(例如参照物可以是图片中具有最独特的颜色或独一无二的形状,不同于所有其他的物体)。
    在任何一种情况下,我们的参考应该以某种方式唯一可识别。

在这个例子中,我们将使用美分硬币作为我们的参照物,并且在所有示例中,确保它始终是我们图像中最左边的对象。
这里写图片描述
图1:我们将使用美分硬币作为参照物,并确保它始终处于图像最左侧位置,这使得我们可以通过对它们位置的轮廓大小进行排序,进一步来提取信息。

通过保证美分硬币是最左边的物体,我们可以从左到右对我们的物体等高线区域进行排列,抓住这个硬币(它将始终对应于排序列表中的第一个等高线区域)。并使用它来定义我们的pixels_per_metric比率,我们将其定义为:

pixels_per_metric =物体像素宽 / 物体真实宽

美分硬币的真实宽度是0.955英寸。现在,假设我们图像中硬币的像素宽为150像素(基于它的相关边界框)。那么这种情况下pixels_per_metric这样计算:

pixels_per_metric = 150px / 0.955in = 157px

因此,在我们这幅图像中,每英寸大约有157个像素。有了这个比率,我们可以计算图像中其他物体的大小了。

利用计算机视觉测量物体大小

既然我们已经理解了pixels_per_metric,我们就可以实现用于测量图像中对象大小的Python程序脚本了。
打开一个新的py文件,插入以下代码:

# import the necessary packages
from scipy.spatial import distance as dist
from imutils import perspective
from imutils import contours
import numpy as np
import argparse
import imutils
import cv2

def midpoint(ptA, ptB):
    return ((ptA[0] + ptB[0]) * 0.5, (ptA[1] + ptB[1]) * 0.5)

# construct the argument parse and parse the arguments
ap = argparse.ArgumentParser()
ap.add_argument("-i", "--image", required=True,
    help="path to the input image")
ap.add_argument("-w", "--width", type=float, required=True,
    help="width of the left-most object in the image (in inches)")
args = vars(ap.parse_args())

第2-8行用来导入我们所需的Python包。在本例中,我们将大量使用imutils包,因此,如果您没有安装它,请确保在使用之前安装它:

$ pip install imutils

另外如果你已经安装过这个imutils包,也请确保它为最新版本,在我这里所使用最新的版本是0.3.6。

$ pip install --upgrade imutils

第10行和第11行定义了一个midpoint函数,顾名思义,它用于计算两个(x,y)坐标之间的中点。

然后我们在第14-19行中解析我们的命令行参数。我们需要两个参数,–image,它是我们输入图像的路径,其中包含我们想要测量的对象,–width,也就是我们的参照物的宽度(英寸),–image路径图像中所认定的那个最左边的物体。

我们现在可以加载我们的图像并对其进行预处理:

# load the image, convert it to grayscale, and blur it slightly
image = cv2.imread("F:\py\example_03.jpg")
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
gray = cv2.GaussianBlur(gray, (7, 7), 0)

# perform edge detection, then perform a dilation + erosion to
# close gaps in between object edges
edged = cv2.Canny(gray, 50, 100)
edged = cv2.dilate(edged, None, iterations=1)
edged = cv2.erode(edged, None, iterations=1)

# find contours in the edge map
cnts = cv2.findContours(edged.copy(), cv2.RETR_EXTERNAL,
    cv2.CHAIN_APPROX_SIMPLE)
cnts = cnts[0] if imutils.is_cv2() else cnts[1]

# sort the contours from left-to-right and initialize the
# 'pixels per metric' calibration variable
(cnts, _) = contours.sort_contours(cnts)
pixelsPerMetric = None

第2-4行从磁盘加载我们的图像,将其转换为灰度,然后使用高斯过滤器平滑它。然后我们执行边缘检测和扩张+磨平,以消除边缘图中边缘之间的任何间隙(第8-10行)。

第13-15行找到等高线,也就是我们边缘图中物体相对应的轮廓线。

然后,这些等高线区域从左到右(使得我们可以提取到参照物)在第19行中进行排列。然后我们在第20行时,对pixelsPerMetric值进行初始化。

下一步是对每一个等高线区域值大小进行检查校验。

# loop over the contours individually
for c in cnts:
    # if the contour is not sufficiently large, ignore it
    if cv2.contourArea(c) < 100:
        continue

    # compute the rotated bounding box of the contour
    orig = image.copy()
    box = cv2.minAreaRect(c)
    box = cv2.cv.BoxPoints(box) if imutils.is_cv2() else cv2.boxPoints(box)
    box = np.array(box, dtype="int")

    # order the points in the contour such that they appear
    # in top-left, top-right, bottom-right, and bottom-left
    # order, then draw the outline of the rotated bounding
    # box
    box = perspective.order_points(box)
    cv2.drawContours(orig, [box.astype("int")], -1, (0, 255, 0), 2)

    # loop over the original points and draw them
    for (x, y) in box:
        cv2.circle(orig, (int(x), int(y)), 5, (0, 0, 255), -1)

在第2行,我们开始对每个单独的轮廓值进行循环。如果等高线区域大小不够大,我们就会丢弃该区域,认为它是边缘检测过程遗留下来的噪音(第4和5行)。

如果等高线区域足够大,我们就会在第9-11行计算图像的旋转边界框,特别注意:cv2.cv.BoxPoints函数是针对于opencv2.4版本,而cv2.BoxPoints函数是针对于OpenCV 3版本。

然后我们将旋转的边界框坐标按顺序排列在左上角,右上角,右下角,左下角,正如上周的博客文章(第17行)所讨论的。

最后,第18-22行用绿色画出物体的轮廓,然后将边界框矩形的顶点画在小的红色圆圈中。现在我们已经有了边界框,接下来就可以计算出一系列的中点:

    # unpack the ordered bounding box, then compute the midpoint
    # between the top-left and top-right coordinates, followed by
    # the midpoint between bottom-left and bottom-right coordinates
    (tl, tr, br, bl) = box
    (tltrX, tltrY) = midpoint(tl, tr)
    (blbrX, blbrY) = midpoint(bl, br)

    # compute the midpoint between the top-left and top-right points,
    # followed by the midpoint between the top-righ and bottom-right
    (tlblX, tlblY) = midpoint(tl, bl)
    (trbrX, trbrY) = midpoint(tr, br)

    # draw the midpoints on the image
    cv2.circle(orig, (int(tltrX), int(tltrY)), 5, (255, 0, 0), -1)
    cv2.circle(orig, (int(blbrX), int(blbrY)), 5, (255, 0, 0), -1)
    cv2.circle(orig, (int(tlblX), int(tlblY)), 5, (255, 0, 0), -1)
    cv2.circle(orig, (int(trbrX), int(trbrY)), 5, (255, 0, 0), -1)

    # draw lines between the midpoints
    cv2.line(orig, (int(tltrX), int(tltrY)), (int(blbrX), int(blbrY)),
        (255, 0, 255), 2)
    cv2.line(orig, (int(tlblX), int(tlblY)), (int(trbrX), int(trbrY)),
        (255, 0, 255), 2)

第4-6行将我们前面所得的有序边界框各个值拆分出来,然后计算左上角和右上角之间的中点,然后是计算左下角和右下角之间的中点。

  此外,我们还分别计算左上角与左下角,右上角和右下角的中点(第10和11行)。

  第14-17行在我们的图像上画出蓝色的中点,然后将各中间点用紫色线连接起来。

  接下来,我们需要通过查看我们的参照物来初始化pixelsPerMetric变量:

    # compute the Euclidean distance between the midpoints
    dA = dist.euclidean((tltrX, tltrY), (blbrX, blbrY))
    dB = dist.euclidean((tlblX, tlblY), (trbrX, trbrY))

    # if the pixels per metric has not been initialized, then
    # compute it as the ratio of pixels to supplied metric
    # (in this case, inches)
    if pixelsPerMetric is None:
        pixelsPerMetric = dB / args["width"]

首先,我们计算中间点集之间的欧几里得距离(第2行和第3行)。
dA变量将包含高度距离(以像素为单位),而dB将保持我们的宽度距离。

然后,我们在第8行进行检查,看看我们的pixelsPerMetric变量是否已经被初始化了,如果没有,我们将dB除以我们提供的宽度,从而得到每英寸的(近似)像素。

现在我们已经定义了pixelsPerMetric变量,我们可以测量图像中各物体的大小:

    # compute the size of the object
    dimA = dA / pixelsPerMetric
    dimB = dB / pixelsPerMetric

    # draw the object sizes on the image
    cv2.putText(orig, "{:.1f}in".format(dimA),
        (int(tltrX - 15), int(tltrY - 10)), cv2.FONT_HERSHEY_SIMPLEX,
        0.65, (255, 255, 255), 2)
    cv2.putText(orig, "{:.1f}in".format(dimB),
        (int(trbrX + 10), int(trbrY)), cv2.FONT_HERSHEY_SIMPLEX,
        0.65, (255, 255, 255), 2)

    # show the output image
    cv2.imshow("Image", orig)
    cv2.waitKey(0)

第2行和第3行计算物体的尺寸(英寸),方法是通过pixelsper度量值划分各自的欧几里得距离(参见上面的“pixels_per_metric ”一节,以获得关于这个比率如何工作的更多信息)。 第6-11行在我们的图像上画出物体的尺寸,而第14和15行显示输出结果。

物体大小测试结果

为了测试我们的size.py脚本,只需执行以下命令:

 python size.py --image images/example_01.jpg --width 0.955

您的输出应该如下图所示:
这里写图片描述
图2:使用OpenCV、Python和计算机视觉+图像处理技术来测量图像中物体的大小

正如您所看到的,我们已经成功地计算出了我们图像中的每个物体的大小——我们的名片被正确地报告为3.5in x 2in。类似地,我们的镍币被准确地描述为0.8in x 0.8in。 然而,并不是所有的结果都是完美的。 两个游戏男孩的墨盒被报道有稍微不同的尺寸(实际上它们的尺寸是一样的)。两个镍币的尺寸也下降了0.1英寸。 这是为什么?为什么物体的测量不是百分之百准确的?

有两个原因:
  首先,我匆忙用我的iPhone拍了这张照片。这个角度当然不是一个完美的90度角“向下看”(就像鸟的眼睛一样)在物体上。如果没有一个完美的90度视图(或者尽可能接近它),物体的尺寸就会被扭曲。
  
  其次,我没有使用相机的内在和外在参数来校准我的iPhone。如果不确定这些参数,照片就会倾向于径向和切向镜头失真。执行一个额外的校准步骤来发现这些参数可以“不扭曲”我们的图像,并导致更好的对象大小近似(但是我将把关于失真校正的讨论作为未来博客文章的主题)。

  换句话说,在拍摄对象的照片时,尽量接近90度的视角——这将有助于提高对物体大小估计的准确性。

  让我们看第二个测量物体大小的例子,这次测量的是药片的尺寸:

python size.py --image images/example_02.jpg --width 0.955

这里写图片描述
图3:使用OpenCV测量图像中药丸的大小

在美国,几乎有一半的处方药物是圆的和/或白色的,因此,如果我们能根据他们的测量结果来过滤药片,我们就有更好的机会准确识别药物。

最后,我们有一个最后的例子,这次用的是3.5in x 2in 的名片来测量两个黑胶唱片和一个信封的大小:
这里写图片描述
同样,结果也不是很完美,但这是由于(1)视角和(2)镜头失真,正如上述分析的两个原因中所提到的。

本文还参考了另外一篇翻译原英文博客的博客,网址如下:
https://blog.csdn.net/u010636181/article/details/80659700

猜你喜欢

转载自blog.csdn.net/happyjacob/article/details/81055275
今日推荐