使用 OpenCV 测量物体尺寸
小白学视觉
共 16659字,需浏览 34分钟
·
2024-04-21 10:05
点击上方“小白学视觉”,选择加"星标"或“置顶”
重磅干货,第一时间送达
-
导入模块和参数初始化 -
图像加载和预处理 -
寻找纸张轮廓 -
透视变换 -
寻找物体轮廓 -
边长计算 -
将所有内容放在一个函数中
1. 导入模块及参数初始化
cv2
,numpy
而matplotlib
仅用于显示图像。
# Codeblock 1
import cv2
import numpy as np
import matplotlib.pyplot as plt
SCALE
主要用于我们不希望生成的图像太小。同时,和PAPER_W
表示PAPER_H
纸张宽度和高度(以毫米为单位)。
# Codeblock 2
SCALE = 3
PAPER_W = 210 * SCALE
PAPER_H = 297 * SCALE
2.图像加载和预处理
load_image()
,show_image()
我认为这两个函数的名称是不言自明的。它们的详细信息可以在 Codeblock 3 中看到。您可以在下面看到我定义了参数scale
(小写),其中我的目的是使输入图像变得有点小,因为原始图像分辨率非常高。但是,从技术上讲,您也可以通过传递大于 1 的值来使其更大。
show_image()
另一方面,该函数实现cv2.cvtColor()
了将颜色通道从 BGR 转换为 RGB 的功能。这种转换是必要的,因为 Matplotlib 在颜色通道顺序方面与 OpenCV 的工作方式不同。
# Codeblock 3
def load_image(path, scale=0.7):
img = cv2.imread(path)
img_resized = cv2.resize(img, (0,0), None, scale, scale)
return img_resized
def show_image(img):
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
plt.figure(figsize=(6,8))
plt.xticks([])
plt.yticks([])
plt.imshow(img)
plt.show()
scale
为其默认值(0.7),这将导致加载的图像大小为 1120×840 px(原始大小为 1600×1200 px)。
# Codeblock 4
img_original = load_image(path='images/1.jpeg')
show_image(img_original)
print(img_original.shape)
img_original
。接下来要做的步骤是使用一系列图像处理技术对该图像进行预处理,即灰度转换(#1
)、模糊(#2
)、Canny 边缘检测(#3
)、扩张(#5
)和闭合(#6
)。所有这些步骤都包含在preprocess_image()
Codeblock 5 中的函数中。
# Codeblock 5
def preprocess_image(img, thresh_1=57, thresh_2=232):
img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) #1
img_blur = cv2.GaussianBlur(img_gray, (5,5), 1) #2
img_canny = cv2.Canny(img_blur, thresh_1, thresh_2) #3
kernel = np.ones((3,3)) #4
img_dilated = cv2.dilate(img_canny, kernel, iterations=1) #5
img_closed = cv2.morphologyEx(img_dilated, cv2.MORPH_CLOSE,
kernel, iterations=4) #6
img_preprocessed = img_closed.copy()
img_each_step = {'img_dilated': img_dilated,
'img_canny' : img_canny,
'img_blur' : img_blur,
'img_gray' : img_gray}
return img_preprocessed, img_each_step
#2
的参数分别表示高斯滤波器大小和高斯分布标准差。同时,行中的和是 Canny 边缘检测器用来捕获弱边缘和强边缘的两个阈值。在本例中,我决定将 设置为 57 和232,这些数字是通过反复试验确定的。接下来,我们初始化一个 3×3 内核,其中内核的所有元素都设置为 1(标记为 的行)。然后,这个全 1 内核将在扩张和闭合过程中用作结构元素。(5,5)
1
thresh_1
thresh_2
#3
thresh_1
thresh_2
#4
preprocess_image()
函数返回两个值:完全预处理的图像(存储在 中img_preprocessed
)和每个预处理阶段的结果(img_each_step
以字典形式存储在 中)。该函数的输出如图 3 所示。要进入下一个过程的图像是img_preprocessed
(最右边)。
cv2.findContours()
( ) 来执行此操作#1
。我们还将其cv2.RETR_EXTERNAL
作为参数的输入参数传递,因为我们只对捕获图像中检测到的最外层轮廓(即纸张)感兴趣。现在,计算器对象将被忽略。然后,将使用( )mode
绘制检测到的轮廓本身。cv2.drawContours()
#2
# Codeblock 7def find_contours(img_preprocessed, img_original, epsilon_param=0.04):contours, hierarchy = cv2.findContours(image=img_preprocessed, mode=cv2.RETR_EXTERNAL, method=cv2.CHAIN_APPROX_NONE) #1img_contour = img_original.copy()cv2.drawContours(img_contour, contours, -1, (203,192,255), 6) #2polygons = []for contour in contours:epsilon = epsilon_param * cv2.arcLength(curve=contour, closed=True) #3polygon = cv2.approxPolyDP(curve=contour, epsilon=epsilon, closed=True) #4polygon = polygon.reshape(4, 2) #5polygons.append(polygon)for point in polygon: img_contour = cv2.circle(img=img_contour, center=point, radius=8, color=(0,240,0), thickness=-1) #6return polygons, img_contour不仅如此,这里我还尝试使用cv2.approxPolyDP()(#4)来近似轮廓形状。这行代码的目的是获取纸张轮廓的四个角,然后将坐标存储在其中polygon。我们需要注意的一件事是epsilon行中使用的参数#3。较小的 epsilon 值往往会导致检测到更多的角,而另一方面,较大的 epsilon 会导致函数cv2.approxPolyDP()捕获多边形的一般形状,即较少的角。注意 epsilon 值很重要,因为我们需要确保函数将准确捕获四个角点。
#5
),因为 的原始输出cv2.approxPolyDP()
格式为(number of corners, 1, 2)
,其中中间轴在我们的例子中无关紧要。因此,我们可以放心地将其丢弃。除了函数之外find_contours()
,我们还将使用cv2.circle()
( #6
) 显示角。然后,此函数将返回角坐标本身 ( polygon
) 和具有突出显示轮廓的图像 ( img_contour
)。
find_contours()
函数。在这里我决定将设置epsilon_param
为 0.04。如果您使用自己的图片,则可能需要更改此值,特别是如果光照特性与我的图像不同。下面代码块的输出(显示在图 4 中)显示了轮廓和角落的样子。我也打印了存储在中的坐标polygon[0]
。如果您想知道,使用索引器 0 是因为该find_contours()
函数本质上能够捕获多个轮廓,而在这种情况下,我们唯一的外部轮廓是纸张本身。除此之外,可能值得注意的是,存储在中的坐标polygons
是(x,y)
格式,而不是(y,x)
。
重新排序坐标
cv2.approxPolyDP()
。为了使这四个点都有序,我们需要创建一个专门的函数来执行此操作。我正在谈论的函数称为,reorder_coords()
其详细信息可在 Codeblock 9 中看到。
# Codeblock 9
def reorder_coords(polygon):
rect_coords = np.zeros((4, 2))
add = polygon.sum(axis=1)
rect_coords[0] = polygon[np.argmin(add)] # Top left
rect_coords[3] = polygon[np.argmax(add)] # Bottom right
subtract = np.diff(polygon, axis=1)
rect_coords[1] = polygon[np.argmin(subtract)] # Top right
rect_coords[2] = polygon[np.argmax(subtract)] # Bottom left
return rect_coords
polygon[0]
如 Codeblock 10 所示。
# Codeblock 10
rect_coords = np.float32(reorder_coords(polygons[0]))
rect_coords
rect_coords
)将作为变换的源,而变换目标(paper_coords
)由实际纸张大小决定,即PAPER_W
和PAPER_H
。查看 Codeblock 11 以了解我如何手动排列 存储的点。这里要记住的一件事是,目标坐标的顺序需要与源坐标的顺序完全匹配,这就是我们实现重新排序源坐标的功能paper_coords
的原因。reorder_coords()
# Codeblock 11
paper_coords = np.float32([[0,0], # Top left
[PAPER_W,0], # Top right
[0,PAPER_H], # Bottom left
[PAPER_W,PAPER_H]]) # Bottom right
paper_coords
cv2.getPerspectiveTransform()
后续代码块中的函数返回错误。
创建变换矩阵
rect_coords
和paper_coords
。我们现在可以使用它们来扭曲原始图像,使用cv2.getPerspectiveTransform()
和,cv2.warpPerspective()
如下面的代码块 12 所示。为了让代码更简洁,我将它们放在另一个名为的函数中warp_image()
。
# Codeblock 12
def warp_image(rect_coords, paper_coords, img_original, pad=5):
matrix = cv2.getPerspectiveTransform(src=rect_coords,
dst=paper_coords) #1
img_warped = cv2.warpPerspective(img_original, matrix,
(PAPER_W, PAPER_H)) #2
warped_h = img_warped.shape[0]
warped_w = img_warped.shape[1]
img_warped = img_warped[pad:warped_h-pad, pad:warped_w-pad] #3
return img_warped
cv2.getPerspectiveTransform()
( #1
) 创建所谓的变换矩阵。变换矩阵本身的尺寸为 3 × 3,它存储了有关如何将图像从一个角度变换到另一个角度的信息。然后,该矩阵将实际用于使用cv2.warpPerspective()
( #2
) 变换原始图像。然后,我们将变换后的图像存储在名为 的变量中img_warped
。除了函数之外warp_image()
,我们还将丢弃图像边界附近的区域 ( #3
),因为生成的图像可能在边缘处包含一个狭窄的不需要的黑色区域。
warp_image()
在 Codeblock 13 中演示,其中输出显示在图 7 中。
# Codeblock 13
img_warped = warp_image(rect_coords, paper_coords, img_original)
show_image(img_warped)
print(img_warped.shape)
cv2.RETR_EXTERNAL
我们在 Codeblock 7 中实现的轮廓。
# Codeblock 14
img_warped_preprocessed, _ = preprocess_image(img_warped)
show_image(img_warped_preprocessed)
# Codeblock 15
polygons_warped, img_contours_warped = find_contours(img_warped_preprocessed,
img_warped,
epsilon_param=0.04)
show_image(img_contours_warped)
polygons_warped[0]
polygons_warped
,这四个坐标值其实就是用来估算边长的坐标值。
6. 边长计算
#2
)。同时,可以通过计算物体左上角和右上角之间的相同距离度量来获得宽度(#3
)。此外,我们需要记住,返回的检测到的角find_contours()
仍然是无序的。因此,我们需要reorder_coords()
事先调用( )。我想在 Codeblock 16 中强调的最后一件事是,估计的高度和宽度现在存储在按相应顺序#1
命名的数组中( )sizes
#4
# Codeblock 16
def calculate_sizes(polygons_warped):
rect_coords_list = []
for polygon in polygons_warped:
rect_coords = np.float32(reorder_coords(polygon)) #1
rect_coords_list.append(rect_coords)
heights = []
widths = []
for rect_coords in rect_coords_list:
height = cv2.norm(rect_coords[0], rect_coords[2], cv2.NORM_L2) #2
width = cv2.norm(rect_coords[0], rect_coords[1], cv2.NORM_L2) #3
heights.append(height)
widths.append(width)
heights = np.array(heights).reshape(-1,1)
widths = np.array(widths).reshape(-1,1)
sizes = np.hstack((heights, widths)) #4
return sizes, rect_coords_list
sizes, rect_coords_list = calculate_sizes(polygons_warped)
sizes
转换为毫米
convert_to_mm()
(Codeblock 17)。此函数通过取纸张长度(以毫米为单位)和像素(以像素为单位)的比率来工作。然后,我们可以通过将比率值(和)与仍以像素为单位的计算器大小(和#1
)#2
相乘来获得实际的毫米长度。scale_h
scale_w
#3
#4
# Codeblock 17
def convert_to_mm(sizes_pixel, img_warped):
warped_h = img_warped.shape[0]
warped_w = img_warped.shape[1]
scale_h = PAPER_H / warped_h #1
scale_w = PAPER_W / warped_w #2
sizes_mm = []
for size_pixel_h, size_pixel_w in sizes_pixel:
size_mm_h = size_pixel_h * scale_h / SCALE #3
size_mm_w = size_pixel_w * scale_w / SCALE #4
sizes_mm.append([size_mm_h, size_mm_w])
return np.array(sizes_mm)
sizes_mm = convert_to_mm(sizes, img_warped)
sizes_mm
write_size()
。初始化函数后,我们可以直接调用它,如#1
代码块 18 中所示。此代码的输出如图 12 所示。
# Codeblock 18
def write_size(rect_coords_list, sizes, img_warped):
img_result = img_warped.copy()
for rect_coord, size in zip(rect_coords_list, sizes):
top_left = rect_coord[0].astype(int)
top_right = rect_coord[1].astype(int)
bottom_left = rect_coord[2].astype(int)
cv2.line(img_result, top_left, top_right, (255,100,50), 4)
cv2.line(img_result, top_left, bottom_left, (100,50,255), 4)
cv2.putText(img_result, f'{np.int32(size[0])} mm',
(bottom_left[0]-20, bottom_left[1]+50),
cv2.FONT_HERSHEY_DUPLEX, 1, (100,50,255), 1)
cv2.putText(img_result, f'{np.int32(size[1])} mm',
(top_right[0]+20, top_right[1]+20),
cv2.FONT_HERSHEY_DUPLEX, 1, (255,100,50), 1)
return img_result
img_result = write_size(rect_coords_list, sizes_mm, img_warped) #1
show_image(img_result)
print(img_result.shape)
7. 将所有内容放在一个函数中
measure_size()
在 Codeblock 19 中将其命名为该函数。使用此函数,我们可以简单地将要处理的图像与参数一起传递,以完成我们刚刚完成的所有工作。
# Codeblock 19
def measure_size(path, img_original_scale=0.7,
PAPER_W=210, PAPER_H=297, SCALE=3,
paper_eps_param=0.04, objects_eps_param=0.05,
canny_thresh_1=57, canny_thresh_2=232):
PAPER_W = PAPER_W * SCALE
PAPER_H = PAPER_H * SCALE
# Loading and preprocessing original image.
img_original = load_image(path=path, scale=img_original_scale)
img_preprocessed, img_each_step = preprocess_image(img_original,
thresh_1=canny_thresh_1,
thresh_2=canny_thresh_2)
# Finding paper contours and corners.
polygons, img_contours = find_contours(img_preprocessed,
img_original,
epsilon_param=paper_eps_param)
# Reordering paper corners.
rect_coords = np.float32(reorder_coords(polygons[0]))
# Warping image according to paper contours.
paper_coords = np.float32([[0,0],
[PAPER_W,0],
[0,PAPER_H],
[PAPER_W,PAPER_H]])
img_warped = warp_image(rect_coords, paper_coords, img_original)
# Preprocessing the warped image.
img_warped_preprocessed, _ = preprocess_image(img_warped)
# Finding contour in the warped image.
polygons_warped, img_contours_warped = find_contours(img_warped_preprocessed,
img_warped,
epsilon_param=objects_eps_param)
# Edge langth calculation.
sizes, rect_coords_list = calculate_sizes(polygons_warped)
sizes_mm = convert_to_mm(sizes, img_warped)
img_result = write_size(rect_coords_list, sizes_mm, img_warped)
return img_result
measure_size()
创建了函数,我们现在就可以在几个测试用例上测试它。图 13 所示的输出表明,我们估算物体大小的方法对于不同的矩形物体非常有效。
# Codeblock 20
show_image(measure_size('images/1.jpeg'))
show_image(measure_size('images/2.jpeg'))
show_image(measure_size('images/3.jpeg'))
show_image(measure_size('images/4.jpeg'))
show_image(measure_size('images/5.jpeg'))
show_image(measure_size('images/6.jpeg', objects_eps_param=0.1))
一些局限
下载1:OpenCV-Contrib扩展模块中文版教程
在「小白学视觉」公众号后台回复:扩展模块中文教程,即可下载全网第一份OpenCV扩展模块教程中文版,涵盖扩展模块安装、SFM算法、立体视觉、目标跟踪、生物视觉、超分辨率处理等二十多章内容。
下载2:Python视觉实战项目52讲 在「小白学视觉」公众号后台回复:Python视觉实战项目,即可下载包括图像分割、口罩检测、车道线检测、车辆计数、添加眼线、车牌识别、字符识别、情绪检测、文本内容提取、面部识别等31个视觉实战项目,助力快速学校计算机视觉。
下载3:OpenCV实战项目20讲 在「小白学视觉」公众号后台回复:OpenCV实战项目20讲,即可下载含有20个基于OpenCV实现20个实战项目,实现OpenCV学习进阶。
交流群
欢迎加入公众号读者群一起和同行交流,目前有SLAM、三维视觉、传感器、自动驾驶、计算摄影、检测、分割、识别、医学影像、GAN、算法竞赛等微信群(以后会逐渐细分),请扫描下面微信号加群,备注:”昵称+学校/公司+研究方向“,例如:”张三 + 上海交大 + 视觉SLAM“。请按照格式备注,否则不予通过。添加成功后会根据研究方向邀请进入相关微信群。请勿在群内发送广告,否则会请出群,谢谢理解~
评论