-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathASAPAdapter.py
More file actions
223 lines (200 loc) · 8.51 KB
/
ASAPAdapter.py
File metadata and controls
223 lines (200 loc) · 8.51 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
"""
Skinet (Segmentation of the Kidney through a Neural nETwork) Project
Dataset tools
Copyright (c) 2021 Skinet Team
Licensed under the MIT License (see LICENSE for details)
Written by Adrien JAUGEY
"""
import json
import shutil
import xml.etree.ElementTree as et
from common_utils import rgb_to_hex
from datasetTools.AnnotationAdapter import XMLAdapter
from mrcnn.Config import Config
class ASAPAdapter(XMLAdapter):
"""
Exports predictions to ASAP annotation format
"""
'''
<?xml version="1.0"?>
<ASAP_Annotations>
<Annotations>
<Annotation Name="Annotation {MASK_NUM:d}" Type="Polygon" PartOfGroup="{CLASS_NAME}" Color="#F4FA58">
<Coordinates>
<Coordinate Order="{PT_NUM:d}" X="{PT_X:d}" Y="{PT_Y:d}" />
</Coordinates>
</Annotation>
</Annotations>
<AnnotationGroups>
<Group Name="{CLASS_NAME}" PartOfGroup="None" Color="{CORRESPONDING_CLASS_COLOR}">
<Attributes />
</Group>
</AnnotationGroups>
</ASAP_Annotations>
'''
def __init__(self, imageInfo: dict, verbose=0):
super().__init__(imageInfo, "ASAP_Annotations", verbose=verbose)
self.annotations = et.Element('Annotations')
self.addToRoot(self.annotations)
self.groups = et.Element('AnnotationGroups')
self.addToRoot(self.groups)
self.nbAnnotation = 0
self.nbGroup = 0
self.classCount = {}
@staticmethod
def getName():
return "ASAP"
def addAnnotation(self, classInfo: {}, points):
if classInfo["name"] not in self.classCount:
self.classCount[classInfo["name"]] = 0
mask = et.Element('Annotation')
mask.set('Name', f"{classInfo['name']} {self.classCount[classInfo['name']]} ({self.nbAnnotation})")
mask.set("Type", "Polygon")
mask.set("PartOfGroup", classInfo["name"])
mask.set("Color", "#F4FA58")
self.annotations.append(mask)
coordinates = et.Element('Coordinates')
mask.append(coordinates)
for i, pt in enumerate(points):
coordinate = et.Element('Coordinate')
coordinate.set("Order", str(i))
coordinate.set("X", str(pt[0]))
coordinate.set("Y", str(pt[1]))
coordinates.append(coordinate)
self.classCount[classInfo["name"]] += 1
self.nbAnnotation += 1
def addAnnotationClass(self, classInfo: {}):
group = et.Element('Group')
group.set('Name', classInfo["name"])
group.set('PartOfGroup', "None")
group.set("Color", classInfo.get("asap_color", rgb_to_hex(classInfo["color"])))
attribute = et.Element('Attributes')
group.append(attribute)
self.nbGroup += 1
self.groups.append(group)
@staticmethod
def getPriorityLevel():
return 10
@staticmethod
def canRead(filePath):
canRead = XMLAdapter.canRead(filePath)
if canRead:
tree = et.parse(filePath)
root = tree.getroot()
canRead = root.tag == "ASAP_Annotations"
return canRead
@staticmethod
def readFile(filePath):
canRead = ASAPAdapter.canRead(filePath)
if not canRead:
raise TypeError('This file is not an ASAP annotation file')
tree = et.parse(filePath)
root = tree.getroot()
masks = []
# Going through the XML tree and getting all Annotation nodes
for annotation in root.findall('./Annotations/Annotation'):
maskClass = annotation.attrib.get('PartOfGroup')
ptsMask = []
# Going through the Annotation node and getting all Coordinate nodes
for points in annotation.find('Coordinates'):
xCoordinate = points.attrib.get('X')
yCoordinate = points.attrib.get('Y')
ptsMask.append([xCoordinate, yCoordinate])
masks.append((maskClass, ptsMask))
return masks
@staticmethod
def updateAnnotations(filePath, xRatio=1, yRatio=1, xOffset=0, yOffset=0, outputFilePath=None):
canRead = ASAPAdapter.canRead(filePath)
if not canRead:
raise TypeError('This file is not an ASAP annotation file')
if xOffset == yOffset == 0 and xRatio == yRatio == 1:
if outputFilePath is not None and outputFilePath != filePath:
shutil.copyfile(filePath, outputFilePath)
else:
return None
else:
tree = et.parse(filePath)
root = tree.getroot()
# Going through the XML tree and updating all Annotation nodes
for annotation in root.findall('./Annotations/Annotation'):
# Going through the Annotation node and updating all Coordinate nodes
for points in annotation.find('Coordinates'):
xCoordinate = points.attrib.get('X')
yCoordinate = points.attrib.get('Y')
points.set('X', str(round(int(xCoordinate) * xRatio + xOffset)))
points.set('Y', str(round(int(yCoordinate) * yRatio + yOffset)))
if 'xml_declaration' in tree.write.__code__.co_varnames:
tree.write(filePath if outputFilePath is None else outputFilePath,
encoding='unicode', xml_declaration=True)
else:
tree.write(filePath if outputFilePath is None else outputFilePath, encoding='unicode')
return None
@staticmethod
def fuseAnnotationsFiles(filePaths: list, savePath: str):
"""
Fuses multiple
:param filePaths:
:param savePath:
:return:
"""
canRead = all([ASAPAdapter.canRead(filePath) for filePath in filePaths])
if not canRead:
raise TypeError('At least one given file path is not an ASAP annotation file')
if len(filePaths) < 2:
raise ValueError('You must provide at least two file paths')
tree = et.parse(filePaths[0])
root = tree.getroot()
groups = root.findall('./AnnotationGroups/Group')
annotations = root.findall('./Annotations/Annotation')
classes = [aClass.attrib.get('Name') for aClass in groups]
# Merging AnnotationsGroups and annotations
for filePath in filePaths[1:]:
tempTree = et.parse(filePath)
tempRoot = tempTree.getroot()
for aClass in tempRoot.findall('./AnnotationGroups/Group'):
className = aClass.attrib.get('Name')
if className not in classes:
classes.append(className)
groups.append(aClass)
for anAnnotation in tempRoot.findall('./Annotations/Annotation'):
annotations.append(anAnnotation)
newTree = et.ElementTree(element=et.Element("ASAP_Annotations"))
newRoot = newTree.getroot()
annotationsNode = et.Element('Annotations')
annotationsNode.text = "\n\t\t"
annotationsNode.tail = "\n\t"
for node in annotations:
annotationsNode.append(node)
newRoot.append(annotationsNode)
groupsNode = et.Element('AnnotationGroups')
groupsNode.text = "\n\t\t"
groupsNode.tail = "\n"
for node in groups:
groupsNode.append(node)
newRoot.append(groupsNode)
if 'xml_declaration' in tree.write.__code__.co_varnames:
newTree.write(savePath, encoding='unicode', xml_declaration=True)
else:
newTree.write(savePath, encoding='unicode')
@classmethod
def fromLabelMe(cls, filepath: str, config: Config, group_id_2_class: dict = None):
"""
Constructs an ASAPAdapter from an exported LabelMe annotations file
:param filepath: path to the LabelMe annotations file path
:param config: the config
:param group_id_2_class: dict that links group ids to classes names (if None, label is used)
:return:
"""
res = cls({})
for c in config.get_classes_info():
res.addAnnotationClass(c)
with open(filepath, 'r') as labelMeFile:
data = json.load(labelMeFile)
for mask in data['shapes']:
points = mask['points']
if group_id_2_class is not None and mask['group_id'] in group_id_2_class:
name = group_id_2_class[mask['group_id']]
else:
name = mask['label'].split(' ')[0]
res.addAnnotation({'name': name}, points)
return res