{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.11.13","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"none","dataSources":[{"sourceId":107469,"databundleVersionId":13058354,"sourceType":"competition"},{"sourceId":12879633,"sourceType":"datasetVersion","datasetId":8148436}],"dockerImageVersionId":31089,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"!pip install ultralytics --upgrade -q\nfrom ultralytics import YOLO","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"data_yaml='''\n\ntrain:  /kaggle/input/d/kostya876/multi-class-object-detection-challenge/merged_dataset_all/train/images\nval:  /kaggle/input/d/kostya876/multi-class-object-detection-challenge/merged_dataset_all/val/images\ntest:  /kaggle/input/multi-class-object-detection-challenge/testImages/images\nnc: 2\nnames: ['cheerios', 'soup']\n'''\nwith open('data.yaml', 'w') as file:\n    file.write(data_yaml)","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"The model trained with these parameters showed the best result, public 0.976 and private 0.99.","metadata":{}},{"cell_type":"code","source":"from ultralytics import YOLO\nimport yaml, os, shutil\nimport torch\nimport random\nimport numpy as np\nfrom pathlib import Path\nimport os\n\nnp.random.seed(42)\nrandom.seed(42)\ntorch.manual_seed(42)\n\nmodel = YOLO(\"yolo11x.pt\")\n\nmodel.train(\n    data=\"yolo_params.yaml\",\n    epochs=20,                \n    batch=12,                   \n    imgsz=640,\n    patience=50,               \n    optimizer='SGD',\n    momentum=0.937,          \n    lr0=0.001,                \n    weight_decay=0.0005,       \n    cos_lr=True,               \n    save_period=5,             \n    workers=8,\n    # Augmentations\n    close_mosaic=10,\n    hsv_h=0.015,\n    hsv_s=0.7,\n    hsv_v=0.4,\n    flipud=0.5,\n    fliplr=0.5,\n    translate=0.1,\n    scale=0.5,\n    shear=0.01,\n    agnostic_nms=True,\n    project=\"Duality-YOLO-Local\",\n    name=\"run-1\",\n)","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"You may also try using this parameters, but keep in mind that  it performed worse, likely due to its heavier regularization and more aggressive augmentations, so the lighter setup proved more effective. public 0.928 and private 0.941","metadata":{}},{"cell_type":"code","source":"model.train(\n    data=\"data.yaml\",\n    exist_ok=False,\n    epochs=100,\n    batch=16,\n    imgsz=640,\n    patience=100,               \n    optimizer='SGD',\n    momentum=0.937,          \n    lr0=0.0025,\n    lrf=0.0001,\n    weight_decay=0.0001,\n    dropout=0.3,\n    dfl=0.75,\n    cos_lr=True,               \n    save_period=5,             \n    workers=8,\n    freeze=3,\n    mosaic=1.0,           \n    close_mosaic=20,      \n    mixup=0.3,            \n    copy_paste=0.2,       \n    degrees=10,           \n    perspective=0.002,    \n    scale=0.5,            \n    translate=0.2,        \n    shear=0.02,           \n    hsv_h=0.02,          \n    hsv_s=0.3,\n    hsv_v=0.3,\n    flipud=0.1,          \n    fliplr=0.1,          \n    agnostic_nms=True,\n    project=\"Duality-YOLO-Local\",\n    name=\"run-1\",\n)","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Code for merging files into a single dataset.\n","metadata":{}},{"cell_type":"code","source":"import uuid\nimport shutil\nfrom pathlib import Path\n\ndef merge_nested(sources: list[Path], dst: Path) -> None:\n    for split in (\"train\", \"val\"):\n        for kind in (\"images\", \"labels\"):\n            (dst / split / kind).mkdir(parents=True, exist_ok=True)\n\n    for src_root in sources:\n        for split in (\"train\", \"val\"):\n            img_dir = src_root / split / \"images\"\n            lbl_dir = src_root / split / \"labels\"\n            if not img_dir.exists():\n                continue\n            for img_path in img_dir.iterdir():\n                if img_path.suffix.lower() not in {\".jpg\", \".jpeg\", \".png\", \".bmp\"}:\n                    continue\n                lbl_path = lbl_dir / f\"{img_path.stem}.txt\"\n                new_base = uuid.uuid4().hex\n                new_img  = dst / split / \"images\" / f\"{new_base}{img_path.suffix.lower()}\"\n                new_lbl  = dst / split / \"labels\" / f\"{new_base}.txt\"\n\n                shutil.copy2(img_path, new_img)\n                if lbl_path.exists():\n                    shutil.copy2(lbl_path, new_lbl)\n                else:\n                    print(f\"[WARN] no label for {img_path}\")\n\nif __name__ == \"__main__\":\n    srcs = [\n       \n    Path(r\"\"),\n\n    Path(r\"\"),\n   \n    Path(r\"\")\n\n    ]     #Path to FalconCloud scenarios\n\n    dst = Path(r\"\")     #Path for new dataset\n\n    merge_nested(srcs, dst)\n    print(\"Merged into\", dst.resolve())","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"If you have a well-trained model, you can evaluate the dataset quality; this code counts objects above and below various confidence thresholds, tracks statistics for each class and each image, handles empty or corrupted images, and saves a detailed results report.","metadata":{}},{"cell_type":"code","source":"from ultralytics import YOLO\nfrom pathlib import Path\nimport glob\nfrom collections import defaultdict\nfrom datetime import datetime\nfrom PIL import Image\n\n# Path to the model\nbest_path = Path(r\"\")\nmodel = YOLO(best_path)\n\n# Path to images\ntest_imgs = glob.glob(r\"\")\n\nclass_names = ['cheerios', 'soup']\nnum_classes = len(class_names)\n\n# Thresholds\nthresholds = [0.6, 0.7, 0.8, 0.9, 1.0]\nabove_thresholds = [0.8, 0.9, 0.95, 0.97]\n\n# Counters for thresholds\nbelow_counts_global = {thr: 0 for thr in thresholds}\nabove_counts_global = {thr: 0 for thr in above_thresholds}\n\nbelow_counts_by_class = {cls: {thr: 0 for thr in thresholds} for cls in range(num_classes)}\nabove_counts_by_class = {cls: {thr: 0 for thr in above_thresholds} for cls in range(num_classes)}\n\nclass_distribution = defaultdict(int)\nper_image_summary = {}\n\n# Image counters\nempty_images = 0\ncorrupt_images = 0\n\n# Additional counters for objects and images\nimages_all_above_95 = 0\nimages_all_above_97 = 0\nobjects_above_95 = 0\nobjects_above_97 = 0\n\n# Counters of objects > thresholds per class\nobjects_above_95_by_class = {cls: 0 for cls in range(num_classes)}\nobjects_above_97_by_class = {cls: 0 for cls in range(num_classes)}\n\n# Dictionary to store images with objects conf < 0.6\nbelow_threshold_images = []\n\n# Image processing\nfor img in test_imgs:\n    try:\n        results = model.predict([img], imgsz=640, conf=0.25, iou=0.45, verbose=False)\n    except Exception:\n        corrupt_images += 1\n        continue\n\n    boxes = results[0].boxes\n\n    if len(boxes) == 0:\n        empty_images += 1\n        per_image_summary[Path(img).name] = {}\n        continue\n\n    image_class_dist = defaultdict(int)\n    all_conf = []\n\n    for box in boxes:\n        conf = float(box.conf.item())\n        cls = int(box.cls.item())\n        all_conf.append(conf)\n\n        for thr in thresholds:\n            if conf < thr:\n                below_counts_global[thr] += 1\n                below_counts_by_class[cls][thr] += 1\n                if thr == 0.6:\n                    below_threshold_images.append((img, box))  # save for display\n\n        for thr in above_thresholds:\n            if conf > thr:\n                above_counts_global[thr] += 1\n                above_counts_by_class[cls][thr] += 1\n\n        # Class distribution\n        class_distribution[cls] += 1\n        image_class_dist[cls] += 1\n\n        # Counters of objects >0.95/0.97\n        if conf > 0.95:\n            objects_above_95 += 1\n            objects_above_95_by_class[cls] += 1\n        if conf > 0.97:\n            objects_above_97 += 1\n            objects_above_97_by_class[cls] += 1\n\n    # Counters of images where all objects >0.95/0.97\n    if all(conf > 0.95 for conf in all_conf):\n        images_all_above_95 += 1\n    if all(conf > 0.97 for conf in all_conf):\n        images_all_above_97 += 1\n\n    per_image_summary[Path(img).name] = dict(image_class_dist)\n\n# Results file with date and time\ntimestamp = datetime.now().strftime(\"%Y%m%d_%H%M%S\")\noutput_file = best_path.parent / f\"results_{timestamp}.txt\"\n\nwith open(output_file, \"w\", encoding=\"utf-8\") as f:\n    f.write(\"Counts by thresholds (below)\\n\")\n    for thr, count in below_counts_global.items():\n        f.write(f\"All classes - Confidence < {thr}: {count}\\n\")\n    for cls in range(num_classes):\n        f.write(f\"\\nClass {class_names[cls]}:\\n\")\n        for thr, count in below_counts_by_class[cls].items():\n            f.write(f\"  Confidence < {thr}: {count}\\n\")\n\n    f.write(\"\\nCounts by thresholds (above)\\n\")\n    for thr, count in above_counts_global.items():\n        f.write(f\"All classes - Confidence > {thr}: {count}\\n\")\n    for cls in range(num_classes):\n        f.write(f\"\\nClass {class_names[cls]}:\\n\")\n        for thr, count in above_counts_by_class[cls].items():\n            f.write(f\"  Confidence > {thr}: {count}\\n\")\n\n    f.write(\"\\nOverall class distribution\\n\")\n    for cls, count in class_distribution.items():\n        f.write(f\"{class_names[cls]}: {count}\\n\")\n\n    f.write(f\"\\nNumber of images with no detections: {empty_images}\\n\")\n    f.write(f\"Number of corrupted images: {corrupt_images}\\n\")\n\n    f.write(\"\\nImages and objects above thresholds 0.95 / 0.97\\n\")\n    f.write(f\"Images with all objects > 0.95: {images_all_above_95}\\n\")\n    f.write(f\"Images with all objects > 0.97: {images_all_above_97}\\n\")\n    f.write(f\"Number of objects > 0.95: {objects_above_95}\\n\")\n    f.write(f\"Number of objects > 0.97: {objects_above_97}\\n\")\n\n    f.write(\"\\n=== Number of objects > thresholds per class ===\\n\")\n    for cls in range(num_classes):\n        f.write(f\"{class_names[cls]} - >0.95: {objects_above_95_by_class[cls]}, >0.97: {objects_above_97_by_class[cls]}\\n\")\n\nprint(f\"Results saved to file: {output_file}\")\n\n\n#if you want to display images with object conf less than certain number run this code\n'''\nfrom IPython.display import display\nfrom PIL import Image\n\n# Show images with objects conf < 0.6 via display\nfor img_path, box in below_threshold_images:\n    result_copy = model.predict([img_path], imgsz=640, conf=0.25, iou=0.45, verbose=False)[0]\n    result_copy.boxes = [box]\n    im = result_copy.plot()\n    im = Image.fromarray(im)\n    display(im)  \n'''","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"This code creates a new dataset by copying all validation images and labels, filters training images based on model predictions so that only images with objects above a certain confidence or with empty/missing labels are kept.","metadata":{}},{"cell_type":"code","source":"from ultralytics import YOLO\nfrom pathlib import Path\nimport shutil\nimport random\n\n#Path\norig_train_dir = Path(r\"\")\norig_val_dir = Path(r\"\")\nsave_dir = Path(r\"\")\n\ntrain_save_dir = save_dir / \"train\"\nval_save_dir = save_dir / \"val\"\n\ntrain_save_dir.mkdir(parents=True, exist_ok=True)\nval_save_dir.mkdir(parents=True, exist_ok=True)\n\nbest_path = Path(r\"\")\nmodel = YOLO(best_path)\n\nfor split in [\"images\", \"labels\"]:\n    orig_split_dir = orig_val_dir / split\n    save_split_dir = val_save_dir / split\n    save_split_dir.mkdir(exist_ok=True, parents=True)\n    for f in orig_split_dir.glob(\"*\"):\n        shutil.copy(f, save_split_dir / f.name)\n\ntrain_images = list((orig_train_dir / \"images\").glob(\"*\"))\nselected_for_val = []  \n\nfor img_path in train_images:\n    label_path = orig_train_dir / \"labels\" / f\"{img_path.stem}.txt\"\n    keep_image = False\n    passed_097 = False\n\n    if not label_path.exists() or label_path.stat().st_size == 0:\n        keep_image = True\n    else:\n        try:\n            results = model.predict([str(img_path)], imgsz=640, conf=0.25, iou=0.45, verbose=False)\n        except Exception:\n            continue\n\n        boxes = results[0].boxes\n        if len(boxes) == 0:\n            keep_image = True\n        else:\n            all_conf = [float(box.conf) for box in boxes]\n            if all(conf > 0.65 for conf in all_conf):\n                keep_image = True\n                passed_0xx = True\n\n    if keep_image:\n        (train_save_dir / \"images\").mkdir(exist_ok=True, parents=True)\n        shutil.copy(img_path, train_save_dir / \"images\" / img_path.name)\n\n        if label_path.exists():\n            (train_save_dir / \"labels\").mkdir(exist_ok=True, parents=True)\n            shutil.copy(label_path, train_save_dir / \"labels\" / label_path.name)\n\n        if passed_0xx:\n            selected_for_val.append((img_path, label_path if label_path.exists() else None))\n\n#to increase val run this\n'''\nif len(selected_for_val) > 200:\n    chosen = random.sample(selected_for_val, 200)\nelse:\n    chosen = selected_for_val\n\nfor img_path, label_path in chosen:\n    (val_save_dir / \"images\").mkdir(exist_ok=True, parents=True)\n    shutil.copy(img_path, val_save_dir / \"images\" / img_path.name)\n\n    if label_path:\n        (val_save_dir / \"labels\").mkdir(exist_ok=True, parents=True)\n        shutil.copy(label_path, val_save_dir / \"labels\" / label_path.name)\n\n    train_img_copy = train_save_dir / \"images\" / img_path.name\n    if train_img_copy.exists():\n        train_img_copy.unlink()\n\n    if label_path:\n        train_label_copy = train_save_dir / \"labels\" / label_path.name\n        if train_label_copy.exists():\n            train_label_copy.unlink()\n'''","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Check the model’s performance and generate table.","metadata":{}},{"cell_type":"code","source":"from ultralytics import YOLO\nfrom pathlib import Path\nimport glob\nfrom PIL import Image\n\nbest_path = Path(r\"\")   #Path to your model\n\nmodel = YOLO(best_path)\n\ntest_imgs = glob.glob('testImages/images/*')[:20]   #First 20 photos\n\nfor img in test_imgs:\n    results = model.predict(img, imgsz=640, conf=0.25, iou=0.45)\n    im = Image.fromarray(results[0].plot()[:, :, ::-1])\n    display(im)  ","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"model=YOLO(\"\")   #Path to your model\n\ntest_images_path = \"/kaggle/input/multi-class-object-detection-challenge/testImages/images\"\noutput_dir = \"/kaggle/working/predictions/labels\"\n\nconf=0.001\n\ndef predict(test_images_path, output_dir , model, conf):\n    os.makedirs(output_dir, exist_ok=True)\n    model.eval()\n    model.training = False\n    for img_path in Path(test_images_path).glob(\"*\"):\n        if img_path.suffix.lower() not in ['.png', '.jpg', '.jpeg']:\n            continue\n    \n        results = model.predict(img_path, conf=conf, augment=True, iou=0.4, max_det=600, verbose=False)  \n        \n        output_txt = Path(output_dir) / f\"{img_path.stem}.txt\"\n    \n        with open(output_txt, \"w\") as f:\n            for result in results:\n                img_height, img_width = result.orig_shape\n                for box in result.boxes.data:\n                    x1, y1, x2, y2, confidence, cls_id = box.tolist()\n    \n                    x_center = ((x1 + x2) / 2) / img_width\n                    y_center = ((y1 + y2) / 2) / img_height\n                    width = (x2 - x1) / img_width\n                    height = (y2 - y1) / img_height\n    \n                    f.write(f\"{cls_id} {confidence:.6f} {x_center:.6f} {y_center:.6f} {width:.6f} {height:.6f}\\n\")\n    \n    print(f\"[notice] ✅ Predictions saved: {output_dir}\")\npredict(test_images_path, output_dir , model, conf)","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import pandas as pd\nimport csv\n# Convert predictions to CSV\ndef predictions_to_csv(\n    preds_folder: str = \"/kaggle/working/predictions/labels\", \n    output_csv: str = \"/kaggle/working/submission.csv\", \n    test_images_folder: str = \"/kaggle/input/multi-class-object-detection-challenge/testImages/images\",\n    allowed_extensions: tuple = (\".jpg\", \".png\", \".jpeg\")\n):\n    preds_path = Path(preds_folder)\n    test_images_path = Path(test_images_folder)\n\n    test_images = {p.stem for p in test_images_path.glob(\"*\") if p.suffix.lower() in allowed_extensions}\n\n    predictions = []\n    predicted_images = set()\n\n    for txt_file in preds_path.glob(\"*.txt\"):\n        image_id = txt_file.stem\n        predicted_images.add(image_id)\n\n        with open(txt_file, \"r\") as f:\n            valid_lines = [line.strip() for line in f if len(line.strip().split()) == 6]\n\n        pred_str = \" \".join(valid_lines) if valid_lines else \"no boxes\"\n        predictions.append({\"image_id\": image_id, \"prediction_string\": pred_str})\n\n    missing_images = test_images - predicted_images\n    for image_id in missing_images:\n        predictions.append({\"image_id\": image_id, \"prediction_string\": \"no boxes\"})\n\n    submission_df = pd.DataFrame(predictions)\n    submission_df.to_csv(output_csv, index=False, quoting=csv.QUOTE_MINIMAL)\n    print(submission_df.shape)\n    print(submission_df.head(10))\n    print(f\"[notice] ✅ Submission saved to {output_csv}\")\n\npredictions_to_csv()","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}