labelimgplusplus 2.0.0a0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- labelimgplusplus-2.0.0a0.dist-info/LICENSE +9 -0
- labelimgplusplus-2.0.0a0.dist-info/METADATA +282 -0
- labelimgplusplus-2.0.0a0.dist-info/RECORD +30 -0
- labelimgplusplus-2.0.0a0.dist-info/WHEEL +5 -0
- labelimgplusplus-2.0.0a0.dist-info/entry_points.txt +2 -0
- labelimgplusplus-2.0.0a0.dist-info/top_level.txt +1 -0
- libs/__init__.py +2 -0
- libs/canvas.py +748 -0
- libs/colorDialog.py +37 -0
- libs/combobox.py +33 -0
- libs/commands.py +328 -0
- libs/constants.py +26 -0
- libs/create_ml_io.py +135 -0
- libs/default_label_combobox.py +27 -0
- libs/galleryWidget.py +568 -0
- libs/hashableQListWidgetItem.py +28 -0
- libs/labelDialog.py +95 -0
- libs/labelFile.py +174 -0
- libs/lightWidget.py +33 -0
- libs/pascal_voc_io.py +171 -0
- libs/resources.py +4212 -0
- libs/settings.py +45 -0
- libs/shape.py +209 -0
- libs/stringBundle.py +78 -0
- libs/styles.py +82 -0
- libs/toolBar.py +275 -0
- libs/ustr.py +17 -0
- libs/utils.py +119 -0
- libs/yolo_io.py +143 -0
- libs/zoomWidget.py +26 -0
libs/labelFile.py
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# Copyright (c) 2016 Tzutalin
|
|
2
|
+
# Create by TzuTaLin <tzu.ta.lin@gmail.com>
|
|
3
|
+
|
|
4
|
+
try:
|
|
5
|
+
from PyQt5.QtGui import QImage
|
|
6
|
+
except ImportError:
|
|
7
|
+
from PyQt4.QtGui import QImage
|
|
8
|
+
|
|
9
|
+
import os.path
|
|
10
|
+
from enum import Enum
|
|
11
|
+
|
|
12
|
+
from libs.create_ml_io import CreateMLWriter
|
|
13
|
+
from libs.pascal_voc_io import PascalVocWriter
|
|
14
|
+
from libs.pascal_voc_io import XML_EXT
|
|
15
|
+
from libs.yolo_io import YOLOWriter
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class LabelFileFormat(Enum):
|
|
19
|
+
PASCAL_VOC = 1
|
|
20
|
+
YOLO = 2
|
|
21
|
+
CREATE_ML = 3
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class LabelFileError(Exception):
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class LabelFile(object):
|
|
29
|
+
# It might be changed as window creates. By default, using XML ext
|
|
30
|
+
# suffix = '.lif'
|
|
31
|
+
suffix = XML_EXT
|
|
32
|
+
|
|
33
|
+
def __init__(self, filename=None):
|
|
34
|
+
self.shapes = ()
|
|
35
|
+
self.image_path = None
|
|
36
|
+
self.image_data = None
|
|
37
|
+
self.verified = False
|
|
38
|
+
|
|
39
|
+
def save_create_ml_format(self, filename, shapes, image_path, image_data, class_list, line_color=None, fill_color=None, database_src=None):
|
|
40
|
+
img_folder_name = os.path.basename(os.path.dirname(image_path))
|
|
41
|
+
img_file_name = os.path.basename(image_path)
|
|
42
|
+
|
|
43
|
+
image = QImage()
|
|
44
|
+
image.load(image_path)
|
|
45
|
+
image_shape = [image.height(), image.width(),
|
|
46
|
+
1 if image.isGrayscale() else 3]
|
|
47
|
+
writer = CreateMLWriter(img_folder_name, img_file_name,
|
|
48
|
+
image_shape, shapes, filename, local_img_path=image_path)
|
|
49
|
+
writer.verified = self.verified
|
|
50
|
+
writer.write()
|
|
51
|
+
return
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def save_pascal_voc_format(self, filename, shapes, image_path, image_data,
|
|
55
|
+
line_color=None, fill_color=None, database_src=None):
|
|
56
|
+
img_folder_path = os.path.dirname(image_path)
|
|
57
|
+
img_folder_name = os.path.split(img_folder_path)[-1]
|
|
58
|
+
img_file_name = os.path.basename(image_path)
|
|
59
|
+
# imgFileNameWithoutExt = os.path.splitext(img_file_name)[0]
|
|
60
|
+
# Read from file path because self.imageData might be empty if saving to
|
|
61
|
+
# Pascal format
|
|
62
|
+
if isinstance(image_data, QImage):
|
|
63
|
+
image = image_data
|
|
64
|
+
else:
|
|
65
|
+
image = QImage()
|
|
66
|
+
image.load(image_path)
|
|
67
|
+
image_shape = [image.height(), image.width(),
|
|
68
|
+
1 if image.isGrayscale() else 3]
|
|
69
|
+
writer = PascalVocWriter(img_folder_name, img_file_name,
|
|
70
|
+
image_shape, local_img_path=image_path)
|
|
71
|
+
writer.verified = self.verified
|
|
72
|
+
|
|
73
|
+
for shape in shapes:
|
|
74
|
+
points = shape['points']
|
|
75
|
+
label = shape['label']
|
|
76
|
+
# Add Chris
|
|
77
|
+
difficult = int(shape['difficult'])
|
|
78
|
+
bnd_box = LabelFile.convert_points_to_bnd_box(points)
|
|
79
|
+
writer.add_bnd_box(bnd_box[0], bnd_box[1], bnd_box[2], bnd_box[3], label, difficult)
|
|
80
|
+
|
|
81
|
+
writer.save(target_file=filename)
|
|
82
|
+
return
|
|
83
|
+
|
|
84
|
+
def save_yolo_format(self, filename, shapes, image_path, image_data, class_list,
|
|
85
|
+
line_color=None, fill_color=None, database_src=None):
|
|
86
|
+
img_folder_path = os.path.dirname(image_path)
|
|
87
|
+
img_folder_name = os.path.split(img_folder_path)[-1]
|
|
88
|
+
img_file_name = os.path.basename(image_path)
|
|
89
|
+
# imgFileNameWithoutExt = os.path.splitext(img_file_name)[0]
|
|
90
|
+
# Read from file path because self.imageData might be empty if saving to
|
|
91
|
+
# Pascal format
|
|
92
|
+
if isinstance(image_data, QImage):
|
|
93
|
+
image = image_data
|
|
94
|
+
else:
|
|
95
|
+
image = QImage()
|
|
96
|
+
image.load(image_path)
|
|
97
|
+
image_shape = [image.height(), image.width(),
|
|
98
|
+
1 if image.isGrayscale() else 3]
|
|
99
|
+
writer = YOLOWriter(img_folder_name, img_file_name,
|
|
100
|
+
image_shape, local_img_path=image_path)
|
|
101
|
+
writer.verified = self.verified
|
|
102
|
+
|
|
103
|
+
for shape in shapes:
|
|
104
|
+
points = shape['points']
|
|
105
|
+
label = shape['label']
|
|
106
|
+
# Add Chris
|
|
107
|
+
difficult = int(shape['difficult'])
|
|
108
|
+
bnd_box = LabelFile.convert_points_to_bnd_box(points)
|
|
109
|
+
writer.add_bnd_box(bnd_box[0], bnd_box[1], bnd_box[2], bnd_box[3], label, difficult)
|
|
110
|
+
|
|
111
|
+
writer.save(target_file=filename, class_list=class_list)
|
|
112
|
+
return
|
|
113
|
+
|
|
114
|
+
def toggle_verify(self):
|
|
115
|
+
self.verified = not self.verified
|
|
116
|
+
|
|
117
|
+
''' ttf is disable
|
|
118
|
+
def load(self, filename):
|
|
119
|
+
import json
|
|
120
|
+
with open(filename, 'rb') as f:
|
|
121
|
+
data = json.load(f)
|
|
122
|
+
imagePath = data['imagePath']
|
|
123
|
+
imageData = b64decode(data['imageData'])
|
|
124
|
+
lineColor = data['lineColor']
|
|
125
|
+
fillColor = data['fillColor']
|
|
126
|
+
shapes = ((s['label'], s['points'], s['line_color'], s['fill_color'])\
|
|
127
|
+
for s in data['shapes'])
|
|
128
|
+
# Only replace data after everything is loaded.
|
|
129
|
+
self.shapes = shapes
|
|
130
|
+
self.imagePath = imagePath
|
|
131
|
+
self.imageData = imageData
|
|
132
|
+
self.lineColor = lineColor
|
|
133
|
+
self.fillColor = fillColor
|
|
134
|
+
|
|
135
|
+
def save(self, filename, shapes, imagePath, imageData, lineColor=None, fillColor=None):
|
|
136
|
+
import json
|
|
137
|
+
with open(filename, 'wb') as f:
|
|
138
|
+
json.dump(dict(
|
|
139
|
+
shapes=shapes,
|
|
140
|
+
lineColor=lineColor, fillColor=fillColor,
|
|
141
|
+
imagePath=imagePath,
|
|
142
|
+
imageData=b64encode(imageData)),
|
|
143
|
+
f, ensure_ascii=True, indent=2)
|
|
144
|
+
'''
|
|
145
|
+
|
|
146
|
+
@staticmethod
|
|
147
|
+
def is_label_file(filename):
|
|
148
|
+
file_suffix = os.path.splitext(filename)[1].lower()
|
|
149
|
+
return file_suffix == LabelFile.suffix
|
|
150
|
+
|
|
151
|
+
@staticmethod
|
|
152
|
+
def convert_points_to_bnd_box(points):
|
|
153
|
+
x_min = float('inf')
|
|
154
|
+
y_min = float('inf')
|
|
155
|
+
x_max = float('-inf')
|
|
156
|
+
y_max = float('-inf')
|
|
157
|
+
for p in points:
|
|
158
|
+
x = p[0]
|
|
159
|
+
y = p[1]
|
|
160
|
+
x_min = min(x, x_min)
|
|
161
|
+
y_min = min(y, y_min)
|
|
162
|
+
x_max = max(x, x_max)
|
|
163
|
+
y_max = max(y, y_max)
|
|
164
|
+
|
|
165
|
+
# Martin Kersner, 2015/11/12
|
|
166
|
+
# 0-valued coordinates of BB caused an error while
|
|
167
|
+
# training faster-rcnn object detector.
|
|
168
|
+
if x_min < 1:
|
|
169
|
+
x_min = 1
|
|
170
|
+
|
|
171
|
+
if y_min < 1:
|
|
172
|
+
y_min = 1
|
|
173
|
+
|
|
174
|
+
return int(x_min), int(y_min), int(x_max), int(y_max)
|
libs/lightWidget.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
try:
|
|
2
|
+
from PyQt5.QtGui import *
|
|
3
|
+
from PyQt5.QtCore import *
|
|
4
|
+
from PyQt5.QtWidgets import *
|
|
5
|
+
except ImportError:
|
|
6
|
+
from PyQt4.QtGui import *
|
|
7
|
+
from PyQt4.QtCore import *
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class LightWidget(QSpinBox):
|
|
11
|
+
|
|
12
|
+
def __init__(self, title, value=50):
|
|
13
|
+
super(LightWidget, self).__init__()
|
|
14
|
+
self.setButtonSymbols(QAbstractSpinBox.NoButtons)
|
|
15
|
+
self.setRange(0, 100)
|
|
16
|
+
self.setSuffix(' %')
|
|
17
|
+
self.setValue(value)
|
|
18
|
+
self.setToolTip(title)
|
|
19
|
+
self.setStatusTip(self.toolTip())
|
|
20
|
+
self.setAlignment(Qt.AlignCenter)
|
|
21
|
+
|
|
22
|
+
def minimumSizeHint(self):
|
|
23
|
+
height = super(LightWidget, self).minimumSizeHint().height()
|
|
24
|
+
fm = QFontMetrics(self.font())
|
|
25
|
+
width = fm.width(str(self.maximum()))
|
|
26
|
+
return QSize(width, height)
|
|
27
|
+
|
|
28
|
+
def color(self):
|
|
29
|
+
if self.value() == 50:
|
|
30
|
+
return None
|
|
31
|
+
|
|
32
|
+
strength = int(self.value()/100 * 255 + 0.5)
|
|
33
|
+
return QColor(strength, strength, strength)
|
libs/pascal_voc_io.py
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
# -*- coding: utf8 -*-
|
|
3
|
+
import sys
|
|
4
|
+
from xml.etree import ElementTree
|
|
5
|
+
from xml.etree.ElementTree import Element, SubElement
|
|
6
|
+
from lxml import etree
|
|
7
|
+
import codecs
|
|
8
|
+
from libs.constants import DEFAULT_ENCODING
|
|
9
|
+
from libs.ustr import ustr
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
XML_EXT = '.xml'
|
|
13
|
+
ENCODE_METHOD = DEFAULT_ENCODING
|
|
14
|
+
|
|
15
|
+
class PascalVocWriter:
|
|
16
|
+
|
|
17
|
+
def __init__(self, folder_name, filename, img_size, database_src='Unknown', local_img_path=None):
|
|
18
|
+
self.folder_name = folder_name
|
|
19
|
+
self.filename = filename
|
|
20
|
+
self.database_src = database_src
|
|
21
|
+
self.img_size = img_size
|
|
22
|
+
self.box_list = []
|
|
23
|
+
self.local_img_path = local_img_path
|
|
24
|
+
self.verified = False
|
|
25
|
+
|
|
26
|
+
def prettify(self, elem):
|
|
27
|
+
"""
|
|
28
|
+
Return a pretty-printed XML string for the Element.
|
|
29
|
+
"""
|
|
30
|
+
rough_string = ElementTree.tostring(elem, 'utf8')
|
|
31
|
+
root = etree.fromstring(rough_string)
|
|
32
|
+
return etree.tostring(root, pretty_print=True, encoding=ENCODE_METHOD).replace(" ".encode(), "\t".encode())
|
|
33
|
+
# minidom does not support UTF-8
|
|
34
|
+
# reparsed = minidom.parseString(rough_string)
|
|
35
|
+
# return reparsed.toprettyxml(indent="\t", encoding=ENCODE_METHOD)
|
|
36
|
+
|
|
37
|
+
def gen_xml(self):
|
|
38
|
+
"""
|
|
39
|
+
Return XML root
|
|
40
|
+
"""
|
|
41
|
+
# Check conditions
|
|
42
|
+
if self.filename is None or \
|
|
43
|
+
self.folder_name is None or \
|
|
44
|
+
self.img_size is None:
|
|
45
|
+
return None
|
|
46
|
+
|
|
47
|
+
top = Element('annotation')
|
|
48
|
+
if self.verified:
|
|
49
|
+
top.set('verified', 'yes')
|
|
50
|
+
|
|
51
|
+
folder = SubElement(top, 'folder')
|
|
52
|
+
folder.text = self.folder_name
|
|
53
|
+
|
|
54
|
+
filename = SubElement(top, 'filename')
|
|
55
|
+
filename.text = self.filename
|
|
56
|
+
|
|
57
|
+
if self.local_img_path is not None:
|
|
58
|
+
local_img_path = SubElement(top, 'path')
|
|
59
|
+
local_img_path.text = self.local_img_path
|
|
60
|
+
|
|
61
|
+
source = SubElement(top, 'source')
|
|
62
|
+
database = SubElement(source, 'database')
|
|
63
|
+
database.text = self.database_src
|
|
64
|
+
|
|
65
|
+
size_part = SubElement(top, 'size')
|
|
66
|
+
width = SubElement(size_part, 'width')
|
|
67
|
+
height = SubElement(size_part, 'height')
|
|
68
|
+
depth = SubElement(size_part, 'depth')
|
|
69
|
+
width.text = str(self.img_size[1])
|
|
70
|
+
height.text = str(self.img_size[0])
|
|
71
|
+
if len(self.img_size) == 3:
|
|
72
|
+
depth.text = str(self.img_size[2])
|
|
73
|
+
else:
|
|
74
|
+
depth.text = '1'
|
|
75
|
+
|
|
76
|
+
segmented = SubElement(top, 'segmented')
|
|
77
|
+
segmented.text = '0'
|
|
78
|
+
return top
|
|
79
|
+
|
|
80
|
+
def add_bnd_box(self, x_min, y_min, x_max, y_max, name, difficult):
|
|
81
|
+
bnd_box = {'xmin': x_min, 'ymin': y_min, 'xmax': x_max, 'ymax': y_max}
|
|
82
|
+
bnd_box['name'] = name
|
|
83
|
+
bnd_box['difficult'] = difficult
|
|
84
|
+
self.box_list.append(bnd_box)
|
|
85
|
+
|
|
86
|
+
def append_objects(self, top):
|
|
87
|
+
for each_object in self.box_list:
|
|
88
|
+
object_item = SubElement(top, 'object')
|
|
89
|
+
name = SubElement(object_item, 'name')
|
|
90
|
+
name.text = ustr(each_object['name'])
|
|
91
|
+
pose = SubElement(object_item, 'pose')
|
|
92
|
+
pose.text = "Unspecified"
|
|
93
|
+
truncated = SubElement(object_item, 'truncated')
|
|
94
|
+
if int(float(each_object['ymax'])) == int(float(self.img_size[0])) or (int(float(each_object['ymin'])) == 1):
|
|
95
|
+
truncated.text = "1" # max == height or min
|
|
96
|
+
elif (int(float(each_object['xmax'])) == int(float(self.img_size[1]))) or (int(float(each_object['xmin'])) == 1):
|
|
97
|
+
truncated.text = "1" # max == width or min
|
|
98
|
+
else:
|
|
99
|
+
truncated.text = "0"
|
|
100
|
+
difficult = SubElement(object_item, 'difficult')
|
|
101
|
+
difficult.text = str(bool(each_object['difficult']) & 1)
|
|
102
|
+
bnd_box = SubElement(object_item, 'bndbox')
|
|
103
|
+
x_min = SubElement(bnd_box, 'xmin')
|
|
104
|
+
x_min.text = str(each_object['xmin'])
|
|
105
|
+
y_min = SubElement(bnd_box, 'ymin')
|
|
106
|
+
y_min.text = str(each_object['ymin'])
|
|
107
|
+
x_max = SubElement(bnd_box, 'xmax')
|
|
108
|
+
x_max.text = str(each_object['xmax'])
|
|
109
|
+
y_max = SubElement(bnd_box, 'ymax')
|
|
110
|
+
y_max.text = str(each_object['ymax'])
|
|
111
|
+
|
|
112
|
+
def save(self, target_file=None):
|
|
113
|
+
root = self.gen_xml()
|
|
114
|
+
self.append_objects(root)
|
|
115
|
+
out_file = None
|
|
116
|
+
if target_file is None:
|
|
117
|
+
out_file = codecs.open(
|
|
118
|
+
self.filename + XML_EXT, 'w', encoding=ENCODE_METHOD)
|
|
119
|
+
else:
|
|
120
|
+
out_file = codecs.open(target_file, 'w', encoding=ENCODE_METHOD)
|
|
121
|
+
|
|
122
|
+
prettify_result = self.prettify(root)
|
|
123
|
+
out_file.write(prettify_result.decode('utf8'))
|
|
124
|
+
out_file.close()
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class PascalVocReader:
|
|
128
|
+
|
|
129
|
+
def __init__(self, file_path):
|
|
130
|
+
# shapes type:
|
|
131
|
+
# [labbel, [(x1,y1), (x2,y2), (x3,y3), (x4,y4)], color, color, difficult]
|
|
132
|
+
self.shapes = []
|
|
133
|
+
self.file_path = file_path
|
|
134
|
+
self.verified = False
|
|
135
|
+
try:
|
|
136
|
+
self.parse_xml()
|
|
137
|
+
except:
|
|
138
|
+
pass
|
|
139
|
+
|
|
140
|
+
def get_shapes(self):
|
|
141
|
+
return self.shapes
|
|
142
|
+
|
|
143
|
+
def add_shape(self, label, bnd_box, difficult):
|
|
144
|
+
x_min = int(float(bnd_box.find('xmin').text))
|
|
145
|
+
y_min = int(float(bnd_box.find('ymin').text))
|
|
146
|
+
x_max = int(float(bnd_box.find('xmax').text))
|
|
147
|
+
y_max = int(float(bnd_box.find('ymax').text))
|
|
148
|
+
points = [(x_min, y_min), (x_max, y_min), (x_max, y_max), (x_min, y_max)]
|
|
149
|
+
self.shapes.append((label, points, None, None, difficult))
|
|
150
|
+
|
|
151
|
+
def parse_xml(self):
|
|
152
|
+
assert self.file_path.endswith(XML_EXT), "Unsupported file format"
|
|
153
|
+
parser = etree.XMLParser(encoding=ENCODE_METHOD)
|
|
154
|
+
xml_tree = ElementTree.parse(self.file_path, parser=parser).getroot()
|
|
155
|
+
filename = xml_tree.find('filename').text
|
|
156
|
+
try:
|
|
157
|
+
verified = xml_tree.attrib['verified']
|
|
158
|
+
if verified == 'yes':
|
|
159
|
+
self.verified = True
|
|
160
|
+
except KeyError:
|
|
161
|
+
self.verified = False
|
|
162
|
+
|
|
163
|
+
for object_iter in xml_tree.findall('object'):
|
|
164
|
+
bnd_box = object_iter.find("bndbox")
|
|
165
|
+
label = object_iter.find('name').text
|
|
166
|
+
# Add chris
|
|
167
|
+
difficult = False
|
|
168
|
+
if object_iter.find('difficult') is not None:
|
|
169
|
+
difficult = bool(int(object_iter.find('difficult').text))
|
|
170
|
+
self.add_shape(label, bnd_box, difficult)
|
|
171
|
+
return True
|