diff --git a/.gitignore b/.gitignore index dd84e0d..2f57332 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,10 @@ wandb/* track_result.txt .idea/ tracker/results/* +*.mp4 +*.mkv +temp.py +demo_result/ # Byte-compiled / optimized / DLL files __pycache__/ diff --git a/README.md b/README.md index 80d8466..dc1f791 100644 --- a/README.md +++ b/README.md @@ -1,43 +1,26 @@ # YOLO v7 + 各种tracker实现多目标跟踪 -## 0. 更新记录 +**** -**2023.5.6[大更新]**: 对于v5, v7, 改变前处理和后处理方式(采用原有方式), ***解决了部分边界框近大远小的bug, 边界框更加精确***. 此外, 对于v8, 弃用了resize步骤, 直接推理. - -**2023.3.14**解决了`DeepSORT`和`C_BIoUTracker`后面出现的目标不跟踪的bug. - -**2023.2.28**优化了`track_demo.py`, 减少了内存占用. - -**2023.2.24**加入了**推理单个视频或图片文件夹**以及**YOLO v8**的推理功能, 对应的代码为`tracker/track_demo.py`与`tracker/track_yolov8.py`. 推理单个视频或图片文件夹不需要指定数据集与真值, 也没有评测指标的功能, 只需要在命令行中指定`obj`即可, 例如: - -```shell -python tracker/track_demo.py --obj demo.mp4 -``` -YOLO v8 代码的参数与之前完全相同. 安装YOLO v8以及训练步骤请参照[YOLO v8](https://github.com/ultralytics/ultralytics) +本人在写这个代码的时候, 没想到会有这么多人看到. 然而, 必须承认我这份代码是以尽量整合为目的, 加了我自己的理解, 所以有的部分也许和原论文有出入, 导致效果不一定是最好的. +为此, 给大家推荐一个成熟的repo: (https://github.com/mikel-brostrom/yolo_tracking)[https://github.com/mikel-brostrom/yolo_tracking] -**2023.2.11**修复了TrackEval路径报错的问题, 详见[issue35](https://github.com/JackWoo0831/Yolov7-tracker/issues/35) - -**2023.2.10**修改了[DeepSORT](https://github.com/JackWoo0831/Yolov7-tracker/blob/master/tracker/deepsort.py)的代码与相关部分代码, 遵循了DeepSORT原论文**级联匹配和余弦距离计算**的原则, 并且解决了原有DeepSORT代码出现莫名漂移跟踪框的问题. - -**2023.1.15**加入了**MOT17数据集**的训练与测试功能, 增加了MOT17转yolo格式的代码(`./tools/convert_MOT17_to_yolo.py`), 转换的过程中强制使得坐标合法, 且忽略了遮挡率>=0.75的目标. 您可以采用此代码转换并训练, 具体请见后面的说明. 在使用tracker的时候, 注意将`data_format`设置为yolo, 这样可以直接根据txt文件的路径读取图片. +我这个代码大家可以作为学习之用, 也就是熟悉MOT的流程. 如果追求更好的效果, 我建议采纳更成熟的那些. -**2023.1.14**加入了当前DanceTrack的SOTA[C_BIoUTracker](https://arxiv.org/pdf/2211.14317v2.pdf), 该论文提出了一种增广的IoU来避免目标的瞬间大范围移动, 且弃用了Kalman滤波. 该代码没有开源, 我是按照自己的理解进行了复现. **有错误非常欢迎指出**. +我会不断听取大家的问题和建议, 希望和大家一起学习! -**2022.11.26**加入了[TrackEval](https://github.com/JonathonLuiten/TrackEval)评测的方式, 支持MOT, VisDrone和UAVDT三种数据集. 此外将一些路径变量选择了按照`yaml`的方式读取, 尽量让代码可读性高一些. 如果您不想用TrackEval进行评测, 则可以将`track.py`或`track_yolov5.py`的命令配置代码`parser.add_argument('--track_eval', type=bool, default=True, help='Use TrackEval to evaluate')`改为`False`. +**** -**2022.11.10**更新了如何设置数据集路径的说明, 请参见README的`track.py路径读取说明`部分. +## 0. 近期更新 -**2022.11.09**修复了BoT-SORT中的一处错误[issue 16](https://github.com/JackWoo0831/Yolov7-tracker/issues/16), 加粗了边界框与字体. +**2023.10.24**: 对于botsort(`tracker/botsort.py line 358`)和bytetrack(`tracker/bytetrack.py line 89`), 在低置信度检测列表初始化, 改正原有错误 (原本写错了, 写的用`det_high`初始化). -**2022.11.08**更新了track.py, track_yolov5.py, basetrack.py和tracker_dataloader.py, 修复了yolo格式读取数据以及保存视频功能的一些bug, 并增加了隔帧检测的功能(大多数时候用不到). +**2023.9.16**: 对于`track_demo.py`, 由于`yolov7`的`non_maximum_supress`函数中已经对类别做了筛选, 因此删去了主程序中类别计算的部分, 并修复了一些小bug -**2022.10.22**本代码的匹配代码比较简单, 不一定会达到最好的效果(每次匹配只用一次linear assignment, 没有和历史帧的特征相匹配), 您可以使用cascade matching的方式(参见[StrongSORT](https://github.com/dyhBUPT/StrongSORT/blob/master/deep_sort/tracker.py)的line94-134) +**2023.5.6[大更新]**: 对于v5, v7, 改变前处理和后处理方式(采用原有方式), ***解决了部分边界框近大远小的bug, 边界框更加精确***. 此外, 对于v8, 弃用了resize步骤, 直接推理. -**2022.10.15**增加了对yolo v5的支持, 只需替换track.py, 将tracker文件夹放到v5的根目录(我测试的是官方的[repo](https://github.com/ultralytics/yolov5))下即可. 代码在[yolo v5](https://github.com/JackWoo0831/Yolov7-tracker/blob/master/tracker/track_yolov5.py). -**2022.09.27[大更新]**修复了STrack类中update不更新外观的问题, 代码有较大更改, **您可能需要重新下载```./tracker```文件夹**. -尝试加入StrongSORT, 但是目前还不work:(, 尽力调一调 ## 1. 亮点 1. 统一代码风格, 对多种tracker重新整理, 详细注释, 方便阅读, 适合初学者 @@ -249,3 +232,42 @@ python tracker/track_demo.py --obj demo.mp4 > 注意: 推理的时候batch_size要求为1. ## 更多运行命令参考 run_yolov7.txt文件 + + +## 10. 更新记录 + + +**2023.3.14**解决了`DeepSORT`和`C_BIoUTracker`后面出现的目标不跟踪的bug. + +**2023.2.28**优化了`track_demo.py`, 减少了内存占用. + +**2023.2.24**加入了**推理单个视频或图片文件夹**以及**YOLO v8**的推理功能, 对应的代码为`tracker/track_demo.py`与`tracker/track_yolov8.py`. 推理单个视频或图片文件夹不需要指定数据集与真值, 也没有评测指标的功能, 只需要在命令行中指定`obj`即可, 例如: + +```shell +python tracker/track_demo.py --obj demo.mp4 +``` +YOLO v8 代码的参数与之前完全相同. 安装YOLO v8以及训练步骤请参照[YOLO v8](https://github.com/ultralytics/ultralytics) + + +**2023.2.11**修复了TrackEval路径报错的问题, 详见[issue35](https://github.com/JackWoo0831/Yolov7-tracker/issues/35) + +**2023.2.10**修改了[DeepSORT](https://github.com/JackWoo0831/Yolov7-tracker/blob/master/tracker/deepsort.py)的代码与相关部分代码, 遵循了DeepSORT原论文**级联匹配和余弦距离计算**的原则, 并且解决了原有DeepSORT代码出现莫名漂移跟踪框的问题. + +**2023.1.15**加入了**MOT17数据集**的训练与测试功能, 增加了MOT17转yolo格式的代码(`./tools/convert_MOT17_to_yolo.py`), 转换的过程中强制使得坐标合法, 且忽略了遮挡率>=0.75的目标. 您可以采用此代码转换并训练, 具体请见后面的说明. 在使用tracker的时候, 注意将`data_format`设置为yolo, 这样可以直接根据txt文件的路径读取图片. + +**2023.1.14**加入了当前DanceTrack的SOTA[C_BIoUTracker](https://arxiv.org/pdf/2211.14317v2.pdf), 该论文提出了一种增广的IoU来避免目标的瞬间大范围移动, 且弃用了Kalman滤波. 该代码没有开源, 我是按照自己的理解进行了复现. **有错误非常欢迎指出**. + +**2022.11.26**加入了[TrackEval](https://github.com/JonathonLuiten/TrackEval)评测的方式, 支持MOT, VisDrone和UAVDT三种数据集. 此外将一些路径变量选择了按照`yaml`的方式读取, 尽量让代码可读性高一些. 如果您不想用TrackEval进行评测, 则可以将`track.py`或`track_yolov5.py`的命令配置代码`parser.add_argument('--track_eval', type=bool, default=True, help='Use TrackEval to evaluate')`改为`False`. + +**2022.11.10**更新了如何设置数据集路径的说明, 请参见README的`track.py路径读取说明`部分. + +**2022.11.09**修复了BoT-SORT中的一处错误[issue 16](https://github.com/JackWoo0831/Yolov7-tracker/issues/16), 加粗了边界框与字体. + +**2022.11.08**更新了track.py, track_yolov5.py, basetrack.py和tracker_dataloader.py, 修复了yolo格式读取数据以及保存视频功能的一些bug, 并增加了隔帧检测的功能(大多数时候用不到). + +**2022.10.22**本代码的匹配代码比较简单, 不一定会达到最好的效果(每次匹配只用一次linear assignment, 没有和历史帧的特征相匹配), 您可以使用cascade matching的方式(参见[StrongSORT](https://github.com/dyhBUPT/StrongSORT/blob/master/deep_sort/tracker.py)的line94-134) + +**2022.10.15**增加了对yolo v5的支持, 只需替换track.py, 将tracker文件夹放到v5的根目录(我测试的是官方的[repo](https://github.com/ultralytics/yolov5))下即可. 代码在[yolo v5](https://github.com/JackWoo0831/Yolov7-tracker/blob/master/tracker/track_yolov5.py). + +**2022.09.27[大更新]**修复了STrack类中update不更新外观的问题, 代码有较大更改, **您可能需要重新下载```./tracker```文件夹**. +尝试加入StrongSORT, 但是目前还不work:(, 尽力调一调 \ No newline at end of file diff --git a/tracker/botsort.py b/tracker/botsort.py index 66799e3..c3cc65c 100644 --- a/tracker/botsort.py +++ b/tracker/botsort.py @@ -270,7 +270,7 @@ def multi_gmc(stracks, H=np.eye(2, 3)): class BoTSORT(BaseTracker): - def __init__(self, opts, frame_rate=30, gamma=0.02, use_GMC=False, *args, **kwargs) -> None: + def __init__(self, opts, frame_rate=30, gamma=0.02, use_GMC=True, *args, **kwargs) -> None: super().__init__(opts, frame_rate, *args, **kwargs) self.use_apperance_model = False @@ -318,7 +318,7 @@ def update(self, det_results, ori_img): ori_img: original image, np.ndarray, shape(H, W, C) """ if isinstance(det_results, torch.Tensor): - det_results = det_results.cpu().numpy() + det_results = det_results.detach().cpu().numpy() if isinstance(ori_img, torch.Tensor): ori_img = ori_img.numpy() @@ -355,7 +355,7 @@ def update(self, det_results, ori_img): if det_low.shape[0] > 0: D_low = [STrack(cls, STrack.tlbr2tlwh(tlbr), score, kalman_format=self.opts.kalman_format) - for (cls, tlbr, score) in zip(det_high[:, -1], det_high[:, :4], det_high[:, 4])] + for (cls, tlbr, score) in zip(det_low[:, -1], det_low[:, :4], det_low[:, 4])] else: D_low = [] diff --git a/tracker/bytetrack.py b/tracker/bytetrack.py index 61eeee9..19cf74b 100644 --- a/tracker/bytetrack.py +++ b/tracker/bytetrack.py @@ -86,7 +86,7 @@ def update(self, det_results, ori_img): if det_low.shape[0] > 0: D_low = [STrack(cls, STrack.tlbr2tlwh(tlbr), score, kalman_format=self.opts.kalman_format) - for (cls, tlbr, score) in zip(det_high[:, -1], det_high[:, :4], det_high[:, 4])] + for (cls, tlbr, score) in zip(det_low[:, -1], det_low[:, :4], det_low[:, 4])] else: D_low = [] diff --git a/tracker/config_files/uavdt.yaml b/tracker/config_files/uavdt.yaml index d17248c..af4906b 100644 --- a/tracker/config_files/uavdt.yaml +++ b/tracker/config_files/uavdt.yaml @@ -8,7 +8,7 @@ CATEGORY_DICT: 0: 'car' CERTAIN_SEQS: - - 'M0101' + - IGNORE_SEQS: # Seqs you want to ignore - diff --git a/tracker/track_demo.py b/tracker/track_demo.py index ca9b9fa..0a564d6 100644 --- a/tracker/track_demo.py +++ b/tracker/track_demo.py @@ -30,6 +30,7 @@ from models.experimental import attempt_load from evaluate import evaluate from utils.torch_utils import select_device, time_synchronized, TracedModel + from utils.general import non_max_suppression, scale_coords, check_img_size print('Note: running yolo v7 detector') except: @@ -64,8 +65,11 @@ def main(opts): 1. load model """ device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") + ckpt = torch.load(opts.model_path, map_location=device) model = ckpt['ema' if ckpt.get('ema') else 'model'].float().fuse().eval() # for yolo v7 + stride = int(model.stride.max()) # model stride + opts.img_size = check_img_size(opts.img_size, s=stride) # check img_size if opts.trace: print(opts.img_size) @@ -86,7 +90,7 @@ def main(opts): # check path assert os.path.exists(obj_name), 'the path does not exist! ' obj, get_next_frame = None, None # init obj - if 'mp4' in opts.obj or 'MP4' in opts.obj: # if it is a video + if 'mp4' in opts.obj or 'MP4' in opts.obj or 'mkv' in opts.obj: # if it is a video obj = cv2.VideoCapture(obj_name) get_next_frame = lambda _ : obj.read() @@ -94,7 +98,7 @@ def main(opts): else: obj_name = obj_name[:-4] else: - obj = my_queue(os.listdir(obj_name)) + obj = my_queue(os.listdir(obj_name), obj_name) get_next_frame = lambda _ : obj.pop_front() if os.path.isabs(obj_name): obj_name = obj_name.split('/')[-1] @@ -116,13 +120,16 @@ def main(opts): if not is_valid: break # end of reading - img = resize_a_frame(img0, [opts.img_size, opts.img_size]) + img, img0 = preprocess_v7(ori_img=img0, model_size=(opts.img_size, opts.img_size), model_stride=stride) timer.tic() # start timing this img img = img.unsqueeze(0) # (C, H, W) -> (bs == 1, C, H, W) - out = model(img.to(device)) # model forward - out = out[0] # NOTE: for yolo v7 - + with torch.no_grad(): + out = model(img.to(device)) # model forward + out = out[0] # NOTE: for yolo v7 + + out = post_process_v7(out, img_size=img.shape[2:], ori_img_size=img0.shape) + if len(out.shape) == 3: # case (bs, num_obj, ...) # out = out.squeeze() # NOTE: assert batch size == 1 @@ -132,11 +139,11 @@ def main(opts): # NOTE: yolo v7 origin out format: [xc, yc, w, h, conf, cls0_conf, cls1_conf, ..., clsn_conf] - cls_conf, cls_idx = torch.max(out[:, 5:], dim=1) + # cls_conf, cls_idx = torch.max(out[:, 5:], dim=1) # out[:, 4] *= cls_conf # fuse object and cls conf - out[:, 5] = cls_idx - out = out[:, :6] - + # out[:, 5] = cls_idx + # out = out[:, :6] + current_tracks = tracker.update(out, img0) # List[class(STracks)] @@ -157,7 +164,7 @@ def main(opts): results.append((frame_id + 1, cur_id, cur_tlwh, cur_cls)) timer.toc() # end timing this image - plot_img(img0, frame_id, [cur_tlwh, cur_id, cur_cls], save_dir=os.path.join(SAVE_FOLDER, 'reuslt_images', obj_name)) + plot_img(img0, frame_id, [cur_tlwh, cur_id, cur_cls], save_dir=os.path.join(SAVE_FOLDER, 'result_images', obj_name)) frame_id += 1 @@ -175,15 +182,16 @@ class my_queue: """ implement a queue for image seq reading """ - def __init__(self, arr: list) -> None: + def __init__(self, arr: list, root_path: str) -> None: self.arr = arr self.start_idx = 0 + self.root_path = root_path def push_back(self, item): self.arr.append(item) def pop_front(self): - ret = cv2.imread(self.arr[self.start_idx]) + ret = cv2.imread(os.path.join(self.root_path, self.arr[self.start_idx])) self.start_idx += 1 return not self.is_empty(), ret @@ -191,25 +199,63 @@ def is_empty(self): return self.start_idx == len(self.arr) -def resize_a_frame(frame, target_size): - """ - resize a frame to target size - - frame: np.ndarray, shape (H, W, C) - target_size: List[int, int] | Tuple[int, int] +def post_process_v7(out, img_size, ori_img_size): + """ post process for v5 and v7 + """ - # resize to input to the YOLO net - frame_resized = cv2.resize(frame, (target_size[0], target_size[1])) # (H', W', C) - # convert BGR to RGB and to (C, H, W) - frame_resized = frame_resized[:, :, ::-1].transpose(2, 0, 1) - frame_resized = np.ascontiguousarray(frame_resized, dtype=np.float32) - frame_resized /= 255.0 + out = non_max_suppression(out, conf_thres=0.01, )[0] + out[:, :4] = scale_coords(img_size, out[:, :4], ori_img_size, ratio_pad=None).round() - frame_resized = torch.from_numpy(frame_resized) + # out: tlbr, conf, cls - return frame_resized + return out +def preprocess_v7(ori_img, model_size, model_stride): + """ simple preprocess for a single image + + """ + img_resized = _letterbox(ori_img, new_shape=model_size, stride=model_stride)[0] + + img_resized = img_resized[:, :, ::-1].transpose(2, 0, 1) # BGR to RGB + img_resized = np.ascontiguousarray(img_resized) + + img_resized = torch.from_numpy(img_resized).float() + img_resized /= 255.0 + + return img_resized, ori_img + +def _letterbox(img, new_shape=(640, 640), color=(114, 114, 114), auto=True, scaleFill=False, scaleup=True, stride=32): + # Resize and pad image while meeting stride-multiple constraints + shape = img.shape[:2] # current shape [height, width] + if isinstance(new_shape, int): + new_shape = (new_shape, new_shape) + + # Scale ratio (new / old) + r = min(new_shape[0] / shape[0], new_shape[1] / shape[1]) + if not scaleup: # only scale down, do not scale up (for better test mAP) + r = min(r, 1.0) + + # Compute padding + ratio = r, r # width, height ratios + new_unpad = int(round(shape[1] * r)), int(round(shape[0] * r)) + dw, dh = new_shape[1] - new_unpad[0], new_shape[0] - new_unpad[1] # wh padding + if auto: # minimum rectangle + dw, dh = np.mod(dw, stride), np.mod(dh, stride) # wh padding + elif scaleFill: # stretch + dw, dh = 0.0, 0.0 + new_unpad = (new_shape[1], new_shape[0]) + ratio = new_shape[1] / shape[1], new_shape[0] / shape[0] # width, height ratios + + dw /= 2 # divide padding into 2 sides + dh /= 2 + + if shape[::-1] != new_unpad: # resize + img = cv2.resize(img, new_unpad, interpolation=cv2.INTER_LINEAR) + top, bottom = int(round(dh - 0.1)), int(round(dh + 0.1)) + left, right = int(round(dw - 0.1)), int(round(dw + 0.1)) + img = cv2.copyMakeBorder(img, top, bottom, left, right, cv2.BORDER_CONSTANT, value=color) # add border + return img, ratio, (dw, dh) def save_results(obj_name, results, data_type='default'): """ @@ -255,7 +301,7 @@ def plot_img(img, frame_id, results, save_dir): # draw a rect cv2.rectangle(img_, tlbr[:2], tlbr[2:], get_color(id), thickness=3, ) # note the id and cls - text = f'{CATEGORY_DICT[cls]}-{id}' + text = f'id: {id}' cv2.putText(img_, text, (tlbr[0], tlbr[1]), fontFace=cv2.FONT_HERSHEY_PLAIN, fontScale=1, color=(255, 164, 0), thickness=2) @@ -273,14 +319,15 @@ def save_videos(obj_name): obj_name = [obj_name] for seq in obj_name: - images_path = os.path.join(SAVE_FOLDER, 'reuslt_images', seq) + if 'mp4' in seq: seq = seq[:-4] + images_path = os.path.join(SAVE_FOLDER, 'result_images', seq) images_name = sorted(os.listdir(images_path)) to_video_path = os.path.join(images_path, '../', seq + '.mp4') fourcc = cv2.VideoWriter_fourcc(*"mp4v") img0 = Image.open(os.path.join(images_path, images_name[0])) - vw = cv2.VideoWriter(to_video_path, fourcc, 15, img0.size) + vw = cv2.VideoWriter(to_video_path, fourcc, 30, img0.size) for img in images_name: if img.endswith('.jpg'): @@ -303,12 +350,12 @@ def get_color(idx): if __name__ == '__main__': parser = argparse.ArgumentParser() - parser.add_argument('--obj', type=str, default='M1305.mp4', help='video NAME or images FOLDER NAME') + parser.add_argument('--obj', type=str, default='demo.mp4', help='video NAME or images FOLDER NAME') parser.add_argument('--save_txt', type=bool, default=False, help='whether save txt') parser.add_argument('--tracker', type=str, default='sort', help='sort, deepsort, etc') - parser.add_argument('--model_path', type=str, default='./weights/best.pt', help='model path') + parser.add_argument('--model_path', type=str, default='./weights/yolov7_UAVDT_35epochs_20230507.pt', help='model path') parser.add_argument('--trace', type=bool, default=False, help='traced model of YOLO v7') parser.add_argument('--img_size', type=int, default=1280, help='[train, test] image sizes') @@ -319,7 +366,7 @@ def get_color(idx): parser.add_argument('--dhn_path', type=str, default='./weights/DHN.pth', help='path of DHN path for DeepMOT') # threshs - parser.add_argument('--conf_thresh', type=float, default=0.5, help='filter tracks') + parser.add_argument('--conf_thresh', type=float, default=0.05, help='filter tracks') parser.add_argument('--nms_thresh', type=float, default=0.7, help='thresh for NMS') parser.add_argument('--iou_thresh', type=float, default=0.5, help='IOU thresh to filter tracks') diff --git a/tracker/track_yolov5.py b/tracker/track_yolov5.py index 90c33dc..3798324 100644 --- a/tracker/track_yolov5.py +++ b/tracker/track_yolov5.py @@ -167,7 +167,7 @@ def main(opts, cfgs): timer.toc() # end timing this image if opts.save_images: - plot_img(img0, frame_id, [cur_tlwh, cur_id, cur_cls], save_dir=os.path.join(DATASET_ROOT, 'reuslt_images', seq)) + plot_img(img0, frame_id, [cur_tlwh, cur_id, cur_cls], save_dir=os.path.join(DATASET_ROOT, 'result_images', seq)) frame_id += 1 @@ -297,7 +297,7 @@ def save_videos(seq_names): seq_names = [seq_names] for seq in seq_names: - images_path = os.path.join(DATASET_ROOT, 'reuslt_images', seq) + images_path = os.path.join(DATASET_ROOT, 'result_images', seq) images_name = sorted(os.listdir(images_path)) to_video_path = os.path.join(images_path, '../', seq + '.mp4') diff --git a/tracker/track_yolov8.py b/tracker/track_yolov8.py index f3aaa75..5765f51 100644 --- a/tracker/track_yolov8.py +++ b/tracker/track_yolov8.py @@ -176,7 +176,7 @@ def main(opts, cfgs): timer.toc() # end timing this image if opts.save_images: - plot_img(img0, frame_id, [cur_tlwh, cur_id, cur_cls], save_dir=os.path.join(DATASET_ROOT, 'reuslt_images', seq)) + plot_img(img0, frame_id, [cur_tlwh, cur_id, cur_cls], save_dir=os.path.join(DATASET_ROOT, 'result_images', seq)) frame_id += 1 @@ -305,7 +305,7 @@ def save_videos(seq_names): seq_names = [seq_names] for seq in seq_names: - images_path = os.path.join(DATASET_ROOT, 'reuslt_images', seq) + images_path = os.path.join(DATASET_ROOT, 'result_images', seq) images_name = sorted(os.listdir(images_path)) to_video_path = os.path.join(images_path, '../', seq + '.mp4')