-
Notifications
You must be signed in to change notification settings - Fork 129
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Thoughts on YOLO annotation format conversion? #69
Comments
If I am correct, @SkalskiP talked about it in the YouTube livestream today, and they definitely consider it for the future, since many roboflow user are already very comfortable with the YOLO format, but it's not their priority. There is a lot of different, more important work to be done. |
In the interim, one thing the roboflow hosted service is useful for is converting annotation formats. There are 37 import formats (https://roboflow.com/formats?data-import=true) and 31 export formats (https://roboflow.com/formats?data-export=true). For example YOLO TXT to COCO JSON: https://roboflow.com/convert/yolo-keras-txt-to-coco-json |
Hi @Jordan-Pierce @mario-dg 👋🏻! Exactly! Adding YOLO format loading is definitely on our radar. It would be great if someone from the community added this functionality. If not, we'll add it ourselves, but it is not a key priority at the moment. In the meantime, I recommend using the resources linked by @josephofiowa. And of course, you can also use supervision, which allows conversion from PASCAL VOC and YOLO to COCO. |
@SkalskiP, I am thinking about 2 ways to implement this.
Let me know, what you guys prefer, so I can proceed 🚀 |
@mario-dg good thinking! Wouldn't it be even simpler if we just used the existing We could define clear rules for detection (e.g., presence of |
@SkalskiP, yes that's what I was suggesting with my 2nd approach 😄 Currently, we pass in the root directory of the dataset to the The name of the yaml should be fixed to Will draft a first PR later today. |
@mario-dg a YOLO dataset for detection / segmentation is defined by the structure (train/valid/test) and the data.yaml. You could either pass in the data.yaml file and confirm the folder structure, or pass in the directory and confirm the folder structure as well as the existence of data.yaml. Having the data.yaml means it's not an image classification dataset (also in classification, the folder for validation is "val" not "valid"); so if a data.yaml exists it's fair game to use for this model, as if it's instance segmentation data, you can convert them in the dataloader to use. |
Sorry for being lazy, but does roboflow provide these converters via python API? Or is it just available from their platform? |
@Jordan-Pierce, thanks for the clarification. I already have a draft and create the PR soon. |
If someone passes mask data, we can convert it into bounding boxes. |
Good idea! Do you have a quick implementation at hand before I take a closer look on how to do that? |
Depends on where you do it in your code, but at some point you're going to be reading the YOLO label.txt files, line-by-line. The coordinates are normalized (so if users resize the images, it doesn't matter, the values still work). When you read them in, determine if it's a bounding box, or polygon: bounding box will (for every row) always contain the same shape. Polygons will vary. So that's a quick check. Once you determine it's a polygon, you can just use the utils in supervision to convert from polygon. Note that YOLO format are polygons, not masks. |
Yes, I am well aware of the YOLO format. As you can see in the PR, the code changes were quite minimal. If supervision already provides a convert from polygon method, it should be easy to implement. In that case, we probably won't have to touch the dataloader and just can ensure that the during the conversion process polygons are converted to bounding boxes. |
we plan to support masking at some point. I don't want to build infra that makes it harder to add that later |
Here is a Python script which converts a YOLO format dataset to COCO format for use with rf-detr. Note that for single class datasets, you should use the LICENSE: Creative Commons 0 convert_yolo_to_coco.py #!/usr/bin/env python
import os
import json
import glob
import shutil
import random
import argparse
from PIL import Image
# For multi-class YAML support.
try:
import yaml
except ImportError:
yaml = None
def convert_yolo_to_coco(image_paths, labels_dir, class_name="custom", add_dummy=False, categories=None):
"""
Converts a list of images with corresponding YOLO annotations to a COCO-style JSON.
For each annotation line in a YOLO file, the expected format is:
<class_index> <x_center> <y_center> <width> <height>
where coordinates are normalized relative to image dimensions.
If `categories` is provided, it is assumed that the dataset is multi-class.
In that case, the YOLO class index is used (plus one) as the COCO category ID.
For single-class datasets (when categories is None), the conversion will assign
every annotation the same class ID (1) unless the dummy workaround is enabled,
in which case a dummy background class (id 0) is added and the single object is id 1.
"""
if categories is None:
# Single-class conversion.
if add_dummy:
categories = [
{"id": 0, "name": "Workers", "supercategory": "none"},
{"id": 1, "name": class_name, "supercategory": "Workers"}
]
else:
categories = [{"id": 1, "name": class_name, "supercategory": "none"}]
use_multiclass = False
else:
# Multi-class: use the provided categories.
use_multiclass = True
coco = {
"images": [],
"annotations": [],
"categories": categories
}
annotation_id = 1
image_id = 1
for img_path in image_paths:
filename = os.path.basename(img_path)
# Get image dimensions.
with Image.open(img_path) as img:
width, height = img.size
coco["images"].append({
"id": image_id,
"file_name": filename,
"width": width,
"height": height
})
# YOLO annotation file: same basename with .txt extension.
base, _ = os.path.splitext(filename)
label_file = os.path.join(labels_dir, base + ".txt")
if os.path.exists(label_file):
with open(label_file, "r") as f:
lines = f.readlines()
for line in lines:
parts = line.strip().split()
if len(parts) != 5:
continue # Skip improperly formatted lines.
# In multi-class mode, use the provided YOLO class index.
if use_multiclass:
cls_idx = int(parts[0])
# COCO categories are 1-indexed (assuming YAML names are in order).
category_id = cls_idx + 1
else:
# For single-class datasets, always assign the same class.
category_id = 1
# Parse and convert normalized coordinates.
_, x_center, y_center, w_norm, h_norm = map(float, parts)
x_center_abs = x_center * width
y_center_abs = y_center * height
w_abs = w_norm * width
h_abs = h_norm * height
x_min = x_center_abs - (w_abs / 2)
y_min = y_center_abs - (h_abs / 2)
annotation = {
"id": annotation_id,
"image_id": image_id,
"category_id": category_id,
"bbox": [x_min, y_min, w_abs, h_abs],
"area": w_abs * h_abs,
"iscrowd": 0
}
coco["annotations"].append(annotation)
annotation_id += 1
image_id += 1
return coco
def create_coco_dataset_from_yolo(yolo_dataset_dir, coco_dataset_dir, class_name="custom",
add_dummy=False, split_ratios=(0.8, 0.1, 0.1), categories=None):
"""
Converts a YOLO-formatted dataset (with subdirectories "images" and "labels") into a COCO-formatted dataset.
The output directory will include three subdirectories: "train", "valid", and "test".
Each split directory will contain:
- A copy of the corresponding image files.
- An "_annotations.coco.json" file with COCO-style annotations.
If `categories` is provided, the dataset is assumed to be multi-class.
"""
images_dir = os.path.join(yolo_dataset_dir, "images")
labels_dir = os.path.join(yolo_dataset_dir, "labels")
# Gather image file paths (supports common image extensions).
image_extensions = ("*.jpg", "*.jpeg", "*.png")
# Unless we're running on Windows (which matches case-insensitive), add uppercase file extensions to list
if sys.platform != "win32":
image_extensions += tuple(ext.upper() for ext in image_extensions)
image_paths = []
for ext in image_extensions:
image_paths.extend(glob.glob(os.path.join(images_dir, ext)))
if not image_paths:
raise ValueError("No images found in the provided images directory.")
# Shuffle images and split into train/valid/test sets.
random.shuffle(image_paths)
num_images = len(image_paths)
train_end = int(split_ratios[0] * num_images)
valid_end = train_end + int(split_ratios[1] * num_images)
splits = {
"train": image_paths[:train_end],
"valid": image_paths[train_end:valid_end],
"test": image_paths[valid_end:]
}
os.makedirs(coco_dataset_dir, exist_ok=True)
for split_name, paths in splits.items():
split_dir = os.path.join(coco_dataset_dir, split_name)
os.makedirs(split_dir, exist_ok=True)
# Copy images to the split directory.
for img_path in paths:
shutil.copy(img_path, os.path.join(split_dir, os.path.basename(img_path)))
# Convert annotations for this split.
coco_annotations = convert_yolo_to_coco(paths, labels_dir, class_name=class_name,
add_dummy=add_dummy, categories=categories)
json_path = os.path.join(split_dir, "_annotations.coco.json")
with open(json_path, "w") as f:
json.dump(coco_annotations, f, indent=4)
print(f"Created {json_path}: {len(coco_annotations['images'])} images, {len(coco_annotations['annotations'])} annotations.")
return coco_dataset_dir
def main():
parser = argparse.ArgumentParser(
description="Convert a YOLO-formatted dataset to COCO JSON format. "
"Supports both single-class and multi-class datasets. "
"For multi-class, provide a YOLO YAML file."
)
parser.add_argument("--yolo_dataset_dir", type=str, required=True,
help="Path to the YOLO dataset directory (should contain 'images' and 'labels' subdirectories).")
parser.add_argument("--coco_dataset_dir", type=str, default="converted_coco_dataset",
help="Output directory where the COCO formatted dataset will be saved.")
parser.add_argument("--class_name", type=str, default="custom",
help="Name of the object class (for single-class datasets).")
parser.add_argument("--add_dummy", action="store_true",
help="Add a dummy background class for single-class datasets (workaround for RF-DETR).")
parser.add_argument("--yaml_file", type=str, default=None,
help="Path to the YOLO YAML file (for multi-class datasets).")
args = parser.parse_args()
# Determine categories based on whether a YAML file is provided.
categories = None
if args.yaml_file:
if yaml is None:
raise ImportError("PyYAML is required for YAML file parsing. Install it via 'pip install pyyaml'.")
with open(args.yaml_file, "r") as f:
yaml_data = yaml.safe_load(f)
names = yaml_data.get("names")
if not names:
raise ValueError("The YAML file does not contain a 'names' key with class names.")
# For multi-class, create COCO categories (1-indexed).
categories = [{"id": i + 1, "name": name, "supercategory": "none"} for i, name in enumerate(names)]
print(f"Loaded {len(categories)} classes from YAML file.")
print("Starting conversion from YOLO to COCO format...")
create_coco_dataset_from_yolo(
yolo_dataset_dir=args.yolo_dataset_dir,
coco_dataset_dir=args.coco_dataset_dir,
class_name=args.class_name,
add_dummy=args.add_dummy,
categories=categories
)
print("Conversion complete.")
if __name__ == "__main__":
main() |
Hello,
Thanks for the great new model! Just curious if you're open to a PR that allows the current implementation to work with a YOLO-formatted dataset instead of just COCO?
(no worries if not, I can guess why).
Cheers.
The text was updated successfully, but these errors were encountered: