基于模板匹配的视频目标检测与轨迹跟踪2

Jul 15,2017   10285 words   37 min


在之前的这篇博客中,提出了基于模板匹配的方法进行视频中运动目标的检测。 虽然利用窗口进行了计算量上的优化,但还存在一些问题。下面针对这些问题,对算法进行了进一步优化。

零、视频裁剪

由于原始的卫星视频分辨率较高,直接处理相对较慢,因此需要将视频某一部分裁剪出来用于实验。实验效果不错后再放到完整视频上。 在此之前已经实现了视频相关的基本操作,如下:

再加上这次的视频裁剪,就可以满足大部分需求了。 更多和视频处理相关的代码放在了Github上,点击查看。 视频裁剪程序代码如下:

# coding=utf-8
import cv2

# 视频裁剪

input_path = raw_input("Video path:\n")

out_path = raw_input("Out path:\n")

cap = cv2.VideoCapture(input_path)

width = int(cap.get(3))
height = int(cap.get(4))
total = int(cap.get(7))

print 'x:', width, 'y:', height

left_top_x = input("left-top x:\n")

left_top_y = input("left-top y:\n")

right_bottom_x = input("right-bottom x:\n")

right_bottom_y = input("right-bottom y:\n")

fps = 20
waitTime = 1
count = 0

if cap.get(5) != 0:
    waitTime = int(1000.0 / cap.get(5))
    fps = cap.get(5)

fourcc = cv2.VideoWriter_fourcc(*'XVID')
out = cv2.VideoWriter(out_path, fourcc, fps, (right_bottom_x - left_top_x, right_bottom_y - left_top_y))

while cap.isOpened():
    ret, frame = cap.read()
    if frame is None:
        break
    else:
        res = frame[left_top_y:right_bottom_y, left_top_x:right_bottom_x, :]

        out.write(res)
        cv2.imshow("frame", res)
        count += 1
        print round((count * 1.0 / total) * 100, 2), '%'
        k = cv2.waitKey(waitTime) & 0xFF
        if k == 27:
            break

cap.release()
out.release()

一、问题与不足

利用之前的代码,在实际的卫星视频中进行测试,发现了以下问题。

1.无法识别、识别错误

在卫星视频中,由于每帧影像都很大,且动目标较小。所以很容易出现误匹配或没有结果。 如下图是错误的匹配结果。 输入的模板是飞机,但是飞机并没有识别为最佳匹配点,而是一栋白色建筑被当作了飞机。 下图是全局匹配的结果,颜色越亮说明越有可能是模板图像。 可以发现由于图像很大,所以存在很多白色的干扰点。这些干扰点也就会导致出现跟踪跟丢的情况。

2.跟踪不稳

在测试过程中,发现如果动目标出现了某些变化,会导致跟踪的“跳跃”,从而导致跟踪失败。

3.需要手动输入模板

由于模板匹配需要输入影像,而这需要人工事先准备好,显然需要变成自动的。

针对以上问题,对代码进行了较大修改。

二、代码与改进

# coding=utf-8
import cv2
import numpy as np
import math


def calcVelocity(x1, x2, y1, y2, res, wT):
    dist = pow(pow(y1 - y2, 2) + pow(x1 - x2, 2), 0.5) * res
    v = dist / (wT / 1000.0) * 3.6
    return v


# ---------------必要参数---------------
# 待识别视频路径
video_path = 'E:\\object\\v6out.avi'
# 卫星视频地表分辨率
resolution = 1.13
# 估计最快运动速度
velocity = 850
# ---------------必要参数---------------

# ---------------可选参数---------------
# 提取的模板是否为正方形
isSquare = True
# 相邻轨迹点之间的距离阈值
dis_thresh = 10
# 初始待选窗口大小半径
range_d = 30
# 灰度阈值敏感度,越大灰度阈值越低
gray_factor = 0.2
# 识别框缩放因子,越大绘制的识别框越大
scale_factor = 1.5
# 模板缩放因子,越大模板图像越大
template_factor = 0.6
# 识别框颜色
color = (0, 0, 255)
# 输出路径
parent_path = video_path.replace(video_path.split("\\")[-1], '')
out_path = parent_path + "object.avi"
out_path2 = parent_path + "track.avi"
out_path3 = parent_path + "points.txt"
out_path4 = parent_path + "velocity.txt"
out_path5 = parent_path + "template.jpg"
# ---------------可选参数---------------

# 循环变量
count = 0

# 打开视频
cap = cv2.VideoCapture(video_path)
cap2 = cv2.VideoCapture(video_path)
# 获取视频图像大小
# video_h对应竖直方向,video_w对应水平方向
video_h = int(cap.get(4))
video_w = int(cap.get(3))
total = int(cap.get(7))

# 新建一张与视频等大的影像用于绘制轨迹
track = np.zeros((video_h, video_w, 3), np.uint8)

# tlp用于存放待选窗口的左上角点
tlp = []
# rbp用于存放待选窗口的右下角点
rbp = []
# bottom_right_points用于存放目标区域的右下角点
bottom_right_points = []
# center_points用于存放目标区域的中心点
center_points = []
# trackPoints用于存放目标区域的左上角点
trackPoints = []
# Vs用于存放目标各帧速度
Vs = []

# 根据视频信息计算每一帧的等待时间
if cap.get(5) != 0:
    waitTime = int(1000.0 / cap.get(5))
    fps = cap.get(5)

# 计算物体帧间最大运动范围(像素)
max_range = math.ceil((5.0 * velocity) / (18.0 * resolution * (fps - 1)))
# 计算最大移动距离,作为阈值
dis_thresh = math.ceil(pow(pow(max_range, 2) + pow(max_range, 2), 0.5))

fourcc = cv2.VideoWriter_fourcc(*'XVID')
out = cv2.VideoWriter(out_path, fourcc, fps, (video_w, video_h))
out2 = cv2.VideoWriter(out_path2, fourcc, fps, (video_w, video_h))

# 首先提取模板图像
if cap2.isOpened():
    # 读取前两帧
    ret, frame1 = cap2.read()
    ret, frame2 = cap2.read()
    # 相减做差
    sub = cv2.subtract(frame1, frame2)
    # 得到的结果灰度化
    gray = cv2.cvtColor(sub, cv2.COLOR_BGR2GRAY)
    # 判断作差后的结果是否全为0
    if gray.max() != 0:
        # 找到最大值位置
        loc = np.where(gray == gray.max())
        loc_x = loc[1][0]
        loc_y = loc[0][0]

        # 以loc为中心,range_d为距离向外拓展得到window
        win_tl_x = loc_x - range_d
        win_tl_y = loc_y - range_d
        win_rb_x = loc_x + range_d
        win_rb_y = loc_y + range_d

        # 一些越界的判断
        if win_tl_x < 0:
            win_tl_x = 0
        if win_tl_y < 0:
            win_tl_y = 0
        if win_rb_x > video_w:
            win_rb_x = video_w
        if win_rb_y > video_h:
            win_rb_y = video_h

        # 根据窗口坐标提取窗口内容
        win_ini = cv2.cvtColor(frame1[win_tl_y:win_rb_y, win_tl_x:win_rb_x, :], cv2.COLOR_BGR2GRAY)
        # 获取最大值位置对应的灰度值
        tem_img = cv2.cvtColor(frame1, cv2.COLOR_BGR2GRAY)
        # 由最大值对应灰度值计算合适的灰度阈值
        gray_thresh = tem_img[loc_y, loc_x] - gray_factor * tem_img[loc_y, loc_x]
        # 初始窗口二值化处理
        ret, thresh = cv2.threshold(win_ini, gray_thresh, 255, cv2.THRESH_BINARY)

        # 在初始窗口中寻找轮廓
        img2, contours, hi = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
        # 有可能找到多个轮廓,但认为包含点数最多的那个轮廓是要找的轮廓
        length = []
        for item in contours:
            length.append(item.shape[0])
        target_contour = contours[length.index(max(length))]
        # 获取目标轮廓的坐标信息
        x, y, w, h = cv2.boundingRect(target_contour)

        if isSquare:
            # 保证提取的模板为正方形
            tem_tl_x = win_tl_x + x
            tem_tl_y = win_tl_y + y
            tem_rb_x = win_tl_x + x + w
            tem_rb_y = win_tl_y + y + h

            center_x = (tem_tl_x + tem_rb_x) / 2
            center_y = (tem_tl_y + tem_rb_y) / 2

            delta = int(template_factor * max(w, h))

            real_tl_x = center_x - delta
            real_rb_x = center_x + delta
            real_tl_y = center_y - delta
            real_rb_y = center_y + delta
        else:
            # 不保证模板为正方形
            real_tl_x = win_tl_x + x
            real_tl_y = win_tl_y + y
            real_rb_x = win_tl_x + x + w
            real_rb_y = win_tl_y + y + h

        # 一些越界判断
        if real_tl_x < 0:
            real_tl_x = 0
        if real_tl_y < 0:
            real_tl_y = 0
        if real_rb_x > video_w:
            real_rb_x = video_w
        if real_rb_y > video_h:
            real_rb_y = video_h

        # 提取模板内容
        template = frame1[real_tl_y:real_rb_y, real_tl_x:real_rb_x, :]
        cv2.imshow("Template", template)
        cv2.imwrite(out_path5, template)

        # 计算第一帧待选窗口角点坐标
        # 获取模板的宽高,h竖直方向,w水平方向
        h = template.shape[0]
        w = template.shape[1]
        d = max(w, h)

        offset = int(scale_factor * d)

        # 计算待选窗口左上角点坐标
        tlx = loc_x - d
        tly = loc_y - d
        # 判断是否越界,越界则设置为0
        if tlx < 0:
            tlx = 0
        if tly < 0:
            tly = 0
        range_tl = (tlx, tly)

        # 计算待选窗口右下角点坐标
        rbx = loc_x + w + d
        rby = loc_y + h + d
        # 判断是否越界,越界设置为视频长宽最大值
        if rbx > video_w:
            rbx = video_w
        if rby > video_h:
            rby = video_h
        range_rb = (rbx, rby)

        # 放入角点坐标列表
        tlp.append(range_tl)
        rbp.append(range_rb)
        cap2.release()

# 然后进行模板匹配
while cap.isOpened():
    # 读取每帧内容
    ret, frame = cap.read()
    # 判断帧内容是否为空,不为空继续
    if frame is None:
        break
    else:
        res = cv2.matchTemplate(frame[tlp[count][1]:rbp[count][1], tlp[count][0]:rbp[count][0], :], template,
                                cv2.TM_CCOEFF)
        window = frame[tlp[count][1]:rbp[count][1], tlp[count][0]:rbp[count][0], :]
        cv2.imshow("Window", window)

        # 坐标相关计算
        min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(res)
        # top_left坐标顺序(水平,竖直)(→,↓)
        top_left = (max_loc[0] + tlp[count][0], max_loc[1] + tlp[count][1])
        bottom_right = (top_left[0] + w, top_left[1] + h)
        center_point = ((top_left[0] + bottom_right[0]) / 2, (top_left[1] + bottom_right[1]) / 2)
        if trackPoints.__len__() == 0:
            # 计算待选窗口左上角点坐标
            tlx = top_left[0] - d
            tly = top_left[1] - d
            # 判断是否越界,越界则设置为0
            if tlx < 0:
                tlx = 0
            if tly < 0:
                tly = 0
            range_tl = (tlx, tly)

            # 计算待选窗口右下角点坐标
            rbx = top_left[0] + w + d
            rby = top_left[1] + h + d
            # 判断是否越界,越界设置为视频长宽最大值
            if rbx > video_w:
                rbx = video_w
            if rby > video_h:
                rby = video_h
            range_rb = (rbx, rby)

            # 将待选窗口左上角点坐标和右下角点坐标依次添加到列表中
            tlp.append(range_tl)
            rbp.append(range_rb)

            # 将目标区域的左上角点、中心点、右下角点坐标依次加入列表
            trackPoints.append(top_left)
            bottom_right_points.append(bottom_right)
            center_points.append(center_point)
            cv2.circle(track, center_point, 2, (0, 0, 255), -1)
        else:
            # 加入运动连续性约束,若相邻轨迹点距离相差大于阈值,则认为错误
            distance = abs(trackPoints[-1][0] - top_left[0]) + abs(trackPoints[-1][1] - top_left[1])
            if distance > dis_thresh:
                print '100%'
                break
            else:
                # 计算待选窗口左上角点坐标
                tlx = top_left[0] - d
                tly = top_left[1] - d
                # 判断是否越界,越界则设置为0
                if tlx < 0:
                    tlx = 0
                if tly < 0:
                    tly = 0
                range_tl = (tlx, tly)

                # 计算待选窗口右下角点坐标
                rbx = top_left[0] + w + d
                rby = top_left[1] + h + d
                # 判断是否越界,越界设置为视频长宽最大值
                if rbx > video_w:
                    rbx = video_w
                if rby > video_h:
                    rby = video_h
                range_rb = (rbx, rby)

                # 将待选窗口左上角点坐标和右下角点坐标依次添加到列表中
                tlp.append(range_tl)
                rbp.append(range_rb)

                # 将目标区域的左上角点、中心点、右下角点坐标依次加入列表
                trackPoints.append(top_left)
                bottom_right_points.append(bottom_right)
                center_points.append(center_point)

                # 绘制目标识别框

                cv2.rectangle(frame,
                              (center_point[0] - offset, center_point[1] - offset),
                              (center_point[0] + offset, center_point[1] + offset),
                              color, 2)
                # 绘制运动轨迹
                cv2.line(track, center_points[-2], center_points[-1], (255, 255, 255), 1)

                # 计算速度
                Vs.append(calcVelocity(center_points[-2][0],
                                       center_points[-1][0],
                                       center_points[-2][1],
                                       center_points[-1][1],
                                       resolution,
                                       waitTime))

        # 输出目标、轨迹视频
        out.write(frame)
        out2.write(track)
        count += 1
        print round((count * 1.0 / total) * 100, 2), '%'

        # 显示结果
        cv2.imshow("Tr", track)
        cv2.imshow("Fr", frame)

        # 退出控制
        k = cv2.waitKey(waitTime) & 0xFF
        if k == 27:
            break

# 打印轨迹坐标
print trackPoints

print '相邻帧距离阈值:', dis_thresh
print '灰度阈值:', gray_thresh
print '模板缩放因子:', template_factor
print '识别框缩放因子:', scale_factor

# 输出中心点轨迹
output = open(out_path3, 'w')
for item in center_points:
    output.write(item.__str__() + "\n")

# 输出各帧速度
output2 = open(out_path4, 'w')
for item in Vs:
    output2.write(item.__str__() + "\n")

# 释放对象
cap.release()
out.release()
out2.release()
output.close()
output2.close()

相比于上一个版本的代码,主要有如下改进:

1.增加输出内容

在上一版本代码中,没有输出信息。在上面的代码中,依次输出了提取的动目标视频、轨迹视频、轨迹像素坐标、各帧间速度以及程序自动选择的模板图像。

2.解决图像太大,目标太小问题

针对这个问题,在之前便提出了动态窗口搜索的方法解决。这样一方面大大减少了计算量,另一方面变相“增大”了目标大小。 如目标为10×10像素,而原图是4000×3000像素,这样目标只占全图的0.00083%。在这样的情况下,任何特征检测或匹配算法都有可能失效。 而改成动态搜索窗口后,以三倍长或宽向外拓展,窗口大小为30×30,这样目标占比为11.11%。这样更有利于相关算法的计算。 同时动态搜索窗口也顾及了帧间空间连续性,符合物体运动规律。

3.增加运动连续性约束

由于是对每一帧分别进行模板匹配,所以之前并没有考虑各帧之间的空间相关性。但事实上相邻帧之间是有很强的空间相关性的。 因为物体运动的一般表现形式是连续的,不会出现突然跳跃的情况。因此通过对检测结果增加运动连续性约束来提高检测准确度。 将当前帧检测出的运动目标坐标与上一帧坐标相比,如果两点距离相差很大,大于某一阈值,如20。 那么即认为在该帧检测的动目标出现了跳跃,出错了。这样就不把这一点计入轨迹列表。 如果下下帧检测坐标与列表中的最后一个坐标相差小于阈值,则认为是正确的,加入轨迹列表。直接将列表中最后一个坐标点与这个坐标点相连,作为轨迹。

不过,由于动态搜索窗口方法限制,如果动目标跳跃跳出了搜索窗口,那么程序就无法找到目标了。程序会停止搜索。 因为上面说了,跳跃过大会被认为不符合运动连续性,从而停止检测。如果还在窗口内,则可以继续跟踪。 因此,对于窗口搜索而言,其阈值范围可以设置为0到窗口长或宽的一半。 在这样的范围中,阈值越小,提取的轨迹就越精确。当然,如果采用全图搜索方式,可以避免这种情况。 下图是运动在某帧发生了跳跃,但由于仍在搜索窗口内,因此可以被继续追踪。跳跃的两点以直线相连。

4.解决第一帧配不准问题

第一帧原来的算法是全图搜索匹配,但是由于存在很多干扰点。一旦第一帧匹配错误,那么后面就会一直错下去导致跟踪失败。 因此在第一帧找到动目标的正确初始位置十分重要。 受到动态搜索窗口的启发,在动目标初始位置时也采用了窗口法。这样不仅可以减少计算量,也可以减少很多干扰点。 算法具体流程是利用帧间差分法将前两帧相减,得到有变化的区域。再寻找变化的最大值,即认为是运动目标所在位置。 以该点为中心点,向外拓展指定大小,如50像素,这样便得到了第一帧中运动目标所在的范围。 再在此范围中进行模板匹配,可大大降低匹配的错误率。

5.自动生成模板

自动寻找模板算法主要有两方面,一是动目标位置的确定,二是模板大小的确定。 对于动目标大致位置的确定在上一点中已经可以得到解决了。通过帧间差分即可获得动目标的位置。 而模板大小的确定需要对动目标进行轮廓提取,然后确定BoundingBox。再以一定的距离外扩,最终可以得到自动生成的模板。 模板选取的好坏,会直接影响到识别的效果。整个模板生成流程可如下图表示。

6.完善模板生成算法

之前采用的是设置固定阈值进行二值化提取轮廓,但在测试中发现不同视频中合适的阈值差别比较大。 因此因此无法采用固定阈值,而需根据模板图像动态计算阈值。具体做法是获取到最大位置对应的灰度,并以适当比例降低,如以0.2比例降低,降低后的结果作为阈值。 同时在提取轮廓时有可能出现多个轮廓的情况。对于这种情况通过寻找包含点数最多的轮廓的方法进行。 认为包含点数最多的轮廓即是待检测的目标,起到过滤一些小轮廓噪声的作用。 最后也重写了模板外拓的算法,之前的过于复杂。新算法根据获得的轮廓计算中心点坐标,然后统一加上指定距离,算法更加简单,同时可以保证提取的物体在模板中心。 之前的算法有时会出现目标偏离模板中心的情况。

三、测试与结果

1.测试

输入视频分辨率为3840×1970,时长36秒。输入视频地址,程序自动提取模板,并根据提取的模板进行目标识别及跟踪。 最后输出目标识别视频、轨迹视频、轨迹像素点、帧间速度以及模板图像。 程序在执行时会显示模板图像、实时跟踪窗口、目标跟踪视频、轨迹视频。 下图是程序自动提取出的飞机模板。 下图是目标以及轨迹部分截图。 下图是提取时的动态窗口。 下图是输出的文件以及轨迹点。 下面是另一个视频的提取结果。

2.之前的代码

这个视频使用之前的代码就无法提取动目标了,因为干扰点太多,第一帧识别的动目标就错了。 而且由于没有加入运动连续性约束,在最后一帧时识别到其它地方去了。

2.对比
(1)对比1

通过裁剪视频,去除干扰点后,将改进前后代码的耗时进行了对比。一共进行了两组对比实验,结果如下所示。 可以看到差异非常明显。未优化的耗时最低也在100ms以上,而优化后的耗时稳定在10ms以下。 原因在于将全图搜索改成了动态窗口搜索,大大降低了计算量。

(2)对比2

而将采用动态窗口后的代码与最新代码对比结果如下: 由于之前的算法是第一帧全图搜索,而现在第一帧也采用了窗口搜索,所以在第一帧耗时上有很大的提升。 这样各帧的耗时更加稳定。将第一帧耗时去掉,对比折线图如下。 可以看到耗时也有一定的缩短。这主要是因为模板的大小减小了。由于之前是人工选择模板,而人工选择的模板或多或少都会比较大一些(空白边界多一些)。 而程序自动选择的模板是基于物体轮廓的BoundingBox,以较小的距离外拓,相比于人工选择会少一些空白边缘部分,减小了模板大小。所以性能也有提升。

本文作者原创,未经许可不得转载,谢谢配合

返回顶部