Include empty images in exported annotations (#1479)

main
zhiltsov-max 6 years ago committed by GitHub
parent 98a9718e63
commit fb380d9855
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -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)
@ -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
-

@ -413,7 +413,7 @@ class CvatTaskDataExtractor(datumaro.SourceExtractor):
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,

@ -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)),

@ -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:

@ -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/')])
@ -61,6 +64,7 @@ from io import BytesIO
import os.path as osp
import random
import tempfile
import zipfile
from PIL import Image
from django.contrib.auth.models import User, Group
@ -113,38 +117,7 @@ class TaskExportTest(APITestCase):
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": [
@ -256,8 +229,39 @@ class TaskExportTest(APITestCase):
]
}
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):
@ -285,53 +289,99 @@ class TaskExportTest(APITestCase):
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)

@ -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,

@ -93,12 +93,6 @@ class YoloExtractor(SourceExtractor):
(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
@ -122,10 +116,9 @@ class YoloExtractor(SourceExtractor):
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)
@ -134,21 +127,30 @@ class YoloExtractor(SourceExtractor):
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

Loading…
Cancel
Save