|
| 1 | +# Copyright (C) 2019 Intel Corporation |
| 2 | +# |
| 3 | +# SPDX-License-Identifier: MIT |
| 4 | + |
| 5 | +format_spec = { |
| 6 | + "name": "LabelMe", |
| 7 | + "dumpers": [ |
| 8 | + { |
| 9 | + "display_name": "{name} {format} {version} for images", |
| 10 | + "format": "ZIP", |
| 11 | + "version": "3.0", |
| 12 | + "handler": "dump_as_labelme_annotation" |
| 13 | + } |
| 14 | + ], |
| 15 | + "loaders": [ |
| 16 | + { |
| 17 | + "display_name": "{name} {format} {version}", |
| 18 | + "format": "ZIP", |
| 19 | + "version": "3.0", |
| 20 | + "handler": "load", |
| 21 | + } |
| 22 | + ], |
| 23 | +} |
| 24 | + |
| 25 | + |
| 26 | +_DEFAULT_USERNAME = 'cvat' |
| 27 | +_MASKS_DIR = 'Masks' |
| 28 | + |
| 29 | + |
| 30 | +def dump_frame_anno(frame_annotation): |
| 31 | + from collections import defaultdict |
| 32 | + from lxml import etree as ET |
| 33 | + |
| 34 | + root_elem = ET.Element('annotation') |
| 35 | + |
| 36 | + ET.SubElement(root_elem, 'filename').text = frame_annotation.name |
| 37 | + ET.SubElement(root_elem, 'folder').text = '' |
| 38 | + |
| 39 | + source_elem = ET.SubElement(root_elem, 'source') |
| 40 | + ET.SubElement(source_elem, 'sourceImage').text = '' |
| 41 | + ET.SubElement(source_elem, 'sourceAnnotation').text = 'CVAT' |
| 42 | + |
| 43 | + image_elem = ET.SubElement(root_elem, 'imagesize') |
| 44 | + ET.SubElement(image_elem, 'nrows').text = str(frame_annotation.height) |
| 45 | + ET.SubElement(image_elem, 'ncols').text = str(frame_annotation.width) |
| 46 | + |
| 47 | + groups = defaultdict(list) |
| 48 | + |
| 49 | + for obj_id, shape in enumerate(frame_annotation.labeled_shapes): |
| 50 | + obj_elem = ET.SubElement(root_elem, 'object') |
| 51 | + ET.SubElement(obj_elem, 'name').text = str(shape.label) |
| 52 | + ET.SubElement(obj_elem, 'deleted').text = '0' |
| 53 | + ET.SubElement(obj_elem, 'verified').text = '0' |
| 54 | + ET.SubElement(obj_elem, 'occluded').text = \ |
| 55 | + 'yes' if shape.occluded else 'no' |
| 56 | + ET.SubElement(obj_elem, 'date').text = '' |
| 57 | + ET.SubElement(obj_elem, 'id').text = str(obj_id) |
| 58 | + |
| 59 | + parts_elem = ET.SubElement(obj_elem, 'parts') |
| 60 | + if shape.group: |
| 61 | + groups[shape.group].append((obj_id, parts_elem)) |
| 62 | + else: |
| 63 | + ET.SubElement(parts_elem, 'hasparts').text = '' |
| 64 | + ET.SubElement(parts_elem, 'ispartof').text = '' |
| 65 | + |
| 66 | + if shape.type == 'rectangle': |
| 67 | + ET.SubElement(obj_elem, 'type').text = 'bounding_box' |
| 68 | + |
| 69 | + poly_elem = ET.SubElement(obj_elem, 'polygon') |
| 70 | + x0, y0, x1, y1 = shape.points |
| 71 | + points = [ (x0, y0), (x1, y0), (x1, y1), (x0, y1) ] |
| 72 | + for x, y in points: |
| 73 | + point_elem = ET.SubElement(poly_elem, 'pt') |
| 74 | + ET.SubElement(point_elem, 'x').text = '%.2f' % x |
| 75 | + ET.SubElement(point_elem, 'y').text = '%.2f' % y |
| 76 | + |
| 77 | + ET.SubElement(poly_elem, 'username').text = _DEFAULT_USERNAME |
| 78 | + elif shape.type == 'polygon': |
| 79 | + poly_elem = ET.SubElement(obj_elem, 'polygon') |
| 80 | + for x, y in zip(shape.points[::2], shape.points[1::2]): |
| 81 | + point_elem = ET.SubElement(poly_elem, 'pt') |
| 82 | + ET.SubElement(point_elem, 'x').text = '%.2f' % x |
| 83 | + ET.SubElement(point_elem, 'y').text = '%.2f' % y |
| 84 | + |
| 85 | + ET.SubElement(poly_elem, 'username').text = _DEFAULT_USERNAME |
| 86 | + elif shape.type == 'polyline': |
| 87 | + pass |
| 88 | + elif shape.type == 'points': |
| 89 | + pass |
| 90 | + else: |
| 91 | + raise NotImplementedError("Unknown shape type '%s'" % shape.type) |
| 92 | + |
| 93 | + attrs = ['%s=%s' % (a.name, a.value) for a in shape.attributes] |
| 94 | + ET.SubElement(obj_elem, 'attributes').text = ', '.join(attrs) |
| 95 | + |
| 96 | + for _, group in groups.items(): |
| 97 | + leader_id, leader_parts_elem = group[0] |
| 98 | + leader_parts = [str(o_id) for o_id, _ in group[1:]] |
| 99 | + ET.SubElement(leader_parts_elem, 'hasparts').text = \ |
| 100 | + ','.join(leader_parts) |
| 101 | + ET.SubElement(leader_parts_elem, 'ispartof').text = '' |
| 102 | + |
| 103 | + for obj_id, parts_elem in group[1:]: |
| 104 | + ET.SubElement(parts_elem, 'hasparts').text = '' |
| 105 | + ET.SubElement(parts_elem, 'ispartof').text = str(leader_id) |
| 106 | + |
| 107 | + return ET.tostring(root_elem, encoding='unicode', pretty_print=True) |
| 108 | + |
| 109 | +def dump_as_labelme_annotation(file_object, annotations): |
| 110 | + from zipfile import ZipFile, ZIP_DEFLATED |
| 111 | + |
| 112 | + with ZipFile(file_object, 'w', compression=ZIP_DEFLATED) as output_zip: |
| 113 | + for frame_annotation in annotations.group_by_frame(): |
| 114 | + xml_data = dump_frame_anno(frame_annotation) |
| 115 | + filename = frame_annotation.name |
| 116 | + filename = filename[ : filename.rfind('.')] + '.xml' |
| 117 | + output_zip.writestr(filename, xml_data) |
| 118 | + |
| 119 | +def parse_xml_annotations(xml_data, annotations, input_zip): |
| 120 | + from cvat.apps.annotation.coco import mask_to_polygon |
| 121 | + from io import BytesIO |
| 122 | + from lxml import etree as ET |
| 123 | + import numpy as np |
| 124 | + import os.path as osp |
| 125 | + from PIL import Image |
| 126 | + |
| 127 | + def parse_attributes(attributes_string): |
| 128 | + parsed = [] |
| 129 | + if not attributes_string: |
| 130 | + return parsed |
| 131 | + |
| 132 | + read = attributes_string.split(',') |
| 133 | + read = [a.strip() for a in read if a.strip()] |
| 134 | + for attr in read: |
| 135 | + if '=' in attr: |
| 136 | + name, value = attr.split('=', maxsplit=1) |
| 137 | + parsed.append(annotations.Attribute(name, value)) |
| 138 | + else: |
| 139 | + parsed.append(annotations.Attribute(attr, '1')) |
| 140 | + |
| 141 | + return parsed |
| 142 | + |
| 143 | + |
| 144 | + root_elem = ET.fromstring(xml_data) |
| 145 | + |
| 146 | + frame_number = annotations.match_frame(root_elem.find('filename').text) |
| 147 | + |
| 148 | + parsed_annotations = dict() |
| 149 | + group_assignments = dict() |
| 150 | + root_annotations = set() |
| 151 | + for obj_elem in root_elem.iter('object'): |
| 152 | + obj_id = int(obj_elem.find('id').text) |
| 153 | + |
| 154 | + ann_items = [] |
| 155 | + |
| 156 | + attributes = [] |
| 157 | + attributes_elem = obj_elem.find('attributes') |
| 158 | + if attributes_elem is not None and attributes_elem.text: |
| 159 | + attributes = parse_attributes(attributes_elem.text) |
| 160 | + |
| 161 | + occluded = False |
| 162 | + occluded_elem = obj_elem.find('occluded') |
| 163 | + if occluded_elem is not None and occluded_elem.text: |
| 164 | + occluded = (occluded_elem.text == 'yes') |
| 165 | + |
| 166 | + deleted = False |
| 167 | + deleted_elem = obj_elem.find('deleted') |
| 168 | + if deleted_elem is not None and deleted_elem.text: |
| 169 | + deleted = bool(int(deleted_elem.text)) |
| 170 | + |
| 171 | + poly_elem = obj_elem.find('polygon') |
| 172 | + segm_elem = obj_elem.find('segm') |
| 173 | + type_elem = obj_elem.find('type') # the only value is 'bounding_box' |
| 174 | + if poly_elem is not None: |
| 175 | + points = [] |
| 176 | + for point_elem in poly_elem.iter('pt'): |
| 177 | + x = float(point_elem.find('x').text) |
| 178 | + y = float(point_elem.find('y').text) |
| 179 | + points.append(x) |
| 180 | + points.append(y) |
| 181 | + label = obj_elem.find('name').text |
| 182 | + if label and attributes: |
| 183 | + label_id = annotations._get_label_id(label) |
| 184 | + if label_id: |
| 185 | + attributes = [a for a in attributes |
| 186 | + if annotations._get_attribute_id(label_id, a.name) |
| 187 | + ] |
| 188 | + else: |
| 189 | + attributes = [] |
| 190 | + else: |
| 191 | + attributes = [] |
| 192 | + |
| 193 | + if type_elem is not None and type_elem.text == 'bounding_box': |
| 194 | + xmin = min(points[::2]) |
| 195 | + xmax = max(points[::2]) |
| 196 | + ymin = min(points[1::2]) |
| 197 | + ymax = max(points[1::2]) |
| 198 | + ann_items.append(annotations.LabeledShape( |
| 199 | + type='rectangle', |
| 200 | + frame=frame_number, |
| 201 | + label=label, |
| 202 | + points=[xmin, ymin, xmax, ymax], |
| 203 | + occluded=occluded, |
| 204 | + attributes=attributes, |
| 205 | + )) |
| 206 | + else: |
| 207 | + ann_items.append(annotations.LabeledShape( |
| 208 | + type='polygon', |
| 209 | + frame=frame_number, |
| 210 | + label=label, |
| 211 | + points=points, |
| 212 | + occluded=occluded, |
| 213 | + attributes=attributes, |
| 214 | + )) |
| 215 | + elif segm_elem is not None: |
| 216 | + label = obj_elem.find('name').text |
| 217 | + if label and attributes: |
| 218 | + label_id = annotations._get_label_id(label) |
| 219 | + if label_id: |
| 220 | + attributes = [a for a in attributes |
| 221 | + if annotations._get_attribute_id(label_id, a.name) |
| 222 | + ] |
| 223 | + else: |
| 224 | + attributes = [] |
| 225 | + else: |
| 226 | + attributes = [] |
| 227 | + |
| 228 | + mask_file = segm_elem.find('mask').text |
| 229 | + mask = input_zip.read(osp.join(_MASKS_DIR, mask_file)) |
| 230 | + mask = np.asarray(Image.open(BytesIO(mask)).convert('L')) |
| 231 | + mask = (mask != 0) |
| 232 | + polygons = mask_to_polygon(mask) |
| 233 | + |
| 234 | + for polygon in polygons: |
| 235 | + ann_items.append(annotations.LabeledShape( |
| 236 | + type='polygon', |
| 237 | + frame=frame_number, |
| 238 | + label=label, |
| 239 | + points=polygon, |
| 240 | + occluded=occluded, |
| 241 | + attributes=attributes, |
| 242 | + )) |
| 243 | + |
| 244 | + if not deleted: |
| 245 | + parsed_annotations[obj_id] = ann_items |
| 246 | + |
| 247 | + parts_elem = obj_elem.find('parts') |
| 248 | + if parts_elem is not None: |
| 249 | + children_ids = [] |
| 250 | + hasparts_elem = parts_elem.find('hasparts') |
| 251 | + if hasparts_elem is not None and hasparts_elem.text: |
| 252 | + children_ids = [int(c) for c in hasparts_elem.text.split(',')] |
| 253 | + |
| 254 | + parent_ids = [] |
| 255 | + ispartof_elem = parts_elem.find('ispartof') |
| 256 | + if ispartof_elem is not None and ispartof_elem.text: |
| 257 | + parent_ids = [int(c) for c in ispartof_elem.text.split(',')] |
| 258 | + |
| 259 | + if children_ids and not parent_ids and hasparts_elem.text: |
| 260 | + root_annotations.add(obj_id) |
| 261 | + group_assignments[obj_id] = [None, children_ids] |
| 262 | + |
| 263 | + # assign a single group to the whole subtree |
| 264 | + current_group_id = 0 |
| 265 | + annotations_to_visit = list(root_annotations) |
| 266 | + while annotations_to_visit: |
| 267 | + ann_id = annotations_to_visit.pop() |
| 268 | + ann_assignment = group_assignments[ann_id] |
| 269 | + group_id, children_ids = ann_assignment |
| 270 | + if group_id: |
| 271 | + continue |
| 272 | + |
| 273 | + if ann_id in root_annotations: |
| 274 | + current_group_id += 1 # start a new group |
| 275 | + |
| 276 | + group_id = current_group_id |
| 277 | + ann_assignment[0] = group_id |
| 278 | + |
| 279 | + # continue with children |
| 280 | + annotations_to_visit.extend(children_ids) |
| 281 | + |
| 282 | + assert current_group_id == len(root_annotations) |
| 283 | + |
| 284 | + for ann_id, ann_items in parsed_annotations.items(): |
| 285 | + group_id = 0 |
| 286 | + if ann_id in group_assignments: |
| 287 | + ann_assignment = group_assignments[ann_id] |
| 288 | + group_id = ann_assignment[0] |
| 289 | + |
| 290 | + for ann_item in ann_items: |
| 291 | + if group_id: |
| 292 | + ann_item = ann_item._replace(group=group_id) |
| 293 | + if isinstance(ann_item, annotations.LabeledShape): |
| 294 | + annotations.add_shape(ann_item) |
| 295 | + else: |
| 296 | + raise NotImplementedError() |
| 297 | + |
| 298 | +def load(file_object, annotations): |
| 299 | + from zipfile import ZipFile |
| 300 | + |
| 301 | + with ZipFile(file_object, 'r') as input_zip: |
| 302 | + for filename in input_zip.namelist(): |
| 303 | + if not filename.endswith('.xml'): |
| 304 | + continue |
| 305 | + |
| 306 | + xml_data = input_zip.read(filename) |
| 307 | + parse_xml_annotations(xml_data, annotations, input_zip) |
0 commit comments