乒乓球位置检测


2021年3月14日

参考资料:



文章目录

  • 乒乓球位置检测
  • 一、颜色追踪方法:
  • (一)代码:
  • (二)算法思路:
  • (三)实验效果与分析:
  • 二、Hough圆检测方法
  • (一)思路与代码:
  • (二)实验效果与分析:
  • 三、两种方案的比较



一、颜色追踪方法:

(一)代码:

基于python与OpenCV

import cv2
import numpy as np
# 乒乓球位置识别,加入了指示移动方向的箭头


def empty(a):
    pass


def draw_direction(img, lx, ly, nx, ny):
    # 根据上一位置与当前位置计算移动方向并绘制箭头
    dx = nx - lx
    dy = ny - ly
    if abs(dx) < 4 and abs(dy) < 4:
        dx = 0
        dy = 0
    else:
        r = (dx**2 + dy**2)**0.5
        dx = int(dx/r*40)
        dy = int(dy/r*40)
        # print(dx, dy)
    cv2.arrowedLine(img, (60, 100), (60+dx, 100+dy), (0, 255, 0), 2)
    # print(nx-lx, ny-ly)   # 噪声一般为+-1
    # cv2.arrowedLine(img, (150, 150), (150+(nx-lx)*4, 150+(ny-ly)*4), (0, 0, 255), 2, 0, 0, 0.2)


frameWidth = 640
frameHeight = 480
cap = cv2.VideoCapture(0)  # 0对应笔记本自带摄像头
cap.set(3, frameWidth)  # set中,这里的3,下面的4和10是类似于功能号的东西,数字的值没有实际意义
cap.set(4, frameHeight)
cap.set(10, 80)        # 设置亮度
pulse_ms = 30

# 调试用代码,用来产生控制滑条
# cv2.namedWindow("HSV")
# cv2.resizeWindow("HSV", 640, 300)
# cv2.createTrackbar("HUE Min", "HSV", 4, 179, empty)
# cv2.createTrackbar("SAT Min", "HSV", 180, 255, empty)
# cv2.createTrackbar("VALUE Min", "HSV", 156, 255, empty)
# cv2.createTrackbar("HUE Max", "HSV", 32, 179, empty)
# cv2.createTrackbar("SAT Max", "HSV", 255, 255, empty)
# cv2.createTrackbar("VALUE Max", "HSV", 255, 255, empty)

lower = np.array([4, 180, 156])     # 适用于橙色乒乓球4<=h<=32
upper = np.array([32, 255, 255])

targetPos_x = 0
targetPos_y = 0
lastPos_x = 0
lastPos_y = 0

while True:
    _, img = cap.read()

    imgHsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)

    # h_min = cv2.getTrackbarPos("HUE Min", "HSV")
    # h_max = cv2.getTrackbarPos("HUE Max", "HSV")
    # s_min = cv2.getTrackbarPos("SAT Min", "HSV")
    # s_max = cv2.getTrackbarPos("SAT Max", "HSV")
    # v_min = cv2.getTrackbarPos("VALUE Min", "HSV")
    # v_max = cv2.getTrackbarPos("VALUE Max", "HSV")
    #
    # lower = np.array([h_min, s_min, v_min])
    # upper = np.array([h_max, s_max, v_max])

    imgMask = cv2.inRange(imgHsv, lower, upper)     # 获取遮罩
    imgOutput = cv2.bitwise_and(img, img, mask=imgMask)
    contours, hierarchy = cv2.findContours(imgMask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)   # 查找轮廓
    # 
    # CV_RETR_EXTERNAL 只检测最外围轮廓
    # CV_CHAIN_APPROX_NONE 保存物体边界上所有连续的轮廓点到contours向量内
    imgMask = cv2.cvtColor(imgMask, cv2.COLOR_GRAY2BGR)     # 转换后,后期才能够与原画面拼接,否则与原图维数不同

    # 下面的代码查找包围框,并绘制
    x, y, w, h = 0, 0, 0, 0
    for cnt in contours:
        area = cv2.contourArea(cnt)
        # print(area)
        if area > 300:
            x, y, w, h = cv2.boundingRect(cnt)
            lastPos_x = targetPos_x
            lastPos_y = targetPos_y
            targetPos_x = int(x+w/2)
            targetPos_y = int(y+h/2)
            cv2.rectangle(img, (x, y), (x+w, y+h), (0, 255, 0), 2)
            cv2.circle(img, (targetPos_x, targetPos_y), 2, (0, 255, 0), 4)

    # 坐标(图像内的)
    cv2.putText(img, "({:0<2d}, {:0<2d})".format(targetPos_x, targetPos_y), (20, 30),
                cv2.FONT_HERSHEY_PLAIN, 1, (0, 255, 0), 2)  # 文字
    draw_direction(img, lastPos_x, lastPos_y, targetPos_x, targetPos_y)

    imgStack = np.hstack([img, imgOutput])
    # imgStack = np.hstack([img, imgMask])            # 拼接
    cv2.imshow('Horizontal Stacking', imgStack)     # 显示
    if cv2.waitKey(pulse_ms) & 0xFF == ord('q'):          # 按下“q”推出(英文输入法)
        print("Quit\n")
        break

cap.release()
cv2.destroyAllWindows()

(二)算法思路:

选取的小球为橙色乒乓球,故考虑采用颜色追踪的方法来确定小球位置。

while True:

  • 获取一帧图片,将获取到的BGR图转换为HSV图;
  • 根据HSV阈值获取遮罩图(需要通过滑块调试,找到合适的的HSV阈值);
  • 查找遮罩图的轮廓;
  • 判断找到的轮廓各自围成的面积大小,对于大于某个值(这里为300)的,认为是乒乓球的轮廓;
  • 确定该轮廓的包围框,将包围框的中心作为乒乓球的位置,更新乒乓球位置;
  • 根据这一帧的位置与上一帧的位置判断乒乓球移动方向;
  • 绘制必要的标记、文字后,输出图片。

(三)实验效果与分析:

简单背景:

python乒乓球 python乒乓球追踪_opencv

简单背景时,效果很好,遮罩为近似的圆形,几乎覆盖了整个乒乓球,获得的位置与乒乓球球心偏差小。

复杂背景:

python乒乓球 python乒乓球追踪_Max_02


python乒乓球 python乒乓球追踪_ci_03

当背景复杂时,易受到强光影响,也会受到背景中颜色相近物体影响,如某些光照下的皮肤。此时,生成的遮罩往往仅仅覆盖乒乓球的一部分,造成位置的偏离。

快速移动时

python乒乓球 python乒乓球追踪_python乒乓球_04

快速移动时,即使乒乓球在画面中已经不是圆形,颜色追踪的方法依然能够找到一个大致的位置。(Hough圆法在此时将失效)

二、Hough圆检测方法

(一)思路与代码:

主要检测部分:

def Hough_circle(imgGray, canvas):
    # 
    # 基于霍夫圆检测找圆,包含了必要的模糊步骤
    # 在imgGray中查找圆,在canvas中绘制结果
    # canvas必须是shape为[x, y, 3]的图片
    global Hough_x, Hough_y
    img = cv2.medianBlur(imgGray, 3)
    img = cv2.GaussianBlur(img, (17, 19), 0)
    # cv2.imshow("Blur", img)
    # cv2.waitKey(30)
    circles = cv2.HoughCircles(img, cv2.HOUGH_GRADIENT, 1, 200,
                               param1=20, param2=50, minRadius=30, maxRadius=70)
    try:
        # try语法保证在找到圆的前提下才进行绘制
        circles = np.uint16(np.around(circles))
        # print(circles)
        # 经测试,circles为:[[[c0_x, c0_y, c0_r], [c1_x, c1_y, c1_r], ...]]
        # 所以for i in circles[0, :]:中的i为每一个圆的xy坐标和半径
    except:
        pass
    else:
        for i in circles[0, :]:
            # draw the outer circle
            cv2.circle(canvas, (i[0], i[1]), i[2], (255, 100, 0), 2)
            # draw the center of the circle
            cv2.circle(canvas, (i[0], i[1]), 2, (0, 0, 255), 3)
            Hough_x = i[0]
            Hough_y = i[1]

进行了输入图像的模糊(滤波)、找圆、位置更新与绘制

检测前的图像处理:

# 霍夫圆检测前的处理Start
    b, g, r = cv2.split(img)    # 分离三个颜色
    r = np.int16(r)             # 将红色与蓝色转换为int16,为了后期做差
    b = np.int16(b)
    r_minus_b = r - b           # 红色通道减去蓝色通道,得到r_minus_b
    r_minus_b = (r_minus_b + abs(r_minus_b)) / 2    # r_minus_b中小于0的全部转换为0
    r_minus_b = np.uint8(r_minus_b)                 # 将数据类型转换回uint8
    # 霍夫圆检测前的处理End

在运用霍夫圆检测方法时,初期直接使用原图的灰度图滤波后检测,发现乒乓球在深色背景下能够较好识别。但是在浅色背景下(例如白色的墙),乒乓球与背景在灰度图中差别不大,一般灰,导致识别效果不理想。这时,考虑到球为橙色,浅色背景一般为灰白色,可以利用颜色的信息对图像进行处理。处理步骤如下:

将图像分离为RGB三个通道并显示:

python乒乓球 python乒乓球追踪_Max_05

可见在R中,乒乓球发亮,在B中,乒乓球发黑,因为乒乓球中红色成分大,而蓝色成分极少。而背景为灰白色,红色与蓝色成分相差不大。所以,使用R通道减去B通道后,乒乓球还是明亮的(值更大),背景将会变黑(值变小,此处也可能出现负数值,所以要将负数值都改为0),最终输出效果如下:

python乒乓球 python乒乓球追踪_python乒乓球_06

可见效果不错,乒乓球被明显的分离出来了,接下来用处理后的图像进行霍夫圆检测,在浅色背景下也取得了不错的效果。

完整的代码:

import cv2
import numpy as np
# 
# 


def empty(a):
    pass


def draw_direction(img, lx, ly, nx, ny):
    dx = nx - lx
    dy = ny - ly
    if abs(dx) < 4 and abs(dy) < 4:
        dx = 0
        dy = 0
    else:
        r = (dx**2 + dy**2)**0.5
        dx = int(dx/r*40)
        dy = int(dy/r*40)
        # print(dx, dy)
    cv2.arrowedLine(img, (60, 100), (60+dx, 100+dy), (0, 255, 0), 2)
    # print(nx-lx, ny-ly)   # 噪声一般为+-1
    # cv2.arrowedLine(img, (150, 150), (150+(nx-lx), 150+(ny-ly)), (0, 0, 255), 2, 0, 0, 0.2)


def Hough_circle(imgGray, canvas):
    # 基于霍夫圆检测找圆,包含了必要的模糊步骤
    # 在imgGray中查找圆,在canvas中绘制结果
    # canvas必须是shape为[x, y, 3]的图片
    global Hough_x, Hough_y
    img = cv2.medianBlur(imgGray, 3)
    img = cv2.GaussianBlur(img, (17, 19), 0)
    # cv2.imshow("Blur", img)
    # cv2.waitKey(30)
    circles = cv2.HoughCircles(img, cv2.HOUGH_GRADIENT, 1, 200,
                               param1=20, param2=50, minRadius=30, maxRadius=70)
    try:
        # try语法保证在找到圆的前提下才进行绘制
        circles = np.uint16(np.around(circles))
        # print(circles)
        # 经测试,circles为:[[[c0_x, c0_y, c0_r], [c1_x, c1_y, c1_r], ...]]
        # 所以for i in circles[0, :]:中的i为每一个圆的xy坐标和半径
    except:
        pass
    else:
        for i in circles[0, :]:
            # draw the outer circle
            cv2.circle(canvas, (i[0], i[1]), i[2], (255, 100, 0), 2)
            # draw the center of the circle
            cv2.circle(canvas, (i[0], i[1]), 2, (0, 0, 255), 3)
            Hough_x = i[0]
            Hough_y = i[1]


frameWidth = 640
frameHeight = 480
cap = cv2.VideoCapture(0)  # 0对应笔记本自带摄像头
cap.set(3, frameWidth)  # set中,这里的3,下面的4和10是类似于功能号的东西,数字的值没有实际意义
cap.set(4, frameHeight)
cap.set(10, 80)        # 设置亮度
pulse_ms = 30

# 调试用代码,用来产生控制滑条
# cv2.namedWindow("HSV")
# cv2.resizeWindow("HSV", 640, 300)
# cv2.createTrackbar("HUE Min", "HSV", 4, 179, empty)
# cv2.createTrackbar("SAT Min", "HSV", 180, 255, empty)
# cv2.createTrackbar("VALUE Min", "HSV", 156, 255, empty)
# cv2.createTrackbar("HUE Max", "HSV", 32, 179, empty)
# cv2.createTrackbar("SAT Max", "HSV", 255, 255, empty)
# cv2.createTrackbar("VALUE Max", "HSV", 255, 255, empty)

lower = np.array([4, 180, 156])     # 适用于橙色乒乓球4<=h<=32
upper = np.array([32, 255, 255])

targetPos_x = 0     # 颜色检测得到的x坐标
targetPos_y = 0     # 颜色检测得到的y坐标
lastPos_x = 0       # 上一帧图像颜色检测得到的x坐标
lastPos_y = 0       # 上一帧图像颜色检测得到的x坐标
Hough_x = 0         # 霍夫圆检测得到的x坐标
Hough_y = 0         # 霍夫圆检测得到的y坐标
ColorXs = []        # 这些是用来存储x,y坐标的列表,便于后期写入文件
ColorYs = []
HoughXs = []
HoughYs = []

while True:
    _, img = cap.read()

    # 霍夫圆检测前的处理Start
    b, g, r = cv2.split(img)    # 分离三个颜色
    r = np.int16(r)             # 将红色与蓝色转换为int16,为了后期做差
    b = np.int16(b)
    r_minus_b = r - b           # 红色通道减去蓝色通道,得到r_minus_b
    r_minus_b = (r_minus_b + abs(r_minus_b)) / 2    # r_minus_b中小于0的全部转换为0
    r_minus_b = np.uint8(r_minus_b)                 # 将数据类型转换回uint8
    # 霍夫圆检测前的处理End

    imgHough = img.copy()   # 用于绘制识别结果和输出

    imgHsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)

    # h_min = cv2.getTrackbarPos("HUE Min", "HSV")
    # h_max = cv2.getTrackbarPos("HUE Max", "HSV")
    # s_min = cv2.getTrackbarPos("SAT Min", "HSV")
    # s_max = cv2.getTrackbarPos("SAT Max", "HSV")
    # v_min = cv2.getTrackbarPos("VALUE Min", "HSV")
    # v_max = cv2.getTrackbarPos("VALUE Max", "HSV")
    #
    # lower = np.array([h_min, s_min, v_min])
    # upper = np.array([h_max, s_max, v_max])

    imgMask = cv2.inRange(imgHsv, lower, upper)     # 获取遮罩
    imgOutput = cv2.bitwise_and(img, img, mask=imgMask)
    contours, hierarchy = cv2.findContours(imgMask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)   # 查找轮廓
    # 
    # CV_RETR_EXTERNAL 只检测最外围轮廓
    # CV_CHAIN_APPROX_NONE 保存物体边界上所有连续的轮廓点到contours向量内
    imgMask = cv2.cvtColor(imgMask, cv2.COLOR_GRAY2BGR)     # 转换后,后期才能够与原画面拼接,否则与原图维数不同

    # 下面的代码查找包围框,并绘制
    x, y, w, h = 0, 0, 0, 0
    for cnt in contours:
        area = cv2.contourArea(cnt)
        # print(area)
        if area > 300:
            x, y, w, h = cv2.boundingRect(cnt)
            lastPos_x = targetPos_x
            lastPos_y = targetPos_y
            targetPos_x = int(x+w/2)
            targetPos_y = int(y+h/2)
            cv2.rectangle(img, (x, y), (x+w, y+h), (0, 255, 0), 2)
            cv2.circle(img, (targetPos_x, targetPos_y), 2, (0, 255, 0), 4)

    # 坐标(图像内的)
    cv2.putText(img, "({:0<2d}, {:0<2d})".format(targetPos_x, targetPos_y), (20, 30),
                cv2.FONT_HERSHEY_PLAIN, 1, (0, 255, 0), 2)  # 文字
    draw_direction(img, lastPos_x, lastPos_y, targetPos_x, targetPos_y)

    # 霍夫圆检测Start
    Hough_circle(r_minus_b, imgHough)
    cv2.imshow("R_Minus_B", r_minus_b)
    cv2.putText(imgHough, "({:0<2d}, {:0<2d})".format(Hough_x, Hough_y), (20, 30),
                cv2.FONT_HERSHEY_PLAIN, 1, (255, 100, 0), 2)
    # 霍夫圆检测End

    imgStack = np.hstack([img, imgHough])
    # imgStack = np.hstack([img, imgMask])            # 拼接
    cv2.imshow('Horizontal Stacking', imgStack)     # 显示

    ColorXs.append(targetPos_x)     # 坐标存入列表
    ColorYs.append(targetPos_y)
    HoughXs.append(Hough_x)
    HoughYs.append(Hough_y)

    if cv2.waitKey(pulse_ms) & 0xFF == ord('q'):          # 按下“q”推出(英文输入法)
        print("Quit\n")
        break

filename = 'xy.txt'     # 坐标存入文件

with open(filename, 'w') as file_object:
    file_object.write("Color:\n")
    for i in ColorXs:
        file_object.write("{:d}\n".format(i))
    file_object.write("\n***********\n")
    for i in ColorYs:
        file_object.write("{:d}\n".format(i))
    file_object.write("\nHough:\n")
    for i in HoughXs:
        file_object.write("{:d}\n".format(i))
    file_object.write("\n***********\n")
    for i in HoughYs:
        file_object.write("{:d}\n".format(i))

cap.release()
cv2.destroyAllWindows()

完整的代码既有方案一的颜色追踪,也有方案二的霍夫圆检测,并将两种方法得到的坐标值写入了文件中,便于接下来的比较。

(二)实验效果与分析:

python乒乓球 python乒乓球追踪_opencv_07

在乒乓球运动不快时,检测效果好,绘制出的蓝色圆与乒乓球边缘重合度高。但是在乒乓球运动时,出现了部分帧检测不到圆的情况。

图像处理对浅色、深色背景有较好适应性,如上图右边所示,能够较好地把乒乓球凸显出来。

三、两种方案的比较

基于第二部分代码产生的数据,在光线环境较好的情况下(没有强光照射乒乓球导致严重的高光),在Excel中处理绘制折线图,如下所示:

python乒乓球 python乒乓球追踪_Max_08


python乒乓球 python乒乓球追踪_python乒乓球_09

在环境条件较好时,颜色追踪与霍夫圆检测相差不大。

霍夫圆检测存在检测不到圆的情况(折线图中橙色线变水平时,说明位置没有更新,即没有检测到圆)。

有高光干扰使得颜色追踪得到的遮罩不完整时,霍夫圆检测更加准确。