Skip to content

Commit fb380d9

Browse files
authored
Include empty images in exported annotations (#1479)
1 parent 98a9718 commit fb380d9

File tree

7 files changed

+150
-96
lines changed

7 files changed

+150
-96
lines changed

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1111

1212
### Changed
1313
- Downloaded file name in annotations export became more informative (https://github.com/opencv/cvat/pull/1352)
14-
- Added auto trimming for trailing whitespaces style enforsement (https://github.com/opencv/cvat/pull/1352)
14+
- Added auto trimming for trailing whitespaces style enforcement (https://github.com/opencv/cvat/pull/1352)
1515
- REST API: updated `GET /task/<id>/annotations`: parameters are `format`, `filename` (now optional), `action` (optional) (https://github.com/opencv/cvat/pull/1352)
1616
- REST API: removed `dataset/formats`, changed format of `annotation/formats` (https://github.com/opencv/cvat/pull/1352)
1717
- Exported annotations are stored for N hours instead of indefinitely (https://github.com/opencv/cvat/pull/1352)
@@ -20,6 +20,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2020
- Formats: most of formats renamed, no extension in title (https://github.com/opencv/cvat/pull/1352)
2121
- Formats: definitions are changed, are not stored in DB anymore (https://github.com/opencv/cvat/pull/1352)
2222
- cvat-core: session.annotations.put() now returns identificators of added objects (https://github.com/opencv/cvat/pull/1493)
23+
- Images without annotations now also included in dataset/annotations export (https://github.com/opencv/cvat/issues/525)
2324

2425
### Deprecated
2526
-

cvat/apps/dataset_manager/bindings.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -413,7 +413,7 @@ def __init__(self, task_data, include_images=False):
413413
if include_images:
414414
frame_provider = FrameProvider(task_data.db_task.data)
415415

416-
for frame_data in task_data.group_by_frame(include_empty=include_images):
416+
for frame_data in task_data.group_by_frame(include_empty=True):
417417
loader = None
418418
if include_images:
419419
loader = lambda p, i=frame_data.idx: frame_provider.get_frame(i,

cvat/apps/dataset_manager/formats/cvat.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,7 @@ def dump_as_cvat_annotation(file_object, annotations):
175175
dumper.open_root()
176176
dumper.add_meta(annotations.meta)
177177

178-
for frame_annotation in annotations.group_by_frame():
178+
for frame_annotation in annotations.group_by_frame(include_empty=True):
179179
frame_id = frame_annotation.frame
180180
dumper.open_image(OrderedDict([
181181
("id", str(frame_id)),

cvat/apps/dataset_manager/formats/mot.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ def _import(src_file, task_data):
3939
label_cat = dataset.categories()[datumaro.AnnotationType.label]
4040

4141
for item in dataset:
42+
item = item.wrap(id=int(item.id) - 1) # NOTE: MOT frames start from 1
4243
frame_id = match_frame(item, task_data)
4344

4445
for ann in item.annotations:

cvat/apps/dataset_manager/tests/_test_formats.py

Lines changed: 121 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,9 @@ def _setUpModule():
5151
import cvat.apps.dataset_manager as dm
5252
globals()['dm'] = dm
5353

54+
import datumaro
55+
globals()['datumaro'] = datumaro
56+
5457
import sys
5558
sys.path.insert(0, __file__[:__file__.rfind('/dataset_manager/')])
5659

@@ -61,6 +64,7 @@ def _setUpModule():
6164
import os.path as osp
6265
import random
6366
import tempfile
67+
import zipfile
6468

6569
from PIL import Image
6670
from django.contrib.auth.models import User, Group
@@ -113,38 +117,7 @@ def setUp(self):
113117
def setUpTestData(cls):
114118
create_db_users(cls)
115119

116-
def _generate_task(self):
117-
task = {
118-
"name": "my task #1",
119-
"owner": '',
120-
"assignee": '',
121-
"overlap": 0,
122-
"segment_size": 100,
123-
"z_order": False,
124-
"labels": [
125-
{
126-
"name": "car",
127-
"attributes": [
128-
{
129-
"name": "model",
130-
"mutable": False,
131-
"input_type": "select",
132-
"default_value": "mazda",
133-
"values": ["bmw", "mazda", "renault"]
134-
},
135-
{
136-
"name": "parked",
137-
"mutable": True,
138-
"input_type": "checkbox",
139-
"default_value": False
140-
},
141-
]
142-
},
143-
{"name": "person"},
144-
]
145-
}
146-
task = self._create_task(task, 3)
147-
120+
def _generate_annotations(self, task):
148121
annotations = {
149122
"version": 0,
150123
"tags": [
@@ -256,8 +229,39 @@ def _generate_task(self):
256229
]
257230
}
258231
self._put_api_v1_task_id_annotations(task["id"], annotations)
232+
return annotations
259233

260-
return task, annotations
234+
def _generate_task(self):
235+
task = {
236+
"name": "my task #1",
237+
"owner": '',
238+
"assignee": '',
239+
"overlap": 0,
240+
"segment_size": 100,
241+
"z_order": False,
242+
"labels": [
243+
{
244+
"name": "car",
245+
"attributes": [
246+
{
247+
"name": "model",
248+
"mutable": False,
249+
"input_type": "select",
250+
"default_value": "mazda",
251+
"values": ["bmw", "mazda", "renault"]
252+
},
253+
{
254+
"name": "parked",
255+
"mutable": True,
256+
"input_type": "checkbox",
257+
"default_value": False
258+
},
259+
]
260+
},
261+
{"name": "person"},
262+
]
263+
}
264+
return self._create_task(task, 3)
261265

262266
def _create_task(self, data, size):
263267
with ForceLogin(self.user, self.client):
@@ -285,53 +289,99 @@ def _put_api_v1_task_id_annotations(self, tid, data):
285289

286290
return response
287291

288-
def _test_export(self, format_name, save_images=False):
289-
task, _ = self._generate_task()
290-
292+
def _test_export(self, check, task, format_name, **export_args):
291293
with tempfile.TemporaryDirectory() as temp_dir:
292294
file_path = osp.join(temp_dir, format_name)
293295
dm.task.export_task(task["id"], file_path,
294-
format_name, save_images=save_images)
295-
296-
with open(file_path, 'rb') as f:
297-
self.assertTrue(len(f.read()) != 0)
298-
299-
def test_datumaro(self):
300-
self._test_export('Datumaro 1.0', save_images=False)
301-
302-
def test_coco(self):
303-
self._test_export('COCO 1.0', save_images=True)
304-
305-
def test_voc(self):
306-
self._test_export('PASCAL VOC 1.1', save_images=True)
307-
308-
def test_tf_record(self):
309-
self._test_export('TFRecord 1.0', save_images=True)
296+
format_name, **export_args)
310297

311-
def test_yolo(self):
312-
self._test_export('YOLO 1.1', save_images=True)
313-
314-
def test_mot(self):
315-
self._test_export('MOT 1.1', save_images=True)
316-
317-
def test_labelme(self):
318-
self._test_export('LabelMe 3.0', save_images=True)
319-
320-
def test_mask(self):
321-
self._test_export('Segmentation mask 1.1', save_images=True)
322-
323-
def test_cvat_video(self):
324-
self._test_export('CVAT for video 1.1', save_images=True)
325-
326-
def test_cvat_images(self):
327-
self._test_export('CVAT for images 1.1', save_images=True)
298+
check(file_path)
328299

329300
def test_export_formats_query(self):
330301
formats = dm.views.get_export_formats()
331302

332-
self.assertEqual(len(formats), 10)
303+
self.assertEqual({f.DISPLAY_NAME for f in formats},
304+
{
305+
'COCO 1.0',
306+
'CVAT for images 1.1',
307+
'CVAT for video 1.1',
308+
'Datumaro 1.0',
309+
'LabelMe 3.0',
310+
'MOT 1.1',
311+
'PASCAL VOC 1.1',
312+
'Segmentation mask 1.1',
313+
'TFRecord 1.0',
314+
'YOLO 1.1',
315+
})
333316

334317
def test_import_formats_query(self):
335318
formats = dm.views.get_import_formats()
336319

337-
self.assertEqual(len(formats), 8)
320+
self.assertEqual({f.DISPLAY_NAME for f in formats},
321+
{
322+
'COCO 1.0',
323+
'CVAT 1.1',
324+
'LabelMe 3.0',
325+
'MOT 1.1',
326+
'PASCAL VOC 1.1',
327+
'Segmentation mask 1.1',
328+
'TFRecord 1.0',
329+
'YOLO 1.1',
330+
})
331+
332+
def test_exports(self):
333+
def check(file_path):
334+
with open(file_path, 'rb') as f:
335+
self.assertTrue(len(f.read()) != 0)
336+
337+
for f in dm.views.get_export_formats():
338+
format_name = f.DISPLAY_NAME
339+
for save_images in { True, False }:
340+
with self.subTest(format=format_name, save_images=save_images):
341+
task = self._generate_task()
342+
self._generate_annotations(task)
343+
self._test_export(check, task,
344+
format_name, save_images=save_images)
345+
346+
def test_empty_images_are_exported(self):
347+
dm_env = dm.formats.registry.dm_env
348+
349+
for format_name, importer_name in [
350+
('COCO 1.0', 'coco'),
351+
('CVAT for images 1.1', 'cvat'),
352+
# ('CVAT for video 1.1', 'cvat'), # does not support
353+
('Datumaro 1.0', 'datumaro_project'),
354+
('LabelMe 3.0', 'label_me'),
355+
# ('MOT 1.1', 'mot_seq'), # does not support
356+
('PASCAL VOC 1.1', 'voc'),
357+
('Segmentation mask 1.1', 'voc'),
358+
('TFRecord 1.0', 'tf_detection_api'),
359+
('YOLO 1.1', 'yolo'),
360+
]:
361+
with self.subTest(format=format_name):
362+
task = self._generate_task()
363+
364+
def check(file_path):
365+
def load_dataset(src):
366+
if importer_name == 'datumaro_project':
367+
project = datumaro.components.project. \
368+
Project.load(src)
369+
370+
# NOTE: can't import cvat.utils.cli
371+
# for whatever reason, so remove the dependency
372+
project.config.remove('sources')
373+
374+
return project.make_dataset()
375+
return dm_env.make_importer(importer_name)(src) \
376+
.make_dataset()
377+
378+
if zipfile.is_zipfile(file_path):
379+
with tempfile.TemporaryDirectory() as tmp_dir:
380+
zipfile.ZipFile(file_path).extractall(tmp_dir)
381+
dataset = load_dataset(tmp_dir)
382+
else:
383+
dataset = load_dataset(file_path)
384+
385+
self.assertEqual(len(dataset), task["size"])
386+
self._test_export(check, task, format_name, save_images=False)
387+

datumaro/datumaro/components/project.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ def load_project_as_dataset(url):
142142

143143
class Environment:
144144
_builtin_plugins = None
145-
PROJECT_EXTRACTOR_NAME = 'project'
145+
PROJECT_EXTRACTOR_NAME = 'datumaro_project'
146146

147147
def __init__(self, config=None):
148148
config = Config(config,

datumaro/datumaro/plugins/yolo_format/extractor.py

Lines changed: 23 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -93,12 +93,6 @@ def __init__(self, config_path, image_info=None):
9393
(osp.splitext(osp.basename(p.strip()))[0], p.strip())
9494
for p in f
9595
)
96-
97-
for item_id, image_path in subset.items.items():
98-
image_path = self._make_local_path(image_path)
99-
if not osp.isfile(image_path) and item_id not in image_info:
100-
raise Exception("Can't find image '%s'" % item_id)
101-
10296
subsets[subset_name] = subset
10397

10498
self._subsets = subsets
@@ -122,10 +116,9 @@ def _get(self, item_id, subset_name):
122116
image_path = self._make_local_path(item)
123117
image_size = self._image_info.get(item_id)
124118
image = Image(path=image_path, size=image_size)
125-
h, w = image.size
126119

127120
anno_path = osp.splitext(image_path)[0] + '.txt'
128-
annotations = self._parse_annotations(anno_path, w, h)
121+
annotations = self._parse_annotations(anno_path, image)
129122

130123
item = DatasetItem(id=item_id, subset=subset_name,
131124
image=image, annotations=annotations)
@@ -134,21 +127,30 @@ def _get(self, item_id, subset_name):
134127
return item
135128

136129
@staticmethod
137-
def _parse_annotations(anno_path, image_width, image_height):
130+
def _parse_annotations(anno_path, image):
131+
lines = []
138132
with open(anno_path, 'r') as f:
139-
annotations = []
140133
for line in f:
141-
label_id, xc, yc, w, h = line.strip().split()
142-
label_id = int(label_id)
143-
w = float(w)
144-
h = float(h)
145-
x = float(xc) - w * 0.5
146-
y = float(yc) - h * 0.5
147-
annotations.append(Bbox(
148-
round(x * image_width, 1), round(y * image_height, 1),
149-
round(w * image_width, 1), round(h * image_height, 1),
150-
label=label_id
151-
))
134+
line = line.strip()
135+
if line:
136+
lines.append(line)
137+
138+
annotations = []
139+
if lines:
140+
image_height, image_width = image.size # use image info late
141+
for line in lines:
142+
label_id, xc, yc, w, h = line.split()
143+
label_id = int(label_id)
144+
w = float(w)
145+
h = float(h)
146+
x = float(xc) - w * 0.5
147+
y = float(yc) - h * 0.5
148+
annotations.append(Bbox(
149+
round(x * image_width, 1), round(y * image_height, 1),
150+
round(w * image_width, 1), round(h * image_height, 1),
151+
label=label_id
152+
))
153+
152154
return annotations
153155

154156
@staticmethod

0 commit comments

Comments
 (0)