Spaces:
Build error
Build error
lhhj
commited on
Commit
·
463b952
1
Parent(s):
b7ca4bf
initial ppush
Browse files- Dockerfile +49 -0
- Dockerfile.x86.yolov8_trainer +49 -0
- README.md +34 -10
- docker/scripts/docker_build.sh +11 -0
- runner/README.md +1 -0
- trainer/README.md +1 -0
- trainer/train_yolov8.py +146 -0
- trainer/utils/cvat_dataset.py +108 -0
- trainer/utils/download_cvatdata.py +98 -0
- trainer/utils/merge_cocos.py +98 -0
- trainer/utils/path_utils.py +48 -0
- trainer/utils/unzip_datasets.py +7 -0
- trainer/utils/yolo_labels.py +128 -0
Dockerfile
ADDED
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
FROM nvcr.io/nvidia/pytorch:23.03-py3
|
2 |
+
# FROM nvcr.io/nvidia/pytorch:24.02-py3
|
3 |
+
|
4 |
+
ARG HOME_PATH="/home"
|
5 |
+
WORKDIR ${HOME_PATH}
|
6 |
+
|
7 |
+
RUN pip3 install --upgrade pip wheel
|
8 |
+
RUN pip3 install azure-storage-blob azure-identity
|
9 |
+
|
10 |
+
# supervision
|
11 |
+
RUN git clone https://github.com/roboflow/supervision.git && \
|
12 |
+
cd supervision && \
|
13 |
+
grep -v "^opencv-python-headless" pyproject.toml > tmp.toml && \
|
14 |
+
mv tmp.toml pyproject.toml && \
|
15 |
+
pip3 install --no-cache -e .
|
16 |
+
|
17 |
+
# ultralytics
|
18 |
+
ADD https://ultralytics.com/assets/Arial.ttf https://ultralytics.com/assets/Arial.Unicode.ttf /root/.config/Ultralytics/
|
19 |
+
RUN git clone https://github.com/ultralytics/ultralytics && \
|
20 |
+
cd ultralytics && \
|
21 |
+
grep -v "opencv-python\|openvino-dev" pyproject.toml > tmp.toml && mv tmp.toml pyproject.toml && \
|
22 |
+
pip3 install "opencv-python-headless<4.7" "opencv-contrib-python<4.7" "opencv-contrib-python-headless<4.7" "albumentations<1.4.0" && \
|
23 |
+
pip3 install .
|
24 |
+
|
25 |
+
# download dataset
|
26 |
+
ARG CVAT_URL
|
27 |
+
ARG CVAT_ORG
|
28 |
+
ARG CVAT_TASKS_YAML
|
29 |
+
ARG TRAIN_HP_YAML
|
30 |
+
ARG PYPREPROCESS
|
31 |
+
|
32 |
+
COPY . .
|
33 |
+
# COPY AIEM/trainer /home/trainer
|
34 |
+
# COPY ${CVAT_TASKS_YAML} ${CVAT_TASKS_YAML}
|
35 |
+
# COPY ${TRAIN_HP_YAML} ${TRAIN_HP_YAML}
|
36 |
+
|
37 |
+
ENV APP_PYPREPROCESS=${PYPREPROCESS}
|
38 |
+
ENV APP_CVAT_TASKS_YAML=${CVAT_TASKS_YAML}
|
39 |
+
ENV APP_HOME=${HOME_PATH}
|
40 |
+
ENV APP_TRAIN_HP_YAML=${TRAIN_HP_YAML}
|
41 |
+
|
42 |
+
RUN cd AIEM/trainer && \
|
43 |
+
python3 utils/download_cvatdata.py \
|
44 |
+
"$CVAT_URL" \
|
45 |
+
"$CVAT_ORG"
|
46 |
+
RUN cd /data && \
|
47 |
+
rm -rf *.zip
|
48 |
+
|
49 |
+
ENTRYPOINT ["python3", "AIEM/trainer/train_yolov8.py"]
|
Dockerfile.x86.yolov8_trainer
ADDED
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
FROM nvcr.io/nvidia/pytorch:23.03-py3
|
2 |
+
# FROM nvcr.io/nvidia/pytorch:24.02-py3
|
3 |
+
|
4 |
+
ARG HOME_PATH="/home"
|
5 |
+
WORKDIR ${HOME_PATH}
|
6 |
+
|
7 |
+
RUN pip3 install --upgrade pip wheel
|
8 |
+
RUN pip3 install azure-storage-blob azure-identity
|
9 |
+
|
10 |
+
# supervision
|
11 |
+
RUN git clone https://github.com/roboflow/supervision.git && \
|
12 |
+
cd supervision && \
|
13 |
+
grep -v "^opencv-python-headless" pyproject.toml > tmp.toml && \
|
14 |
+
mv tmp.toml pyproject.toml && \
|
15 |
+
pip3 install --no-cache -e .
|
16 |
+
|
17 |
+
# ultralytics
|
18 |
+
ADD https://ultralytics.com/assets/Arial.ttf https://ultralytics.com/assets/Arial.Unicode.ttf /root/.config/Ultralytics/
|
19 |
+
RUN git clone https://github.com/ultralytics/ultralytics && \
|
20 |
+
cd ultralytics && \
|
21 |
+
grep -v "opencv-python\|openvino-dev" pyproject.toml > tmp.toml && mv tmp.toml pyproject.toml && \
|
22 |
+
pip3 install "opencv-python-headless<4.7" "opencv-contrib-python<4.7" "opencv-contrib-python-headless<4.7" "albumentations<1.4.0" && \
|
23 |
+
pip3 install .
|
24 |
+
|
25 |
+
# download dataset
|
26 |
+
ARG CVAT_URL
|
27 |
+
ARG CVAT_ORG
|
28 |
+
ARG CVAT_TASKS_YAML
|
29 |
+
ARG TRAIN_HP_YAML
|
30 |
+
ARG PYPREPROCESS
|
31 |
+
|
32 |
+
COPY . .
|
33 |
+
# COPY AIEM/trainer /home/trainer
|
34 |
+
# COPY ${CVAT_TASKS_YAML} ${CVAT_TASKS_YAML}
|
35 |
+
# COPY ${TRAIN_HP_YAML} ${TRAIN_HP_YAML}
|
36 |
+
|
37 |
+
ENV APP_PYPREPROCESS=${PYPREPROCESS}
|
38 |
+
ENV APP_CVAT_TASKS_YAML=${CVAT_TASKS_YAML}
|
39 |
+
ENV APP_HOME=${HOME_PATH}
|
40 |
+
ENV APP_TRAIN_HP_YAML=${TRAIN_HP_YAML}
|
41 |
+
|
42 |
+
RUN cd AIEM/trainer && \
|
43 |
+
python3 utils/download_cvatdata.py \
|
44 |
+
"$CVAT_URL" \
|
45 |
+
"$CVAT_ORG"
|
46 |
+
RUN cd /data && \
|
47 |
+
rm -rf *.zip
|
48 |
+
|
49 |
+
ENTRYPOINT ["python3", "AIEM/trainer/train_yolov8.py"]
|
README.md
CHANGED
@@ -1,10 +1,34 @@
|
|
1 |
-
|
2 |
-
|
3 |
-
|
4 |
-
|
5 |
-
|
6 |
-
|
7 |
-
|
8 |
-
|
9 |
-
|
10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# AIEM
|
2 |
+
|
3 |
+
AI Edge Management
|
4 |
+
|
5 |
+
**TODO**: introduce segmentation env variable
|
6 |
+
|
7 |
+
AIEM repo can be seen as the core shared across all the projects that require an AI model to be trained or to run an inference server. It talks to the rest of the project-specific repos by means of, e.g., a GitHub Actions workflow. It contains Dockerfiles for different architectures and for different purposes. For example: training a YoloV8 model in an x86 architecture (*Dockerfile.x86.yolov8_trainer*).
|
8 |
+
|
9 |
+
## Structure
|
10 |
+
|
11 |
+
The structure of the project:
|
12 |
+
|
13 |
+
```bash
|
14 |
+
.
|
15 |
+
├── docker
|
16 |
+
│ ├── Dockerfile.x86.yolov8_trainer
|
17 |
+
│ └── scripts
|
18 |
+
│ └── docker_build.sh
|
19 |
+
├── README.md
|
20 |
+
├── runner
|
21 |
+
│ └── README.md
|
22 |
+
└── trainer
|
23 |
+
├── README.md
|
24 |
+
├── train_yolov8.py
|
25 |
+
└── utils
|
26 |
+
├── cvat_dataset.py
|
27 |
+
├── download_cvatdata.py
|
28 |
+
├── merge_cocos.py
|
29 |
+
├── path_utils.py
|
30 |
+
├── unzip_datasets.py
|
31 |
+
└── yolo_labels.py
|
32 |
+
```
|
33 |
+
|
34 |
+
- **Download data** (*trainer/utils/download_cvatdata.py*). Main script to download the dataset into the docker container. It reads from project-specific YAML file with the tasks to download from CVAT, preprocess the data and get the workspace ready for the model be able to be trained.
|
docker/scripts/docker_build.sh
ADDED
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
#!/usr/bin/env bash
|
2 |
+
|
3 |
+
CONTAINER=$1
|
4 |
+
DOCKERFILE=$2
|
5 |
+
|
6 |
+
shift
|
7 |
+
shift
|
8 |
+
|
9 |
+
echo "Building $CONTAINER container..."
|
10 |
+
|
11 |
+
docker build --network=host -t $CONTAINER -f $DOCKERFILE "$@" .
|
runner/README.md
ADDED
@@ -0,0 +1 @@
|
|
|
|
|
1 |
+
To be designed. It has to do with project-wise running models.
|
trainer/README.md
ADDED
@@ -0,0 +1 @@
|
|
|
|
|
1 |
+
This folder reads from a config file specific to a project. This repo must be sym-linked inside the project folder.
|
trainer/train_yolov8.py
ADDED
@@ -0,0 +1,146 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import argparse
|
2 |
+
import os
|
3 |
+
import yaml
|
4 |
+
import shutil
|
5 |
+
import datetime
|
6 |
+
import numpy as np
|
7 |
+
import pandas as pd
|
8 |
+
import yaml
|
9 |
+
from azure.storage.blob import BlobServiceClient
|
10 |
+
from pathlib import Path
|
11 |
+
from sklearn.model_selection import KFold
|
12 |
+
from collections import Counter
|
13 |
+
from ultralytics import YOLO
|
14 |
+
from utils.path_utils import *
|
15 |
+
|
16 |
+
STORAGE_ACCOUNT_KEY = "mhqTCNmdIgsnvyFnfv0r2JKfs8iG//5YVnphCq336XNxhyI72brMy6lP88I9XKVya/G9ZlAAMoNd+AStsXFe0Q=="
|
17 |
+
STORAGE_ACCOUNT_NAME = "camtagstoreaiem"
|
18 |
+
CONNECTION_STRING = "DefaultEndpointsProtocol=https;AccountName=camtagstoreaiem;AccountKey=mhqTCNmdIgsnvyFnfv0r2JKfs8iG//5YVnphCq336XNxhyI72brMy6lP88I9XKVya/G9ZlAAMoNd+AStsXFe0Q==;EndpointSuffix=core.windows.net"
|
19 |
+
CONTAINER_NAME = "upload"
|
20 |
+
|
21 |
+
# Get YAML file containing the training hyperparameters
|
22 |
+
HOME = os.getenv("APP_HOME")
|
23 |
+
APP_TRAIN_HP_YAML = os.path.join(HOME, os.getenv("APP_TRAIN_HP_YAML"))
|
24 |
+
|
25 |
+
def azure_upload(local_fname, blob_fname, overwrite=True):
|
26 |
+
blob_service_client = BlobServiceClient.from_connection_string(CONNECTION_STRING)
|
27 |
+
blob_client = blob_service_client.get_blob_client(
|
28 |
+
container = CONTAINER_NAME,
|
29 |
+
blob = blob_fname
|
30 |
+
)
|
31 |
+
with open(local_fname, "rb") as data:
|
32 |
+
blob_client.upload_blob(data, overwrite=overwrite)
|
33 |
+
|
34 |
+
|
35 |
+
if __name__ == "__main__":
|
36 |
+
with open(APP_TRAIN_HP_YAML, "r") as f:
|
37 |
+
y = yaml.safe_load(f)
|
38 |
+
KSPLIT = y['ksplit']
|
39 |
+
EPOCHS = y['epochs']
|
40 |
+
MODEL = y['model']
|
41 |
+
DATA_PATH = y['data_path']
|
42 |
+
BATCH_SIZE = y['batch_size']
|
43 |
+
|
44 |
+
# coco
|
45 |
+
coco_dataset_path = Path(DATA_PATH)
|
46 |
+
coco_dict = read_coco_json(coco_dataset_path / "merged.json")
|
47 |
+
|
48 |
+
classes = {cat['id']-1: cat['name'] for cat in coco_dict['categories']}
|
49 |
+
cls_idx = sorted(classes.keys())
|
50 |
+
|
51 |
+
labels = sorted((coco_dataset_path / "labels").rglob("*.txt"))
|
52 |
+
indx = [l.stem for l in labels]
|
53 |
+
labels_df = pd.DataFrame([], columns=cls_idx, index=indx)
|
54 |
+
|
55 |
+
for label in labels:
|
56 |
+
label_counter = Counter()
|
57 |
+
with open(label, 'r') as lf:
|
58 |
+
lines = lf.readlines()
|
59 |
+
|
60 |
+
for l in lines:
|
61 |
+
label_counter[int(l.split(' ')[0])] += 1
|
62 |
+
labels_df.loc[label.stem] = label_counter
|
63 |
+
|
64 |
+
labels_df = labels_df.fillna(0.0)
|
65 |
+
|
66 |
+
# KFOLD
|
67 |
+
kf = KFold(
|
68 |
+
n_splits = KSPLIT,
|
69 |
+
shuffle = True,
|
70 |
+
random_state = 42
|
71 |
+
)
|
72 |
+
kfolds = list(kf.split(labels_df))
|
73 |
+
|
74 |
+
folds = [f'split_{n}' for n in range(1, KSPLIT + 1)]
|
75 |
+
folds_df = pd.DataFrame(index=indx, columns=folds)
|
76 |
+
for idx, (train, val) in enumerate(kfolds, start=1):
|
77 |
+
folds_df[f'split_{idx}'].loc[labels_df.iloc[train].index] = 'train'
|
78 |
+
folds_df[f'split_{idx}'].loc[labels_df.iloc[val].index] = 'val'
|
79 |
+
|
80 |
+
# check distributions. balanced?
|
81 |
+
fold_lbl_distrb = pd.DataFrame(index=folds, columns=cls_idx)
|
82 |
+
for n, (train_indices, val_indices) in enumerate(kfolds, start=1):
|
83 |
+
train_totals = labels_df.iloc[train_indices].sum()
|
84 |
+
val_totals = labels_df.iloc[val_indices].sum()
|
85 |
+
|
86 |
+
ratio = val_totals / (train_totals + 1E-7)
|
87 |
+
fold_lbl_distrb.loc[f'split_{n}'] = ratio
|
88 |
+
|
89 |
+
# datasets for each fold
|
90 |
+
save_path = Path(coco_dataset_path / f'{datetime.date.today().isoformat()}_{KSPLIT}-Fold_Cross-val')
|
91 |
+
save_path.mkdir(parents=True, exist_ok=True)
|
92 |
+
|
93 |
+
suffix = sorted((coco_dataset_path / 'images').rglob("*.*"))[0].suffix
|
94 |
+
images = [coco_dataset_path / "images" / l.with_suffix(suffix).name for l in labels]
|
95 |
+
ds_yamls = []
|
96 |
+
|
97 |
+
for split in folds_df.columns:
|
98 |
+
# create directories
|
99 |
+
split_dir = save_path / split
|
100 |
+
split_dir.mkdir(parents=True, exist_ok=True)
|
101 |
+
(split_dir / 'train' / 'images').mkdir(parents=True, exist_ok=True)
|
102 |
+
(split_dir / 'train' / 'labels').mkdir(parents=True, exist_ok=True)
|
103 |
+
(split_dir / 'val' / 'images').mkdir(parents=True, exist_ok=True)
|
104 |
+
(split_dir / 'val' / 'labels').mkdir(parents=True, exist_ok=True)
|
105 |
+
|
106 |
+
# create yaml files
|
107 |
+
dataset_yaml = split_dir / f'{split}_dataset.yaml'
|
108 |
+
ds_yamls.append(dataset_yaml)
|
109 |
+
|
110 |
+
with open(dataset_yaml, 'w') as ds_y:
|
111 |
+
yaml.safe_dump({
|
112 |
+
'path' : split_dir.resolve().as_posix(),
|
113 |
+
'train': 'train',
|
114 |
+
'val' : 'val',
|
115 |
+
'names': classes
|
116 |
+
}, ds_y)
|
117 |
+
|
118 |
+
for image, label in zip(images, labels):
|
119 |
+
for split, k_split in folds_df.loc[image.stem].items():
|
120 |
+
# destination directory
|
121 |
+
img_to_path = save_path / split / k_split / 'images'
|
122 |
+
lbl_to_path = save_path / split / k_split / 'labels'
|
123 |
+
|
124 |
+
# copy image and label file to new directory
|
125 |
+
shutil.copy(image, img_to_path / image.name)
|
126 |
+
shutil.copy(label, lbl_to_path / label.name)
|
127 |
+
|
128 |
+
folds_df.to_csv(save_path / "kfold_datasplit.csv")
|
129 |
+
fold_lbl_distrb.to_csv(save_path / "kfold_label_distributions.csv")
|
130 |
+
|
131 |
+
model = YOLO(MODEL)
|
132 |
+
|
133 |
+
for k in range(KSPLIT):
|
134 |
+
dataset_yaml = ds_yamls[k]
|
135 |
+
model.train(
|
136 |
+
data = dataset_yaml,
|
137 |
+
epochs = EPOCHS,
|
138 |
+
batch = BATCH_SIZE,
|
139 |
+
plots = False
|
140 |
+
)
|
141 |
+
|
142 |
+
# azure upload
|
143 |
+
flag = '2' * (KSPLIT - 1)
|
144 |
+
local_fname = f'runs/detect/train{flag}/weights/best.pt'
|
145 |
+
blob_fname = f"kohberg/host_train_{MODEL}"
|
146 |
+
azure_upload(local_fname, blob_fname, overwrite=True)
|
trainer/utils/cvat_dataset.py
ADDED
@@ -0,0 +1,108 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import os
|
2 |
+
import sys
|
3 |
+
import requests
|
4 |
+
import shutil
|
5 |
+
import time
|
6 |
+
from pathlib import Path
|
7 |
+
from tqdm.auto import tqdm
|
8 |
+
|
9 |
+
class CVATDataset:
|
10 |
+
def __init__(self, cvat_url, org, task_ids, headers=None, params=None, names=None, dest_folder=None):
|
11 |
+
"""
|
12 |
+
Connects to serverless CVAT to download datasets.
|
13 |
+
|
14 |
+
Args:
|
15 |
+
cvat_url (str) : CVAT base URL where the server is loaded.
|
16 |
+
org (str) : organization we are working with, e.g.: 'bulow'
|
17 |
+
task_ids (list): list with the task IDs inside CVAT.
|
18 |
+
params (dict): query parameters.
|
19 |
+
names (dict): dict where the keys are the task id and values
|
20 |
+
the names of the local files.
|
21 |
+
dest_folder (str) : destination folder of the zip files.
|
22 |
+
|
23 |
+
Returns:
|
24 |
+
Content ZIP file containing JSON coco annotations and the images.
|
25 |
+
"""
|
26 |
+
self.cvat_url = cvat_url
|
27 |
+
self.org = org
|
28 |
+
self.task_ids = task_ids
|
29 |
+
self.dest_folder = dest_folder
|
30 |
+
self.names_dict = names
|
31 |
+
if self.names_dict is not None:
|
32 |
+
assert all([id_ in self.names_dict.keys() for id_ in self.task_ids]), \
|
33 |
+
"The keys in names do not match the task IDs."
|
34 |
+
|
35 |
+
self.headers = headers
|
36 |
+
if self.headers is None:
|
37 |
+
# FIXME: avoid hardcoded authorization.
|
38 |
+
self.headers = {"Authorization": "Basic ZGphbmdvOlMwbHNraW4xMjM0IQ=="}
|
39 |
+
|
40 |
+
self.params = params
|
41 |
+
if self.params is None:
|
42 |
+
self.params = {
|
43 |
+
"format" : "COCO 1.0",
|
44 |
+
"action" : "download",
|
45 |
+
"location": "local",
|
46 |
+
"org" : self.org
|
47 |
+
}
|
48 |
+
|
49 |
+
@staticmethod
|
50 |
+
def countdown_clock(waiting_time):
|
51 |
+
t0 = time.monotonic()
|
52 |
+
while time.monotonic() - t0 < waiting_time:
|
53 |
+
remaining_time = waiting_time - (time.monotonic() - t0)
|
54 |
+
mins, secs = divmod(int(remaining_time), 60)
|
55 |
+
sys.stdout.write("\r")
|
56 |
+
sys.stdout.write(f"{mins:02d}:{secs:02d}")
|
57 |
+
sys.stdout.flush()
|
58 |
+
time.sleep(1)
|
59 |
+
sys.stdout.write("\n")
|
60 |
+
|
61 |
+
def _get_dataset(self, endpoint):
|
62 |
+
response = requests.get(
|
63 |
+
endpoint,
|
64 |
+
headers = self.headers,
|
65 |
+
params = self.params,
|
66 |
+
stream = True
|
67 |
+
)
|
68 |
+
return response
|
69 |
+
|
70 |
+
def _download_task(self, task_id: int, fname: str):
|
71 |
+
""" Downloads dataset linked to a task. """
|
72 |
+
endpoint = f"{self.cvat_url}/api/tasks/{task_id}/dataset"
|
73 |
+
r = self._get_dataset(endpoint)
|
74 |
+
while r.status_code != 200:
|
75 |
+
if r.status_code == 202:
|
76 |
+
print(f" Status code {r.status_code}: server processing request")
|
77 |
+
self.countdown_clock(10)
|
78 |
+
else:
|
79 |
+
print(f" Status code {r.status_code}: connection error")
|
80 |
+
self.countdown_clock(30)
|
81 |
+
r = self._get_dataset(endpoint)
|
82 |
+
|
83 |
+
print(f" Status code {r.status_code}: request is ready")
|
84 |
+
total_length = int(r.headers.get("Content-Length"))
|
85 |
+
with tqdm.wrapattr(r.raw, "read", total=total_length, desc="") as raw:
|
86 |
+
with open(fname, "wb") as file:
|
87 |
+
shutil.copyfileobj(raw, file)
|
88 |
+
|
89 |
+
def download_tasks(self):
|
90 |
+
""" Download all the tasks passed as input. """
|
91 |
+
for task_id in self.task_ids:
|
92 |
+
name_label = task_id
|
93 |
+
if self.names_dict is not None:
|
94 |
+
name_label = self.names_dict[task_id]
|
95 |
+
fname = f"dataset_{name_label}.zip"
|
96 |
+
if self.dest_folder is not None:
|
97 |
+
self.dest_folder = Path(self.dest_folder)
|
98 |
+
self.dest_folder.mkdir(exist_ok=True, parents=True)
|
99 |
+
fname = (self.dest_folder / fname).resolve().as_posix()
|
100 |
+
|
101 |
+
if os.path.exists(fname):
|
102 |
+
print(f"File {fname} already exists.")
|
103 |
+
continue
|
104 |
+
|
105 |
+
print(f"\nDownloading task {task_id}, with fname {fname}")
|
106 |
+
self._download_task(task_id, fname)
|
107 |
+
|
108 |
+
# TODO: implement unzip function for the tasks
|
trainer/utils/download_cvatdata.py
ADDED
@@ -0,0 +1,98 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
This script reads from a YAML file and downloads data from CVAT.
|
3 |
+
"""
|
4 |
+
|
5 |
+
import os
|
6 |
+
import argparse
|
7 |
+
import subprocess
|
8 |
+
import shutil
|
9 |
+
import yaml
|
10 |
+
from pathlib import Path
|
11 |
+
from cvat_dataset import CVATDataset
|
12 |
+
from merge_cocos import merge
|
13 |
+
from yolo_labels import get_yolo_labels
|
14 |
+
|
15 |
+
HOME = os.getenv("APP_HOME")
|
16 |
+
CVAT_TASKS = os.path.join(HOME, os.getenv("APP_CVAT_TASKS_YAML"))
|
17 |
+
PYPREPROCESS = os.getenv("APP_PYPREPROCESS")
|
18 |
+
import sys
|
19 |
+
sys.path.append(HOME)
|
20 |
+
|
21 |
+
if __name__ == "__main__":
|
22 |
+
parser = argparse.ArgumentParser()
|
23 |
+
parser.add_argument(
|
24 |
+
'cvat_url',
|
25 |
+
type = str,
|
26 |
+
help = 'cvat url'
|
27 |
+
)
|
28 |
+
parser.add_argument(
|
29 |
+
'cvat_org',
|
30 |
+
type = str,
|
31 |
+
help = 'cvat organization'
|
32 |
+
)
|
33 |
+
parser.add_argument(
|
34 |
+
'-odir', '--output_dir',
|
35 |
+
type = str,
|
36 |
+
help = "path to download directory",
|
37 |
+
default = "/data"
|
38 |
+
)
|
39 |
+
args = parser.parse_args()
|
40 |
+
|
41 |
+
with open(CVAT_TASKS, "r") as f:
|
42 |
+
y = yaml.safe_load(f)
|
43 |
+
TASK_IDS = y["task_ids"]
|
44 |
+
NAMES = None
|
45 |
+
if "names" in y:
|
46 |
+
NAMES = y["names"]
|
47 |
+
|
48 |
+
data_folder = Path(args.output_dir)
|
49 |
+
data_folder.mkdir(parents=True, exist_ok=True)
|
50 |
+
|
51 |
+
CVAT = CVATDataset(
|
52 |
+
args.cvat_url,
|
53 |
+
args.cvat_org,
|
54 |
+
TASK_IDS,
|
55 |
+
names = NAMES,
|
56 |
+
dest_folder = data_folder
|
57 |
+
)
|
58 |
+
CVAT.download_tasks()
|
59 |
+
|
60 |
+
paths2imgs = []
|
61 |
+
paths2json = []
|
62 |
+
paths2dirs = []
|
63 |
+
for dataset in data_folder.rglob("*.zip"):
|
64 |
+
dir_name = dataset.parent / dataset.stem
|
65 |
+
paths2dirs.append(dir_name)
|
66 |
+
paths2imgs.append(dir_name / "images")
|
67 |
+
paths2json.append(dir_name / "annotations" / "instances_default.json")
|
68 |
+
if dir_name.exists():
|
69 |
+
continue
|
70 |
+
subprocess.call(['unzip', '-o', dataset, '-d', dir_name])
|
71 |
+
|
72 |
+
if PYPREPROCESS == 'true':
|
73 |
+
# looks for the py script called: trainer_files/preprocess.py
|
74 |
+
# this script is characteristic to the project
|
75 |
+
from trainer_files.preprocess import preprocess_cvat
|
76 |
+
paths2json, paths2imgs = preprocess_cvat(paths2dirs)
|
77 |
+
|
78 |
+
# TODO: add debugging / assert script to make sure preprocess is done correctly
|
79 |
+
|
80 |
+
# merge everything into a single json file
|
81 |
+
if len(paths2json) > 1:
|
82 |
+
merge(
|
83 |
+
paths2json, paths2imgs, data_folder / 'merged_cocos', 'merged', verbose=True
|
84 |
+
)
|
85 |
+
else:
|
86 |
+
json_file = Path(paths2json[0])
|
87 |
+
shutil.copy(
|
88 |
+
json_file.as_posix(),
|
89 |
+
(json_file.parents[1] / 'merged.json').as_posix()
|
90 |
+
)
|
91 |
+
shutil.move(
|
92 |
+
json_file.parents[1].as_posix(),
|
93 |
+
(data_folder / 'merged_cocos').as_posix()
|
94 |
+
)
|
95 |
+
|
96 |
+
# yolo format - labels
|
97 |
+
path2json = data_folder / 'merged_cocos' / 'merged.json'
|
98 |
+
get_yolo_labels(path2json, use_segment=False)
|
trainer/utils/merge_cocos.py
ADDED
@@ -0,0 +1,98 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import os
|
2 |
+
import glob
|
3 |
+
from pathlib import Path
|
4 |
+
from datetime import date
|
5 |
+
from collections import defaultdict
|
6 |
+
from warnings import warn
|
7 |
+
|
8 |
+
from path_utils import *
|
9 |
+
|
10 |
+
def merge_cats_get_id(cats, this_cat):
|
11 |
+
cat_nms = [c['name'] for c in cats]
|
12 |
+
if this_cat['name'] not in cat_nms:
|
13 |
+
this_cat['id'] = len(cats) + 1
|
14 |
+
cats.append(this_cat)
|
15 |
+
return this_cat["id"]
|
16 |
+
else:
|
17 |
+
return this_cat["id"]
|
18 |
+
|
19 |
+
|
20 |
+
def filter_images(images, annotations):
|
21 |
+
img_ids_from_anns = [ann['image_id'] for ann in annotations]
|
22 |
+
images_ = [
|
23 |
+
img_info for img_info in images if img_info['id'] in img_ids_from_anns
|
24 |
+
]
|
25 |
+
return images_
|
26 |
+
|
27 |
+
|
28 |
+
def merge(jsons, img_roots, output_dir, output_nm="merged", verbose=True):
|
29 |
+
assert len(jsons) == len(img_roots)
|
30 |
+
|
31 |
+
out_dir_path = Path(output_dir)
|
32 |
+
out_imgs_dir_path = out_dir_path / "images"
|
33 |
+
|
34 |
+
merged_img_id_state = 1
|
35 |
+
merged_ann_id_state = 1
|
36 |
+
merged_names = []
|
37 |
+
merged_dict = {
|
38 |
+
"info" : {"description": "", "data_created": f"{date.today():%Y/%m/%d}"},
|
39 |
+
"annotations": [],
|
40 |
+
"categories" : [],
|
41 |
+
"images" : []
|
42 |
+
}
|
43 |
+
for i, (json_path, imgs_dir_path) in enumerate(zip(jsons, img_roots)):
|
44 |
+
coco_dict = read_coco_json(json_path)
|
45 |
+
dataset_name = get_setname(json_path)
|
46 |
+
merged_names.append(dataset_name)
|
47 |
+
|
48 |
+
# categories
|
49 |
+
cat_id_old2new = {}
|
50 |
+
for cat in coco_dict['categories']:
|
51 |
+
old_cat_id = cat['id']
|
52 |
+
new_cat_id = merge_cats_get_id(merged_dict['categories'], cat)
|
53 |
+
cat_id_old2new[old_cat_id] = new_cat_id
|
54 |
+
|
55 |
+
# images
|
56 |
+
coco_dict['images'] = filter_images(
|
57 |
+
coco_dict['images'], coco_dict['annotations']
|
58 |
+
)
|
59 |
+
img_id_old2new = {}
|
60 |
+
for img in coco_dict['images']:
|
61 |
+
img_id_old2new[img["id"]] = merged_img_id_state
|
62 |
+
img["id"] = merged_img_id_state
|
63 |
+
|
64 |
+
old_img_path = Path(imgs_dir_path) / img['file_name']
|
65 |
+
img['file_name'] = dataset_name + "_" + img['file_name']
|
66 |
+
new_img_path = out_imgs_dir_path / img['file_name']
|
67 |
+
assure_copy(old_img_path, new_img_path)
|
68 |
+
|
69 |
+
merged_img_id_state += 1
|
70 |
+
merged_dict['images'].append(img)
|
71 |
+
|
72 |
+
# annotations
|
73 |
+
for ann in coco_dict['annotations']:
|
74 |
+
ann['id'] = merged_ann_id_state
|
75 |
+
ann['image_id'] = img_id_old2new[ann['image_id']]
|
76 |
+
ann['category_id'] = cat_id_old2new[ann['category_id']]
|
77 |
+
|
78 |
+
merged_ann_id_state += 1
|
79 |
+
merged_dict['annotations'].append(ann)
|
80 |
+
|
81 |
+
merged_dict["info"]["description"] = "+".join(merged_names)
|
82 |
+
|
83 |
+
out_json = out_dir_path / f"{output_nm}.json"
|
84 |
+
write_json(out_json, merged_dict)
|
85 |
+
|
86 |
+
if verbose:
|
87 |
+
print(f"Number of images: {len(merged_dict['images'])}")
|
88 |
+
print(f"Number of annotations: {len(merged_dict['annotations'])}")
|
89 |
+
|
90 |
+
|
91 |
+
if __name__ == '__main__':
|
92 |
+
paths2images = []
|
93 |
+
paths2json = []
|
94 |
+
for dataset in glob.glob("dataset_*"):
|
95 |
+
paths2images.append(os.path.join(dataset, "images"))
|
96 |
+
paths2json.append(os.path.join(dataset, "annotations/instances_default.json"))
|
97 |
+
|
98 |
+
merge(paths2json, paths2images, './merged_cocos', 'merged', verbose=True)
|
trainer/utils/path_utils.py
ADDED
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import json
|
2 |
+
import filecmp
|
3 |
+
from pathlib import Path
|
4 |
+
from shutil import copy
|
5 |
+
|
6 |
+
|
7 |
+
def read_json(json_path):
|
8 |
+
with open(json_path, "r") as f:
|
9 |
+
d = json.load(f)
|
10 |
+
return d
|
11 |
+
|
12 |
+
|
13 |
+
def write_json(json_path, dic):
|
14 |
+
with open(json_path, "w") as f:
|
15 |
+
json.dump(dic, f)
|
16 |
+
print(f"Wrote json to {json_path}")
|
17 |
+
|
18 |
+
|
19 |
+
def get_setname(json_path):
|
20 |
+
json_path_ = Path(json_path)
|
21 |
+
dataset_nm = json_path_.parent.parts[-2]
|
22 |
+
print(f"Processing {dataset_nm} (name derived from json path)")
|
23 |
+
return dataset_nm
|
24 |
+
|
25 |
+
|
26 |
+
def read_coco_json(coco_json):
|
27 |
+
coco_dict = read_json(coco_json)
|
28 |
+
return coco_dict
|
29 |
+
|
30 |
+
|
31 |
+
def assure_copy(src, dst):
|
32 |
+
assert Path(src).is_file()
|
33 |
+
if Path(dst).is_file() and filecmp.cmp(src, dst, shallow=True):
|
34 |
+
return
|
35 |
+
Path(dst).parent.mkdir(exist_ok=True, parents=True)
|
36 |
+
copy(src, dst)
|
37 |
+
|
38 |
+
|
39 |
+
def path(str_path, is_dir=False, mkdir=False):
|
40 |
+
path_ = Path(str_path)
|
41 |
+
if is_dir:
|
42 |
+
if mkdir:
|
43 |
+
path_.mkdir(parents=True, exist_ok=True)
|
44 |
+
assert path_.is_dir(), path_
|
45 |
+
else:
|
46 |
+
assert path_.is_file(), path_
|
47 |
+
return path_
|
48 |
+
|
trainer/utils/unzip_datasets.py
ADDED
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import subprocess
|
2 |
+
import glob
|
3 |
+
|
4 |
+
if __name__ == "__main__":
|
5 |
+
for dataset in glob.glob("*.zip"):
|
6 |
+
dir_name = dataset.split(".")[0]
|
7 |
+
subprocess.call(['unzip', '-o', dataset, '-d', dir_name])
|
trainer/utils/yolo_labels.py
ADDED
@@ -0,0 +1,128 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import numpy as np
|
2 |
+
import json
|
3 |
+
from pathlib import Path, PosixPath
|
4 |
+
from pycocotools.coco import COCO
|
5 |
+
|
6 |
+
def min_index(arr1, arr2):
|
7 |
+
"""
|
8 |
+
Find a pair of indexes with the shortest distance.
|
9 |
+
|
10 |
+
Args:
|
11 |
+
arr1: (N, 2).
|
12 |
+
arr2: (M, 2).
|
13 |
+
Return:
|
14 |
+
a pair of indexes (tuple)
|
15 |
+
"""
|
16 |
+
dis = ((arr1[:, None, :] - arr2[None, :, :]) ** 2).sum(-1)
|
17 |
+
return np.unravel_index(np.argmin(dis, axis=None), dis.shape)
|
18 |
+
|
19 |
+
|
20 |
+
def merge_multi_segment(segments):
|
21 |
+
"""
|
22 |
+
Merge multi segments to one list.
|
23 |
+
Find coordinates with min distance between each segment,
|
24 |
+
then connect these coordinates with one thin line to merge all
|
25 |
+
segments into one.
|
26 |
+
|
27 |
+
Args:
|
28 |
+
segments (List(List)): original segmentations in coco's json file
|
29 |
+
like [segmentation1, segmentation2, ...], where
|
30 |
+
each segmentation is a list of coordinates
|
31 |
+
"""
|
32 |
+
s = []
|
33 |
+
segments = [np.array(i).reshape(-1,2) for i in segments]
|
34 |
+
idx_list = [[] for _ in range(len(segments))]
|
35 |
+
|
36 |
+
# record the indexes with the min distance between each segment
|
37 |
+
for i in range(1, len(segments)):
|
38 |
+
idx1, idx2 = min_index(segments[i - 1, segments[i]])
|
39 |
+
idx_list[i - 1].append(idx1)
|
40 |
+
idx_list[i].append(idx2)
|
41 |
+
|
42 |
+
# use two round to connect all the segments
|
43 |
+
for k in range(2):
|
44 |
+
# forward connection
|
45 |
+
if k == 0:
|
46 |
+
for i, idx in enumerate(idx_list):
|
47 |
+
# middle segments have two indexes
|
48 |
+
# reverse the index of middle segments
|
49 |
+
if len(idx) == 2 and idx[0] > idx[1]:
|
50 |
+
idx = idx[::-1]
|
51 |
+
segments[i] = segments[i][::-1, :]
|
52 |
+
|
53 |
+
segments[i] = np.roll(segments[i], -idx[0], axis=0)
|
54 |
+
segments[i] = np.concatenate([segments[i], segments[i][:1]])
|
55 |
+
|
56 |
+
# deal with the first segment and the last one
|
57 |
+
if i in [0, len(idx_list) - 1]:
|
58 |
+
s.append(segments[i])
|
59 |
+
else:
|
60 |
+
idx = [0, idx[1] - idx[0]]
|
61 |
+
s.append(segments[i][idx[0]:idx[1] + 1])
|
62 |
+
|
63 |
+
else:
|
64 |
+
for i in range(len(idx_list) - 1, -1, -1):
|
65 |
+
if i not in [0, len(idx_list) - 1]:
|
66 |
+
idx = idx_list[i]
|
67 |
+
nidx = abs(idx[1] - idx[0])
|
68 |
+
s.append(segments[i][nidx:])
|
69 |
+
return s
|
70 |
+
|
71 |
+
|
72 |
+
def get_yolo_labels(path2json, use_segment=False):
|
73 |
+
if not isinstance(path2json, PosixPath):
|
74 |
+
path2json = Path(path2json)
|
75 |
+
path2labels = path2json.parents[0] / "labels"
|
76 |
+
path2labels.mkdir(parents=True, exist_ok=True)
|
77 |
+
|
78 |
+
coco = COCO(path2json)
|
79 |
+
|
80 |
+
img2anns = {}
|
81 |
+
for ann in coco.dataset['annotations']:
|
82 |
+
img_id = ann['image_id']
|
83 |
+
if img_id not in img2anns:
|
84 |
+
img2anns[img_id] = [ann]
|
85 |
+
else:
|
86 |
+
img2anns[img_id].append(ann)
|
87 |
+
|
88 |
+
id2img = {img["id"]: img for img in coco.dataset["images"]}
|
89 |
+
|
90 |
+
for img_id, anns in img2anns.items():
|
91 |
+
img = id2img[img_id]
|
92 |
+
h, w, f = img['height'], img['width'], img['file_name']
|
93 |
+
|
94 |
+
bboxes = []
|
95 |
+
segments = []
|
96 |
+
for ann in anns:
|
97 |
+
if ann['iscrowd']:
|
98 |
+
continue
|
99 |
+
# coco box format: [top left x, top left y, width, height]
|
100 |
+
box = np.array(ann['bbox'], dtype=np.float64)
|
101 |
+
box[:2] += box[2:] / 2 # center coordinates
|
102 |
+
box[[0, 2]] /= w # normalize x
|
103 |
+
box[[1, 3]] /= h # normalize y
|
104 |
+
if box[2] <= 0 or box[3] <= 0:
|
105 |
+
continue
|
106 |
+
|
107 |
+
cls = ann['category_id'] - 1
|
108 |
+
box = [cls] + box.tolist()
|
109 |
+
if box not in bboxes:
|
110 |
+
bboxes.append(box)
|
111 |
+
|
112 |
+
# segmentation?
|
113 |
+
if use_segment:
|
114 |
+
if len(ann['segmentation']) > 1:
|
115 |
+
s = merge_multi_segment(ann['segmentation'])
|
116 |
+
s = (np.concatenate(s, axis=0) / np.array([w, h])).reshape(-1).tolist()
|
117 |
+
else:
|
118 |
+
s = [j for i in ann['segmentation'] for j in i] # all segments concatenated
|
119 |
+
s = (np.array(s).reshape(-1, 2) / np.array([w, h])).reshape(-1).tolist()
|
120 |
+
s = [cls] + s
|
121 |
+
if s not in segments:
|
122 |
+
segments.append(s)
|
123 |
+
|
124 |
+
# write
|
125 |
+
with open((path2labels / f).with_suffix('.txt'), 'a') as file:
|
126 |
+
for i in range(len(bboxes)):
|
127 |
+
line = *(segments[i] if use_segment else bboxes[i]),
|
128 |
+
file.write(('%g ' * len(line)).rstrip() % line + '\n')
|