Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Include empty images in exported annotations #1479

Merged
merged 6 commits into from
May 17, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@zhiltsov-max , I don't think that it is a good idea to read the whole file into memory. I understand that the code below does it in any case but probably annotations.append can be clever in the future.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These files contain annotations only for a single image, so it's unlikely to become a bottleneck. Whole dataset jsons in COCO hurt much more.

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