diff --git a/README.md b/README.md index 3b2b118..c39417f 100644 --- a/README.md +++ b/README.md @@ -340,11 +340,23 @@ Sample of annotated image along with its mask and settings is show below. } ``` -YOLO Format +### YOLO Format + +YOLO format is also supported by A.Lab. Below is an example of annotated ripe and unripe tomatoes. In this example, `0` represents ripe tomatoes and `1` represents unripe ones. + +![yolo_annotation_example](./sample_images/yolo_annotation_example.png) + +The label of the above image are as follows: ``` -Orange 0.0015012463401089847 0.0018903253731438908 0.0016967868433852144 0.0026982117835804955 +0 0.213673 0.474717 0.310212 0.498856 +0 0.554777 0.540507 0.306350 0.433638 +1 0.378432 0.681239 0.223970 0.268879 ``` +Applying the generated labels we get following results. + +![yolo_with_generated_labels](./sample_images/yolo_applied_annotation.jpg) + ## Troubleshooting [[documentation page]](https://annotate-docs.dwaste.live/troubleshooting) - Ensure that both the client and server are running. diff --git a/sample_images/yolo_annotation_example.png b/sample_images/yolo_annotation_example.png new file mode 100644 index 0000000..f656ee3 Binary files /dev/null and b/sample_images/yolo_annotation_example.png differ diff --git a/sample_images/yolo_applied_annotation.jpg b/sample_images/yolo_applied_annotation.jpg new file mode 100644 index 0000000..ff05cb7 Binary files /dev/null and b/sample_images/yolo_applied_annotation.jpg differ diff --git a/server/app.py b/server/app.py index 7941086..b50818b 100644 --- a/server/app.py +++ b/server/app.py @@ -564,47 +564,46 @@ def create_yolo_annotations(image_names, color_map=None): # Convert points to normalized YOLO format if points: - xmin = min(point[0] for point in points) / width - ymin = min(point[1] for point in points) / height - xmax = max(point[0] for point in points) / width - ymax = max(point[1] for point in points) / height + xmin = min(point[0] for point in points) + ymin = min(point[1] for point in points) + xmax = max(point[0] for point in points) + ymax = max(point[1] for point in points) # YOLO format: class_index x_center y_center width height (all normalized) - annotations.append(f"{class_name} {(xmin + xmax) / 2} {(ymin + ymax) / 2} {xmax - xmin} {ymax - ymin}") + annotations.append(f"{class_name} {(xmin + xmax) / 2:.6f} {(ymin + ymax) / 2:.6f} {xmax - xmin:.6f} {ymax - ymin:.6f}") # Process box regions if boxRegions is not None: for index, region in boxRegions.iterrows(): class_name = region.get('class', 'unknown') try: - x = float(region['x'][1:-1]) * width if isinstance(region['x'], str) else float(region['x'][0]) * width - y = float(region['y'][1:-1]) * height if isinstance(region['y'], str) else float(region['y'][0]) * height - w = float(region['w'][1:-1]) * width if isinstance(region['w'], str) else float(region['w'][0]) * width - h = float(region['h'][1:-1]) * height if isinstance(region['h'], str) else float(region['h'][0]) * height + x = float(region['x'][1:-1]) if isinstance(region['x'], str) else float(region['x'][0]) + y = float(region['y'][1:-1]) if isinstance(region['y'], str) else float(region['y'][0]) + w = float(region['w'][1:-1]) if isinstance(region['w'], str) else float(region['w'][0]) + h = float(region['h'][1:-1]) if isinstance(region['h'], str) else float(region['h'][0]) except (ValueError, TypeError) as e: raise ValueError(f"Invalid format in region dimensions: {region}, Error: {e}") - # YOLO format: class_index x_center y_center width height (all normalized) - annotations.append(f"{class_name} {x + w / 2} {y + h / 2} {w} {h}") + annotations.append(f"{class_name} {x + w / 2:.6f} {y + h / 2:.6f} {w:.6f} {h:.6f}") # Process circle/ellipse regions if circleRegions is not None: for index, region in circleRegions.iterrows(): class_name = region.get('class', 'unknown') try: - rx = float(region['rx'][1:-1]) * width if isinstance(region['rx'], str) else float(region['rx'][0]) * width - ry = float(region['ry'][1:-1]) * height if isinstance(region['ry'], str) else float(region['ry'][0]) * height - rw = float(region['rw'][1:-1]) * width if isinstance(region['rw'], str) else float(region['rw'][0]) * width - rh = float(region['rh'][1:-1]) * height if isinstance(region['rh'], str) else float(region['rh'][0]) * height + rx = float(region['rx'][1:-1]) * width if isinstance(region['rx'], str) else float(region['rx'][0]) + ry = float(region['ry'][1:-1]) * height if isinstance(region['ry'], str) else float(region['ry'][0]) + rw = float(region['rw'][1:-1]) * width if isinstance(region['rw'], str) else float(region['rw'][0]) + rh = float(region['rh'][1:-1]) * height if isinstance(region['rh'], str) else float(region['rh'][0]) except (ValueError, TypeError) as e: raise ValueError(f"Invalid format in region dimensions: {region}, Error: {e}") # For YOLO, if width and height are equal, it represents a circle if rw == rh: - annotations.append(f"{class_name} {rx} {ry} {rw} {rw}") # Treat as circle + annotations.append(f"{class_name} {rx:.6f} {ry:.6f} {rw:.6f} {rw:.6f}") # Treat as circle else: # Treat as ellipse (YOLO does not directly support ellipse, so treat as box) - annotations.append(f"{class_name} {rx + rw / 2} {ry + rh / 2} {rw} {rh}") + annotations.append(f"{class_name} {rx + rw / 2:.6f} {ry + rh / 2:.6f} {rw:.6f} {rh:.6f}") # Append annotations for current image to all_annotations list all_annotations.extend(annotations) diff --git a/server/examples/check_label.py b/server/examples/check_label.py new file mode 100644 index 0000000..9e97369 --- /dev/null +++ b/server/examples/check_label.py @@ -0,0 +1,51 @@ +import cv2 +import os + +# Function to draw bounding boxes +def draw_bboxes(image_path, label_path, output_path): + # Define colors for different classes + colors = [(0, 0, 255), (0, 255, 0)] # Example colors for two classes: green and red for unripe and riped + + # Read the image + image = cv2.imread(image_path) + height, width, _ = image.shape + + # Read the label file + with open(label_path, 'r') as f: + labels = f.readlines() + + for label in labels: + label_data = label.strip().split() + class_id = int(label_data[0]) + x_center, y_center, bbox_width, bbox_height = map(float, label_data[1:]) + + # Convert normalized coordinates to pixel coordinates + x_center *= width + y_center *= height + bbox_width *= width + bbox_height *= height + + # Calculate top-left and bottom-right coordinates + x1 = int(x_center - bbox_width / 2) + y1 = int(y_center - bbox_height / 2) + x2 = int(x_center + bbox_width / 2) + y2 = int(y_center + bbox_height / 2) + + # Draw the bounding box with a color based on class ID + color = colors[class_id] + cv2.rectangle(image, (x1, y1), (x2, y2), color, 2) + cv2.putText(image, str(class_id), (x1, y1 - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.5, color, 2) + + # Save the output image + cv2.imwrite(output_path, image) + + +# Paths to the image, label, and output +image_path = './tomato_example.jpeg' +label_path = './tomato_example.txt' +output_path = './output_tomato_example.jpg' + +# Draw bounding boxes on the image +draw_bboxes(image_path, label_path, output_path) + +print(f"Output image saved to {output_path}") diff --git a/server/examples/output_tomato_example.jpg b/server/examples/output_tomato_example.jpg new file mode 100644 index 0000000..ff05cb7 Binary files /dev/null and b/server/examples/output_tomato_example.jpg differ diff --git a/server/examples/tomato_example.jpeg b/server/examples/tomato_example.jpeg new file mode 100644 index 0000000..0f61864 Binary files /dev/null and b/server/examples/tomato_example.jpeg differ diff --git a/server/examples/tomato_example.txt b/server/examples/tomato_example.txt new file mode 100644 index 0000000..303135b --- /dev/null +++ b/server/examples/tomato_example.txt @@ -0,0 +1,3 @@ +0 0.213673 0.474717 0.310212 0.498856 +0 0.554777 0.540507 0.306350 0.433638 +1 0.378432 0.681239 0.223970 0.268879 diff --git a/server/requirements.txt b/server/requirements.txt index 1b5c9f9..f419033 100644 --- a/server/requirements.txt +++ b/server/requirements.txt @@ -1,4 +1,4 @@ -flask +opencv-python flask-cors pandas pillow