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.
@@ -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,,
@@ -0,0 +1,6 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py2-none-any
5
+ Tag: py3-none-any
6
+
@@ -0,0 +1,2 @@
1
+ [gui_scripts]
2
+ labelcraft = main:main
@@ -0,0 +1,3 @@
1
+ libs
2
+ main
3
+ resources
libs/__init__.py ADDED
@@ -0,0 +1,2 @@
1
+ __version_info__ = ('1', '8', '6')
2
+ __version__ = '.'.join(__version_info__)
@@ -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)