Skip to content

Commit 5458de7

Browse files
zhiltsov-maxnmanovic
authored andcommitted
[Datumaro] Add YOLO converter (#906)
* Add YOLO converter * Added yolo extractor * Added YOLO format test * Add YOLO export in UI
1 parent 1af9105 commit 5458de7

File tree

11 files changed

+410
-13
lines changed

11 files changed

+410
-13
lines changed

cvat/apps/dataset_manager/task.py

+6-1
Original file line numberDiff line numberDiff line change
@@ -379,7 +379,12 @@ def clear_export_cache(task_id, file_path, file_ctime):
379379
'name': 'MS COCO',
380380
'tag': 'coco',
381381
'is_default': False,
382-
}
382+
},
383+
{
384+
'name': 'YOLO',
385+
'tag': 'yolo',
386+
'is_default': False,
387+
},
383388
]
384389

385390
def get_export_formats():

datumaro/datumaro/components/converters/__init__.py

+4
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323
VocSegmentationConverter,
2424
)
2525

26+
from datumaro.components.converters.yolo import YoloConverter
27+
2628

2729
items = [
2830
('datumaro', DatumaroConverter),
@@ -40,4 +42,6 @@
4042
('voc_segm', VocSegmentationConverter),
4143
('voc_action', VocActionConverter),
4244
('voc_layout', VocLayoutConverter),
45+
46+
('yolo', YoloConverter),
4347
]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
2+
# Copyright (C) 2019 Intel Corporation
3+
#
4+
# SPDX-License-Identifier: MIT
5+
6+
from collections import OrderedDict
7+
import logging as log
8+
import os
9+
import os.path as osp
10+
11+
from datumaro.components.converter import Converter
12+
from datumaro.components.extractor import AnnotationType
13+
from datumaro.components.formats.yolo import YoloPath
14+
from datumaro.util.image import save_image
15+
16+
17+
def _make_yolo_bbox(img_size, box):
18+
# https://github.com/pjreddie/darknet/blob/master/scripts/voc_label.py
19+
# <x> <y> <width> <height> - values relative to width and height of image
20+
# <x> <y> - are center of rectangle
21+
x = (box[0] + box[2]) / 2 / img_size[0]
22+
y = (box[1] + box[3]) / 2 / img_size[1]
23+
w = (box[2] - box[0]) / img_size[0]
24+
h = (box[3] - box[1]) / img_size[1]
25+
return x, y, w, h
26+
27+
class YoloConverter(Converter):
28+
# https://github.com/AlexeyAB/darknet#how-to-train-to-detect-your-custom-objects
29+
30+
def __init__(self, task=None, save_images=False, apply_colormap=False):
31+
super().__init__()
32+
self._task = task
33+
self._save_images = save_images
34+
self._apply_colormap = apply_colormap
35+
36+
def __call__(self, extractor, save_dir):
37+
os.makedirs(save_dir, exist_ok=True)
38+
39+
label_categories = extractor.categories()[AnnotationType.label]
40+
label_ids = {label.name: idx
41+
for idx, label in enumerate(label_categories.items)}
42+
with open(osp.join(save_dir, 'obj.names'), 'w') as f:
43+
f.writelines('%s\n' % l[0]
44+
for l in sorted(label_ids.items(), key=lambda x: x[1]))
45+
46+
subsets = extractor.subsets()
47+
if len(subsets) == 0:
48+
subsets = [ None ]
49+
50+
subset_lists = OrderedDict()
51+
52+
for subset_name in subsets:
53+
if subset_name and subset_name in YoloPath.SUBSET_NAMES:
54+
subset = extractor.get_subset(subset_name)
55+
elif not subset_name:
56+
subset_name = YoloPath.DEFAULT_SUBSET_NAME
57+
subset = extractor
58+
else:
59+
log.warn("Skipping subset export '%s'. "
60+
"If specified, the only valid names are %s" % \
61+
(subset_name, ', '.join(
62+
"'%s'" % s for s in YoloPath.SUBSET_NAMES)))
63+
continue
64+
65+
subset_dir = osp.join(save_dir, 'obj_%s_data' % subset_name)
66+
os.makedirs(subset_dir, exist_ok=True)
67+
68+
image_paths = OrderedDict()
69+
70+
for item in subset:
71+
image_name = '%s.jpg' % item.id
72+
image_paths[item.id] = osp.join('data',
73+
osp.basename(subset_dir), image_name)
74+
75+
if self._save_images:
76+
image_path = osp.join(subset_dir, image_name)
77+
if not osp.exists(image_path):
78+
save_image(image_path, item.image)
79+
80+
height, width, _ = item.image.shape
81+
82+
yolo_annotation = ''
83+
for bbox in item.annotations:
84+
if bbox.type is not AnnotationType.bbox:
85+
continue
86+
if bbox.label is None:
87+
continue
88+
89+
yolo_bb = _make_yolo_bbox((width, height), bbox.points)
90+
yolo_bb = ' '.join('%.6f' % p for p in yolo_bb)
91+
yolo_annotation += '%s %s\n' % (bbox.label, yolo_bb)
92+
93+
annotation_path = osp.join(subset_dir, '%s.txt' % item.id)
94+
with open(annotation_path, 'w') as f:
95+
f.write(yolo_annotation)
96+
97+
subset_list_name = '%s.txt' % subset_name
98+
subset_lists[subset_name] = subset_list_name
99+
with open(osp.join(save_dir, subset_list_name), 'w') as f:
100+
f.writelines('%s\n' % s for s in image_paths.values())
101+
102+
with open(osp.join(save_dir, 'obj.data'), 'w') as f:
103+
f.write('classes = %s\n' % len(label_ids))
104+
105+
for subset_name, subset_list_name in subset_lists.items():
106+
f.write('%s = %s\n' % (subset_name,
107+
osp.join('data', subset_list_name)))
108+
109+
f.write('names = %s\n' % osp.join('data', 'obj.names'))
110+
f.write('backup = backup/\n')

datumaro/datumaro/components/extractors/__init__.py

+5
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@
2626
VocComp_9_10_Extractor,
2727
)
2828

29+
from datumaro.components.extractors.yolo import (
30+
YoloExtractor,
31+
)
2932

3033
items = [
3134
('datumaro', DatumaroExtractor),
@@ -47,4 +50,6 @@
4750
('voc_comp_5_6', VocComp_5_6_Extractor),
4851
('voc_comp_7_8', VocComp_7_8_Extractor),
4952
('voc_comp_9_10', VocComp_9_10_Extractor),
53+
54+
('yolo', YoloExtractor),
5055
]

datumaro/datumaro/components/extractors/ms_coco.py

+6-5
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
#
44
# SPDX-License-Identifier: MIT
55

6+
from collections import OrderedDict
67
import numpy as np
78
import os.path as osp
89

@@ -49,7 +50,7 @@ def __init__(self, name, parent):
4950
self._name = name
5051
self._parent = parent
5152
self.loaders = {}
52-
self.items = set()
53+
self.items = OrderedDict()
5354

5455
def __iter__(self):
5556
for img_id in self.items:
@@ -75,7 +76,7 @@ def __init__(self, path, task, merge_instance_polygons=False):
7576
loader = self._make_subset_loader(path)
7677
subset.loaders[task] = loader
7778
for img_id in loader.getImgIds():
78-
subset.items.add(img_id)
79+
subset.items[img_id] = None
7980
self._subsets[subset_name] = subset
8081

8182
self._load_categories()
@@ -151,9 +152,9 @@ def categories(self):
151152
return self._categories
152153

153154
def __iter__(self):
154-
for subset_name, subset in self._subsets.items():
155-
for img_id in subset.items:
156-
yield self._get(img_id, subset_name)
155+
for subset in self._subsets.values():
156+
for item in subset:
157+
yield item
157158

158159
def __len__(self):
159160
length = 0

datumaro/datumaro/components/extractors/voc.py

+6-6
Original file line numberDiff line numberDiff line change
@@ -137,9 +137,9 @@ def categories(self):
137137
return self._categories
138138

139139
def __iter__(self):
140-
for subset_name, subset in self._subsets.items():
141-
for item in subset.items:
142-
yield self._get(item, subset_name)
140+
for subset in self._subsets.values():
141+
for item in subset:
142+
yield item
143143

144144
def _get(self, item, subset_name):
145145
image = None
@@ -468,9 +468,9 @@ def categories(self):
468468
return self._categories
469469

470470
def __iter__(self):
471-
for subset_name, subset in self._subsets.items():
472-
for item in subset.items:
473-
yield self._get(item, subset_name)
471+
for subset in self._subsets.values():
472+
for item in subset:
473+
yield item
474474

475475
def _get(self, item, subset_name):
476476
image = None
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
2+
# Copyright (C) 2019 Intel Corporation
3+
#
4+
# SPDX-License-Identifier: MIT
5+
6+
from collections import OrderedDict
7+
import os.path as osp
8+
import re
9+
10+
from datumaro.components.extractor import (Extractor, DatasetItem,
11+
AnnotationType, BboxObject, LabelCategories
12+
)
13+
from datumaro.components.formats.yolo import YoloPath
14+
from datumaro.util.image import lazy_image
15+
16+
17+
class YoloExtractor(Extractor):
18+
class Subset(Extractor):
19+
def __init__(self, name, parent):
20+
super().__init__()
21+
self._name = name
22+
self._parent = parent
23+
self.items = OrderedDict()
24+
25+
def __iter__(self):
26+
for item_id in self.items:
27+
yield self._parent._get(item_id, self._name)
28+
29+
def __len__(self):
30+
return len(self.items)
31+
32+
def categories(self):
33+
return self._parent.categories()
34+
35+
def __init__(self, config_path):
36+
super().__init__()
37+
38+
if not osp.isfile(config_path):
39+
raise Exception("Can't read dataset descriptor file '%s'" % \
40+
config_path)
41+
42+
rootpath = osp.dirname(config_path)
43+
self._path = rootpath
44+
45+
with open(config_path, 'r') as f:
46+
config_lines = f.readlines()
47+
48+
subsets = OrderedDict()
49+
names_path = None
50+
51+
for line in config_lines:
52+
match = re.match(r'(\w+)\s*=\s*(.+)$', line)
53+
if not match:
54+
continue
55+
56+
key = match.group(1)
57+
value = match.group(2)
58+
if key == 'names':
59+
names_path = value
60+
elif key in YoloPath.SUBSET_NAMES:
61+
subsets[key] = value
62+
else:
63+
continue
64+
65+
if not names_path:
66+
raise Exception("Failed to parse labels path from '%s'" % \
67+
config_path)
68+
69+
for subset_name, list_path in subsets.items():
70+
list_path = self._make_local_path(list_path)
71+
if not osp.isfile(list_path):
72+
raise Exception("Not found '%s' subset list file" % subset_name)
73+
74+
subset = YoloExtractor.Subset(subset_name, self)
75+
with open(list_path, 'r') as f:
76+
subset.items = OrderedDict(
77+
(osp.splitext(osp.basename(p))[0], p.strip()) for p in f)
78+
79+
for image_path in subset.items.values():
80+
image_path = self._make_local_path(image_path)
81+
if not osp.isfile(image_path):
82+
raise Exception("Can't find image '%s'" % image_path)
83+
84+
subsets[subset_name] = subset
85+
86+
self._subsets = subsets
87+
88+
self._categories = {
89+
AnnotationType.label:
90+
self._load_categories(self._make_local_path(names_path))
91+
}
92+
93+
def _make_local_path(self, path):
94+
default_base = osp.join('data', '')
95+
if path.startswith(default_base): # default path
96+
path = path[len(default_base) : ]
97+
return osp.join(self._path, path) # relative or absolute path
98+
99+
def _get(self, item_id, subset_name):
100+
subset = self._subsets[subset_name]
101+
item = subset.items[item_id]
102+
103+
if isinstance(item, str):
104+
image_path = self._make_local_path(item)
105+
image = lazy_image(image_path)
106+
h, w, _ = image().shape
107+
anno_path = osp.splitext(image_path)[0] + '.txt'
108+
annotations = self._parse_annotations(anno_path, w, h)
109+
110+
item = DatasetItem(id=item_id, subset=subset_name,
111+
image=image, annotations=annotations)
112+
subset.items[item_id] = item
113+
114+
return item
115+
116+
@staticmethod
117+
def _parse_annotations(anno_path, image_width, image_height):
118+
with open(anno_path, 'r') as f:
119+
annotations = []
120+
for line in f:
121+
label_id, xc, yc, w, h = line.strip().split()
122+
label_id = int(label_id)
123+
w = float(w)
124+
h = float(h)
125+
x = float(xc) - w * 0.5
126+
y = float(yc) - h * 0.5
127+
annotations.append(BboxObject(
128+
x * image_width, y * image_height,
129+
w * image_width, h * image_height,
130+
label=label_id
131+
))
132+
return annotations
133+
134+
@staticmethod
135+
def _load_categories(names_path):
136+
label_categories = LabelCategories()
137+
138+
with open(names_path, 'r') as f:
139+
for label in f:
140+
label_categories.add(label)
141+
142+
return label_categories
143+
144+
def categories(self):
145+
return self._categories
146+
147+
def __iter__(self):
148+
for subset in self._subsets.values():
149+
for item in subset:
150+
yield item
151+
152+
def __len__(self):
153+
length = 0
154+
for subset in self._subsets.values():
155+
length += len(subset)
156+
return length
157+
158+
def subsets(self):
159+
return list(self._subsets)
160+
161+
def get_subset(self, name):
162+
return self._subsets[name]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
2+
# Copyright (C) 2019 Intel Corporation
3+
#
4+
# SPDX-License-Identifier: MIT
5+
6+
7+
class YoloPath:
8+
DEFAULT_SUBSET_NAME = 'train'
9+
SUBSET_NAMES = ['train', 'valid']

0 commit comments

Comments
 (0)