From 2bd731620c9f21c509f9662c31f89f2205098d34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Trkan?= Date: Fri, 1 May 2026 21:42:32 +0200 Subject: [PATCH] update --- .DS_Store | Bin 6148 -> 8196 bytes 01_dataset_prep.ipynb | 115 ++++++++++--- 02_training.ipynb | 90 ++++++++-- 03_inference_tiles.ipynb | 71 +++++++- 04_map_assembly.ipynb | 344 +++++++++++++++++++++++---------------- 5 files changed, 435 insertions(+), 185 deletions(-) diff --git a/.DS_Store b/.DS_Store index c9f9d4604ffbfd14d8813002b38de5dc887c9554..2d1a5e18b439a65e453f87fc21c54721dc7ba5ca 100644 GIT binary patch literal 8196 zcmeHM&u<$=6n^8T$(oRMlg1?#Ag$^PQlpYz1yvkyom40SDN!6sqNKRH_Qot-&un+s zNkbILcQ|q3#)Ursi5s`_2f&dl2mS#roXa;q?Ai`NoT^rxk!IfW%=_M(_jzZ=vnC=| z?KBsO=7`8amC~I=^Djk?%X+31OwT1q20W2ZIci2Bq%D>K%YbFTGGH0747`X8;LMiA zIp^HhqqemSSO#862IT(Wp-NdXva6-qI?$*q0OT0DRe_FsfWml2R*dXwsi5$wZx75s znYm&x4ac}A%^@pBcD2-SVj51&L}uoO!c=tNJY`O-qNQyu1D1ia49L5Ci56&)>eL*% zzt<_D6gT;VZc>Eym^cONv1kOb2!N6ct%`c{sVdNjrr~J`o^l|4pt!27|M2kBh=)0N zScV6QJwS9h15ORJ(rZL9wCsF;g>=N9%xwkOHs+H+%C{5p{Y0Ar_&Pq{lgjP;f!vC; zN}tdj`jpn_E^uvX4zt0&9f>{GV|3V#WX^-0`*y)mmKb(_4w5*^@`Z)pS>eLi#qkMe z!kKjL*S-)(wcO7;S=CSXXdrzk$|IC~6#HfP65Ywe$9XOK3PUp^fUU6k`LnL=;{-Od z&pMRTkJ2#Yd#yBTZX6tBVZJk<@5*S6RK)S7$f7(Fe0N_(&%^dQKM;s0XRwjTq3m9m zW{_tvJG*|G`n(lZkMGBR5XKw4D)L;kcOqmVXRbol62B4gR{6ufG>ye_``}Byq^d0>`KhiJs8~sUt(^EFZuCnXw z2AgNM*!!%)?yxPk#~v`BeW~$<-k1Eg@n;0fp!C1w!&=H?!CUB)_TyL?0#AO3^_2}j z`RqEz2cP_LSaz_k6WNZ~%GESz>#Dur5iBF6wGn{>D|dsO9*^P+@(04;-ZTNr29Rj delta 218 zcmZp1XfcprU|?W$DortDU=RQ@Ie-{MGjUEV6q~50D9Q?w2aCBdxH9N5on z&dA6**+|e-S&$)xA)ld~A&(&kNGAe$@eCylnLu7DLorZ&0Yl|vD*;8t42Eo=Od?RU z2q>Efl#geqVlZT|0IJCXs!E?+BOpI{tBAv9Wzi>08yn=97PE732r>h$2Lb_ZAmIvf f*2co`%#- Zhu, P., Wen, L., Du, D., et al. (2021). Detection and Tracking Meet Drones Challenge. *IEEE TPAMI*." ] }, + { + "cell_type": "markdown", + "id": "md-1d291f", + "metadata": {}, + "source": [ + "## Instalace závislostí\n", + "\n", + "Nainstalujeme potřebné knihovny: `ultralytics` (YOLOv8), `Pillow`, `tqdm`, `pandas` a `matplotlib` pro analýzu a vizualizaci." + ] + }, { "cell_type": "code", "execution_count": 1, @@ -36,9 +46,19 @@ "%pip install ultralytics Pillow tqdm requests pyyaml pandas matplotlib --quiet" ] }, + { + "cell_type": "markdown", + "id": "md-75919d", + "metadata": {}, + "source": [ + "## Stažení VisDrone datasetu\n", + "\n", + "VisDrone2019-DET je rozdělený na tři části (train/val/test). Každá část obsahuje letecké snímky a textové anotace." + ] + }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 1, "metadata": {}, "outputs": [ { @@ -47,13 +67,13 @@ "text": [ "Stahuji train.zip ...\n", "Rozbaluji train.zip ...\n", - " OK → data/visdrone/VisDrone2019-DET-train\n", + " OK data/visdrone/VisDrone2019-DET-train\n", "Stahuji val.zip ...\n", "Rozbaluji val.zip ...\n", - " OK → data/visdrone/VisDrone2019-DET-val\n", + " OK data/visdrone/VisDrone2019-DET-val\n", "Stahuji test.zip ...\n", "Rozbaluji test.zip ...\n", - " OK → data/visdrone/VisDrone2019-DET-test-dev\n", + " OK data/visdrone/VisDrone2019-DET-test-dev\n", "Dataset připraven.\n" ] } @@ -66,7 +86,6 @@ "DATA_DIR = Path(\"data/visdrone\")\n", "DATA_DIR.mkdir(parents=True, exist_ok=True)\n", "\n", - "# Mapování split → název složky (VisDrone používá plné názvy)\n", "SPLIT_DIRS = {\n", " \"train\": DATA_DIR / \"VisDrone2019-DET-train\",\n", " \"val\": DATA_DIR / \"VisDrone2019-DET-val\",\n", @@ -91,11 +110,19 @@ " print(f\"Rozbaluji {zip_path.name} ...\")\n", " with zipfile.ZipFile(zip_path) as zf:\n", " zf.extractall(DATA_DIR)\n", - " print(f\" OK → {split_dir}\")\n", + " print(f\" OK {split_dir}\")\n", "\n", "print(\"Dataset připraven.\")" ] }, + { + "cell_type": "markdown", + "id": "md-c4fe96", + "metadata": {}, + "source": [ + "## Ověření struktury datasetu" + ] + }, { "cell_type": "code", "execution_count": 3, @@ -122,9 +149,19 @@ " print(f\"{split}: CHYBÍ ({split_dir})\")" ] }, + { + "cell_type": "markdown", + "id": "md-f2ffb4", + "metadata": {}, + "source": [ + "## Formát anotací VisDrone\n", + "\n", + "Každá řádka anotačního souboru obsahuje: `bbox_left, bbox_top, bbox_width, bbox_height, score, category, truncation, occlusion`." + ] + }, { "cell_type": "code", - "execution_count": 4, + "execution_count": null, "metadata": {}, "outputs": [ { @@ -149,8 +186,6 @@ } ], "source": [ - "# Ukázkový anotační soubor\n", - "# Formát: ,,,,,,,\n", "import pandas as pd\n", "\n", "ann_dir = SPLIT_DIRS[\"train\"] / \"annotations\"\n", @@ -162,9 +197,19 @@ "print(\"\\nRozdělení tříd:\", df[\"cat\"].value_counts().to_dict())" ] }, + { + "cell_type": "markdown", + "id": "md-3f6c92", + "metadata": {}, + "source": [ + "## Konverze anotací do formátu YOLO\n", + "\n", + "YOLO používá normalizované souřadnice středu ohraničovacího rámečku (cx, cy, w, h) v rozsahu 0–1. VisDrone má 10 tříd — my vybereme pouze 4 vozidlové: car, van, truck, bus. Pěší a cyklisté jsou pro tuto úlohu irelevantní." + ] + }, { "cell_type": "code", - "execution_count": 5, + "execution_count": null, "metadata": {}, "outputs": [ { @@ -219,12 +264,11 @@ "# 0=ignored, 1=pedestrian, 2=people, 3=bicycle, 4=car,\n", "# 5=van, 6=truck, 7=tricycle, 8=awning-tricycle, 9=bus, 10=motor, 11=others\n", "\n", - "# Přemapování na 4 vozidlové třídy\n", "VISDRONE_TO_YOLO = {\n", - " 4: 0, # car → car\n", - " 5: 1, # van → van\n", - " 6: 2, # truck → truck\n", - " 9: 3, # bus → bus\n", + " 4: 0, # car -> car\n", + " 5: 1, # van -> van\n", + " 6: 2, # truck -> truck\n", + " 9: 3, # bus -> bus\n", "}\n", "CLASS_NAMES = [\"car\", \"van\", \"truck\", \"bus\"]\n", "\n", @@ -278,9 +322,19 @@ "print(\"Konverze hotova!\")" ] }, + { + "cell_type": "markdown", + "id": "md-4b68ce", + "metadata": {}, + "source": [ + "## Konfigurace datasetu (YAML)\n", + "\n", + "YOLOv8 potřebuje YAML soubor popisující cestu k datům a názvy tříd. " + ] + }, { "cell_type": "code", - "execution_count": 6, + "execution_count": null, "metadata": {}, "outputs": [ { @@ -302,7 +356,6 @@ } ], "source": [ - "# Vytvoření YAML konfigurace pro YOLOv8\n", "import yaml\n", "\n", "cfg = {\n", @@ -321,9 +374,19 @@ "print(yaml.dump(cfg, allow_unicode=True))" ] }, + { + "cell_type": "markdown", + "id": "md-a00834", + "metadata": {}, + "source": [ + "## Vizualizace anotací\n", + "\n", + "Vybereme několik trénovacích snímků a zobrazíme jejich anotace — každá třída je vyznačena jinou barvou rámečku." + ] + }, { "cell_type": "code", - "execution_count": 7, + "execution_count": null, "metadata": {}, "outputs": [ { @@ -345,7 +408,6 @@ } ], "source": [ - "# Vizualizace ukázkových anotací\n", "import matplotlib.pyplot as plt\n", "import matplotlib.patches as patches\n", "import random\n", @@ -397,9 +459,17 @@ "show_sample()" ] }, + { + "cell_type": "markdown", + "id": "md-663f41", + "metadata": {}, + "source": [ + "## Statistiky datasetu" + ] + }, { "cell_type": "code", - "execution_count": 8, + "execution_count": null, "metadata": {}, "outputs": [ { @@ -424,7 +494,6 @@ } ], "source": [ - "# Statistiky datasetu\n", "from collections import Counter\n", "\n", "for split in [\"train\", \"val\"]:\n", diff --git a/02_training.ipynb b/02_training.ipynb index 94ba2086..a72640e0 100644 --- a/02_training.ipynb +++ b/02_training.ipynb @@ -12,6 +12,16 @@ "**GPU doporučeno** (trénink na CPU trvá ~10× déle)." ] }, + { + "cell_type": "markdown", + "id": "md-8d392d", + "metadata": {}, + "source": [ + "## Kontrola prostředí\n", + "\n", + "Zjistíme, zda je k dispozici GPU (CUDA) nebo Apple Silicon (MPS), a vypíšeme verzi PyTorche. Trénink na GPU je přibližně 10× rychlejší než na CPU." + ] + }, { "cell_type": "code", "execution_count": null, @@ -43,9 +53,19 @@ " print(\"MPS (Apple Silicon) dostupné\")" ] }, + { + "cell_type": "markdown", + "id": "md-af9159", + "metadata": {}, + "source": [ + "## Načtení předtrénovaného modelu\n", + "\n", + "Použijeme YOLOv8s (small) s váhami předtrénovanými na ImageNet. Tento model budeme fine-tunovat na VisDrone dataset." + ] + }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "metadata": {}, "outputs": [ { @@ -68,12 +88,20 @@ "YAML = Path(\"data/yolo_visdrone/dataset.yaml\")\n", "assert YAML.exists(), f\"Nejprve spusť 01_dataset_prep.ipynb! Chybí: {YAML}\"\n", "\n", - "# Zvolíme YOLOv8s — dobrý poměr přesnosti a rychlosti\n", - "# Alternativy: yolov8n (nejrychlejší), yolov8m (přesnější)\n", - "model = YOLO(\"models/yolov8s.pt\") # stáhne předtrénované ImageNet váhy\n", + "model = YOLO(\"models/yolov8s.pt\") \n", "print(\"Model načten:\", model.info())" ] }, + { + "cell_type": "markdown", + "id": "md-58715d", + "metadata": {}, + "source": [ + "## Trénování modelu\n", + "\n", + "Spustíme trénink na 50 epoch s early stoppingem (patience=10). Augmentace (rotace, překlápění, mosaic) pomáhají modelu generalizovat na různé letecké pohledy a světelné podmínky." + ] + }, { "cell_type": "code", "execution_count": null, @@ -83,7 +111,7 @@ "import torch\n", "from pathlib import Path\n", "\n", - "# Detekce dostupného zařízení\n", + "\n", "if torch.cuda.is_available():\n", " DEVICE = \"cuda\"\n", "elif torch.backends.mps.is_available():\n", @@ -106,22 +134,30 @@ " save_period=10,\n", " val=True,\n", " plots=True,\n", - " # Augmentace pro letecké snímky\n", - " degrees=15.0, # rotace\n", - " flipud=0.5, # vertikální flip\n", + " degrees=15.0,\n", + " flipud=0.5,\n", " fliplr=0.5,\n", " mosaic=1.0,\n", " scale=0.5,\n", ")" ] }, + { + "cell_type": "markdown", + "id": "md-1fc179", + "metadata": {}, + "source": [ + "## Výsledky tréninku\n", + "\n", + "Zobrazíme grafy průběhu trénování (loss, mAP), matici záměn a příklady predikcí na validační sadě." + ] + }, { "cell_type": "code", - "execution_count": 6, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ - "# Zobrazení tréninkových grafů\n", "import matplotlib.pyplot as plt\n", "import matplotlib.image as mpimg\n", "from pathlib import Path\n", @@ -142,11 +178,12 @@ }, { "cell_type": "markdown", + "id": "md-021cf0", "metadata": {}, "source": [ - "## Fine-tuning na Vrchlabí (volitelné)\n", + "## Validace nejlepšího modelu\n", "\n", - "Pokud existuje `dataset_vrchlabi.zip` nebo rozbalená složka `vrchlabi/`, provede se fine-tuning předchozího modelu na vlastních datech z Vrchlabí." + "Načteme nejlepší checkpoint z tréninku a spustíme plnou validaci na validační sadě. Sledujeme metriku mAP50, která je standardem pro detekci objektů." ] }, { @@ -169,6 +206,16 @@ " print(f\" AP50[{cls}]: {ap:.4f}\")" ] }, + { + "cell_type": "markdown", + "id": "md-f05d8c", + "metadata": {}, + "source": [ + "## Fine-tuning na vlastních datech (Vrchlabí)\n", + "\n", + "Pokud je k dispozici lokálně anotovaný dataset z Vrchlabí, provedeme fine-tuning modelu natrénovaného na VisDrone. Tímto krokem model přizpůsobíme konkrétním podmínkám českých měst." + ] + }, { "cell_type": "code", "execution_count": null, @@ -182,7 +229,6 @@ "VRCHLABI_ZIP = Path('dataset_vrchlabi.zip')\n", "VRCHLABI_YAML = Path('vrchlabi/dataset.yaml')\n", "\n", - "# Rozbal zip pokud yaml ještě není\n", "if VRCHLABI_ZIP.exists() and not VRCHLABI_YAML.exists():\n", " print('Rozbaluji dataset_vrchlabi.zip ...')\n", " with zipfile.ZipFile(VRCHLABI_ZIP) as zf:\n", @@ -195,12 +241,11 @@ "else:\n", " print(f'Fine-tuning na: {VRCHLABI_YAML}')\n", "\n", - " # Záloha modelu před fine-tuningem\n", " backup = best_weights.parent / 'best_before_finetune.pt'\n", " shutil.copy(best_weights, backup)\n", " print(f'Záloha uložena: {backup}')\n", "\n", - " vrchlabi_model = YOLO(str(best_weights)) # navazuje na VisDrone trénink\n", + " vrchlabi_model = YOLO(str(best_weights))\n", "\n", " vrchlabi_results = vrchlabi_model.train(\n", " data=str(VRCHLABI_YAML),\n", @@ -218,16 +263,26 @@ " flipud=0.5,\n", " fliplr=0.5,\n", " mosaic=0.5,\n", - " lr0=0.001, # nižší LR pro fine-tuning\n", + " lr0=0.001,\n", " )\n", "\n", " best_weights = Path(vrchlabi_results.save_dir) / 'weights' / 'best.pt'\n", " print(f'Vrchlabí fine-tuning hotov. Nejlepší váhy: {best_weights}')" ] }, + { + "cell_type": "markdown", + "id": "md-805d37", + "metadata": {}, + "source": [ + "## Uložení konfigurace modelu\n", + "\n", + "Cestu k nejlepšímu modelu zapíšeme do `models/model_config.json`, odkud ji načtou následující notebooky." + ] + }, { "cell_type": "code", - "execution_count": 11, + "execution_count": null, "metadata": {}, "outputs": [ { @@ -240,7 +295,6 @@ } ], "source": [ - "# Uložení cesty k nejlepšímu modelu pro další notebooky\n", "import json\n", "\n", "config = {\"best_model\": str(best_weights.resolve())}\n", diff --git a/03_inference_tiles.ipynb b/03_inference_tiles.ipynb index 6badb6b5..5ee10c59 100644 --- a/03_inference_tiles.ipynb +++ b/03_inference_tiles.ipynb @@ -23,6 +23,16 @@ "| bus | modrá |" ] }, + { + "cell_type": "markdown", + "id": "md-83b798", + "metadata": {}, + "source": [ + "## Načtení modelů\n", + "\n", + "Načteme oba modely: `best_finetuned.pt` (po fine-tuningu na Vrchlabí) a `best_before_finetune.pt` (pouze VisDrone). Porovnání obou modelů ukáže, jak moc fine-tuning zlepšil detekci na českých datech." + ] + }, { "cell_type": "code", "execution_count": 1, @@ -59,9 +69,19 @@ "print(f\"Base: {MODEL_BASE}\")" ] }, + { + "cell_type": "markdown", + "id": "md-dc5c89", + "metadata": {}, + "source": [ + "## Konfigurace inference\n", + "\n", + "Nastavíme vstupní a výstupní adresáře, barevné kódování tříd (zelená=car, žlutá=van, červená=truck, modrá=bus), confidence threshold a velikost dávky. Dlaždice jsou 256×256 px odpovídající zoom level 18 mapových dlaždic." + ] + }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "metadata": {}, "outputs": [ { @@ -82,9 +102,6 @@ "ANNOTATED_DIR_FINETUNED.mkdir(exist_ok=True)\n", "ANNOTATED_DIR_BASE.mkdir(exist_ok=True)\n", "\n", - "# Zpětná kompatibilita — hlavní vrstva je finetuned\n", - "ANNOTATED_DIR = ANNOTATED_DIR_FINETUNED\n", - "\n", "# Barvy pro každou třídu (RGB)\n", "CLASS_COLORS = {\n", " 0: (0, 220, 0), # car — zelená\n", @@ -110,9 +127,19 @@ "print(f\"Zařízení: {DEVICE}\")" ] }, + { + "cell_type": "markdown", + "id": "md-452ad1", + "metadata": {}, + "source": [ + "## Inferenční smyčka\n", + "\n", + "Pro každou dlaždici spustíme detekci objektů oběma modely. Detekovaná vozidla vyznačíme barevnými rámečky přímo do obrázku dlaždice a výsledky uložíme ve formátu JSON pro pozdější sestavení mapy." + ] + }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, "metadata": {}, "outputs": [ { @@ -189,7 +216,6 @@ "\n", "\n", "def run_inference(model, annotated_dir: Path, label: str) -> dict:\n", - " \"\"\"Spustí inferenci na všech dlaždicích, uloží anotované obrázky a vrátí dict detekcí.\"\"\"\n", " detections = {}\n", " vehicle_counts = defaultdict(int)\n", "\n", @@ -234,6 +260,16 @@ "all_detections_base = run_inference(model_base, ANNOTATED_DIR_BASE, \"base\")" ] }, + { + "cell_type": "markdown", + "id": "md-3e6ffd", + "metadata": {}, + "source": [ + "## Uložení výsledků detekcí\n", + "\n", + "Detekce z obou modelů uložíme do JSON souborů. Každý záznam obsahuje název dlaždice a seznam detekcí s třídou, souřadnicemi rámečku a confidence score." + ] + }, { "cell_type": "code", "execution_count": 4, @@ -265,9 +301,19 @@ "print(f\" detections_base.json ({len(all_detections_base)} dlaždic)\")" ] }, + { + "cell_type": "markdown", + "id": "md-95a7d7", + "metadata": {}, + "source": [ + "## Vizualizace ukázkových dlaždic\n", + "\n", + "Náhodně vybereme 6 dlaždic s detekcemi a zobrazíme je — tak si ověříme kvalitu výsledků finetuned modelu." + ] + }, { "cell_type": "code", - "execution_count": 5, + "execution_count": null, "metadata": {}, "outputs": [ { @@ -298,7 +344,6 @@ "print(f\"[Finetuned] Dlaždice s detekcemi: {len(tiles_with_detections_ft)} / {len(all_detections_finetuned)}\")\n", "print(f\"[Base] Dlaždice s detekcemi: {len(tiles_with_detections_base)} / {len(all_detections_base)}\")\n", "\n", - "# Ukázkové dlaždice — finetuned model\n", "samples = random.sample(tiles_with_detections_ft, min(6, len(tiles_with_detections_ft)))\n", "fig, axes = plt.subplots(2, 3, figsize=(15, 10))\n", "\n", @@ -314,6 +359,16 @@ "plt.show()" ] }, + { + "cell_type": "markdown", + "id": "md-960e2d", + "metadata": {}, + "source": [ + "## Heatmapa hustoty vozidel\n", + "\n", + "Z detekcí sestavíme heatmapu — každý pixel odpovídá jedné dlaždici a jeho barva udává počet detekovaných vozidel. Porovnáme oba modely vedle sebe." + ] + }, { "cell_type": "code", "execution_count": 6, diff --git a/04_map_assembly.ipynb b/04_map_assembly.ipynb index b3f647b6..be192730 100644 --- a/04_map_assembly.ipynb +++ b/04_map_assembly.ipynb @@ -8,14 +8,24 @@ "\n", "Složíme otagované dlaždice (z notebooku 03) do velkých leteckých snímků HK pro **dvě vrstvy**:\n", "\n", - "| Vrstva | Zdroj dlaždic | Výstup (preview) | Výstup (plný) |\n", - "|--------|--------------|-----------------|--------------|\n", - "| Finetuned model | `tiles_annotated_finetuned/` | `hk_finetuned_preview.jpg` | `hk_finetuned_full.jpg` |\n", - "| Base model | `tiles_annotated_base/` | `hk_base_preview.jpg` | `hk_base_full.jpg` |\n", + "| Vrstva | Zdroj dlaždic | Výstup (plný) |\n", + "|--------|--------------|--------------|\n", + "| Finetuned model | `tiles_annotated_finetuned/` | `hk_finetuned_full.jpg` |\n", + "| Base model | `tiles_annotated_base/` | `hk_base_full.jpg` |\n", "\n", "Výsledná mapa: **74 × 69 dlaždic × 256 px = 18 944 × 17 664 px**" ] }, + { + "cell_type": "markdown", + "id": "md-a222d4", + "metadata": {}, + "source": [ + "## Příprava mřížky dlaždic\n", + "\n", + "Názvy dlaždic kódují jejich pozici ve formátu `18_X_Y.jpg` (tile schema XYZ, zoom 18). Z těchto souřadnic odvodíme rozměry výsledné mapy: počet sloupců, řádků a výsledné rozlišení v pixelech." + ] + }, { "cell_type": "code", "execution_count": 1, @@ -46,9 +56,7 @@ " assert d.exists(), f\"Nejprve spusť 03_inference_tiles.ipynb! (chybí {d})\"\n", "\n", "TILE_SIZE = 256\n", - "PREVIEW_SCALE = 4\n", "\n", - "# Rozsah mřížky odvoď z finetuned (obě mají stejné dlaždice)\n", "tile_files = sorted(ANNOTATED_DIR_FINETUNED.glob(\"18_*.jpg\"))\n", "xs, ys = [], []\n", "for f in tile_files:\n", @@ -68,82 +76,49 @@ "print(f\"Odhadovaná velikost jedné mapy (JPEG 88%): ~{W*H*3/1e6/10:.0f} MB\")" ] }, + { + "cell_type": "markdown", + "id": "md-a8c48b", + "metadata": {}, + "source": [ + "## Sestavení map\n", + "\n", + "Pro obě vrstvy (finetuned a base model) složíme dlaždice do jednoho velkého obrázku." + ] + }, { "cell_type": "code", "execution_count": 2, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Dostupná RAM: 6.8 GB\n", - "Potřeba pro jednu plnou mapu: 1.0 GB (dvě: 2.0 GB)\n", - "RAM dostačuje — generuji plné mapy pro obě vrstvy\n" - ] - } - ], - "source": [ - "import psutil\n", - "\n", - "ram_gb = psutil.virtual_memory().available / 1e9\n", - "needed_gb = W * H * 3 / 1e9 # jedna mapa; dvě = 2×\n", - "\n", - "print(f\"Dostupná RAM: {ram_gb:.1f} GB\")\n", - "print(f\"Potřeba pro jednu plnou mapu: {needed_gb:.1f} GB (dvě: {2*needed_gb:.1f} GB)\")\n", - "\n", - "BUILD_FULL = needed_gb <= ram_gb * 0.5 # nech puffer pro druhou mapu\n", - "if BUILD_FULL:\n", - " print(\"RAM dostačuje — generuji plné mapy pro obě vrstvy\")\n", - "else:\n", - " print(\"Nedostatek RAM — generuji pouze preview mapy\")" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "a375151f0f8f4fd5ac94ecb5567c9849", + "model_id": "08bd8aac266e47808fd9018d7788e249", "version_major": 2, "version_minor": 0 }, "text/plain": [ - "Preview finetuned: 0%| | 0/5106 [00:00 \u001b[39m\u001b[32m34\u001b[39m assemble_map(ANNOTATED_DIR_FINETUNED, Path(\"output/hk_finetuned_full.jpg\"),\n\u001b[32m 35\u001b[39m scale=\u001b[32m1\u001b[39m, desc=\u001b[33m\"Plná mapa finetuned\"\u001b[39m)\n\u001b[32m 36\u001b[39m assemble_map(ANNOTATED_DIR_BASE, Path(\"output/hk_base_full.jpg\"),\n\u001b[32m 37\u001b[39m scale=\u001b[32m1\u001b[39m, desc=\u001b[33m\"Plná mapa base\"\u001b[39m)\n", + "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[2]\u001b[39m\u001b[32m, line 26\u001b[39m, in \u001b[36massemble_map\u001b[39m\u001b[34m(annotated_dir, output_path, scale, desc)\u001b[39m\n\u001b[32m 22\u001b[39m canvas.paste(tile, (xi * ts, yi * ts))\n\u001b[32m 23\u001b[39m \u001b[38;5;28;01mexcept\u001b[39;00m Exception:\n\u001b[32m 24\u001b[39m missing += \u001b[32m1\u001b[39m\n\u001b[32m 25\u001b[39m \n\u001b[32m---> \u001b[39m\u001b[32m26\u001b[39m canvas.save(output_path, quality=\u001b[32m88\u001b[39m, optimize=\u001b[38;5;28;01mTrue\u001b[39;00m)\n\u001b[32m 27\u001b[39m print(f\"Uloženo: {output_path} ({pw}×{ph} px)\", end=\u001b[33m\"\"\u001b[39m)\n\u001b[32m 28\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m missing:\n\u001b[32m 29\u001b[39m print(f\" [{missing} chybějících dlaždic]\", end=\u001b[33m\"\"\u001b[39m)\n", + "\u001b[36mFile \u001b[39m\u001b[32m~/workspace/su2-2/.venv/lib/python3.14/site-packages/PIL/Image.py:2713\u001b[39m, in \u001b[36mImage.save\u001b[39m\u001b[34m(self, fp, format, **params)\u001b[39m\n\u001b[32m 2710\u001b[39m fp = cast(IO[\u001b[38;5;28mbytes\u001b[39m], fp)\n\u001b[32m 2712\u001b[39m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[32m-> \u001b[39m\u001b[32m2713\u001b[39m \u001b[43msave_handler\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mfp\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mfilename\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 2714\u001b[39m \u001b[38;5;28;01mexcept\u001b[39;00m \u001b[38;5;167;01mException\u001b[39;00m:\n\u001b[32m 2715\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m open_fp:\n", + "\u001b[36mFile \u001b[39m\u001b[32m~/workspace/su2-2/.venv/lib/python3.14/site-packages/PIL/JpegImagePlugin.py:847\u001b[39m, in \u001b[36m_save\u001b[39m\u001b[34m(im, fp, filename)\u001b[39m\n\u001b[32m 842\u001b[39m \u001b[38;5;28;01melse\u001b[39;00m:\n\u001b[32m 843\u001b[39m \u001b[38;5;66;03m# The EXIF info needs to be written as one block, + APP1, + one spare byte.\u001b[39;00m\n\u001b[32m 844\u001b[39m \u001b[38;5;66;03m# Ensure that our buffer is big enough. Same with the icc_profile block.\u001b[39;00m\n\u001b[32m 845\u001b[39m bufsize = \u001b[38;5;28mmax\u001b[39m(\u001b[38;5;28mlen\u001b[39m(exif) + \u001b[32m5\u001b[39m, \u001b[38;5;28mlen\u001b[39m(extra) + \u001b[32m1\u001b[39m)\n\u001b[32m--> \u001b[39m\u001b[32m847\u001b[39m \u001b[43mImageFile\u001b[49m\u001b[43m.\u001b[49m\u001b[43m_save\u001b[49m\u001b[43m(\u001b[49m\n\u001b[32m 848\u001b[39m \u001b[43m \u001b[49m\u001b[43mim\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mfp\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43m[\u001b[49m\u001b[43mImageFile\u001b[49m\u001b[43m.\u001b[49m\u001b[43m_Tile\u001b[49m\u001b[43m(\u001b[49m\u001b[33;43m\"\u001b[39;49m\u001b[33;43mjpeg\u001b[39;49m\u001b[33;43m\"\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43m(\u001b[49m\u001b[32;43m0\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[32;43m0\u001b[39;49m\u001b[43m)\u001b[49m\u001b[43m \u001b[49m\u001b[43m+\u001b[49m\u001b[43m \u001b[49m\u001b[43mim\u001b[49m\u001b[43m.\u001b[49m\u001b[43msize\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[32;43m0\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mrawmode\u001b[49m\u001b[43m)\u001b[49m\u001b[43m]\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mbufsize\u001b[49m\n\u001b[32m 849\u001b[39m \u001b[43m\u001b[49m\u001b[43m)\u001b[49m\n", + "\u001b[36mFile \u001b[39m\u001b[32m~/workspace/su2-2/.venv/lib/python3.14/site-packages/PIL/ImageFile.py:665\u001b[39m, in \u001b[36m_save\u001b[39m\u001b[34m(im, fp, tile, bufsize)\u001b[39m\n\u001b[32m 663\u001b[39m fh = fp.fileno()\n\u001b[32m 664\u001b[39m fp.flush()\n\u001b[32m--> \u001b[39m\u001b[32m665\u001b[39m \u001b[43m_encode_tile\u001b[49m\u001b[43m(\u001b[49m\u001b[43mim\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mfp\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mtile\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mbufsize\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mfh\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 666\u001b[39m \u001b[38;5;28;01mexcept\u001b[39;00m (\u001b[38;5;167;01mAttributeError\u001b[39;00m, io.UnsupportedOperation) \u001b[38;5;28;01mas\u001b[39;00m exc:\n\u001b[32m 667\u001b[39m _encode_tile(im, fp, tile, bufsize, \u001b[38;5;28;01mNone\u001b[39;00m, exc)\n", + "\u001b[36mFile \u001b[39m\u001b[32m~/workspace/su2-2/.venv/lib/python3.14/site-packages/PIL/ImageFile.py:700\u001b[39m, in \u001b[36m_encode_tile\u001b[39m\u001b[34m(im, fp, tile, bufsize, fh, exc)\u001b[39m\n\u001b[32m 697\u001b[39m \u001b[38;5;28;01melse\u001b[39;00m:\n\u001b[32m 698\u001b[39m \u001b[38;5;66;03m# slight speedup: compress to real file object\u001b[39;00m\n\u001b[32m 699\u001b[39m \u001b[38;5;28;01massert\u001b[39;00m fh \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m\n\u001b[32m--> \u001b[39m\u001b[32m700\u001b[39m errcode = \u001b[43mencoder\u001b[49m\u001b[43m.\u001b[49m\u001b[43mencode_to_file\u001b[49m\u001b[43m(\u001b[49m\u001b[43mfh\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mbufsize\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 701\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m errcode < \u001b[32m0\u001b[39m:\n\u001b[32m 702\u001b[39m \u001b[38;5;28;01mraise\u001b[39;00m _get_oserror(errcode, encoder=\u001b[38;5;28;01mTrue\u001b[39;00m) \u001b[38;5;28;01mfrom\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34;01mexc\u001b[39;00m\n", + "\u001b[31mKeyboardInterrupt\u001b[39m: " ] } ], @@ -181,75 +156,25 @@ " return canvas\n", "\n", "\n", - "# --- Preview mapy (vždy) ---\n", - "preview_ft = assemble_map(ANNOTATED_DIR_FINETUNED, Path(\"output/hk_finetuned_preview.jpg\"),\n", - " scale=PREVIEW_SCALE, desc=\"Preview finetuned\")\n", - "preview_base = assemble_map(ANNOTATED_DIR_BASE, Path(\"output/hk_base_preview.jpg\"),\n", - " scale=PREVIEW_SCALE, desc=\"Preview base\")" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": {}, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "348dac183a834470b930654ef862a4f2", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Plná mapa finetuned: 0%| | 0/5106 [00:00 \u001b[39m\u001b[32m62\u001b[39m _tile_server = http.server.HTTPServer((\u001b[33m\"\"\u001b[39m, PORT), TileHandler)\n\u001b[32m 63\u001b[39m thread = threading.Thread(target=_tile_server.serve_forever, daemon=\u001b[38;5;28;01mTrue\u001b[39;00m)\n\u001b[32m 64\u001b[39m thread.start()\n\u001b[32m 65\u001b[39m \n", + "\u001b[36mFile \u001b[39m\u001b[32m/opt/homebrew/Cellar/python@3.14/3.14.4/Frameworks/Python.framework/Versions/3.14/lib/python3.14/socketserver.py:457\u001b[39m, in \u001b[36mTCPServer.__init__\u001b[39m\u001b[34m(self, server_address, RequestHandlerClass, bind_and_activate)\u001b[39m\n\u001b[32m 455\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m bind_and_activate:\n\u001b[32m 456\u001b[39m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[32m--> \u001b[39m\u001b[32m457\u001b[39m \u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43mserver_bind\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 458\u001b[39m \u001b[38;5;28mself\u001b[39m.server_activate()\n\u001b[32m 459\u001b[39m \u001b[38;5;28;01mexcept\u001b[39;00m:\n", + "\u001b[36mFile \u001b[39m\u001b[32m/opt/homebrew/Cellar/python@3.14/3.14.4/Frameworks/Python.framework/Versions/3.14/lib/python3.14/http/server.py:148\u001b[39m, in \u001b[36mHTTPServer.server_bind\u001b[39m\u001b[34m(self)\u001b[39m\n\u001b[32m 146\u001b[39m \u001b[38;5;28;01mdef\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34mserver_bind\u001b[39m(\u001b[38;5;28mself\u001b[39m):\n\u001b[32m 147\u001b[39m \u001b[38;5;250m \u001b[39m\u001b[33;03m\"\"\"Override server_bind to store the server name.\"\"\"\u001b[39;00m\n\u001b[32m--> \u001b[39m\u001b[32m148\u001b[39m \u001b[43msocketserver\u001b[49m\u001b[43m.\u001b[49m\u001b[43mTCPServer\u001b[49m\u001b[43m.\u001b[49m\u001b[43mserver_bind\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[43m)\u001b[49m\n\u001b[32m 149\u001b[39m host, port = \u001b[38;5;28mself\u001b[39m.server_address[:\u001b[32m2\u001b[39m]\n\u001b[32m 150\u001b[39m \u001b[38;5;28mself\u001b[39m.server_name = socket.getfqdn(host)\n", + "\u001b[36mFile \u001b[39m\u001b[32m/opt/homebrew/Cellar/python@3.14/3.14.4/Frameworks/Python.framework/Versions/3.14/lib/python3.14/socketserver.py:478\u001b[39m, in \u001b[36mTCPServer.server_bind\u001b[39m\u001b[34m(self)\u001b[39m\n\u001b[32m 473\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m (\n\u001b[32m 474\u001b[39m \u001b[38;5;28mself\u001b[39m.allow_reuse_port \u001b[38;5;129;01mand\u001b[39;00m \u001b[38;5;28mhasattr\u001b[39m(socket, \u001b[33m\"\u001b[39m\u001b[33mSO_REUSEPORT\u001b[39m\u001b[33m\"\u001b[39m)\n\u001b[32m 475\u001b[39m \u001b[38;5;129;01mand\u001b[39;00m \u001b[38;5;28mself\u001b[39m.address_family \u001b[38;5;129;01min\u001b[39;00m (socket.AF_INET, socket.AF_INET6)\n\u001b[32m 476\u001b[39m ):\n\u001b[32m 477\u001b[39m \u001b[38;5;28mself\u001b[39m.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, \u001b[32m1\u001b[39m)\n\u001b[32m--> \u001b[39m\u001b[32m478\u001b[39m \u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43msocket\u001b[49m\u001b[43m.\u001b[49m\u001b[43mbind\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43mserver_address\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 479\u001b[39m \u001b[38;5;28mself\u001b[39m.server_address = \u001b[38;5;28mself\u001b[39m.socket.getsockname()\n", + "\u001b[31mOSError\u001b[39m: [Errno 48] Address already in use" + ] + } + ], + "source": [ + "import threading\n", + "import http.server\n", + "import webbrowser\n", + "import re\n", + "from pathlib import Path\n", + "\n", + "PORT = 8765\n", + "ROOT = Path(\".\").parent.resolve()\n", + "\n", + "TILE_DIRS = {\n", + " \"tiles\": ROOT / \"tiles\",\n", + " \"tiles_annotated_finetuned\": ROOT / \"output\" / \"tiles_annotated_finetuned\",\n", + " \"tiles_annotated_base\": ROOT / \"output\" / \"tiles_annotated_base\",\n", + "}\n", + "\n", + "class TileHandler(http.server.BaseHTTPRequestHandler):\n", + " def log_message(self, *args):\n", + " pass # potlač výpisy každého requestu\n", + "\n", + " def do_GET(self):\n", + " path = self.path.split(\"?\")[0]\n", + "\n", + " # /tiles_annotated_finetuned/18/X/Y.jpg → flat file 18_X_Y.jpg\n", + " m = re.match(r\"^/([^/]+)/(\\d+)/(\\d+)/(\\d+)\\.jpg$\", path)\n", + " if m:\n", + " prefix, z, x, y = m.group(1), m.group(2), m.group(3), m.group(4)\n", + " tile_dir = TILE_DIRS.get(prefix)\n", + " if tile_dir:\n", + " tile_file = tile_dir / f\"{z}_{x}_{y}.jpg\"\n", + " if tile_file.exists():\n", + " data = tile_file.read_bytes()\n", + " self.send_response(200)\n", + " self.send_header(\"Content-Type\", \"image/jpeg\")\n", + " self.send_header(\"Content-Length\", str(len(data)))\n", + " self.end_headers()\n", + " self.wfile.write(data)\n", + " return\n", + " self.send_error(404)\n", + " return\n", + "\n", + " # /output/map.html nebo / → static file\n", + " if path == \"/\" or path == \"\":\n", + " path = \"/output/map.html\"\n", + " file_path = ROOT / path.lstrip(\"/\")\n", + " if file_path.exists() and file_path.is_file():\n", + " suffix = file_path.suffix.lower()\n", + " ct = {\"html\": \"text/html\", \"css\": \"text/css\", \"js\": \"application/javascript\"}.get(suffix[1:], \"application/octet-stream\")\n", + " data = file_path.read_bytes()\n", + " self.send_response(200)\n", + " self.send_header(\"Content-Type\", ct)\n", + " self.send_header(\"Content-Length\", str(len(data)))\n", + " self.end_headers()\n", + " self.wfile.write(data)\n", + " else:\n", + " self.send_error(404)\n", + "\n", + "# Zastav předchozí instanci serveru (pokud existuje)\n", + "if \"_tile_server\" in globals() and _tile_server is not None:\n", + " _tile_server.shutdown()\n", + " print(\"Předchozí server zastaven.\")\n", + "\n", + "_tile_server = http.server.HTTPServer((\"\", PORT), TileHandler)\n", + "thread = threading.Thread(target=_tile_server.serve_forever, daemon=True)\n", + "thread.start()\n", + "\n", + "url = f\"http://localhost:{PORT}\"\n", + "print(f\"Server běží na {url}\")\n", + "print(f\"Otevři v prohlížeči: {url}\")\n", + "webbrowser.open(url)\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Server neběží.\n" + ] + } + ], + "source": [ + "# Zastav server\n", + "if \"_tile_server\" in globals() and _tile_server is not None:\n", + " _tile_server.shutdown()\n", + " _tile_server = None\n", + " print(\"Server zastaven.\")\n", + "else:\n", + " print(\"Server neběží.\")\n" + ] } ], "metadata": { @@ -428,4 +500,4 @@ }, "nbformat": 4, "nbformat_minor": 4 -} \ No newline at end of file +}