Ball Tracking with OpenCV
https://pyimagesearch.com/2015/09/14/ball-tracking-with-opencv/?utm_source=Drip&utm_medium=Email&utm_campaign=CVandDLCrashCourse&utm_content=email5
goal:
- Detect the presence of the ball
- Track the ball as it moves around in the video frames, drawing its previous position as it moves.
# import the necessary packages
from collections import deque
# 使用deque,一种类似list的数据结构,具有超快的append和pop功能
# 用来维护追踪的球轨迹列表
from imutils.video import VideoStream
import numpy as np
import argparse
import cv2
import imutils
import time
# construct the argument parse and parse the arguments
ap = argparse.ArgumentParser()
ap.add_argument("-v", "--video",
help="path to the (optional) video file")
# 可选命令行参数,提供则指向视频文件;否则访问摄像头
ap.add_argument("-b", "--buffer", type=int, default=64,
help="max buffer size")
# 指定:deque的最大长度,维护了我们正在跟踪的球的previous(x,y)坐标的列表
# 当队列已满时,新的元素将从队头删除,以保持队列的最大长度不变。
# 当向队尾插入新元素时,队列会自动调整以满足最大长度的限制。
args = vars(ap.parse_args())
# define the lower and upper boundaries of the "green"
# ball in the HSV color space, then initialize the
# list of tracked points
greenLower = (29, 86, 6)
greenUpper = (64, 255, 255)
# 在HSV颜色空间中定义“绿色”球的上下边界
pts = deque(maxlen=args["buffer"])
# 然后初始化跟踪点列表,即最大缓冲区大小(默认为64)初始化我们的pts deque
# if a video path was not supplied, grab the reference
# to the webcam
if not args.get("video", False):
vs = VideoStream(src=0).start()
# VideoStream(src=0) 创建了一个视频流对象 vs,并使用参数 src=0 指定了视频源的索引。
# start()方法会启动视频流,开始读取视频帧并提供实时的图像数据
# video空:访问摄像头
# otherwise, grab a reference to the video file
else:
vs = cv2.VideoCapture(args["video"])
# cv2.VideoCapture(args["video"])创建了一个视频捕获对象 `vs`
# video不空:指向视频文件
# allow the camera or video file to warm up
time.sleep(2.0)
# keep looping
while True:
# grab the current frame
frame = vs.read() # frame 将包含成功读取到的图像帧数据
# handle the frame from VideoCapture or VideoStream
frame = frame[1] if args.get("video", False) else frame
# 从args获得video值,如果不存在返回False;存在即frame = frame[1](实际的图像帧数据)
# 如果参数 "video" 的值不存在, frame不变
# if we are viewing a video and we did not grab a frame,
# then we have reached the end of the video
if frame is None:
break
# resize the frame, blur it, and convert it to the HSV
# color space
frame = imutils.resize(frame, width=600) # 调整帧大小,能提高处理速度
blurred = cv2.GaussianBlur(frame, (11, 11), 0) # 高斯模糊,减少高频,专注结构对象
hsv = cv2.cvtColor(blurred, cv2.COLOR_BGR2HSV) # 转换为HSV颜色空间
# construct a mask for the color "green", then perform
# a series of dilations and erosions to remove any small
# blobs left in the mask
# 制作一个颜色为“绿色”的mask,然后进行一系列的扩张和侵蚀,以去除mask中残留的小斑点
mask = cv2.inRange(hsv, greenLower, greenUpper)
# 过滤出指定颜色范围内的像素
# 在hsv中每个像素位置判断,返回一个与 hsv 相同大小的mask数组
# mask中的像素值为 0(不在)或 255(在),表示像素是否在指定颜色范围内
mask = cv2.erode(mask, None, iterations=2)
# 腐蚀,默认腐蚀内核,迭代次数为2
# 腐蚀 用于缩小或消除图像中物体的边界
# 通过将内核与图像进行逐像素的比较,如果内核完全包含在图像中的某个区域,则该区域的像素值为 1,否则为 0。
# 作用:减小mask中前景(绿色区域)的大小或连接性。
# 常用于去除图像中的噪声、分离接触的物体或者缩小物体的尺寸等应用场景
mask = cv2.dilate(mask, None, iterations=2)
# 膨胀 用于扩大图像中物体的边界。
# 通过将内核与图像进行逐像素的比较,如果内核与图像中的某个区域有重叠,则该区域的像素值为 1,否则为 0。
# 扩大掩膜中前景(绿色区域)的大小或连接性
# 常用于填充图像中的空洞、连接断开的物体或者增大物体的尺寸
# 计算绿色球的轮廓(即轮廓)并将其绘制在我们的框架上
# find contours in the mask and initialize the current
# (x, y) center of the ball
cnts = cv2.findContours(mask.copy(), cv2.RETR_EXTERNAL,
cv2.CHAIN_APPROX_SIMPLE)
# mask中找到轮廓
# cv2.RETR_EXTERNAL 是轮廓检索模式,表示只检测最外层的轮廓
# cv2.CHAIN_APPROX_SIMPLE 是轮廓近似方法,表示使用简单的近似方法来压缩轮廓的表示
# cv2.findContours() 函数将对输入进行轮廓检测,返回检测到的轮廓列表
# 每个轮廓是一个表示闭合边界的点集合
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) # 找到mask中最大轮廓
# cv2.contourArea 计算轮廓的面积
# 在轮廓列表 cnts 中找到满足指定条件的最大值,将轮廓的面积作为比较的关键字,
((x, y), radius) = cv2.minEnclosingCircle(c) #计算包围轮廓c的最小外接圆
# 返回圆心坐标和半径
# 找到一个最小的圆,使得轮廓 c 完全位于圆内
M = cv2.moments(c)
# 计算轮廓 c 的矩
center = (int(M["m10"] / M["m00"]), int(M["m01"] / M["m00"])) # 质心
# 在cv中,轮廓的重心是指轮廓区域的质心或中心点,通常用于表示对象的位置
# 重心计算:通过轮廓的一阶矩(m10和m01)和零阶矩(m00)
# 重心的 x 坐标 = m10 / m00
# 重心的 y 坐标 = m01 / m00
# M["m10"] 表示轮廓的一阶矩的 x 分量,M["m01"] 表示轮廓的一阶矩的 y 分量,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)
# update the points queue
pts.appendleft(center) # 将质心appear到更新跟踪点列表
# 最后一步:绘制球的轨迹,或者简单地绘制检测到球的过去N(x,y)坐标
# loop over the set of tracked points
# 在一组跟踪点上循环
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
# 如果当前点或前一点为“无”(表示在该给定帧中未成功检测到球),则忽略当前索引,继续在点上循环
# 检查队列中前一个和当前位置的重心坐标是否都存在。
# 如果其中任意一个重心坐标不存在(即为 None),则说明无法进行连线或绘制操作,需要继续下一个迭代。
# otherwise, compute the thickness of the line and
# draw the connecting lines
thickness = int(np.sqrt(args["buffer"] / float(i + 1)) * 2.5)
# 计算轨迹厚度
# 根据输入参数和迭代次数得出的一个线宽值
# 根据当前的迭代次数和队列的最大长度,动态调整线宽的大小。
# 随着迭代次数的增加,线宽逐渐变细,使得绘制的线条呈现出一种渐变效果,能够更好地表示轨迹的运动特征。
cv2.line(frame, pts[i - 1], pts[i], (0, 0, 255), thickness)
# 框架上绘制轨迹
# show the frame to our screen
cv2.imshow("Frame", frame) # 显示帧
key = cv2.waitKey(1) & 0xFF
# if the 'q' key is pressed, stop the loop
if key == ord("q"):
break
# if we are not using a video file, stop the camera video stream
if not args.get("video", False):
vs.stop()
# otherwise, release the camera
else:
vs.release()
# close all windows
cv2.destroyAllWindows()
注:
deque
是一种双端队列(double-ended queue)数据结构,它是由collections
模块提供的一种容器类型。通过from collections import deque
导入。deque
可以在队列的两端进行高效的插入和删除操作。适合在队头和队尾频繁插入和删除元素的场景。它还可以限制队列的最大长度,以防止无限增长。当队列已满时,新的元素将从队头删除,以保持队列的最大长度不变。当向队尾插入新元素时,队列会自动调整以满足最大长度的限制。VideoStream
是一个用于视频捕获和处理的类,它提供了一个简单的接口来读取实时视频流或已经录制的视频文件。将src=0
传递给VideoStream
,我们指定要使用默认的摄像头作为视频源。摄像头索引通常从0开始递增,所以src=0
表示使用第一个可用的摄像头。start()
方法会启动视频流,开始读取视频帧并提供实时的图像数据。后续使用vs
对象来访问视频流,并进行图像处理或分析。vs = VideoStream(src=0).start()
vs = cv2.VideoCapture(args["video"])
,cv2.VideoCapture(args["video"])
创建了一个视频捕获对象vs
,并使用参数args["video"]
指定了要打开的视频文件路径。vs.read()
是用于从视频流或视频捕获对象中读取一帧图像,read()方法读取视频流的下一帧图像- 对图像先腐蚀后膨胀–即开运算(Opening)。腐蚀消除图像中的小细节、噪声,突出物体边界,使得物体边界向内收缩同时保持物体整体形状;膨胀填充物体空洞、连接物体断开部分,增大物体尺寸,边界向外扩张,保持整体形状。结合的开运算效果:消除小细节和噪声,分离接触的物体,平滑边界,去除空洞,有助于改善图像质量和凸显目标物体。
- 矩:矩(Moments)是一种描述图像特征和形状的数学工具。矩可以用于计算图像的重心、面积、形状特征等。矩是对图像像素的空间分布和强度分布进行数学建模的结果。它们提供了关于图像的统计信息,可以用于计算图像的各种特征。在计算机视觉中,常用的矩包括零阶矩(表示图像的总体强度)、一阶矩(表示图像的重心)、二阶矩(表示图像的方向和形状)、中心矩(表示图像的归一化形状)等。总而言之,矩是用于描述图像特征和形状的数学工具,通过对图像的像素进行加权求和,可以计算出图像的各种统计特征和形状属性。