diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..91abb11 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,11 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates + +version: 2 +updates: + - package-ecosystem: "pip" # See documentation for possible values + directory: "/" # Location of package manifests + schedule: + interval: "weekly" diff --git a/.gitignore b/.gitignore index 68bc17f..2dc53ca 100644 --- a/.gitignore +++ b/.gitignore @@ -157,4 +157,4 @@ cython_debug/ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. -#.idea/ +.idea/ diff --git a/README.md b/README.md index 87dc795..6df8de5 100644 --- a/README.md +++ b/README.md @@ -3,16 +3,25 @@ # Labelme2YOLO [![PyPI - Version](https://img.shields.io/pypi/v/labelme2yolo.svg)](https://pypi.org/project/labelme2yolo) +![PyPI - Downloads](https://img.shields.io/pypi/dm/labelme2yolo?style=flat) [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/labelme2yolo.svg)](https://pypi.org/project/labelme2yolo) +[![Codacy Badge](https://app.codacy.com/project/badge/Grade/12122fe86f8643c4aa5667c20d528f61)](https://www.codacy.com/gh/GreatV/labelme2yolo/dashboard?utm_source=github.com&utm_medium=referral&utm_content=GreatV/labelme2yolo&utm_campaign=Badge_Grade) Help converting LabelMe Annotation Tool JSON format to YOLO text file format. If you've already marked your segmentation dataset by LabelMe, it's easy to use this tool to help converting to YOLO format dataset. --------- + ## New - export data as yolo polygon annotation (for YOLOv5 v7.0 segmentation) +## Installation + +```console +pip install labelme2yolo +``` + ## Parameters Explain **--json_dir** LabelMe JSON files folder path. @@ -27,57 +36,51 @@ If you've already marked your segmentation dataset by LabelMe, it's easy to use ### 1. Convert JSON files, split training, validation and test dataset by --val_size and --test_size Put all LabelMe JSON files under **labelme_json_dir**, and run this python command. ```bash -python labelme2yolo.py --json_dir /home/username/labelme_json_dir/ --val_size 0.15 --test_size 0.15 +labelme2yolo --json_dir /path/to/labelme_json_dir/ --val_size 0.15 --test_size 0.15 ``` Script would generate YOLO format dataset labels and images under different folders, for example, ```bash -/home/username/labelme_json_dir/YOLODataset/labels/train/ -/home/username/labelme_json_dir/YOLODataset/labels/test/ -/home/username/labelme_json_dir/YOLODataset/labels/val/ -/home/username/labelme_json_dir/YOLODataset/images/train/ -/home/username/labelme_json_dir/YOLODataset/images/test/ -/home/username/labelme_json_dir/YOLODataset/images/val/ +/path/to/labelme_json_dir/YOLODataset/labels/train/ +/path/to/labelme_json_dir/YOLODataset/labels/test/ +/path/to/labelme_json_dir/YOLODataset/labels/val/ +/path/to/labelme_json_dir/YOLODataset/images/train/ +/path/to/labelme_json_dir/YOLODataset/images/test/ +/path/to/labelme_json_dir/YOLODataset/images/val/ -/home/username/labelme_json_dir/YOLODataset/dataset.yaml +/path/to/labelme_json_dir/YOLODataset/dataset.yaml ``` ### 2. Convert JSON files, split training and validation dataset by folder If you already split train dataset and validation dataset for LabelMe by yourself, please put these folder under labelme_json_dir, for example, ```bash -/home/username/labelme_json_dir/train/ -/home/username/labelme_json_dir/val/ +/path/to/labelme_json_dir/train/ +/path/to/labelme_json_dir/val/ ``` Put all LabelMe JSON files under **labelme_json_dir**. Script would read train and validation dataset by folder. Run this python command. ```bash -python labelme2yolo.py --json_dir /home/username/labelme_json_dir/ +labelme2yolo --json_dir /path/to/labelme_json_dir/ ``` Script would generate YOLO format dataset labels and images under different folders, for example, ```bash -/home/username/labelme_json_dir/YOLODataset/labels/train/ -/home/username/labelme_json_dir/YOLODataset/labels/val/ -/home/username/labelme_json_dir/YOLODataset/images/train/ -/home/username/labelme_json_dir/YOLODataset/images/val/ +/path/to/labelme_json_dir/YOLODataset/labels/train/ +/path/to/labelme_json_dir/YOLODataset/labels/val/ +/path/to/labelme_json_dir/YOLODataset/images/train/ +/path/to/labelme_json_dir/YOLODataset/images/val/ -/home/username/labelme_json_dir/YOLODataset/dataset.yaml +/path/to/labelme_json_dir/YOLODataset/dataset.yaml ``` ### 3. Convert single JSON file Put LabelMe JSON file under **labelme_json_dir**. , and run this python command. ```bash -python labelme2yolo.py --json_dir /home/username/labelme_json_dir/ --json_name 2.json +labelme2yolo --json_dir /path/to/labelme_json_dir/ --json_name 2.json ``` Script would generate YOLO format text label and image under **labelme_json_dir**, for example, ```bash -/home/username/labelme_json_dir/2.text -/home/username/labelme_json_dir/2.png -``` - -## Installation - -```console -pip install labelme2yolo +/path/to/labelme_json_dir/2.text +/path/to/labelme_json_dir/2.png ``` ## License diff --git a/pyproject.toml b/pyproject.toml index 41a373e..0d1efe9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,15 +24,16 @@ classifiers = [ ] dependencies = [ "opencv-python>=4.1.2", - "Pillow", - "scikit-learn" + "Pillow>=9.2,<9.4", + "scikit-learn~=1.1.1", + "numpy~=1.23.1" ] dynamic = ["version"] [project.urls] -Documentation = "https://github.com/unknown/labelme2yolo#readme" -Issues = "https://github.com/unknown/labelme2yolo/issues" -Source = "https://github.com/unknown/labelme2yolo" +Documentation = "https://github.com/greatv/labelme2yolo#readme" +Issues = "https://github.com/greatv/labelme2yolo/issues" +Source = "https://github.com/greatv/labelme2yolo" [tool.hatch.version] path = "src/labelme2yolo/__about__.py" diff --git a/requirements.txt b/requirements.txt index 022b726..8ded313 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ -opencv-python>=4.1.2 +opencv-python Pillow scikit-learn +numpy diff --git a/src/labelme2yolo/__about__.py b/src/labelme2yolo/__about__.py index a64614b..2fc3af0 100644 --- a/src/labelme2yolo/__about__.py +++ b/src/labelme2yolo/__about__.py @@ -1,4 +1,5 @@ # SPDX-FileCopyrightText: 2022-present Wang Xin # # SPDX-License-Identifier: MIT + __version__ = '0.0.5' diff --git a/src/labelme2yolo/__main__.py b/src/labelme2yolo/__main__.py index beda96e..7880844 100644 --- a/src/labelme2yolo/__main__.py +++ b/src/labelme2yolo/__main__.py @@ -3,7 +3,7 @@ # SPDX-License-Identifier: MIT import sys -if __name__ == '__main__': +if __name__ == "__main__": from .cli import run - sys.exit(run()) \ No newline at end of file + sys.exit(run()) diff --git a/src/labelme2yolo/cli/__init__.py b/src/labelme2yolo/cli/__init__.py index 18e2d70..64f96e8 100644 --- a/src/labelme2yolo/cli/__init__.py +++ b/src/labelme2yolo/cli/__init__.py @@ -2,30 +2,47 @@ # # SPDX-License-Identifier: MIT import argparse -import sys -# from labelme2yolo.__about__ import version + from labelme2yolo.l2y import Labelme2YOLO def run(): parser = argparse.ArgumentParser("labelme2yolo") - parser.add_argument('--json_dir',type=str, - help='Please input the path of the labelme json files.') - parser.add_argument('--val_size',type=float, nargs='?', default=None, - help='Please input the validation dataset size, for example 0.1 ') - parser.add_argument('--test_size',type=float, nargs='?', default=0.0, - help='Please input the validation dataset size, for example 0.1 ') - parser.add_argument('--json_name',type=str, nargs='?', default=None, - help='If you put json name, it would convert only one json file to YOLO.') + parser.add_argument( + "--json_dir", type=str, help="Please input the path of the labelme json files." + ) + parser.add_argument( + "--val_size", + type=float, + nargs="?", + default=None, + help="Please input the validation dataset size, for example 0.1 ", + ) + parser.add_argument( + "--test_size", + type=float, + nargs="?", + default=None, + help="Please input the validation dataset size, for example 0.1 ", + ) + parser.add_argument( + "--json_name", + type=str, + nargs="?", + default=None, + help="If you put json name, it would convert only one json file to YOLO.", + ) args = parser.parse_args() - + if not args.json_dir: - parser.print_help(sys.stderr) - sys.exit(1) - + parser.print_help() + return 0 + convertor = Labelme2YOLO(args.json_dir) - + if args.json_name is None: convertor.convert(val_size=args.val_size, test_size=args.test_size) else: - convertor.convert_one(args.json_name) \ No newline at end of file + convertor.convert_one(args.json_name) + + return 0 diff --git a/src/labelme2yolo/l2y.py b/src/labelme2yolo/l2y.py index 40797f9..6075c01 100644 --- a/src/labelme2yolo/l2y.py +++ b/src/labelme2yolo/l2y.py @@ -1,26 +1,28 @@ -''' +""" Created on Aug 18, 2021 @author: xiaosonh @author: GreatV(Wang Xin) -''' -import os -import sys -import argparse -import shutil -import math +""" import base64 import io +import json +import math +import os +import shutil from collections import OrderedDict from multiprocessing import Pool -import json import cv2 -from sklearn.model_selection import train_test_split import numpy as np import PIL.ExifTags import PIL.Image import PIL.ImageOps +from sklearn.model_selection import train_test_split + + +# number of LabelMe2YOLO multiprocessing threads +NUM_THREADS = max(1, os.cpu_count() - 1) # copy form https://github.com/wkentaro/labelme/blob/main/labelme/utils/image.py @@ -68,60 +70,54 @@ def img_arr_to_b64(img_arr): # copy form https://github.com/wkentaro/labelme/blob/main/labelme/utils/image.py def img_data_to_png_data(img_data): - with io.BytesIO() as f: - f.write(img_data) - img = PIL.Image.open(f) + with io.BytesIO() as f_out: + f_out.write(img_data) + img = PIL.Image.open(f_out) - with io.BytesIO() as f: - img.save(f, "PNG") - f.seek(0) - return f.read() + with io.BytesIO() as f_in: + img.save(f_in, "PNG") + f_in.seek(0) + return f_in.read() -# copy form https://github.com/wkentaro/labelme/blob/main/labelme/utils/image.py -def apply_exif_orientation(image): - try: - exif = image._getexif() - except AttributeError: - exif = None +def get_label_id_map(json_dir): + label_set = set() - if exif is None: - return image + for file_name in os.listdir(json_dir): + if file_name.endswith("json"): + json_path = os.path.join(json_dir, file_name) + data = json.load(open(json_path)) + for shape in data["shapes"]: + label_set.add(shape["label"]) - exif = { - PIL.ExifTags.TAGS[k]: v - for k, v in exif.items() - if k in PIL.ExifTags.TAGS - } + return OrderedDict([(label, label_id) for label_id, label in enumerate(label_set)]) - orientation = exif.get("Orientation", None) - if orientation == 1: - # do nothing - return image - elif orientation == 2: - # left-to-right mirror - return PIL.ImageOps.mirror(image) - elif orientation == 3: - # rotate 180 - return image.transpose(PIL.Image.ROTATE_180) - elif orientation == 4: - # top-to-bottom mirror - return PIL.ImageOps.flip(image) - elif orientation == 5: - # top-to-left mirror - return PIL.ImageOps.mirror(image.transpose(PIL.Image.ROTATE_270)) - elif orientation == 6: - # rotate 270 - return image.transpose(PIL.Image.ROTATE_270) - elif orientation == 7: - # top-to-right mirror - return PIL.ImageOps.mirror(image.transpose(PIL.Image.ROTATE_90)) - elif orientation == 8: - # rotate 90 - return image.transpose(PIL.Image.ROTATE_90) - else: - return image +def save_yolo_label(json_name, label_dir_path, target_dir, yolo_obj_list): + txt_path = os.path.join( + label_dir_path, target_dir, json_name.replace(".json", ".txt") + ) + + with open(txt_path, "w+") as f: + for yolo_obj_idx, yolo_obj in enumerate(yolo_obj_list): + yolo_obj_line = ( + "%s %s %s %s %s\n" % yolo_obj + if yolo_obj_idx + 1 != len(yolo_obj_list) + else "%s %s %s %s %s" % yolo_obj + ) + f.write(yolo_obj_line) + + +def save_yolo_image(json_data, json_name, image_dir_path, target_dir): + img_name = json_name.replace(".json", ".png") + img_path = os.path.join(image_dir_path, target_dir, img_name) + + if not os.path.exists(img_path): + img = img_b64_to_arr(json_data["imageData"]) + PIL.Image.fromarray(img).save(img_path) + + return img_path + class Labelme2YOLO(object): @@ -209,6 +205,7 @@ class Labelme2YOLO(object): for target_dir, json_names in zip(('train/', 'val/', 'test/'), (train_json_names, val_json_names, test_json_names)): pool = Pool(os.cpu_count() - 1) + for json_name in json_names: pool.apply_async(self.covert_json_to_text, args=(target_dir, json_name)) @@ -252,7 +249,7 @@ class Labelme2YOLO(object): yolo_obj_list = [] img_h, img_w, _ = cv2.imread(img_path).shape - for shape in json_data['shapes']: + for shape in json_data["shapes"]: # labelme circle shape is different from others # it only has 2 points, 1st is circle center, 2nd is drag end point if shape['shape_type'] == 'circle': @@ -329,7 +326,8 @@ class Labelme2YOLO(object): yaml_file.write('nc: %i\n' % len(self._label_id_map)) names_str = '' + for label, _ in self._label_id_map.items(): names_str += "'%s', " % label - names_str = names_str.rstrip(', ') - yaml_file.write('names: [%s]' % names_str) + names_str = names_str.rstrip(", ") + yaml_file.write("names: [%s]" % names_str)