This commit is contained in:
Lukáš Trkan
2026-05-01 22:06:01 +02:00
parent 2bd731620c
commit e8c1c3f2ee
9336 changed files with 231 additions and 403 deletions

View File

@@ -2,8 +2,8 @@
"""
Anotátor dlaždic pro trénink YOLOv8
Formát: YOLO class cx cy w h (normalizovaný 01)
Anotace se ukládají do data/hk_custom/train/labels/
Obrázky se kopírují do data/hk_custom/train/images/
Anotace se ukládají do data/vrchlabi_custom/train/labels/
Obrázky se kopírují do data/vrchlabi_custom/train/images/
"""
import json, shutil, urllib.parse
from http.server import HTTPServer, BaseHTTPRequestHandler

161
scripts/map.html Normal file
View File

@@ -0,0 +1,161 @@
<!DOCTYPE html>
<html lang="cs">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>HK Aerial — Detekce vozidel</title>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { height: 100%; }
#map { height: 100vh; width: 100%; }
.layer-toggle {
position: absolute;
top: 12px;
right: 12px;
z-index: 1000;
background: white;
border-radius: 6px;
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
overflow: hidden;
font-family: Arial, sans-serif;
font-size: 13px;
}
.layer-toggle button {
display: block;
width: 100%;
padding: 9px 16px;
border: none;
background: white;
cursor: pointer;
text-align: left;
color: #333;
transition: background 0.15s;
}
.layer-toggle button:hover { background: #f0f0f0; }
.layer-toggle button.active { background: #2563eb; color: white; font-weight: bold; }
.layer-toggle .label {
padding: 7px 16px 4px;
font-size: 11px;
text-transform: uppercase;
color: #888;
letter-spacing: 0.05em;
border-bottom: 1px solid #eee;
}
.info-box {
position: absolute;
bottom: 24px;
left: 12px;
z-index: 1000;
background: rgba(255,255,255,0.9);
border-radius: 6px;
padding: 8px 12px;
font-family: Arial, sans-serif;
font-size: 12px;
color: #444;
box-shadow: 0 1px 4px rgba(0,0,0,0.2);
}
.legend {
position: absolute;
bottom: 24px;
right: 12px;
z-index: 1000;
background: rgba(255,255,255,0.9);
border-radius: 6px;
padding: 8px 12px;
font-family: Arial, sans-serif;
font-size: 12px;
color: #444;
box-shadow: 0 1px 4px rgba(0,0,0,0.2);
}
.legend-item { display: flex; align-items: center; gap: 6px; margin: 3px 0; }
.legend-swatch { width: 14px; height: 14px; border-radius: 2px; flex-shrink: 0; }
</style>
</head>
<body>
<div id="map"></div>
<div class="layer-toggle">
<div class="label">Vrstva</div>
<button id="btn-osm" onclick="setLayer('osm')">OSM podklad</button>
<button id="btn-tiles" onclick="setLayer('tiles')">Letecké snímky</button>
<button id="btn-annotated_finetuned" class="active" onclick="setLayer('annotated_finetuned')">Finetuned model</button>
<button id="btn-annotated_base" onclick="setLayer('annotated_base')">Base model</button>
</div>
<div class="info-box">
Leaflet &nbsp;|&nbsp; Zoom 1022 &nbsp;|&nbsp; Dlaždice: 256×256 px, z=18
</div>
<div class="legend">
<div style="font-weight:bold;margin-bottom:4px;">Typ vozidla</div>
<div class="legend-item"><div class="legend-swatch" style="background:#00DC00;"></div> car</div>
<div class="legend-item"><div class="legend-swatch" style="background:#FFDC00;"></div> van</div>
<div class="legend-item"><div class="legend-swatch" style="background:#DC0000;"></div> truck</div>
<div class="legend-item"><div class="legend-swatch" style="background:#0078FF;"></div> bus</div>
</div>
<script>
const base = window.location.origin;
const map = L.map('map', {
center: [50.208, 15.836],
zoom: 16,
minZoom: 10,
maxZoom: 22,
zoomControl: true
});
const layers = {
osm: L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© OpenStreetMap contributors',
maxZoom: 19,
maxNativeZoom: 19
}),
tiles: L.tileLayer(`${base}/tiles/{z}/{x}/{y}.jpg`, {
minNativeZoom: 18,
maxNativeZoom: 18,
maxZoom: 22,
attribution: 'Letecké snímky HK'
}),
annotated_finetuned: L.tileLayer(`${base}/tiles_annotated_finetuned/{z}/{x}/{y}.jpg`, {
minNativeZoom: 18,
maxNativeZoom: 18,
maxZoom: 22,
attribution: 'Anotované snímky — Finetuned'
}),
annotated_base: L.tileLayer(`${base}/tiles_annotated_base/{z}/{x}/{y}.jpg`, {
minNativeZoom: 18,
maxNativeZoom: 18,
maxZoom: 22,
attribution: 'Anotované snímky — Base'
})
};
let current = 'annotated_finetuned';
layers[current].addTo(map);
L.control.scale({ imperial: false }).addTo(map);
function setLayer(name) {
if (name === current) return;
map.removeLayer(layers[current]);
layers[name].addTo(map);
current = name;
document.querySelectorAll('.layer-toggle button').forEach(b => b.classList.remove('active'));
document.getElementById('btn-' + name).classList.add('active');
}
document.addEventListener('keydown', e => {
if (e.key === '1') setLayer('osm');
if (e.key === '2') setLayer('tiles');
if (e.key === '3') setLayer('annotated_finetuned');
if (e.key === '4') setLayer('annotated_base');
});
</script>
</body>
</html>

View File

@@ -30,7 +30,6 @@ SOURCES = [
# Podmíněné zdroje — stejný YOLO formát, stejné třídy
for name, path in [
("hk", "data/hk_custom/train"),
("visdrone", "data/yolo_visdrone/train"),
]:
img_dir = Path(path) / "images"

View File

@@ -1,150 +0,0 @@
#!/usr/bin/env python3
"""Local tile server for HK aerial map. Translates /tiles/{z}/{x}/{y}.jpg -> tiles/18_{x}_{y}.jpg
At zoom < 18: composites z=18 tiles into a single output tile.
At zoom > 18: crops from the parent z=18 tile.
"""
import http.server
import socketserver
import os
import re
import webbrowser
import threading
import io
from pathlib import Path
from functools import lru_cache
PORT = 8080
SCRIPT_DIR = Path(__file__).parent.parent.joinpath("output").resolve()
TRANSPARENT_GIF = (
b'GIF89a\x01\x00\x01\x00\x80\x00\x00\xff\xff\xff\x00\x00\x00'
b'!\xf9\x04\x00\x00\x00\x00\x00,\x00\x00\x00\x00\x01\x00\x01'
b'\x00\x00\x02\x02D\x01\x00;'
)
TILE_PATTERN = re.compile(
r'^/(tiles(?:_annotated(?:_finetuned|_base)?)?)/(\d+)/(\d+)/(\d+)\.(jpg|png|gif)$'
)
try:
from PIL import Image
PIL_AVAILABLE = True
except ImportError:
PIL_AVAILABLE = False
print("WARNING: Pillow not installed — zoom out won't show tiles. Run: pip install Pillow")
@lru_cache(maxsize=512)
def _read_tile(path_str):
"""Cache tile file reads."""
path = Path(path_str)
if path.exists():
return path.read_bytes()
return None
def build_tile(layer_path_str, z, x, y):
"""Return (bytes, content_type) for the requested tile, or (None, None) if empty."""
layer_path = Path(layer_path_str)
z, x, y = int(z), int(x), int(y)
if z == 18:
data = _read_tile(str(layer_path / f'18_{x}_{y}.jpg'))
if data:
return data, 'image/jpeg'
return None, None
if not PIL_AVAILABLE:
return None, None
if z > 18:
dz = z - 18
scale = 1 << dz
x18 = x >> dz
y18 = y >> dz
data = _read_tile(str(layer_path / f'18_{x18}_{y18}.jpg'))
if not data:
return None, None
tile_img = Image.open(io.BytesIO(data))
tile_w = tile_img.width // scale
tile_h = tile_img.height // scale
ox = (x % scale) * tile_w
oy = (y % scale) * tile_h
crop = tile_img.crop((ox, oy, ox + tile_w, oy + tile_h))
out = crop.resize((256, 256), Image.LANCZOS)
buf = io.BytesIO()
out.save(buf, format='JPEG', quality=85)
return buf.getvalue(), 'image/jpeg'
# z < 18: composite multiple z=18 tiles
dz = 18 - z
scale = 1 << dz # number of z=18 tiles per side
tile_px = max(1, 256 // scale) # pixels per z=18 tile in output
result = None
for dx in range(scale):
for dy in range(scale):
x18 = x * scale + dx
y18 = y * scale + dy
data = _read_tile(str(layer_path / f'18_{x18}_{y18}.jpg'))
if data:
if result is None:
result = Image.new('RGB', (256, 256), (0, 0, 0))
sub = Image.open(io.BytesIO(data))
if tile_px < 256:
sub = sub.resize((tile_px, tile_px), Image.LANCZOS)
result.paste(sub, (dx * tile_px, dy * tile_px))
if result is None:
return None, None
buf = io.BytesIO()
result.save(buf, format='JPEG', quality=85)
return buf.getvalue(), 'image/jpeg'
class Handler(http.server.SimpleHTTPRequestHandler):
def do_GET(self):
path = self.path.split('?')[0]
m = TILE_PATTERN.match(path)
if m:
layer, z, x, y, ext = m.groups()
data, ctype = build_tile(str(SCRIPT_DIR / layer), z, x, y)
if data:
self.send_response(200)
self.send_header('Content-Type', ctype)
self.send_header('Content-Length', len(data))
self.send_header('Cache-Control', 'public, max-age=3600')
self.end_headers()
self.wfile.write(data)
else:
self.send_response(200)
self.send_header('Content-Type', 'image/gif')
self.send_header('Content-Length', len(TRANSPARENT_GIF))
self.end_headers()
self.wfile.write(TRANSPARENT_GIF)
return
super().do_GET()
def log_message(self, fmt, *args):
if not TILE_PATTERN.match(self.path.split('?')[0]):
print(f' {self.address_string()} {fmt % args}')
def main():
os.chdir(SCRIPT_DIR )
with socketserver.TCPServer(('', PORT), Handler) as httpd:
httpd.allow_reuse_address = True
url = f'http://localhost:{PORT}/map.html'
print(f'HK Aerial Map → {url}')
print('Press Ctrl+C to stop.\n')
threading.Timer(0.4, webbrowser.open, args=(url,)).start()
try:
httpd.serve_forever()
except KeyboardInterrupt:
print('\nServer stopped.')
if __name__ == '__main__':
main()