网球目标检测——基于Python-OpenCV

1.问题描述

本文章实现了通过读取摄像头所拍摄的图像,实时检测图像中的网球并推算其距离、确定其方位。核心问题是如何从摄像头拍摄的画面中检测出网球,并排除干扰项。此外,为了将该方法运用在嵌入式系统上,系统的计算复杂度应当尽量减少,避免影响实时性。

2.实现方法

对于网球这样的球体单色目标,可以选择霍夫变换进行圆检测,也可以通过色彩分割将网球从视频帧中分割出来。如果背景复杂,障碍物多,也可以选择训练一个专门识别网球的模型,比如常见的YOLO,这样可以提高识别的准确率。但这种方法的缺点是难以部署。下面,我将分别详细介绍这几种方法。

2.1 霍夫变换检测网球

霍夫变换检测网球的基本思想是,网球从每个方向看去基本都可以认为是圆,而任何一个圆,都可以从二维坐标系下的表达式映射到三维空间中去。也就是说,一个圆在笛卡尔坐标系下的表达式:

(x-a)²+(y-b)²=r²

可以映射为三维坐标系下的一个点(a,b,r)。那么,将一张图像映射于该三维坐标系后,该坐标系下的一个峰值可能就意味着一个圆。该方法的具体数学原理不再进行赘述,我们的首要目标是了解它的用法。如果想要了解更多有关霍夫变换的原理问题,可以搜索其他文章。

2.1.1 HoughCircles参数与返回值介绍

1、函数参数

在OpenCV中,有一个函数 cv2.HoughCircles() 就是使用的霍夫变换来检测圆。该函数共有8个参数。分别是:

image:8bit、单通道灰度图像

method:Hough变换方法,但目前只支持 cv2.HOUGH_GRADIENT

dp:累加器图像的分辨率。例如,当dp的值为1时,累加器将与源图像有相同的分辨率;当dp值设置为2时,累加器的高度和宽度都变为源图像的一半。dp的值不能小于1

min_dist:能区分两个不同圆之间的最小值。如果该值过小,可能有很多不存在的圆被错误地检出。如果该值过大,可能会丢失一些圆。

param1:Canny边缘检测的阈值上限,下限为该值的一半。即超过阈值上限为边缘点,低于阈值下限则抛弃。中间部分视边缘是否相连而定。

param2:检测出圆质量的阈值。该值越大,检测出的圆就越接近标准的圆。该值越小,则就更容易有错误的圆被检测出来。

min_radius:限制能检测出圆的最小半径。低于该值的圆将被全部抛弃。

max_radius:限制能检测出圆的最大半径。高于该值的圆将被全部抛弃。

2、返回值

函数会返回一个numpy数组,包含了检测出所有圆的圆心坐标和半径信息。要调用出这些信息,可以通过分割数组的方式实现。例如:

circles=cv2.HoughCircles(gray,cv2.HOUGH_GRADIENT,1,100,param1=50,param2=70,minRadius=1,maxRadius=200)
for i in circles[0,:]:
    cv2.circle(frame, (i[0],i[1]),i[2],(0,255,0),2)

分割后,得到的数组第一位为x坐标,第二位为y坐标,第三位为半径。得到这些数据后,结合cv2.circle()方法可以在图像上绘制圆,以便显示检测结果。

2.1.2 算法流程

先大致介绍算法流程,代码会放在2.1.3中。

(1)初始化一个VideoCapture对象,使用read()方法从摄像头中读取一帧图像;

(2)使用cvtColor()方法将图像从BGR色域转为灰度图像;

(3)使用高斯模糊对图像进行滤波,核的大小可以视情况而定。我是用的是(7,7)的核;

(4)使用HoughCircles()方法检测圆。

(5)获取圆心坐标与圆的半径,将圆在源图像上标注出来。

2.1.3 代码应用实例

由于代码是在虚拟机上的Thonny里编写的,我不知道怎么调成中文,因此注释是英文写的。另外,该程序本来是用作捡球车控制的,因此会在控制台中输出球体相对于摄像头的方向和距离。

import cv2
import numpy as np
import math
try:
    cap = cv2.VideoCapture(0)   #from camera
    #cap = cv2.VideoCapture('./cvtest.mp4')   #from video file
except:
    exit()
NoneType = None   #variable for comparison
RotateAngle = 0   #the angle needs to be rotate (the nearest ball)
halfWidth = 0     #half of the image's width
StdDiameter = 6.70   #the standard diameter of tennis ball
StdPing = 4       #the standard diameter of table tennis ball
FixValue = 5      #fix the calculation of real angle for vehicle to rotate
while(True):
    ret, frame = cap.read()  #capture frame by frame
    halfWidth = frame.shape[1]/2
    gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)  #convert frame to gray
    gray = cv2.GaussianBlur(gray, (7, 7), 0)   #gaussianblur denoise    
    circles = cv2.HoughCircles(gray,cv2.HOUGH_GRADIENT,1,100,param1=50,param2=70,minRadius=1,maxRadius=200)
    gray = cv2.Canny(gray, 100, 300)   #houghcircles find the circles, canny detect the edge 
    binary, contours, hierarchy = cv2.findContours(gray, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) #find contours
    cv2.imshow('edge', binary)  #show the image of contours
    if type(circles) != type(NoneType):  #to avoid shutdown due to no circles found
        TimeStamp = 0
        Reset = False
        for i in circles[0,:]:
            cv2.circle(frame, (i[0],i[1]),i[2],(0,255,0),2)  #draw the circle
            cv2.circle(frame, (i[0],i[1]),2,(0,0,255),3)   #draw the center of circle
            tanTheta = (i[0]-halfWidth)/(1.43*halfWidth)   #the tan value
            RotateAngle = math.atan(tanTheta) #calculate the angle
            Distance = 850/i[2]               #calculate the distance between ball and camera
            FixAngle = math.atan(Distance*math.sin(RotateAngle)/(Distance*math.cos(RotateAngle)+FixValue))  #calculate the fixed angle
            Angle = FixAngle*180/math.pi   #transform from rad to degree
            if RotateAngle < 0:
                print('Rotate Angle is ',abs(Angle),' Left')
            else:
                print('Rotate Angle is ',abs(Angle),' Right')
            print('Distance is ',Distance,' cm')
    cv2.imshow('frame', frame)
    if cv2.waitKey(1) & 0xFF == ord('q'):
        break

这段代码中几个需要注意的地方:

(1)代码中的Canny边缘检测和cv2.findContours()方法是用于对比效果的。可以不加这两行代码;

(2)其中选择加入NoneType这个变量可能会看起来比较笨,但是HoughCircles()方法未检测到圆时,会产生一个None的返回值。如果不对circles判断一下是否有返回numpy数组,程序就会在执行到for循环时报错。因为NoneType是没法进行分割的。

(3)输出的球体方向和距离是由摄像头的视野范围决定的,该程序中的数据只能用于我所用的摄像头,对于其他摄像头输出的结果可能是无意义的。如果想要输出正确结果需要针对自己的摄像头进行参数的调整。

霍夫变换法检测球体

使用该段代码的效果如上图。可见,霍夫变换方法的效果还算不错,能稳定地检测出轮廓清晰的球体,但是也能看出其显著的缺点。正如上图所示,我们看到左侧的网球没有被检测出来,而且下方的乒乓球也会被检测出来。因此,它的主要问题就是,对于圆形的物体来者不拒,甚至衣服上的圆形花纹也会被它标注出来。这显然不是我们希望的。

另外,这种方法还要求物体有清晰的轮廓,否则成功检测到它的概率会很低。最致命的问题是,当这一程序遇到条纹状物体时,会极大增加错检率,并且程序会因为大量计算导致每秒钟能处理的帧数降低到0.1~1。这是使用霍夫变换没有办法避免的问题。

2.2 颜色分割法检测网球

颜色分割法的思想是,网球基本可以看做一个单色物体,只要把源图像中仅包含网球的颜色块分割出来,就能得到一幅只包含网球的图像(得到的是二值化的掩膜,mask。后面会详细说明),而排除其他颜色的物体的干扰。

2.2.1 OpenCV中的HSV颜色空间

在现实中,网球不同部分受到的光照是不一样的,这就导致它在BGR颜色空间下并非单色,难以分割。因此我们使用这种方法时,需要在HSV颜色空间下进行。HSV颜色空间也是用三个值来表示一个颜色。分别是:

色调(Hue):用角度度量,范围为0°~360°,其中0°为红色,120°为绿色,240°为蓝色。值得注意的是,在OpenCV中,该属性的范围缩小了一倍,为0至180。

饱和度(Saturation):饱和度S表示颜色接近光谱色的程度。一种颜色,可以看成是某种光谱色与白色混合的结果。其中光谱色所占的比例愈大,颜色接近光谱色的程度就愈高,颜色的饱和度也就愈高。在OpenCV中其取值范围是0至255,值越大,颜色越饱和。通俗地讲,这个值越小越发白。

明度(Value):明度表示颜色明亮的程度,通常取值范围为0%(黑)到100%(白)。在OpenCV中,该值的取值范围是0至255。

将图像转换到HSV颜色空间后,网球的色调峰值就会集中于某一个很短的区间,因为网球的明处和暗处只存在饱和度和明度的差别。只要我们保留这一色调区间,并调整饱和度和明度,就能将网球从图像中提取出来。

2.2.2 算法流程

先大致介绍算法流程,代码会放在2.2.3中。

(1)初始化一个VideoCapture对象,使用read()方法从摄像头中读取一帧图像;

(2)使用cv2.cvtColor()方法将图像从BGR颜色空间转换到HSV颜色空间;

(3)使用(3,3)大小的核进行高斯模糊,并使用cv2.inRange()方法分割出网球;(cv2.inRange()方法能够将给定的上下阈值中间的色彩分离出来,分离出的这部分为白色,其他部分为黑色)

(4)使用cv2.morphologyEx()方法做一次闭运算,减小掩膜中的干扰项造成的孔洞;(闭运算先膨胀后腐蚀,可以弥合小孔洞)

(5)使用cv2.findContours()方法取得(4)步后所得的掩膜中所有轮廓;

(6)使用cv2.boundingRect()方法取得所有轮廓对应的外接矩形的坐标和宽高值;

(7)使用cv2.countArea()方法计算轮廓所包含的面积,并将它与设定的检出圆阈值作比较,高于该值认为是一个网球,否则舍弃。将检测出的网球标记在源图像上。

2.2.3 代码应用实例

为了保证代码的规范性,我将代码分成了两个文件 main.py 和 function.py,其中,main.py直观地展示了这一方法的流程,function.py 中则定义了主程序中所用函数。
main.py

import function as tennis
import cv2
cap = cv2.VideoCapture(0)
while(True):
    ret, src = cap.read()
    contours = tennis.FindBallContours(src)
    src = tennis.DrawRectangle(src, contours)
    cv2.imshow('src',src)
    if cv2.waitKey(1) & 0xFF == ord('e'):
        break

funciton.py

import cv2
import numpy as np

#global variable
#########################################################################################
tennisLowDist = np.array([20, 50, 140])     #tested best threshold of tennis detection
tennisHighDist = np.array([40, 200, 255])
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE,(5,5))  #ellipse kernel
radius = 0
#########################################################################################

#functions definition
#########################################################################################
def FindBallContours(src):
    img = cv2.cvtColor(src, cv2.COLOR_BGR2HSV)   #transfer to HSV field
    img = cv2.GaussianBlur(img,(3,3),0)     #denoise
    mask = cv2.inRange(img, tennisLowDist, tennisHighDist)   #generate a mask with only tennis ball
    mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel)   #morphology close operation for remove small noise point
    #mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel)
    image, contours, hierarchy= cv2.findContours(mask,cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_SIMPLE)   #find the contours
    return contours


def DrawRectangle(src, contours):
    for c in contours:
        x,y,w,h = cv2.boundingRect(c)   #find each contours' circumscribed rectangle
        if y!=0:    #contour only makes sense when y!=0
            if w>h:
                radius = w
            else: radius = h
            area = cv2.contourArea(c)    #calculate the area of each contour
            if area > w*h*0.75*0.5 and 0.7 < w/h < 1.5 and area > 100:      #threshold of area limit and w/h
                cv2.rectangle(src,(x,y),(x+radius,y+radius),(0,0,255),2)   #draw rectangle of ball
        else:
            continue
    return src
#########################################################################################

这段代码的效果如图所示:
颜色分割法检测网球
左侧是在源图像中标注出网球后的效果,右图是在HSV颜色空间提取出网球后的掩膜。

识别效果发布于B站:https://www.bilibili.com/video/BV1BS4y1C7LG/

这段代码需要注意的地方:

(1)if area > w*h*0.75*0.5 这一行代码是通过色块面积外接矩形面积百分比来推算是不是网球,同时也限制面积的大小避免噪声被当做网球检测出来。可以通过改变这一行代码中的条件调整误识别的概率,使其性能符合要求。

(2)闭运算可以视情况决定是否使用。对于网球上的LOGO使用闭运算是一种非常有效的方法。而对于纯色的网球,包括该程序扩展的应用场景,则不一定需要使用闭运算。

相比于第一种霍夫变换的方法,这一方法的性能更加稳定,此外经过闭运算后可以保证网球在一定的遮挡下也能被检测出来。但这种方法仍然不是最完美的方法。它受限于摄像头的好坏,如果摄像头性能不佳,网球的颜色可能完全变为白色,导致网球中一大块颜色变为白色,无法在HSV颜色空间中分离出来,造成检测失败。而且,分割时用到的阈值需要在不断的调试中进行调整。目前,针对我的摄像头,这一阈值是H(20,40),S(50,200),V(140,255)。

版权声明:本文为CSDN博主「洛伦兹变换砖家」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/weixin_44638585/article/details/122827717

我还没有学会写个人说明!

暂无评论

发表评论

相关推荐

Python如何优雅地可视化目标检测框

1 引言 随着计算机视觉算法工程师的内卷,从事目标检测的小伙伴们越来越多了. 很多时候我们费了九牛二虎之力训练了一版模型,可是可视化出来的效果平淡无奇. 是不是有点太不给力啦,作为计算机视觉工程师,我们是不是应该关注下如何优雅地可视化我们模型

目标检测修改检测框大小、粗细

修改mmdetection参数 如果你的网络是用mmdetection写的,可视化预测结果时,发现框的线条太细,当输入图片太大时会看不清标注的框。 我们可以通过修改mmdtection中的一些参数