Skip to content
This repository has been archived by the owner on Aug 21, 2024. It is now read-only.

Commit

Permalink
feat: Support for exporting to YOLO OBB format (#287)
Browse files Browse the repository at this point in the history
* Added support for exporting to Yolo OBB format

* Corrected rotation and normalization.

* Fixed typo
  • Loading branch information
NickSwardh authored Jun 4, 2024
1 parent dec9c58 commit 9a3db49
Show file tree
Hide file tree
Showing 6 changed files with 1,068 additions and 19 deletions.
65 changes: 46 additions & 19 deletions label_studio_converter/converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@
get_annotator,
get_json_root_type,
prettify_result,
convert_annotation_to_yolo,
convert_annotation_to_yolo_obb
)
from label_studio_converter import brush
from label_studio_converter.audio import convert_to_asr_json_manifest
Expand All @@ -56,7 +58,8 @@ class Format(Enum):
BRUSH_TO_PNG = 9
ASR_MANIFEST = 10
YOLO = 11
CSV_OLD = 12
YOLO_OBB = 12
CSV_OLD = 13

def __str__(self):
return self.name
Expand Down Expand Up @@ -120,6 +123,13 @@ class Converter(object):
'the corresponding image file, that is object class, object coordinates, height & width.',
'link': 'https://labelstud.io/guide/export.html#YOLO',
'tags': ['image segmentation', 'object detection'],
},Format.YOLO_OBB: {
'title': 'YOLOv8 OBB',
'description': 'Popular TXT format is created for each image file. Each txt file contains annotations for '
'the corresponding image file. The YOLO OBB format designates bounding boxes by their four corner points '
'with coordinates normalized between 0 and 1, so it is possible to export rotated objects.',
'link': 'https://labelstud.io/guide/export.html#YOLO',
'tags': ['image segmentation', 'object detection'],
},
Format.BRUSH_TO_NUMPY: {
'title': 'Brush labels to NumPy',
Expand Down Expand Up @@ -215,15 +225,16 @@ def convert(self, input_data, output_data, format, is_dir=True, **kwargs):
self.convert_to_coco(
input_data, output_data, output_image_dir=image_dir, is_dir=is_dir
)
elif format == Format.YOLO:
elif format == Format.YOLO or format == Format.YOLO_OBB:
image_dir = kwargs.get('image_dir')
label_dir = kwargs.get('label_dir')
self.convert_to_yolo(
input_data,
output_data,
output_image_dir=image_dir,
output_label_dir=label_dir,
is_dir=is_dir,
output_image_dir = image_dir,
output_label_dir = label_dir,
is_dir = is_dir,
is_obb = (format == Format.YOLO_OBB)
)
elif format == Format.VOC:
image_dir = kwargs.get('image_dir')
Expand Down Expand Up @@ -728,6 +739,7 @@ def convert_to_yolo(
output_label_dir=None,
is_dir=True,
split_labelers=False,
is_obb=False
):
"""Convert data in a specific format to the YOLO format.
Expand All @@ -745,8 +757,13 @@ def convert_to_yolo(
A boolean indicating whether `input_data` is a directory (True) or a JSON file (False).
split_labelers : bool, optional
A boolean indicating whether to create a dedicated subfolder for each labeler in the output label directory.
obb : bool, optional
A boolean indicating whether to convert to Oriented Bounding Box (OBB) format.
"""
self._check_format(Format.YOLO)
if is_obb:
self._check_format(Format.YOLO_OBB)
else:
self._check_format(Format.YOLO)
ensure_dir(output_dir)
notes_file = os.path.join(output_dir, 'notes.json')
class_file = os.path.join(output_dir, 'classes.txt')
Expand Down Expand Up @@ -852,20 +869,30 @@ def convert_to_yolo(
or 'rectangle' in label
or 'labels' in label
):
xywh = self.rotated_rectangle(label)
if xywh is None:
continue
if is_obb:

obb_annotation = convert_annotation_to_yolo_obb(label)

if obb_annotation == None:
continue

top_left, top_right, bottom_right, bottom_left = obb_annotation

x1, y1 = top_left
x2, y2 = top_right
x3, y3 = bottom_right
x4, y4 = bottom_left

annotations.append([category_id, x1, y1, x2, y2, x3, y3, x4, y4])
else:
annotation = convert_annotation_to_yolo(label)

if annotation == None:
continue

x, y, w, h, = annotation
annotations.append([category_id, x, y, w, h])

x, y, w, h = xywh
annotations.append(
[
category_id,
(x + w / 2) / 100,
(y + h / 2) / 100,
w / 100,
h / 100,
]
)
elif "polygonlabels" in label or 'polygon' in label:
points_abs = [(x / 100, y / 100) for x, y in label["points"]]
annotations.append(
Expand Down
2 changes: 2 additions & 0 deletions label_studio_converter/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,8 @@ def export(args):
)
elif args.format == Format.YOLO:
c.convert_to_yolo(args.input, args.output, is_dir=not args.heartex_format)
elif args.format == Format.YOLO_OBB:
c.convert_to_yolo(args.input, args.output, is_dir=not args.heartex_format, is_obb=True)
else:
raise FormatNotSupportedError()

Expand Down
83 changes: 83 additions & 0 deletions label_studio_converter/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import argparse
import re
import datetime
import math

from copy import deepcopy
from operator import itemgetter
Expand Down Expand Up @@ -388,3 +389,85 @@ def prettify_result(v):
else:
out.append(j)
return out[0] if tag_type in ('Choices', 'TextArea') and len(out) == 1 else out


def convert_annotation_to_yolo(label):
"""
Convert LS annotation to Yolo format.
Args:
label (dict): Dictionary containing annotation information including:
- width (float): Width of the object.
- height (float): Height of the object.
- x (float): X-coordinate of the top-left corner of the object.
- y (float): Y-coordinate of the top-left corner of the object.
Returns:
tuple or None: If the conversion is successful, returns a tuple (x, y, w, h) representing
the coordinates and dimensions of the object in Yolo format, where (x, y) are the center
coordinates of the object, and (w, h) are the width and height of the object respectively.
"""

if not ("x" in label and "y" in label and 'width' in label and 'height' in label):
return None

w = label['width']
h = label['height']

x = (label['x'] + w / 2) / 100
y = (label['y'] + h / 2) / 100
w = w / 100
h = h / 100

return x, y, w, h


def convert_annotation_to_yolo_obb(label):
"""
Convert LS annotation to Yolo OBB format.
Args:
label (dict): Dictionary containing annotation information including:
- original_width (int): Original width of the image.
- original_height (int): Original height of the image.
- x (float): X-coordinate of the top-left corner of the object in percentage of the original width.
- y (float): Y-coordinate of the top-left corner of the object in percentage of the original height.
- width (float): Width of the object in percentage of the original width.
- height (float): Height of the object in percentage of the original height.
- rotation (float, optional): Rotation angle of the object in degrees (default is 0).
Returns:
list of tuple or None: List of tuples containing the coordinates of the object in Yolo OBB format.
Each tuple represents a corner of the bounding box in the order:
(top-left, top-right, bottom-right, bottom-left).
"""

if not (
"original_width" in label and
"original_height" in label and
'x' in label and
'y' in label and
'width' in label and
'height' in label and
'rotation' in label
):
return None

org_width, org_height = label['original_width'], label['original_height']
x = label['x'] / 100 * org_width
y = label['y'] / 100 * org_height
w = label['width'] / 100 * org_width
h = label['height'] / 100 * org_height

rotation = math.radians(label.get("rotation", 0))
cos, sin = math.cos(rotation), math.sin(rotation)

coords = [
(x, y),
(x + w * cos, y + w * sin),
(x + w * cos - h * sin, y + w * sin + h * cos),
(x - h * sin, y + h * cos)
]

# Normalize coordinates
return [(coord[0] / org_width, coord[1] / org_height) for coord in coords]
Loading

0 comments on commit 9a3db49

Please sign in to comment.