{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.6.6","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"none","dataSources":[],"dockerImageVersionId":28755,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","metadata":{},"source":"# Submit notebook\n\nSubmit notebook for attending Global Wheat Detection competition.\n\nBest score:\n\nPrivate score — 0.5830\n\nPublic score — 0.6465\n- - -\nRelated links:\n\nNotebook used for model training, can be found — [here](https://www.kaggle.com/code/pavlozelinskiy/global-wheat-detection-train-v1).\n\nBest model dataset, can be found — [here](https://www.kaggle.com/datasets/pavlozelinskiy/global-wheat-detection-weights-v1)."},{"cell_type":"code","metadata":{"ExecuteTime":{"end_time":"2026-09-25T08:49:28.677831200Z","start_time":"2026-09-25T08:49:26.161557Z"}},"source":"# Core\nimport numpy as np\nimport pandas as pd\nimport torch\n\n# Data related\nimport os\nfrom pathlib import Path\nfrom torch.utils.data import DataLoader\n\n# from kaggle_second_competition.dataset import ImageDataset\n# from kaggle_second_competition.detector import build_detector\n# from kaggle_second_competition.detection_transform import build_validation_transform, detection_collate\n\n# region Constants\nDEVICE = 'cuda'\n\nIMAGE_SIZE = 1024\nNUM_CLASSES = 2\nBATCH_SIZE = 4\nNUM_WORKERS = 2\n# endregion\n\n# region Config\nRUN_SANITY_CHECKS = True\n# endregion\n\n# region Path related\nIS_KAGGLE = bool(os.environ.get('KAGGLE_KERNEL_RUN_TYPE'))\nKAGGLE_DIR = Path('/kaggle') if IS_KAGGLE else Path.cwd().parent / 'kaggle'\n\nINPUT_DIR = KAGGLE_DIR / 'input' / 'competitions' / 'global-wheat-detection'\nOUTPUT_DIR = KAGGLE_DIR / 'working'\nMODEL_DIR = (KAGGLE_DIR / 'input' / 'datasets' / 'pavlozelinskiy' / 'global-wheat-detection-weights-v1') if IS_KAGGLE else OUTPUT_DIR\n\nBEST_MODEL_PATH = MODEL_DIR / 'best_model.pth'\nSUBMISSION_PATH = OUTPUT_DIR / 'submission.csv'\n\nfor label, d in [('INPUT_DIR', INPUT_DIR), ('MODEL_DIR', MODEL_DIR)]:\n    if not d.is_dir():\n        raise FileNotFoundError(\n            f\"{label} missing: {d}\\nmounted: {[p.name for p in Path('/kaggle/input').iterdir()]}\"\n            if IS_KAGGLE else f\"{label} missing: {d}\"\n        )\nassert BEST_MODEL_PATH.is_file(), f\"{BEST_MODEL_PATH}\\nfound: {list(MODEL_DIR.iterdir())}\"\n# endregion\n","outputs":[],"execution_count":1},{"cell_type":"markdown","metadata":{},"source":"# Copy pasted classes and functions\nto run inference locally on Windows the .py versions are imported instead"},{"cell_type":"code","metadata":{"ExecuteTime":{"end_time":"2026-09-24T17:47:07.588572900Z","start_time":"2026-09-24T17:47:07.583595300Z"}},"source":"# Copy pasted from dataset.py and detection_transform.py and detector.py\n\nfrom pathlib import Path\n\nimport numpy as np\nimport torch\nfrom PIL import Image\nfrom torch.utils.data import Dataset\n\nWHEAT_HEAD_CLASS_ID = 1\n\n\nclass ImageDataset(Dataset):\n    \"\"\"\n    One item == one image plus all of its boxes.\n        transform: callable(PIL.Image, ndarray[N, 4]) -> (image_tensor, boxes[M, 4])\n    Boxes come out of here as absolute-pixel xyxy (x_min, y_min, x_max, y_max).\n    \"\"\"\n\n    def __init__(self, data_frame, image_dir, transform, has_label=True):\n        self.image_dir = Path(image_dir)\n        self.transform = transform\n\n        if not has_label:\n            self.image_ids = [str(i) for i in data_frame['image_id']]\n            self.boxes = None\n            self.source = [None] * len(self.image_ids)\n            return\n\n        groups = data_frame.groupby('image_id', sort=False).indices\n        boxes = self.parse_bounding_boxes(data_frame['bbox'])\n        is_finite_mask = np.isfinite(boxes).all(axis=1)\n        sources = data_frame['source'].to_numpy()\n\n        # Note: wrapping into `str` to prevent TypeError when working with path\n        self.image_ids = [str(i) for i in groups]\n        self.boxes = [boxes[rows[is_finite_mask[rows]]] for rows in groups.values()]\n        self.source = [sources[rows[0]] for rows in groups.values()]\n\n    @staticmethod\n    def parse_bounding_boxes(bounding_box_column):\n        \"\"\"\n        Convert `xywh` style bounding boxes to `xyxy`.\n        \"\"\"\n        xyxy = np.full((len(bounding_box_column), 4), np.nan, dtype='float32')\n        is_box = bounding_box_column.notna().to_numpy()\n        if not is_box.any():\n            return xyxy\n\n        xywh = (bounding_box_column[is_box]\n                .str.strip('[]')\n                .str.split(',', expand=True).to_numpy(dtype='float32'))\n        assert xywh.shape[1] == 4, f'expected 4 numbers per bbox, parsed {xywh.shape[1]}'\n\n        xyxy[is_box, :2] = xywh[:, :2]\n        xyxy[is_box, 2:] = xywh[:, :2] + xywh[:, 2:]\n        return xyxy\n\n    def __len__(self):\n        return len(self.image_ids)\n\n    def __getitem__(self, i):\n        image_id = self.image_ids[i]\n        # Note: .convert('RGB') added as a sanity check in hidden test set\n        img = Image.open(self.image_dir / f'{image_id}.jpg').convert('RGB')\n        boxes = self.boxes[i] if self.boxes is not None else np.zeros((0, 4), dtype='float32')\n\n        img, boxes = self.transform(img, boxes)\n\n        target = {'image_id': image_id, 'source': self.source[i]}\n        if self.boxes is not None:\n            target['boxes'] = torch.as_tensor(np.asarray(boxes, dtype='float32')).reshape(-1, 4)\n            target['labels'] = torch.full((len(target['boxes']),), WHEAT_HEAD_CLASS_ID, dtype=torch.int64)\n        return img, target\n\n\nimport numpy as np\nimport torch\nfrom torchvision import tv_tensors\nfrom torchvision.transforms import v2\n\n\ndef box_areas(boxes):\n    widths = (boxes[:, 2] - boxes[:, 0]).clamp(min=0)\n    heights = (boxes[:, 3] - boxes[:, 1]).clamp(min=0)\n    return widths * heights\n\n\nclass BoxAwareTransform:\n    \"\"\"\n    Box-aware transform satisfying ImageDataset's contract:\n        callable(PIL.Image, ndarray[N, 4] xyxy) -> (float tensor[3, H, W], ndarray[M, 4] xyxy)\n    \"\"\"\n\n    def __init__(self, pipeline, min_visibility=0.0):\n        self.pipeline = pipeline\n        self.clamp_boxes = v2.ClampBoundingBoxes(clamping_mode='hard')\n        self.to_float = v2.ToDtype(torch.float32, scale=True)\n        self.min_visibility = min_visibility\n\n    def __call__(self, image, boxes):\n        # Note: most of this logic is unused before we have some Crop, or Scale augmentations\n        #  clamping to < 1px size, is important when resize from 1024 to lower resolution will be done.\n        boxes = tv_tensors.BoundingBoxes(\n            data=torch.as_tensor(np.asarray(boxes, dtype='float32')).reshape(-1, 4),\n            format='XYXY', canvas_size=(image.height, image.width), clamping_mode=None)\n\n        image, boxes = self.pipeline(v2.functional.to_image(image), boxes)\n\n        clamped = self.clamp_boxes(boxes).as_subclass(torch.Tensor)\n        widths = clamped[:, 2] - clamped[:, 0]\n        heights = clamped[:, 3] - clamped[:, 1]\n        visibility = (widths * heights) / box_areas(boxes.as_subclass(torch.Tensor)).clamp(min=1e-6)\n        keep = (visibility >= self.min_visibility) & (widths >= 1) & (heights >= 1)\n\n        # Note: float32 scaled to [0, 1], bc the detector normalizes it\n        image = self.to_float(image)\n        return image, clamped[keep].numpy()\n\n\ndef build_train_transform(image_size, min_visibility):\n    steps = [\n        v2.Resize(size=(image_size, image_size)),\n        v2.RandomHorizontalFlip(p=0.5),\n        v2.RandomVerticalFlip(p=0.5),\n        v2.RandomApply([v2.RandomRotation(degrees=(90, 90))], p=0.5),\n        v2.ColorJitter(brightness=(0.7, 1.3), contrast=(0.8, 1.2), saturation=(0.7, 1.3), hue=(-0.08, 0.08)),\n        v2.RandomGrayscale(p=0.05),\n        v2.RandomApply([v2.GaussianBlur(kernel_size=9, sigma=(0.5, 2.0))], p=0.1),\n    ]\n    return BoxAwareTransform(pipeline=v2.Compose(steps), min_visibility=min_visibility)\n\n\ndef build_validation_transform(image_size):\n    steps = [v2.Resize(size=(image_size, image_size))]\n    return BoxAwareTransform(v2.Compose(steps), min_visibility=0.0)\n\n\ndef detection_collate(batch):\n    \"\"\"\n    The default collate cannot stack targets, because the number of boxes differs per image.\n    Returns (list[Tensor], list[dict]), which is exactly what torchvision's detectors accept.\n    \"\"\"\n    images, targets = zip(*batch)\n    shapes = {tuple(image.shape) for image in images}\n    assert len(shapes) == 1, f'images in one batch must share a shape, got {shapes}'\n    return list(images), list(targets)\n\nfrom functools import partial\n\nimport torch.nn as nn\nfrom torchvision.models.detection import (fasterrcnn_resnet50_fpn_v2, fcos_resnet50_fpn,\n                                          retinanet_resnet50_fpn_v2)\nfrom torchvision.models.detection.faster_rcnn import FastRCNNPredictor\nfrom torchvision.models.detection.fcos import FCOSClassificationHead\nfrom torchvision.models.detection.retinanet import RetinaNetClassificationHead\n\nDETECTION_HEADS = ('faster_rcnn', 'retinanet', 'fcos')\n\n\ndef build_detector(head, image_size, num_classes, max_detections_per_image=300,\n                   score_threshold=0.05, nms_threshold=0.5, pretrained=True):\n    assert head in DETECTION_HEADS, f'unknown head {head!r}, expected one of {DETECTION_HEADS}'\n    weights = 'DEFAULT' if pretrained else None\n    shared = dict(weights=weights, min_size=image_size, max_size=image_size)\n    if not pretrained:\n        shared['weights_backbone'] = None\n\n    if head == 'faster_rcnn':\n        model = fasterrcnn_resnet50_fpn_v2(\n            box_detections_per_img=max_detections_per_image,\n            box_score_thresh=score_threshold, box_nms_thresh=nms_threshold, **shared)\n        # The COCO predictor emits 91 classes; only this last layer is replaced.\n        in_features = model.roi_heads.box_predictor.cls_score.in_features\n        model.roi_heads.box_predictor = FastRCNNPredictor(in_features, num_classes)\n        return model\n\n    one_stage = dict(detections_per_img=max_detections_per_image,\n                     score_thresh=score_threshold, nms_thresh=nms_threshold, **shared)\n\n    if head == 'retinanet':\n        model = retinanet_resnet50_fpn_v2(**one_stage)\n        # norm_layer matches what the _v2 builder uses for its own head, so the replacement is\n        # structurally identical to the one it shipped with\n        model.head.classification_head = RetinaNetClassificationHead(\n            in_channels=model.backbone.out_channels,\n            num_anchors=model.anchor_generator.num_anchors_per_location()[0],\n            num_classes=num_classes,\n            norm_layer=partial(nn.GroupNorm, 32))\n        return model\n\n    model = fcos_resnet50_fpn(**one_stage)\n    model.head.classification_head = FCOSClassificationHead(\n        in_channels=model.backbone.out_channels,\n        num_anchors=model.anchor_generator.num_anchors_per_location()[0],\n        num_classes=num_classes)\n    return model","outputs":[],"execution_count":2},{"cell_type":"markdown","metadata":{},"source":"# Load the trained model"},{"cell_type":"code","metadata":{"ExecuteTime":{"end_time":"2026-09-24T17:47:08.147192500Z","start_time":"2026-09-24T17:47:07.590572200Z"}},"source":"checkpoint = torch.load(BEST_MODEL_PATH, map_location=DEVICE, weights_only=True)\n# Note: score_threshold is the MODEL one (applied before NMS). The submission threshold is\n#  checkpoint['confidence_threshold'], applied further down in keep_confident.\nmodel = build_detector(checkpoint['head'], image_size=checkpoint['image_size'],\n                       num_classes=NUM_CLASSES, pretrained=False,\n                       max_detections_per_image=checkpoint['max_detections_per_image'],\n                       score_threshold=checkpoint['model_score_threshold'],\n                       nms_threshold=checkpoint['nms_threshold'])\nmodel.load_state_dict(checkpoint['model'])\nmodel.to(DEVICE).eval()\n\nassert checkpoint['image_size'] == IMAGE_SIZE, f\"checkpoint {checkpoint['image_size']} vs {IMAGE_SIZE}\"\nprint(f\"{checkpoint['head']}: epoch {checkpoint['epoch']}, local validation metric \"\n      f\"{checkpoint['metric']:.4f} (scored at confidence {checkpoint['confidence_threshold']})\")","outputs":[],"execution_count":3},{"cell_type":"markdown","metadata":{},"source":"# Create submission.csv"},{"cell_type":"code","metadata":{"ExecuteTime":{"end_time":"2026-09-24T17:47:11.283930200Z","start_time":"2026-09-24T17:47:08.148194100Z"}},"source":"@torch.inference_mode()\ndef predict_test(model, loader, device=DEVICE):\n    model.eval()\n    image_ids, predictions = [], []\n    for images, targets in loader:\n        with torch.autocast(device, dtype=torch.bfloat16):\n            outputs = model([image.to(device, non_blocking=True) for image in images])\n        predictions.extend({'boxes': output['boxes'].float().cpu(),\n                            'scores': output['scores'].float().cpu()} for output in outputs)\n        image_ids.extend(target['image_id'] for target in targets)\n    return image_ids, predictions\n\n\nsample_submission = pd.read_csv(INPUT_DIR / 'sample_submission.csv')\ntest_dataset = ImageDataset(sample_submission, image_dir=INPUT_DIR / 'test',\n                            transform=build_validation_transform(image_size=IMAGE_SIZE), has_label=False)\ntest_loader = DataLoader(test_dataset, batch_size=BATCH_SIZE, shuffle=False, drop_last=False,\n                         collate_fn=detection_collate, num_workers=NUM_WORKERS, pin_memory=True)\n\nimage_ids, predictions = predict_test(model, test_loader)\nprint(f'{len(image_ids)} test images, '\n      f'{sum(len(prediction[\"boxes\"]) for prediction in predictions)} boxes above {checkpoint['model_score_threshold']}')\n\n\ndef format_prediction_string(boxes, confidences, image_size=IMAGE_SIZE):\n    boxes = boxes.clamp(min=0, max=image_size)\n    parts = []\n    for (x_min, y_min, x_max, y_max), confidence in zip(boxes.tolist(), confidences.tolist()):\n        parts.append(f'{confidence:.4f} {round(x_min)} {round(y_min)} '\n                     f'{round(x_max - x_min)} {round(y_max - y_min)}')\n    return ' '.join(parts)\n\n\ndef keep_confident(prediction, threshold):\n    keep = prediction['scores'] >= threshold\n    return prediction['boxes'][keep], prediction['scores'][keep]\n\n\nsubmission = pd.DataFrame({\n    'image_id': image_ids,\n    # Note: as per competition format\n    'PredictionString': [format_prediction_string(*keep_confident(prediction, threshold=checkpoint['confidence_threshold']))\n                         for prediction in predictions],\n})\nsubmission.to_csv(SUBMISSION_PATH, index=False)","outputs":[],"execution_count":4},{"cell_type":"markdown","metadata":{},"source":"## Sanity checks\noptional sanity checks for proper submission csv file formatting"},{"cell_type":"code","metadata":{"ExecuteTime":{"end_time":"2026-09-24T17:49:04.921501400Z","start_time":"2026-09-24T17:49:04.907121800Z"}},"source":"if RUN_SANITY_CHECKS:\n    written = pd.read_csv(SUBMISSION_PATH, keep_default_na=False)\n\n    assert list(written.columns) == ['image_id', 'PredictionString'], written.columns.tolist()\n    assert written['image_id'].tolist() == sample_submission['image_id'].tolist(), 'image_id set or order changed'\n\n    max_difference, total_boxes = 0.0, 0\n    for (_, row), prediction in zip(written.iterrows(), predictions):\n        boxes, confidences = keep_confident(prediction, threshold=checkpoint['confidence_threshold'])\n        expected = boxes.clamp(min=0, max=IMAGE_SIZE).numpy()\n        total_boxes += len(boxes)\n\n        tokens = row['PredictionString'].split()\n        assert len(tokens) % 5 == 0, f\"{row['image_id']}: {len(tokens)} tokens is not a multiple of 5\"\n        assert len(tokens) // 5 == len(boxes), f\"{row['image_id']}: {len(tokens) // 5} written, {len(boxes)} predicted\"\n        if not tokens:\n            continue\n\n        values = np.asarray(tokens, dtype='float64').reshape(-1, 5)\n        written_confidences, x, y, width, height = values.T\n        assert ((written_confidences >= 0) & (written_confidences <= 1)).all(), 'confidence outside [0, 1]'\n        assert (np.diff(written_confidences) <= 0).all(), f\"{row['image_id']}: confidences not descending\"\n        assert (width > 0).all() and (height > 0).all(), f\"{row['image_id']}: non-positive width or height\"\n\n        reconstructed = np.stack([x, y, x + width, y + height], axis=1)\n        max_difference = max(max_difference, float(np.abs(reconstructed - expected).max()))\n\n    assert max_difference <= 1.0, f'round trip differs by {max_difference:.2f}px - the xywh conversion is wrong'\n    print(f'round trip max difference {max_difference:.2f}px (rounding only)')\n    print(f'{total_boxes} boxes over {len(written)} images = {total_boxes / len(written):.1f} per image, '\n          f'against 43.2 per image in the training data')\n    print(f'empty rows: {int((written[\"PredictionString\"] == \"\").sum())}')\n","outputs":[],"execution_count":6}]}