Primary competition visual

Lacuna Solar Survey Challenge

Helping Madagascar
$5 000 USD
Completed (12 months ago)
Computer Vision
Prediction
729 joined
247 active
Starti
Feb 14, 25
Closei
Mar 23, 25
Reveali
Mar 24, 25
LB 1.10 Approach using YOLOv8x
Help · 21 Mar 2025, 13:25 · 4

First, I apologize. In a recent post, I mentioned that YOLO could achieve an LB of 0.80, which was based on my past experience and intuition. However, the reality is not entirely clear. The approach below is a mid-level one—not particularly good or bad—and it resulted in an LB of almost 1.10. That said, there is plenty of room for improvement.

Approach and Methodology

Data Preparation

This part took me many tries to just make it working .

  1. Polygon Parsing: First create a function to safely parse the polygon annotations, handling various formats and potential corruption in the data.(
  2. Bounding Box Conversion: COnvert the polygon annotations to YOLO-compatible bounding boxes by calculating the minimum and maximum coordinates from each polygon.
  3. Data Splitting: I used GroupKFold cross-validation to maintain image groups intact during splitting, preventing data leakage.

Model Architecture

I selected YOLOv8x (extra-large variant) for its balance of accuracy and computational efficiency. I configured the model just average perametrs

Due to memort constraint i selected some perameters (i am not proud of) .

Handling the Polygon Annotation Challenge

The main challenge was the images with numerous panels (up to 15 or more ) annotated by a single polygon.

  1. Instance Separation: I relied on YOLO's inherent ability to detect multiple instances within a single bounding area to distinguish individual panels. but i did not worked good for panles above 5.
  2. Data Augmentation: I tried various augmentations including HSV color space adjustments, geometric transformations, and mosaic augmentation.
  3. Threshold Optimization: I tried to tuned the confidence (0.25) and IoU (0.45) thresholds to balance detection sensitivity and specificity.

Inference and Submission

During inference, I:

  1. Processed each test image through the trained YOLO model
  2. Counted detected instances of boilers (class 0) and panels (class 1)
  3. Generated submission files .

Key Technical Components

Robust Polygon Parser

An example of a polygon parser to handle various annotation formats and potential corruption in the data:

def parse_polygon(poly_str):
    """Safe polygon parsing with multiple validation layers"""
    try:
        parsed = ast.literal_eval(str(poly_str))
        if not isinstance(parsed, list):
            return []
        valid_poly = []
        for item in parsed:
            if isinstance(item, (list, tuple)) and len(item) >= 2:
                try:
                    x = float(item[0])
                    y = float(item[1])
                    valid_poly.append([x, y])
                except:
                    continue
        return valid_poly
    except:
        return []

YOLO Label Creation

An example of a function to convert polygon annotations to YOLO-compatible bounding boxes:

def create_yolo_labels(df, mode='train'):
    print(f"\n📋 Creating YOLO labels for {mode} data...")
    missing_count = 0
    processed_ids = []
    grouped = df.groupby('ID', sort=False)
    for img_id, group in tqdm(grouped, total=len(grouped), desc=f"Processing {mode} images"):
        img_path = f"{CFG.data_path}/images/{img_id}.jpg"
        label_path = f"{CFG.work_dir}/{mode}/labels/{img_id}.txt"
        if not os.path.exists(img_path):
            missing_count += 1
            continue
        try:
            img = cv2.imread(img_path)
            h, w = img.shape[:2]
        except Exception as e:
            print(f"⚠️ Corrupt image {img_id}: {str(e)}")
            missing_count += 1
            continue
        valid_boxes = []
        for _, row in group.iterrows():
            try:
                boil_count = int(row['boil_nbr'])
                pan_count = int(row['pan_nbr'])
            except KeyError:
                print(f"⚠️ Missing target columns in {img_id}")
                continue
            polygon_points = parse_polygon(row['polygon'])
            if not polygon_points:
                continue
            try:
                points = np.array(polygon_points, dtype=np.float32)
                # Handle coordinate types
                if np.max(points) <= 1.0:  # Normalized coordinates
                    x_coords = points[:, 0] * w
                    y_coords = points[:, 1] * h
                else:  # Absolute coordinates
                    x_coords = points[:, 0]
                    y_coords = points[:, 1]
                # Calculate bounding box from all polygon points
                x_min = max(0, np.min(x_coords))
                x_max = min(w, np.max(x_coords))
                y_min = max(0, np.min(y_coords))
                y_max = min(h, np.max(y_coords))
                if x_max <= x_min or y_max <= y_min:
                    continue
                # Convert to YOLO format
                x_center = ((x_min + x_max) / 2) / w
                y_center = ((y_min + y_max) / 2) / h
                bbox_w = (x_max - x_min) / w
                bbox_h = (y_max - y_min) / h
                # Create boxes for each target type
                if boil_count > 0:
                    valid_boxes.append(f"0 {x_center:.6f} {y_center:.6f} {bbox_w:.6f} {bbox_h:.6f}")
                if pan_count > 0:
                    valid_boxes.append(f"1 {x_center:.6f} {y_center:.6f} {bbox_w:.6f} {bbox_h:.6f}")
            except Exception as e:
                print(f"⚠️ Error processing polygon in {img_id}: {str(e)}")
                continue
        if valid_boxes:
            with open(label_path, 'w') as f:
                f.write("\n".join(valid_boxes))
            shutil.copy(img_path, f"{CFG.work_dir}/{mode}/images/{img_id}.jpg")
            processed_ids.append(img_id)
        else:
            missing_count += 1
            print(f"⚠️ No valid boxes for {img_id} - Total entries: {len(group)}")
    print(f"✓ Successfully processed {len(processed_ids)} images")
    print(f"⚠️ Missing/Skipped {missing_count} images")
    return processed_ids

Results and Analysis

My current solution achieved a competitive score but had room for improvement, particularly in handling images with numerous panels under single polygons. The main issue was that when a single polygon annotated many panels (e.g., 15+), the model might not detect each individual panel, leading to undercounting.

Future Improvements

I have several ideas for improving the solution:

  1. Advanced Polygon Processing: Implement more sophisticated methods to split large polygons into smaller regions based on panel arrangements.
  2. Density Estimation: For areas with many panels, implement density estimation techniques to count objects when individual detection becomes challenging.
  3. Higher Resolution Training: Experiment with larger input sizes to capture finer details of individual panels.
  4. Ensemble Methods: Combine predictions from multiple models to improve counting accuracy.
  5. Post-processing Refinement: Develop more sophisticated counting algorithms that consider panel layouts and arrangements.

I'm proud of what I've accomplished with this solution. It demonstrates the effectiveness of YOLOv8 for detecting and counting solar installations in aerial imagery. While there's still work to be done, particularly in handling challenging polygon annotations, this solution has the potential to significantly impact renewable energy planning in Madagascar and similar regions by providing accurate, scalable detection of solar installations.

Discussion 4 answers
User avatar
marching_learning
Nostalgic Mathematics

Thank you @zulo40 for sharing your insights. But Since time is running out, do you have a notebook readily available to share.

21 Mar 2025, 13:51
Upvotes 1

i have but i think i will share if after the competation ends .

User avatar
marching_learning
Nostalgic Mathematics

Thank you. I'll be waiting