{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.12.12","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"gpu","dataSources":[{"sourceType":"competition","sourceId":129750,"databundleVersionId":15563344}],"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"# =========================================================\n# ✅ DAL SHEMAGH — KAGGLE ONE CELL (GROUP-AWARE CORRECT SPLIT + TRAIN) ✅\n# -  Kaggle competition dataset: /kaggle/input/dal-shemagh-detection-challenge\n# =========================================================\n\n!pip -q install ultralytics==8.4.12 scikit-learn\n\nimport os, glob, math, gc, shutil, random, zipfile\nimport numpy as np\nimport pandas as pd\nfrom pathlib import Path\nfrom tqdm import tqdm\n\nfrom ultralytics import YOLO\nfrom sklearn.model_selection import GroupShuffleSplit\nfrom sklearn.metrics import (\n    confusion_matrix, classification_report,\n    precision_recall_fscore_support, fbeta_score\n)\n\n# =========================\n# SETTINGS\n# =========================\nSEED = 42\nrandom.seed(SEED); np.random.seed(SEED)\n\nBASE_OUT = \"/kaggle/working\"\n\n# ✅ Competition dataset root\nCOMP_DIR = \"/kaggle/input/dal-shemagh-detection-challenge\"\n\n# ---- MODEL ----\nMODEL_NAME = \"yolov8m.pt\"\nRUNS = f\"{BASE_OUT}/runs\"\nBEST_PT = f\"{RUNS}/dal_run/weights/best.pt\"\n\n# ---- TRAIN ----\nEPOCHS      = 150\nIMGSZ_TRAIN = 768\nBATCH_TRAIN = 8\nPATIENCE    = 40\n\n# ---- TRUE VAL BUILD ----\nVAL_POST = f\"{BASE_OUT}/yolo_val_post\"\nVAL_FRAC = 0.20\nNEG_TRAIN_SAMPLE  = 450\nMIN_POS_VAL       = 12\nPOS_REPEAT        = 60  # oversample positives in TRAIN only\n\n# ---- INFER ----\nIMGSZ_RULE  = 640\nIMGSZ_DET   = 640\nBATCH_INFER = 32\nVAL_BATCH   = 32\n\n# RULE pass\nCONF_RULE = 0.05\nIOU_NMS   = 0.65\n\n# DET tuning grid\nCAND_CONF_DET = [0.10, 0.15, 0.20, 0.25]\nCAND_IOU_DET  = [0.55, 0.60, 0.65, 0.70]\nMAX_DET = 50\n\n# RULE grid\nGRID = {\n    \"head_min_conf\": [0.08, 0.10],\n    \"shem_min_conf\": [0.05, 0.08],\n    \"dist_k\":        [2.8, 3.2],\n    \"top_k\":         [0.6, 0.9],\n    \"bottom_k\":      [4.0, 5.0],\n    \"align_k\":       [0.8, 0.9],\n    \"min_h_ratio\":   [1.0, 1.1],\n    \"min_w_ratio\":   [1.0, 1.05],\n    \"require_shem_below\": [True],\n}\n\n# Gate thresholds\nGATE_THR_LIST = [0.00, 0.02, 0.03, 0.05, 0.07, 0.10, 0.12, 0.15, 0.18, 0.20, 0.25, 0.30, 0.35]\nMIN_GATE_FOR_TUNE = 0.08\n\n# Heavy FP penalty metric\nBETA_TUNE = 0.25\n\n# =========================\n# Helpers\n# =========================\ndef flush():\n    gc.collect()\n    try:\n        import torch\n        if torch.cuda.is_available():\n            torch.cuda.empty_cache()\n    except:\n        pass\n\ndef safe_rmtree(p):\n    if os.path.exists(p):\n        shutil.rmtree(p, ignore_errors=True)\n\ndef must_exist(path, msg):\n    if not os.path.exists(path):\n        raise FileNotFoundError(f\"{msg}\\nMissing: {path}\")\n\ndef write_yaml(path, root_dir, train_rel, val_rel):\n    txt = f\"\"\"path: {root_dir}\ntrain: {train_rel}\nval: {val_rel}\nnc: 2\nnames: ['Head','Shemagh']\n\"\"\"\n    with open(path, \"w\") as f:\n        f.write(txt)\n\ndef make_group(fn):\n    base = Path(str(fn)).stem\n    if \"__pos\" in base:\n        base = base.split(\"__pos\")[0]\n    for sep in [\"_\", \"-\"]:\n        if sep in base:\n            return base.split(sep)[0]\n    return base[:6]\n\ndef get_boxes(res, cls_id):\n    out = []\n    if res.boxes is None or len(res.boxes) == 0:\n        return out\n    for b in res.boxes:\n        if int(b.cls[0]) != cls_id:\n            continue\n        c = float(b.conf[0])\n        x, y, w, h = b.xywhn[0].tolist()\n        out.append((x, y, w, h, c))\n    return out\n\ndef right_place_rule_v3(head, shem,\n                        head_min_conf=0.10,\n                        shem_min_conf=0.08,\n                        dist_k=3.8,\n                        top_k=0.9,\n                        bottom_k=5.5,\n                        align_k=0.9,\n                        min_h_ratio=1.15,\n                        min_w_ratio=1.05,\n                        require_shem_below=True):\n    if head is None or shem is None:\n        return 0\n    hx, hy, hw, hh, hc = head\n    sx, sy, sw, sh, sc = shem\n\n    if hc < head_min_conf or sc < shem_min_conf:\n        return 0\n    if require_shem_below and (sy < hy):\n        return 0\n\n    d = math.hypot(hx - sx, hy - sy)\n    if d > dist_k * hh + 0.05:\n        return 0\n\n    if abs(hx - sx) > align_k * (hw + sw) / 2:\n        return 0\n\n    head_top = hy - hh/2\n    shem_top = sy - sh/2\n    shem_bottom = sy + sh/2\n\n    if shem_top > head_top + top_k * hh:\n        return 0\n    if shem_bottom > hy + bottom_k * hh:\n        return 0\n\n    if sh < min_h_ratio * hh:\n        return 0\n    if sw < min_w_ratio * hw:\n        return 0\n\n    if sw < 1.05 * hw:\n        return 0\n    if abs(hx - sx) > 0.6 * hw:\n        return 0\n\n    return 1\n\ndef pick_best_pair_by_rule(res, params):\n    heads = get_boxes(res, 0)\n    shems = get_boxes(res, 1)\n    if not heads or not shems:\n        return None, None, 0.0\n\n    best_head, best_shem, best_prod = None, None, 0.0\n    for h in heads:\n        for s in shems:\n            if not right_place_rule_v3(h, s, **params):\n                continue\n            prod = h[4] * s[4]\n            if prod > best_prod:\n                best_head, best_shem, best_prod = h, s, prod\n    return best_head, best_shem, best_prod\n\ndef right_place_with_gate(res, params, gate_thr=0.0):\n    head, shem, conf_prod = pick_best_pair_by_rule(res, params)\n    if head is None or shem is None:\n        return 0\n    return 1 if conf_prod >= gate_thr else 0\n\ndef make_pred_string(res_det):\n    parts = []\n    if res_det.boxes is None or len(res_det.boxes) == 0:\n        return \"-\"\n    for box in res_det.boxes:\n        cls_id = int(box.cls[0])\n        c = float(box.conf[0])\n        x, y, w, h = box.xywhn[0].tolist()\n        parts.extend([str(cls_id), f\"{c:.4f}\", f\"{x:.4f}\", f\"{y:.4f}\", f\"{w:.4f}\", f\"{h:.4f}\"])\n    return \" \".join(parts) if parts else \"-\"\n\n# =========================\n# 0) Dataset paths\n# =========================\nCSV_PATH      = os.path.join(COMP_DIR, \"train_labels.csv\")\nTRAIN_IMG_DIR = os.path.join(COMP_DIR, \"images/train\")\nTEST_IMG_DIR  = os.path.join(COMP_DIR, \"images/test\")\nLBL_DIR       = os.path.join(COMP_DIR, \"labels/train\")\n\nmust_exist(CSV_PATH,      \"❌ Missing train_labels.csv\")\nmust_exist(TRAIN_IMG_DIR, \"❌ Missing images/train\")\nmust_exist(TEST_IMG_DIR,  \"❌ Missing images/test\")\nmust_exist(LBL_DIR,       \"❌ Missing labels/train\")\n\nprint(\"✅ COMP_DIR:\", COMP_DIR)\nprint(\"train imgs:\", len(glob.glob(TRAIN_IMG_DIR+\"/*.jpg\")))\nprint(\"test imgs :\", len(glob.glob(TEST_IMG_DIR +\"/*.jpg\")))\nprint(\"labels    :\", len(glob.glob(LBL_DIR+\"/*.txt\")))\n\n# =========================\n# 1) Build TRUE VAL folder (GROUP-AWARE)\n# =========================\nprint(\"\\n[1] Building TRUE VAL (Group-aware) ...\")\nsafe_rmtree(VAL_POST)\nos.makedirs(f\"{VAL_POST}/images/train\", exist_ok=True)\nos.makedirs(f\"{VAL_POST}/images/val\", exist_ok=True)\nos.makedirs(f\"{VAL_POST}/labels/train\", exist_ok=True)\nos.makedirs(f\"{VAL_POST}/labels/val\", exist_ok=True)\n\ndf = pd.read_csv(CSV_PATH)\ndf[\"right_place\"] = df[\"right_place\"].astype(bool)\ndf[\"group\"] = df[\"filename\"].astype(str).apply(make_group)\n\nprint(\"Total:\", len(df), \"| Pos:\", int(df[\"right_place\"].sum()), \"| Neg:\", int((~df[\"right_place\"]).sum()))\nprint(\"Groups:\", df[\"group\"].nunique())\n\ngss = GroupShuffleSplit(n_splits=60, test_size=VAL_FRAC, random_state=SEED)\nbest_split = None\n\nfor tr_idx, va_idx in gss.split(df, y=df[\"right_place\"].astype(int), groups=df[\"group\"]):\n    tr = df.iloc[tr_idx].copy()\n    va = df.iloc[va_idx].copy()\n    if int(va[\"right_place\"].sum()) >= MIN_POS_VAL and int(tr[\"right_place\"].sum()) >= 5:\n        best_split = (tr, va)\n        break\n\nif best_split is None:\n    tr_idx, va_idx = next(gss.split(df, y=df[\"right_place\"].astype(int), groups=df[\"group\"]))\n    train_df = df.iloc[tr_idx].copy()\n    val_df   = df.iloc[va_idx].copy()\n    print(\"⚠️ Couldn't satisfy MIN_POS_VAL; used fallback group split.\")\nelse:\n    train_df, val_df = best_split\n\noverlap = len(set(train_df[\"group\"]) & set(val_df[\"group\"]))\nprint(\"Train:\", len(train_df), \"| Pos:\", int(train_df[\"right_place\"].sum()))\nprint(\"Val  :\", len(val_df),   \"| Pos:\", int(val_df[\"right_place\"].sum()))\nprint(\"Group overlap (must be 0):\", overlap)\n\npos_train = train_df[train_df[\"right_place\"]==True].copy()\nneg_train = train_df[train_df[\"right_place\"]==False].copy()\npos_val   = val_df[val_df[\"right_place\"]==True].copy()\nneg_val   = val_df[val_df[\"right_place\"]==False].copy()\n\nneg_train = neg_train.sample(n=min(NEG_TRAIN_SAMPLE, len(neg_train)), random_state=SEED).copy()\n\nfor dfx in [neg_train, neg_val, pos_train, pos_val]:\n    dfx[\"dst_filename\"] = dfx[\"filename\"]\n\n# oversample positives ONLY in TRAIN\npos_os_parts = []\nfor rep in range(POS_REPEAT):\n    tmp = pos_train.copy()\n    tmp[\"dst_filename\"] = tmp[\"filename\"].apply(lambda f: f\"{Path(f).stem}__pos{rep}.jpg\")\n    pos_os_parts.append(tmp)\ntrain_pos_os = pd.concat(pos_os_parts, ignore_index=True) if len(pos_train) else pos_train.copy()\n\ntrain_data = pd.concat([neg_train, train_pos_os], ignore_index=True).sample(frac=1, random_state=SEED).reset_index(drop=True)\nval_data   = pd.concat([neg_val, pos_val], ignore_index=True).sample(frac=1, random_state=SEED).reset_index(drop=True)\n\nprint(\"TRUE VAL pos:\", int(val_data[\"right_place\"].sum()), \"/\", len(val_data))\nprint(\"TRAIN pos   :\", int(train_data[\"right_place\"].sum()), \"/\", len(train_data))\n\ndef copy_split(df_part, split):\n    for _, row in tqdm(df_part.iterrows(), total=len(df_part), desc=f\"prep {split}\"):\n        src_fn = row[\"filename\"]\n        dst_fn = row[\"dst_filename\"]\n        src_stem = Path(src_fn).stem\n        dst_stem = Path(dst_fn).stem\n\n        src_img = os.path.join(TRAIN_IMG_DIR, src_fn)\n        dst_img = os.path.join(VAL_POST, \"images\", split, dst_fn)\n        shutil.copy(src_img, dst_img)\n\n        src_lbl = os.path.join(LBL_DIR, f\"{src_stem}.txt\")\n        dst_lbl = os.path.join(VAL_POST, \"labels\", split, f\"{dst_stem}.txt\")\n        if os.path.exists(src_lbl):\n            shutil.copy(src_lbl, dst_lbl)\n        else:\n            open(dst_lbl, \"w\").close()\n\ncopy_split(train_data, \"train\")\ncopy_split(val_data, \"val\")\n\nDATA_YAML = f\"{VAL_POST}/data.yaml\"\nVAL_CSV_A = f\"{VAL_POST}/val_data.csv\"\nwrite_yaml(DATA_YAML, VAL_POST, \"images/train\", \"images/val\")\nval_data.to_csv(VAL_CSV_A, index=False)\n\nprint(\"✅ data.yaml:\", DATA_YAML)\nprint(\"✅ val_data.csv:\", VAL_CSV_A)\n\n# =========================\n# 2) TRAIN (ALWAYS retrain)\n# =========================\nprint(\"\\n[2] Training (always retrain to match split) ...\")\nsafe_rmtree(f\"{RUNS}/dal_run\")\nos.makedirs(RUNS, exist_ok=True)\n\nmodel_train = YOLO(MODEL_NAME)\nmodel_train.train(\n    data=DATA_YAML,\n    epochs=EPOCHS,\n    imgsz=IMGSZ_TRAIN,\n    batch=BATCH_TRAIN,\n    patience=PATIENCE,\n    close_mosaic=10,\n    seed=SEED,\n\n    cache=\"disk\",\n    workers=2,\n    project=RUNS,\n    name=\"dal_run\",\n    exist_ok=True,\n    pretrained=True,\n\n    optimizer=\"AdamW\",\n    lr0=1.2e-4,\n    lrf=2.0e-3,\n    cos_lr=True,\n    weight_decay=3e-3,\n\n    mosaic=0.7,\n    mixup=0.10,\n    copy_paste=0.10,\n    fliplr=0.5,\n    flipud=0.0,\n    hsv_h=0.01,\n    hsv_s=0.4,\n    hsv_v=0.3,\n    translate=0.10,\n    scale=0.50,\n)\n\nmust_exist(BEST_PT, \"❌ training finished but best.pt not found\")\nmodel = YOLO(BEST_PT)\nprint(\"✅ Loaded best:\", BEST_PT)\n\n# =========================\n# 3) Tune DET conf/iou on TRUE VAL\n# =========================\nprint(\"\\n[3] Tuning DET conf/iou on TRUE VAL ...\")\nbest_map = -1\nBEST_CONF_DET, BEST_IOU_DET = 0.15, 0.60\n\nfor cd in CAND_CONF_DET:\n    for iod in CAND_IOU_DET:\n        m = model.val(data=DATA_YAML, conf=cd, iou=iod, imgsz=IMGSZ_DET, verbose=False)\n        mp = float(getattr(m.box, \"map\", -1))\n        print(f\"conf={cd:.2f} iou={iod:.2f} -> mAP50-95={mp:.4f}\")\n        if mp > best_map:\n            best_map = mp\n            BEST_CONF_DET, BEST_IOU_DET = float(cd), float(iod)\n\nprint(\"\\n✅ Best det params:\", BEST_CONF_DET, BEST_IOU_DET, \"| mAP50-95:\", best_map)\n\n# =========================\n# 4) Cache TRUE VAL preds (RULE pass)\n# =========================\nprint(\"\\n[4] Cache TRUE val predictions (RULE pass) ...\")\nval_files = val_data[\"dst_filename\"].tolist()\npred_cache = {}\n\nfor i in tqdm(range(0, len(val_files), VAL_BATCH), desc=\"cache val\"):\n    batch_names = val_files[i:i+VAL_BATCH]\n    batch_paths = [os.path.join(VAL_POST, \"images/val\", fn) for fn in batch_names]\n    results = model.predict(batch_paths, conf=CONF_RULE, iou=IOU_NMS, imgsz=IMGSZ_RULE, device=0, verbose=False)\n    for fnm, res in zip(batch_names, results):\n        pred_cache[fnm] = res\n\ny_true = val_data[\"right_place\"].astype(int).values.tolist()\n\n# =========================\n# 5) Tune RULE + GATE\n# =========================\nprint(f\"\\n[5] Auto-tuning RULE + GATE (beta={BETA_TUNE}) ...\")\nimport itertools\n\nkeys = list(GRID.keys())\nall_vals = list(itertools.product(*[GRID[k] for k in keys]))\nprint(\"rule grid =\", len(all_vals), \"| gate =\", len(GATE_THR_LIST), \"| MIN_GATE_FOR_TUNE =\", MIN_GATE_FOR_TUNE)\n\nbest_score = None\nbest_params = None\nbest_gate = None\nbest_stats = None\n\nfor vals in tqdm(all_vals, desc=\"rule+gate\"):\n    params = dict(zip(keys, vals))\n    for gthr in GATE_THR_LIST:\n        if gthr < MIN_GATE_FOR_TUNE:\n            continue\n\n        y_pred = [right_place_with_gate(pred_cache[fnm], params, gthr) for fnm in val_files]\n        cm = confusion_matrix(y_true, y_pred, labels=[0,1])\n        tn, fp, fn, tp = cm.ravel()\n\n        p, r, f1, _ = precision_recall_fscore_support(y_true, y_pred, average=\"binary\", zero_division=0)\n        fbeta = fbeta_score(y_true, y_pred, beta=BETA_TUNE, zero_division=0)\n        pred_pos = int(sum(y_pred))\n\n        score = (float(fbeta), -6*int(fp), -pred_pos, float(p))\n        if (best_score is None) or (score > best_score):\n            best_score = score\n            best_params = params\n            best_gate = float(gthr)\n            best_stats = (float(p), float(r), float(f1), float(fbeta), int(tn), int(fp), int(fn), int(tp), pred_pos)\n\nbp, br, bf1, bfbeta, tn, fp, fn, tp, bpos = best_stats\nprint(\"\\n=== RULE BEST (TRUE VAL) — CONSERVATIVE ===\")\nprint(\"best_params:\", best_params)\nprint(\"best_gate  :\", best_gate)\nprint(f\"F{BETA_TUNE:.2f}={bfbeta:.4f} | P={bp:.4f} R={br:.4f} F1={bf1:.4f} | TN={tn} FP={fp} FN={fn} TP={tp} | pred_pos={bpos}\")\nprint(f\"Positives (VAL): {sum(y_true)} / {len(y_true)}\")\n\n# =========================\n# METRICS PAGE\n# =========================\nprint(\"\\n\" + \"=\"*60)\nprint(\"📄 METRICS PAGE — DAL SHEMAGH (TRUE VAL)\")\nprint(\"=\"*60)\n\nprint(\"\\n[1] DETECTOR METRICS (YOLO VAL)\")\nprint(f\"Using: conf={BEST_CONF_DET:.2f} | iou={BEST_IOU_DET:.2f} | imgsz={IMGSZ_DET}\")\nm_det = model.val(data=DATA_YAML, conf=BEST_CONF_DET, iou=BEST_IOU_DET, imgsz=IMGSZ_DET, verbose=False)\ndet_map5095 = float(getattr(m_det.box, \"map\", float(\"nan\")))\ndet_map50   = float(getattr(m_det.box, \"map50\", float(\"nan\")))\ndet_mp      = float(getattr(m_det.box, \"mp\", float(\"nan\")))\ndet_mr      = float(getattr(m_det.box, \"mr\", float(\"nan\")))\nprint(f\"mAP50-95 : {det_map5095}\")\nprint(f\"mAP50    : {det_map50}\")\nprint(f\"Mean P   : {det_mp}\")\nprint(f\"Mean R   : {det_mr}\")\n\nprint(\"\\n[2] RULE METRICS (right_place on TRUE VAL)\")\nprint(\"Rule params:\", best_params)\nprint(\"Gate thr   :\", best_gate)\n","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2026-02-24T07:20:50.836578Z","iopub.execute_input":"2026-02-24T07:20:50.837177Z","iopub.status.idle":"2026-02-24T09:22:57.220900Z","shell.execute_reply.started":"2026-02-24T07:20:50.837146Z","shell.execute_reply":"2026-02-24T09:22:57.220131Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"y_pred_best = [right_place_with_gate(pred_cache[fnm], best_params, best_gate) for fnm in val_files]\np, r, f1, _ = precision_recall_fscore_support(y_true, y_pred_best, average=\"binary\", zero_division=0)\nf05 = fbeta_score(y_true, y_pred_best, beta=0.5, zero_division=0)\nfbeta = fbeta_score(y_true, y_pred_best, beta=BETA_TUNE, zero_division=0)\n\ncm = confusion_matrix(y_true, y_pred_best, labels=[0,1])\ntn2, fp2, fn2, tp2 = cm.ravel()\n\nprint(\"\\nConfusion Matrix [[TN FP],[FN TP]]:\")\nprint(cm)\nprint(f\"TN={tn2} FP={fp2} FN={fn2} TP={tp2}\")\n\nprint(\"\\nClassification report:\")\nprint(classification_report(y_true, y_pred_best, digits=4, zero_division=0))\n\nprint(\"\\n\" + \"=\"*60)\nprint(\"✅ END METRICS PAGE\")\nprint(\"=\"*60)\n\n# =========================\n# 6) Build submission (RULE+GATE + DET) — OFFICIAL TEST\n# =========================\nprint(\"\\n[6] Building submission ...\")\nprint(\"✅ TEST_IMG_DIR:\", TEST_IMG_DIR)\nprint(\"test imgs :\", len(glob.glob(TEST_IMG_DIR + \"/*.jpg\")))\n\ntest_images = sorted([f for f in os.listdir(TEST_IMG_DIR) if f.lower().endswith(\".jpg\")])\ntest_paths  = [os.path.join(TEST_IMG_DIR, fn) for fn in test_images]\n\nrows = []\nno_det = 0\nflush()\n\nfor i in tqdm(range(0, len(test_paths), BATCH_INFER), desc=\"TEST -> submission\"):\n    batch_paths = test_paths[i:i+BATCH_INFER]\n    batch_names = test_images[i:i+BATCH_INFER]\n\n    results_rule = model.predict(\n        batch_paths,\n        conf=CONF_RULE,\n        iou=IOU_NMS,\n        imgsz=IMGSZ_RULE,\n        device=0,\n        verbose=False\n    )\n\n    results_det = model.predict(\n        batch_paths,\n        conf=BEST_CONF_DET,\n        iou=BEST_IOU_DET,\n        imgsz=IMGSZ_DET,\n        max_det=MAX_DET,\n        device=0,\n        verbose=False\n    )\n\n    for fnm, res_rule, res_det in zip(batch_names, results_rule, results_det):\n        pred_str = make_pred_string(res_det)\n        if pred_str == \"-\":\n            no_det += 1\n        rp = right_place_with_gate(res_rule, best_params, best_gate)\n        rows.append({\"filename\": fnm, \"right_place\": int(rp), \"prediction_string\": pred_str})\n\n    flush()\n\nsubmission = pd.DataFrame(rows)\nout_path = os.path.join(BASE_OUT, \"submission.csv\")\nsubmission.to_csv(out_path, index=False)\n\nprint(\"\\n===== FINAL SUMMARY =====\")\nprint(\"TRUE VAL pos      :\", sum(y_true), \"/\", len(y_true))\nprint(\"DET conf/iou      :\", BEST_CONF_DET, BEST_IOU_DET, \"| mAP50-95(val):\", best_map)\nprint(\"RULE params       :\", best_params)\nprint(\"GATE thr          :\", best_gate)\nprint(\"right_place=1     :\", int(submission[\"right_place\"].sum()))\nprint(\"prediction_string '-' :\", int((submission[\"prediction_string\"]==\"-\").sum()))\nprint(\"no_det            :\", no_det)\nprint(\"saved             :\", out_path)\n\nprint(\"\\n===== FINAL VAL CONFUSION (copy) =====\")\nprint(f\"TN={tn2} FP={fp2} FN={fn2} TP={tp2}\")\nprint(f\"P={p:.4f} R={r:.4f} F1={f1:.4f} F0.5={f05:.4f} F{BETA_TUNE:.2f}={fbeta:.4f}\")\nprint(\"=====================================\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-24T09:22:57.223021Z","iopub.execute_input":"2026-02-24T09:22:57.223317Z","iopub.status.idle":"2026-02-24T09:23:44.949182Z","shell.execute_reply.started":"2026-02-24T09:22:57.223281Z","shell.execute_reply":"2026-02-24T09:23:44.948519Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Save to CSV\nsubmission.to_csv(\"submission.csv\", index=False)\n\nprint(\"\\n\" + \"=\"*60)\nprint(\"✓ SUBMISSION SAVED: submission.csv\")\nprint(\"=\"*60)\nprint(\"\\nFirst 10 rows:\")\ndisplay(submission.head(10))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-24T09:23:44.950096Z","iopub.execute_input":"2026-02-24T09:23:44.950336Z","iopub.status.idle":"2026-02-24T09:23:44.977968Z","shell.execute_reply.started":"2026-02-24T09:23:44.950311Z","shell.execute_reply":"2026-02-24T09:23:44.977379Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"!pip -q install ultralytics==8.4.12 scikit-learn","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-24T09:23:44.978860Z","iopub.execute_input":"2026-02-24T09:23:44.979517Z","iopub.status.idle":"2026-02-24T09:23:48.425396Z","shell.execute_reply.started":"2026-02-24T09:23:44.979489Z","shell.execute_reply":"2026-02-24T09:23:48.424527Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\n\nimport os, glob, math, gc, shutil, json, random\nimport numpy as np\nimport pandas as pd\nfrom pathlib import Path\nfrom tqdm import tqdm\n\nfrom ultralytics import YOLO\nfrom sklearn.model_selection import train_test_split\nfrom sklearn.metrics import confusion_matrix, precision_recall_fscore_support, fbeta_score\n\n\n# SETTINGS\n# =========================\nSEED = 42\nrandom.seed(SEED); np.random.seed(SEED)\n\nBASE_OUT = \"/kaggle/working\"\nCOMP_DIR = \"/kaggle/input/dal-shemagh-detection-challenge\"\n\n\nPT_PATH  = \"/kaggle/working/runs/dal_run/weights/best.pt\"\nCFG_PATH = \"/kaggle/input/PUT_YOUR_DATASET_HERE/dal_infer_cfg.json\"\n\n# output val folder\nVAL_POST = f\"{BASE_OUT}/yolo_val_post\"\nDATA_YAML = f\"{VAL_POST}/data.yaml\"\nVAL_CSV  = f\"{VAL_POST}/val_data.csv\"\n\n\nNEG_VAL_SIZE     = 0.20\nNEG_TRAIN_SAMPLE = 450\nPOS_VAL_FRAC     = 0.40\nMIN_POS_VAL      = 12\nPOS_REPEAT       = 60  \n\n# RULE pass inference\nCONF_RULE = 0.05\nIOU_NMS   = 0.65\nIMGSZ_RULE = 640\n\n\nBEST_CONF_DET   = 0.15\nBEST_IOU_DET    = 0.55\nIMGSZ_DET_TEST  = 896\nMAX_DET_TEST    = 300\nUSE_TTA_DET     = True\n\nbest_gate = 0.10\nbest_params = {\n    \"head_min_conf\": 0.08,\n    \"shem_min_conf\": 0.05,\n    \"dist_k\": 2.8,\n    \"top_k\": 0.6,\n    \"bottom_k\": 4.0,\n    \"align_k\": 0.8,\n    \"min_h_ratio\": 1.0,\n    \"min_w_ratio\": 1.0,\n    \"require_shem_below\": True,\n}\n\n# =========================\n# Helpers\n# =========================\ndef flush():\n    gc.collect()\n    try:\n        import torch\n        if torch.cuda.is_available():\n            torch.cuda.empty_cache()\n    except:\n        pass\n\ndef safe_rmtree(p):\n    if os.path.exists(p):\n        shutil.rmtree(p, ignore_errors=True)\n\ndef must_exist(path, msg):\n    if not os.path.exists(path):\n        raise FileNotFoundError(f\"{msg}\\nMissing: {path}\")\n\ndef write_yaml(path, root_dir, train_rel, val_rel):\n    txt = f\"\"\"path: {root_dir}\ntrain: {train_rel}\nval: {val_rel}\nnc: 2\nnames: ['Head','Shemagh']\n\"\"\"\n    with open(path, \"w\") as f:\n        f.write(txt)\n\ndef get_boxes(res, cls_id):\n    out = []\n    if res.boxes is None or len(res.boxes) == 0:\n        return out\n    for b in res.boxes:\n        if int(b.cls[0]) != cls_id:\n            continue\n        c = float(b.conf[0])\n        x, y, w, h = b.xywhn[0].tolist()\n        out.append((x, y, w, h, c))\n    return out\n\ndef right_place_rule_v3(head, shem,\n                        head_min_conf=0.10,\n                        shem_min_conf=0.08,\n                        dist_k=3.8,\n                        top_k=0.9,\n                        bottom_k=5.5,\n                        align_k=0.9,\n                        min_h_ratio=1.15,\n                        min_w_ratio=1.05,\n                        require_shem_below=True):\n    if head is None or shem is None:\n        return 0\n    hx, hy, hw, hh, hc = head\n    sx, sy, sw, sh, sc = shem\n\n    if hc < head_min_conf or sc < shem_min_conf:\n        return 0\n    if require_shem_below and (sy < hy):\n        return 0\n\n    d = math.hypot(hx - sx, hy - sy)\n    if d > dist_k * hh + 0.05:\n        return 0\n\n    if abs(hx - sx) > align_k * (hw + sw) / 2:\n        return 0\n\n    head_top = hy - hh/2\n    shem_top = sy - sh/2\n    shem_bottom = sy + sh/2\n\n    if shem_top > head_top + top_k * hh:\n        return 0\n    if shem_bottom > hy + bottom_k * hh:\n        return 0\n\n    if sh < min_h_ratio * hh:\n        return 0\n    if sw < min_w_ratio * hw:\n        return 0\n\n    if sw < 1.05 * hw:\n        return 0\n    if abs(hx - sx) > 0.6 * hw:\n        return 0\n\n    return 1\n\ndef pick_best_pair_by_rule(res, params):\n    heads = get_boxes(res, 0)\n    shems = get_boxes(res, 1)\n    if not heads or not shems:\n        return None, None, 0.0\n\n    best_h, best_s, best_prod = None, None, 0.0\n    for h in heads:\n        for s in shems:\n            if not right_place_rule_v3(h, s, **params):\n                continue\n            prod = h[4] * s[4]\n            if prod > best_prod:\n                best_h, best_s, best_prod = h, s, prod\n    return best_h, best_s, best_prod\n\ndef right_place_with_gate(res, params, gate_thr=0.0):\n    h, s, prod = pick_best_pair_by_rule(res, params)\n    if h is None or s is None:\n        return 0\n    return 1 if prod >= gate_thr else 0\n\ndef make_pred_string(res_det):\n    parts = []\n    if res_det.boxes is None or len(res_det.boxes) == 0:\n        return \"-\"\n    for box in res_det.boxes:\n        cls_id = int(box.cls[0])\n        c = float(box.conf[0])\n        x, y, w, h = box.xywhn[0].tolist()\n        parts.extend([str(cls_id), f\"{c:.4f}\", f\"{x:.4f}\", f\"{y:.4f}\", f\"{w:.4f}\", f\"{h:.4f}\"])\n    return \" \".join(parts) if parts else \"-\"\n\n\nprint(\"PT exists :\", os.path.exists(PT_PATH), PT_PATH)\nprint(\"CFG exists:\", os.path.exists(CFG_PATH), CFG_PATH)\n\nif os.path.exists(CFG_PATH):\n    with open(CFG_PATH, \"r\") as f:\n        cfg = json.load(f)\n\n   \n    BEST_CONF_DET   = float(cfg.get(\"BEST_CONF_DET\", BEST_CONF_DET))\n    BEST_IOU_DET    = float(cfg.get(\"BEST_IOU_DET\",  BEST_IOU_DET))\n    CONF_RULE       = float(cfg.get(\"CONF_RULE\", CONF_RULE))\n    IOU_NMS         = float(cfg.get(\"IOU_NMS\", IOU_NMS))\n    IMGSZ_RULE      = int(cfg.get(\"IMGSZ_RULE\", IMGSZ_RULE))\n    IMGSZ_DET_TEST  = int(cfg.get(\"IMGSZ_DET_TEST\", IMGSZ_DET_TEST))\n    MAX_DET_TEST    = int(cfg.get(\"MAX_DET_TEST\", MAX_DET_TEST))\n    USE_TTA_DET     = bool(cfg.get(\"USE_TTA_DET\", USE_TTA_DET))\n    best_gate       = float(cfg.get(\"best_gate\", best_gate))\n    best_params     = cfg.get(\"best_params\", best_params)\n\nprint(\"\\n✅ DET:\", {\"conf\": BEST_CONF_DET, \"iou\": BEST_IOU_DET, \"imgsz\": IMGSZ_DET_TEST, \"max_det\": MAX_DET_TEST, \"tta\": USE_TTA_DET})\nprint(\"✅ RULE:\", {\"conf\": CONF_RULE, \"iou\": IOU_NMS, \"imgsz\": IMGSZ_RULE, \"gate\": best_gate})\n\n# =========================\n# 1) Ensure dataset root (Kaggle competition)\n# =========================\nCSV_PATH      = os.path.join(COMP_DIR, \"train_labels.csv\")\nTRAIN_IMG_DIR = os.path.join(COMP_DIR, \"images/train\")\nTEST_IMG_DIR  = os.path.join(COMP_DIR, \"images/test\")\nLBL_DIR       = os.path.join(COMP_DIR, \"labels/train\")\n\nmust_exist(CSV_PATH,      \"❌ Missing train_labels.csv\")\nmust_exist(TRAIN_IMG_DIR, \"❌ Missing images/train\")\nmust_exist(TEST_IMG_DIR,  \"❌ Missing images/test\")\nmust_exist(LBL_DIR,       \"❌ Missing labels/train\")\n\nprint(\"\\n✅ COMP_DIR:\", COMP_DIR)\nprint(\"train imgs:\", len(glob.glob(TRAIN_IMG_DIR+\"/*.jpg\")))\nprint(\"test imgs :\", len(glob.glob(TEST_IMG_DIR +\"/*.jpg\")))\nprint(\"labels    :\", len(glob.glob(LBL_DIR+\"/*.txt\")))\n\n# =========================\n# 2) REBUILD TRUE VAL \n# =========================\nprint(\"\\n[1] Rebuilding TRUE VAL folder (fix positives) ...\")\nsafe_rmtree(VAL_POST)\nos.makedirs(f\"{VAL_POST}/images/train\", exist_ok=True)\nos.makedirs(f\"{VAL_POST}/images/val\", exist_ok=True)\nos.makedirs(f\"{VAL_POST}/labels/train\", exist_ok=True)\nos.makedirs(f\"{VAL_POST}/labels/val\", exist_ok=True)\n\ndf = pd.read_csv(CSV_PATH)\n\n# robust normalize right_place\nrp = df[\"right_place\"]\nif rp.dtype == \"bool\":\n    df[\"right_place\"] = rp.astype(bool)\nelse:\n    rp2 = rp.astype(str).str.strip().str.lower().map({\"true\":True,\"false\":False,\"1\":True,\"0\":False})\n    df[\"right_place\"] = rp2.fillna(False).astype(bool)\n\npos_df = df[df[\"right_place\"]==True].copy()\nneg_df = df[df[\"right_place\"]==False].copy()\n\nprint(\"Total:\", len(df), \"| Pos:\", len(pos_df), \"| Neg:\", len(neg_df))\nif len(pos_df) == 0:\n    raise ValueError(\"❌ CSV has 0 positives! check train_labels.csv right_place column\")\n\n# negatives split\nneg_train, neg_val = train_test_split(neg_df, test_size=NEG_VAL_SIZE, random_state=SEED, shuffle=True)\nneg_train = neg_train.sample(n=min(NEG_TRAIN_SAMPLE, len(neg_train)), random_state=SEED).copy()\n\n# positives split\nif len(pos_df) <= MIN_POS_VAL:\n    pos_val = pos_df.copy()\n    pos_train = pos_df.iloc[0:0].copy()\nelse:\n    pos_train, pos_val = train_test_split(pos_df, test_size=POS_VAL_FRAC, random_state=SEED, shuffle=True)\n    if len(pos_val) < MIN_POS_VAL:\n        need = MIN_POS_VAL - len(pos_val)\n        extra = pos_train.sample(n=min(need, len(pos_train)), random_state=SEED)\n        pos_val = pd.concat([pos_val, extra], ignore_index=True)\n        pos_train = pos_train.drop(extra.index).copy()\n\nfor dfx in [neg_train, neg_val, pos_train, pos_val]:\n    dfx[\"dst_filename\"] = dfx[\"filename\"]\n\n# oversample positives ONLY in TRAIN\npos_os_parts = []\nfor rep in range(POS_REPEAT):\n    tmp = pos_train.copy()\n    tmp[\"dst_filename\"] = tmp[\"filename\"].apply(lambda f: f\"{Path(f).stem}__pos{rep}.jpg\")\n    pos_os_parts.append(tmp)\ntrain_pos_os = pd.concat(pos_os_parts, ignore_index=True) if len(pos_train) else pos_train.copy()\n\ntrain_data = pd.concat([neg_train, train_pos_os], ignore_index=True).sample(frac=1, random_state=SEED).reset_index(drop=True)\nval_data   = pd.concat([neg_val, pos_val], ignore_index=True).sample(frac=1, random_state=SEED).reset_index(drop=True)\n\nprint(\"TRUE VAL pos:\", int(val_data[\"right_place\"].sum()), \"/\", len(val_data))\n\ndef copy_split(df_part, split):\n    for _, row in tqdm(df_part.iterrows(), total=len(df_part), desc=f\"prep {split}\"):\n        src_fn = row[\"filename\"]\n        dst_fn = row[\"dst_filename\"]\n        src_stem = Path(src_fn).stem\n        dst_stem = Path(dst_fn).stem\n\n        src_img = os.path.join(TRAIN_IMG_DIR, src_fn)\n        dst_img = os.path.join(VAL_POST, \"images\", split, dst_fn)\n        shutil.copy(src_img, dst_img)\n\n        src_lbl = os.path.join(LBL_DIR, f\"{src_stem}.txt\")\n        dst_lbl = os.path.join(VAL_POST, \"labels\", split, f\"{dst_stem}.txt\")\n        if os.path.exists(src_lbl):\n            shutil.copy(src_lbl, dst_lbl)\n        else:\n            open(dst_lbl, \"w\").close()\n\ncopy_split(train_data, \"train\")\ncopy_split(val_data, \"val\")\n\nwrite_yaml(DATA_YAML, VAL_POST, \"images/train\", \"images/val\")\nval_data.to_csv(VAL_CSV, index=False)\n\nprint(\"✅ Rebuilt:\", VAL_POST)\nprint(\"✅ data.yaml:\", DATA_YAML)\nprint(\"✅ val_data.csv:\", VAL_CSV)\n\n# 3) Load existing best.pt\n# =========================\nmust_exist(PT_PATH, \"❌ best.pt path not found. Fix PT_PATH to your model dataset in /kaggle/input/...\")\nmodel = YOLO(PT_PATH)\nprint(\"\\n✅ Loaded:\", PT_PATH)\n\n# 4) Build submission\n# =========================\nprint(\"\\n[2] Building submission ...\")\ntest_images = sorted([f for f in os.listdir(TEST_IMG_DIR) if f.lower().endswith(\".jpg\")])\ntest_paths  = [os.path.join(TEST_IMG_DIR, fn) for fn in test_images]\n\nrows = []\nno_det = 0\nBATCH_INFER = 32\nflush()\n\nfor i in tqdm(range(0, len(test_paths), BATCH_INFER), desc=\"TEST -> submission\"):\n    batch_paths = test_paths[i:i+BATCH_INFER]\n    batch_names = test_images[i:i+BATCH_INFER]\n\n    # RULE pass\n    results_rule = model.predict(\n        batch_paths, conf=CONF_RULE, iou=IOU_NMS, imgsz=IMGSZ_RULE,\n        device=0, verbose=False, augment=False\n    )\n\n    # DET pass (TTA + bigger imgsz)\n    results_det = model.predict(\n        batch_paths,\n        conf=BEST_CONF_DET,\n        iou=BEST_IOU_DET,\n        imgsz=IMGSZ_DET_TEST,\n        max_det=MAX_DET_TEST,\n        device=0,\n        verbose=False,\n        augment=USE_TTA_DET\n    )\n\n    for fnm, res_rule, res_det in zip(batch_names, results_rule, results_det):\n        pred_str = make_pred_string(res_det)\n        if pred_str == \"-\":\n            no_det += 1\n        rp = right_place_with_gate(res_rule, best_params, best_gate)\n        rows.append({\"filename\": fnm, \"right_place\": int(rp), \"prediction_string\": pred_str})\n\n    flush()\n\nsub = pd.DataFrame(rows)\nout_path = f\"{BASE_OUT}/submission.csv\"\nsub.to_csv(out_path, index=False)\n\nprint(\"\\n✅ saved ->\", out_path)\nprint(\"right_place=1:\", int(sub[\"right_place\"].sum()))\nprint(\"prediction_string '-':\", int((sub[\"prediction_string\"].astype(str).str.strip()==\"-\").sum()))\nprint(\"no_det:\", no_det)\n\n# =========================\n# 5) Optional TRUE VAL check\n# =========================\nprint(\"\\n[Optional] TRUE VAL metrics ...\")\nval_files = val_data[\"dst_filename\"].tolist()\ny_true = val_data[\"right_place\"].astype(int).tolist()\n\npred_cache = {}\nfor i in tqdm(range(0, len(val_files), 32), desc=\"Predict TRUE VAL (RULE)\"):\n    batch_names = val_files[i:i+32]\n    batch_paths = [os.path.join(VAL_POST, \"images/val\", fn) for fn in batch_names]\n    results = model.predict(batch_paths, conf=CONF_RULE, iou=IOU_NMS, imgsz=IMGSZ_RULE, device=0, verbose=False)\n    for fnm, res in zip(batch_names, results):\n        pred_cache[fnm] = res\n\ny_pred = [right_place_with_gate(pred_cache[fnm], best_params, best_gate) for fnm in val_files]\ncm = confusion_matrix(y_true, y_pred, labels=[0,1])\ntn, fp, fn, tp = cm.ravel()\np, r, f1, _ = precision_recall_fscore_support(y_true, y_pred, average=\"binary\", zero_division=0)\nf05 = fbeta_score(y_true, y_pred, beta=0.5, zero_division=0)\n\nprint(\"Positives (VAL):\", sum(y_true), \"/\", len(y_true))\nprint(f\"Precision      : {p:.4f}\")\nprint(f\"Recall         : {r:.4f}\")\nprint(f\"F1             : {f1:.4f}\")\nprint(f\"F0.5           : {f05:.4f}\")\nprint(\"\\nConfusion Matrix [[TN FP],[FN TP]]:\")\nprint(cm)\nprint(f\"TN={tn} FP={fp} FN={fn} TP={tp}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-24T09:23:48.428296Z","iopub.execute_input":"2026-02-24T09:23:48.428623Z","iopub.status.idle":"2026-02-24T09:25:02.965490Z","shell.execute_reply.started":"2026-02-24T09:23:48.428586Z","shell.execute_reply":"2026-02-24T09:25:02.964698Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import pandas as pd, os\n\nVAL_POST = \"/kaggle/working/yolo_val_post\"\nVAL_CSV  = f\"{VAL_POST}/val_data.csv\"\n\nprint(\"VAL_CSV exists:\", os.path.exists(VAL_CSV), VAL_CSV)\n\ndfv = pd.read_csv(VAL_CSV)\nprint(\"rows:\", len(dfv))\nprint(\"columns:\", dfv.columns.tolist())\n\nprint(\"\\nright_place value counts (raw):\")\nprint(dfv[\"right_place\"].value_counts(dropna=False).head(20))\n\n# normalize safely\nrp = dfv[\"right_place\"]\n\nif rp.dtype == \"bool\":\n    rp2 = rp.astype(int)\nelse:\n    rp2 = rp.astype(str).str.strip().str.lower()\n    rp2 = rp2.map({\"true\":1, \"false\":0, \"1\":1, \"0\":0})\n    rp2 = rp2.fillna(0).astype(int)\n\nprint(\"\\nright_place value counts (normalized):\")\nprint(rp2.value_counts(dropna=False))\n\nprint(\"\\npositives:\", int(rp2.sum()), \"/\", len(rp2))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-24T09:25:02.966583Z","iopub.execute_input":"2026-02-24T09:25:02.966831Z","iopub.status.idle":"2026-02-24T09:25:02.985125Z","shell.execute_reply.started":"2026-02-24T09:25:02.966807Z","shell.execute_reply":"2026-02-24T09:25:02.984469Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import numpy as np\nimport matplotlib.pyplot as plt\nfrom sklearn.metrics import confusion_matrix, precision_recall_fscore_support, fbeta_score\n\ncm = confusion_matrix(y_true, y_pred, labels=[0,1])\ntn2, fp2, fn2, tp2 = cm.ravel()\n\np, r, f1, _ = precision_recall_fscore_support(\n    y_true, y_pred, average=\"binary\", zero_division=0\n)\n\nf05 = fbeta_score(y_true, y_pred, beta=0.5, zero_division=0)\n\n# ---- \nprint(f\"Precision      : {p:.4f}\")\nprint(f\"Recall         : {r:.4f}\")\nprint(f\"F1             : {f1:.4f}\")\nprint(f\"F0.5           : {f05:.4f}\")\nprint(\"\\nConfusion Matrix [[TN FP],[FN TP]]:\")\nprint(cm)\nprint(f\"TN={tn2} FP={fp2} FN={fn2} TP={tp2}\")\n\n\n#  PLOT 1: Confusion Matrix\n\ncm_arr = np.array(cm, dtype=int)\n\nplt.figure(figsize=(5, 4))\nplt.imshow(cm_arr, interpolation=\"nearest\", cmap=\"Blues\")\nplt.title(\"Confusion Matrix (TRUE VAL)\")\nplt.colorbar()\n\ntick_labels = [\"0 (neg)\", \"1 (pos)\"]\nplt.xticks([0, 1], tick_labels)\nplt.yticks([0, 1], tick_labels)\nplt.xlabel(\"Predicted\")\nplt.ylabel(\"True\")\n\nfor i in range(2):\n    for j in range(2):\n        plt.text(j, i, str(cm_arr[i, j]), ha=\"center\", va=\"center\")\n\nplt.tight_layout()\nplt.show()\n\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-24T09:25:02.985910Z","iopub.execute_input":"2026-02-24T09:25:02.986180Z","iopub.status.idle":"2026-02-24T09:25:03.119261Z","shell.execute_reply.started":"2026-02-24T09:25:02.986155Z","shell.execute_reply":"2026-02-24T09:25:03.118747Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"!pip -q install ultralytics==8.4.12 opencv-python ipywidgets","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-24T09:25:03.120112Z","iopub.execute_input":"2026-02-24T09:25:03.120306Z","iopub.status.idle":"2026-02-24T09:25:06.551750Z","shell.execute_reply.started":"2026-02-24T09:25:03.120285Z","shell.execute_reply":"2026-02-24T09:25:06.550946Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =========================================================\n# ✅  Viewer (Kaggle) (Next/Prev) + YOLO boxes + right_place from submission.csv\n# =========================================================\n\n!pip -q install ultralytics==8.4.12 opencv-python ipywidgets pandas\n\nimport os, random, cv2\nimport pandas as pd\nimport ipywidgets as widgets\nfrom IPython.display import display, clear_output\nfrom ultralytics import YOLO\n\n\n# 🔧 PATHS \n# =========================\nBEST_PT = \"/kaggle/working/runs/dal_run/weights/best.pt\"          \nSUB_CSV = \"/kaggle/working/submission.csv\"                      \nTEST_DIR = \"/kaggle/input/dal-shemagh-detection-challenge/images/test\"     \n\n# =========================\n# Visualization settings\n# =========================\nCONF_VIS = 0.10\nIOU_VIS  = 0.65\nIMGSZ    = 640\nMAX_DET  = 50\n\nCLASS_NAMES = {0: \"Head\", 1: \"Shemagh\"}\n\n# =========================\n# Checks\n# =========================\nif not os.path.exists(BEST_PT):\n    raise FileNotFoundError(f\"❌ best.pt not found: {BEST_PT}\")\nif not os.path.exists(SUB_CSV):\n    raise FileNotFoundError(f\"❌ submission.csv not found: {SUB_CSV}\")\nif not os.path.exists(TEST_DIR):\n    raise FileNotFoundError(f\"❌ test folder not found: {TEST_DIR}\")\n\ndf = pd.read_csv(SUB_CSV)\nneed = {\"filename\", \"right_place\", \"prediction_string\"}\nif not need.issubset(df.columns):\n    raise ValueError(f\"❌ submission.csv missing columns: {need}\")\n\ndf[\"filename\"] = df[\"filename\"].astype(str)\ndf[\"right_place\"] = df[\"right_place\"].astype(int)\ndf[\"prediction_string\"] = df[\"prediction_string\"].astype(str)\n\n# Load model\nmodel = YOLO(BEST_PT)\nprint(\"✅ Loaded model:\", BEST_PT)\nprint(\"✅ Loaded submission:\", SUB_CSV, \"| rows:\", len(df))\nprint(\"✅ TEST_DIR:\", TEST_DIR)\n\n# Prepare file list (keep submission order)\npaths = [os.path.join(TEST_DIR, fn) for fn in df[\"filename\"].tolist()]\nmissing = [df[\"filename\"].iloc[i] for i, p in enumerate(paths) if not os.path.exists(p)]\nif missing:\n    raise FileNotFoundError(f\"❌ Some test images not found (examples): {missing[:10]}\")\n\n# =========================\n# Draw helper\n# =========================\ndef draw_boxes(img_bgr, result):\n    if result.boxes is None or len(result.boxes) == 0:\n        return img_bgr, {0:0, 1:0}\n\n    counts = {0:0, 1:0}\n    for b in result.boxes:\n        cls_id = int(b.cls[0])\n        conf   = float(b.conf[0])\n        x1, y1, x2, y2 = map(int, b.xyxy[0].tolist())\n\n        if cls_id in counts:\n            counts[cls_id] += 1\n\n        label = f\"{CLASS_NAMES.get(cls_id, str(cls_id))} {conf:.2f}\"\n        color = (0,255,0) if cls_id == 0 else (255,0,0)\n\n        cv2.rectangle(img_bgr, (x1,y1), (x2,y2), color, 2)\n        cv2.putText(img_bgr, label, (x1, max(20, y1-8)),\n                    cv2.FONT_HERSHEY_SIMPLEX, 0.6, color, 2, cv2.LINE_AA)\n\n    return img_bgr, counts\n\n# =========================\n# Viewer UI\n# =========================\nbtn_prev = widgets.Button(description=\"⬅ Prev\", layout=widgets.Layout(width=\"100px\"))\nbtn_next = widgets.Button(description=\"Next ➡\", layout=widgets.Layout(width=\"100px\"))\nbtn_rand = widgets.Button(description=\"🎲 Random\", layout=widgets.Layout(width=\"120px\"))\nslider   = widgets.IntSlider(value=0, min=0, max=len(df)-1, step=1,\n                             description=\"Index\", continuous_update=False)\nout = widgets.Output()\n\ndef show(i):\n    i = int(i)\n    row = df.iloc[i]\n    fn = row[\"filename\"]\n    rp = int(row[\"right_place\"])\n    pred_str = str(row[\"prediction_string\"]).strip()\n\n    img_path = os.path.join(TEST_DIR, fn)\n\n    # predict for visualization\n    res = model.predict(\n        img_path, conf=CONF_VIS, iou=IOU_VIS, imgsz=IMGSZ, max_det=MAX_DET,\n        device=0, verbose=False\n    )[0]\n\n    img = cv2.imread(img_path)\n    img2, counts = draw_boxes(img.copy(), res)\n    img_rgb = cv2.cvtColor(img2, cv2.COLOR_BGR2RGB)\n\n    with out:\n        clear_output(wait=True)\n        print(f\"[{i+1}/{len(df)}]  {fn}\")\n        print(f\"submission right_place: {bool(rp)}  (value={rp})\")\n        print(f\"prediction_string: {'-' if pred_str=='-' else (pred_str[:120]+'...')}\")\n        print(f\"Detected (VIS): Head={counts.get(0,0)} | Shemagh={counts.get(1,0)} | conf={CONF_VIS} iou={IOU_VIS}\")\n\n        ok, buf = cv2.imencode('.jpg', cv2.cvtColor(img_rgb, cv2.COLOR_RGB2BGR))\n        display(widgets.Image(value=buf.tobytes()))\n\ndef on_prev(_):\n    slider.value = max(0, slider.value - 1)\n\ndef on_next(_):\n    slider.value = min(len(df)-1, slider.value + 1)\n\ndef on_rand(_):\n    slider.value = random.randint(0, len(df)-1)\n\nbtn_prev.on_click(on_prev)\nbtn_next.on_click(on_next)\nbtn_rand.on_click(on_rand)\nslider.observe(lambda ch: show(ch[\"new\"]), names=\"value\")\n\ndisplay(widgets.HBox([btn_prev, btn_next, btn_rand]), slider, out)\nshow(0)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-24T09:25:06.553097Z","iopub.execute_input":"2026-02-24T09:25:06.553392Z","iopub.status.idle":"2026-02-24T09:25:10.909225Z","shell.execute_reply.started":"2026-02-24T09:25:06.553362Z","shell.execute_reply":"2026-02-24T09:25:10.908567Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\n#  SUBMISSION Grid Viewer Kaggle\n\n\n\n!pip -q install opencv-python ipywidgets pandas\n\nimport os, math, random\nimport pandas as pd\nimport cv2\nimport ipywidgets as widgets\nfrom IPython.display import display, clear_output\n\n\nTEST_IMG_DIR = \"/kaggle/input/dal-shemagh-detection-challenge/images/test\"  \nSUB_CSV      = \"/kaggle/working/submission.csv\"                 \n# =========================\n# SETTINGS\n# =========================\nCLASS_NAMES = {0: \"Head\", 1: \"Shemagh\"}\nPAGE_SIZE = 8            \nGRID_COLS = 4            \n\n# =========================\n# Checks\n# =========================\nif not os.path.exists(TEST_IMG_DIR):\n    raise FileNotFoundError(f\"❌ Missing TEST_IMG_DIR: {TEST_IMG_DIR}\")\nif not os.path.exists(SUB_CSV):\n    raise FileNotFoundError(f\"❌ Missing submission.csv: {SUB_CSV}\")\n\nsub = pd.read_csv(SUB_CSV)\nneed_cols = {\"filename\", \"right_place\", \"prediction_string\"}\nmissing = need_cols - set(sub.columns)\nif missing:\n    raise ValueError(f\"❌ submission.csv missing columns: {missing}\")\n\nsub[\"filename\"] = sub[\"filename\"].astype(str)\nsub[\"right_place\"] = sub[\"right_place\"].astype(int)\nsub[\"prediction_string\"] = sub[\"prediction_string\"].astype(str)\n\n# =========================\n# Helpers\n# =========================\ndef parse_prediction_string(pred_str):\n    pred_str = (pred_str or \"\").strip()\n    if pred_str == \"-\" or pred_str == \"\":\n        return []\n\n    parts = pred_str.split()\n    if len(parts) % 6 != 0:\n        return []\n\n    boxes = []\n    for i in range(0, len(parts), 6):\n        try:\n            cls_id = int(float(parts[i]))\n            conf   = float(parts[i+1])\n            x      = float(parts[i+2])\n            y      = float(parts[i+3])\n            w      = float(parts[i+4])\n            h      = float(parts[i+5])\n            boxes.append({\"cls\": cls_id, \"conf\": conf, \"x\": x, \"y\": y, \"w\": w, \"h\": h})\n        except:\n            continue\n    return boxes\n\ndef xywhn_to_xyxy_px(x, y, w, h, W, H):\n    x1 = int((x - w/2) * W)\n    y1 = int((y - h/2) * H)\n    x2 = int((x + w/2) * W)\n    y2 = int((y + h/2) * H)\n    x1 = max(0, min(W-1, x1))\n    y1 = max(0, min(H-1, y1))\n    x2 = max(0, min(W-1, x2))\n    y2 = max(0, min(H-1, y2))\n    return x1, y1, x2, y2\n\ndef draw_boxes_from_pred(img_bgr, boxes):\n    H, W = img_bgr.shape[:2]\n    counts = {0: 0, 1: 0}\n\n    for b in boxes:\n        cls_id = int(b[\"cls\"])\n        conf   = float(b[\"conf\"])\n        x, y, w, h = b[\"x\"], b[\"y\"], b[\"w\"], b[\"h\"]\n\n        if cls_id in counts:\n            counts[cls_id] += 1\n\n        x1, y1, x2, y2 = xywhn_to_xyxy_px(x, y, w, h, W, H)\n\n        color = (0, 255, 0) if cls_id == 0 else (255, 0, 0)\n        label = f\"{CLASS_NAMES.get(cls_id, str(cls_id))} {conf:.2f}\"\n\n        cv2.rectangle(img_bgr, (x1, y1), (x2, y2), color, 2)\n        cv2.putText(img_bgr, label, (x1, max(20, y1 - 8)),\n                    cv2.FONT_HERSHEY_SIMPLEX, 0.6, color, 2, cv2.LINE_AA)\n\n    return img_bgr, counts\n\ndef encode_jpg(img_bgr):\n    ok, buf = cv2.imencode(\".jpg\", img_bgr)\n    return buf.tobytes() if ok else None\n\n# =========================\n# UI state\n# =========================\nmode = widgets.ToggleButtons(\n    options=[(\"All\", \"all\"), (\"right_place=1\", \"pos\"), (\"right_place=0\", \"neg\"), (\"No det (-)\", \"nodet\")],\n    value=\"all\",\n    description=\"Filter:\"\n)\n\nbtn_prev = widgets.Button(description=\"⬅ Prev 8\", layout=widgets.Layout(width=\"110px\"))\nbtn_next = widgets.Button(description=\"Next 8 ➡\", layout=widgets.Layout(width=\"110px\"))\nbtn_rand = widgets.Button(description=\"🎲 Random page\", layout=widgets.Layout(width=\"140px\"))\nout = widgets.Output()\n\ndef get_filtered_df():\n    if mode.value == \"pos\":\n        return sub[sub[\"right_place\"] == 1].reset_index(drop=True)\n    if mode.value == \"neg\":\n        return sub[sub[\"right_place\"] == 0].reset_index(drop=True)\n    if mode.value == \"nodet\":\n        return sub[sub[\"prediction_string\"].str.strip() == \"-\"].reset_index(drop=True)\n    return sub.reset_index(drop=True)\n\ndf_view = get_filtered_df()\nnum_pages = max(1, math.ceil(len(df_view) / PAGE_SIZE))\n\npage_slider = widgets.IntSlider(\n    value=0, min=0, max=max(0, num_pages-1), step=1,\n    description=\"Page\", continuous_update=False\n)\n\n# =========================\n# Render 8 images grid\n# =========================\ndef show_page(page_idx):\n    global df_view, num_pages\n\n    df_view = get_filtered_df()\n    n = len(df_view)\n    num_pages = max(1, math.ceil(n / PAGE_SIZE))\n\n    page_slider.max = max(0, num_pages-1)\n    page_idx = int(min(max(page_idx, 0), page_slider.max))\n\n    with out:\n        clear_output(wait=True)\n\n        if n == 0:\n            print(\"⚠️ No rows match this filter.\")\n            return\n\n        start = page_idx * PAGE_SIZE\n        end = min(start + PAGE_SIZE, n)\n        chunk = df_view.iloc[start:end]\n\n        print(f\"Page {page_idx+1}/{num_pages} | showing rows {start+1}..{end} of {n} | Filter={mode.value}\")\n\n        cells = []\n        for _, row in chunk.iterrows():\n            fn = row[\"filename\"]\n            rp = int(row[\"right_place\"])\n            pred_str = row[\"prediction_string\"]\n\n            img_path = os.path.join(TEST_IMG_DIR, fn)\n            if not os.path.exists(img_path):\n                title = widgets.HTML(f\"<b>{fn}</b><br>❌ missing image\")\n                cells.append(widgets.VBox([title]))\n                continue\n\n            img = cv2.imread(img_path)\n            boxes = parse_prediction_string(pred_str)\n            img2, counts = draw_boxes_from_pred(img.copy(), boxes)\n\n            header = f\"{fn} | rp={rp} | H={counts.get(0,0)} S={counts.get(1,0)}\"\n            cv2.rectangle(img2, (0,0), (img2.shape[1], 28), (0,0,0), -1)\n            cv2.putText(img2, header, (8,20), cv2.FONT_HERSHEY_SIMPLEX, 0.55, (255,255,255), 1, cv2.LINE_AA)\n\n            jpg = encode_jpg(img2)\n            if jpg is None:\n                title = widgets.HTML(f\"<b>{fn}</b><br>❌ encode fail\")\n                cells.append(widgets.VBox([title]))\n                continue\n\n            imw = widgets.Image(value=jpg)\n            cells.append(widgets.VBox([imw]))\n\n        while len(cells) < PAGE_SIZE:\n            cells.append(widgets.VBox([widgets.HTML(\"&nbsp;\")]))\n\n        grid = widgets.GridBox(\n            children=cells,\n            layout=widgets.Layout(\n                grid_template_columns=\"repeat(%d, 1fr)\" % GRID_COLS,\n                grid_gap=\"6px\"\n            )\n        )\n        display(grid)\n\ndef refresh(_=None):\n    global df_view, num_pages\n    df_view = get_filtered_df()\n    num_pages = max(1, math.ceil(len(df_view) / PAGE_SIZE))\n    page_slider.max = max(0, num_pages-1)\n    page_slider.value = 0\n    show_page(0)\n\ndef on_prev(_):\n    page_slider.value = max(0, page_slider.value - 1)\n\ndef on_next(_):\n    page_slider.value = min(page_slider.max, page_slider.value + 1)\n\ndef on_rand(_):\n    if page_slider.max >= 0:\n        page_slider.value = random.randint(0, page_slider.max)\n\nbtn_prev.on_click(on_prev)\nbtn_next.on_click(on_next)\nbtn_rand.on_click(on_rand)\npage_slider.observe(lambda ch: show_page(ch[\"new\"]), names=\"value\")\nmode.observe(refresh, names=\"value\")\n\ndisplay(widgets.HBox([mode, btn_prev, btn_next, btn_rand]), page_slider, out)\nshow_page(0)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-24T09:25:10.910368Z","iopub.execute_input":"2026-02-24T09:25:10.910655Z","iopub.status.idle":"2026-02-24T09:25:14.308661Z","shell.execute_reply.started":"2026-02-24T09:25:10.910625Z","shell.execute_reply":"2026-02-24T09:25:14.307972Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\n#  YOLO Overfitting Calculator \n\nimport os, glob\nimport pandas as pd\nimport numpy as np\nimport matplotlib.pyplot as plt\n\n# ✅ Kaggle bases\nBASES = [\"/kaggle/working\", \"/kaggle/input\"]\n\n# 1) Collect candidates (robust)\ncands = []\nfor base in BASES:\n    cands += glob.glob(f\"{base}/**/results.csv\", recursive=True)\n\n# remove duplicates + sort newest first\ncands = sorted(list(set(cands)), key=lambda p: os.path.getmtime(p), reverse=True)\n\nif not cands:\n    raise FileNotFoundError(\n        \"❌ ما لقيت أي results.csv داخل /kaggle/working أو /kaggle/input.\\n\"\n        \"ملاحظة: results.csv يظهر فقط بعد TRAIN (وليس val).\"\n    )\n\ndef is_valid_results_csv(path):\n    try:\n        d = pd.read_csv(path)\n    except:\n        return False\n\n    cols = set(d.columns)\n\n    need_any_map = any(x in cols for x in [\"metrics/mAP50-95\", \"metrics/mAP50-95(B)\"])\n    need_train = all(x in cols for x in [\"train/box_loss\", \"train/cls_loss\", \"train/dfl_loss\"])\n    need_val   = all(x in cols for x in [\"val/box_loss\", \"val/cls_loss\", \"val/dfl_loss\"])\n    return need_any_map and need_train and need_val and len(d) >= 2\n\n# 2) Pick latest valid results.csv\nresults_csv = None\nfor p in cands:\n    if is_valid_results_csv(p):\n        results_csv = p\n        break\n\n# fallback common paths (optional)\nfallbacks = [\n    \"/kaggle/working/runs/detect/train/results.csv\",\n    \"/kaggle/working/runs/dal_run/results.csv\",\n    \"/kaggle/working/runs/detect/train1/results.csv\",\n]\nif results_csv is None:\n    for fb in fallbacks:\n        if os.path.exists(fb) and is_valid_results_csv(fb):\n            results_csv = fb\n            break\n\nif results_csv is None:\n    print(\"⚠️ لقيت results.csv لكن ما فيها أعمدة التدريب المتوقعة.\")\n    print(\"أقرب ملفات موجودة (أحدث 15):\")\n    for x in cands[:15]:\n        print(\" -\", x)\n    raise FileNotFoundError(\"❌ ما لقيت results.csv صالح (لازم يكون من TRAIN وليس val).\")\n\nprint(\"✅ Using results.csv:\", results_csv)\n\ndf = pd.read_csv(results_csv)\n\n# 3) Column helpers\ndef col_like(patterns):\n    # exact\n    for p in patterns:\n        if p in df.columns:\n            return p\n    # contains fallback\n    for p in patterns:\n        hits = [c for c in df.columns if p in c]\n        if hits:\n            return hits[0]\n    return None\n\ncol_train_box = col_like([\"train/box_loss\"])\ncol_train_cls = col_like([\"train/cls_loss\"])\ncol_train_dfl = col_like([\"train/dfl_loss\"])\n\ncol_val_box = col_like([\"val/box_loss\"])\ncol_val_cls = col_like([\"val/cls_loss\"])\ncol_val_dfl = col_like([\"val/dfl_loss\"])\n\ncol_map5095 = col_like([\"metrics/mAP50-95\", \"metrics/mAP50-95(B)\"])\ncol_map50   = col_like([\"metrics/mAP50\", \"metrics/mAP50(B)\"])\ncol_prec    = col_like([\"metrics/precision\", \"metrics/precision(B)\"])\ncol_rec     = col_like([\"metrics/recall\", \"metrics/recall(B)\"])\n\nneeded = [col_train_box, col_train_cls, col_train_dfl, col_val_box, col_val_cls, col_val_dfl, col_map5095]\nif any(x is None for x in needed):\n    raise ValueError(f\"❌ أعمدة ناقصة في results.csv. Columns found:\\n{df.columns.tolist()}\")\n\n# 4) Compute total losses\ndf[\"train_total_loss\"] = df[col_train_box] + df[col_train_cls] + df[col_train_dfl]\ndf[\"val_total_loss\"]   = df[col_val_box]   + df[col_val_cls]   + df[col_val_dfl]\n\n# 5) Best epoch by val mAP50-95\nbest_idx = int(np.nanargmax(df[col_map5095].values))\nbest_row = df.iloc[best_idx]\nbest_epoch = best_idx + 1\n\ntrain_loss_best = float(best_row[\"train_total_loss\"])\nval_loss_best   = float(best_row[\"val_total_loss\"])\ngap_best = val_loss_best - train_loss_best\noverfit_index_best = gap_best / (train_loss_best + 1e-9)\n\n# 6) Last epoch stats\nlast_row = df.iloc[-1]\ntrain_loss_last = float(last_row[\"train_total_loss\"])\nval_loss_last   = float(last_row[\"val_total_loss\"])\ngap_last = val_loss_last - train_loss_last\noverfit_index_last = gap_last / (train_loss_last + 1e-9)\n\n# 7) Trend check (last K epochs)\nK = min(10, len(df))\ntrain_trend = df[\"train_total_loss\"].iloc[-K:].values\nval_trend   = df[\"val_total_loss\"].iloc[-K:].values\ntrain_delta = float(train_trend[-1] - train_trend[0])  # negative => train improved\nval_delta   = float(val_trend[-1] - val_trend[0])      # positive => val got worse\noverfit_trend_flag = (train_delta < 0) and (val_delta > 0)\n\nprint(\"⚠️ Overfit warning\" if overfit_trend_flag else \"✅ No clear overfit trend (by loss trend).\")\nprint(\"=\"*60)\n\n# 9) Plots\nplt.figure()\nplt.plot(df[\"train_total_loss\"].values, label=\"train_total_loss\")\nplt.plot(df[\"val_total_loss\"].values, label=\"val_total_loss\")\nplt.title(\"Train vs Val Total Loss\")\nplt.xlabel(\"Epoch-1\")\nplt.ylabel(\"Loss\")\nplt.legend()\nplt.show()\n\nplt.figure()\nplt.plot(df[col_map5095].values, label=\"val mAP50-95\")\nplt.title(\"Val mAP50-95 over epochs\")\nplt.xlabel(\"Epoch-1\")\nplt.ylabel(\"mAP50-95\")\nplt.legend()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-24T09:25:14.309835Z","iopub.execute_input":"2026-02-24T09:25:14.310102Z","iopub.status.idle":"2026-02-24T09:25:15.349999Z","shell.execute_reply.started":"2026-02-24T09:25:14.310072Z","shell.execute_reply":"2026-02-24T09:25:15.349386Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\n# ✅ Competition Final Score \n# Final Score = 0.5*mAP50-95 + 0.5*F1\n# =========================\n\ndef _to_float(x, name):\n    try:\n        return float(x)\n    except Exception as e:\n        raise ValueError(f\"❌ ما قدرت أحول {name} إلى float. قيمة {name} الحالية: {x}\") from e\n\nmissing = []\nif \"det_map5095\" not in globals():\n    missing.append(\"det_map5095\")\nif \"f1\" not in globals():\n    missing.append(\"f1\")\n\nif missing:\n    raise NameError(\n        \"❌ المتغيرات التالية غير موجودة في النوتبوك:\\n\"\n        f\"{missing}\\n\\n\"\n        \"لازم قبلها تحسب:\\n\"\n        \"- det_map5095 (من model.val ...)\\n\"\n        \"- f1 (من rule metrics على TRUE VAL)\\n\"\n    )\n\ndet_map5095_f = _to_float(det_map5095, \"det_map5095\")\nf1_f         = _to_float(f1, \"f1\")\n\nfinal_score = 0.5 * det_map5095_f + 0.5 * f1_f\n\nprint(\"\\n\" + \"=\"*45)\nprint(\"🏁 Competition Score (on TRUE VAL)\")\nprint(\"=\"*45)\nprint(f\"mAP@[0.5:0.95] : {det_map5095_f:.6f}\")\nprint(f\"F1-Score       : {f1_f:.6f}\")\nprint(f\"Final Score    : {final_score:.6f}\")\nprint(\"=\"*45)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-24T09:25:15.350886Z","iopub.execute_input":"2026-02-24T09:25:15.351149Z","iopub.status.idle":"2026-02-24T09:25:15.357945Z","shell.execute_reply.started":"2026-02-24T09:25:15.351124Z","shell.execute_reply":"2026-02-24T09:25:15.357201Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}