| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
|
|
| import argparse |
| import datetime |
| import json |
| import os |
| import random |
| import time |
| from copy import deepcopy |
| from pathlib import Path |
|
|
| import datasets |
| import datasets.samplers as samplers |
|
|
| import numpy as np |
| import torch |
| import util.misc as utils |
| from datasets import build_dataset, get_coco_api_from_dataset |
| from engine import evaluate, train_one_epoch |
| from engine_tta import evaluate_tta |
| from models import build_model |
| from torch.utils.data import DataLoader |
| from util.ema import requires_grad, update_ema |
|
|
|
|
| def get_args_parser(): |
| parser = argparse.ArgumentParser("Deformable DETR Detector", add_help=False) |
| parser.add_argument("--lr", default=2e-4, type=float) |
| parser.add_argument( |
| "--lr_backbone_names", default=["backbone.0"], type=str, nargs="+" |
| ) |
| parser.add_argument("--lr_backbone", default=2e-5, type=float) |
| parser.add_argument( |
| "--lr_linear_proj_names", |
| default=["reference_points", "sampling_offsets"], |
| type=str, |
| nargs="+", |
| ) |
| parser.add_argument("--lr_linear_proj_mult", default=0.1, type=float) |
| parser.add_argument("--batch_size", default=2, type=int) |
| parser.add_argument("--weight_decay", default=1e-4, type=float) |
| parser.add_argument("--epochs", default=50, type=int) |
| parser.add_argument("--eval_per_epochs", default=1, type=int) |
| parser.add_argument("--save_per_epochs", default=1, type=int) |
| parser.add_argument("--lr_drop", default=40, type=int) |
| parser.add_argument("--lr_drop_epochs", default=None, type=int, nargs="+") |
| parser.add_argument( |
| "--clip_max_norm", default=0.1, type=float, help="gradient clipping max norm" |
| ) |
|
|
| parser.add_argument("--sgd", action="store_true") |
| parser.add_argument("--ema", action="store_true") |
| parser.add_argument("--ema_decay", default=0.999, type=float) |
|
|
| |
| parser.add_argument("--with_box_refine", default=False, action="store_true") |
| parser.add_argument("--two_stage", default=False, action="store_true") |
|
|
| |
| parser.add_argument( |
| "--frozen_weights", |
| type=str, |
| default=None, |
| help="Path to the pretrained model. If set, only the mask head will be trained", |
| ) |
|
|
| |
| parser.add_argument( |
| "--backbone", |
| default="resnet50", |
| type=str, |
| help="Name of the convolutional backbone to use", |
| ) |
| parser.add_argument( |
| "--backbone_size", |
| default="Gwin384", |
| type=str, |
| help="backbone size", |
| ) |
| parser.add_argument( |
| "--backbone_path", |
| default="", |
| type=str, |
| ) |
| parser.add_argument( |
| "--backbone_lrd", |
| default=1.0, |
| type=float, |
| ) |
| parser.add_argument( |
| "--backbone_layers", |
| default=12, |
| type=int, |
| ) |
| parser.add_argument( |
| "--backbone_init_values", |
| default=0.0, |
| type=float, |
| ) |
| parser.add_argument( |
| "--backbone_tile_posemb", |
| default=False, |
| type=bool, |
| ) |
| parser.add_argument( |
| "--backbone_use_act_checkpoint", |
| action="store_true", |
| help="If true, we use act_checkpoint in backbone", |
| ) |
| parser.add_argument( |
| "--backbone_act_checkpoint_ratio", |
| default=1.0, |
| type=float, |
| ) |
| parser.add_argument( |
| "--backbone_tta_rope", |
| action="store_true", |
| ) |
| parser.add_argument( |
| "--backbone_multi_layer", |
| action="store_true", |
| ) |
|
|
| parser.add_argument( |
| "--backbone_win_aug", |
| action="store_true", |
| ) |
|
|
| parser.add_argument( |
| "--backbone_dp", |
| default=-1.0, |
| type=float, |
| ) |
|
|
| parser.add_argument( |
| "--bf16", |
| action="store_true", |
| ) |
| parser.add_argument( |
| "--fp16", |
| action="store_true", |
| ) |
| parser.add_argument( |
| "--dilation", |
| action="store_true", |
| help="If true, we replace stride with dilation in the last convolutional block (DC5)", |
| ) |
| parser.add_argument( |
| "--position_embedding", |
| default="sine", |
| type=str, |
| choices=("sine", "learned"), |
| help="Type of positional embedding to use on top of the image features", |
| ) |
| parser.add_argument( |
| "--position_embedding_scale", |
| default=2 * np.pi, |
| type=float, |
| help="position / size * scale", |
| ) |
| parser.add_argument( |
| "--num_feature_levels", default=4, type=int, help="number of feature levels" |
| ) |
|
|
| |
| parser.add_argument( |
| "--enc_layers", |
| default=6, |
| type=int, |
| help="Number of encoding layers in the transformer", |
| ) |
| parser.add_argument( |
| "--dec_layers", |
| default=6, |
| type=int, |
| help="Number of decoding layers in the transformer", |
| ) |
| parser.add_argument( |
| "--dim_feedforward", |
| default=1024, |
| type=int, |
| help="Intermediate size of the feedforward layers in the transformer blocks", |
| ) |
| parser.add_argument( |
| "--hidden_dim", |
| default=256, |
| type=int, |
| help="Size of the embeddings (dimension of the transformer)", |
| ) |
| parser.add_argument( |
| "--dropout", default=0.1, type=float, help="Dropout applied in the transformer" |
| ) |
| parser.add_argument( |
| "--nheads", |
| default=8, |
| type=int, |
| help="Number of attention heads inside the transformer's attentions", |
| ) |
| parser.add_argument( |
| "--num_queries", default=300, type=int, help="Number of query slots" |
| ) |
| parser.add_argument("--dec_n_points", default=4, type=int) |
| parser.add_argument("--enc_n_points", default=4, type=int) |
|
|
| |
| parser.add_argument( |
| "--masks", |
| action="store_true", |
| help="Train segmentation head if the flag is provided", |
| ) |
|
|
| |
| parser.add_argument( |
| "--no_aux_loss", |
| dest="aux_loss", |
| action="store_false", |
| help="Disables auxiliary decoding losses (loss at each layer)", |
| ) |
| parser.add_argument("--use_fed_loss", action="store_true") |
|
|
| |
| parser.add_argument("--assign_first_stage", action="store_true") |
| parser.add_argument("--assign_second_stage", action="store_true") |
| parser.add_argument( |
| "--set_cost_class", |
| default=2, |
| type=float, |
| help="Class coefficient in the matching cost", |
| ) |
| parser.add_argument( |
| "--set_cost_bbox", |
| default=5, |
| type=float, |
| help="L1 box coefficient in the matching cost", |
| ) |
| parser.add_argument( |
| "--set_cost_giou", |
| default=2, |
| type=float, |
| help="giou box coefficient in the matching cost", |
| ) |
|
|
| |
| parser.add_argument("--mask_loss_coef", default=1, type=float) |
| parser.add_argument("--dice_loss_coef", default=1, type=float) |
| parser.add_argument("--cls_loss_coef", default=2, type=float) |
| parser.add_argument("--bbox_loss_coef", default=5, type=float) |
| parser.add_argument("--giou_loss_coef", default=2, type=float) |
| parser.add_argument("--focal_alpha", default=0.25, type=float) |
|
|
| |
| parser.add_argument("--new_mean_std", action="store_true") |
| parser.add_argument("--dataset_file", default="coco") |
| parser.add_argument("--coco_path", default="./data/coco", type=str) |
| parser.add_argument("--coco_panoptic_path", type=str) |
| parser.add_argument("--remove_difficult", action="store_true") |
| parser.add_argument("--bigger", action="store_true") |
| parser.add_argument("--lsj", action="store_true") |
| parser.add_argument("--lsj_ms", action="store_true") |
|
|
| parser.add_argument("--lsj_img_size", default=1024, type=int) |
| parser.add_argument("--lsj_img_train_min", default=480, type=int) |
| parser.add_argument("--lsj_img_size_max", default=-1, type=int) |
| parser.add_argument("--lsj_strong_aug", action="store_true") |
|
|
| parser.add_argument("--save_result", action="store_true") |
| parser.add_argument("--save_result_dir", default="", type=str) |
| parser.add_argument("--test_hflip_aug", action="store_true") |
| parser.add_argument("--tta", action="store_true") |
| parser.add_argument("--soft_nms", action="store_true") |
| parser.add_argument("--soft_nms_method", default="quad", type=str) |
| parser.add_argument("--nms_thresh", default=0.7, type=float) |
| parser.add_argument("--quad_scale", default=0.5, type=float) |
| parser.add_argument( |
| "--output_dir", default="", help="path where to save, empty for no saving" |
| ) |
| parser.add_argument( |
| "--device", default="cuda", help="device to use for training / testing" |
| ) |
| parser.add_argument("--seed", default=42, type=int) |
| parser.add_argument("--resume", default="", help="resume from checkpoint") |
| parser.add_argument("--auto_resume", action="store_true") |
|
|
| parser.add_argument( |
| "--resume_norope", |
| action="store_true", |
| help="resume from checkpoint without rope params", |
| ) |
| parser.add_argument("--finetune", default="", help="finetune from checkpoint") |
| parser.add_argument("--keep_class_embed", action="store_true") |
| parser.add_argument( |
| "--start_epoch", default=0, type=int, metavar="N", help="start epoch" |
| ) |
| parser.add_argument("--eval", action="store_true") |
| parser.add_argument("--num_workers", default=8, type=int) |
| parser.add_argument( |
| "--cache_mode", |
| default=False, |
| action="store_true", |
| help="whether to cache images on memory", |
| ) |
|
|
| return parser |
|
|
|
|
| |
| def match_name_keywords(n, name_keywords): |
| out = False |
| for b in name_keywords: |
| if b in n: |
| out = True |
| break |
| return out |
|
|
|
|
| def get_vit_lr_decay_rate_vev01(name, lr_decay_rate=1.0, num_layers=12): |
| layer_id = num_layers + 1 |
| if ".positional_embedding" in name or ".conv1" in name or ".ln_pre" in name: |
| layer_id = 0 |
| elif ".resblocks." in name: |
| layer_id = int(name[name.find(".resblocks.") :].split(".")[2]) + 1 |
| return lr_decay_rate ** (num_layers + 1 - layer_id) |
|
|
|
|
| def custom_lr(model_without_ddp, args): |
| param_dicts = [ |
| { |
| "params": [ |
| p |
| for n, p in model_without_ddp.named_parameters() |
| if not match_name_keywords(n, args.lr_backbone_names) |
| and not match_name_keywords(n, args.lr_linear_proj_names) |
| and p.requires_grad |
| ], |
| "lr": args.lr, |
| }, |
| { |
| "params": [ |
| p |
| for n, p in model_without_ddp.named_parameters() |
| if match_name_keywords(n, args.lr_linear_proj_names) and p.requires_grad |
| ], |
| "lr": args.lr * args.lr_linear_proj_mult, |
| }, |
| ] |
| if "vev01" in args.backbone: |
| for p_key, p_value in model_without_ddp.named_parameters(): |
| if ( |
| match_name_keywords(p_key, args.lr_backbone_names) |
| and p_value.requires_grad |
| ): |
| p_lr = args.lr_backbone * get_vit_lr_decay_rate_vev01( |
| p_key, args.backbone_lrd, args.backbone_layers |
| ) |
| param_dicts.append( |
| { |
| "params": [p_value], |
| "lr": p_lr, |
| } |
| ) |
| print(f"param_name: {p_key}, lr: {p_lr}") |
| else: |
| param_groups_backbone = { |
| "params": [ |
| p |
| for n, p in model_without_ddp.named_parameters() |
| if match_name_keywords(n, args.lr_backbone_names) and p.requires_grad |
| ], |
| "lr": args.lr_backbone, |
| } |
| param_dicts.append(param_groups_backbone) |
|
|
| return param_dicts |
|
|
|
|
| def main(args): |
| utils.init_distributed_mode(args) |
| print("git:\n {}\n".format(utils.get_sha())) |
|
|
| if args.frozen_weights is not None: |
| assert args.masks, "Frozen training is meant for segmentation only" |
| print(args) |
|
|
| device = torch.device(args.device) |
|
|
| |
| seed = args.seed + utils.get_rank() |
| torch.manual_seed(seed) |
| np.random.seed(seed) |
| random.seed(seed) |
|
|
| model, criterion, postprocessors = build_model(args) |
| model.to(device) |
|
|
| model_without_ddp = model |
| n_parameters = sum(p.numel() for p in model.parameters() if p.requires_grad) |
| print("model:", model_without_ddp) |
| for n, p in model_without_ddp.named_parameters(): |
| print(n) |
| print("number of params:", n_parameters) |
|
|
| if args.ema: |
| ema = deepcopy(model).to(device) |
| requires_grad(ema, False) |
| print(f"EMA Parameters: {sum(p.numel() for p in ema.parameters()):,}") |
|
|
| dataset_train = build_dataset(image_set="train", args=args) |
| dataset_val = build_dataset(image_set="val", args=args) |
|
|
| if args.distributed: |
| if args.cache_mode: |
| sampler_train = samplers.NodeDistributedSampler(dataset_train) |
| sampler_val = samplers.NodeDistributedSampler(dataset_val, shuffle=False) |
| else: |
| if args.dataset_file == "lvis": |
| sampler_train = samplers.RepeatFactorTrainingSampler(dataset_train) |
| else: |
| sampler_train = samplers.DistributedSampler(dataset_train) |
| sampler_val = samplers.DistributedSampler(dataset_val, shuffle=False) |
| else: |
| sampler_train = torch.utils.data.RandomSampler(dataset_train) |
| sampler_val = torch.utils.data.SequentialSampler(dataset_val) |
|
|
| batch_sampler_train = torch.utils.data.BatchSampler( |
| sampler_train, args.batch_size, drop_last=True |
| ) |
| if args.lsj_ms: |
| collator = utils.CollatorLSJMultiscale(args.lsj_img_size, args.tta) |
| elif args.lsj: |
| lsj_img_size_colla = ( |
| args.lsj_img_size_max if args.lsj_img_size_max > 0 else args.lsj_img_size |
| ) |
| collator = utils.CollatorLSJ(lsj_img_size_colla, args.tta) |
| else: |
| collator = utils.collate_fn |
|
|
| data_loader_train = DataLoader( |
| dataset_train, |
| batch_sampler=batch_sampler_train, |
| collate_fn=collator, |
| num_workers=args.num_workers, |
| pin_memory=True, |
| ) |
| data_loader_val = DataLoader( |
| dataset_val, |
| args.batch_size, |
| sampler=sampler_val, |
| drop_last=False, |
| collate_fn=collator, |
| num_workers=args.num_workers, |
| pin_memory=True, |
| ) |
|
|
| param_dicts = custom_lr(model_without_ddp, args) |
|
|
| if args.sgd: |
| optimizer = torch.optim.SGD( |
| param_dicts, lr=args.lr, momentum=0.9, weight_decay=args.weight_decay |
| ) |
| else: |
| optimizer = torch.optim.AdamW( |
| param_dicts, lr=args.lr, weight_decay=args.weight_decay |
| ) |
| lr_scheduler = torch.optim.lr_scheduler.StepLR(optimizer, args.lr_drop) |
|
|
| if args.distributed: |
| model = torch.nn.parallel.DistributedDataParallel(model, device_ids=[args.gpu]) |
| model_without_ddp = model.module |
|
|
| if args.dataset_file == "coco_panoptic": |
| |
| coco_val = datasets.coco.build("val", args) |
| base_ds = get_coco_api_from_dataset(coco_val) |
| else: |
| base_ds = get_coco_api_from_dataset(dataset_val) |
|
|
| if args.frozen_weights is not None: |
| checkpoint = torch.load(args.frozen_weights, map_location="cpu") |
| model_without_ddp.detr.load_state_dict(checkpoint["model"]) |
| |
| if args.tta: |
| evaluate_fn = evaluate_tta |
| else: |
| evaluate_fn = evaluate |
|
|
| output_dir = Path(args.output_dir) |
| if args.auto_resume: |
| resumed_ckpt = os.path.join(args.output_dir, "checkpoint.pth") |
| if os.path.exists(resumed_ckpt): |
| args.resume = resumed_ckpt |
| args.finetune = None |
|
|
| if args.finetune: |
| checkpoint = torch.load(args.finetune, map_location="cpu") |
| state_dict = checkpoint["model"] |
| for k in list(state_dict.keys()): |
| if "class_embed" in k and not args.keep_class_embed: |
| print("removing", k) |
| del state_dict[k] |
| if "freqs" in k: |
| print("removing", k) |
| del state_dict[k] |
|
|
| missing_keys, unexpected_keys = model_without_ddp.load_state_dict( |
| state_dict, strict=False |
| ) |
| unexpected_keys = [ |
| k |
| for k in unexpected_keys |
| if not (k.endswith("total_params") or k.endswith("total_ops")) |
| ] |
| if len(missing_keys) > 0: |
| print("Missing Keys: {}".format(missing_keys)) |
| if len(unexpected_keys) > 0: |
| print("Unexpected Keys: {}".format(unexpected_keys)) |
|
|
| if "epoch" in checkpoint: |
| print("finetuning from epoch", checkpoint["epoch"]) |
|
|
| if args.ema: |
| ema.load_state_dict( |
| checkpoint["ema"] if "ema" in checkpoint else state_dict, strict=False |
| ) |
|
|
| if args.resume: |
| print("Resuming training from {}".format(args.resume)) |
| if args.resume.startswith("https"): |
| checkpoint = torch.hub.load_state_dict_from_url( |
| args.resume, map_location="cpu", check_hash=True |
| ) |
| else: |
| checkpoint = torch.load(args.resume, map_location="cpu") |
|
|
| if args.resume_norope: |
| state_dict = checkpoint["model"] |
| for k in list(state_dict.keys()): |
| if "freqs" in k: |
| print("removing", k) |
| del state_dict[k] |
|
|
| missing_keys, unexpected_keys = model_without_ddp.load_state_dict( |
| state_dict, strict=False |
| ) |
| if args.ema: |
| ema.load_state_dict( |
| checkpoint["ema"] if "ema" in checkpoint else state_dict, |
| strict=False, |
| ) |
| else: |
| missing_keys, unexpected_keys = model_without_ddp.load_state_dict( |
| checkpoint["model"], strict=False |
| ) |
| if args.ema: |
| ema.load_state_dict( |
| checkpoint["ema"] if "ema" in checkpoint else state_dict, |
| strict=False, |
| ) |
| unexpected_keys = [ |
| k |
| for k in unexpected_keys |
| if not (k.endswith("total_params") or k.endswith("total_ops")) |
| ] |
| if len(missing_keys) > 0: |
| print("Missing Keys: {}".format(missing_keys)) |
| if len(unexpected_keys) > 0: |
| print("Unexpected Keys: {}".format(unexpected_keys)) |
| if ( |
| not args.eval |
| and "optimizer" in checkpoint |
| and "lr_scheduler" in checkpoint |
| and "epoch" in checkpoint |
| ): |
| import copy |
|
|
| p_groups = copy.deepcopy(optimizer.param_groups) |
| optimizer.load_state_dict(checkpoint["optimizer"]) |
| for pg, pg_old in zip(optimizer.param_groups, p_groups): |
| pg["lr"] = pg_old["lr"] |
| pg["initial_lr"] = pg_old["initial_lr"] |
| print(optimizer.param_groups) |
| lr_scheduler.load_state_dict(checkpoint["lr_scheduler"]) |
| |
| args.override_resumed_lr_drop = True |
| if args.override_resumed_lr_drop: |
| print( |
| "Warning: (hack) args.override_resumed_lr_drop is set to True, so args.lr_drop would override lr_drop in resumed lr_scheduler." |
| ) |
| lr_scheduler.step_size = args.lr_drop |
| lr_scheduler.base_lrs = list( |
| map(lambda group: group["initial_lr"], optimizer.param_groups) |
| ) |
| lr_scheduler.step(lr_scheduler.last_epoch) |
| args.start_epoch = checkpoint["epoch"] + 1 |
| |
| if not args.eval: |
| test_stats, coco_evaluator = evaluate_fn( |
| model, |
| criterion, |
| postprocessors, |
| data_loader_val, |
| base_ds, |
| device, |
| args.output_dir, |
| args.test_hflip_aug, |
| args.tta, |
| args.soft_nms, |
| ema if args.ema else None, |
| args.save_result, |
| args.save_result_dir, |
| soft_nms_method=args.soft_nms_method, |
| nms_thresh=args.nms_thresh, |
| quad_scale=args.quad_scale, |
| lsj_img_size=args.lsj_img_size, |
| ) |
| torch.cuda.empty_cache() |
|
|
| if args.eval: |
| test_stats, coco_evaluator = evaluate_fn( |
| model, |
| criterion, |
| postprocessors, |
| data_loader_val, |
| base_ds, |
| device, |
| args.output_dir, |
| args.test_hflip_aug, |
| args.tta, |
| args.soft_nms, |
| ema if args.ema else None, |
| args.save_result, |
| args.save_result_dir, |
| soft_nms_method=args.soft_nms_method, |
| nms_thresh=args.nms_thresh, |
| quad_scale=args.quad_scale, |
| lsj_img_size=args.lsj_img_size, |
| ) |
|
|
| if args.output_dir: |
| utils.save_on_master( |
| coco_evaluator.coco_eval["bbox"].eval, output_dir / "eval.pth" |
| ) |
| return |
|
|
| print("Start training") |
| start_time = time.time() |
| if args.ema: |
| ema.eval() |
| for epoch in range(args.start_epoch, args.epochs): |
| if args.distributed: |
| sampler_train.set_epoch(epoch) |
| train_stats = train_one_epoch( |
| model, |
| criterion, |
| data_loader_train, |
| optimizer, |
| device, |
| epoch, |
| args.clip_max_norm, |
| ema if args.ema else None, |
| ema_decay=args.ema_decay, |
| ) |
| lr_scheduler.step() |
| if args.output_dir: |
| checkpoint_paths = [output_dir / "checkpoint.pth"] |
| |
| if ( |
| (epoch + 1) % args.lr_drop == 0 |
| or (epoch + 1) % args.save_per_epochs == 0 |
| or epoch + 1 == args.epochs |
| ): |
| checkpoint_paths.append(output_dir / f"checkpoint{epoch:04}.pth") |
| for checkpoint_path in checkpoint_paths: |
| ckpt_dict = { |
| "model": model_without_ddp.state_dict(), |
| "optimizer": optimizer.state_dict(), |
| "lr_scheduler": lr_scheduler.state_dict(), |
| "epoch": epoch, |
| "args": args, |
| } |
| if args.ema: |
| ckpt_dict["ema"] = ema.state_dict() |
| utils.save_on_master( |
| ckpt_dict, |
| checkpoint_path, |
| ) |
|
|
| torch.cuda.empty_cache() |
| if epoch % args.eval_per_epochs == 0 or epoch + 1 == args.epochs: |
| test_stats, coco_evaluator = evaluate_fn( |
| model, |
| criterion, |
| postprocessors, |
| data_loader_val, |
| base_ds, |
| device, |
| args.output_dir, |
| args.test_hflip_aug, |
| args.tta, |
| args.soft_nms, |
| ema if args.ema else None, |
| args.save_result, |
| args.save_result_dir, |
| soft_nms_method=args.soft_nms_method, |
| nms_thresh=args.nms_thresh, |
| quad_scale=args.quad_scale, |
| lsj_img_size=args.lsj_img_size, |
| ) |
| log_stats = { |
| **{f"train_{k}": v for k, v in train_stats.items()}, |
| **{f"test_{k}": v for k, v in test_stats.items()}, |
| "epoch": epoch, |
| "n_parameters": n_parameters, |
| } |
|
|
| if args.output_dir and utils.is_main_process(): |
| with (output_dir / "log.txt").open("a") as f: |
| f.write(json.dumps(log_stats) + "\n") |
|
|
| |
| if coco_evaluator is not None: |
| (output_dir / "eval").mkdir(exist_ok=True) |
| if "bbox" in coco_evaluator.coco_eval: |
| filenames = ["latest.pth"] |
| if epoch % 50 == 0: |
| filenames.append(f"{epoch:03}.pth") |
| for name in filenames: |
| torch.save( |
| coco_evaluator.coco_eval["bbox"].eval, |
| output_dir / "eval" / name, |
| ) |
| torch.cuda.empty_cache() |
|
|
| total_time = time.time() - start_time |
| total_time_str = str(datetime.timedelta(seconds=int(total_time))) |
| print("Training time {}".format(total_time_str)) |
|
|
|
|
| if __name__ == "__main__": |
| parser = argparse.ArgumentParser( |
| "Deformable DETR training and evaluation script", parents=[get_args_parser()] |
| ) |
| args = parser.parse_args() |
| if args.output_dir: |
| Path(args.output_dir).mkdir(parents=True, exist_ok=True) |
| main(args) |
|
|