目前为止,我们已经推出了《从零开始学习 深度学习》和《从零开始学习模型部署》系列教程,方便大家入门算法开发。
欢迎大家进抠抠裙:
deeplearningYYDS裙3:1015081610
威信裙需先jia个人威信:deeplearningYYDS 经审核后进入。
话不多说,进入实操:
一、首先说多目标跟踪
多目标跟踪处理的对象是视频,从视频的第一帧到最后一帧,里边有多个目标在不断运动。多目标跟踪的目的就是将每个目标和其他目标进行区分开来,具体方法是给每个目标分配一个 ID,并记录他们的轨迹。
刚开始接触,可能觉得直接将目标检测的算法应用在视频的每一帧就可以完成这个任务了。实际上,目标检测的效果是很不稳定的,其实最大的区别在于,仅仅使用目标检测无法给对象分配 ID,并且跟踪能够优化整个跟踪过程,能让目标框更加稳定。
多目标跟踪中一个比较经典的和深度学习结合比较好的方法就是 DetectionBased Tracking,对前后两帧进行目标检测检测,然后根据得到的前后两帧的所有目标进行匹配,从而维持 ID。初学者接触比较多的就是 SORT 和 Deep SORT。
二、MOT16 数据集
MOT16 数据集是在 2016 年提出来的用于衡量多目标跟踪检测和跟踪方法标准的数据集,专门用于行人跟踪。官网地址是:https://motchallenge.net/
从官网下载的数据是按照以下的文件结构进行组织的:
在 MOT16 数据集中,是包含了检测得到的框的,这样是可以免去目标检测这个部分,提供统一的目标检测框以后,然后可以比较目标跟踪更关注的部分,而不用在花费精力在目标检测上。
2、主要功能和特点
• 支持各种格式的视频(avi,mpg 等)和图像列表(jpg,bmp,png 等)
• 多框设置和标签设置支持
• 支持对象识别和图像跟踪中使用的各种数据格式
• 使用图像跟踪器自动标记(通过跟踪标记)
• 支持使用插值功能的间隔标签
• 自动标记功能,可按类别自动为每个对象分配唯一的 ID
3、主要用法
鼠标/键盘界面(Shift / Ctrl = Shift 或 Ctrl)
• 鼠标拖动:创建一个框
• Shift / Ctrl + 拖动:编辑框
• 双击:选择/取消相同 ID 对象的轨迹
• 右键单击:删除所有选定的对象轨迹(删除部分)
• 右键单击:删除最近创建的框(如果未选择任何轨迹)
• Shift / Ctrl + 右键单击(特定框):仅删除所选框
• Shift / Ctrl + 右键单击(空):删除当前屏幕上的所有框
• Shift / Ctrl + 双击(特定框):修改所选框的标签
• Shift / Ctrl + 双击(轨迹):在所选轨迹上批量更改标签
• 箭头键/ PgUp / PgDn / Home / End:移动视频帧(图像)
• Enter 键:使用图像跟踪功能自动生成框(通过跟踪进行标记)
指定标签和 ID
• 无标签:创建未标签的框
• 框标签:用户指定的标签(例如,人类)
• box 标签 + 自动编号:自动编号自定义标签(例如 human0,human1 等)
• 如果指定了 id,则可以选择/编辑轨迹单位对象
• popuplabeleditor:注册标签列表窗口的弹出窗口(已在 labels.txt 文件中注册)
• 如果在弹出窗口中按快捷键(1 9),则会自动输入标签。
• Label + id 显示在屏幕上,但在内部,标签和 ID 分开。
• 当另存为 gt 数据时,选择仅标签格式以保存可见标签(标签 + id) • 另存为 gt 数据时,如果选择了标签和 ID 分类格式,则标签和 ID 将分开保存。
追踪功能
这是这个软件比较好的功能之一,可以用传统方法(KCF 类似的算法)跟踪目标,只需要对不准确的目标进行人工调整即可,大大减少了工作量。
• 通过使用图像跟踪功能设置下一帧的框(分配相同的 ID /标签)
• 多达 100 个同时跟踪
• tracker1(稳健)算法:长时间跟踪目标
• tracker2(准确)算法:准确跟踪目标(例如汽车)
• 输入键/下一步和预测按钮
• 注意!使用跟踪时,下一帧上的原始框消失
插值功能
• 跟踪功能方便,但问题不准确
• 在视频部分按对象标记时使用
– 开始插补按钮:开始插补功能
– 在目标对象的轨迹的一半处绘制一个方框(航路点的种类)
– 航路点框为紫色,插值框为黑色。
– 更正插值错误的部分(Shift / Ctrl + 拖动),添加任意数量的航路点(不考虑顺序)/删除
– 结束插补按钮:将工作结束和工作轨迹注册为数据
导入视频/视频并在帧之间移动
• 打开视频文件:打开视频文件(avi,mpg,mp4,wmv,mov,…)
• 打开图像目录:打开文件夹中的所有图像(jpg,bmp,png 等)
• 在视频帧之间移动:键盘 →,←,PgUp,PgDn,Home,End,滑块控制
保存并调出作业数据
• 加载 GT:以所选格式加载地面真相文件。
• 保存 GT:以所选数据格式保存到目前为止已获得的结果。
• 导入数据时,需要选择与实际数据文件匹配的格式,但是在保存数据时,可以将其保存为所需
的任何格式。
• 在图像列表中工作时,使用帧号(frame #)格式,按文件名排序时的图像顺序将变为帧号(对
于诸如 00000.jpg,00002.jpg 等的列表很有用)
• 保存设置:保存当前选择的数据格式和选项(运行程序时自动还原)
数据格式(语法)
• |:换行
• []:重复短语
• frame #:帧号(视频的帧号,图像列表中的图像顺序)
• iname:图像文件名(仅在使用图像列表时有效)
• 标签:标签
• id:对象的唯一 ID
• n:在图像上设置的边界矩形的数量
• x,y:边界矩形的左侧和顶部位置
• w,h:边界矩形的宽度和高度
• cx,cy:边界矩形的中心坐标
• x1,y1,x2,y2:边界矩形的左上,右下位置
ffmpeg 切割视频
ffmpeg -i C:/plutopr.mp4 -acodec copy
-vf scale=1280:720
-ss 00:00:10 -t 15 C:/cutout1.mp4 -y
5. -ss time_off set the start time offset 设置从视频的哪个时间点开始截取,上文从视频的第 10s
开始截取
6. -to 截到视频的哪个时间点结束。上文到视频的第 15s 结束。截出的视频共 5s. 如果用-t 表示截
取多长的时间如上文-to 换位-t 则是截取从视频的第 10s 开始,截取 15s 时长的视频。即截出来
的视频共 15s.
7. -vcodec copy 表示使用跟原视频一样的视频编解码器。
8. -acodec copy 表示使用跟原视频一样的音频编解码器。
9. -i 表示源视频文件
10. -y 表示如果输出文件已存在则覆盖。
总结
这个软件是笔者自己进行项目的时候用到的一款标注软件,大部分视频标注软件要不就是太大(ViTBAT 软件),要不就是需要 Linux 环境,所以在 Window 上标注的话很不方便,经过了很长时间探
索,最终找到这款软件。此外,这款软件源码没有公开,开发者声明可以用于非商业目的。
DarkLabel 软件的获取可以在 GiantPandaCV 公众号后台回复“darklabel”,即可得到该软件的下载链
接。
四、DarkLabel 配套代码
先附上脚本地址:https://github.com/pprp/SimpleCVReproduction/tree/master/DarkLabel
先来了解一下为何 DarkLabel 能生成这么多格式的数据集,来看看 DarkLabel 的格式:
frame(从 0 开始计), 数量, id(从 0 开始), box(x1,y1,x2,y2), class=null
0,4,0,450,194,558,276,null,1,408,147,469,206,null,2,374,199,435,307,null,3,153,213,218,314,null
1,4,0,450,194,558,276,null,1,408,147,469,206,null,2,374,199,435,307,null,3,153,213,218,314,null
2,4,0,450,194,558,276,null,1,408,147,469,206,null,2,374,199,435,307,null,3,153,213,218,3
每一帧,每张图片上的目标都可以提取到,并且每个目标有 bbox、分配了一个 ID、class
这些信息都可以满足目标检测、ReID、跟踪数据集。
ps:说明一下,以下脚本都是笔者自己写的,专用于单类的检测、跟踪、重识别的代码,如果有需要多类
的,还需要自己修改多类部分的代码。另外以下只针对 Darklabel 中 frame#,n,[,id,x1,y1,x2,y2,label]
格式。
DarkLabel 转 Detection
这里笔者写了一个脚本转成 VOC2007 中的 xml 格式的标注,代码如下:
import cv2
import os
import shutil
import tqdm
import sys
root_path = r"I:\Dataset\VideoAnnotation"
def print_flush(str):
print(str, end='\r')
sys.stdout.flush()
def genXML(xml_dir, outname, bboxes, width, height):
xml_file = open((xml_dir + '/' + outname + '.xml'), 'w')
xml_file.write('\n')
xml_file.write(' VOC2007 \n')
xml_file.write(' ' + outname + '.jpg' + ' \n')
xml_file.write(' \n')
xml_file.write(' ' + str(width) + ' \n')
xml_file.write(' ' + str(height) + ' \n')
xml_file.write(' 3 \n')
xml_file.write(' \n')
for bbox in bboxes:
x1, y1, x2, y2 = bbox
xml_file.write(' \n')
xml_file.write(' ')
def gen_empty_xml(xml_dir, outname, width, height):
xml_file = open((xml_dir + '/' + outname + '.xml'), 'w')
xml_file.write('\n')
xml_file.write(' VOC2007 \n')
xml_file.write(' ' + outname + '.png' + ' \n')
xml_file.write(' \n')
xml_file.write(' ' + str(width) + ' \n')
xml_file.write(' ' + str(height) + ' \n')
xml_file.write(' 3 \n')
xml_file.write(' \n')
xml_file.write(' ')
def getJPG(src_video_file, tmp_video_frame_save_dir):
# gen jpg from video
cap = cv2.VideoCapture(src_video_file)
if not os.path.exists(tmp_video_frame_save_dir):
os.makedirs(tmp_video_frame_save_dir)
frame_cnt = 0
isrun, frame = cap.read()
width, height = frame.shape[1], frame.shape[0]
while (isrun):
save_name = append_name + "_" + str(frame_cnt) + ".jpg"
cv2.imwrite(os.path.join(tmp_video_frame_save_dir, save_name), frame)
frame_cnt += 1
print_flush("Extracting frame :%d" % frame_cnt)
isrun, frame = cap.read()
return width, height
def delTmpFrame(tmp_video_frame_save_dir):
if os.path.exists(tmp_video_frame_save_dir):
shutil.rmtree(tmp_video_frame_save_dir)
print('delete %s success!' % tmp_video_frame_save_dir)
def assign_jpgAndAnnot(src_annot_file, dst_annot_dir, dst_jpg_dir,
↪ tmp_video_frame_save_dir, width, height):
# get coords from annotations files
txt_file = open(src_annot_file, "r")
content = txt_file.readlines()
for line in content:
item = line[:-1]
items = item.split(',')
frame_id, num_of_cow = items[0], items[1]
print_flush("Assign jpg and annotion : %s" % frame_id)
bboxes = []
for i in range(int(num_of_cow)):
obj_id = items[1 + i * 6 + 1]
obj_x1, obj_y1 = int(items[1 + i * 6 + 2]), int(items[1 + i * 6 + ↪ 3])
obj_x2, obj_y2 = int(items[1 + i * 6 + 4]), int(items[1 + i * 6 + ↪ 5])
# preprocess the coords
obj_x1 = max(1, obj_x1)
obj_y1 = max(1, obj_y1)
obj_x2 = min(width, obj_x2)
obj_y2 = min(height, obj_y2)
bboxes.append([obj_x1, obj_y1, obj_x2, obj_y2])
genXML(dst_annot_dir, append_name + "_" + str(frame_id), bboxes,
↪ width,
height)
shutil.copy(
os.path.join(tmp_video_frame_save_dir,
append_name + "_" + str(frame_id) + ".jpg"),
os.path.join(dst_jpg_dir, append_name + "_" + str(frame_id) + ↪ ".jpg"))
txt_file.close()
if __name__ == "__main__":
append_names = ["cutout%d" % i for i in range(19, 66)]
for append_name in append_names:
print("processing",append_name)
src_video_file = os.path.join(root_path, append_name + ".mp4")
if not os.path.exists(src_video_file):
continue
src_annot_file = os.path.join(root_path, append_name + "_gt.txt")
dst_annot_dir = os.path.join(root_path, "Annotations")
dst_jpg_dir = os.path.join(root_path, "JPEGImages")
tmp_video_frame_save_dir = os.path.join(root_path, append_name)
width, height = getJPG(src_video_file, tmp_video_frame_save_dir)
assign_jpgAndAnnot(src_annot_file, dst_annot_dir, dst_jpg_dir,
↪ tmp_video_frame_save_dir, width, height)
delTmpFrame(tmp_video_frame_save_dir)
`
如果想转成 U 版 yolo 需要的格式可以点击 https://github.com/pprp/voc2007_for_yolo_torch 使用这
里的脚本。
DarkLabel 转 ReID 数据集
ReID 数据集其实与分类数据集很相似,最出名的是 Market1501 数据集,对这个数据集不熟悉的可以先百度一下。简单来说 ReID 数据集只比分类中多了 query, gallery 的概念,也很简单。转换代码如下:
import os
import shutil
import cv2
import numpy as np
import glob
import sys
import random
"""[summary]
根据视频和 darklabel 得到的标注文件
"""
def preprocessVideo(video_path):
'''
预处理,将视频变为一帧一帧的图片
'''
if not os.path.exists(video_frame_save_path):
os.mkdir(video_frame_save_path)
vidcap = cv2.VideoCapture(video_path)
(cap, frame) = vidcap.read()
height = frame.shape[0]
width = frame.shape[1]
cnt_frame = 0
while (cap):
cv2.imwrite(
os.path.join(video_frame_save_path, "frame_%d.jpg" % ↪ (cnt_frame)),
frame)
cnt_frame += 1
print(cnt_frame, end="\r")
sys.stdout.flush()
(cap, frame) = vidcap.read()
vidcap.release()
return width, height
def postprocess(video_frame_save_path):
'''
后处理,删除无用的文件夹
'''
if os.path.exists(video_frame_save_path):
shutil.rmtree(video_frame_save_path)
def extractVideoImgs(frame, video_frame_save_path, coords):
'''
抠图
'''
x1, y1, x2, y2 = coords
# get image from save path
img = cv2.imread(
os.path.join(video_frame_save_path, "frame_%d.jpg" % (frame)))
if img is None:
return None
# crop
save_img = img[y1:y2, x1:x2]
return save_img
def bbox_ious(box1, box2):
b1_x1, b1_y1, b1_x2, b1_y2 = box1[0], box1[1], box1[2], box1[3]
b2_x1, b2_y1, b2_x2, b2_y2 = box2[0], box2[1], box2[2], box2[3]
# Intersection area
inter_area = (min(b1_x2, b2_x2) - max(b1_x1, b2_x1)) * \
(min(b1_y2, b2_y2) - max(b1_y1, b2_y1))
# Union Area
w1, h1 = b1_x2 - b1_x1, b1_y2 - b1_y1
w2, h2 = b2_x2 - b2_x1, b2_y2 - b2_y1
union_area = (w1 * h1 + 1e-16) + w2 * h2 - inter_area
return inter_area / union_area
def bbox_iou(box1, box2):
# format box1: x1,y1,x2,y2
# format box2: a1,b1,a2,b2
x1, y1, x2, y2 = box1
a1, b1, a2, b2 = box2
i_left_top_x = max(a1, x1)
i_left_top_y = max(b1, y1)
i_bottom_right_x = min(a2, x2)
i_bottom_right_y = min(b2, y2)
intersection = (i_bottom_right_x - i_left_top_x) * (i_bottom_right_y -
i_left_top_y)
area_two_box = (x2 - x1) * (y2 - y1) + (a2 - a1) * (b2 - b1)
return intersection * 1.0 / (area_two_box - intersection)
def restrictCoords(width, height, x, y):
x = max(1, x)
y = max(1, y)
x = min(x, width)
y = min(y, height)
return x, y
if __name__ == "__main__":
total_cow_num = 0
root_dir = "./data/videoAndLabel"
reid_dst_path = "./data/reid"
done_dir = "./data/done"
txt_list = glob.glob(os.path.join(root_dir, "*.txt"))
video_list = glob.glob(os.path.join(root_dir, "*.mp4"))
for i in range(len(txt_list)):
txt_path = txt_list[i]
video_path = video_list[i]
print("processing:", video_path)
if not os.path.exists(txt_path):
continue
video_name = os.path.basename(video_path).split('.')[0]
video_frame_save_path = os.path.join(os.path.dirname(video_path),
video_name)
f_txt = open(txt_path, "r")
width, height = preprocessVideo(video_path)
print("done")
# video_cow_id = video_name + str(total_cow_num)
for line in f_txt.readlines():
bboxes = line.split(',')
ids = []
frame_id = int(bboxes[0])
box_list = []
if frame_id % 30 != 0:
continue
num_object = int(bboxes[1])
for num_obj in range(num_object):
# obj = 0, 1, 2
obj_id = bboxes[1 + (num_obj) * 6 + 1]
obj_x1 = int(bboxes[1 + (num_obj) * 6 + 2])
obj_y1 = int(bboxes[1 + (num_obj) * 6 + 3])
obj_x2 = int(bboxes[1 + (num_obj) * 6 + 4])
obj_y2 = int(bboxes[1 + (num_obj) * 6 + 5])
box_list.append([obj_x1, obj_y1, obj_x2, obj_y2])
# process coord
obj_x1, obj_y1 = restrictCoords(width, height, obj_x1, obj_y1)
obj_x2, obj_y2 = restrictCoords(width, height, obj_x2, obj_y2)
specific_object_name = video_name + "_" + obj_id
# mkdir for reid dataset
id_dir = os.path.join(reid_dst_path, specific_object_name)
if not os.path.exists(id_dir):
os.makedirs(id_dir)
# save pic
img = extractVideoImgs(frame_id, video_frame_save_path,
(obj_x1, obj_y1, obj_x2, obj_y2))
print(type(img))
if img is None or img.shape[0] == 0 or img.shape[1] == 0:
print(specific_object_name + " is empty")
continue
# print(frame_id)
img = cv2.resize(img, (256, 256))
normalizedImg = np.zeros((256, 256))
img = cv2.normalize(img, normalizedImg, 0, 255,
cv2.NORM_MINMAX)
cv2.imwrite(
os.path.join(id_dir, "%s_%d.jpg") %
(specific_object_name, frame_id), img)
max_w = width - 256
max_h = height - 256
# 随机选取左上角坐标
select_x = random.randint(1, max_w)
select_y = random.randint(1, max_h)
rand_box = [select_x, select_y, select_x + 256, select_y + 256] # 背景图保存位置
bg_dir = os.path.join(reid_dst_path, "bg")
if not os.path.exists(bg_dir):
os.makedirs(bg_dir)
iou_list = []
for idx in range(len(box_list)):
cow_box = box_list[idx]
iou = bbox_iou(cow_box, rand_box)
iou_list.append(iou)
# print("iou list:" , iou_list)
if np.array(iou_list).all() < 0:
img = extractVideoImgs(frame_id, video_frame_save_path,
rand_box)
if img is None:
print(specific_object_name + "is empty")
continue
normalizedImg = np.zeros((256, 256))
img = cv2.normalize(img, normalizedImg, 0, 255,
cv2.NORM_MINMAX)
cv2.imwrite(
os.path.join(bg_dir, "bg_%s_%d.jpg") %
(video_name, frame_id), img)
f_txt.close()
postprocess(video_frame_save_path)
shutil.move(video_path, done_dir)
shutil.move(txt_path, done_dir)
DarkLabel 转 MOT16 格式
其实 DarkLabel 标注得到信息和 MOT16 是几乎一致的,只不过需要转化一下,脚本如下:
import os
'''
gt.txt:
---------
frame(从 1 开始计), id, box(left top w, h),ignore=1(不忽略), class=1(从 1 开始),
↪ 覆盖 =1),
1,1,1363,569,103,241,1,1,0.86014
2,1,1362,568,103,241,1,1,0.86173
3,1,1362,568,103,241,1,1,0.86173
4,1,1362,568,103,241,1,1,0.86173
cutout24_gt.txt
---
frame(从 0 开始计), 数量, id(从 0 开始), box(x1,y1,x2,y2), class=null
0,4,0,450,194,558,276,null,1,408,147,469,206,null,2,374,199,435,307,null,3,153,213,218,314,null
1,4,0,450,194,558,276,null,1,408,147,469,206,null,2,374,199,435,307,null,3,153,213,218,314,null
2,4,0,450,194,558,276,null,1,408,147,469,206,null,2,374,199,435,307,null,3,153,213,218,314,null
def xyxy2xywh(x):
# Convert bounding box format from [x1, y1, x2, y2] to [x, y, w, h]
# y = torch.zeros_like(x) if isinstance(x,
# torch.Tensor) else np.zeros_like(x)
y = [0, 0, 0, 0]
y[0] = (x[0] + x[2]) / 2
y[1] = (x[1] + x[3]) / 2
y[2] = x[2] - x[0]
y[3] = x[3] - x[1]
return y
def process_darklabel(video_label_path, mot_label_path):
f = open(video_label_path, "r")
f_o = open(mot_label_path, "w")
contents = f.readlines()
for line in contents:
line = line[:-1]
num_list = [num for num in line.split(',')]
frame_id = int(num_list[0]) + 1
total_num = int(num_list[1])
base = 2
for i in range(total_num):
print(base, base + i * 6, base + i * 6 + 4)
_id = int(num_list[base + i * 6]) + 1
_box_x1 = int(num_list[base + i * 6 + 1])
_box_y1 = int(num_list[base + i * 6 + 2])
_box_x2 = int(num_list[base + i * 6 + 3])
_box_y2 = int(num_list[base + i * 6 + 4])
y = xyxy2xywh([_box_x1, _box_y1, _box_x2, _box_y2])
write_line = "%d,%d,%d,%d,%d,%d,1,1,1\n" % (frame_id, _id, y[0],
y[1], y[2], y[3])
f_o.write(write_line)
f.close()
f_o.close()
if __name__ == "__main__":
root_dir = "./data/videosample"
for item in os.listdir(root_dir):
full_path = os.path.join(root_dir, item)
video_path = os.path.join(full_path, item+".mp4")
video_label_path = os.path.join(full_path, item + "_gt.txt")
mot_label_path = os.path.join(full_path, "gt.txt")
process_darklabel(video_label_path, mot_label_path)
'''
以上就是 DarkLabel 转各种数据集格式的脚本了,DarkLabel 还是非常方便的,可以快速构建自己的数据集。通常两分钟的视频可以生成 2880 张之多的图片,但是在目标检测中并不推荐将所有的图片都作为训练集,因为前后帧之间差距太小了,几乎是一模一样的。这种数据会导致训练速度很慢、泛化能力变差。
有两种解决方案:
• 可以选择隔几帧选取一帧作为数据集,比如每隔 10 帧作为数据集。具体选择多少作为间隔还是具体问题具体分析,如果视频中变化目标变化较快,可以适当缩短间隔;如果视频中大部分都是静止对象,可以适当增大间隔。
• 还有一种更好的方案是:对原视频用 ffmpeg 提取关键帧,将关键帧的内容作为数据集。关键帧和关键帧之间的差距比较大,适合作为目标检测数据集。