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 ### Changed
- Downloaded file name in annotations export became more informative (https://github.com/opencv/cvat/pull/1352) - 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: 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) - 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) - 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: 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) - 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) - 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 ### Deprecated
- -

@ -413,7 +413,7 @@ class CvatTaskDataExtractor(datumaro.SourceExtractor):
if include_images: if include_images:
frame_provider = FrameProvider(task_data.db_task.data) 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 loader = None
if include_images: if include_images:
loader = lambda p, i=frame_data.idx: frame_provider.get_frame(i, 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.open_root()
dumper.add_meta(annotations.meta) 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 frame_id = frame_annotation.frame
dumper.open_image(OrderedDict([ dumper.open_image(OrderedDict([
("id", str(frame_id)), ("id", str(frame_id)),

@ -39,6 +39,7 @@ def _import(src_file, task_data):
label_cat = dataset.categories()[datumaro.AnnotationType.label] label_cat = dataset.categories()[datumaro.AnnotationType.label]
for item in dataset: 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) frame_id = match_frame(item, task_data)
for ann in item.annotations: for ann in item.annotations:

@ -51,6 +51,9 @@ def _setUpModule():
import cvat.apps.dataset_manager as dm import cvat.apps.dataset_manager as dm
globals()['dm'] = dm globals()['dm'] = dm
import datumaro
globals()['datumaro'] = datumaro
import sys import sys
sys.path.insert(0, __file__[:__file__.rfind('/dataset_manager/')]) sys.path.insert(0, __file__[:__file__.rfind('/dataset_manager/')])
@ -61,6 +64,7 @@ from io import BytesIO
import os.path as osp import os.path as osp
import random import random
import tempfile import tempfile
import zipfile
from PIL import Image from PIL import Image
from django.contrib.auth.models import User, Group from django.contrib.auth.models import User, Group
@ -113,38 +117,7 @@ class TaskExportTest(APITestCase):
def setUpTestData(cls): def setUpTestData(cls):
create_db_users(cls) create_db_users(cls)
def _generate_task(self): def _generate_annotations(self, task):
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)
annotations = { annotations = {
"version": 0, "version": 0,
"tags": [ "tags": [
@ -256,8 +229,39 @@ class TaskExportTest(APITestCase):
] ]
} }
self._put_api_v1_task_id_annotations(task["id"], annotations) 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): def _create_task(self, data, size):
with ForceLogin(self.user, self.client): with ForceLogin(self.user, self.client):
@ -285,53 +289,99 @@ class TaskExportTest(APITestCase):
return response return response
def _test_export(self, format_name, save_images=False): def _test_export(self, check, task, format_name, **export_args):
task, _ = self._generate_task()
with tempfile.TemporaryDirectory() as temp_dir: with tempfile.TemporaryDirectory() as temp_dir:
file_path = osp.join(temp_dir, format_name) file_path = osp.join(temp_dir, format_name)
dm.task.export_task(task["id"], file_path, dm.task.export_task(task["id"], file_path,
format_name, save_images=save_images) format_name, **export_args)
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)
def test_yolo(self): check(file_path)
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)
def test_export_formats_query(self): def test_export_formats_query(self):
formats = dm.views.get_export_formats() 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): def test_import_formats_query(self):
formats = dm.views.get_import_formats() 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: class Environment:
_builtin_plugins = None _builtin_plugins = None
PROJECT_EXTRACTOR_NAME = 'project' PROJECT_EXTRACTOR_NAME = 'datumaro_project'
def __init__(self, config=None): def __init__(self, config=None):
config = Config(config, config = Config(config,

@ -93,12 +93,6 @@ class YoloExtractor(SourceExtractor):
(osp.splitext(osp.basename(p.strip()))[0], p.strip()) (osp.splitext(osp.basename(p.strip()))[0], p.strip())
for p in f 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 subsets[subset_name] = subset
self._subsets = subsets self._subsets = subsets
@ -122,10 +116,9 @@ class YoloExtractor(SourceExtractor):
image_path = self._make_local_path(item) image_path = self._make_local_path(item)
image_size = self._image_info.get(item_id) image_size = self._image_info.get(item_id)
image = Image(path=image_path, size=image_size) image = Image(path=image_path, size=image_size)
h, w = image.size
anno_path = osp.splitext(image_path)[0] + '.txt' 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, item = DatasetItem(id=item_id, subset=subset_name,
image=image, annotations=annotations) image=image, annotations=annotations)
@ -134,21 +127,30 @@ class YoloExtractor(SourceExtractor):
return item return item
@staticmethod @staticmethod
def _parse_annotations(anno_path, image_width, image_height): def _parse_annotations(anno_path, image):
lines = []
with open(anno_path, 'r') as f: with open(anno_path, 'r') as f:
annotations = []
for line in f: for line in f:
label_id, xc, yc, w, h = line.strip().split() line = line.strip()
label_id = int(label_id) if line:
w = float(w) lines.append(line)
h = float(h)
x = float(xc) - w * 0.5 annotations = []
y = float(yc) - h * 0.5 if lines:
annotations.append(Bbox( image_height, image_width = image.size # use image info late
round(x * image_width, 1), round(y * image_height, 1), for line in lines:
round(w * image_width, 1), round(h * image_height, 1), label_id, xc, yc, w, h = line.split()
label=label_id 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 return annotations
@staticmethod @staticmethod

Loading…
Cancel
Save