使用Python,OpenCV追踪对象的轨迹,来确定其移动方向

这篇博客是上一篇博客: 使用Python,OpenCV转换颜色空间,追踪对象的轨迹的扩展。将使用Python,OpenCV追踪对象的轨迹,来确定其移动方向;

虽然球跟踪展示了目标检测和跟踪的基础知识,但无法计算球的实际移动方向。通过在两个单独的帧中简单地计算对象(x,y)坐标之间的增量,就能够正确地跟踪对象的运动,甚至报告其移动的方向。
也可以通过简单地分别取dX和dY的反正切来报告实际的运动角度,从而使这个对象运动跟踪器更加精确。

1. 效果图

gif效果图如下:
在这里插入图片描述

部分截图如下:
注意:有点像镜面成像,刚好跟看到的方向相反
向右侧东移动
可以看到球被正确检测到,并正在向“东”移动,北”方向是通过检查dX和dY值(显示在帧的左下角)来确定的。自| dY |>20以来,能够确定y坐标发生了重大变化。dX正,dY负,因此确定是向右。
在这里插入图片描述

在这里插入图片描述
向西南方向移动
在这里插入图片描述

2. 源码

# 使用Python,OpenCV追踪对象的轨迹以确定其移动方向
# USAGE
# 使用网络摄像头流
# python object_movement.py
# 使用视频文件
# python object_movement.py --video imgs/ball_movement.mp4

# 导入必要的包
import argparse
import math
import time
from collections import deque  # 使用Python内置的deque数据类型来高效地存储对象检测和跟踪的过去N个点

import cv2
import imutils  # 使用IMUTIL(已经收集了OpenCV和Python方便的函数)
import numpy as np
from imutils.video import VideoStream


# 获取角度
def get_angle(delta_x, delta_y):
    angle = 0
    if delta_x == 0 or delta_x == 0.0:
        b = math.pi / 2.0
        angle = b / math.pi * 180
    elif delta_y == 0 or delta_y == 0.0:
        angle = 0.0
    elif delta_y < 0:
        angle -= 180
    else:
        b = math.atan(delta_y / delta_x)
        angle = b / math.pi * 180
    if delta_y > 0 and delta_x < 0:
        angle = angle + 180
    if delta_y < 0 and delta_x < 0:
        angle = angle - 180
    return angle


# 绘制轮廓ID号
def draw_contour(image, c, i):
    # cv2.drawContours(image, [c], 0, (0, 255, 255), 2)
    # 计算轮廓区域的中心,并绘制⭕代表中心
    M = cv2.moments(c)
    cX = int(M["m10"] / M["m00"])
    cY = int(M["m01"] / M["m00"])

    # 在图像上绘制轮廓数
    cv2.putText(image, "{}".format(i + 1), (cX - 5, cY - 5), cv2.FONT_HERSHEY_SIMPLEX,
                0.5, (0, 255, 0), 1)

    (x, y), radius = cv2.minEnclosingCircle(c)
    center = (int(x), int(y))
    radius = int(radius)
    cv2.circle(image, center, radius, (0, 255, 255), 1)

    # 返回绘制了轮廓数的图像
    return image


# 构建命令行参数及解析
# --video:如果省略了--video开关,则将(尝试)使用网络摄像头。
# -buffer,它控制点的deque的最大大小。deque越大,跟踪对象的(x,y)坐标就越多,基本上提供了对象在视频流中的位置的更大“历史”。默认为32表示将只为之前的32帧维护对象(x,y)坐标的缓冲区。
ap = argparse.ArgumentParser()
ap.add_argument("-v", "--video",
                help="path to the (optional) video file")
ap.add_argument("-b", "--buffer", type=int, default=8,
                help="max buffer size")
args = vars(ap.parse_args())

# 定义绿色的HSV空间的上下限值
greenLower = (29, 86, 6)
greenUpper = (64, 255, 255)
greenLower = (0, 0, 57)
greenUpper = (179, 207, 255)

# 初始化list以存储追踪的中心点,帧计数器,坐标值,及方向
pts = deque(maxlen=args["buffer"])
counter = 0
(dX, dY) = (0, 0)
direction = ""

# 如果未提供视频文件,则获取摄像头
# imutils.video VideoStream类以线程方式处理相机帧。处理视频文件帧时使用cv2.VideoCapture捕获做得最好
if not args.get("video", False):
    vs = VideoStream(src=0).start()
    # 预热2s
    time.sleep(2.0)
# 否则,获取视频文件指针
else:
    vs = cv2.VideoCapture(args["video"])

num = 0
# 遍历帧
while True:
    # 获取当前帧
    frame = vs.read()

    # 处理VideoCapture or VideoStream中的帧
    frame = frame[1] if args.get("video", False) else frame
    # 如果处理的是视频文件,未获取到帧则表明到达文件末尾,跳出循环
    if frame is None:
        break

    # 创建HSV图像,并根据最低、最高阈值进行阈值化
    # 缩放帧,应用高斯模糊来平滑图像并减少高频噪声,对帧进行预处理,最后将帧转换为HSV颜色空间
    # frame = imutils.resize(frame, width=400)
    blurred = cv2.GaussianBlur(frame, (11, 11), 0)
    hsv = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV)
    mask = cv2.inRange(hsv, greenLower, greenUpper)
    # 构建一个绿色mask
    # 执行一系列腐蚀、膨胀以去除面具上留下的任何小的斑点
    mask = cv2.erode(mask, None, iterations=2)
    mask = cv2.dilate(mask, None, iterations=2)
    # cv2.imshow("mask", mask)

    (T, thresh) = cv2.threshold(mask, 0, 255, cv2.THRESH_BINARY_INV)
    # cv2.imshow("thresh", thresh)
    output = cv2.bitwise_and(frame, frame, mask=thresh)
    # cv2.imshow("output", output)

    # 查找轮廓,初始化球的中心坐标
    cnts = cv2.findContours(thresh.copy(), cv2.RETR_EXTERNAL,
                            cv2.CHAIN_APPROX_SIMPLE)
    cnts = imutils.grab_contours(cnts)
    center = None
    # print(len(cnts))

    # 至少一个轮廓找到时继续处理
    if len(cnts) > 0:
        # # 根据面积过滤掉面积过大、过小的不合法轮廓
        # cnts = [cnt for cnt in cnts if cv2.contourArea(cnt) > 2000]
        # for (i, c) in enumerate(cnts):
        #     print(cv2.contourArea(c))
        #     draw_contour(frame, c, i)
        # # 展示排序后的输出图像
        # cv2.imshow("res", frame)
        # cv2.waitKey(0)

        # 寻找mask中面积最大的轮廓,并计算最小外接圆和质心(质心只是对象的中心(x,y)坐标。)
        c = max(cnts, key=cv2.contourArea)
        ((x, y), radius) = cv2.minEnclosingCircle(c)
        M = cv2.moments(c)
        center = (int(M["m10"] / M["m00"]), int(M["m01"] / M["m00"]))

        # 当半径大于5时继续处理
        if radius > 3:
            # 在帧上绘制圆的中心,然后更新追踪点队列
            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)

    # 实际跟踪对象的移动,然后使用该对象的移动来计算对象的移动方向,只使用对象的(x,y)-坐标:
    # 遍历追踪点队列点
    for i in np.arange(1, len(pts)):
        # 如果追踪点为None,则跳过
        if pts[i - 1] is None or pts[i] is None:
            continue

        # 检查缓冲队列是否已累计足够的点
        if counter >= 5 and i == 1 and pts[-5] is not None:
            # 计算x,y坐标的变化,并重新初始化方向文本变量
            dX = pts[-5][0] - pts[i][0]
            dY = pts[-5][1] - pts[i][1]
            (dirX, dirY) = ("", "")

            # 计算当前帧和前一帧之间对象的方向。然而,使用当前帧和前一帧有点不稳定。除非对象移动得非常快,否则(x,y)坐标之间的增量将非常小。
            # 如果使用这个值来报告方向,那么结果将非常嘈杂,这意味着即使轨迹上微小的变化也会被视为方向变化(这些变化可能非常小,以至于肉眼几乎看不见(或者至少是微不足道的))
            # 相反,更有可能对较大的对象移动感兴趣,并报告对象移动的方向,因此计算当前帧坐标与队列中较后帧坐标之间的差值。执行此操作有助于减少噪音和方向更改的错误报告。
            # 通过降低阈值,可以使方向检测代码更加敏感。在这种情况下,20个像素的差异可以获得良好的结果。但是如果要检测微小的移动,只需减小该值即可。另一方面,如果只想报告大型对象的移动,只需增加该阈值即可。
            # 确保x方向有有效移动
            if np.abs(dX) > 20:
                dirX = "East" if np.sign(dX) == 1 else "West"

            # 确保y方向有有效移动
            if np.abs(dY) > 20:
                dirY = "North" if np.sign(dY) == 1 else "South"

            # 当x,y方向均有移动时
            if dirX != "" and dirY != "":
                direction = "{}-{}".format(dirY, dirX)
            # 仅有一个方向移动时
            else:
                direction = dirX if dirX != "" else dirY
            print(dX, dY, str(get_angle(dX, dY)), direction)

        # 否则,计算物体轨迹线的宽度,并绘制连接线
        thickness = int(np.sqrt(args["buffer"] / float(i + 1)) * 2.5)
        cv2.line(frame, pts[i - 1], pts[i], (0, 0, 255), thickness)

    # 在帧上,展示移动的方向和dx,dy增量
    cv2.putText(frame, direction, (5, 80), cv2.FONT_HERSHEY_SIMPLEX,
                0.65, (0, 0, 255), 3)
    cv2.putText(frame, "dx: {}, dy: {}, angle: {}".format(dX, dY, str(get_angle(delta_x=dX, delta_y=dY))),
                (5, frame.shape[0] - 5), cv2.FONT_HERSHEY_SIMPLEX,
                0.35, (0, 0, 255), 1)

    num = num + 1
    cv2.imwrite("images/" + str(num) + ".jpg", frame)
    time.sleep(0.020)

    # 展示帧,并累计帧计数器
    cv2.imshow("Frame", frame)
    key = cv2.waitKey(1) & 0xFF
    counter += 1

    # 按下‘q’键退出
    if key == ord("q"):
        break

# 释放摄像头
if not args.get("video", False):
    vs.stop()

# 否则,释放文件
else:
    vs.release()

# 关闭所有窗口
cv2.destroyAllWindows()

参考

猜你喜欢

转载自blog.csdn.net/qq_40985985/article/details/125505108
今日推荐