Skip to content

Commit

Permalink
Include empty images in exported annotations (#1479)
Browse files Browse the repository at this point in the history
  • Loading branch information
zhiltsov-max authored May 17, 2020
1 parent 98a9718 commit fb380d9
Show file tree
Hide file tree
Showing 7 changed files with 150 additions and 96 deletions.
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

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

### Deprecated
-
Expand Down
2 changes: 1 addition & 1 deletion cvat/apps/dataset_manager/bindings.py
Original file line number Diff line number Diff line change
Expand Up @@ -413,7 +413,7 @@ def __init__(self, task_data, include_images=False):
if include_images:
frame_provider = FrameProvider(task_data.db_task.data)

for frame_data in task_data.group_by_frame(include_empty=include_images):
for frame_data in task_data.group_by_frame(include_empty=True):
loader = None
if include_images:
loader = lambda p, i=frame_data.idx: frame_provider.get_frame(i,
Expand Down
2 changes: 1 addition & 1 deletion cvat/apps/dataset_manager/formats/cvat.py
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ def dump_as_cvat_annotation(file_object, annotations):
dumper.open_root()
dumper.add_meta(annotations.meta)

for frame_annotation in annotations.group_by_frame():
for frame_annotation in annotations.group_by_frame(include_empty=True):
frame_id = frame_annotation.frame
dumper.open_image(OrderedDict([
("id", str(frame_id)),
Expand Down
1 change: 1 addition & 0 deletions cvat/apps/dataset_manager/formats/mot.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ def _import(src_file, task_data):
label_cat = dataset.categories()[datumaro.AnnotationType.label]

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

for ann in item.annotations:
Expand Down
192 changes: 121 additions & 71 deletions cvat/apps/dataset_manager/tests/_test_formats.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ def _setUpModule():
import cvat.apps.dataset_manager as dm
globals()['dm'] = dm

import datumaro
globals()['datumaro'] = datumaro

import sys
sys.path.insert(0, __file__[:__file__.rfind('/dataset_manager/')])

Expand All @@ -61,6 +64,7 @@ def _setUpModule():
import os.path as osp
import random
import tempfile
import zipfile

from PIL import Image
from django.contrib.auth.models import User, Group
Expand Down Expand Up @@ -113,38 +117,7 @@ def setUp(self):
def setUpTestData(cls):
create_db_users(cls)

def _generate_task(self):
task = {
"name": "my task #1",
"owner": '',
"assignee": '',
"overlap": 0,
"segment_size": 100,
"z_order": False,
"labels": [
{
"name": "car",
"attributes": [
{
"name": "model",
"mutable": False,
"input_type": "select",
"default_value": "mazda",
"values": ["bmw", "mazda", "renault"]
},
{
"name": "parked",
"mutable": True,
"input_type": "checkbox",
"default_value": False
},
]
},
{"name": "person"},
]
}
task = self._create_task(task, 3)

def _generate_annotations(self, task):
annotations = {
"version": 0,
"tags": [
Expand Down Expand Up @@ -256,8 +229,39 @@ def _generate_task(self):
]
}
self._put_api_v1_task_id_annotations(task["id"], annotations)
return annotations

return task, annotations
def _generate_task(self):
task = {
"name": "my task #1",
"owner": '',
"assignee": '',
"overlap": 0,
"segment_size": 100,
"z_order": False,
"labels": [
{
"name": "car",
"attributes": [
{
"name": "model",
"mutable": False,
"input_type": "select",
"default_value": "mazda",
"values": ["bmw", "mazda", "renault"]
},
{
"name": "parked",
"mutable": True,
"input_type": "checkbox",
"default_value": False
},
]
},
{"name": "person"},
]
}
return self._create_task(task, 3)

def _create_task(self, data, size):
with ForceLogin(self.user, self.client):
Expand Down Expand Up @@ -285,53 +289,99 @@ def _put_api_v1_task_id_annotations(self, tid, data):

return response

def _test_export(self, format_name, save_images=False):
task, _ = self._generate_task()

def _test_export(self, check, task, format_name, **export_args):
with tempfile.TemporaryDirectory() as temp_dir:
file_path = osp.join(temp_dir, format_name)
dm.task.export_task(task["id"], file_path,
format_name, save_images=save_images)

with open(file_path, 'rb') as f:
self.assertTrue(len(f.read()) != 0)

def test_datumaro(self):
self._test_export('Datumaro 1.0', save_images=False)

def test_coco(self):
self._test_export('COCO 1.0', save_images=True)

def test_voc(self):
self._test_export('PASCAL VOC 1.1', save_images=True)

def test_tf_record(self):
self._test_export('TFRecord 1.0', save_images=True)
format_name, **export_args)

def test_yolo(self):
self._test_export('YOLO 1.1', save_images=True)

def test_mot(self):
self._test_export('MOT 1.1', save_images=True)

def test_labelme(self):
self._test_export('LabelMe 3.0', save_images=True)

def test_mask(self):
self._test_export('Segmentation mask 1.1', save_images=True)

def test_cvat_video(self):
self._test_export('CVAT for video 1.1', save_images=True)

def test_cvat_images(self):
self._test_export('CVAT for images 1.1', save_images=True)
check(file_path)

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

self.assertEqual(len(formats), 10)
self.assertEqual({f.DISPLAY_NAME for f in formats},
{
'COCO 1.0',
'CVAT for images 1.1',
'CVAT for video 1.1',
'Datumaro 1.0',
'LabelMe 3.0',
'MOT 1.1',
'PASCAL VOC 1.1',
'Segmentation mask 1.1',
'TFRecord 1.0',
'YOLO 1.1',
})

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

self.assertEqual(len(formats), 8)
self.assertEqual({f.DISPLAY_NAME for f in formats},
{
'COCO 1.0',
'CVAT 1.1',
'LabelMe 3.0',
'MOT 1.1',
'PASCAL VOC 1.1',
'Segmentation mask 1.1',
'TFRecord 1.0',
'YOLO 1.1',
})

def test_exports(self):
def check(file_path):
with open(file_path, 'rb') as f:
self.assertTrue(len(f.read()) != 0)

for f in dm.views.get_export_formats():
format_name = f.DISPLAY_NAME
for save_images in { True, False }:
with self.subTest(format=format_name, save_images=save_images):
task = self._generate_task()
self._generate_annotations(task)
self._test_export(check, task,
format_name, save_images=save_images)

def test_empty_images_are_exported(self):
dm_env = dm.formats.registry.dm_env

for format_name, importer_name in [
('COCO 1.0', 'coco'),
('CVAT for images 1.1', 'cvat'),
# ('CVAT for video 1.1', 'cvat'), # does not support
('Datumaro 1.0', 'datumaro_project'),
('LabelMe 3.0', 'label_me'),
# ('MOT 1.1', 'mot_seq'), # does not support
('PASCAL VOC 1.1', 'voc'),
('Segmentation mask 1.1', 'voc'),
('TFRecord 1.0', 'tf_detection_api'),
('YOLO 1.1', 'yolo'),
]:
with self.subTest(format=format_name):
task = self._generate_task()

def check(file_path):
def load_dataset(src):
if importer_name == 'datumaro_project':
project = datumaro.components.project. \
Project.load(src)

# NOTE: can't import cvat.utils.cli
# for whatever reason, so remove the dependency
project.config.remove('sources')

return project.make_dataset()
return dm_env.make_importer(importer_name)(src) \
.make_dataset()

if zipfile.is_zipfile(file_path):
with tempfile.TemporaryDirectory() as tmp_dir:
zipfile.ZipFile(file_path).extractall(tmp_dir)
dataset = load_dataset(tmp_dir)
else:
dataset = load_dataset(file_path)

self.assertEqual(len(dataset), task["size"])
self._test_export(check, task, format_name, save_images=False)

2 changes: 1 addition & 1 deletion datumaro/datumaro/components/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ def load_project_as_dataset(url):

class Environment:
_builtin_plugins = None
PROJECT_EXTRACTOR_NAME = 'project'
PROJECT_EXTRACTOR_NAME = 'datumaro_project'

def __init__(self, config=None):
config = Config(config,
Expand Down
44 changes: 23 additions & 21 deletions datumaro/datumaro/plugins/yolo_format/extractor.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,12 +93,6 @@ def __init__(self, config_path, image_info=None):
(osp.splitext(osp.basename(p.strip()))[0], p.strip())
for p in f
)

for item_id, image_path in subset.items.items():
image_path = self._make_local_path(image_path)
if not osp.isfile(image_path) and item_id not in image_info:
raise Exception("Can't find image '%s'" % item_id)

subsets[subset_name] = subset

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

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

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

@staticmethod
def _parse_annotations(anno_path, image_width, image_height):
def _parse_annotations(anno_path, image):
lines = []
with open(anno_path, 'r') as f:
annotations = []
for line in f:
label_id, xc, yc, w, h = line.strip().split()
label_id = int(label_id)
w = float(w)
h = float(h)
x = float(xc) - w * 0.5
y = float(yc) - h * 0.5
annotations.append(Bbox(
round(x * image_width, 1), round(y * image_height, 1),
round(w * image_width, 1), round(h * image_height, 1),
label=label_id
))
line = line.strip()
if line:
lines.append(line)

annotations = []
if lines:
image_height, image_width = image.size # use image info late
for line in lines:
label_id, xc, yc, w, h = line.split()
label_id = int(label_id)
w = float(w)
h = float(h)
x = float(xc) - w * 0.5
y = float(yc) - h * 0.5
annotations.append(Bbox(
round(x * image_width, 1), round(y * image_height, 1),
round(w * image_width, 1), round(h * image_height, 1),
label=label_id
))

return annotations

@staticmethod
Expand Down

0 comments on commit fb380d9

Please sign in to comment.