本文来自光头哥哥的博客【Measuring size of objects in an image with OpenCV】,仅做学习分享。
代码语言:javascript复制原文链接:https://www.pyimagesearch.com/2016/03/28/measuring-size-of-objects-in-an-image-with-opencv/
今天的文章是关于测量图像中物体大小和计算它们之间距离的系列文章的第二部分。
上篇,我们学习了一项重要的技术:将一组旋转的边界框坐标按左上、右上、右下和左下排列的可靠性如何。
今天我们将利用这一技术来帮助我们计算图像中物体的大小。请务必阅读整篇文章,看看是如何做到的!
测量图像中物体的大小类似于计算相机到物体的距离——在这两种情况下,我们都需要定义一个比率来测量每个计算对象的像素数。
我将其称为“像素/度量”比率,我将在下面中对其进行更正式的定义。
“单位像素”比率
为了确定图像中对象的大小,我们首先需要使用参考对象执行“校准”(不要与内在/外在校准混淆)。我们的引用对象应该有两个重要的属性:
- 属性1:我们应该知道物体的尺寸(以宽度或高度表示),单位是可测量的(如毫米、英寸等)。
- 属性2:我们应该能够轻松地找到这个引用对象在一个图像,要么基于对象的位置(如引用对象总是被放置在一个图像的左上角)或通过表象(像一个独特的颜色或形状,独特和不同图像中所有其他对象)。在任何一种情况下,我们的引用都应该以某种方式是唯一可识别的。
在这个例子中,我们将使用0.25美分作为我们的参考对象,在所有的例子中,确保它总是我们图像中最左边的对象。
通过保证0.25美分是最左边的对象,我们可以从左到右排序我们的对象轮廓,获取美分(它总是排序列表中的第一个轮廓),并使用它来定义pixels_per_metric,我们定义为:
代码语言:javascript复制pixels_per_metric = object_width / know_width
一枚0.25美分硬币的已知宽度为0.955英寸。现在,假设我们的object_width(以像素为单位)被计算为150像素宽(基于其最小矩形边框)。
因此,pixels_per_metric为:
代码语言:javascript复制pixels_per_metric = 150px / 0.955in = 157px
这意味着在我们的图像中,每0.955英寸大约有157个像素。使用这个比率,我们可以计算图像中物体的大小。
用计算机视觉测量物体的大小
现在我们了解了“像素/度量”比率,我们可以实现用于测量图像中对象大小的Python驱动程序脚本。 打开一个新文件,将其命名为object_size.py,并插入以下代码:
代码语言:javascript复制# 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包,所以如果你没有安装它,确保你安装之前继续。
代码语言:javascript复制$ pip install imutils
第9行和第10行定义了一个称为midpoint的辅助算法,顾名思义,它用于计算两组点的(x, y)坐标之间的中点。
然后我们在第12-17行解析命令行参数。我们需要两个参数,--image,它是包含我们要测量的对象的输入图像的路径,以及--width,它是我们参考对象的宽度(以英寸为单位),假定它是--image中最左边的对象。
我们现在可以加载图像并进行预处理:
代码语言:javascript复制# load the image, convert it to grayscale, and blur it slightly
image = cv2.imread(args["image"])
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 = imutils.grab_contours(cnts)
# sort the contours from left-to-right and initialize the
# 'pixels per metric' calibration variable
(cnts, _) = contours.sort_contours(cnts)
pixelsPerMetric = None
第2-4行从磁盘加载我们的图像,将其转换为灰度,然后使用高斯滤波器平滑它。然后我们通过膨胀 侵蚀来进行边缘检测,以缩小边缘图中边缘之间的任何缝隙(第7-9行)。
找到边缘图中目标的轮廓线(第11-15行)。 然后在第16行从左到右(允许我们提取参考对象)对这些轮廓进行排序。我们还在第17行初始化了pixelsPerMetric值。
下一步是处理每一个轮廓:
代码语言:javascript复制# 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行)。
如果轮廓区域足够大,我们将计算图像的旋转包围框(第8-10行)。
然后按左上、右上、右下和左下顺序排列边框四个顶点坐标,如上篇的博客文章(第15行)所述。
最后,第16-19行用绿色绘制对象的轮廓,然后将边界矩形的顶点绘制成红色的小圆。
现在我们已经对边界框进行了排序,我们可以计算一系列中点:
代码语言:javascript复制 # 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-10行展开我们的有序边界框,然后计算各顶点之间的中点。
第12-20行在我们的图像上绘制蓝色中点,然后用紫色的线连接中点。
接下来,我们需要通过计算参考对象像素长度来初始化pixelsPerMetric变量:
代码语言:javascript复制 # 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将保留宽度距离。
然后我们在第7行检查pixelsPerMetric变量是否已经初始化,如果没有,我们用dB除以我们提供的--width,从而得到我们的(近似)"pixels per inch"。
代码语言:javascript复制 # 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行计算对象的尺寸(单位为英寸),方法是将各自的欧几里德距离除以像素度量值。
第5-10行绘制图像上对象的尺寸,而第12和13行显示输出结果。
物体尺寸测量结果
要测试我们的object_size.py脚本,只需发出以下命令:
代码语言:javascript复制$ python object_size.py --image images/example_01.png --width 0.955
输出如下所示:
可以看到,我们已经成功地计算出了图像中每个对象的大小——我们的名片被正确地报告为3.5英寸x 2英寸。同样,我们的0.25美分硬币准确地描述为0.8in x 0.8in。
然而,并不是所有的结果都是完美的:
显示的Game Boy墨盒的尺寸略有不同(尽管它们的大小是相同的)。两个0.25美分的高度也降了0.1英寸。
为什么会这样呢?为什么物体测量不是100%准确的?
原因有两方面:
- 首先,我匆忙地用iPhone拍下了这张照片。这个角度肯定不是完美的90度向下看(就像鸟瞰)。如果没有完美的90度视图(或尽可能接近它),对象的尺寸可能会出现扭曲。
- 第二,我没有使用相机的内在参数和外在参数来校准我的iPhone。不确定这些参数,照片可能容易发生径向和切向透镜畸变。执行一个额外的校准步骤来找到这些参数可以“消除”我们图像的失真,并得到更精确的物体大小。
让我们来看看测量物体大小的第二个例子,这次是测量药丸的尺寸:
在美国20000多种处方药中,近50%是圆形或白色的,因此如果我们能根据药片的尺寸进行筛选,我们就更有可能准确地识别出药物。
THE END