cursor-detection-yolov8n / train_cursor.py
AdithyaSK's picture
AdithyaSK HF Staff
Upload train_cursor.py
b8011fd verified
Raw
History Blame Contribute Delete
5.22 kB
"""
End-to-end cursor detection: generate synthetic YOLO dataset + train YOLOv8n.
"""
import os
import random
from pathlib import Path
from PIL import Image
from datasets import load_dataset
from tqdm import tqdm
from ultralytics import YOLO
# ========== CONFIG ==========
NUM_TRAIN = 500
NUM_VAL = 100
NUM_TEST = 50
IMAGE_SIZE = (640, 640)
CURSOR_SIZE_RANGE = (16, 48)
OUTPUT_DIR = Path("/app/cursor_dataset")
YAML_PATH = OUTPUT_DIR / "cursor.yaml"
# ========== SETUP ==========
for split in ["train", "val", "test"]:
(OUTPUT_DIR / "images" / split).mkdir(parents=True, exist_ok=True)
(OUTPUT_DIR / "labels" / split).mkdir(parents=True, exist_ok=True)
print("Loading datasets...")
cursors_ds = load_dataset("Fraser/cursors", split="train")
screenshots_ds = load_dataset("naorm/website-screenshots", split="train")
static_cursors = []
animated_cursors = []
for row in cursors_ds:
frames = row["frames"]
if len(frames) == 1:
static_cursors.append(row)
else:
animated_cursors.append(row)
print(f"Cursors: {len(static_cursors)} static, {len(animated_cursors)} animated")
print(f"Screenshots: {len(screenshots_ds)}")
screenshot_images = [row["image"] for row in screenshots_ds]
def get_random_cursor():
if random.random() < 0.7 and static_cursors:
row = random.choice(static_cursors)
frame = row["frames"][0]
elif animated_cursors:
row = random.choice(animated_cursors)
frame = random.choice(row["frames"])
else:
row = random.choice(static_cursors)
frame = row["frames"][0]
return frame, row["hotspot_x"], row["hotspot_y"], row["size_px"]
def composite_cursor(bg_img, cursor_frame, hotspot_x, hotspot_y, cursor_size):
bg = bg_img.convert("RGBA").resize(IMAGE_SIZE, Image.LANCZOS)
cw, ch = cursor_frame.size
scale = cursor_size / max(cw, ch)
new_w = max(1, round(cw * scale))
new_h = max(1, round(ch * scale))
cursor_scaled = cursor_frame.convert("RGBA").resize((new_w, new_h), Image.LANCZOS)
target_x = random.randint(0, IMAGE_SIZE[0] - 1)
target_y = random.randint(0, IMAGE_SIZE[1] - 1)
paste_x = target_x - round(hotspot_x * new_w)
paste_y = target_y - round(hotspot_y * new_h)
sw, sh = bg.size
src_left = max(0, -paste_x)
src_top = max(0, -paste_y)
src_right = min(new_w, sw - paste_x)
src_bottom = min(new_h, sh - paste_y)
if src_left >= src_right or src_top >= src_bottom:
return None, None
cropped = cursor_scaled.crop((src_left, src_top, src_right, src_bottom))
dst_x = paste_x + src_left
dst_y = paste_y + src_top
bg.alpha_composite(cropped, (dst_x, dst_y))
bbox_x = (dst_x + (src_right - src_left) / 2) / sw
bbox_y = (dst_y + (src_bottom - src_top) / 2) / sh
bbox_w = (src_right - src_left) / sw
bbox_h = (src_bottom - src_top) / sh
bbox_x = max(bbox_w / 2, min(1 - bbox_w / 2, bbox_x))
bbox_y = max(bbox_h / 2, min(1 - bbox_h / 2, bbox_y))
bbox_w = min(1.0, bbox_w)
bbox_h = min(1.0, bbox_h)
return bg.convert("RGB"), (0, bbox_x, bbox_y, bbox_w, bbox_h)
def generate_split(split_name, num_samples):
print(f"Generating {split_name} ({num_samples} samples)...")
img_dir = OUTPUT_DIR / "images" / split_name
lbl_dir = OUTPUT_DIR / "labels" / split_name
for i in tqdm(range(num_samples), desc=split_name):
bg = random.choice(screenshot_images)
cursor_frame, hs_x, hs_y, size_px = get_random_cursor()
cursor_size = random.randint(*CURSOR_SIZE_RANGE)
composed, bbox = composite_cursor(bg, cursor_frame, hs_x, hs_y, cursor_size)
retries = 0
while composed is None and retries < 5:
bg = random.choice(screenshot_images)
cursor_frame, hs_x, hs_y, size_px = get_random_cursor()
cursor_size = random.randint(*CURSOR_SIZE_RANGE)
composed, bbox = composite_cursor(bg, cursor_frame, hs_x, hs_y, cursor_size)
retries += 1
if composed is None:
continue
img_path = img_dir / f"{split_name}_{i:06d}.jpg"
lbl_path = lbl_dir / f"{split_name}_{i:06d}.txt"
composed.save(img_path, "JPEG", quality=95)
with open(lbl_path, "w") as f:
f.write(f"0 {bbox[1]:.6f} {bbox[2]:.6f} {bbox[3]:.6f} {bbox[4]:.6f}\n")
random.seed(42)
generate_split("train", NUM_TRAIN)
generate_split("val", NUM_VAL)
generate_split("test", NUM_TEST)
# Write YAML
with open(YAML_PATH, "w") as f:
f.write(f"path: {OUTPUT_DIR.absolute()}\n")
f.write("train: images/train\n")
f.write("val: images/val\n")
f.write("test: images/test\n")
f.write("nc: 1\n")
f.write("names: ['cursor']\n")
print(f"Dataset ready at {OUTPUT_DIR}")
# ========== TRAIN YOLO ==========
model = YOLO("yolov8n.pt")
results = model.train(
data=str(YAML_PATH),
epochs=30,
imgsz=640,
batch=16,
device=0,
project="cursor-detection",
name="yolov8n-cursor",
exist_ok=True,
verbose=True,
)
# Evaluate on test set
metrics = model.val(data=str(YAML_PATH), split="test")
print("Test metrics:", metrics)
# Push to Hub
model.push_to_hub("AdithyaSK/cursor-detection-yolov8n")
print("Model pushed to Hub!")