{"metadata":{"kernelspec":{"display_name":"Python 3","language":"python","name":"python3"},"language_info":{"codemirror_mode":{"name":"ipython","version":3},"file_extension":".py","mimetype":"text/x-python","name":"python","nbconvert_exporter":"python","pygments_lexer":"ipython3","version":"3.12.12"},"kaggle":{"accelerator":"nvidiaTeslaT4","dataSources":[{"sourceId":129750,"databundleVersionId":15563344,"sourceType":"competition"},{"sourceId":746642,"sourceType":"modelInstanceVersion","isSourceIdPinned":true,"modelInstanceId":570086,"modelId":582366},{"sourceId":749526,"sourceType":"modelInstanceVersion","isSourceIdPinned":true,"modelInstanceId":572455,"modelId":584803},{"sourceId":750189,"sourceType":"modelInstanceVersion","isSourceIdPinned":true,"modelInstanceId":572985,"modelId":585334},{"sourceId":753425,"sourceType":"modelInstanceVersion","isSourceIdPinned":true,"modelInstanceId":575290,"modelId":587597}],"dockerImageVersionId":31260,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"!pip install -q ultralytics ensemble_boxes\nimport os\nimport pandas as pd\nimport numpy as np\nfrom ultralytics import YOLO\nimport yaml\nfrom tqdm.notebook import tqdm\nimport cv2\nimport shutil\nimport yaml\nimport glob\nfrom pathlib import Path\nfrom sklearn.model_selection import train_test_split, StratifiedKFold , KFold\nfrom sklearn.metrics import f1_score, accuracy_score\nimport joblib\nfrom ensemble_boxes import weighted_boxes_fusion\nimport torch\nimport torch.nn as nn\nimport torch.optim as optim\nfrom torch.utils.data import Dataset, DataLoader\nfrom torchvision import transforms, ops\nimport matplotlib.pyplot as plt\nimport random\nimport json\nimport base64\nfrom io import BytesIO\nfrom IPython.display import HTML, display\nimport timm\nfrom PIL import Image","metadata":{"execution":{"iopub.status.busy":"2026-02-17T22:22:11.077773Z","iopub.execute_input":"2026-02-17T22:22:11.078424Z","iopub.status.idle":"2026-02-17T22:22:14.593507Z","shell.execute_reply.started":"2026-02-17T22:22:11.078378Z","shell.execute_reply":"2026-02-17T22:22:14.592499Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Training YOLO using k fold 5 ","metadata":{}},{"cell_type":"code","source":"# Global Seed Setup \ndef set_all_seeds(seed=42):\n    random.seed(seed)\n    np.random.seed(seed)\n    torch.manual_seed(seed)\n    torch.cuda.manual_seed_all(seed)\n    # Ensures deterministic algorithms are used where possible\n    torch.backends.cudnn.deterministic = True\n    torch.backends.cudnn.benchmark = False\n\nset_all_seeds(42)\n\n# Setup Absolute Paths \nWORK_DIR = Path('/kaggle/input/dal-shemagh-detection-challenge').absolute()\nimages_path = WORK_DIR / 'images/train' \nall_images = sorted([f.name for f in images_path.glob('*') if f.suffix.lower() in ['.jpg', '.jpeg', '.png']])\nk = 5\nresults = {}\n\n# random_state=42 ensures the folds are always the same\nkf = KFold(n_splits=k, shuffle=True, random_state=42)\n\n# K-Fold Training Loop \nfor fold, (train_idx, val_idx) in enumerate(kf.split(all_images)):\n    # after some tests using the same random seed, 3ed fold usally gives the hieghst validation score. \n    if fold != 2: \n        continue\n    \n    train_list = [str(images_path / all_images[i]) for i in train_idx]\n    val_list = [str(images_path / all_images[i]) for i in val_idx]\n    \n    with open(f\"train_f{fold}.txt\", \"w\") as f: f.write(\"\\n\".join(train_list))\n    with open(f\"val_f{fold}.txt\", \"w\") as f: f.write(\"\\n\".join(val_list))\n\n    yaml_path = os.path.abspath(f\"fold_{fold}.yaml\")\n    with open(yaml_path, \"w\") as f:\n        yaml.dump({\n            'path': str(WORK_DIR),\n            'train': os.path.abspath(f\"train_f{fold}.txt\"),\n            'val': os.path.abspath(f\"val_f{fold}.txt\"),\n            'nc': 2, \n            'names': ['Face', 'Shemagh']\n        }, f)\n\n    # Initialize and Train\n    torch.cuda.empty_cache()\n    model = YOLO('yolo11m.pt') \n    \n    model.train(\n        data=yaml_path,\n        epochs=200,\n        imgsz=640,          \n        batch=16,              \n        device=1,\n        seed=42,\n        workers=4,              \n        deterministic=True,    \n\n        hsv_h=0.015,   \n        hsv_s=0.8,     \n        hsv_v=0.6,     \n        scale=0.8,\n\n        mosaic=1.0,    \n        mixup=0.1,     \n        copy_paste=0.3,\n        close_mosaic=50, \n        erasing=0.3,\n        \n        optimizer='SGD',\n        lr0=0.001,\n        project='Shemagh_KFold_BuiltIn3',\n        name=f'fold_{fold}'\n\n    )\n    \n\n    \n    stats = model.val()\n    if stats:\n        results[fold] = stats.results_dict\n    with open(\"kfold_results_backup.json\", \"w\") as f:\n        json.dump(results, f)","metadata":{"execution":{"iopub.status.busy":"2026-02-17T22:22:16.883889Z","iopub.execute_input":"2026-02-17T22:22:16.884574Z","iopub.status.idle":"2026-02-17T23:54:10.463385Z","shell.execute_reply.started":"2026-02-17T22:22:16.884532Z","shell.execute_reply":"2026-02-17T23:54:10.462564Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Use YOLO and heuristics To generate pseudo labels","metadata":{}},{"cell_type":"code","source":"# CONFIGURATION\nMODEL_PATH = '/kaggle/working/runs/detect/Shemagh_KFold_BuiltIn3/fold_2/weights/best.pt'\nTEST_IMG_DIR = '/kaggle/input/dal-shemagh-detection-challenge/images/test'\n\n# Inference Hyperparameters\nTTA_SCALES = [640, 800, 512]\nCONF_THRES = 0.40   # Lowered to capture all possible detections\nIOU_WBF = 0.50      # Aggressive merging for multi-scale consensus\nW_SYM = 0.8\nW_DX = 0.2\nFINAL_THRESH = 0.45 \n\n# Pseudo Labeling Thresholds\nSURE_CORRECT_THRESH = 0.85   \nSURE_INCORRECT_THRESH = 0.15 \n\n# HYBRID UTILITY FUNCTIONS\ndef xyxy_to_xywh(box):\n    x1, y1, x2, y2 = box\n    return [(x1 + x2) / 2, (y1 + y2) / 2, x2 - x1, y2 - y1]\n\ndef clean_duplicate_shemaghs(shemaghs, overlap_threshold=0.50):\n    if not shemaghs: return []\n    cleaned = []\n    sorted_s = sorted(shemaghs, key=lambda x: x[2] * x[3], reverse=True)\n    for s_big in sorted_s:\n        keep = True\n        bx, by, bw, bh = s_big\n        for s_checked in cleaned:\n            cx, cy, cw, ch = s_checked\n            ix1, iy1 = max(bx-bw/2, cx-cw/2), max(by-bh/2, cy-ch/2)\n            ix2, iy2 = min(bx+bw/2, cx+cw/2), min(by+bh/2, cy+ch/2)\n            inter = max(0, ix2-ix1) * max(0, iy2-iy1)\n            if inter / (bw * bh) > overlap_threshold:\n                keep = False\n                break\n        if keep: cleaned.append(s_big)\n    return cleaned\n\ndef calculate_hybrid_features(head, shemagh):\n    hx, hy, hw, hh = head\n    sx, sy, sw, sh = shemagh\n    ix1, iy1 = max(hx-hw/2, sx-sw/2), max(hy-hh/2, sy-sh/2)\n    ix2, iy2 = min(hx+hw/2, sx+sw/2), min(hy+hh/2, sy+sh/2)\n    inter = max(0, ix2-ix1) * max(0, iy2-iy1)\n    ioh = inter / (hw * hh) if (hw * hh) > 0 else 0.0\n    half_area = (hw * hh) / 2\n    l_inter = max(0, min(hx, sx+sw/2) - max(hx-hw/2, sx-sw/2)) * max(0, min(hy+hh/2, sy+sh/2) - max(hy-hh/2, sy-sh/2))\n    r_inter = max(0, min(hx+hw/2, sx+sw/2) - max(hx, sx-sw/2)) * max(0, min(hy+hh/2, sy+sh/2) - max(hy-hh/2, sy-sh/2))\n    symmetry = abs((l_inter / half_area) - (r_inter / half_area)) if half_area > 0 else 1.0\n    h_ymin, s_ymin = hy - hh/2, sy - sh/2\n    return {\n        'ioh': ioh, 'symmetry': symmetry, 'dx_norm': abs(sx - hx) / hw,\n        'starts_at_top': s_ymin < (hy - hh/4), \n        'is_above': s_ymin < h_ymin,\n        'head_flat': hh < (hw * 0.9)\n    }\n\n\n# INFERENCE LOOP\ntest_files = sorted([f for f in os.listdir(TEST_IMG_DIR) if f.lower().endswith(('.jpg', '.jpeg', '.png'))])\nmodel = YOLO(MODEL_PATH)\nall_results = []\n\nprint(f\"Running inference on {len(test_files)} images...\")\n\nfor filename in tqdm(test_files):\n    img_path = os.path.join(TEST_IMG_DIR, filename)\n    img_orig = cv2.imread(img_path)\n    img_orig = cv2.cvtColor(img_orig, cv2.COLOR_BGR2RGB)\n    h_img, w_img, _ = img_orig.shape\n\n    # TTA Detection\n    b_list, s_list, l_list, raw_viz_boxes = [], [], [], []\n    for scale in TTA_SCALES:\n        res = model.predict(img_path, imgsz=scale, conf=CONF_THRES, augment=True, verbose=False)[0]\n        if len(res.boxes) > 0:\n            bn, cn, cl = res.boxes.xyxyn.cpu().tolist(), res.boxes.conf.cpu().tolist(), res.boxes.cls.cpu().tolist()\n            b_list.append(bn); s_list.append(cn); l_list.append(cl)\n            for b, s, l in zip(bn, cn, cl):\n                raw_viz_boxes.append({'box': b, 'cls': int(l)})\n\n    # Weighted Boxes Fusion\n    if b_list:\n        boxes, scores, labels = weighted_boxes_fusion(b_list, s_list, l_list, weights=None, iou_thr=IOU_WBF, skip_box_thr=CONF_THRES)\n    else:\n        boxes, scores, labels = [], [], []\n\n    # Apply Logic\n    raw_heads = [xyxy_to_xywh(b) for b, l in zip(boxes, labels) if int(l) == 0]\n    raw_shemaghs = [xyxy_to_xywh(b) for b, l in zip(boxes, labels) if int(l) == 1]\n    cleaned_shemaghs = clean_duplicate_shemaghs(raw_shemaghs)\n    \n    is_correct = 0\n    max_img_score = -1.0 # Default if no valid pair found\n    \n    if raw_heads and cleaned_shemaghs:\n        for h in raw_heads:\n            for s in cleaned_shemaghs:\n                f = calculate_hybrid_features(h, s)\n                if not f['is_above']: continue\n                if f['head_flat'] and f['ioh'] < 0.80: continue\n                \n                score = f['ioh'] - (W_SYM * f['symmetry']) - (W_DX * f['dx_norm'])\n                if f['starts_at_top']: score += 0.05\n                if score > max_img_score: max_img_score = score\n        \n        if max_img_score > FINAL_THRESH:\n            is_correct = 1\n\n    # Store Data\n    pred_parts = [f\"{int(l)} {s:.4f} {xyxy_to_xywh(b)[0]:.4f} {xyxy_to_xywh(b)[1]:.4f} {xyxy_to_xywh(b)[2]:.4f} {xyxy_to_xywh(b)[3]:.4f}\" \n                  for b, s, l in zip(boxes, scores, labels)]\n    pred_str = \" \".join(pred_parts) if pred_parts else \"-\"\n    \n    all_results.append({\n        \"filename\": filename,\n        \"right_place\": is_correct,\n        \"prediction_string\": pred_str,\n        \"max_img_score\": max_img_score\n    })\n\n# EXPORTING DATA\ndf_all = pd.DataFrame(all_results)\n\n\ncertain_labels = []\nfor idx, row in df_all.iterrows():\n    if row['max_img_score'] >= SURE_CORRECT_THRESH:\n        certain_labels.append({\"filename\": row['filename'], \"label\": 1})\n    elif row['max_img_score'] <= SURE_INCORRECT_THRESH:\n        certain_labels.append({\"filename\": row['filename'], \"label\": 0})\n\ndf_pseudo = pd.DataFrame(certain_labels)\ndf_pseudo.to_csv(\"pseudo_labels.csv\", index=False)","metadata":{"execution":{"iopub.status.busy":"2026-02-17T23:54:10.465730Z","iopub.execute_input":"2026-02-17T23:54:10.466199Z","iopub.status.idle":"2026-02-17T23:56:33.235996Z","shell.execute_reply.started":"2026-02-17T23:54:10.466156Z","shell.execute_reply":"2026-02-17T23:56:33.235407Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import pandas as pd\n\n# Load your existing pseudo label CSV\ninput_csv = 'pseudo_labels.csv'\ndf = pd.read_csv(input_csv)\n\n# Filter for Label 1 \ndf_correct = df[df['label'] == 1].copy()\n\n# Save to a new file\noutput_csv = 'correct_images_only.csv'\ndf_correct.to_csv(output_csv, index=False)\n\n\nprint(f\"Filtered {len(df_correct)} correct images out of {len(df)} total pseudo labels.\")\nprint(f\"Saved to: {output_csv}\")","metadata":{"execution":{"iopub.status.busy":"2026-02-18T00:03:28.511997Z","iopub.execute_input":"2026-02-18T00:03:28.512521Z","iopub.status.idle":"2026-02-18T00:03:28.526659Z","shell.execute_reply.started":"2026-02-18T00:03:28.512491Z","shell.execute_reply":"2026-02-18T00:03:28.526044Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Training ConvNext for 'right_place' using Pseudo labeling\n\nFun fact: we got 0.848 private score using only heuristics. ","metadata":{}},{"cell_type":"code","source":"# CONFIGURATION\nORIGINAL_IMG_DIR = '/kaggle/input/dal-shemagh-detection-challenge/images/train'\nORIGINAL_CSV = '/kaggle/input/dal-shemagh-detection-challenge/train_labels.csv'\nPSEUDO_IMG_DIR = '/kaggle/input/dal-shemagh-detection-challenge/images/test'\nPSEUDO_CSV = '/kaggle/working/correct_images_only.csv'\nMODEL_NAME = 'convnext_tiny.fb_in22k_ft_in1k'\nNUM_CLASSES = 2\nIMG_SIZE = 224\nBATCH_SIZE = 16\nEPOCHS = 15\nLEARNING_RATE = 5e-5\nN_FOLDS = 5\nDEVICE = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\nOUTPUT_DIR = 'ConvNext_KFold_Models'\n\nos.makedirs(OUTPUT_DIR, exist_ok=True)\n\n# DATASET CLASS\nclass ShemaghDataset(Dataset):\n    def __init__(self, df, transform=None):\n        self.df = df\n        self.transform = transform\n        \n    def __len__(self):\n        return len(self.df)\n    \n    def __getitem__(self, idx):\n        row = self.df.iloc[idx]\n        img_path = row['path']\n        label = int(row['right_place'])\n        \n        # Read Image using OpenCV \n        image = cv2.imread(img_path)\n        if image is None:\n            # Fallback (black image) to prevent crash\n            image = np.zeros((IMG_SIZE, IMG_SIZE, 3), dtype=np.uint8)\n            \n        # Convert BGR -> RGB -> PIL Image \n        image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)\n        image = Image.fromarray(image)\n        \n        if self.transform:\n            image = self.transform(image)\n            \n        return image, torch.tensor(label, dtype=torch.long)\n\n\n# AUGMENTATIONS\ndef get_transforms(data='train'):\n    if data == 'train':\n        return transforms.Compose([\n            transforms.Resize((IMG_SIZE, IMG_SIZE)),\n            transforms.RandomHorizontalFlip(p=0.5),\n            transforms.RandomRotation(degrees=15),\n            transforms.ColorJitter(brightness=0.2, contrast=0.2, saturation=0.2, hue=0.02),\n            transforms.ToTensor(),\n            transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])\n        ])\n    else:\n        return transforms.Compose([\n            transforms.Resize((IMG_SIZE, IMG_SIZE)),\n            transforms.ToTensor(),\n            transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])\n        ])\n\n# 4. TRAINING ENGINE\ndef train_one_epoch(model, loader, optimizer, criterion):\n    model.train()\n    running_loss = 0.0\n    correct = 0\n    total = 0\n    \n    for images, labels in tqdm(loader, desc=\"Training\", leave=False):\n        images, labels = images.to(DEVICE), labels.to(DEVICE)\n        \n        optimizer.zero_grad()\n        outputs = model(images)\n        loss = criterion(outputs, labels)\n        loss.backward()\n        optimizer.step()\n        \n        running_loss += loss.item()\n        _, predicted = torch.max(outputs.data, 1)\n        total += labels.size(0)\n        correct += (predicted == labels).sum().item()\n        \n    return running_loss / len(loader), correct / total\n\ndef validate_one_epoch(model, loader, criterion):\n    model.eval()\n    running_loss = 0.0\n    all_preds = []\n    all_labels = []\n    \n    with torch.no_grad():\n        for images, labels in tqdm(loader, desc=\"Validating\", leave=False):\n            images, labels = images.to(DEVICE), labels.to(DEVICE)\n            \n            outputs = model(images)\n            loss = criterion(outputs, labels)\n            \n            running_loss += loss.item()\n            _, predicted = torch.max(outputs.data, 1)\n            \n            all_preds.extend(predicted.cpu().numpy())\n            all_labels.extend(labels.cpu().numpy())\n            \n    acc = accuracy_score(all_labels, all_preds)\n    f1 = f1_score(all_labels, all_preds, zero_division=0)\n    \n    return running_loss / len(loader), acc, f1\n\n# PIPELINE \nif __name__ == '__main__':\n    print(f\"--- Training ConvNeXt on {DEVICE} ---\")\n    \n    # 1. Load Data\n    df_orig = pd.read_csv(ORIGINAL_CSV)\n    df_orig['path'] = df_orig['filename'].apply(lambda x: os.path.join(ORIGINAL_IMG_DIR, x))\n    \n    df_pseudo = pd.read_csv(PSEUDO_CSV)\n    # rename from 'label' to 'right_place', thank you Omar \n    df_pseudo = df_pseudo.rename(columns={'label': 'right_place'})\n    df_pseudo['path'] = df_pseudo['filename'].apply(lambda x: os.path.join(PSEUDO_IMG_DIR, x))\n    \n    # Merge\n    df_final = pd.concat([df_orig, df_pseudo], axis=0).reset_index(drop=True)\n    \n    # Class Weighting Calculation\n    counts = df_final['right_place'].value_counts()\n    print(f\"Class Distribution: {counts.to_dict()}\")\n    \n    n_class0 = counts.get(0, 1)\n    n_class1 = counts.get(1, 1)\n    # Weight Class 1 higher to force model to pay attention to it\n    pos_weight = n_class0 / n_class1\n    class_weights = torch.tensor([1.0, float(pos_weight)]).to(DEVICE)\n    print(f\"Using Class Weights: [1.0, {pos_weight:.2f}]\")\n\n    # 2. Stratified K-Fold\n    skf = StratifiedKFold(n_splits=N_FOLDS, shuffle=True, random_state=42)\n    \n    for fold, (train_idx, val_idx) in enumerate(skf.split(df_final, df_final['right_place'])):\n        if fold > 0: \n            break\n        \n        train_df = df_final.iloc[train_idx].reset_index(drop=True)\n        val_df = df_final.iloc[val_idx].reset_index(drop=True)\n        \n        # Loaders (Workers=0 for Windows Safety)\n        train_ds = ShemaghDataset(train_df, transform=get_transforms('train'))\n        val_ds = ShemaghDataset(val_df, transform=get_transforms('val'))\n        \n        train_loader = DataLoader(train_ds, batch_size=BATCH_SIZE, shuffle=True, num_workers=0)\n        val_loader = DataLoader(val_ds, batch_size=BATCH_SIZE, shuffle=False, num_workers=0)\n        \n        # Model Setup\n        model = timm.create_model(MODEL_NAME, pretrained=True, num_classes=NUM_CLASSES)\n        model = model.to(DEVICE)\n        \n        optimizer = optim.AdamW(model.parameters(), lr=LEARNING_RATE, weight_decay=1e-2)\n        criterion = nn.CrossEntropyLoss(weight=class_weights)\n        \n        best_val_f1 = 0.0\n        \n        for epoch in range(EPOCHS):\n            train_loss, train_acc = train_one_epoch(model, train_loader, optimizer, criterion)\n            val_loss, val_acc, val_f1 = validate_one_epoch(model, val_loader, criterion)\n            \n            print(f\"Epoch {epoch+1:02d} | Train Loss: {train_loss:.4f} | Val F1: {val_f1:.4f}\")\n            \n            if val_f1 > best_val_f1:\n                best_val_f1 = val_f1\n                save_path = os.path.join(OUTPUT_DIR, f\"convnext_fold{fold}_best.pth\")\n                torch.save(model.state_dict(), save_path)\n                \n        print(f\"Fold {fold+1} Finished. Best F1: {best_val_f1:.4f}\")\n        \n        # Cleanup\n        del model, optimizer\n        torch.cuda.empty_cache()\n\n    print(f\"\\nTraining Complete. Models saved to {OUTPUT_DIR}\")","metadata":{"execution":{"iopub.status.busy":"2026-02-18T00:03:46.237361Z","iopub.execute_input":"2026-02-18T00:03:46.237705Z","iopub.status.idle":"2026-02-18T00:10:11.403635Z","shell.execute_reply.started":"2026-02-18T00:03:46.237674Z","shell.execute_reply":"2026-02-18T00:10:11.402958Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Generate Submission ","metadata":{}},{"cell_type":"code","source":"# CONFIGURATION\n\nYOLO_MODEL_PATH = '/kaggle/working/runs/detect/Shemagh_KFold_BuiltIn3/fold_2/weights/best.pt' \n# after some experiments, the first fold gives the hieghts validation score. \nCLS_MODEL_PATH = '/kaggle/working/ConvNext_KFold_Models/convnext_fold0_best.pth' \nTEST_IMG_DIR = '/kaggle/input/dal-shemagh-detection-challenge/images/test'\n\n# Inference Settings\nTTA_SCALES = [640, 800, 512]\nCONF_THRES = 0.05 \nIOU_WBF = 0.60  \nNMS_THRES = 0.65 \n\n# Classifier Settings\nCLS_MODEL_NAME = 'convnext_tiny.fb_in22k_ft_in1k'\nIMG_SIZE = 224\nDEVICE = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\n\n\n# UTILITY FUNCTIONS\ndef xyxy_to_xywh(box):\n    \"\"\"Converts [x1, y1, x2, y2] to [x_center, y_center, w, h].\"\"\"\n    x1, y1, x2, y2 = box\n    w = x2 - x1\n    h = y2 - y1\n    x = (x1 + x2) / 2\n    y = (y1 + y2) / 2\n    return [x, y, w, h]\n\ndef get_inference_transforms():\n    \"\"\"Exact same transforms used in Validation.\"\"\"\n    return transforms.Compose([\n        transforms.Resize((IMG_SIZE, IMG_SIZE)),\n        transforms.ToTensor(),\n        transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])\n    ])\n\ndef apply_nms(boxes, scores, labels, iou_threshold=0.65):\n    \"\"\"\n    Applies Non-Maximum Suppression per class.\n    If overlap > threshold, keeps only the one with higher confidence.\n    \"\"\"\n    if len(boxes) == 0:\n        return [], [], []\n\n    # Convert to tensor\n    boxes_t = torch.tensor(boxes, dtype=torch.float32)\n    scores_t = torch.tensor(scores, dtype=torch.float32)\n    labels_t = torch.tensor(labels, dtype=torch.int64)\n\n    keep_indices = []\n    \n    # Apply NMS for each class independently\n    unique_labels = labels_t.unique()\n    for lbl in unique_labels:\n        mask = (labels_t == lbl)\n        class_boxes = boxes_t[mask]\n        class_scores = scores_t[mask]\n        original_indices = torch.where(mask)[0]\n        \n        # torchvision NMS returns indices to keep\n        nms_keep = ops.nms(class_boxes, class_scores, iou_threshold)\n        \n        # Map back to original indices\n        keep_indices.extend(original_indices[nms_keep].tolist())\n\n    keep_indices = sorted(keep_indices)\n    \n    return [boxes[i] for i in keep_indices], \\\n           [scores[i] for i in keep_indices], \\\n           [labels[i] for i in keep_indices]\n\n# LOAD MODELS\nprint(f\"Loading YOLO: {YOLO_MODEL_PATH}\")\nyolo_model = YOLO(YOLO_MODEL_PATH)\n\nprint(f\"Loading ConvNeXt: {CLS_MODEL_PATH}\")\ncls_model = timm.create_model(CLS_MODEL_NAME, pretrained=False, num_classes=2)\n# Load weights\nstate_dict = torch.load(CLS_MODEL_PATH, map_location=DEVICE)\ncls_model.load_state_dict(state_dict)\ncls_model.to(DEVICE)\ncls_model.eval()\n\ncls_transforms = get_inference_transforms()\n\n\n# FINAL INFERENCE PIPELINE\nprint(\"\\nStarting Final Inference...\")\n\ntest_files = sorted([f for f in os.listdir(TEST_IMG_DIR) if f.lower().endswith(('.jpg', '.jpeg', '.png'))])\nsubmission_rows = []\n\nfor filename in tqdm(test_files, desc=\"Processing Images\"):\n    img_path = os.path.join(TEST_IMG_DIR, filename)\n    \n    # YOLO TTA \n    boxes_list = []\n    scores_list = []\n    labels_list = []\n    \n    for scale in TTA_SCALES:\n        results = yolo_model.predict(img_path, imgsz=scale, conf=CONF_THRES, augment=True, verbose=False)[0]\n        \n        if len(results.boxes) > 0:\n            boxes_list.append(results.boxes.xyxyn.cpu().tolist())\n            scores_list.append(results.boxes.conf.cpu().tolist())\n            labels_list.append(results.boxes.cls.cpu().tolist())\n\n    # WBF \n    if len(boxes_list) > 0:\n        boxes, scores, labels = weighted_boxes_fusion(\n            boxes_list, scores_list, labels_list, \n            weights=None, iou_thr=IOU_WBF, skip_box_thr=CONF_THRES\n        )\n    else:\n        boxes, scores, labels = [], [], []\n\n    # NMS  \n    boxes, scores, labels = apply_nms(boxes, scores, labels, iou_threshold=NMS_THRES)\n\n    # Prepare Logic & Prediction String \n    has_head = False\n    has_shemagh = False\n    pred_parts = []\n    \n    for b, s, l in zip(boxes, scores, labels):\n        xywh = xyxy_to_xywh(b)\n        x, y, w, h = xywh\n        \n        pred_parts.append(f\"{int(l)} {s:.4f} {x:.4f} {y:.4f} {w:.4f} {h:.4f}\")\n        \n        if int(l) == 0: has_head = True\n        elif int(l) == 1: has_shemagh = True\n            \n    prediction_string = \" \".join(pred_parts) if pred_parts else \"-\"\n\n    # Classifier Logic \n    is_correct = 0\n    \n    if has_head or has_shemagh: # YOLO might not see all faces and shemaghs in the image\n        # Load and Preprocess Image for Classifier\n        # Read with OpenCV to match training pipeline\n        img_cv = cv2.imread(img_path)\n        img_cv = cv2.cvtColor(img_cv, cv2.COLOR_BGR2RGB)\n        img_pil = Image.fromarray(img_cv)\n        \n        input_tensor = cls_transforms(img_pil).unsqueeze(0).to(DEVICE)\n        \n        with torch.no_grad():\n            outputs = cls_model(input_tensor)\n            # Get class with highest score (0 or 1)\n            probs = torch.softmax(outputs, dim=1)\n            pred_cls = torch.argmax(probs, dim=1).item()\n            \n            # Assuming Class 1 is \"Right Place\"\n            is_correct = pred_cls\n\n    submission_rows.append({\n        \"filename\": filename,\n        \"right_place\": is_correct,\n        \"prediction_string\": prediction_string\n    })\n\n# 5. SAVE SUBMISSION\ndf_sub = pd.DataFrame(submission_rows)\ndf_sub.to_csv(\"submission.csv\", index=False)\n\nprint(\"Submission saved to submission.csv\")\nprint(f\"Total Positive Predictions: {df_sub['right_place'].sum()}\")","metadata":{"execution":{"iopub.status.busy":"2026-02-18T00:19:57.703954Z","iopub.execute_input":"2026-02-18T00:19:57.704800Z","iopub.status.idle":"2026-02-18T00:22:22.430985Z","shell.execute_reply.started":"2026-02-18T00:19:57.704768Z","shell.execute_reply":"2026-02-18T00:22:22.430384Z"},"trusted":true},"outputs":[],"execution_count":null}]}