目标估计模型-卡尔曼滤波


日萌社

人工智能AI:Keras PyTorch MXNet TensorFlow PaddlePaddle 深度学习实战(不定时更新)


4.4. 目标估计模型-卡尔曼滤波

学习目标

  • 了解卡尔曼滤波器中的状态变量和观测输入
  • 了解目标框的状态更新向量
  • 了解预测边界框的估计

在这里我们主要完成卡尔曼滤波器进行跟踪的相关内容的实现。

  • 初始化:卡尔曼滤波器的状态变量和观测输入
  • 更新状态变量
  • 根据状态变量预测目标的边界框
  1. 初始化:

    状态量x的设定是一个七维向量:

分别表示目标中心位置的x,y坐标,面积s和当前目标框的纵横比,最后三个则是横向,纵向,面积的变化速率,其中速度部分初始化为0,其他根据观测进行输入。

初始化卡尔曼滤波器参数,7个状态变量和4个观测输入,运动形式和转换矩阵的确定都是基于匀速运动模型,状态转移矩阵F根据运动学公式确定:

量测矩阵H是4*7的矩阵,将观测值与状态变量相对应:

以及相应的协方差参数的设定,根据经验值进行设定。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
       def __init__(self, bbox):
           # 定义等速模型
           # 内部使用KalmanFilter,7个状态变量和4个观测输入
           self.kf = KalmanFilter(dim_x=7, dim_z=4)
           # F是状态变换模型,为7*7的方阵
           self.kf.F = np.array(
               [[1, 0, 0, 0, 1, 0, 0], [0, 1, 0, 0, 0, 1, 0], [0, 0, 1, 0, 0, 0, 1], [0, 0, 0, 1, 0, 0, 0],
                [0, 0, 0, 0, 1, 0, 0], [0, 0, 0, 0, 0, 1, 0], [0, 0, 0, 0, 0, 0, 1]])
           # H是量测矩阵,是4*7的矩阵
           self.kf.H = np.array(
               [[1, 0, 0, 0, 0, 0, 0], [0, 1, 0, 0, 0, 0, 0], [0, 0, 1, 0, 0, 0, 0], [0, 0, 0, 1, 0, 0, 0]])
           # R是测量噪声的协方差,即真实值与测量值差的协方差
           self.kf.R[2:, 2:] *= 10.
           # P是先验估计的协方差
           self.kf.P[4:, 4:] *= 1000.  # give high uncertainty to the unobservable initial velocities
           self.kf.P *= 10.
           # Q是过程激励噪声的协方差
           self.kf.Q[-1, -1] *= 0.01
           self.kf.Q[4:, 4:] *= 0.01
           # 状态估计
           self.kf.x[:4] = convert_bbox_to_z(bbox)
           # 参数的更新
           self.time_since_update = 0
           self.id = KalmanBoxTracker.count
           KalmanBoxTracker.count += 1
           self.history = []
           self.hits = 0
           self.hit_streak = 0
           self.age = 0
  1. 更新状态变量

    使用观测到的目标框更新状态变量

1
2
3
4
5
6
7
8
9
10
 def update(self, bbox):
        # 重置
        self.time_since_update = 0
        # 清空history
        self.history = []
        # hits计数加1
        self.hits += 1
        self.hit_streak += 1
        # 根据观测结果修改内部状态x
        self.kf.update(convert_bbox_to_z(bbox))

  1. 进行目标框的预测

    推进状态变量并返回预测的边界框结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
    def predict(self):
        # 推进状态变量
        if (self.kf.x[6] + self.kf.x[2]) <= 0:
            self.kf.x[6] *= 0.0
        # 进行预测
        self.kf.predict()
        # 卡尔曼滤波的次数
        self.age += 1
        # 若过程中未更新过,将hit_streak置为0
        if self.time_since_update > 0:
            self.hit_streak = 0
        self.time_since_update += 1
        # 将预测结果追加到history中
        self.history.append(convert_x_to_bbox(self.kf.x))
        return self.history[-1]

整个代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
class KalmanBoxTracker(object):
    count = 0

    def __init__(self, bbox):
        """
        初始化边界框和跟踪器
        :param bbox:
        """
        # 定义等速模型
        # 内部使用KalmanFilter,7个状态变量和4个观测输入
        self.kf = KalmanFilter(dim_x=7, dim_z=4)
        self.kf.F = np.array(
            [[1, 0, 0, 0, 1, 0, 0], [0, 1, 0, 0, 0, 1, 0], [0, 0, 1, 0, 0, 0, 1], [0, 0, 0, 1, 0, 0, 0],
             [0, 0, 0, 0, 1, 0, 0], [0, 0, 0, 0, 0, 1, 0], [0, 0, 0, 0, 0, 0, 1]])
        self.kf.H = np.array(
            [[1, 0, 0, 0, 0, 0, 0], [0, 1, 0, 0, 0, 0, 0], [0, 0, 1, 0, 0, 0, 0], [0, 0, 0, 1, 0, 0, 0]])
        self.kf.R[2:, 2:] *= 10.
        self.kf.P[4:, 4:] *= 1000.  # give high uncertainty to the unobservable initial velocities
        self.kf.P *= 10.
        self.kf.Q[-1, -1] *= 0.01
        self.kf.Q[4:, 4:] *= 0.01
        self.kf.x[:4] = convert_bbox_to_z(bbox)
        self.time_since_update = 0
        self.id = KalmanBoxTracker.count
        KalmanBoxTracker.count += 1
        self.history = []
        self.hits = 0
        self.hit_streak = 0
        self.age = 0

    def update(self, bbox):
        """
        使用观察到的目标框更新状态向量。filterpy.kalman.KalmanFilter.update 会根据观测修改内部状态估计self.kf.x。
        重置self.time_since_update,清空self.history。
        :param bbox:目标框
        :return:
        """
        self.time_since_update = 0
        self.history = []
        self.hits += 1
        self.hit_streak += 1
        self.kf.update(convert_bbox_to_z(bbox))

    def predict(self):
        """
        推进状态向量并返回预测的边界框估计。
        将预测结果追加到self.history。由于 get_state 直接访问 self.kf.x,所以self.history没有用到
        :return:
        """
        if (self.kf.x[6] + self.kf.x[2]) <= 0:
            self.kf.x[6] *= 0.0
        self.kf.predict()
        self.age += 1
        if self.time_since_update > 0:
            self.hit_streak = 0
        self.time_since_update += 1
        self.history.append(convert_x_to_bbox(self.kf.x))
        return self.history[-1]

    def get_state(self):
        """
        返回当前边界框估计值
        :return:
        """
        return convert_x_to_bbox(self.kf.x)

总结

  1. 了解初始化,卡尔曼滤波器的状态变量和观测输入

  2. 更新状态变量update()

  3. 根据状态变量预测目标的边界框predict()


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
from filterpy.kalman import KalmanFilter
import numpy as np

def convert_bbox_to_z(bbox):
    """
    将[x1,y1,x2,y2]形式的检测框转为滤波器的状态表示形式[x,y,s,r]。
    其中x、y是框的中心坐标点,s 是面积尺度,r 是宽高比w/h
    :param bbox: [x1,y1,x2,y2] 分别是左上角坐标和右下角坐标 即 [左上角的x坐标,左上角的y坐标,右下角的x坐标,右下角的y坐标]
    :return: [ x, y, s, r ] 4行1列,其中x、y是box中心位置的坐标,s是面积,r是纵横比w/h
    """
    w = bbox[2] - bbox[0] #右下角的x坐标 - 左上角的x坐标 = 检测框的宽
    h = bbox[3] - bbox[1] #右下角的y坐标 - 左上角的y坐标 = 检测框的高
    x = bbox[0] + w / 2.  #左上角的x坐标 + 宽/2 = 检测框中心位置的x坐标
    y = bbox[1] + h / 2.  #左上角的y坐标 + 高/2 = 检测框中心位置的y坐标
    s = w * h   #检测框的宽 * 高 = 检测框面积
    r = w / float(h) #检测框的宽w / 高h = 宽高比
    #因为卡尔曼滤波器的输入格式要求为4行1列,因此该[x, y, s, r]的形状要转换为4行1列再输入到卡尔曼滤波器
    return np.array([x, y, s, r]).reshape((4, 1))

"""
将候选框从中心面积的形式[x,y,s,r] 转换为 坐标的形式[x1,y1,x2,y2]
"""
def convert_x_to_bbox(x, score=None):
    """
    将[cx,cy,s,r]的目标框表示转为[x_min,y_min,x_max,y_max]的形式
    :param x:[ x, y, s, r ],其中x,y是box中心位置的坐标,s是面积,r是纵横比w/h
    :param score: 置信度
    :return:[x1,y1,x2,y2],左上角坐标和右下角坐标
    """
    """
    x[2]:s是面积,原公式s的来源为s = w * h,即检测框的宽 * 高 = 检测框面积。
    x[3]:r是纵横比w/h,原公式r的来源为r = w / float(h),即检测框的宽w / 高h = 宽高比。
    x[2] * x[3]:s*r 即(w * h) * (w / float(h)) = w^2
    sqrt(x[2] * x[3]):sqrt(w^2) = w
    """
    w = np.sqrt(x[2] * x[3]) #sqrt(w^2) = w
    h = x[2] / w #w * h / w = h
    if score is None:
        return np.array([x[0] - w / 2., #检测框中心位置的x坐标 - 宽 / 2 = 左上角的x坐标
                         x[1] - h / 2., #检测框中心位置的y坐标 - 高 / 2 = 左上角的y坐标
                         x[0] + w / 2., #检测框中心位置的x坐标 + 宽 / 2 = 右下角的x坐标
                         x[1] + h / 2.] #检测框中心位置的y坐标 + 高 / 2 = 右下角的y坐标
                        ).reshape((1, 4))
    else:
        return np.array([x[0] - w / 2.,
                         x[1] - h / 2.,
                         x[0] + w / 2.,
                         x[1] + h / 2.,
                         score]).reshape((1, 5))

"""
卡尔曼滤波器进行跟踪的相关内容的实现
    目标估计模型:
        1.根据上一帧的目标框结果来预测当前帧的目标框状态,预测边界框(目标框)的模型定义为一个等速运动/匀速运动模型。
        2.每个目标框都有对应的一个卡尔曼滤波器(KalmanBoxTracker实例对象),
          KalmanBoxTracker类中的实例属性专门负责记录其对应的一个目标框中各种统计参数,
          并且使用类属性负责记录卡尔曼滤波器的创建个数,增加一个目标框就增加一个卡尔曼滤波器(KalmanBoxTracker实例对象)。  
        3.yoloV3、卡尔曼滤波器预测/更新流程步骤
            1.第一步:
                yoloV3目标检测阶段:
                    --> 1.检测到目标则创建检测目标链/跟踪目标链,反之检测不到目标则重新循环目标检测。
                    --> 2.检测目标链/跟踪目标链不为空则进入卡尔曼滤波器predict预测阶段,反之为空则重新循环目标检测。
            2.第二步:
                卡尔曼滤波器predict预测阶段:
                    连续多次预测而不进行一次更新操作,那么代表了每次预测之后所进行的“预测目标和检测目标之间的”相似度匹配都不成功,
                    所以才会出现连续多次的“预测然后相似度匹配失败的”情况,导致不会进入一次更新阶段。
                    如果一次预测然后相似度匹配成功的话,那么然后就会进入更新阶段。
                   
                    --> 1.目标位置预测
                                1.kf.predict():目标位置预测
                                2.目标框预测总次数:age+=1。
                                3.if time_since_update > 0:
                                     hit_streak = 0
                                  time_since_update += 1
                                  1.连续预测的次数,每执行predict一次即进行time_since_update+=1。
                                  2.在连续预测(连续执行predict)的过程中,一旦执行update的话,time_since_update就会被重置为0。
                                  3.在连续预测(连续执行predict)的过程中,只要连续预测的次数time_since_update大于0的话,
                                    就会把hit_streak(连续更新的次数)重置为0,表示连续预测的过程中没有出现过一次更新状态更新向量x(状态变量x)的操作,
                                    即连续预测的过程中没有执行过一次update。
                                  4.在连续更新(连续执行update)的过程中,一旦开始连续执行predict两次或以上的情况下,
                                    当连续第一次执行predict时,因为time_since_update仍然为0,并不会把hit_streak重置为0,
                                    然后才会进行time_since_update+=1;
                                    当连续第二次执行predict时,因为time_since_update已经为1,那么便会把hit_streak重置为0,
                                    然后继续进行time_since_update+=1。
                    --> 2.预测的目标和检测的目标之间的相似度匹配成功则进入update更新阶段,反之匹配失败则删除跟踪目标。
            3.第三步:
                卡尔曼滤波器update更新阶段:
                    如果一次预测然后“预测目标和检测目标之间的”相似度匹配成功的话,那么然后就会进入更新阶段。
                    kf.update([x,y,s,r]):使用的是通过yoloV3得到的“并且和预测框相匹配的”检测框来更新预测框。
                   
                    --> 1.目标位置信息更新到检测目标链/跟踪目标链
                                1.目标框更新总次数:hits+=1。
                                2.history = []
                                  time_since_update = 0
                                  hit_streak += 1
                                    1.history列表用于在预测阶段保存单个目标框连续预测的多个结果,一旦执行update就会清空history列表。
                                    2.连续更新的次数,每执行update一次即进行hit_streak+=1。
                                    3.在连续预测(连续执行predict)的过程中,一旦执行update的话,time_since_update就会被重置为0。
                                    4.在连续预测(连续执行predict)的过程中,只要连续预测的次数time_since_update大于0的话,
                                      就会把hit_streak(连续更新的次数)重置为0,表示连续预测的过程中没有出现过一次更新状态更新向量x(状态变量x)的操作,
                                      即连续预测的过程中没有执行过一次update。
                                    5.在连续更新(连续执行update)的过程中,一旦开始连续执行predict两次或以上的情况下,
                                      当连续第一次执行predict时,因为time_since_update仍然为0,并不会把hit_streak重置为0,
                                      然后才会进行time_since_update+=1;
                                      当连续第二次执行predict时,因为time_since_update已经为1,那么便会把hit_streak重置为0,
                                      然后继续进行time_since_update+=1。
                    --> 2.目标位置修正。
                                1.kf.update([x,y,s,r]):
                                        使用观测到的目标框bbox更新状态变量x(状态更新向量x)。
                                        使用的是通过yoloV3得到的“并且和预测框相匹配的”检测框来更新卡尔曼滤波器得到的预测框。
 
    1.初始化、预测、更新
        1.__init__(bbox):
            初始化卡尔曼滤波器的状态更新向量x(状态变量x)、观测输入[x,y,s,r](通过[x1,y1,x2,y2]转化而来)、状态转移矩阵F、
            量测矩阵H(观测矩阵H)、测量噪声的协方差矩阵R、先验估计的协方差矩阵P、过程激励噪声的协方差矩阵Q。
        2.update(bbox):根据观测输入来对状态更新向量x(状态变量x)进行更新
        3.predict():根据状态更新向量x(状态变量x)更新的结果来预测目标的边界框

    2.状态变量、状态转移矩阵F、量测矩阵H(观测矩阵H)、测量噪声的协方差矩阵R、先验估计的协方差矩阵P、过程激励噪声的协方差矩阵Q
        1.状态更新向量x(状态变量x)
            状态更新向量x(状态变量x)的设定是一个7维向量:x=[u,v,s,r,u^,v^,s^]T。
            u、v分别表示目标框的中心点位置的x、y坐标,s表示目标框的面积,r表示目标框的纵横比/宽高比。
            u^、v^、s^分别表示横向u(x方向)、纵向v(y方向)、面积s的运动变化速率。
            u、v、s、r初始化:根据第一帧的观测结果进行初始化。
            u^、v^、s^初始化:当第一帧开始的时候初始化为0,到后面帧时会根据预测的结果来进行变化。
        2.状态转移矩阵F
            定义的是一个7*7的方阵(其对角线上的值都是1)。。
            运动形式和转换矩阵的确定都是基于匀速运动模型,状态转移矩阵F根据运动学公式确定,跟踪的目标假设为一个匀速运动的目标。
            通过7*7的状态转移矩阵F 乘以 7*1的状态更新向量x(状态变量x)即可得到一个更新后的7*1的状态更新向量x,
            其中更新后的u、v、s即为当前帧结果。
        3.量测矩阵H(观测矩阵H)
            量测矩阵H(观测矩阵H),定义的是一个4*7的矩阵。
            通过4*7的量测矩阵H(观测矩阵H) 乘以 7*1的状态更新向量x(状态变量x) 即可得到一个 4*1的[u,v,s,r]的估计值。
        4.测量噪声的协方差矩阵R、先验估计的协方差矩阵P、过程激励噪声的协方差矩阵Q
            1.测量噪声的协方差矩阵R:diag([1,1,10,10]T)
            2.先验估计的协方差矩阵P:diag([10,10,10,10,1e4,1e4,1e4]T)。1e4:1x10的4次方。
            3.过程激励噪声的协方差矩阵Q:diag([1,1,1,1,0.01,0.01,1e-4]T)。1e-4:1x10的-4次方。
            4.1e数字的含义
                1e4:1x10的4次方
                1e-4:1x10的-4次方
            5.diag表示对角矩阵,写作为diag(a1,a2,...,an)的对角矩阵实际表示为主对角线上的值依次为a1,a2,...,an,
              而主对角线之外的元素皆为0的矩阵。
              对角矩阵(diagonal matrix)是一个主对角线之外的元素皆为0的矩阵,常写为diag(a1,a2,...,an) 。
              对角矩阵可以认为是矩阵中最简单的一种,值得一提的是:对角线上的元素可以为 0 或其他值,对角线上元素相等的对角矩阵称为数量矩阵;
              对角线上元素全为1的对角矩阵称为单位矩阵。对角矩阵的运算包括和、差运算、数乘运算、同阶对角阵的乘积运算,且结果仍为对角阵。
"""
#目标估计模型-卡尔曼滤波
class KalmanBoxTracker(object):
    """
    每个目标框都有对应的一个卡尔曼滤波器(KalmanBoxTracker实例对象),
    KalmanBoxTracker类中的实例属性专门负责记录其对应的一个目标框中各种统计参数,
    并且使用类属性负责记录卡尔曼滤波器的创建个数,增加一个目标框就增加一个卡尔曼滤波器(KalmanBoxTracker实例对象)。
    """
    count = 0 #类属性负责记录卡尔曼滤波器的创建个数,增加一个目标框就增加一个卡尔曼滤波器(KalmanBoxTracker实例对象

    """
    __init__(bbox)
        使用目标框bbox为卡尔曼滤波的状态进行初始化。初始化时传入bbox,即根据观测到的检测框的结果来进行初始化。
        每个目标框都有对应的一个卡尔曼滤波器(KalmanBoxTracker实例对象),
        KalmanBoxTracker类中的实例属性专门负责记录其对应的一个目标框中各种统计参数,
        并且使用类属性负责记录卡尔曼滤波器的创建个数,增加一个目标框就增加一个卡尔曼滤波器(KalmanBoxTracker实例对象)。
       
        1.kf = KalmanFilter(dim_x=7, dim_z=4)
                定义一个卡尔曼滤波器,利用这个卡尔曼滤波器对目标的状态进行估计。
                dim_x=7定义是一个7维的状态更新向量x(状态变量x):x=[u,v,s,r,u^,v^,s^]T。
                dim_z=4定义是一个4维的观测输入,即中心面积的形式[x,y,s,r],即[检测框中心位置的x坐标,y坐标,面积,宽高比]。
        2.kf.F = np.array(7*7的方阵)
                状态转移矩阵F,定义的是一个7*7的方阵其(对角线上的值都是1)。
                通过7*7的状态转移矩阵F 乘以 7*1的状态更新向量x(状态变量x)即可得到一个更新后的7*1的状态更新向量x,
                其中更新后的u、v、s即为当前帧结果。
                通过状态转移矩阵对当前的观测结果进行估计获得预测的结果,然后用当前的预测的结果来作为下一次估计预测的基础。
        3.kf.H = np.array(4*7的矩阵)
                量测矩阵H(观测矩阵H),定义的是一个4*7的矩阵。
                通过4*7的量测矩阵H(观测矩阵H) 乘以 7*1的状态更新向量x(状态变量x) 即可得到一个 4*1的[u,v,s,r]的估计值。
        4.相应的协方差参数的设定,根据经验值进行设定。
                1.R是测量噪声的协方差矩阵,即真实值与测量值差的协方差。
                  R=diag([1,1,10,10]T)
                        kf.R[2:, 2:] *= 10.
                2.P是先验估计的协方差矩阵
                  diag([10,10,10,10,1e4,1e4,1e4]T)。1e4:1x10的4次方。
                        kf.P[4:, 4:] *= 1000.  # 设置了一个较大的值,给无法观测的初始速度带来很大的不确定性
                        kf.P *= 10.
                3.Q是过程激励噪声的协方差矩阵
                  diag([1,1,1,1,0.01,0.01,1e-4]T)。1e-4:1x10的-4次方。
                        kf.Q[-1, -1] *= 0.01
                        kf.Q[4:, 4:] *= 0.01
        5.kf.x[:4] = convert_bbox_to_z(bbox)
                convert_bbox_to_z负责将[x1,y1,x2,y2]形式的检测框bbox转为中心面积的形式[x,y,s,r]。
                状态更新向量x(状态变量x)设定是一个七维向量:x=[u,v,s,r,u^,v^,s^]T。
                x[:4]即表示 u、v、s、r初始化为第一帧bbox观测到的结果[x,y,s,r]。
        6.单个目标框对应的单个卡尔曼滤波器中的统计参数的更新
            每个目标框都有对应的一个卡尔曼滤波器(KalmanBoxTracker实例对象),
            KalmanBoxTracker类中的实例属性专门负责记录其对应的一个目标框中各种统计参数,
            并且使用类属性负责记录卡尔曼滤波器的创建个数,增加一个目标框就增加一个卡尔曼滤波器(KalmanBoxTracker实例对象)。
            1.卡尔曼滤波器的个数
                有多少个目标框就有多少个卡尔曼滤波器,每个目标框都会有一个卡尔曼滤波器,即每个目标框都会有一个KalmanBoxTracker实例对象。
                count = 0:类属性负责记录卡尔曼滤波器的创建个数,增加一个目标框就增加一个卡尔曼滤波器(KalmanBoxTracker实例对象。
                id = KalmanBoxTracker.count:卡尔曼滤波器的个数,即目标框的个数。
                KalmanBoxTracker.count += 1:每增加一个目标框,即增加一个KalmanBoxTracker实例对象(卡尔曼滤波器),那么类属性count+=1。
               
            2.统计一个目标框对应的卡尔曼滤波器中各参数统计的次数
                1.age = 0:
                    该目标框进行预测的总次数。每执行predict一次,便age+=1。
                2.hits = 0:
                    该目标框进行更新的总次数。每执行update一次,便hits+=1。
                3.time_since_update = 0
                    1.连续预测的次数,每执行predict一次即进行time_since_update+=1。
                    2.在连续预测(连续执行predict)的过程中,一旦执行update的话,time_since_update就会被重置为0。
                    3.在连续预测(连续执行predict)的过程中,只要连续预测的次数time_since_update大于0的话,
                      就会把hit_streak(连续更新的次数)重置为0,表示连续预测的过程中没有出现过一次更新状态更新向量x(状态变量x)的操作,
                      即连续预测的过程中没有执行过一次update。
                4.hit_streak = 0
                    1.连续更新的次数,每执行update一次即进行hit_streak+=1。
                    2.在连续更新(连续执行update)的过程中,一旦开始连续执行predict两次或以上的情况下,
                      当连续第一次执行predict时,因为time_since_update仍然为0,并不会把hit_streak重置为0,
                      然后才会进行time_since_update+=1;
                      当连续第二次执行predict时,因为time_since_update已经为1,那么便会把hit_streak重置为0,
                      然后继续进行time_since_update+=1。
        7.history = []:
                保存单个目标框连续预测的多个结果到history列表中,一旦执行update就会清空history列表。
                将预测的候选框从中心面积的形式[x,y,s,r]转换为坐标的形式[x1,y1,x2,y2] 的bbox 再保存到 history列表中。
    """
    def __init__(self, bbox):
        # 定义等速模型
        # 内部使用KalmanFilter,7个状态变量和4个观测输入
        self.kf = KalmanFilter(dim_x=7, dim_z=4)
        # F是状态变换模型,为7*7的方阵
        self.kf.F = np.array([[1, 0, 0, 0, 1, 0, 0],
                              [0, 1, 0, 0, 0, 1, 0],
                              [0, 0, 1, 0, 0, 0, 1],
                              [0, 0, 0, 1, 0, 0, 0],
                              [0, 0, 0, 0, 1, 0, 0],
                              [0, 0, 0, 0, 0, 1, 0],
                              [0, 0, 0, 0, 0, 0, 1]])
        # H是量测矩阵,是4*7的矩阵
        self.kf.H = np.array([[1, 0, 0, 0, 0, 0, 0],
                              [0, 1, 0, 0, 0, 0, 0],
                              [0, 0, 1, 0, 0, 0, 0],
                              [0, 0, 0, 1, 0, 0, 0]])
        # R是测量噪声的协方差,即真实值与测量值差的协方差
        self.kf.R[2:, 2:] *= 10.
        # P是先验估计的协方差
        self.kf.P[4:, 4:] *= 1000.  # 给无法观测的初始速度带来很大的不确定性
        self.kf.P *= 10.
        # Q是过程激励噪声的协方差
        self.kf.Q[-1, -1] *= 0.01
        self.kf.Q[4:, 4:] *= 0.01
        # 状态估计
        self.kf.x[:4] = convert_bbox_to_z(bbox)
        # 参数的更新
        self.time_since_update = 0
        self.id = KalmanBoxTracker.count
        KalmanBoxTracker.count += 1
        self.history = []
        self.hits = 0
        self.hit_streak = 0
        self.age = 0

    """
    update(bbox):使用观测到的目标框bbox更新状态更新向量x(状态变量x)
 
    1.time_since_update = 0
            1.连续预测的次数,每执行predict一次即进行time_since_update+=1。
            2.在连续预测(连续执行predict)的过程中,一旦执行update的话,time_since_update就会被重置为0。
            2.在连续预测(连续执行predict)的过程中,只要连续预测的次数time_since_update大于0的话,
              就会把hit_streak(连续更新的次数)重置为0,表示连续预测的过程中没有出现过一次更新状态更新向量x(状态变量x)的操作,
              即连续预测的过程中没有执行过一次update。
    2.history = []      
           清空history列表。
           history列表保存的是单个目标框连续预测的多个结果([x,y,s,r]转换后的[x1,y1,x2,y2]),一旦执行update就会清空history列表。
    3.hits += 1:
            该目标框进行更新的总次数。每执行update一次,便hits+=1。
    4.hit_streak += 1
            1.连续更新的次数,每执行update一次即进行hit_streak+=1。
            2.在连续更新(连续执行update)的过程中,一旦开始连续执行predict两次或以上的情况下,
              当连续第一次执行predict时,因为time_since_update仍然为0,并不会把hit_streak重置为0,
              然后才会进行time_since_update+=1;
              当连续第二次执行predict时,因为time_since_update已经为1,那么便会把hit_streak重置为0,
              然后继续进行time_since_update+=1。
    5.kf.update(convert_bbox_to_z(bbox))
            convert_bbox_to_z负责将[x1,y1,x2,y2]形式的检测框转为滤波器的状态表示形式[x,y,s,r],那么传入的为kf.update([x,y,s,r])。
            然后根据观测结果修改内部状态x(状态更新向量x)。
            使用的是通过yoloV3得到的“并且和预测框相匹配的”检测框来更新卡尔曼滤波器得到的预测框。
    """
    #更新状态变量,使用观测到的目标框bbox更新状态变量
    def update(self, bbox):
        """
        使用观察到的目标框更新状态向量。filterpy.kalman.KalmanFilter.update 会根据观测修改内部状态估计self.kf.x。
        重置self.time_since_update,清空self.history。
        :param bbox:目标框
        :return:
        """
        # 重置
        self.time_since_update = 0
        # 清空history
        self.history = []
        # hits计数加1
        self.hits += 1
        self.hit_streak += 1
        # 根据观测结果修改内部状态x。
        #convert_bbox_to_z负责将[x1,y1,x2,y2]形式的检测框转为滤波器的状态表示形式[x,y,s,r],那么update传入的为(x,y,s,r)
        self.kf.update(convert_bbox_to_z(bbox))

    """
    predict:进行目标框的预测并返回预测的边界框结果
   
    1.if(kf.x[6] + kf.x[2]) <= 0:
            self.kf.x[6] *= 0.0
            状态更新向量x(状态变量x)为[u,v,s,r,u^,v^,s^]T,那么x[6]为s^,x[2]为s。
            如果x[6]+x[2]<= 0,那么x[6] *= 0.0,即把s^置为0.0。
    2.kf.predict()
            进行目标框的预测。
    3.age += 1
            该目标框进行预测的总次数。每执行predict一次,便age+=1。
    4.if time_since_update > 0:
        hit_streak = 0
      time_since_update += 1
            1.连续预测的次数,每执行predict一次即进行time_since_update+=1。
            2.在连续预测(连续执行predict)的过程中,一旦执行update的话,time_since_update就会被重置为0。
            3.在连续预测(连续执行predict)的过程中,只要连续预测的次数time_since_update大于0的话,
              就会把hit_streak(连续更新的次数)重置为0,表示连续预测的过程中没有出现过一次更新状态更新向量x(状态变量x)的操作,
              即连续预测的过程中没有执行过一次update。
            4.在连续更新(连续执行update)的过程中,一旦开始连续执行predict两次或以上的情况下,
              当连续第一次执行predict时,因为time_since_update仍然为0,并不会把hit_streak重置为0,
              然后才会进行time_since_update+=1;
              当连续第二次执行predict时,因为time_since_update已经为1,那么便会把hit_streak重置为0,
              然后继续进行time_since_update+=1。
    5.history.append(convert_x_to_bbox(kf.x))
            convert_x_to_bbox(kf.x):将目标框所预测的结果从中心面积的形式[x,y,s,r] 转换为 坐标的形式[x1,y1,x2,y2] 的bbox。
            history列表保存的是单个目标框连续预测的多个结果([x,y,s,r]转换后的[x1,y1,x2,y2]),一旦执行update就会清空history列表。
    6.predict 返回值:history[-1]
            把目标框当前该次的预测的结果([x,y,s,r]转换后的[x1,y1,x2,y2])进行返回输出。
    """
    #进行目标框的预测,推进状态变量并返回预测的边界框结果
    def predict(self):
        """
        推进状态向量并返回预测的边界框估计。
        将预测结果追加到self.history。由于 get_state 直接访问 self.kf.x,所以self.history没有用到
        :return:
        """
        # 推进状态变量
        if (self.kf.x[6] + self.kf.x[2]) <= 0:
            self.kf.x[6] *= 0.0
        # 进行预测
        self.kf.predict()
        # 卡尔曼滤波的次数
        self.age += 1
        # 若过程中未更新过,将hit_streak置为0
        if self.time_since_update > 0:
            self.hit_streak = 0
        self.time_since_update += 1
        # 将预测结果追加到history中
        self.history.append(convert_x_to_bbox(self.kf.x))
        return self.history[-1]

    """
    get_state():
        获取当前目标框预测的结果([x,y,s,r]转换后的[x1,y1,x2,y2])。
        return convert_x_to_bbox(kf.x):将候选框从中心面积的形式[x,y,s,r] 转换为 坐标的形式[x1,y1,x2,y2] 的bbox并进行返回输出。
        直接访问 kf.x并进行返回,所以history没有用到。
    """
    def get_state(self):
        """
        返回当前边界框估计值。
        由于 get_state 直接访问 self.kf.x,所以self.history没有用到。
        :return:
        """
        #将候选框从中心面积的形式[x,y,s,r] 转换为 坐标的形式[x1,y1,x2,y2] 的bbox
        return convert_x_to_bbox(self.kf.x)