本文是机器视觉实战系列第1篇,实现的一个通过颜色来小球的检测和运动轨迹跟踪,效果如下:

ball tracking

原文出处:Ball Tracking with OpenCV.

项目学习

代码学习

我使用的环境是:MacOS+Python3.7.4+OpenCV 4.2.0.

相比于原文,我对代码做了一些小修改,并且加了一些中文注释,方便你理解。整个项目代码如下:

import argparse
import time
from collections import deque
import cv2
import imutils
import numpy as np
from imutils.video import VideoStream

# 命令行参数
ap = argparse.ArgumentParser()
ap.add_argument("-v", "--video", help="path to video")
ap.add_argument("-b", "--buffer", type=int, default=64, help="max buffer size")
args = vars(ap.parse_args())

# 绿色球的HSV色域空间范围
greenLower = (29, 86, 6)
greenUpper = (64, 255, 255)
pts = deque(maxlen=args["buffer"])

# 判断是读入的视频文件,还是摄像头实时采集的,这里作区分是因为两种情况下后面的有些操作是有区别的
if args.get("video", None) is None:
    useCamera = True
    print("video is none, use camera...")
    vs = VideoStream(src=0).start()
else:
    useCamera = False
    vs = cv2.VideoCapture(args["video"])
    time.sleep(2.0)

while True:
    frame = vs.read()
    # 摄像头返回的数据格式为(帧数据),而从视频抓取的格式为(grabbed, 帧数据),grabbed表示是否读到了数据
    frame = frame if useCamera else frame[1]

    # 对于从视频读取的情况,frame为None表示数据读完了
    if frame is None:
        break

    # resize the frame(become small) to process faster(increase FPS)
    frame = imutils.resize(frame, width=600)
    # blur the frame to reduce high frequency noise, and allow
    # us to focus on the structural objects inside the frame
    # 通过高斯滤波去除掉一些高频噪声,使得重要的数据更加突出
    blurred = cv2.GaussianBlur(frame, (11, 11), 0)
    # convert frame to HSV color space
    hsv = cv2.cvtColor(blurred, cv2.COLOR_BGR2HSV)
    # handles the actual localization of the green ball in the frame
    # inRange的作用是根据阈值进行二值化:阈值内的像素设置为白色(255),阈值外的设置为黑色(0)
    mask = cv2.inRange(hsv, greenLower, greenUpper)

    # A series of erosions and dilations remove any small blobs that may be left on the mask
    # 腐蚀(erode)和膨胀(dilate)的作用:
    # 1. 消除噪声;
    # 2. 分割(isolate)独立的图像元素,以及连接(join)相邻的元素;
    # 3. 寻找图像中的明显的极大值区域或极小值区域
    mask = cv2.erode(mask, None, iterations=2)
    mask = cv2.dilate(mask, None, iterations=2)

    # 寻找轮廓,不同opencv的版本cv2.findContours返回格式有区别,所以调用了一下imutils.grab_contours做了一些兼容性处理
    cnts = cv2.findContours(mask.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
    cnts = imutils.grab_contours(cnts)
    center = None

    # only proceed if at least one contour was found
    if len(cnts) > 0:
        # find the largest contour in the mask, then use it to compute the minimum enclosing circle
        # and centroid
        c = max(cnts, key=cv2.contourArea)
        ((x, y), radius) = cv2.minEnclosingCircle(c)
        M = cv2.moments(c)
        # 对于01二值化的图像,m00即为轮廓的面积, 一下公式用于计算中心距
        center = (int(M["m10"] / M["m00"]), int(M["m01"] / M["m00"]))

        # only proceed if the radius meets a minimum size
        if radius > 10:
            # draw the circle and centroid on the frame, then update the list of tracked points
            cv2.circle(frame, (int(x), int(y)), int(radius), (0, 255, 255), 2)
            cv2.circle(frame, center, 5, (0, 0, 255), -1)

        pts.appendleft(center)

    for i in range(1, len(pts)):
        # if either of the tracked points are None, ignore them
        if pts[i - 1] is None or pts[i] is None:
            continue

        # compute the thickness of the line and draw the connecting line
        thickness = int(np.sqrt(args["buffer"] / float(i + 1)) * 2.5)
        cv2.line(frame, pts[i - 1], pts[i], (0, 0, 255), thickness)

    cv2.imshow("Frame", frame)
    key = cv2.waitKey(1) & 0xFF

    if key == ord("q"):
        break

if useCamera:
    vs.stop()
else:
    vs.release()

cv2.destroyAllWindows()

该项目主要使用OpenCV实现,里面重要的地方我都已经加了注释说明,这里再理一下整个流程:

  1. 处理命令行参数。
  2. 定义绿色球在HSV色域空间的颜色范围,后面会根据这个范围进行二值化,从而实现轮廓检测。
  3. 区分一下是直接从摄像头读取的数据还是处理的已经拍好的视频,这里作区分是因为两种情况下,后面有些代码要做不同处理。
  4. 循环处理视频中的帧:

    1. 获取1帧的图像。对于从已有视频读的情况,如果读到的帧为空,则表示处理视频处理完了。
    2. 调整图片大小,主要是缩小一下图片,提高处理速度。
    3. 通过高斯滤波去除掉一些高频噪声,使得重要的数据更加突出。
    4. 将图像从BGR色域转换到HSV色域。
    5. 根据绿色球在HSV色域的颜色范围进行二值化。这样处理之后,绿色就会变成白色,其它都会变成黑色,方便后面提取球的轮廓。后面进行的腐蚀和膨胀操作也是为了消除噪声点(毛刺),更准确的提取小球轮廓。
    6. 提取轮廓。
    7. 后面就是根据提取出来的轮廓,画了一些小球的边沿,以及运动轨迹,就不细述了,代码里面很清楚。

图像处理效果展示

对于没有图像处理基础的人来说,可能不了解其中一些处理的作用,这里我把一些关键步骤处理的效果图放上来,方便你理解(以其中一帧图片为例):

  • resize后的效果:

    31bY38.png

    ​ 这一步除了尺寸有变化,其它都和原图一样。

  • 高斯滤波后的效果:

    31b84P.png

滤掉了高频噪音,看着变模糊了。

  • 转换到HSV色域后的效果:

    31btgS.png

  • 二值化后的效果:

    31b3Nt.png

  • 腐蚀和膨胀后的效果:

    31b1AI.png

HSV颜色空间

先简单介绍下HSV颜色空间吧(其实还有个HSL,和HSV类似,但有些不同)。RGB是在笛卡尔坐标系来表示颜色的,而HSV则是在圆柱坐标系中表示颜色的,所以它能表示更多的信息。下图是一个HSV的图(图片来自维基百科):

33qP6U.png

简单说就是HSV使用圆柱坐标系表示颜色,分三个维度(如上图中的e):

  • Hue:即色相,就是我们平时说的颜色名称,比如红色、黄色。H的取值为绕圆柱中心轴一圈的角度(即圆柱坐标系中的方位角),所以取值范围是0\~360。
  • Saturation:即饱和度,是指色彩的纯度,越高色彩越纯,低则逐渐变灰,取0-100%的数值。S为圆柱的半径值(即圆柱坐标系中的径向距离)。
  • Value:即明度,也称亮度,取0-100%的数值。V为圆柱坐标系中的高度

圆柱的中心轴底部为黑色,顶部为白色,中间为灰色。更详细的信息请参加维基百科:HSV

那为什么我们要将图像从RGB转到HSV再做颜色识别呢?因为虽然RGB通道对人眼来说比较友好,但却不能很好地反映出物体具体的颜色信息。相反,HSV空间能够非常直观的表达色彩的明暗,色调,以及鲜艳程度,方便进行颜色之间的对比。所以在通过颜色识别对象时,都是采用HSV颜色空间的。

最后说一下两个实战时需要注意的事项:

  1. HSV标准中,Hue是角度,所以取值范围是0\~360,但在OpenCV中做了精简,实际范围变成了0\~180;S和V的取值是0\~100%,但OpenCV中是0\~255.所以使用OpenCV的时候需要注意一下。
  2. 一个颜色在HSV中没有特别明确的值或者范围,实际使用的时候往往需要我们自己去观察测试进行确定。下面是一个常见颜色H值大致的范围参考(注意范围是0\~360):

    • 红色(Red):0\~60
    • 黄色(Yellow):61\~120
    • 绿色(Green): 121\~180
    • 青色(Cyan): 181\~240
    • 蓝色(Blue): 241\~300
    • 品红(Magenta): 301\~360

资源信息

  • 完整代码:code.
  • 涉及的视频资源:在公众号回复“机器视觉实战1”获取下载链接。

通过这个项目我们可以学到如何通过颜色来做物体识别。