{"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":19989,"databundleVersionId":1160143,"sourceType":"competition"}],"dockerImageVersionId":31089,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"# ===========================\n# БЛОК 0. Импорты, среда, утилиты логирования\n# ===========================\nimport os, sys, re, ast, json, time, random\nfrom datetime import datetime\nfrom pathlib import Path\n\nimport numpy as np\nimport pandas as pd\nfrom PIL import Image, ImageDraw, ImageFont\n\nimport torch\nfrom torch.utils.data import Dataset, DataLoader\nfrom tqdm.auto import tqdm\n\nfrom transformers import (\n    AutoImageProcessor,\n    AutoModelForObjectDetection,\n    TrainingArguments,\n    Trainer,\n)\nimport datasets as hf_datasets  # только чтобы отключить кэш\n\n# ---- Настройки окружения для стабильности ----\nos.environ[\"TOKENIZERS_PARALLELISM\"] = \"false\"\nos.environ[\"HF_HUB_DISABLE_SYMLINKS_WARNING\"] = \"1\"\nhf_datasets.disable_caching()  # чтобы Datasets не лезли в кэш\n\n# детерминированность\nSEED = int(os.environ.get(\"SEED\", \"42\"))\nrandom.seed(SEED); np.random.seed(SEED); torch.manual_seed(SEED)\ntorch.backends.cudnn.benchmark = False\ntorch.use_deterministic_algorithms(False)\n\nDEVICE = \"cuda\" if torch.cuda.is_available() else \"cpu\"\n\ndef log(msg):\n    print(f\"[{datetime.now().strftime('%H:%M:%S')}] {msg}\", flush=True)\n\nlog(\"Старт. Проверка версий и устройства…\")\nlog(f\"Python: {sys.version.split()[0]}\")\nlog(f\"PyTorch: {torch.__version__} | CUDA: {torch.version.cuda} | Device: {DEVICE}\")","metadata":{"trusted":true,"execution":{"execution_failed":"2025-08-30T06:07:51.327Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ===========================\n# БЛОК 1. Пути, константы, sanity-check\n# ===========================\nDATA_DIR = Path(\"/kaggle/input/global-wheat-detection\")\nCSV_PATH = DATA_DIR / \"train.csv\"\nIMG_DIR_CANDIDATES = [\n    DATA_DIR / \"train\",\n    DATA_DIR / \"train_images\",\n    DATA_DIR / \"images\",\n]\n\nMODEL_ID = \"microsoft/conditional-detr-resnet-50\"\nID2LABEL = {0: \"wheat_head\"}\nLABEL2ID = {v: k for k, v in ID2LABEL.items()}\n\nIMAGE_SIZE = int(os.environ.get(\"IMAGE_SIZE\", \"640\"))\nDEBUG_MAX_IMAGES = int(os.environ.get(\"DEBUG_MAX_IMAGES\", \"800\"))   # ограничение на кол-во изображений\nTRAIN_FRAC = float(os.environ.get(\"TRAIN_FRAC\", \"0.85\"))\n\nlog(f\"DATA_DIR = {DATA_DIR}\")\nassert CSV_PATH.exists(), f\"Не найден CSV: {CSV_PATH}\"\n\nIMG_DIR = None\nfor cand in IMG_DIR_CANDIDATES:\n    if cand.exists():\n        IMG_DIR = cand\n        break\nassert IMG_DIR is not None, \"Не найдена папка с изображениями (train/train_images/images)\"\nlog(f\"IMG_DIR = {IMG_DIR}\")","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ===========================\n# БЛОК 2. Загрузка train.csv, парсинг bbox, сбор записей\n# ===========================\ndf = pd.read_csv(CSV_PATH)\nneeded_cols = {\"image_id\", \"bbox\"}\nassert needed_cols.issubset(df.columns), f\"В CSV нет нужных колонок: {needed_cols - set(df.columns)}\"\nlog(f\"CSV считан: {len(df)} строк\")\n\n# Сгруппируем боксы по изображению\ngrouped = df.groupby(\"image_id\")[\"bbox\"].apply(list).reset_index()\nlog(f\"Сгруппированных изображений: {len(grouped)}\")\n\ndef parse_bbox_str(bbox_str):\n    # bbox хранится как строка вида \"[x, y, w, h]\"\n    # используем `ast.literal_eval` безопасно\n    arr = ast.literal_eval(bbox_str)\n    if len(arr) != 4:\n        raise ValueError(\"bbox не из 4 элементов\")\n    x, y, w, h = map(float, arr)\n    return x, y, w, h\n\nrecords = []\nskipped_boxes = 0\nkept_boxes = 0\nmissing_images = 0\n\nlog(\"Сбор записей (с прогрессом и проверками)…\")\nfor _, row in tqdm(grouped.iterrows(), total=len(grouped)):\n    image_id = row[\"image_id\"]\n    img_path = IMG_DIR / f\"{image_id}.jpg\"\n    if not img_path.exists():\n        # на всякий случай попробуем .png\n        alt = IMG_DIR / f\"{image_id}.png\"\n        if alt.exists(): img_path = alt\n    if not img_path.exists():\n        missing_images += 1\n        continue\n\n    try:\n        with Image.open(img_path) as im:\n            W, H = im.size\n    except Exception as e:\n        log(f\"⚠️ Не читается {img_path}: {e}\")\n        continue\n\n    bboxes_xywh = []\n    cats = []\n    for b in row[\"bbox\"]:\n        try:\n            x, y, w, h = parse_bbox_str(b)\n        except Exception:\n            skipped_boxes += 1\n            continue\n\n        # клиппинг в границы\n        x = max(0.0, min(x, W - 1))\n        y = max(0.0, min(y, H - 1))\n        w = max(0.0, min(w, W - x))\n        h = max(0.0, min(h, H - y))\n\n        if w <= 1e-6 or h <= 1e-6:\n            skipped_boxes += 1\n            continue\n\n        bboxes_xywh.append([x, y, w, h])\n        cats.append(0)  # один класс\n\n    if len(bboxes_xywh) == 0:\n        # можно пропустить изображения без валидных боксов\n        continue\n\n    kept_boxes += len(bboxes_xywh)\n    records.append({\n        \"image_id\": image_id,\n        \"image_path\": str(img_path),\n        \"width\": W,\n        \"height\": H,\n        \"bboxes\": bboxes_xywh,\n        \"labels\": cats\n    })\n\nlog(f\"Итого записей: {len(records)} | kept_boxes={kept_boxes} | skipped_boxes={skipped_boxes} | missing_images={missing_images}\")\nassert len(records) > 0, \"После фильтрации записей не осталось\"\n\n# Ограничим для отладки\nif DEBUG_MAX_IMAGES and len(records) > DEBUG_MAX_IMAGES:\n    records = records[:DEBUG_MAX_IMAGES]\n    log(f\"DEBUG: Ограничили до {len(records)} изображений\")","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ===========================\n# БЛОК 3. Трен/вал сплит + sanity-checks\n# ===========================\nrng = np.random.default_rng(SEED)\nidx = np.arange(len(records))\nrng.shuffle(idx)\ncut = int(TRAIN_FRAC * len(records))\ntrain_idx, val_idx = idx[:cut], idx[cut:]\n\ntrain_recs = [records[i] for i in train_idx]\nval_recs   = [records[i] for i in val_idx]\n\nlog(f\"Train: {len(train_recs)} | Val: {len(val_recs)}\")\nassert len(train_recs) > 0 and len(val_recs) > 0, \"Проверьте размеры сплита\"","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ===========================\n# БЛОК 4. Процессор изображений DETR + проверка на одном примере\n# ===========================\nlog(\"Загружаем процессор изображений (AutoImageProcessor)…\")\nimage_processor = AutoImageProcessor.from_pretrained(\n    MODEL_ID,\n    do_resize=True, size={\"shortest_edge\": IMAGE_SIZE},  # корректно для DETR\n    do_pad=True, pad_size={\"height\": IMAGE_SIZE, \"width\": IMAGE_SIZE},\n)\nlog(f\"Processor OK. Формат ожидания: {image_processor}\")\n\n# Быстрый тест на одном изображении\nwith Image.open(train_recs[0][\"image_path\"]) as test_im:\n    test_ann = {\n        \"image_id\": 0,\n        \"annotations\": [\n            {\"bbox\": bbox, \"category_id\": 0} for bbox in train_recs[0][\"bboxes\"]\n        ],\n    }\n    enc = image_processor(images=test_im, annotations=test_ann, return_tensors=\"pt\")\n    log(f\"Sanity processor → pixel_values: {tuple(enc['pixel_values'].shape)}; labels-keys: {list(enc['labels'][0].keys())}\")","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ===========================\n# БЛОК 5. Кастомный Dataset + collate_fn + dry-run\n# ===========================\nclass WheatDataset(Dataset):\n    def __init__(self, recs, image_processor):\n        self.recs = recs\n        self.proc = image_processor\n\n    def __len__(self):\n        return len(self.recs)\n\n    def __getitem__(self, i):\n        r = self.recs[i]\n        with Image.open(r[\"image_path\"]).convert(\"RGB\") as im:\n            ann = {\n                \"image_id\": i,  # важно быть int\n                \"annotations\": [\n                    {\"bbox\": bbox, \"category_id\": 0} for bbox in r[\"bboxes\"]\n                ],\n            }\n            enc = self.proc(images=im, annotations=ann, return_tensors=\"pt\")\n            # enc['pixel_values']: (1, 3, H, W); enc['labels']: list из 1 элемента\n            item = {\n                \"pixel_values\": enc[\"pixel_values\"].squeeze(0),  # -> (3,H,W)\n                \"labels\": enc[\"labels\"][0],                      # dict: boxes, class_labels\n                \"orig_size\": (r[\"height\"], r[\"width\"]),\n                \"image_path\": r[\"image_path\"],\n                \"image_id\": r[\"image_id\"],\n            }\n            return item\n\ndef collate_fn(batch):\n    pixel_values = torch.stack([b[\"pixel_values\"] for b in batch])  # (B,3,H,W)\n    labels = [{\"boxes\": b[\"labels\"][\"boxes\"], \"class_labels\": b[\"labels\"][\"class_labels\"]} for b in batch]\n    return {\"pixel_values\": pixel_values, \"labels\": labels}\n\ntrain_ds = WheatDataset(train_recs, image_processor)\nval_ds   = WheatDataset(val_recs,   image_processor)\n\n# Dry-run DataLoader (важно для отладки зависаний)\nlog(\"Dry-run DataLoader (2 батча)…\")\ndl = DataLoader(train_ds, batch_size=2, shuffle=True, num_workers=0, collate_fn=collate_fn)\nfor step, batch in enumerate(dl):\n    log(f\"Batch {step}: pixel_values={tuple(batch['pixel_values'].shape)}; labels={len(batch['labels'])}\")\n    if step >= 1: break","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ===========================\n# БЛОК 6. Модель (та же), загрузка и проверка\n# ===========================\nlog(\"Грузим модель AutoModelForObjectDetection…\")\nmodel = AutoModelForObjectDetection.from_pretrained(\n    MODEL_ID,\n    ignore_mismatched_sizes=True,\n    id2label=ID2LABEL,\n    label2id=LABEL2ID,\n)\nmodel.to(DEVICE)\nlog(\"Модель загружена.\")\n\n# Быстрый forward на одном батче (поймать shape/NaN)\nbatch = next(iter(DataLoader(train_ds, batch_size=2, num_workers=0, collate_fn=collate_fn)))\nwith torch.no_grad():\n    out = model(pixel_values=batch[\"pixel_values\"].to(DEVICE), labels=[\n        {\"boxes\": x[\"boxes\"].to(DEVICE), \"class_labels\": x[\"class_labels\"].to(DEVICE)} for x in batch[\"labels\"]\n    ])\nlog(f\"Sanity forward → loss={float(out.loss):.4f}\")","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ===========================\n# БЛОК 7. Аргументы тренировки + Trainer (без многопроцессности)\n# ===========================\nMAX_STEPS = int(os.environ.get(\"MAX_STEPS\", \"300\"))   # безопасно маленькое значение\nPER_DEVICE_BS = int(os.environ.get(\"PER_DEVICE_BS\", \"2\"))\nLR = float(os.environ.get(\"LR\", \"5e-5\"))\n\nargs = TrainingArguments(\n    output_dir=\"/kaggle/working/wheat-cond-detr\",\n    per_device_train_batch_size=PER_DEVICE_BS,\n    per_device_eval_batch_size=PER_DEVICE_BS,\n    dataloader_num_workers=0,              # критично: без воркеров\n    remove_unused_columns=False,           # для object detection\n    learning_rate=LR,\n    weight_decay=5e-4,\n    max_steps=MAX_STEPS,                   # вместо num_train_epochs\n    logging_steps=10,\n    eval_strategy=\"no\",                    # для стабильности; оценку делаем вручную ниже\n    save_strategy=\"no\",\n    report_to=[],                          # без W&B\n    fp16=torch.cuda.is_available(),        # ускорение, если поддерживается\n    lr_scheduler_type=\"cosine\",\n    gradient_accumulation_steps=1,\n    seed=SEED,\n)\n\ndef hf_collator(features):\n    # Trainer свой collator — используем наш\n    return collate_fn(features)\n\ntrainer = Trainer(\n    model=model,\n    args=args,\n    train_dataset=train_ds,\n    eval_dataset=None,   # оценим вручную\n    data_collator=hf_collator,\n)\n\nlog(\"Старт trainer.train()… (пара минут макс. при MAX_STEPS=300)\")\ntrain_out = trainer.train()\nlog(f\"Тренировка завершена. Итоговый global_step={trainer.state.global_step}, loss={train_out.training_loss:.4f}\")","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ===========================\n# БЛОК 8. Быстрая ручная оценка mAP на валидации (torchmetrics)\n# ===========================\nfrom torchmetrics.detection.mean_ap import MeanAveragePrecision\n\ndef xywh_to_xyxy(xywh):\n    x, y, w, h = xywh.T\n    xyxy = np.stack([x, y, x+w, y+h], axis=1)\n    return xyxy\n\n@torch.no_grad()\ndef evaluate_map(model, dataset, image_processor, max_batches=50, score_thresh=0.05):\n    model.eval()\n    metric = MeanAveragePrecision(iou_type=\"bbox\")\n    dl = DataLoader(dataset, batch_size=2, shuffle=False, num_workers=0, collate_fn=collate_fn)\n\n    n_batches = 0\n    for batch in tqdm(dl, total=min(max_batches, len(dl)), desc=\"Eval\"):\n        outputs = model(pixel_values=batch[\"pixel_values\"].to(DEVICE))\n        # post-process в исходные размеры\n        target_sizes = torch.tensor([list(b[\"orig_size\"]) for b in batch], device=DEVICE)\n        results = image_processor.post_process_object_detection(outputs, threshold=score_thresh, target_sizes=target_sizes)\n\n        preds, gts = [], []\n        for i, res in enumerate(results):\n            # предсказания\n            boxes = res[\"boxes\"].cpu()  # xyxy\n            scores = res[\"scores\"].cpu()\n            labels = res[\"labels\"].cpu()\n\n            preds.append({\n                \"boxes\": boxes,\n                \"scores\": scores,\n                \"labels\": labels,\n            })\n\n            # таргеты (переведём xywh -> xyxy)\n            # Возьмём их из batch['labels'] НЕпосле процессора (они уже в координатах до ресайза)\n            # Мы их не держим отдельно; поэтому возьмём из исходных записей dataset.recs\n            # Сопоставим индекс по батчу → глобальный индекс через image_path\n            rec = next(r for r in dataset.recs if r[\"image_path\"] == batch[\"image_path\"][i] if \"image_path\" in batch else True)  # fallback\n            # Но проще: мы уже сохранили в batch[\"labels\"] xyxy под процессором? Нет, там нормированные под processor размеры.\n            # Поэтому сделаем аккуратно: возьмём из dataset.recs исходные xywh.\n            # У нас есть batch[\"image_path\"] → добавим его в batch в Dataset.\n        # Исправим стратегию: в Dataset мы уже кладём orig_size, а боксы передавали только в processor.\n        # Значит, легче прямо сейчас получить GT из dataset по image_id:\n\n        break  # выходим, чтобы аккуратно переписать цикл с корректными GT\n\n    # Перепишем оценку с явной индексацией датасета по DataLoader'у.\n\n@torch.no_grad()\ndef evaluate_map(model, dataset, image_processor, max_images=100, score_thresh=0.05):\n    model.eval()\n    metric = MeanAveragePrecision(iou_type=\"bbox\")\n\n    # Возьмём первые N изображений из вал-датасета\n    N = min(max_images, len(dataset))\n    idxs = list(range(N))\n    for i in tqdm(idxs, desc=\"Eval img-by-img\"):\n        item = dataset[i]\n        pv = item[\"pixel_values\"].unsqueeze(0).to(DEVICE)\n        out = model(pixel_values=pv)\n\n        target_sizes = torch.tensor([list(item[\"orig_size\"])], device=DEVICE)\n        res = image_processor.post_process_object_detection(out, threshold=score_thresh, target_sizes=target_sizes)[0]\n\n        # preds\n        pred = {\n            \"boxes\": res[\"boxes\"].cpu(),\n            \"scores\": res[\"scores\"].cpu(),\n            \"labels\": res[\"labels\"].cpu(),\n        }\n\n        # gts (из исходного набора записей)\n        gt_xywh = np.array(dataset.recs[i][\"bboxes\"], dtype=np.float32)\n        gt_xyxy = torch.tensor(xywh_to_xyxy(gt_xywh), dtype=torch.float32)\n        gt = {\n            \"boxes\": gt_xyxy,\n            \"labels\": torch.tensor([0]*len(gt_xyxy), dtype=torch.int64),\n        }\n\n        metric.update([pred], [gt])\n\n        if (i+1) % 10 == 0:\n            inter = metric.compute()\n            log(f\"  ↳ промежуточно @ {i+1} изображений: mAP50={inter['map_50']:.4f}, mAP={inter['map']:.4f}\")\n\n    final = metric.compute()\n    log(f\"VAL mAP50={final['map_50']:.4f} | mAP={final['map']:.4f} | map_small={final.get('map_small', torch.nan):.4f}\")\n    return final\n\nlog(\"Оценка на валидации (до 100 изображений)…\")\n_ = evaluate_map(model, val_ds, image_processor, max_images=100, score_thresh=0.1)","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ===========================\n# БЛОК 9. Визуализация одного примера (GT vs Pred)\n# ===========================\ndef draw_boxes(image_path, gt_xywh, pred_xyxy, pred_scores, score_thr=0.5, out_path=\"/kaggle/working/pred_example.jpg\"):\n    im = Image.open(image_path).convert(\"RGB\")\n    draw = ImageDraw.Draw(im)\n    # GT (зелёный)\n    for (x, y, w, h) in gt_xywh:\n        draw.rectangle([x, y, x+w, y+h], outline=(0,255,0), width=2)\n    # Pred (красный)\n    for (x1,y1,x2,y2), s in zip(pred_xyxy, pred_scores):\n        if s < score_thr: continue\n        draw.rectangle([x1,y1,x2,y2], outline=(255,0,0), width=2)\n        draw.text((x1, max(0, y1-12)), f\"{s:.2f}\", fill=(255,0,0))\n    im.save(out_path)\n    return out_path\n\n# возьмём произвольное вал-изображение\ni = 0 if len(val_ds)==0 else min(0, len(val_ds)-1)\nitem = val_ds[i]\nwith torch.no_grad():\n    out = model(pixel_values=item[\"pixel_values\"].unsqueeze(0).to(DEVICE))\nres = image_processor.post_process_object_detection(out, threshold=0.1, target_sizes=torch.tensor([list(item[\"orig_size\"])], device=DEVICE))[0]\n\npred_xyxy = res[\"boxes\"].cpu().numpy()\npred_scores = res[\"scores\"].cpu().numpy()\ngt_xywh = np.array(val_ds.recs[i][\"bboxes\"])\n\nout_path = draw_boxes(val_ds.recs[i][\"image_path\"], gt_xywh, pred_xyxy, pred_scores, score_thr=0.5)\nlog(f\"Сохранил визуализацию: {out_path}\")","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}