LabelCraft 1.8.6__py2.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.
- labelcraft-1.8.6.dist-info/METADATA +71 -0
- labelcraft-1.8.6.dist-info/RECORD +33 -0
- labelcraft-1.8.6.dist-info/WHEEL +6 -0
- labelcraft-1.8.6.dist-info/entry_points.txt +2 -0
- labelcraft-1.8.6.dist-info/top_level.txt +3 -0
- libs/__init__.py +2 -0
- libs/annotation_converter.py +531 -0
- libs/canvas.py +771 -0
- libs/coco_io.py +102 -0
- libs/colorDialog.py +38 -0
- libs/combobox.py +23 -0
- libs/constants.py +20 -0
- libs/create_ml_io.py +135 -0
- libs/csv_io.py +53 -0
- libs/default_label_combobox.py +23 -0
- libs/hashableQListWidgetItem.py +15 -0
- libs/i18n.py +86 -0
- libs/labelDialog.py +91 -0
- libs/labelFile.py +267 -0
- libs/lightWidget.py +85 -0
- libs/newProjectDialog.py +220 -0
- libs/pascal_voc_io.py +171 -0
- libs/project.py +199 -0
- libs/resources.py +34899 -0
- libs/settings.py +45 -0
- libs/shape.py +205 -0
- libs/toolBar.py +35 -0
- libs/ustr.py +6 -0
- libs/utils.py +109 -0
- libs/yolo_io.py +155 -0
- libs/zoomWidget.py +85 -0
- main.py +4455 -0
- resources.py +16023 -0
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: LabelCraft
|
|
3
|
+
Version: 1.8.6
|
|
4
|
+
Summary: LabelCraft - A modern graphical image annotation tool based on labelImg
|
|
5
|
+
Home-page: https://github.com/syd168/LabelCraft
|
|
6
|
+
Author: TzuTa Lin (Original labelImg), LabelCraft Contributors
|
|
7
|
+
Author-email: tzu.ta.lin@gmail.com
|
|
8
|
+
License: MIT license
|
|
9
|
+
Keywords: labelCraft labelImg labelTool development annotation deeplearning
|
|
10
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Natural Language :: English
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
21
|
+
Classifier: Topic :: Multimedia :: Graphics
|
|
22
|
+
Requires-Python: >=3.8.0
|
|
23
|
+
Description-Content-Type: text/x-rst
|
|
24
|
+
Requires-Dist: pyside6>=6.5.0
|
|
25
|
+
Requires-Dist: lxml>=4.9.0
|
|
26
|
+
Dynamic: author
|
|
27
|
+
Dynamic: author-email
|
|
28
|
+
Dynamic: classifier
|
|
29
|
+
Dynamic: description
|
|
30
|
+
Dynamic: description-content-type
|
|
31
|
+
Dynamic: home-page
|
|
32
|
+
Dynamic: keywords
|
|
33
|
+
Dynamic: license
|
|
34
|
+
Dynamic: requires-dist
|
|
35
|
+
Dynamic: requires-python
|
|
36
|
+
Dynamic: summary
|
|
37
|
+
|
|
38
|
+
LabelCraft - Image Annotation Tool
|
|
39
|
+
===================================
|
|
40
|
+
|
|
41
|
+
LabelCraft is a modern graphical image annotation tool based on labelImg.
|
|
42
|
+
|
|
43
|
+
Features
|
|
44
|
+
--------
|
|
45
|
+
|
|
46
|
+
- Support for multiple annotation formats (PASCAL VOC, YOLO, CreateML, COCO, CSV)
|
|
47
|
+
- Multi-language support
|
|
48
|
+
- Brightness adjustment
|
|
49
|
+
- Zoom and pan
|
|
50
|
+
- Keyboard shortcuts
|
|
51
|
+
- Predefined class management
|
|
52
|
+
- Annotation verification
|
|
53
|
+
- GitHub Actions automated builds
|
|
54
|
+
|
|
55
|
+
Note: This project is based on the original labelImg project by TzuTa Lin.
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
History
|
|
59
|
+
=======
|
|
60
|
+
|
|
61
|
+
1.8.6 (2024-XX-XX)
|
|
62
|
+
------------------
|
|
63
|
+
|
|
64
|
+
* Based on labelImg project
|
|
65
|
+
* Added modern UI improvements
|
|
66
|
+
* Enhanced multi-language support
|
|
67
|
+
* Added brightness adjustment feature
|
|
68
|
+
* Improved annotation workflow
|
|
69
|
+
* Added GitHub Actions CI/CD
|
|
70
|
+
|
|
71
|
+
Note: This project is a fork and enhancement of the original labelImg project.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
main.py,sha256=qnUDgWV9coUPM9AYpwU27IFbI0ZZJrbXrXpPqfWmuRw,194063
|
|
2
|
+
resources.py,sha256=CESQ59qOPqlRXAXxVEXnZRJ_Zu5RzaM_m_TtPs3rZW0,646332
|
|
3
|
+
libs/__init__.py,sha256=aowPcBtSZJJEfdXOoT4X-Arg8E2tlXm-CBnjjeuXkj8,76
|
|
4
|
+
libs/annotation_converter.py,sha256=Xfv-7i-8Mdeh-eH4Dq9HcD_xGOw0e2LBsEZSegAu0Zo,19720
|
|
5
|
+
libs/canvas.py,sha256=m7GEyCMHfbfh9vJvZ7q9EUvDGlffAcjbk8V1x7xgCMI,30516
|
|
6
|
+
libs/coco_io.py,sha256=M6vCVWp3maXj4PR9OYOxm7CGUk5_TcgLub2XHSBtHCM,3407
|
|
7
|
+
libs/colorDialog.py,sha256=X1Ib0XtYKjm-ffPehDjbS7DTosvIDVLAazx6jHU_ths,1502
|
|
8
|
+
libs/combobox.py,sha256=v_wHunzg8xuT0_MUrtbFIm4czVDxA_pvenCPLi3Ny1k,588
|
|
9
|
+
libs/constants.py,sha256=o9eVCe3sXA0FF3WUsJKMLoyuTfFusWJJ1MC5J4H-rAM,668
|
|
10
|
+
libs/create_ml_io.py,sha256=B8XRDRQ1PUjfuH2Qqw7jyvGbO6oCB7Oy2WhcqieaDpo,3978
|
|
11
|
+
libs/csv_io.py,sha256=FD3tCEEoelgLjw5qwPqzGCPEy6W70upxJz508RvjKYk,1793
|
|
12
|
+
libs/default_label_combobox.py,sha256=9vuMqTV7SZQrZ8xt3E3PyeCq5FiI6znpS6LpR7ygU0Y,670
|
|
13
|
+
libs/hashableQListWidgetItem.py,sha256=r8NqBrxm-BtG-KYmv0o-MgC2yj7J9IVCFz2uDLYKRbY,344
|
|
14
|
+
libs/i18n.py,sha256=EKUy_JXw_DtyI3148Zlj7tJQBV6XkElHRUBRLiHY4Dw,2962
|
|
15
|
+
libs/labelDialog.py,sha256=grAMR8_kRVC84DGNPlKkljRSN_X6N46DBwnym9dmryo,3544
|
|
16
|
+
libs/labelFile.py,sha256=9WQ0wjT0N0rAo68nzdpzIif5Vl5A0bZaipeeuCQraTU,10190
|
|
17
|
+
libs/lightWidget.py,sha256=8ZY7tYEUn8JZvaLJIDA8uoc-v2r10KemVCoZshlXIgE,3033
|
|
18
|
+
libs/newProjectDialog.py,sha256=lZoAEx0aLpVm1zTogbRGS2-wzI06qjfJOSaxf6lhU1w,8293
|
|
19
|
+
libs/pascal_voc_io.py,sha256=IHwovl8pp-r9wiE80_W_CQc6GGSb1MNj6U4-snZdKfM,6324
|
|
20
|
+
libs/project.py,sha256=pSUpRaToYO7RxgJPptzNOLnWbloapHvhy7xYbzSd7hQ,7774
|
|
21
|
+
libs/resources.py,sha256=zxLdp_0w70QiLYboQUXMDSm_O6DfYeueCNsFa0ffCW8,1560848
|
|
22
|
+
libs/settings.py,sha256=QRfLkhCSG640UoQhCsibrkMycJxjOT7A_p1wcQm0Mqc,1231
|
|
23
|
+
libs/shape.py,sha256=REh8S69tUQWkuubbStvg-4An4gp1zXmdabyf1ivoH8I,6603
|
|
24
|
+
libs/toolBar.py,sha256=dFdoSsqdlUWbja2q1tgsYd714JbOKkv6weh8lSCM6-c,1111
|
|
25
|
+
libs/ustr.py,sha256=C52vDriWdTUk2J0ohK30t0TRPMutEpbpY4CbZnPbZBE,111
|
|
26
|
+
libs/utils.py,sha256=lfVTPOstPSjTnLvAfVLIfrZB9sGlopqTH3B-dW90qF0,2707
|
|
27
|
+
libs/yolo_io.py,sha256=mT5Tv3k828vXfwmne1dQgNhTpFR-HAdTraFXdfg50gA,5622
|
|
28
|
+
libs/zoomWidget.py,sha256=qe1jfAsflDOJLIT8e6-UeSDrHCV1E_tEk2-BIh8xlts,3127
|
|
29
|
+
labelcraft-1.8.6.dist-info/METADATA,sha256=vrVcU_HLbBwDnFkJFolfKT7G38L1vVu4_O1o3uw0888,2165
|
|
30
|
+
labelcraft-1.8.6.dist-info/WHEEL,sha256=TdQ5LtNwLuxTCjgxN51AgdU5w-KkB9ttmLbzjTH02pg,109
|
|
31
|
+
labelcraft-1.8.6.dist-info/entry_points.txt,sha256=FbGSZFTwuGHC3woywLpgAOy4O9YQ5dN8rx6L6fBJ0Ao,37
|
|
32
|
+
labelcraft-1.8.6.dist-info/top_level.txt,sha256=rC1CE9s5nvkt1E-f3Yzgx7NQO6XlCxcnW4Kjv2mCT4k,20
|
|
33
|
+
labelcraft-1.8.6.dist-info/RECORD,,
|
libs/__init__.py
ADDED
|
@@ -0,0 +1,531 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
Annotation Converter Module
|
|
5
|
+
|
|
6
|
+
Provides unified annotation format conversion between different formats.
|
|
7
|
+
All conversions go through an internal intermediate format to ensure consistency.
|
|
8
|
+
|
|
9
|
+
Internal Format Structure:
|
|
10
|
+
{
|
|
11
|
+
"image_path": str, # Path to the image file
|
|
12
|
+
"image_width": int, # Image width in pixels
|
|
13
|
+
"image_height": int, # Image height in pixels
|
|
14
|
+
"annotations": [ # List of annotations
|
|
15
|
+
{
|
|
16
|
+
"label": str, # Object class label
|
|
17
|
+
"bbox": [x, y, w, h], # Bounding box (x, y) is top-left corner
|
|
18
|
+
"difficult": bool, # Whether the object is difficult to recognize
|
|
19
|
+
"truncated": bool, # Whether the object is truncated
|
|
20
|
+
"polygon": [[x1,y1], [x2,y2], ...] # Optional: polygon points
|
|
21
|
+
}
|
|
22
|
+
]
|
|
23
|
+
}
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
import os
|
|
27
|
+
import json
|
|
28
|
+
import xml.etree.ElementTree as ET
|
|
29
|
+
from xml.dom import minidom
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class AnnotationConverter:
|
|
33
|
+
"""Unified annotation format converter"""
|
|
34
|
+
|
|
35
|
+
@staticmethod
|
|
36
|
+
def read_voc(xml_path):
|
|
37
|
+
"""Read PASCAL VOC XML format and convert to internal format"""
|
|
38
|
+
tree = ET.parse(xml_path)
|
|
39
|
+
root = tree.getroot()
|
|
40
|
+
|
|
41
|
+
# Get image info
|
|
42
|
+
filename = root.find('filename').text
|
|
43
|
+
size = root.find('size')
|
|
44
|
+
width = int(size.find('width').text)
|
|
45
|
+
height = int(size.find('height').text)
|
|
46
|
+
|
|
47
|
+
# Build image path (assume same directory as XML)
|
|
48
|
+
image_path = os.path.join(os.path.dirname(xml_path), filename)
|
|
49
|
+
|
|
50
|
+
# Parse annotations
|
|
51
|
+
annotations = []
|
|
52
|
+
for obj in root.findall('object'):
|
|
53
|
+
label = obj.find('name').text
|
|
54
|
+
bbox_elem = obj.find('bndbox')
|
|
55
|
+
xmin = int(bbox_elem.find('xmin').text)
|
|
56
|
+
ymin = int(bbox_elem.find('ymin').text)
|
|
57
|
+
xmax = int(bbox_elem.find('xmax').text)
|
|
58
|
+
ymax = int(bbox_elem.find('ymax').text)
|
|
59
|
+
|
|
60
|
+
difficult = obj.find('difficult')
|
|
61
|
+
difficult = int(difficult.text) if difficult is not None else 0
|
|
62
|
+
|
|
63
|
+
truncated = obj.find('truncated')
|
|
64
|
+
truncated = int(truncated.text) if truncated is not None else 0
|
|
65
|
+
|
|
66
|
+
annotations.append({
|
|
67
|
+
'label': label,
|
|
68
|
+
'bbox': [xmin, ymin, xmax - xmin, ymax - ymin],
|
|
69
|
+
'difficult': bool(difficult),
|
|
70
|
+
'truncated': bool(truncated)
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
'image_path': image_path,
|
|
75
|
+
'image_width': width,
|
|
76
|
+
'image_height': height,
|
|
77
|
+
'annotations': annotations
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
@staticmethod
|
|
81
|
+
def read_yolo(txt_path, classes_file=None):
|
|
82
|
+
"""Read YOLO TXT format and convert to internal format"""
|
|
83
|
+
# Find corresponding image file
|
|
84
|
+
base_name = os.path.splitext(txt_path)[0]
|
|
85
|
+
image_extensions = ['.jpg', '.jpeg', '.png', '.bmp', '.tif', '.tiff']
|
|
86
|
+
image_path = None
|
|
87
|
+
|
|
88
|
+
for ext in image_extensions:
|
|
89
|
+
candidate = base_name + ext
|
|
90
|
+
if os.path.exists(candidate):
|
|
91
|
+
image_path = candidate
|
|
92
|
+
break
|
|
93
|
+
|
|
94
|
+
if not image_path:
|
|
95
|
+
raise FileNotFoundError(f"Cannot find image file for {txt_path}")
|
|
96
|
+
|
|
97
|
+
# Read image dimensions
|
|
98
|
+
from PySide6.QtGui import QImage
|
|
99
|
+
img = QImage(image_path)
|
|
100
|
+
width = img.width()
|
|
101
|
+
height = img.height()
|
|
102
|
+
|
|
103
|
+
# Load classes if provided
|
|
104
|
+
classes = []
|
|
105
|
+
if classes_file and os.path.exists(classes_file):
|
|
106
|
+
with open(classes_file, 'r') as f:
|
|
107
|
+
classes = [line.strip() for line in f.readlines()]
|
|
108
|
+
|
|
109
|
+
# Parse annotations
|
|
110
|
+
annotations = []
|
|
111
|
+
if os.path.exists(txt_path):
|
|
112
|
+
with open(txt_path, 'r') as f:
|
|
113
|
+
for line in f.readlines():
|
|
114
|
+
parts = line.strip().split()
|
|
115
|
+
if len(parts) >= 5:
|
|
116
|
+
class_id = int(parts[0])
|
|
117
|
+
x_center = float(parts[1])
|
|
118
|
+
y_center = float(parts[2])
|
|
119
|
+
w = float(parts[3])
|
|
120
|
+
h = float(parts[4])
|
|
121
|
+
|
|
122
|
+
# Convert from normalized to absolute coordinates
|
|
123
|
+
x = int((x_center - w / 2) * width)
|
|
124
|
+
y = int((y_center - h / 2) * height)
|
|
125
|
+
w_abs = int(w * width)
|
|
126
|
+
h_abs = int(h * height)
|
|
127
|
+
|
|
128
|
+
# Get label name
|
|
129
|
+
label = classes[class_id] if class_id < len(classes) else str(class_id)
|
|
130
|
+
|
|
131
|
+
annotations.append({
|
|
132
|
+
'label': label,
|
|
133
|
+
'bbox': [x, y, w_abs, h_abs],
|
|
134
|
+
'difficult': False,
|
|
135
|
+
'truncated': False
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
'image_path': image_path,
|
|
140
|
+
'image_width': width,
|
|
141
|
+
'image_height': height,
|
|
142
|
+
'annotations': annotations
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
@staticmethod
|
|
146
|
+
def read_createml(json_path):
|
|
147
|
+
"""Read CreateML JSON format and convert to internal format"""
|
|
148
|
+
with open(json_path, 'r') as f:
|
|
149
|
+
data = json.load(f)
|
|
150
|
+
|
|
151
|
+
# CreateML format is a list, find the entry for this file
|
|
152
|
+
base_name = os.path.basename(json_path)
|
|
153
|
+
|
|
154
|
+
for entry in data:
|
|
155
|
+
if entry.get('image') == base_name or entry.get('image').startswith(os.path.basename(json_path).replace('.json', '')):
|
|
156
|
+
image_path = os.path.join(os.path.dirname(json_path), entry['image'])
|
|
157
|
+
|
|
158
|
+
# Get image dimensions
|
|
159
|
+
from PySide6.QtGui import QImage
|
|
160
|
+
img = QImage(image_path)
|
|
161
|
+
width = img.width()
|
|
162
|
+
height = img.height()
|
|
163
|
+
|
|
164
|
+
# Parse annotations
|
|
165
|
+
annotations = []
|
|
166
|
+
for annotation in entry.get('annotations', []):
|
|
167
|
+
label = annotation['label']
|
|
168
|
+
coords = annotation['coordinates']
|
|
169
|
+
|
|
170
|
+
# CreateML uses x, y, width, height
|
|
171
|
+
x = int(coords['x'])
|
|
172
|
+
y = int(coords['y'])
|
|
173
|
+
w = int(coords['width'])
|
|
174
|
+
h = int(coords['height'])
|
|
175
|
+
|
|
176
|
+
annotations.append({
|
|
177
|
+
'label': label,
|
|
178
|
+
'bbox': [x, y, w, h],
|
|
179
|
+
'difficult': False,
|
|
180
|
+
'truncated': False
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
return {
|
|
184
|
+
'image_path': image_path,
|
|
185
|
+
'image_width': width,
|
|
186
|
+
'image_height': height,
|
|
187
|
+
'annotations': annotations
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
raise ValueError(f"Cannot find matching entry in CreateML file: {json_path}")
|
|
191
|
+
|
|
192
|
+
@staticmethod
|
|
193
|
+
def read_coco(json_path, image_id=None):
|
|
194
|
+
"""Read COCO JSON format and convert to internal format"""
|
|
195
|
+
with open(json_path, 'r') as f:
|
|
196
|
+
coco_data = json.load(f)
|
|
197
|
+
|
|
198
|
+
# Build category map
|
|
199
|
+
categories = {cat['id']: cat['name'] for cat in coco_data.get('categories', [])}
|
|
200
|
+
|
|
201
|
+
# Find image
|
|
202
|
+
images = coco_data.get('images', [])
|
|
203
|
+
if image_id is not None:
|
|
204
|
+
image_info = next((img for img in images if img['id'] == image_id), None)
|
|
205
|
+
else:
|
|
206
|
+
# Use first image
|
|
207
|
+
image_info = images[0] if images else None
|
|
208
|
+
|
|
209
|
+
if not image_info:
|
|
210
|
+
raise ValueError("Cannot find image in COCO file")
|
|
211
|
+
|
|
212
|
+
# Get image dimensions
|
|
213
|
+
width = image_info['width']
|
|
214
|
+
height = image_info['height']
|
|
215
|
+
image_path = image_info.get('file_name', '')
|
|
216
|
+
|
|
217
|
+
# Parse annotations for this image
|
|
218
|
+
annotations = []
|
|
219
|
+
for ann in coco_data.get('annotations', []):
|
|
220
|
+
if ann['image_id'] == image_info['id']:
|
|
221
|
+
bbox = ann['bbox'] # [x, y, width, height]
|
|
222
|
+
category_id = ann['category_id']
|
|
223
|
+
label = categories.get(category_id, str(category_id))
|
|
224
|
+
|
|
225
|
+
annotations.append({
|
|
226
|
+
'label': label,
|
|
227
|
+
'bbox': [int(bbox[0]), int(bbox[1]), int(bbox[2]), int(bbox[3])],
|
|
228
|
+
'difficult': False,
|
|
229
|
+
'truncated': False
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
return {
|
|
233
|
+
'image_path': image_path,
|
|
234
|
+
'image_width': width,
|
|
235
|
+
'image_height': height,
|
|
236
|
+
'annotations': annotations
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
@staticmethod
|
|
240
|
+
def read_csv(csv_path):
|
|
241
|
+
"""Read CSV format and convert to internal format"""
|
|
242
|
+
import csv
|
|
243
|
+
|
|
244
|
+
# Find corresponding image file
|
|
245
|
+
base_name = os.path.splitext(csv_path)[0]
|
|
246
|
+
image_extensions = ['.jpg', '.jpeg', '.png', '.bmp', '.tif', '.tiff']
|
|
247
|
+
image_path = None
|
|
248
|
+
|
|
249
|
+
for ext in image_extensions:
|
|
250
|
+
candidate = base_name + ext
|
|
251
|
+
if os.path.exists(candidate):
|
|
252
|
+
image_path = candidate
|
|
253
|
+
break
|
|
254
|
+
|
|
255
|
+
if not image_path:
|
|
256
|
+
raise FileNotFoundError(f"Cannot find image file for {csv_path}")
|
|
257
|
+
|
|
258
|
+
# Read image dimensions
|
|
259
|
+
from PySide6.QtGui import QImage
|
|
260
|
+
img = QImage(image_path)
|
|
261
|
+
width = img.width()
|
|
262
|
+
height = img.height()
|
|
263
|
+
|
|
264
|
+
# Parse annotations
|
|
265
|
+
annotations = []
|
|
266
|
+
with open(csv_path, 'r') as f:
|
|
267
|
+
reader = csv.DictReader(f)
|
|
268
|
+
for row in reader:
|
|
269
|
+
# Expected columns: filename, width, height, class, xmin, ymin, xmax, ymax
|
|
270
|
+
label = row.get('class', row.get('label', ''))
|
|
271
|
+
xmin = int(row.get('xmin', 0))
|
|
272
|
+
ymin = int(row.get('ymin', 0))
|
|
273
|
+
xmax = int(row.get('xmax', 0))
|
|
274
|
+
ymax = int(row.get('ymax', 0))
|
|
275
|
+
|
|
276
|
+
annotations.append({
|
|
277
|
+
'label': label,
|
|
278
|
+
'bbox': [xmin, ymin, xmax - xmin, ymax - ymin],
|
|
279
|
+
'difficult': False,
|
|
280
|
+
'truncated': False
|
|
281
|
+
})
|
|
282
|
+
|
|
283
|
+
return {
|
|
284
|
+
'image_path': image_path,
|
|
285
|
+
'image_width': width,
|
|
286
|
+
'image_height': height,
|
|
287
|
+
'annotations': annotations
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
@staticmethod
|
|
291
|
+
def write_voc(internal_data, output_path):
|
|
292
|
+
"""Write internal format to PASCAL VOC XML"""
|
|
293
|
+
root = ET.Element('annotation')
|
|
294
|
+
|
|
295
|
+
# Add folder (optional)
|
|
296
|
+
folder = ET.SubElement(root, 'folder')
|
|
297
|
+
folder.text = os.path.basename(os.path.dirname(internal_data['image_path']))
|
|
298
|
+
|
|
299
|
+
# Add filename
|
|
300
|
+
filename = ET.SubElement(root, 'filename')
|
|
301
|
+
filename.text = os.path.basename(internal_data['image_path'])
|
|
302
|
+
|
|
303
|
+
# Add source (optional)
|
|
304
|
+
source = ET.SubElement(root, 'source')
|
|
305
|
+
database = ET.SubElement(source, 'database')
|
|
306
|
+
database.text = 'Unknown'
|
|
307
|
+
|
|
308
|
+
# Add size
|
|
309
|
+
size = ET.SubElement(root, 'size')
|
|
310
|
+
width = ET.SubElement(size, 'width')
|
|
311
|
+
width.text = str(internal_data['image_width'])
|
|
312
|
+
height = ET.SubElement(size, 'height')
|
|
313
|
+
height.text = str(internal_data['image_height'])
|
|
314
|
+
depth = ET.SubElement(size, 'depth')
|
|
315
|
+
depth.text = '3'
|
|
316
|
+
|
|
317
|
+
# Add segmented (optional)
|
|
318
|
+
segmented = ET.SubElement(root, 'segmented')
|
|
319
|
+
segmented.text = '0'
|
|
320
|
+
|
|
321
|
+
# Add objects
|
|
322
|
+
for ann in internal_data['annotations']:
|
|
323
|
+
obj = ET.SubElement(root, 'object')
|
|
324
|
+
|
|
325
|
+
name = ET.SubElement(obj, 'name')
|
|
326
|
+
name.text = ann['label']
|
|
327
|
+
|
|
328
|
+
pose = ET.SubElement(obj, 'pose')
|
|
329
|
+
pose.text = 'Unspecified'
|
|
330
|
+
|
|
331
|
+
truncated = ET.SubElement(obj, 'truncated')
|
|
332
|
+
truncated.text = '1' if ann.get('truncated', False) else '0'
|
|
333
|
+
|
|
334
|
+
difficult = ET.SubElement(obj, 'difficult')
|
|
335
|
+
difficult.text = '1' if ann.get('difficult', False) else '0'
|
|
336
|
+
|
|
337
|
+
bndbox = ET.SubElement(obj, 'bndbox')
|
|
338
|
+
xmin = ET.SubElement(bndbox, 'xmin')
|
|
339
|
+
xmin.text = str(ann['bbox'][0])
|
|
340
|
+
ymin = ET.SubElement(bndbox, 'ymin')
|
|
341
|
+
ymin.text = str(ann['bbox'][1])
|
|
342
|
+
xmax = ET.SubElement(bndbox, 'xmax')
|
|
343
|
+
xmax.text = str(ann['bbox'][0] + ann['bbox'][2])
|
|
344
|
+
ymax = ET.SubElement(bndbox, 'ymax')
|
|
345
|
+
ymax.text = str(ann['bbox'][1] + ann['bbox'][3])
|
|
346
|
+
|
|
347
|
+
# Write with pretty printing
|
|
348
|
+
xml_str = ET.tostring(root, encoding='unicode')
|
|
349
|
+
dom = minidom.parseString(xml_str)
|
|
350
|
+
pretty_xml = dom.toprettyxml(indent=' ')
|
|
351
|
+
|
|
352
|
+
# Remove extra blank lines
|
|
353
|
+
lines = [line for line in pretty_xml.split('\n') if line.strip()]
|
|
354
|
+
with open(output_path, 'w', encoding='utf-8') as f:
|
|
355
|
+
f.write('\n'.join(lines))
|
|
356
|
+
|
|
357
|
+
@staticmethod
|
|
358
|
+
def write_yolo(internal_data, output_path, classes_list):
|
|
359
|
+
"""Write internal format to YOLO TXT"""
|
|
360
|
+
# Build class to ID mapping
|
|
361
|
+
class_to_id = {cls: idx for idx, cls in enumerate(classes_list)}
|
|
362
|
+
|
|
363
|
+
width = internal_data['image_width']
|
|
364
|
+
height = internal_data['image_height']
|
|
365
|
+
|
|
366
|
+
lines = []
|
|
367
|
+
for ann in internal_data['annotations']:
|
|
368
|
+
label = ann['label']
|
|
369
|
+
if label not in class_to_id:
|
|
370
|
+
print(f"Warning: Label '{label}' not in classes list, skipping")
|
|
371
|
+
continue
|
|
372
|
+
|
|
373
|
+
class_id = class_to_id[label]
|
|
374
|
+
x, y, w, h = ann['bbox']
|
|
375
|
+
|
|
376
|
+
# Convert to normalized center format
|
|
377
|
+
x_center = (x + w / 2) / width
|
|
378
|
+
y_center = (y + h / 2) / height
|
|
379
|
+
w_norm = w / width
|
|
380
|
+
h_norm = h / height
|
|
381
|
+
|
|
382
|
+
lines.append(f"{class_id} {x_center:.6f} {y_center:.6f} {w_norm:.6f} {h_norm:.6f}")
|
|
383
|
+
|
|
384
|
+
with open(output_path, 'w') as f:
|
|
385
|
+
f.write('\n'.join(lines))
|
|
386
|
+
if lines:
|
|
387
|
+
f.write('\n')
|
|
388
|
+
|
|
389
|
+
@staticmethod
|
|
390
|
+
def write_createml(internal_data, output_path):
|
|
391
|
+
"""Write internal format to CreateML JSON"""
|
|
392
|
+
image_filename = os.path.basename(internal_data['image_path'])
|
|
393
|
+
|
|
394
|
+
annotations = []
|
|
395
|
+
for ann in internal_data['annotations']:
|
|
396
|
+
x, y, w, h = ann['bbox']
|
|
397
|
+
annotations.append({
|
|
398
|
+
'label': ann['label'],
|
|
399
|
+
'coordinates': {
|
|
400
|
+
'x': x,
|
|
401
|
+
'y': y,
|
|
402
|
+
'width': w,
|
|
403
|
+
'height': h
|
|
404
|
+
}
|
|
405
|
+
})
|
|
406
|
+
|
|
407
|
+
data = [{
|
|
408
|
+
'image': image_filename,
|
|
409
|
+
'annotations': annotations
|
|
410
|
+
}]
|
|
411
|
+
|
|
412
|
+
with open(output_path, 'w', encoding='utf-8') as f:
|
|
413
|
+
json.dump(data, f, indent=2, ensure_ascii=False)
|
|
414
|
+
|
|
415
|
+
@staticmethod
|
|
416
|
+
def write_coco(internal_data, output_path, categories_list):
|
|
417
|
+
"""Write internal format to COCO JSON"""
|
|
418
|
+
# Build category map
|
|
419
|
+
cat_map = {name: idx + 1 for idx, name in enumerate(categories_list)}
|
|
420
|
+
|
|
421
|
+
coco_data = {
|
|
422
|
+
'info': {
|
|
423
|
+
'description': 'Converted from LabelCraft',
|
|
424
|
+
'version': '1.0'
|
|
425
|
+
},
|
|
426
|
+
'licenses': [],
|
|
427
|
+
'images': [{
|
|
428
|
+
'id': 1,
|
|
429
|
+
'file_name': os.path.basename(internal_data['image_path']),
|
|
430
|
+
'width': internal_data['image_width'],
|
|
431
|
+
'height': internal_data['image_height']
|
|
432
|
+
}],
|
|
433
|
+
'annotations': [],
|
|
434
|
+
'categories': [{'id': idx + 1, 'name': name} for idx, name in enumerate(categories_list)]
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
ann_id = 1
|
|
438
|
+
for ann in internal_data['annotations']:
|
|
439
|
+
label = ann['label']
|
|
440
|
+
if label not in cat_map:
|
|
441
|
+
print(f"Warning: Label '{label}' not in categories list, skipping")
|
|
442
|
+
continue
|
|
443
|
+
|
|
444
|
+
x, y, w, h = ann['bbox']
|
|
445
|
+
coco_data['annotations'].append({
|
|
446
|
+
'id': ann_id,
|
|
447
|
+
'image_id': 1,
|
|
448
|
+
'category_id': cat_map[label],
|
|
449
|
+
'bbox': [x, y, w, h],
|
|
450
|
+
'area': w * h,
|
|
451
|
+
'iscrowd': 0
|
|
452
|
+
})
|
|
453
|
+
ann_id += 1
|
|
454
|
+
|
|
455
|
+
with open(output_path, 'w', encoding='utf-8') as f:
|
|
456
|
+
json.dump(coco_data, f, indent=2, ensure_ascii=False)
|
|
457
|
+
|
|
458
|
+
@staticmethod
|
|
459
|
+
def write_csv(internal_data, output_path):
|
|
460
|
+
"""Write internal format to CSV"""
|
|
461
|
+
import csv
|
|
462
|
+
|
|
463
|
+
with open(output_path, 'w', newline='', encoding='utf-8') as f:
|
|
464
|
+
writer = csv.writer(f)
|
|
465
|
+
writer.writerow(['filename', 'width', 'height', 'class', 'xmin', 'ymin', 'xmax', 'ymax'])
|
|
466
|
+
|
|
467
|
+
image_filename = os.path.basename(internal_data['image_path'])
|
|
468
|
+
width = internal_data['image_width']
|
|
469
|
+
height = internal_data['image_height']
|
|
470
|
+
|
|
471
|
+
for ann in internal_data['annotations']:
|
|
472
|
+
x, y, w, h = ann['bbox']
|
|
473
|
+
writer.writerow([
|
|
474
|
+
image_filename,
|
|
475
|
+
width,
|
|
476
|
+
height,
|
|
477
|
+
ann['label'],
|
|
478
|
+
x,
|
|
479
|
+
y,
|
|
480
|
+
x + w,
|
|
481
|
+
y + h
|
|
482
|
+
])
|
|
483
|
+
|
|
484
|
+
@staticmethod
|
|
485
|
+
def convert(input_path, input_format, output_path, output_format, classes_list=None):
|
|
486
|
+
"""
|
|
487
|
+
Convert annotation from one format to another
|
|
488
|
+
|
|
489
|
+
Args:
|
|
490
|
+
input_path: Path to input annotation file
|
|
491
|
+
input_format: Input format ('voc', 'yolo', 'createml', 'coco', 'csv')
|
|
492
|
+
output_path: Path to output annotation file
|
|
493
|
+
output_format: Output format ('voc', 'yolo', 'createml', 'coco', 'csv')
|
|
494
|
+
classes_list: List of class names (required for YOLO format)
|
|
495
|
+
"""
|
|
496
|
+
# Read input format to internal format
|
|
497
|
+
readers = {
|
|
498
|
+
'voc': AnnotationConverter.read_voc,
|
|
499
|
+
'yolo': AnnotationConverter.read_yolo,
|
|
500
|
+
'createml': AnnotationConverter.read_createml,
|
|
501
|
+
'coco': AnnotationConverter.read_coco,
|
|
502
|
+
'csv': AnnotationConverter.read_csv
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
if input_format not in readers:
|
|
506
|
+
raise ValueError(f"Unsupported input format: {input_format}")
|
|
507
|
+
|
|
508
|
+
internal_data = readers[input_format](input_path)
|
|
509
|
+
|
|
510
|
+
# Write internal format to output format
|
|
511
|
+
writers = {
|
|
512
|
+
'voc': AnnotationConverter.write_voc,
|
|
513
|
+
'yolo': AnnotationConverter.write_yolo,
|
|
514
|
+
'createml': AnnotationConverter.write_createml,
|
|
515
|
+
'coco': AnnotationConverter.write_coco,
|
|
516
|
+
'csv': AnnotationConverter.write_csv
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
if output_format not in writers:
|
|
520
|
+
raise ValueError(f"Unsupported output format: {output_format}")
|
|
521
|
+
|
|
522
|
+
# For YOLO and COCO, classes_list is required
|
|
523
|
+
if output_format in ['yolo', 'coco'] and not classes_list:
|
|
524
|
+
raise ValueError(f"classes_list is required for {output_format} format")
|
|
525
|
+
|
|
526
|
+
if output_format == 'yolo':
|
|
527
|
+
writers[output_format](internal_data, output_path, classes_list)
|
|
528
|
+
elif output_format == 'coco':
|
|
529
|
+
writers[output_format](internal_data, output_path, classes_list)
|
|
530
|
+
else:
|
|
531
|
+
writers[output_format](internal_data, output_path)
|