刚刚算是完成一个完整的项目,记一篇絮絮叨叨的学习笔记
题目描述:多目标检测与跟踪任务,要求跟踪图片中的行人(走路的行人和骑车的行人),遮挡,背景变换等干扰,自动初始化、自动终止,目标的区分还有跟丢再识别等问题。按照规定的格式输出目标跟踪的结果。
采用的算法分为目标检测部分和目标跟踪部分,两部分是独立的。
使用python Keras框架搭建yolo v3结构
YOLOv3的整体思想是将一张图片划分为不同尺度的网格,判断每个网格中是否存在要识别的物体,物体落在哪个网格,就由所在网格的左上角的点的坐标对物体的框进行调整。
1.特征提取网络DarkNet53
2.解码(对先验框进行调整)
3.训练过程(loss值的计算)
4.预测结果
在DarkNet53网络中,在Keras程序中可以看成用卷积操作和残差结构获得不同尺度的特征。
首先在model.py
DarknetConv2D_BN_Leaky
函数中定义了一个卷积块,即Conv+BN+LeakyReLU。因为通常每一个卷积操作后面都要接上归一化和费线性激活函数的操作,为了方便书写代码,直接定义一个卷积块直接调用
卷积块代码:
def DarknetConv2D_BN_Leaky(*args, **kwargs):
"""Darknet Convolution2D followed by BatchNormalization and LeakyReLU."""
no_bias_kwargs = {'use_bias': False}
no_bias_kwargs.update(kwargs)
return compose(
DarknetConv2D(*args, **no_bias_kwargs),
BatchNormalization(),
LeakyReLU(alpha=0.1))
在model.py
resblock_body
中定义了残差块,zeropadding+步长为2的卷积块+残差结构,在残差结构中定义了一个循环,可以在调用时选择循环的次数来决定进行多少次残差操作。(在YOLOV3j结构中分别调用了1,2,8,8,4次。)
残差结构中包含两种卷积:1.卷积核大小为1×1,输出的通道数减半,作用是减少参数个数,增加网络的深度;2.卷积核大小为3×3,输出的通道数还原,作用是进行特征提取。
由于有一个步长是2的卷积块,每运行一次残差块的代码,输出特征层的大小就减半
残差块的代码:
def resblock_body(x, num_filters, num_blocks):
'''A series of resblocks starting with a downsampling Convolution2D'''
# Darknet uses left and top padding instead of 'same' mode
x = ZeroPadding2D(((1,0),(1,0)))(x)
x = DarknetConv2D_BN_Leaky(num_filters, (3,3), strides=(2,2))(x)
for i in range(num_blocks):
y = compose(
DarknetConv2D_BN_Leaky(num_filters//2, (1,1)),
DarknetConv2D_BN_Leaky(num_filters, (3,3)))(x)
# 残差边
x = Add()([x,y])
return x
在model.py
darknet_body
函数中,分别调用上面的卷积块和残差块,在残差块的调用过程中分别定义了输出的通道数和残差结构执行的次数。
代码如下:
def darknet_body(x):
'''Darknent body having 52 Convolution2D layers'''
x = DarknetConv2D_BN_Leaky(32, (3,3))(x)
x = resblock_body(x, 64, 1)# 208*208*64
x = resblock_body(x, 128, 2)# 104*104*128
x = resblock_body(x, 256, 8)# 52*52*256
x = resblock_body(x, 512, 8)# 26*26*512
x = resblock_body(x, 1024, 4)# 13*13*1024
return x
函数model.py
make_last_layers
中进行5次卷积得到结果保留,再进行两次卷积。
5次卷积是1×1卷积核3×3卷积交替进行,(同样1×1卷积减少通道数,目的是减少参数个数,减少计算量,增加网络深度;3×3卷积将通道数还原,目的是进行特征提取)。
量次卷积结果一个是分类预测,另一个是回归预测。
代码如下:
def make_last_layers(x, num_filters, out_filters):
'''6 Conv2D_BN_Leaky layers followed by a Conv2D_linear layer'''
x = compose(
DarknetConv2D_BN_Leaky(num_filters, (1, 1)),
DarknetConv2D_BN_Leaky(num_filters*2, (3, 3)),
DarknetConv2D_BN_Leaky(num_filters, (1, 1)),
DarknetConv2D_BN_Leaky(num_filters*2, (3, 3)),
DarknetConv2D_BN_Leaky(num_filters, (1, 1)))(x)
y = compose(
DarknetConv2D_BN_Leaky(num_filters*2, (3,3)),# 分类预测
DarknetConv2D(out_filters, (1,1)))(x)# 回归预测
return x, y
make_last_layers
函数的输出结果包含两部分:第一部分是经过5次卷积后的结果,第二部分是经过后量次卷积的结果(分类预测和回归预测的结果)
最后函数model.py
yolo_body
调用上面定义的函数,构建完整的特征提取网络(特征金字塔):调用make_last_layers
函数,前5次卷积,后2次卷积的通道数为num_anchors(num_classes+5)。
num_anchors默认值为3,即每个网络上画3个候选框;num_classes表示待识别物体的分类总数;5=1+4,表示框框中是否包含物体和框的调整参数。
因为之前函数darknet_body
已经生成了大小依次递减,通道数依次递增的特征层。
1→208×208×64
2→104×104×128
3→52×52×256
4→26×26×512
5→13×13×1024
第5个特征层调用make_last_layers
,将前5次卷积的到的结果的通道数转化成512→13×13×512,再进行通道数为256的1×1卷积→13×13×256,然后上采样→26×26×256,与上一个特征层进行叠加→26×26×768;后两次卷积得到特征层大小为13×13×num_anchors(num_classes+5);
第4个特征层调用make_last_layers
,将前5次卷积得到的结果的通道数转化为256→26×26×256,再进行通道数为128的1×1卷积→26×26×128,然后上采样→52×52×128,与上一个特征层进行叠加→52×52×384;后两次卷积得到特征层大小为26×26×num_anchors(num_classes+5);
第3个特征层调用make_last_layers
,将前5次卷积得到的结果的通道数转化为128→52×52×128,不再进行上采样,后两次卷积得到的特征层大小为52×52×num_anchors(num_classes+5)。
函数yolo_body
的代码如下:
def yolo_body(inputs, num_anchors, num_classes):
"""Create YOLO_V3 model CNN body in Keras."""
darknet = Model(inputs, darknet_body(inputs))
x, y1 = make_last_layers(darknet.output, 512, num_anchors*(num_classes+5))
x = compose(
DarknetConv2D_BN_Leaky(256, (1, 1)),
UpSampling2D(2))(x)
x = Concatenate()([x,darknet.layers[152].output])
x, y2 = make_last_layers(x, 256, num_anchors*(num_classes+5))
x = compose(
DarknetConv2D_BN_Leaky(128, (1, 1)),
UpSampling2D(2))(x)
x = Concatenate()([x,darknet.layers[92].output])
x, y3 = make_last_layers(x, 128, num_anchors*(num_classes+5))
return Model(inputs, [y1,y2,y3])
至此YOLOv3网络利用DarkNet53网络的特征提取部分完成
在特征提取阶段,得到了
52×52×256,
26×26×512,
13×13×1024的特征层,相当于把一张图片划分成不同大小的网格。52×52大小的特征层划分的网格密,网络层数浅,用来识别较小的物体;13×13大小的特征层划分的网格疏,网络的层数深,用来识别较大的物体。
用不同尺度的网格划分原图像,网格中是否存在物体由该网格左上角的点进行预测。将预测的结果转化成可以表示在图片上的框的过程称为解码。
在yolo.py
generate
函数中
1.首先在yolo.py
中加载已经训练好的模型;
2.加载由先验框生成的辅助预测框调整的不同尺度的框的文本文件yolo_anchors.txt
(先验框是指由打好标签的图片统计出的满足不同大小的待识别物体的框的平均值,本例中共有9个尺寸的先验框);
3.加载数据集中所有类的名称coco_classes.txt
,本例中使用的是COCO数据集。
不同类别的物体框有不同的颜色。
函数generate
代码如下:
def generate(self):
model_path = os.path.expanduser(self.model_path)
assert model_path.endswith('.h5'), 'Keras model or weights must be a .h5 file.'
# Load model, or construct model and load weights.
num_anchors = len(self.anchors)
num_classes = len(self.class_names)
is_tiny_version = num_anchors==6 # default setting
try:
self.yolo_model = load_model(model_path, compile=False)
except:
self.yolo_model = tiny_yolo_body(Input(shape=(None,None,3)), num_anchors//2, num_classes) \
if is_tiny_version else yolo_body(Input(shape=(None,None,3)), num_anchors//3, num_classes)
self.yolo_model.load_weights(self.model_path) # make sure model, anchors and classes match
else:
assert self.yolo_model.layers[-1].output_shape[-1] == \
num_anchors/len(self.yolo_model.output) * (num_classes + 5), \
'Mismatch between model and given anchor and class sizes'
print('{} model, anchors, and classes loaded.'.format(model_path))
# Generate colors for drawing bounding boxes.
hsv_tuples = [(x / len(self.class_names), 1., 1.)
for x in range(len(self.class_names))]
self.colors = list(map(lambda x: colorsys.hsv_to_rgb(*x), hsv_tuples))
self.colors = list(
map(lambda x: (int(x[0] * 255), int(x[1] * 255), int(x[2] * 255)),
self.colors))
np.random.seed(10101) # Fixed seed for consistent colors across runs.
np.random.shuffle(self.colors) # Shuffle colors to decorrelate adjacent classes.
np.random.seed(None) # Reset seed to default.
# Generate output tensor targets for filtered bounding boxes.
self.input_image_shape = K.placeholder(shape=(2, ))
if self.gpu_num>=2:
self.yolo_model = multi_gpu_model(self.yolo_model, gpus=self.gpu_num)
boxes, scores, classes = yolo_eval(self.yolo_model.output, self.anchors,
len(self.class_names), self.input_image_shape,
score_threshold=self.score, iou_threshold=self.iou)
return boxes, scores, classes
下面用到了四个函数:yolo_head
、yolo_correct_boxes
、yolo_boxes_and_scores
、yolo_eval
1、函数yolo_head
实现的就是利用特征层将原图像划分为不同的网格(对特征提取部分得到的三个尺度的特征层分别进行处理)。
计算预测框中心点相对于网格顶点的偏移量box_xy
,用的是sigmoid函数,sigmoid函数将结果归一化到0-1之间,根据物体所在网格的左上角顶点对物体预测框的中心点进行预测,将结果归一化到0-1之间,所以调整的范围是在一个网格之内不会超出网格范围。
预测框的宽高用box_wh
表示,取e的指数。
box_confidence
表示预测框的置信度,可以设定参数根据预测矿的置信度进行筛选。
box_class_probs
表示框中物体所属类别的置信度,对于任意一个框中的物体,对应每一个类别都有一个置信度,置信度最高的类别就是物体所属的类别。
函数yolo_head
的代码如下:
def yolo_head(feats, anchors, num_classes, input_shape, calc_loss=False):
"""Convert final layer features to bounding box parameters."""
num_anchors = len(anchors)
# Reshape to batch, height, width, num_anchors, box_params.
anchors_tensor = K.reshape(K.constant(anchors), [1, 1, 1, num_anchors, 2])
grid_shape = K.shape(feats)[1:3] # height, width
grid_y = K.tile(K.reshape(K.arange(0, stop=grid_shape[0]), [-1, 1, 1, 1]),
[1, grid_shape[1], 1, 1])
grid_x = K.tile(K.reshape(K.arange(0, stop=grid_shape[1]), [1, -1, 1, 1]),
[grid_shape[0], 1, 1, 1])
grid = K.concatenate([grid_x, grid_y])
grid = K.cast(grid, K.dtype(feats))
feats = K.reshape(
feats, [-1, grid_shape[0], grid_shape[1], num_anchors, num_classes + 5])
# Adjust preditions to each spatial grid point and anchor size.
box_xy = (K.sigmoid(feats[..., :2]) + grid) / K.cast(grid_shape[::-1], K.dtype(feats))
box_wh = K.exp(feats[..., 2:4]) * anchors_tensor / K.cast(input_shape[::-1], K.dtype(feats))
box_confidence = K.sigmoid(feats[..., 4:5])
box_class_probs = K.sigmoid(feats[..., 5:])
if calc_loss == True:
return grid, feats, box_xy, box_wh
return box_xy, box_wh, box_confidence, box_class_probs
函数返回值是物体的预测框的中心点坐标相对于网格的交点横纵坐标的偏移量、预测框的宽和高、框的置信度和物体所属类别的置信度。
2、函数yolo_correct_boxes
将x,y/w,h转换,并将box_yx
转化成预测框左上角和右下角在原图像上的坐标,这一步是解码的关键步骤。
函数yolo_correct_boxes
代码如下:
def yolo_correct_boxes(box_xy, box_wh, input_shape, image_shape):
'''Get corrected boxes'''
# 获得调整以后的框
box_yx = box_xy[..., ::-1]
box_hw = box_wh[..., ::-1]
input_shape = K.cast(input_shape, K.dtype(box_yx))
image_shape = K.cast(image_shape, K.dtype(box_yx))
new_shape = K.round(image_shape * K.min(input_shape/image_shape))
offset = (input_shape-new_shape)/2./input_shape
scale = input_shape/new_shape
box_yx = (box_yx - offset) * scale
box_hw *= scale
box_mins = box_yx - (box_hw / 2.)
box_maxes = box_yx + (box_hw / 2.)
boxes = K.concatenate([
box_mins[..., 0:1], # y_min
box_mins[..., 1:2], # x_min
box_maxes[..., 0:1], # y_max
box_maxes[..., 1:2] # x_max
])
# Scale boxes back to original image shape.
boxes *= K.concatenate([image_shape, image_shape])
return boxes
函数yolo_correct_boxes
的返回值是物体的预测框左上角和右下角坐标在原图像上的坐标。
最后利用函数yolo_boxes_and_scores
调用函数yolo_head
和函数yolo_correct_boxe
def yolo_boxes_and_scores(feats, anchors, num_classes, input_shape, image_shape):
'''Process Conv layer output'''
box_xy, box_wh, box_confidence, box_class_probs = yolo_head(feats,
anchors, num_classes, input_shape)
boxes = yolo_correct_boxes(box_xy, box_wh, input_shape, image_shape)
boxes = K.reshape(boxes, [-1, 4])
# 框的得分=框的置信度*框中物体所属类别的置信度
box_scores = box_confidence * box_class_probs
box_scores = K.reshape(box_scores, [-1, num_classes])
return boxes, box_scores
3、在model.py
yolo_eval
函数中的作用是将预测结果处理成能够直接表示在图片上的框。
yolo_eval
函数首先计算传入的特征层的数量(这里为3),3个shape的特征层分别传入;
anchor_mask表示先验框,不同大小的特征层和yolo_anchors.txt中的先验框进行对应;
传入输入图片的shape,(416×416×3);
boxes用来存放预测框;
box_scores用来存放预测框的置信度。
yolo_eval
函数的代码如下:
def yolo_eval(yolo_outputs,
anchors,
num_classes,
image_shape,
max_boxes=20,
score_threshold=.6,
iou_threshold=.8):
"""Evaluate YOLO model on given input and return filtered boxes."""
# 计算有多少个特征层52×52×256,26×26×512,13×13×1024
num_layers = len(yolo_outputs)
anchor_mask = [[6, 7, 8], [3, 4, 5], [0, 1, 2]] if num_layers==3 else [[3,4,5], [1,2,3]] # default setting
# 输入图片的shape,416×416
input_shape = K.shape(yolo_outputs[0])[1:3] * 32
boxes = []
box_scores = []
for l in range(num_layers):
_boxes, _box_scores = yolo_boxes_and_scores(yolo_outputs[l],
anchors[anchor_mask[l]], num_classes, input_shape, image_shape)
boxes.append(_boxes)
box_scores.append(_box_scores)
boxes = K.concatenate(boxes, axis=0)
box_scores = K.concatenate(box_scores, axis=0)
mask = box_scores >= score_threshold
max_boxes_tensor = K.constant(max_boxes, dtype='int32')
boxes_ = []
scores_ = []
classes_ = []
for c in range(num_classes):
# TODO: use keras backend instead of tf.
class_boxes = tf.boolean_mask(boxes, mask[:, c])
class_box_scores = tf.boolean_mask(box_scores[:, c], mask[:, c])
nms_index = tf.image.non_max_suppression(
class_boxes, class_box_scores, max_boxes_tensor, iou_threshold=iou_threshold)
class_boxes = K.gather(class_boxes, nms_index)
class_box_scores = K.gather(class_box_scores, nms_index)
classes = K.ones_like(class_box_scores, 'int32') * c
boxes_.append(class_boxes)
scores_.append(class_box_scores)
classes_.append(classes)
boxes_ = K.concatenate(boxes_, axis=0)
scores_ = K.concatenate(scores_, axis=0)
classes_ = K.concatenate(classes_, axis=0)
return boxes_, scores_, classes_
在for循环中遍历每一个特征层,调用函数yolo_boxes_and_scores
计算出框的左上角和右下角坐标,宽高和得分,根据根据得分对预测框进行筛选。
max_boxes
表示一张图中最多识别20个框。box_scores
表示某一个框中物体匹配每一个类别的置信度。
IOU参数表示对框的重合度进行限制,如果两的框的重构和成都高于设置的阈值,那么就要进行非极大值抑制。
训练的过程可以看做是一个计算loss的过程,需要用到预测值和真实值。接下来的任务就是将真实框和预测框转化成相同的格式以便计算loss
mdoel.py
preprocess_true_boxes
函数的作用是得到真实框在相对于原图像的信息(利用已经打好标签的原图像得到的.xml文件得到真实框的表示,即真实值)
函数preprocess_true_boxes
用到preprocess_true_boxes(true_boxes, input_shape, anchors, num_classes) 中真实框的位置和原始图像的shape,根据这两个参数计算出真实框相对于原图像的位置。
anchor_mask
表示先验框,共有9个,shape为(n, 9, 1)分别属于三个尺度的特征层(将真实框和先验框进行匹配,真实框和哪一个先验框匹配度最高,那么就按照该先验框所属的特征层对原图像划分网格,判断真实框中的物体中心点属于哪个网格),而且只有这一个网格点的y_true是有值的(x_offset, y_offset, h, w, 置信度, 分类结果才是有值的)。
然后进行一系列的格式转换,因为真实框是由左上角和右下角点的坐标表示的,这里
boxes_xy
表示计算出的真实框的中心点坐标。
boxes_wh
表示真实框的宽高,归一化到0-1之间,和预测框的原理相同,真实框中的物体能够由物体中心点所在网格的左上角点的坐标来确定。
函数preprocess_true_boxes
返回的结果是真实框相对于原图像的中心点坐标,宽高,框的置信度和物体的类别。
终于得到了真实框和预测框在原图像坐标下的中心点坐标,宽高,框的置信度和物体所属类别,而且每一个框都属于某一个尺度的特征层。
下面开始计算loss:
函数yolo_loss
的作用是将转化为相对于原图像的真实框y_true和预测框yolo_output进行比较计算出loss,真实框和预测框都有三个特征层,以13×13为例,shape为13×13×3×(num_classes+5)。
函数yolo_loss
首先进行一系列的预处理,处理先验框、原始图像、先验框在原始图像上划分网格。计算一共有多少张图片。
预处理结束后,进入正题:
对于真实框,只有有物体的网格对应的object_mask
值才是1;取出真实值中的物体所属类别。
这里又用到yolo_head函数,前面讲过yolo_head函数的作用是对预测框的信息进行解码(也就是获得框相对于原始图像的坐标表示)
ignor_mask
是对负样本进行筛选,(为什么要筛选负样本,因为对于某一特征层的网格,没有值的网格要比有值的网格要多,如果把所有有值的网格当做正样本,所有没有值的网格都当做负样本,那么将造成严重的数据不平衡问题,所以要对这么多的负样本进行筛选。)
这段函数就是筛选负样本的具体操作:
(对于每一个特征层中,计算预测框和真实框的重合程度,当重合程度的最大值仍小于某一阈值,才判定为该网格点为负样本)
def loop_body(b, ignore_mask):
true_box = tf.boolean_mask(y_true[l][b, ..., 0:4], object_mask_bool[b, ..., 0])
iou = box_iou(pred_box[b], true_box)
best_iou = K.max(iou, axis=-1)
ignore_mask = ignore_mask.write(b, K.cast(best_iou<ignore_thresh, K.dtype(true_box)))
return b+1, ignore_mask
然后对真实值进行编码,得到和yolo_body相同的输出格式,计算loss值。
最后计算四部分的loss: xy_loss
,wh_loss
, confidence_loss
, class_loss
confidence_loss
表示框中存在物体的置信度,如果真实框中有物体,预测值为框中没有物体loss值会很大;(负样本的情况)当真实框目中没有物体,而预测值为框中有物体,loss值也会很大,这种情况最后还乘了一个ignore_mask,用来筛选负样本的数量。
class_loss
表示框中物体所属的类别。
最后计算loss值就是将这四个loss值相加,作为函数的返回结果。
yolo_matt.py
是预测结果的代码,
"model_path"
为模型文件.h5
"anchors_path"
为先验框的.txt文件
"classes_path"
为存放物体所属类别的.txt文件
函数generate
运用这些实现提供的文件生成预测结果,即预测框、框的置信度和物体所属类别。
同时还定义了框的颜色、
hsv_tuples = [(x / len(self.class_names), 1., 1.)
for x in range(len(self.class_names))]
self.colors = list(map(lambda x: colorsys.hsv_to_rgb(*x), hsv_tuples))
self.colors = list(
map(lambda x: (int(x[0] * 255), int(x[1] * 255), int(x[2] * 255)),
self.colors))
np.random.seed(10101) # Fixed seed for consistent colors across runs.
np.random.shuffle(self.colors) # Shuffle colors to decorrelate adjacent classes.
np.random.seed(None)
显示在图片上的字体Font
font = ImageFont.truetype(font='font/FiraMono-Medium.otf',
size=np.floor(3e-2 * image.size[1] + 0.5).astype('int32'))
遍历每一个预测框,boxes中存放的是每个框的信息(左上角和右下角点的坐标),
top, left, bottom, right = box
根据box确定左上角和右下角的点的坐标,在图片中把框画出来。
在物体的框上再画出一个小框,标注出物体所属类别和置信度。
函数返回的是处理后的图片、boxes、物体所属类别和框的置信度。
最后在调用的程序yolov3_object_tracking_3.py
首先在函数calc_center
中,利用返回的目标检测结果进行处理作为最后输出的结果。
毕竟最后是要以这种格式输出的。
def calc_center(out_boxes, out_classes, out_scores, score_limit=0.5):
outboxes_filter = []
# 把目标检测的结果进行堆叠
for x, y, z in zip(out_boxes, out_classes, out_scores):
# 只有置信度大于0.5的框被保留
if z > score_limit:
if y == 0:
outboxes_filter.append(x)
centers = []
r_bs = []
number = len(outboxes_filter)
for box in out_boxes:
# for box in outboxes_filter:
top, left, bottom, right = box
center = np.array([[left], [top]])
r_b = np.array([[right], [bottom]])
centers.append(center)
r_bs.append(r_b)
return centers, r_bs, number
因为原始程序中返回的中心点的坐标,而本次任务中要返回的是左上角和右下角点的坐标。这里用的是源程序中的命名,center原本表示中心点的坐标这里我改成了框的左上角的坐标,添加了一个右下角的坐标,用r_b来表示。
完成了目标检测任务那么跟踪呢?
卡尔曼滤波没有做系统的整理,参考博客:
https://blog.csdn.net/j879159541/article/details/89202728
本次任务中,目标检测和目标跟踪是分开进行。
用卡尔曼滤波进行目标跟踪任务。
这里没有推导公式 ,单从代码的角度,个人理解:
KalmanFilterTracker.py
代码
卡尔曼滤波分为两部分:状态、噪声的预测值&对预测的状态、噪声进行修正
图片来自博客:https://blog.csdn.net/j879159541/article/details/89202728
函数predict
用来计算状态的预测值和不确定量(噪声)的预测值。
F
表示状态转移矩阵,Q
表示噪声的控制量,
P
表示噪声的协方差,u
表示状态的向量
函数的返回值是状态的预测值u
函数correct
用来对得到的预测值进行修正。
u
表示状态的预测值
K
表示滤波增益
P
表示噪声分布
函数返回值是调整以后的状态向量u
根据目标检测的结果,用卡尔曼滤波的方法进行目标跟踪任务。
1.对第一张图片进行目标检测,在下一帧图片中如果没有轨迹向量,则创建新的轨迹。
2.计算预测结果和测量值(上一张图片中的目标)质心的距离平方,作为Sum值
3.使用匈牙利算法将测量值和预测的轨迹匹配。
4.识别没有被匹配的轨迹 ,如果一个轨迹很差那个一段时间没有被匹配,则将该轨迹删除。
5.查找没有被匹配的检测
6.跟踪卡尔曼滤波结果最后的轨迹跟踪。
代码链接:https://pan.baidu.com/s/1mToeJwZuq2GQYR8jLQk5Kg
提取码:4d0k
一开始兴致勃勃,后来焦头烂额,埋头苦学YOLO V3的时候,YOLO v4已经到来。
小白深感学的速度赶不上更新的速度啊
下一篇把项目的细节部分再整理一下
一篇博客活活写了三天
感谢:https://blog.csdn.net/weixin_44791964/article/details/103276106?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522158951700519725247640432%2522%252C%2522scm%2522%253A%252220140713.130102334.pc%255Fblog.%2522%257D&request_id=158951700519725247640432&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2blogfirst_rank_v2~rank_v25-2-103276106.nonecase&utm_term=YOLOV3
https://blog.csdn.net/j879159541/article/details/89202728