• 单目标跟踪之相关滤波 MOSSE


    相关滤波

    相关操作

    卷积操作

    MOSSE

    基本思想

    具体操作流程

    代码解读

    初始化

    在线更新

    缺点

      论文名称:Visual Object Tracking using Adaptive Correlation Filters

      文献地址:https://www.cs.colostate.edu/~vision/publications/bolme_cvpr10.pdf

      源码地址:https://github.com/xingqing45678/Mosse_CF

      由于基于CNNs深度学习在单目标跟踪方法的参数量和计算量都较大,难以与目标检测算法一起移植到嵌入式中。同时,受CVPR2020 AutoTrack的影响,开始从基于传统CF,DCF思想角度入手,对相关滤波CF的鼻祖MOSSE进行攻读。

      本文主要介绍相关滤波系列算法的开篇——MOSSE基本原理及其python代码实现流程。

      由于论文中涉及到大量傅里叶变换的公式,可能显得MOSSE晦涩难懂,本篇尽量淡化傅里叶变换的公式内容,突出MOSSE在跟踪过程中所做的工作。

    相关滤波

      相关滤波(CF)源于信号处理领域,有这么一句话"两个信号越相似,其相关值越高。在跟踪,就是找到与跟踪目标响应最大的项" 。通过后续代码的解读,会体会到相关值越高在MOSSE中的作用。

    相关操作

      假设有两个信号f和g,则两个信号的相关性(correlation)为:

    其中,f表示f的复共轭...(说好的淡化公式的... 看不明白不要紧,只要明白卷积就可以往下看)

      对于图像而言,相关性可以理解为相关核在图像上进行点积操作,如下图所示。图像的相关公式可以表达为g = f ⓧ h,具体而言,对应元素相乘在求和:

      从上图的结果可以看出,输出正好是相关核旋转了180°

      具体步骤:

      a. 相关核,中心分别位于输入图像的f(i, j)像素上进行滑动;(会补零)

      b. 利用上式进行点积操作,并求和,得到输出图像对应的像素值g(i, j);

      c. 在输出图像上进行滑动,重复b的操作,求出输出图像上的所有像素值。

      你可能会觉得相关操作和卷积操作一样,但二者实际上是有区别的。

    卷积操作

      图像卷积操作的公式:g = f ★ h,具体而言,对应到每个像素的表达式如下所示,可以看出,卷积操作满足交换律。

       In Convolution operation, the kernel is first flipped by an angle of 180 degrees and is then applied to the image.

      也就是说, 卷积操作与相关操作的差异在于卷积将核kernel旋转了180度(顺逆都一样);相关可以反应两个信号的相似度,卷积不可以;卷积满足交换律,相关不可以;卷积可以直接通过卷积定理(时域上的卷积等于频域上的乘积)来加速运算,相关不可以。

    参考文章:https://towardsdatascience.com/convolution-vs-correlation-af868b6b4fb5

    MOSSE

    基本思想

      MOSSE的基本思想:以上一帧目标位置为bbox,在当前帧图像上截取目标,使用相关核找到当前截取图像上的最大响应,这里的最大响应就是当前目标的中心,然后更新当前帧目标的中心,并通过第一帧给出的bbox的宽高为宽高,生成此时物体的框体。(这里有一个假设,就是相邻帧物体不会发生过大的位移,即使用上一帧的bbox截取当前帧的图像,仍可以获得到目标的中心) --- 可以看出,MOSSE对于物体大小的变化,以及物体的消失没有丝毫抵抗能力。

    具体操作流程

      通过基本思想可以看出,需要首先获得一个可以在目标中心获得最大响应的相关核h。即下述公式操作后,g中最大值是物体的中心。

      由于上式在计算机中计算量较大,作者对上式采用快速傅里叶变换(FFT):

      便得到论文中给出的

      因此,跟踪的任务便是找到相应的H*

      在实际跟踪中,需要考虑目标外观的变化等因素的影响,需要进行一个优化的求解,即输出的响应与期望的响应越接近越好:

      该优化的求解可以根据偏导数进行求取,求取结果可以直接表达为:

      以上的操作均是在跟踪第一帧完成的。也就是说,相关核H的确定是通过第一帧标出的目标在线得出的。而上式中的i,对应到代码中即为若干次图像的仿射变换。而对应的输出响应的期望G*是高斯核,后续会结合代码具体解读。

      为了提升算法的鲁棒性,应对目标在跟踪过程中的外观变换,需要对核进行在线的更新,更新策略如下所示:

      值得一提的是,F、G、H的尺寸都是一致的。并且,在工程实践中确实将H分解为A,B两部分,并进行迭代更新。

    代码解读

    初始化

      初始化应用于第一帧的功能实现,包括初始bbox信息的获取(中心点、宽、高、特征),以及核的生成。其中,包含图像的预处理和准备工作:

    • 灰度化(输入特征为灰度,特征表达能力有限)
    • 归一化
    • 余弦窗的生成
    • 期望输出响应G的生成(高斯核,同时让图片的左边缘与右边缘连接,上边缘与下边缘连接)

      之后,便是最终要的核的生成。代码如下:

     1 def init(self,first_frame,bbox):
     2     if len(first_frame.shape)!=2:
     3         assert first_frame.shape[2] == 3
     4         first_frame=cv2.cvtColor(first_frame, cv2.COLOR_BGR2GRAY)  
     5     first_frame=first_frame.astype(np.float32)/255   # 归一化
     6     x,y,w,h=tuple(bbox)   # x,y为bbox的左下角点,和宽高
     7     self._center=(x+w/2, y+h/2)  # 中心点坐标
     8     self.w,self.h=w,h
     9     w,h=int(round(w)), int(round(h))  # 取整
    10     self.cos_window=cos_window((w,h))   # 生成宽高的一维hanning窗 后再进行矩阵乘法 形成h*w的hanning矩阵
    11     self._fi=cv2.getRectSubPix(first_frame,(w,h), self._center)   # 根据宽高和中心点的位置截取图像/特征
    12     self._G=np.fft.fft2(gaussian2d_labels((w,h), self.sigma))   # 高斯相应图
    13     self.crop_size=(w,h)   # 裁剪尺寸
    14     self._Ai=np.zeros_like(self._G)
    15     self._Bi=np.zeros_like(self._G)
    16     for _ in range(8):
    17         fi=self._rand_warp(self._fi)   # 对裁剪的图像进行8次仿射变化(旋转),从而求得H*
    18         Fi=np.fft.fft2(self._preprocessing(fi,self.cos_window))   # 进行余弦窗处理
    19         self._Ai += self._G*np.conj(Fi)   # conj共轭负数
    20         self._Bi += Fi*np.conj(Fi)     #  求解H*所需

    其中,代码第4-5行:图像转化为灰度图;

          代码第6-9行:获取第一帧图像的信息,将左下角点,宽高信息转化为中心点坐标。(bbox是    通过cv2.selectROI()获得到目标的左下角点坐标和宽高信息。)

          代码第10行:获取余弦窗;

          代码第11行:通过中心点坐标和宽高在第一帧图像上截取目标图像;

          代码第12行:获得高斯响应作为期望输出G。

          代码第16-20行:通过8次仿射变换,获取相关核H。

      余弦窗的生成

      余弦窗通过汉宁窗生成。

    1 def cos_window(sz):
    2     cos_window = np.hanning(int(sz[1]))[:, np.newaxis].dot(np.hanning(int(sz[0]))[np.newaxis, :])
    3     return cos_window

      汉宁窗的模样

      通过下图余弦矩阵的颜色分布可以看出,余弦窗具有两边小中间大的特性,并且四周的值趋近于零,有利于突出靠近中心的目标。

      期望响应输出G的生成

      G相当于是真值,但此处不需要人为的标注。而是通过高斯函数生成即可。高斯函数产生的最大值在图像的中心。生成后,需要进行傅里叶变换转化到频域。

    1 def gaussian2d_labels(sz,sigma):
    2     w, h=sz
    3     xs, ys = np.meshgrid(np.arange(w), np.arange(h))   # 生成两个矩阵 一个矩阵各行是0-w-1  一个矩阵各列是0-h-1
    4     center_x, center_y = w / 2, h / 2
    5     dist = ((xs - center_x) ** 2 + (ys - center_y) ** 2) / (sigma**2)
    6     labels = np.exp(-0.5*dist)
    7     return labels

      为什么可以用高斯函数产生的值来作为期望的响应输出呢?这是由于高斯函数自带中间大两边小的特性,十分符合物体中心是最大值的要求。并且在第一帧框定目标时,也会标出十分贴合的bbox,bbox的中心可以近似为物体的中心

      随机仿射变换

      通过随机数的生成控制仿射变换的力度。

     1 def _rand_warp(self,img):   # 输入是裁剪下的图形
     2     h, w = img.shape[:2]
     3     C = .1
     4     ang = np.random.uniform(-C, C)
     5     c, s = np.cos(ang), np.sin(ang)
     6     W = np.array([[c + np.random.uniform(-C, C), -s + np.random.uniform(-C, C), 0],
     7                   [s + np.random.uniform(-C, C), c + np.random.uniform(-C, C), 0]])
     8     center_warp = np.array([[w / 2], [h / 2]])
     9     tmp = np.sum(W[:, :2], axis=1).reshape((2, 1))
    10     W[:, 2:] = center_warp - center_warp * tmp  # 仿射矩阵
    11     warped = cv2.warpAffine(img, W, (w, h), cv2.BORDER_REFLECT)    # 进行仿射变换  W是仿射矩阵
    12     return warped

    在线更新

      在线更新核心操作在于相关核的更新(下述代码第6行)以及影响相关核的两个因素的更新(下述代码22-23行)。

      从代码14-18行以及返回值可以看出,在线更新的过程只有中心点位置的更新,宽高不更新,如果物体的大小发生明显的变化,很难自适应。

     1 def update(self,current_frame,vis=False):
     2     if len(current_frame.shape)!=2:
     3         assert current_frame.shape[2]==3
     4         current_frame=cv2.cvtColor(current_frame,cv2.COLOR_BGR2GRAY)
     5     current_frame=current_frame.astype(np.float32)/255
     6     Hi=self._Ai/self._Bi   # Ht  kernel
     7     fi=cv2.getRectSubPix(current_frame,(int(round(self.w)),int(round(self.h))),self._center)   # 裁剪
     8     fi=self._preprocessing(fi,self.cos_window)
     9     Gi=Hi*np.fft.fft2(fi)   # fft
    10     gi=np.real(np.fft.ifft2(Gi))   # 返回负数类型参数的实部, 以及二维傅里叶反变换
    11     if vis is True:
    12         self.score=gi
    13     curr=np.unravel_index(np.argmax(gi, axis=None),gi.shape)   # 找到最大那个相应的位置
    14     dy,dx=curr[0]-(self.h/2),curr[1]-(self.w/2)   # 中心点位置移动量
    15     x_c,y_c=self._center
    16     x_c+=dx
    17     y_c+=dy   # 中心点更新
    18     self._center=(x_c,y_c)
    19     fi=cv2.getRectSubPix(current_frame,(int(round(self.w)),int(round(self.h))),self._center)
    20     fi=self._preprocessing(fi,self.cos_window)
    21     Fi=np.fft.fft2(fi)
    22     self._Ai=self.interp_factor*(self._G*np.conj(Fi))+(1-self.interp_factor)*self._Ai
    23     self._Bi=self.interp_factor*(Fi*np.conj(Fi))+(1-self.interp_factor)*self._Bi
    24     return [self._center[0]-self.w/2,self._center[1]-self.h/2,self.w,self.h]

    缺点

    • 输入的特征为单通道灰度图像,特征表达能力有限
    • 没有尺度更新,对于尺度变化的跟踪目标不敏感
  • 相关阅读:
    Gridview linkbutton click
    Linux Bluetooth编程 HCI层编程
    Google Android 开发工程师职位面试题
    Android下文件操作模式(含SDCard的读写)
    Android中悬浮窗口的实现原理和示例代码
    Android.mk的用法和基础
    Android.mk的用法和基础
    Linux Bluetooth编程 HCI层编程
    Android下文件操作模式(含SDCard的读写)
    init.rc 脚本语法学习
  • 原文地址:https://www.cnblogs.com/monologuesmw/p/13396104.html
Copyright © 2020-2023  润新知