""" Parse CVAT XML annotations and convert to COCO format for evaluation. """ import xml.etree.ElementTree as ET import json import numpy as np from pathlib import Path from PIL import Image try: import pycocotools.mask as mask_util HAS_PYCOCOTOOLS = True except ImportError: HAS_PYCOCOTOOLS = False print("Warning: pycocotools not available. Install with: pip install pycocotools") def parse_rle(rle_string, width, height): """ Parse RLE (Run-Length Encoding) string from CVAT format. CVAT RLE format is a simple list of counts: "count1, count2, count3, ..." This represents a flattened binary mask where counts alternate between runs of 0s and 1s. """ if not rle_string or not rle_string.strip(): return None try: # Split by comma and convert to integers counts = [int(x.strip()) for x in rle_string.split(',') if x.strip()] if len(counts) == 0: return None # Create binary mask mask = np.zeros((height, width), dtype=np.uint8) # Parse RLE: counts alternate between 0s and 1s # First count is typically 0s, then 1s, then 0s, etc. pos = 0 is_foreground = False # Start with background (0s) for count in counts: if is_foreground: # Fill foreground pixels for _ in range(count): y = pos // width x = pos % width if y < height and x < width: mask[y, x] = 1 pos += 1 else: # Skip background pixels pos += count is_foreground = not is_foreground # Convert to COCO RLE format try: rle = mask_util.encode(np.asfortranarray(mask)) rle['counts'] = rle['counts'].decode('utf-8') return rle except ImportError: # Fallback if pycocotools not available print("Warning: pycocotools not available, using bbox only") return None except Exception as e: print(f"Warning: Failed to parse RLE: {e}") return None def bbox_from_mask(rle, width, height): """Extract bounding box from RLE mask.""" if rle is None or not HAS_PYCOCOTOOLS: return None try: # Decode RLE to get mask rle_decoded = rle.copy() rle_decoded['counts'] = rle_decoded['counts'].encode('utf-8') mask = mask_util.decode(rle_decoded) # Find bounding box rows = np.any(mask, axis=1) cols = np.any(mask, axis=0) if not np.any(rows) or not np.any(cols): return None y_min, y_max = np.where(rows)[0][[0, -1]] x_min, x_max = np.where(cols)[0][[0, -1]] # COCO format: [x, y, width, height] return [int(x_min), int(y_min), int(x_max - x_min + 1), int(y_max - y_min + 1)] except Exception as e: print(f"Warning: Failed to extract bbox from mask: {e}") return None def parse_cvat_xml(xml_path, images_dir): """ Parse CVAT XML file and convert to COCO format. Handles , , and annotations. Args: xml_path: Path to CVAT annotations.xml file images_dir: Directory containing the images Returns: COCO format dictionary """ # 1. Load the XML try: tree = ET.parse(xml_path) root = tree.getroot() except FileNotFoundError: print(f"Error: Could not find XML file: {xml_path}") return None # 2. Initialize COCO structure coco = { "info": { "description": "Converted from CVAT XML", "year": 2024, "version": "1.0" }, "licenses": [], "images": [], "annotations": [], "categories": [] } # 3. Create Category (Label) Map # First, try to get labels from