Last active
March 26, 2026 21:53
-
-
Save Xnuvers007/2bed5b61b0a126504517b8569c420b53 to your computer and use it in GitHub Desktop.
AI Self Driving
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import cv2, json, os, copy, time, warnings, zipfile | |
| import numpy as np | |
| import matplotlib | |
| import matplotlib.pyplot as plt | |
| import matplotlib.patches as mpatches | |
| from matplotlib.gridspec import GridSpec | |
| from PIL import Image, ImageDraw, ImageFont | |
| from IPython.display import display, clear_output, HTML | |
| import ipywidgets as widgets | |
| from google.colab import files | |
| from tqdm.notebook import tqdm | |
| from ultralytics import YOLO | |
| import glob | |
| warnings.filterwarnings("ignore") | |
| matplotlib.rcParams["font.family"] = "DejaVu Sans" | |
| # ══════════════════════════════════════════════════════════════════ | |
| # PALETTE & LABEL MASTER | |
| # ══════════════════════════════════════════════════════════════════ | |
| CATEGORY_META = { | |
| "Kendaraan": { | |
| "color": "#FF6B35", "icon": "[V]", | |
| "labels": [ | |
| "mobil", "sepeda_motor", "bus", "truk", "sepeda", | |
| "van", "minibus", "kendaraan_parkir", | |
| "ambulans", "mobil_polisi", "mobil_pemadam", | |
| ], | |
| }, | |
| "Manusia": { | |
| "color": "#4ECDC4", "icon": "[P]", | |
| "labels": [ | |
| "pejalan_kaki", "orang_menyeberang", "pengendara_sepeda", | |
| "pengendara_motor", "orang_di_trotoar", | |
| ], | |
| }, | |
| "Rambu & Lampu": { | |
| "color": "#FFE66D", "icon": "[S]", | |
| "labels": [ | |
| "rambu_lalu_lintas", "lampu_lalu_lintas", "rambu_stop", | |
| "rambu_batas_kecepatan", "rambu_belok", | |
| "lampu_merah", "lampu_kuning", "lampu_hijau", | |
| ], | |
| }, | |
| "Struktur Jalan": { | |
| "color": "#A8E6CF", "icon": "[R]", | |
| "labels": [ | |
| "marka_jalan", "zebra_cross", "trotoar", "median_jalan", | |
| "bahu_jalan", "persimpangan", "jalur_belok", | |
| ], | |
| }, | |
| "Objek Sekitar": { | |
| "color": "#C3B1E1", "icon": "[E]", | |
| "labels": [ | |
| "pohon", "tiang_listrik", "tiang_lampu", "pagar", | |
| "gedung", "halte_bus", "papan_iklan", | |
| ], | |
| }, | |
| "Penghalang": { | |
| "color": "#FF8B94", "icon": "[O]", | |
| "labels": [ | |
| "kerucut_lalu_lintas", "barikade", "kendaraan_rusak", | |
| "sampah_besar", "batu", "lubang_jalan", | |
| ], | |
| }, | |
| "Permukaan Jalan": { | |
| "color": "#85C1E9", "icon": "[G]", | |
| "labels": [ | |
| "jalan_aspal", "trotoar_permukaan", "rumput", | |
| "tanah", "area_parkir", | |
| ], | |
| }, | |
| } | |
| CATEGORY_COLORS = {k: v["color"] for k, v in CATEGORY_META.items()} | |
| CATEGORY_LABELS = {k: v["labels"] for k, v in CATEGORY_META.items()} | |
| ALL_LABELS = [lbl for meta in CATEGORY_META.values() for lbl in meta["labels"]] | |
| LABEL2ID = {lbl: i for i, lbl in enumerate(ALL_LABELS)} | |
| LABEL2CAT = {lbl: cat | |
| for cat, meta in CATEGORY_META.items() | |
| for lbl in meta["labels"]} | |
| # ══════════════════════════════════════════════════════════════════ | |
| # MAPPING YOLO COCO → LABEL KUSTOM (lengkap 80 kelas COCO) | |
| # ══════════════════════════════════════════════════════════════════ | |
| YOLO_MAP = { | |
| # Kendaraan | |
| "car": ("Kendaraan", "mobil"), | |
| "motorcycle": ("Kendaraan", "sepeda_motor"), | |
| "motorbike": ("Kendaraan", "sepeda_motor"), | |
| "bus": ("Kendaraan", "bus"), | |
| "truck": ("Kendaraan", "truk"), | |
| "bicycle": ("Kendaraan", "sepeda"), | |
| "van": ("Kendaraan", "van"), | |
| "ambulance": ("Kendaraan", "ambulans"), | |
| "fire truck": ("Kendaraan", "mobil_pemadam"), | |
| "police car": ("Kendaraan", "mobil_polisi"), | |
| "train": ("Kendaraan", "truk"), | |
| "boat": ("Kendaraan", "kendaraan_parkir"), | |
| # Manusia | |
| "person": ("Manusia", "pejalan_kaki"), | |
| "people": ("Manusia", "pejalan_kaki"), | |
| # Rambu & Lampu | |
| "traffic light": ("Rambu & Lampu", "lampu_lalu_lintas"), | |
| "stop sign": ("Rambu & Lampu", "rambu_stop"), | |
| "parking meter": ("Rambu & Lampu", "rambu_lalu_lintas"), | |
| # Objek Sekitar | |
| "bench": ("Objek Sekitar", "halte_bus"), | |
| "potted plant": ("Objek Sekitar", "pohon"), | |
| "tree": ("Objek Sekitar", "pohon"), | |
| "fire hydrant": ("Objek Sekitar", "tiang_listrik"), | |
| "umbrella": ("Objek Sekitar", "pagar"), | |
| # Penghalang | |
| "suitcase": ("Penghalang", "sampah_besar"), | |
| "backpack": ("Penghalang", "sampah_besar"), | |
| "sports ball": ("Penghalang", "batu"), | |
| "bottle": ("Penghalang", "sampah_besar"), | |
| "billboard": ("Objek Sekitar", "papan_iklan"), | |
| } | |
| # ══════════════════════════════════════════════════════════════════ | |
| # YOLOE TEXT PROMPTS: kelas yang ingin dideteksi secara eksplisit | |
| # ══════════════════════════════════════════════════════════════════ | |
| YOLOE_PROMPTS = [ | |
| "person", "pedestrian", "motorcycle", "motorbike", "scooter", | |
| "car", "sedan", "SUV", "pickup truck", "van", "minibus", | |
| "bus", "truck", "ambulance", "police car", "fire truck", | |
| "bicycle", "traffic light", "stop sign", "speed limit sign", | |
| "road sign", "cone", "barrier", "pothole", "billboard" | |
| ] | |
| # ══════════════════════════════════════════════════════════════════ | |
| # CELL 3 MODEL MANAGER (YOLO26 + YOLOE + SEG + POSE) | |
| # ══════════════════════════════════════════════════════════════════ | |
| class ModelManager: | |
| DETECT_MODELS = { | |
| "YOLO26n — Nano (cepat, CPU ok)": "yolo26n.pt", | |
| "YOLO26s — Small (seimbang)": "yolo26s.pt", | |
| "YOLO26m — Medium (akurat)": "yolo26m.pt", | |
| "YOLO26l — Large (sangat akurat)": "yolo26l.pt", | |
| "YOLO26x — XLarge (maksimal)": "yolo26x.pt", | |
| } | |
| SEG_MODELS = { | |
| "YOLO26l-seg — Segmentation Large": "yolo26l-seg.pt", | |
| "YOLO26m-seg — Segmentation Medium": "yolo26m-seg.pt", | |
| } | |
| POSE_MODELS = { | |
| "YOLO26l-pose — Pose Estimation": "yolo26l-pose.pt", | |
| } | |
| YOLOE_MODELS = { | |
| "YOLOE-26l-seg — Text Prompt (akurat)": "yoloe-26l-seg.pt", | |
| "YOLOE-26m-seg — Text Prompt (medium)": "yoloe-26m-seg.pt", | |
| } | |
| def __init__(self): | |
| self.det_model = None | |
| self.seg_model = None | |
| self.pose_model = None | |
| self.loaded = {} # {name: model} | |
| # ── Load satu atau beberapa model sekaligus ────────────────── | |
| def load(self, det_key: str, use_seg=False, use_pose=False, | |
| use_yoloe=False, yoloe_key: str = None): | |
| print("=" * 55) | |
| # Detection model | |
| pt = self.DETECT_MODELS.get(det_key, "yolo26l.pt") | |
| print(f"[*] Loading detection: {pt}") | |
| self.det_model = YOLO(pt) | |
| print(f" OK — {len(self.det_model.names)} kelas COCO") | |
| # Segmentation model | |
| if use_seg: | |
| seg_key = list(self.SEG_MODELS.keys())[0] | |
| seg_pt = list(self.SEG_MODELS.values())[0] | |
| print(f"[*] Loading segmentation: {seg_pt}") | |
| self.seg_model = YOLO(seg_pt) | |
| print(f" OK — instance segmentation siap") | |
| # Pose model | |
| if use_pose: | |
| pose_pt = "yolo26l-pose.pt" | |
| print(f"[*] Loading pose: {pose_pt}") | |
| self.pose_model = YOLO(pose_pt) | |
| print(f" OK — pose estimation siap") | |
| # YOLOE text-prompt model | |
| if use_yoloe and yoloe_key: | |
| yoloe_pt = self.YOLOE_MODELS.get(yoloe_key, | |
| list(self.YOLOE_MODELS.values())[0]) | |
| print(f"[*] Loading YOLOE text-prompt: {yoloe_pt}") | |
| self.yoloe_model = YOLO(yoloe_pt) | |
| self.yoloe_model.set_classes(YOLOE_PROMPTS) | |
| print(f" OK — {len(YOLOE_PROMPTS)} prompt kelas set") | |
| else: | |
| self.yoloe_model = None | |
| print("=" * 55 + "\n") | |
| # ── Deteksi Utama ──────────────────────────────────────────── | |
| def detect_full( | |
| self, | |
| img_rgb : np.ndarray, | |
| conf : float = 0.25, | |
| iou : float = 0.45, | |
| use_sahi : bool = False, | |
| sahi_size : int = 640, | |
| use_seg : bool = False, | |
| use_pose : bool = False, | |
| use_yoloe : bool = False, | |
| end2end : bool = True, # YOLO26 dual-head: True = no NMS | |
| ) -> dict: | |
| """ | |
| Jalankan semua model aktif, kembalikan dict berisi: | |
| detections : list annotation standar | |
| masks : list np.ndarray (opsional, dari seg) | |
| keypoints : list (opsional, dari pose) | |
| yoloe_dets : list annotation dari YOLOE prompt | |
| """ | |
| result = {"detections": [], "masks": [], "keypoints": [], | |
| "yoloe_dets": []} | |
| # 1. Detection (YOLO26) | |
| if self.det_model: | |
| if use_sahi: | |
| dets = self._detect_sahi(self.det_model, img_rgb, | |
| conf, iou, sahi_size) | |
| else: | |
| dets = self._detect_standard(self.det_model, img_rgb, | |
| conf, iou, end2end) | |
| result["detections"] = dets | |
| # 2. Segmentation | |
| if use_seg and self.seg_model: | |
| seg_out = self._detect_seg(img_rgb, conf, iou) | |
| result["masks"] = seg_out["masks"] | |
| # merge seg detections (dedup dengan IoU) | |
| result["detections"] = self._merge_and_dedup( | |
| result["detections"], seg_out["dets"], iou) | |
| # 3. Pose | |
| if use_pose and self.pose_model: | |
| result["keypoints"] = self._detect_pose(img_rgb, conf) | |
| # 4. YOLOE Text Prompt | |
| if use_yoloe and self.yoloe_model: | |
| yoloe_raw = self.yoloe_model.predict( | |
| img_rgb, conf=conf, iou=iou, verbose=False)[0] | |
| yoloe_dets = [] | |
| for box in yoloe_raw.boxes: | |
| cls_name = self.yoloe_model.names[int(box.cls)].lower() | |
| cat, lbl = YOLO_MAP.get(cls_name, | |
| ("Objek Sekitar", cls_name.replace(" ","_"))) | |
| x1,y1,x2,y2 = map(int, box.xyxy[0].tolist()) | |
| yoloe_dets.append({ | |
| "category": cat, "label": lbl, | |
| "bbox": [x1,y1,x2,y2], | |
| "confidence": round(float(box.conf[0]),3), | |
| "source": "yoloe", | |
| "yolo_class": cls_name, | |
| }) | |
| result["yoloe_dets"] = yoloe_dets | |
| # merge ke detections utama | |
| result["detections"] = self._merge_and_dedup( | |
| result["detections"], yoloe_dets, iou) | |
| result["detections"] = self._refine_pedestrians(result["detections"]) | |
| return result | |
| # ── Standard inference ─────────────────────────────────────── | |
| def _detect_standard(self, model, img_rgb, conf, iou, end2end=True): | |
| kw = {} if end2end else {"end2end": False} | |
| results = model(img_rgb, conf=conf, iou=iou, | |
| verbose=False, **kw)[0] | |
| return self._parse_boxes(model, results.boxes) | |
| # ── Segmentation ───────────────────────────────────────────── | |
| def _detect_seg(self, img_rgb, conf, iou): | |
| results = self.seg_model(img_rgb, conf=conf, iou=iou, | |
| verbose=False)[0] | |
| dets, masks_out = [], [] | |
| if results.masks is not None: | |
| for i, (box, mask) in enumerate( | |
| zip(results.boxes, results.masks.data)): | |
| cls_name = self.seg_model.names[int(box.cls)].lower() | |
| cat, lbl = YOLO_MAP.get(cls_name, | |
| ("Objek Sekitar", cls_name.replace(" ","_"))) | |
| x1,y1,x2,y2 = map(int, box.xyxy[0].tolist()) | |
| dets.append({ | |
| "category": cat, "label": lbl, | |
| "bbox": [x1,y1,x2,y2], | |
| "confidence": round(float(box.conf[0]),3), | |
| "source": "seg", | |
| "yolo_class": cls_name, | |
| "mask_idx": i, | |
| }) | |
| m = mask.cpu().numpy() | |
| # resize mask ke ukuran gambar asli | |
| H, W = img_rgb.shape[:2] | |
| m_resized = cv2.resize(m.astype(np.uint8), | |
| (W, H), | |
| interpolation=cv2.INTER_NEAREST) | |
| masks_out.append(m_resized) | |
| else: | |
| dets = self._parse_boxes(self.seg_model, results.boxes) | |
| return {"dets": dets, "masks": masks_out} | |
| # ── Pose Estimation ────────────────────────────────────────── | |
| def _detect_pose(self, img_rgb, conf): | |
| results = self.pose_model(img_rgb, conf=conf, verbose=False)[0] | |
| poses = [] | |
| if results.keypoints is not None: | |
| for i, (box, kp) in enumerate( | |
| zip(results.boxes, results.keypoints.data)): | |
| x1,y1,x2,y2 = map(int, box.xyxy[0].tolist()) | |
| poses.append({ | |
| "bbox": [x1,y1,x2,y2], | |
| "confidence": round(float(box.conf[0]),3), | |
| "keypoints": kp.cpu().numpy().tolist(), | |
| }) | |
| return poses | |
| # ── SAHI Slicing ───────────────────────────────────────────── | |
| def _detect_sahi(self, model, img_rgb, conf, iou, slice_size): | |
| H, W = img_rgb.shape[:2] | |
| overlap = slice_size // 4 | |
| stride = slice_size - overlap | |
| raw = [] | |
| tile_count = 0 | |
| for y0 in range(0, H, stride): | |
| for x0 in range(0, W, stride): | |
| x1_ = min(x0+slice_size, W) | |
| y1_ = min(y0+slice_size, H) | |
| tile = img_rgb[y0:y1_, x0:x1_] | |
| res = model(tile, conf=conf, iou=iou, verbose=False)[0] | |
| tile_count += 1 | |
| for box in res.boxes: | |
| bx1,by1,bx2,by2 = map(int, box.xyxy[0].tolist()) | |
| raw.append({ | |
| "cls": model.names[int(box.cls)].lower(), | |
| "conf": float(box.conf[0]), | |
| "bbox": [bx1+x0, by1+y0, bx2+x0, by2+y0], | |
| }) | |
| print(f" SAHI: {tile_count} tiles -> {len(raw)} raw detections") | |
| deduped = self._nms_raw(raw, iou) | |
| print(f" After NMS: {len(deduped)} detections") | |
| return self._convert_raw(deduped) | |
| # ── NMS & Convert ──────────────────────────────────────────── | |
| @staticmethod | |
| def _nms_raw(boxes, iou_thresh=0.45): | |
| if not boxes: return [] | |
| boxes = sorted(boxes, key=lambda x: -x["conf"]) | |
| kept = [] | |
| for b in boxes: | |
| if all(ModelManager._iou(b["bbox"],k["bbox"]) < iou_thresh | |
| for k in kept): | |
| kept.append(b) | |
| return kept | |
| @staticmethod | |
| def _iou(a, b): | |
| ix1=max(a[0],b[0]); iy1=max(a[1],b[1]) | |
| ix2=min(a[2],b[2]); iy2=min(a[3],b[3]) | |
| inter=max(0,ix2-ix1)*max(0,iy2-iy1) | |
| ua=(a[2]-a[0])*(a[3]-a[1])+(b[2]-b[0])*(b[3]-b[1])-inter | |
| return inter/ua if ua>0 else 0 | |
| @staticmethod | |
| def _convert_raw(boxes): | |
| dets = [] | |
| for b in boxes: | |
| cat, lbl = YOLO_MAP.get(b["cls"], | |
| ("Objek Sekitar", b["cls"].replace(" ","_"))) | |
| dets.append({ | |
| "category": cat, "label": lbl, | |
| "bbox": b["bbox"], | |
| "confidence": round(b["conf"],3), | |
| "source": "auto", | |
| "yolo_class": b["cls"], | |
| }) | |
| return dets | |
| @staticmethod | |
| def _parse_boxes(model, boxes_tensor): | |
| dets = [] | |
| for box in boxes_tensor: | |
| cls_name = model.names[int(box.cls)].lower() | |
| cat, lbl = YOLO_MAP.get(cls_name, | |
| ("Objek Sekitar", cls_name.replace(" ","_"))) | |
| x1,y1,x2,y2 = map(int, box.xyxy[0].tolist()) | |
| dets.append({ | |
| "category": cat, "label": lbl, | |
| "bbox": [x1,y1,x2,y2], | |
| "confidence": round(float(box.conf[0]),3), | |
| "source": "auto", | |
| "yolo_class": cls_name, | |
| }) | |
| return dets | |
| @staticmethod | |
| def _merge_and_dedup(base, extra, iou_thresh=0.5): | |
| merged = list(base) | |
| for e in extra: | |
| duplicate = any( | |
| ModelManager._iou(e["bbox"], b["bbox"]) > iou_thresh | |
| and e["label"] == b["label"] | |
| for b in merged | |
| ) | |
| if not duplicate: | |
| merged.append(e) | |
| return merged | |
| @staticmethod | |
| def _refine_pedestrians(dets): | |
| people = [d for d in dets if d["label"] == "pejalan_kaki"] | |
| bikes = [d for d in dets if d["label"] in ["sepeda_motor", "sepeda"]] | |
| for p in people: | |
| for b in bikes: | |
| px1, py1, px2, py2 = p["bbox"] | |
| bx1, by1, bx2, by2 = b["bbox"] | |
| # Ambil titik tengah bagian bawah orang (asumsi ini posisi kaki/pinggul) | |
| p_bottom_cx = (px1 + px2) / 2 | |
| p_bottom_cy = py2 | |
| # Cek apakah kaki orang berada sejajar/di dalam batas horizontal motor | |
| is_inside_x = bx1 <= p_bottom_cx <= bx2 | |
| # Cek apakah kaki orang berada di area ketinggian motor | |
| # (toleransi tambahan 50 pixel ke bawah kalau kakinya menjuntai) | |
| is_inside_y = by1 <= p_bottom_cy <= (by2 + 50) | |
| # Hitung sedikit irisan (intersection) untuk jaga-jaga | |
| ix1 = max(px1, bx1); iy1 = max(py1, by1) | |
| ix2 = min(px2, bx2); iy2 = min(py2, by2) | |
| inter_area = max(0, ix2 - ix1) * max(0, iy2 - iy1) | |
| p_area = max(1, (px2 - px1) * (py2 - py1)) | |
| # Jika kaki orang masuk di area motor ATAU ada irisan area > 5%, ubah labelnya! | |
| if (is_inside_x and is_inside_y) or (inter_area / p_area > 0.05): | |
| p["label"] = "pengendara_motor" if b["label"] == "sepeda_motor" else "pengendara_sepeda" | |
| p["category"] = "Manusia" | |
| break # Selesai, lanjut ke orang berikutnya | |
| return dets | |
| model_manager = ModelManager() | |
| # ══════════════════════════════════════════════════════════════════ | |
| # CELL 4 ANNOTATION MANAGER | |
| # ══════════════════════════════════════════════════════════════════ | |
| class AnnotationManager: | |
| def __init__(self): | |
| self.data : dict = {} # {fname: {annotations:[], masks:[], kps:[]}} | |
| self.images: dict = {} # {fname: np.ndarray RGB} | |
| def add_image(self, fname, img_rgb): | |
| if fname not in self.data: | |
| self.data[fname] = { | |
| "image_path": fname, | |
| "width": img_rgb.shape[1], | |
| "height": img_rgb.shape[0], | |
| "annotations": [], | |
| "masks": [], | |
| "keypoints": [], | |
| } | |
| self.images[fname] = img_rgb | |
| def add_annotations(self, fname, anns, masks=None, keypoints=None): | |
| self.data[fname]["annotations"].extend(anns) | |
| if masks: | |
| self.data[fname]["masks"].extend(masks) | |
| if keypoints: | |
| self.data[fname]["keypoints"].extend(keypoints) | |
| def add_one(self, fname, ann): | |
| self.data[fname]["annotations"].append(ann) | |
| def remove(self, fname, idx): | |
| a = self.data[fname]["annotations"] | |
| if 0 <= idx < len(a): | |
| a.pop(idx) | |
| def clear_auto(self, fname): | |
| self.data[fname]["annotations"] = [ | |
| a for a in self.data[fname]["annotations"] | |
| if a.get("source") == "manual"] | |
| self.data[fname]["masks"] = [] | |
| self.data[fname]["keypoints"] = [] | |
| def get_anns(self, fname): | |
| return self.data.get(fname, {}).get("annotations", []) | |
| def get_masks(self, fname): | |
| return self.data.get(fname, {}).get("masks", []) | |
| def get_poses(self, fname): | |
| return self.data.get(fname, {}).get("keypoints", []) | |
| def total(self): | |
| return sum(len(v["annotations"]) for v in self.data.values()) | |
| # ── Export JSON ────────────────────────────────────────────── | |
| def export_json(self, path="annotations.json"): | |
| out = [] | |
| for fname, info in self.data.items(): | |
| for ann in info["annotations"]: | |
| row = {k: v for k, v in ann.items() | |
| if k not in ("mask_idx", "yolo_class")} | |
| row["image"] = fname | |
| out.append(row) | |
| with open(path, "w") as f: | |
| json.dump(out, f, indent=2, ensure_ascii=False) | |
| print(f"[JSON ] -> {path} ({len(out)} objek)") | |
| return path | |
| # ── Export YOLO TXT ────────────────────────────────────────── | |
| def export_yolo(self, out_dir="yolo_dataset"): | |
| img_dir = os.path.join(out_dir, "images") | |
| lbl_dir = os.path.join(out_dir, "labels") | |
| os.makedirs(img_dir, exist_ok=True) | |
| os.makedirs(lbl_dir, exist_ok=True) | |
| # classes.txt | |
| with open(os.path.join(out_dir,"classes.txt"),"w") as f: | |
| f.write("\n".join(ALL_LABELS)) | |
| # data.yaml | |
| with open(os.path.join(out_dir,"data.yaml"),"w") as f: | |
| f.write(f"path: {os.path.abspath(out_dir)}\n") | |
| f.write("train: images\nval: images\n") | |
| f.write(f"nc: {len(ALL_LABELS)}\n") | |
| f.write(f"names: {ALL_LABELS}\n") | |
| count = 0 | |
| for fname, info in self.data.items(): | |
| W, H = info.get("width",1), info.get("height",1) | |
| base = os.path.splitext(os.path.basename(fname))[0] | |
| # salin gambar | |
| if os.path.exists(fname): | |
| import shutil | |
| shutil.copy(fname, os.path.join(img_dir, os.path.basename(fname))) | |
| # label txt | |
| with open(os.path.join(lbl_dir, base+".txt"),"w") as f: | |
| for ann in info["annotations"]: | |
| x1,y1,x2,y2 = ann["bbox"] | |
| cx=(x1+x2)/(2*W); cy=(y1+y2)/(2*H) | |
| bw=(x2-x1)/W; bh=(y2-y1)/H | |
| lid = LABEL2ID.get(ann["label"],0) | |
| f.write(f"{lid} {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}\n") | |
| count += 1 | |
| print(f"[YOLO ] -> {out_dir}/ ({count} anotasi, {len(ALL_LABELS)} kelas)") | |
| return out_dir | |
| # ── Export COCO JSON ───────────────────────────────────────── | |
| def export_coco(self, path="coco_annotations.json"): | |
| categories = [{"id": i+1, "name": l, "supercategory": | |
| LABEL2CAT.get(l,"object")} | |
| for i,l in enumerate(ALL_LABELS)] | |
| label2id = {l: i+1 for i,l in enumerate(ALL_LABELS)} | |
| images_list, anns_list = [], [] | |
| ann_id = 1 | |
| for img_id, (fname, info) in enumerate(self.data.items(), 1): | |
| W = info.get("width",0); H = info.get("height",0) | |
| images_list.append({ | |
| "id": img_id, "file_name": os.path.basename(fname), | |
| "width": W, "height": H | |
| }) | |
| for ann in info["annotations"]: | |
| x1,y1,x2,y2 = ann["bbox"] | |
| w,h = x2-x1, y2-y1 | |
| anns_list.append({ | |
| "id": ann_id, "image_id": img_id, | |
| "category_id": label2id.get(ann["label"],1), | |
| "bbox": [x1,y1,w,h], "area": w*h, | |
| "iscrowd": 0, | |
| "score": ann.get("confidence",1.0), | |
| "source": ann.get("source","manual"), | |
| }) | |
| ann_id += 1 | |
| coco = {"info": {"description": "AI Self-Driving Dataset", | |
| "version": "1.0"}, | |
| "images": images_list, | |
| "annotations": anns_list, | |
| "categories": categories} | |
| with open(path,"w") as f: | |
| json.dump(coco, f, indent=2, ensure_ascii=False) | |
| print(f"[COCO ] -> {path} ({len(anns_list)} anotasi)") | |
| return path | |
| # ── Export Segmentation Mask PNG ───────────────────────────── | |
| def export_masks(self, out_dir="seg_masks"): | |
| os.makedirs(out_dir, exist_ok=True) | |
| count = 0 | |
| for fname, info in self.data.items(): | |
| masks = info.get("masks",[]) | |
| if not masks: continue | |
| base = os.path.splitext(os.path.basename(fname))[0] | |
| H = info.get("height",1) | |
| W = info.get("width",1) | |
| # combined mask (255 = objek) | |
| combined = np.zeros((H,W), dtype=np.uint8) | |
| for m in masks: | |
| combined = np.maximum(combined, | |
| (m*255).astype(np.uint8) if m.max()<=1 else m) | |
| cv2.imwrite(os.path.join(out_dir, base+"_mask.png"), combined) | |
| count += 1 | |
| print(f"[MASK ] -> {out_dir}/ ({count} mask files)") | |
| return out_dir | |
| # ── Export Semua ke ZIP ────────────────────────────────────── | |
| def export_zip(self, path="annotation_export.zip"): | |
| j = self.export_json("_tmp_annotations.json") | |
| c = self.export_coco("_tmp_coco.json") | |
| y = self.export_yolo("_tmp_yolo") | |
| with zipfile.ZipFile(path,"w") as zf: | |
| zf.write(j, "annotations.json") | |
| zf.write(c, "coco_annotations.json") | |
| for root,_,fls in os.walk(y): | |
| for fn in fls: | |
| fp = os.path.join(root,fn) | |
| zf.write(fp, os.path.relpath(fp, "_tmp_yolo")) | |
| print(f"[ZIP ] -> {path}") | |
| return path | |
| ann_manager = AnnotationManager() | |
| # ══════════════════════════════════════════════════════════════════ | |
| # CELL 5 VISUALISASI | |
| # ══════════════════════════════════════════════════════════════════ | |
| # ── Skeleton COCO 17 keypoints ─────────────────────────────────── | |
| POSE_SKELETON = [ | |
| (0,1),(0,2),(1,3),(2,4), # kepala | |
| (5,6),(5,7),(7,9),(6,8),(8,10), # bahu-tangan | |
| (5,11),(6,12),(11,12), # torso | |
| (11,13),(13,15),(12,14),(14,16), # kaki | |
| ] | |
| POSE_COLORS = [(255,100,100),(100,255,100),(100,100,255), | |
| (255,255,100),(255,100,255),(100,255,255)] | |
| def draw_full( | |
| img_rgb : np.ndarray, | |
| anns : list, | |
| masks : list = None, | |
| poses : list = None, | |
| alpha : float = 0.18, | |
| show_conf: bool = True, | |
| show_cat : bool = True, | |
| ) -> np.ndarray: | |
| """Gambar BBox + Mask overlay + Skeleton pada satu gambar.""" | |
| out = img_rgb.copy() | |
| # ── Mask overlay (segmentation) ────────────────────────────── | |
| if masks: | |
| for i, ann in enumerate(anns): | |
| mid = ann.get("mask_idx", i) | |
| if mid < len(masks): | |
| m = masks[mid] | |
| cat = ann.get("category","Kendaraan") | |
| hex_c = CATEGORY_COLORS.get(cat,"#FFFFFF") | |
| r,g,b = int(hex_c[1:3],16),int(hex_c[3:5],16),int(hex_c[5:7],16) | |
| colored = np.zeros_like(out) | |
| colored[m > 0] = (r,g,b) | |
| out = cv2.addWeighted(out, 1-alpha*1.2, colored, alpha*1.2, 0) | |
| # ── BBox ───────────────────────────────────────────────────── | |
| for ann in anns: | |
| x1,y1,x2,y2 = ann["bbox"] | |
| cat = ann.get("category","Objek Sekitar") | |
| label = ann.get("label","?") | |
| conf = ann.get("confidence",1.0) | |
| src = ann.get("source","manual") | |
| hex_c = CATEGORY_COLORS.get(cat,"#FFFFFF") | |
| r,g,b = int(hex_c[1:3],16),int(hex_c[3:5],16),int(hex_c[5:7],16) | |
| bgr = (b,g,r) | |
| # isi semi-transparan | |
| ov = out.copy() | |
| cv2.rectangle(ov,(x1,y1),(x2,y2),bgr,-1) | |
| out = cv2.addWeighted(out,1-alpha,ov,alpha,0) | |
| # border (tebal berbeda per source) | |
| thick = 3 if src=="manual" else 2 if src=="seg" else 1 | |
| cv2.rectangle(out,(x1,y1),(x2,y2),bgr,thick) | |
| # label tag | |
| parts = [] | |
| if show_cat: parts.append(CATEGORY_META.get(cat,{}).get("icon","")) | |
| parts.append(label) | |
| if show_conf and src!="manual": | |
| parts.append(f"{conf:.0%}") | |
| tag = " ".join(p for p in parts if p) | |
| (tw,th),_ = cv2.getTextSize(tag, cv2.FONT_HERSHEY_SIMPLEX, 0.42, 1) | |
| ty = max(y1-2, th+6) | |
| cv2.rectangle(out,(x1,ty-th-5),(x1+tw+6,ty+1),bgr,-1) | |
| cv2.putText(out, tag, (x1+3,ty-2), | |
| cv2.FONT_HERSHEY_SIMPLEX, 0.42, (255,255,255), 1, | |
| cv2.LINE_AA) | |
| # ── Pose skeleton ──────────────────────────────────────────── | |
| if poses: | |
| for pose_data in poses: | |
| kps = np.array(pose_data["keypoints"]) | |
| # joints | |
| for j, (kx,ky,kv) in enumerate(kps): | |
| if kv > 0.3: | |
| col = POSE_COLORS[j % len(POSE_COLORS)] | |
| cv2.circle(out,(int(kx),int(ky)),3,col,-1,cv2.LINE_AA) | |
| # skeleton | |
| for (a,b_) in POSE_SKELETON: | |
| if a<len(kps) and b_<len(kps): | |
| (ax,ay,av),(bx,by,bv) = kps[a], kps[b_] | |
| if av>0.3 and bv>0.3: | |
| cv2.line(out,(int(ax),int(ay)),(int(bx),int(by)), | |
| (200,200,0),1,cv2.LINE_AA) | |
| return out | |
| def show_result(fname, img_rgb, anns, masks=None, poses=None, | |
| title="", show_stats=True): | |
| """Panel visualisasi 3-kolom: Asli | Anotasi | Statistik mini.""" | |
| has_extra = bool(masks or poses) | |
| ncols = 3 if show_stats else 2 | |
| fig = plt.figure(figsize=(18, 7) if ncols==3 else (14,6)) | |
| fig.patch.set_facecolor("#12121e") | |
| gs = GridSpec(1, ncols, figure=fig, wspace=0.05) | |
| axes = [fig.add_subplot(gs[0,i]) for i in range(ncols)] | |
| for ax in axes: | |
| ax.set_facecolor("#1a1a2e"); ax.axis("off") | |
| # kolom 0: gambar asli | |
| axes[0].imshow(img_rgb) | |
| axes[0].set_title("Gambar Asli", color="#aaa", fontsize=11, pad=6) | |
| # kolom 1: hasil anotasi lengkap | |
| img_ann = draw_full(img_rgb, anns, masks, poses) | |
| axes[1].imshow(img_ann) | |
| src_tag = "" | |
| if any(a.get("source")=="seg" for a in anns): src_tag += " +SEG" | |
| if any(a.get("source")=="yoloe" for a in anns): src_tag += " +YOLOE" | |
| if poses: src_tag += " +POSE" | |
| axes[1].set_title(f"Hasil Anotasi ({len(anns)} objek){src_tag}", | |
| color="white", fontsize=11, pad=6) | |
| # kolom 2: mini bar chart | |
| if show_stats and ncols == 3: | |
| ax3 = axes[2] | |
| ax3.set_facecolor("#0f0f1e") | |
| ax3.axis("on") | |
| cat_cnt = {} | |
| for a in anns: | |
| cat_cnt[a.get("category","?")] = cat_cnt.get(a.get("category","?"),0)+1 | |
| cats = sorted(cat_cnt.items(), key=lambda x:-x[1]) | |
| y_pos = range(len(cats)) | |
| colors = [CATEGORY_COLORS.get(c[0],"#888") for c in cats] | |
| bars = ax3.barh([c[0] for c in cats],[c[1] for c in cats], | |
| color=colors, edgecolor="#12121e", height=0.6) | |
| for bar, (_,cnt) in zip(bars,cats): | |
| ax3.text(bar.get_width()+0.1, bar.get_y()+bar.get_height()/2, | |
| str(cnt), va="center", color="white", fontsize=9) | |
| ax3.set_title("Per Kategori", color="#aaa", fontsize=10, pad=6) | |
| ax3.tick_params(colors="#aaa", labelsize=8) | |
| ax3.spines[["top","right","bottom","left"]].set_color("#333") | |
| ax3.set_facecolor("#0f0f1e") | |
| # legend bawah | |
| legend_p = [mpatches.Patch(color=c,label=k) | |
| for k,c in CATEGORY_COLORS.items()] | |
| fig.legend(handles=legend_p, loc="lower center", ncol=7, | |
| facecolor="#0f3460", edgecolor="none", | |
| labelcolor="white", fontsize=8, framealpha=0.9, | |
| bbox_to_anchor=(0.5,-0.05)) | |
| plt.suptitle(title or os.path.basename(fname), | |
| color="white", fontsize=13, y=1.01) | |
| plt.tight_layout() | |
| plt.show() | |
| # ══════════════════════════════════════════════════════════════════ | |
| # CELL 6 STATISTIK | |
| # ══════════════════════════════════════════════════════════════════ | |
| def show_statistics(ann_manager: AnnotationManager): | |
| all_anns = [a for info in ann_manager.data.values() | |
| for a in info["annotations"]] | |
| if not all_anns: | |
| print("[!] Belum ada anotasi."); return | |
| cat_cnt = {k:0 for k in CATEGORY_META} | |
| lbl_cnt = {} | |
| src_cnt = {"auto":0,"seg":0,"yoloe":0,"manual":0} | |
| conf_all = [] | |
| for a in all_anns: | |
| cat = a.get("category","Objek Sekitar") | |
| cat_cnt[cat] = cat_cnt.get(cat,0)+1 | |
| lbl = a.get("label","?") | |
| lbl_cnt[lbl] = lbl_cnt.get(lbl,0)+1 | |
| src = a.get("source","manual") | |
| src_cnt[src] = src_cnt.get(src,0)+1 | |
| if src != "manual": | |
| conf_all.append(a.get("confidence",1.0)) | |
| fig = plt.figure(figsize=(18,9)) | |
| fig.patch.set_facecolor("#12121e") | |
| # 1. Pie | |
| ax1 = fig.add_subplot(2,3,1); ax1.set_facecolor("#1a1a2e") | |
| labs = [k for k,v in cat_cnt.items() if v>0] | |
| sizes = [cat_cnt[k] for k in labs] | |
| cols = [CATEGORY_COLORS[k] for k in labs] | |
| _,_,at = ax1.pie(sizes,labels=labs,autopct="%1.0f%%", | |
| colors=cols,startangle=90, | |
| textprops={"color":"white","fontsize":7}) | |
| for t in at: t.set_color("black"); t.set_fontsize(7) | |
| ax1.set_title("Distribusi Kategori",color="white",fontsize=11) | |
| # 2. Top label bar | |
| ax2 = fig.add_subplot(2,3,2); ax2.set_facecolor("#1a1a2e") | |
| top = sorted(lbl_cnt.items(),key=lambda x:-x[1])[:15] | |
| bar_colors = [CATEGORY_COLORS.get(LABEL2CAT.get(l,"Objek Sekitar"),"#888") | |
| for l,_ in top] | |
| bars = ax2.barh([t[0] for t in top],[t[1] for t in top], | |
| color=bar_colors,edgecolor="#1a1a2e") | |
| for bar,(_,cnt) in zip(bars,top): | |
| ax2.text(bar.get_width()+.1,bar.get_y()+bar.get_height()/2, | |
| str(cnt),va="center",color="white",fontsize=8) | |
| ax2.set_title("Top 15 Label",color="white",fontsize=11) | |
| ax2.tick_params(colors="white",labelsize=8) | |
| ax2.spines[["top","right","bottom","left"]].set_color("#333") | |
| # 3. Objek per gambar | |
| ax3 = fig.add_subplot(2,3,3); ax3.set_facecolor("#1a1a2e") | |
| names = [os.path.basename(f)[:12]+"~" if len(os.path.basename(f))>12 | |
| else os.path.basename(f) for f in ann_manager.data] | |
| counts = [len(i["annotations"]) for i in ann_manager.data.values()] | |
| ax3.bar(names,counts,color="#FF6B35",edgecolor="#12121e") | |
| ax3.set_title("Objek per Gambar",color="white",fontsize=11) | |
| ax3.tick_params(colors="white",labelsize=7,axis="x",rotation=35) | |
| ax3.tick_params(colors="white",labelsize=8,axis="y") | |
| ax3.spines[["top","right"]].set_color("#333") | |
| # 4. Source pie | |
| ax4 = fig.add_subplot(2,3,4); ax4.set_facecolor("#1a1a2e") | |
| src_labs = [k for k,v in src_cnt.items() if v>0] | |
| src_sizes = [src_cnt[k] for k in src_labs] | |
| src_cols = ["#4ECDC4","#85C1E9","#FFE66D","#FF8B94"][:len(src_labs)] | |
| ax4.pie(src_sizes,labels=src_labs,autopct="%1.0f%%", | |
| colors=src_cols,startangle=90, | |
| textprops={"color":"white","fontsize":9}) | |
| ax4.set_title("Sumber Anotasi",color="white",fontsize=11) | |
| # 5. Confidence histogram | |
| ax5 = fig.add_subplot(2,3,5); ax5.set_facecolor("#1a1a2e") | |
| if conf_all: | |
| ax5.hist(conf_all,bins=20,color="#4ECDC4",edgecolor="#12121e") | |
| ax5.axvline(np.mean(conf_all),color="#FFE66D",linewidth=1.5, | |
| linestyle="--",label=f"Avg {np.mean(conf_all):.1%}") | |
| ax5.legend(facecolor="#0f3460",labelcolor="white",fontsize=8) | |
| ax5.set_title("Distribusi Confidence",color="white",fontsize=11) | |
| ax5.tick_params(colors="white",labelsize=8) | |
| ax5.spines[["top","right"]].set_color("#333") | |
| ax5.set_xlabel("Confidence",color="#aaa",fontsize=9) | |
| # 6. Summary teks | |
| ax6 = fig.add_subplot(2,3,6); ax6.axis("off") | |
| ax6.set_facecolor("#0f0f1e") | |
| lines = [ | |
| f"Total Gambar : {len(ann_manager.data)}", | |
| f"Total Objek : {len(all_anns)}", | |
| f"Auto YOLO26 : {src_cnt['auto']}", | |
| f"Segmentation : {src_cnt['seg']}", | |
| f"YOLOE Prompt : {src_cnt['yoloe']}", | |
| f"Manual : {src_cnt['manual']}", | |
| "", | |
| f"Avg Conf(auto) : {np.mean(conf_all):.1%}" if conf_all else "No auto dets", | |
| f"Label unik : {len(lbl_cnt)}", | |
| ] | |
| for i,l in enumerate(lines): | |
| ax6.text(0.05, 0.95-i*0.1, l, transform=ax6.transAxes, | |
| color="white" if l else "#555", | |
| fontsize=9, va="top", fontfamily="monospace") | |
| ax6.set_title("Ringkasan",color="white",fontsize=11) | |
| plt.suptitle("STATISTIK ANOTASI — AI Self-Driving Dataset", | |
| color="white",fontsize=14,y=1.01) | |
| plt.tight_layout() | |
| plt.show() | |
| # ══════════════════════════════════════════════════════════════════ | |
| # CELL 7 UI WIDGET | |
| # ══════════════════════════════════════════════════════════════════ | |
| class AnnotationUI: | |
| def __init__(self): | |
| self._build_ui() | |
| def _build_ui(self): | |
| s = {"description_width": "160px"} | |
| W6 = widgets.Layout(width="66%") | |
| W3 = widgets.Layout(width="30%") | |
| # ── Header ────────────────────────────────────────────── | |
| self.header = widgets.HTML(""" | |
| <div style="background:linear-gradient(135deg,#0a0a1e,#0f3460,#0a0a1e); | |
| border-radius:14px;padding:20px 28px;margin-bottom:10px; | |
| border:1px solid #1a5276;"> | |
| <div style="font-family:monospace;color:#4ECDC4;font-size:18px; | |
| letter-spacing:3px;font-weight:bold;"> | |
| [YOLO26] AI SELF-DRIVING ANNOTATION v3.0 | |
| </div> | |
| <div style="color:#85C1E9;font-size:12px;margin-top:4px;"> | |
| Detection | Segmentation | Pose | YOLOE Text-Prompt | SAHI | |
| </div> | |
| <div style="margin-top:8px;font-size:11px;color:#555;"> | |
| YOLO26 Dual-Head · 7 Kategori · 48 Label · 4 Format Export | |
| </div> | |
| </div>""") | |
| # ── Model Settings ────────────────────────────────────── | |
| self.dd_det = widgets.Dropdown( | |
| options=list(ModelManager.DETECT_MODELS.keys()), | |
| value="YOLO26l — Large (sangat akurat)", | |
| description="Detection Model:", style=s, layout=W6) | |
| self.chk_seg = widgets.Checkbox(value=False, | |
| description="Aktifkan Segmentation (YOLO26-seg)", | |
| style={"description_width":"initial"}) | |
| self.chk_pose = widgets.Checkbox(value=False, | |
| description="Aktifkan Pose Estimation (YOLO26-pose)", | |
| style={"description_width":"initial"}) | |
| self.chk_yoloe = widgets.Checkbox(value=False, | |
| description="Aktifkan YOLOE Text-Prompt (deteksi kelas kustom)", | |
| style={"description_width":"initial"}) | |
| self.dd_yoloe = widgets.Dropdown( | |
| options=list(ModelManager.YOLOE_MODELS.keys()), | |
| description="YOLOE Model:", style=s, layout=W6, | |
| disabled=True) | |
| # toggle yoloe dropdown | |
| def _on_yoloe_chk(change): | |
| self.dd_yoloe.disabled = not change["new"] | |
| self.chk_yoloe.observe(_on_yoloe_chk, "value") | |
| # ── Detection Settings ─────────────────────────────────── | |
| self.sl_conf = widgets.FloatSlider( | |
| value=0.22, min=0.05, max=0.85, step=0.01, | |
| description="Confidence:", readout_format=".0%", | |
| style=s, layout=W6) | |
| self.sl_iou = widgets.FloatSlider( | |
| value=0.45, min=0.10, max=0.80, step=0.05, | |
| description="IoU (NMS):", readout_format=".0%", | |
| style=s, layout=W6) | |
| self.chk_end2end = widgets.Checkbox(value=True, | |
| description="YOLO26 One-to-One Head (no NMS, lebih cepat)", | |
| style={"description_width":"initial"}) | |
| self.chk_sahi = widgets.Checkbox(value=False, | |
| description="SAHI Slicing — untuk objek kecil/jauh", | |
| style={"description_width":"initial"}) | |
| self.sl_sahi = widgets.IntSlider( | |
| value=640, min=320, max=1280, step=64, | |
| description="Slice Size (px):", style=s, layout=W6) | |
| self.chk_showcat = widgets.Checkbox(value=True, | |
| description="Tampilkan ikon kategori pada label", | |
| style={"description_width":"initial"}) | |
| self.chk_showconf = widgets.Checkbox(value=True, | |
| description="Tampilkan confidence pada label", | |
| style={"description_width":"initial"}) | |
| # ── Buttons ───────────────────────────────────────────── | |
| btn = lambda txt, style, w="155px": widgets.Button( | |
| description=txt, button_style=style, | |
| layout=widgets.Layout(width=w)) | |
| self.btn_load = btn("[*] Load Model", "primary", "165px") | |
| self.lbl_status = widgets.Label(" Belum dimuat") | |
| self.btn_upload = btn("[+] Upload Gambar", "info", "175px") | |
| self.btn_detect = btn("[>] Auto Detect", "success") | |
| self.btn_detect_all= btn("[>>] Detect Semua", "success") | |
| self.btn_redetect = btn("[!] Re-Detect (reset)","warning", "185px") | |
| self.btn_show = btn("[v] Tampilkan", "") | |
| self.btn_add = btn("[+] Tambah Manual", "warning", "175px") | |
| self.btn_del = btn("[x] Hapus Idx", "danger", "135px") | |
| self.btn_stats = btn("[S] Statistik", "") | |
| self.btn_exp_json = btn("[J] JSON", "") | |
| self.btn_exp_yolo = btn("[Y] YOLO", "") | |
| self.btn_exp_coco = btn("[C] COCO", "") | |
| self.btn_exp_mask = btn("[M] Mask PNG", "") | |
| self.btn_exp_zip = btn("[Z] ZIP Semua", "primary") | |
| for b in [self.btn_upload, self.btn_detect, self.btn_detect_all, | |
| self.btn_redetect, self.btn_show, self.btn_add, self.btn_del]: | |
| b.disabled = True | |
| # ── Gambar & Manual Input ──────────────────────────────── | |
| self.dd_img = widgets.Dropdown( | |
| options=[], description="Gambar aktif:", | |
| style=s, layout=W6, disabled=True) | |
| self.dd_cat = widgets.Dropdown( | |
| options=list(CATEGORY_LABELS.keys()), | |
| description="Kategori:", style=s, layout=W6) | |
| self.dd_lbl = widgets.Dropdown( | |
| options=CATEGORY_LABELS["Kendaraan"], | |
| description="Label:", style=s, layout=W6) | |
| self.int_x1 = widgets.IntText(description="x1:", style=s, layout=W3) | |
| self.int_y1 = widgets.IntText(description="y1:", style=s, layout=W3) | |
| self.int_x2 = widgets.IntText(description="x2:", style=s, layout=W3) | |
| self.int_y2 = widgets.IntText(description="y2:", style=s, layout=W3) | |
| self.int_del = widgets.IntText(description="Index:", value=0, | |
| style=s, layout=W3) | |
| self.out = widgets.Output() | |
| # ── Events ────────────────────────────────────────────── | |
| self.btn_load.on_click(self._load) | |
| self.btn_upload.on_click(self._upload) | |
| self.btn_detect.on_click(lambda _: self._detect(False)) | |
| self.btn_detect_all.on_click(self._detect_all) | |
| self.btn_redetect.on_click(lambda _: self._detect(True)) | |
| self.btn_show.on_click(self._show) | |
| self.dd_cat.observe(lambda _: setattr( | |
| self.dd_lbl,"options", | |
| CATEGORY_LABELS.get(self.dd_cat.value,[])), "value") | |
| self.btn_add.on_click(self._add_manual) | |
| self.btn_del.on_click(self._delete) | |
| self.btn_stats.on_click(lambda _: self._stats()) | |
| self.btn_exp_json.on_click(lambda _: self._export("json")) | |
| self.btn_exp_yolo.on_click(lambda _: self._export("yolo")) | |
| self.btn_exp_coco.on_click(lambda _: self._export("coco")) | |
| self.btn_exp_mask.on_click(lambda _: self._export("mask")) | |
| self.btn_exp_zip.on_click(self._download_zip) | |
| # ── Layout ────────────────────────────────────────────────── | |
| def _sec(self, t): | |
| return widgets.HTML( | |
| f'<div style="background:linear-gradient(90deg,#0f3460,#0a0a1e);' | |
| f'border-radius:8px;padding:6px 16px;margin:10px 0 4px;' | |
| f'border-left:3px solid #4ECDC4;">' | |
| f'<b style="color:#FFE66D;font-size:12px;font-family:monospace;">' | |
| f'{t}</b></div>') | |
| def _tip(self, t): | |
| return widgets.HTML( | |
| f'<p style="color:#666;font-size:10px;margin:2px 0 6px 4px;">{t}</p>') | |
| def display(self): | |
| ui = widgets.VBox([ | |
| self.header, | |
| self._sec("(1) KONFIGURASI MODEL"), | |
| self.dd_det, | |
| widgets.HBox([self.chk_seg, self.chk_pose]), | |
| widgets.HBox([self.chk_yoloe, self.dd_yoloe]), | |
| widgets.HBox([self.btn_load, self.lbl_status]), | |
| self._sec("(2) UPLOAD GAMBAR"), | |
| self.btn_upload, | |
| self._sec("(3) PILIH GAMBAR"), | |
| widgets.HBox([self.dd_img, self.btn_show]), | |
| self._sec("(4) PENGATURAN DETEKSI"), | |
| self._tip("Tips: Gunakan 15-25% untuk jalan ramai. SAHI membantu objek kecil di kejauhan."), | |
| self.sl_conf, self.sl_iou, | |
| self.chk_end2end, | |
| widgets.HBox([self.chk_sahi, self.sl_sahi]), | |
| widgets.HBox([self.chk_showcat, self.chk_showconf]), | |
| widgets.HBox([self.btn_detect, self.btn_detect_all, | |
| self.btn_redetect]), | |
| self._sec("(5) TAMBAH MANUAL"), | |
| widgets.HBox([self.dd_cat, self.dd_lbl]), | |
| widgets.HBox([self.int_x1, self.int_y1, | |
| self.int_x2, self.int_y2]), | |
| self.btn_add, | |
| self._sec("(6) HAPUS ANOTASI"), | |
| widgets.HBox([self.int_del, self.btn_del]), | |
| self._sec("(7) STATISTIK & EXPORT"), | |
| widgets.HBox([self.btn_stats]), | |
| widgets.HBox([self.btn_exp_json, self.btn_exp_yolo, | |
| self.btn_exp_coco, self.btn_exp_mask, | |
| self.btn_exp_zip]), | |
| self.out, | |
| ], layout=widgets.Layout(padding="14px", | |
| border="1px solid #1a3a5c", | |
| border_radius="14px")) | |
| display(ui) | |
| # ── Callbacks ─────────────────────────────────────────────── | |
| def _load(self, _): | |
| with self.out: | |
| clear_output() | |
| model_manager.load( | |
| det_key = self.dd_det.value, | |
| use_seg = self.chk_seg.value, | |
| use_pose = self.chk_pose.value, | |
| use_yoloe = self.chk_yoloe.value, | |
| yoloe_key = self.dd_yoloe.value if self.chk_yoloe.value else None, | |
| ) | |
| mods = ["YOLO26"] | |
| if self.chk_seg.value: mods.append("SEG") | |
| if self.chk_pose.value: mods.append("POSE") | |
| if self.chk_yoloe.value: mods.append("YOLOE") | |
| self.lbl_status.value = f" [OK] {'+'.join(mods)} siap" | |
| self.btn_upload.disabled = False | |
| def _upload(self, _): | |
| with self.out: | |
| clear_output() | |
| print("Pilih gambar (bisa multiple) ...") | |
| uploaded = files.upload() | |
| if not uploaded: return | |
| for fname in uploaded: | |
| img_bgr = cv2.imread(fname) | |
| if img_bgr is None: | |
| print(f"[!] Gagal: {fname}"); continue | |
| img_rgb = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB) | |
| ann_manager.add_image(fname, img_rgb) | |
| print(f" [OK] {fname} " | |
| f"({img_rgb.shape[1]}x{img_rgb.shape[0]} px)") | |
| opts = list(ann_manager.images.keys()) | |
| self.dd_img.options = opts | |
| self.dd_img.value = opts[-1] | |
| self.dd_img.disabled = False | |
| for b in [self.btn_show, self.btn_add, self.btn_del, | |
| self.btn_detect, self.btn_detect_all, | |
| self.btn_redetect]: | |
| b.disabled = False | |
| print(f"\n[OK] {len(uploaded)} gambar siap dianotasi!") | |
| def _run_detect(self, fname, clear_prev=False): | |
| if fname not in ann_manager.images: | |
| print("[!] Gambar tidak ada."); return | |
| img_rgb = ann_manager.images[fname] | |
| if clear_prev: | |
| ann_manager.clear_auto(fname) | |
| print("[*] Anotasi auto sebelumnya dihapus.") | |
| conf = self.sl_conf.value | |
| iou = self.sl_iou.value | |
| sahi = self.chk_sahi.value | |
| sz = self.sl_sahi.value | |
| e2e = self.chk_end2end.value | |
| print(f"[*] Deteksi: conf={conf:.0%} iou={iou:.0%}" | |
| f" e2e={e2e}" | |
| f" SAHI={'ON '+str(sz)+'px' if sahi else 'OFF'}") | |
| mods = [] | |
| if self.chk_seg.value: mods.append("SEG") | |
| if self.chk_pose.value: mods.append("POSE") | |
| if self.chk_yoloe.value: mods.append("YOLOE") | |
| if mods: print(f" Extra engines: {', '.join(mods)}") | |
| t0 = time.time() | |
| res = model_manager.detect_full( | |
| img_rgb, | |
| conf = conf, | |
| iou = iou, | |
| use_sahi = sahi, | |
| sahi_size = sz, | |
| use_seg = self.chk_seg.value and model_manager.seg_model is not None, | |
| use_pose = self.chk_pose.value and model_manager.pose_model is not None, | |
| use_yoloe = self.chk_yoloe.value and getattr(model_manager,"yoloe_model",None) is not None, | |
| end2end = e2e, | |
| ) | |
| elapsed = time.time()-t0 | |
| dets = res["detections"] | |
| ann_manager.add_annotations(fname, dets, | |
| masks = res["masks"] or None, | |
| keypoints= res["keypoints"] or None) | |
| total = len(ann_manager.get_anns(fname)) | |
| print(f"[OK] {len(dets)} objek baru | total: {total}" | |
| f" | {elapsed:.1f}s\n") | |
| # ringkasan kelas | |
| cls_cnt = {} | |
| for d in dets: | |
| k = f"{d['label']:25s} ({d['yolo_class']})" | |
| cls_cnt[k] = cls_cnt.get(k,0)+1 | |
| for k,v in sorted(cls_cnt.items(),key=lambda x:-x[1]): | |
| bar = "#"*min(v,25) | |
| print(f" {v:4d} {bar} {k}") | |
| show_result( | |
| fname, img_rgb, | |
| ann_manager.get_anns(fname), | |
| ann_manager.get_masks(fname) or None, | |
| ann_manager.get_poses(fname) or None, | |
| title=f"YOLO26 Detect | {os.path.basename(fname)}" | |
| f" | conf={conf:.0%}", | |
| show_stats=True, | |
| ) | |
| self._print_table(fname) | |
| def _detect(self, clear_prev): | |
| with self.out: | |
| clear_output() | |
| self._run_detect(self.dd_img.value, clear_prev) | |
| def _detect_all(self, _): | |
| with self.out: | |
| clear_output() | |
| total = 0 | |
| for fname in list(ann_manager.images.keys()): | |
| print(f"\n{'='*50}") | |
| print(f"[>>] {os.path.basename(fname)}") | |
| self._run_detect(fname, clear_prev=False) | |
| total += len(ann_manager.get_anns(fname)) | |
| print(f"\n{'='*50}") | |
| print(f"[OK] Selesai! Total {total} objek di " | |
| f"{len(ann_manager.images)} gambar.") | |
| def _show(self, _): | |
| with self.out: | |
| clear_output() | |
| fname = self.dd_img.value | |
| if fname not in ann_manager.images: return | |
| anns = ann_manager.get_anns(fname) | |
| show_result(fname, ann_manager.images[fname], anns, | |
| ann_manager.get_masks(fname) or None, | |
| ann_manager.get_poses(fname) or None) | |
| self._print_table(fname) | |
| def _add_manual(self, _): | |
| with self.out: | |
| clear_output() | |
| fname = self.dd_img.value | |
| if not fname: return | |
| ann = { | |
| "category": self.dd_cat.value, | |
| "label": self.dd_lbl.value, | |
| "bbox": [self.int_x1.value, self.int_y1.value, | |
| self.int_x2.value, self.int_y2.value], | |
| "confidence": 1.0, | |
| "source": "manual", | |
| } | |
| ann_manager.add_one(fname, ann) | |
| anns = ann_manager.get_anns(fname) | |
| print(f"[+] Manual anotasi ditambahkan (idx {len(anns)-1})\n") | |
| show_result(fname, ann_manager.images[fname], anns) | |
| self._print_table(fname) | |
| def _delete(self, _): | |
| with self.out: | |
| clear_output() | |
| fname = self.dd_img.value; idx = self.int_del.value | |
| ann_manager.remove(fname, idx) | |
| anns = ann_manager.get_anns(fname) | |
| print(f"[x] Index {idx} dihapus. Sisa: {len(anns)}\n") | |
| show_result(fname, ann_manager.images[fname], anns) | |
| self._print_table(fname) | |
| def _export(self, fmt): | |
| with self.out: | |
| clear_output() | |
| if fmt=="json": ann_manager.export_json() | |
| elif fmt=="yolo":ann_manager.export_yolo() | |
| elif fmt=="coco":ann_manager.export_coco() | |
| elif fmt=="mask":ann_manager.export_masks() | |
| def _stats(self): | |
| with self.out: | |
| clear_output() | |
| show_statistics(ann_manager) | |
| def _download_zip(self, _): | |
| with self.out: | |
| clear_output() | |
| zp = ann_manager.export_zip("annotation_export.zip") | |
| print(f"\n[D] Mengunduh {zp} ...") | |
| files.download(zp) | |
| print("[OK] Download selesai!") | |
| def _print_table(self, fname): | |
| anns = ann_manager.get_anns(fname) | |
| if not anns: print(" (belum ada anotasi)"); return | |
| print(f"\n{'Idx':>4} {'Kategori':<18} {'Label':<24}" | |
| f" {'BBox':<22} {'Conf':>5} {'Src'}") | |
| print("─"*90) | |
| for i,a in enumerate(anns): | |
| print(f"{i:4d} {a.get('category',''):<18} " | |
| f"{a.get('label',''):<24} " | |
| f"{str(a.get('bbox','')):<22} " | |
| f"{a.get('confidence',1.0):>5.1%} " | |
| f"{a.get('source','')}") | |
| print(f"\n Total: {len(anns)} objek " | |
| f"| {len(ann_manager.images)} gambar " | |
| f"| {ann_manager.total()} semua") | |
| # ══════════════════════════════════════════════════════════════════ | |
| # CELL 8 MAIN | |
| # ══════════════════════════════════════════════════════════════════ | |
| def main(): | |
| banner = """ | |
| ╔══════════════════════════════════════════════════════════╗ | |
| ║ AI SELF-DRIVING ANNOTATION TOOL v3.0 — YOLO26 ║ | |
| ╠══════════════════════════════════════════════════════════╣ | |
| ║ PANDUAN CEPAT untuk jalan ramai: ║ | |
| ║ 1. Load Model → pilih YOLO26l (atau x untuk max) ║ | |
| ║ 2. Aktifkan Segmentation + YOLOE jika GPU cukup ║ | |
| ║ 3. Upload gambar jalan raya ║ | |
| ║ 4. Set Confidence: 15-22% | IoU: 40-45% ║ | |
| ║ 5. Aktifkan SAHI jika masih ada objek terlewat ║ | |
| ║ 6. Klik [Auto Detect] ║ | |
| ║ 7. Export → ZIP (JSON + YOLO + COCO sekaligus) ║ | |
| ╚══════════════════════════════════════════════════════════╝ | |
| """ | |
| print(banner) | |
| ui = AnnotationUI() | |
| ui.display() | |
| if __name__ == "__main__": | |
| main() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # !pip install ultralytics opencv-python-headless matplotlib ipywidgets Pillow tqdm | |
| # !pip install -q lapx # tracker opsional | |
| !pip install ultralytics opencv-python-headless matplotlib ipywidgets Pillow tqdm | |
| !pip install -q lapx # tracker opsional | |
| !pip install sahi |
Author
Xnuvers007
commented
Mar 17, 2026
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment