#!/usr/bin/env python3
"""
Anotátor dlaždic pro trénink YOLOv8
Formát: YOLO class cx cy w h (normalizovaný 0–1)
Anotace se ukládají do data/hk_custom/train/labels/
Obrázky se kopírují do data/hk_custom/train/images/
"""
import json, shutil, urllib.parse
from http.server import HTTPServer, BaseHTTPRequestHandler
from pathlib import Path
TILES_DIR = Path("tiles_lanov")
OUT_IMG_DIR = Path("data/lanov_custom/train/images")
OUT_LBL_DIR = Path("data/lanov_custom/train/labels")
OUT_IMG_DIR.mkdir(parents=True, exist_ok=True)
OUT_LBL_DIR.mkdir(parents=True, exist_ok=True)
TILE_SIZE = 256
CLASS_NAMES = ["car", "van", "truck", "bus"]
CLASS_COLORS = ["#00dc00", "#ffdc00", "#dc0000", "#0078ff"]
HTML = r"""
Anotátor vozidel
"""
class Handler(BaseHTTPRequestHandler):
def log_message(self, format, *args): pass # potlač logy
def send_json(self, data, code=200):
body = json.dumps(data).encode()
self.send_response(code)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(body)))
self.send_header("Access-Control-Allow-Origin", "*")
self.end_headers()
self.wfile.write(body)
def do_GET(self):
path = urllib.parse.urlparse(self.path)
qs = urllib.parse.parse_qs(path.query)
if path.path == "/":
body = HTML.encode()
self.send_response(200)
self.send_header("Content-Type", "text/html; charset=utf-8")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
elif path.path == "/api/tiles":
filt = qs.get("filter", ["all"])[0]
all_tiles = sorted(p.name for p in TILES_DIR.glob("*.jpg"))
if filt == "annotated":
all_tiles = [t for t in all_tiles if (OUT_LBL_DIR / (Path(t).stem + ".txt")).exists()]
elif filt == "unannotated":
all_tiles = [t for t in all_tiles if not (OUT_LBL_DIR / (Path(t).stem + ".txt")).exists()]
self.send_json(all_tiles)
elif path.path.startswith("/api/ann/"):
name = urllib.parse.unquote(path.path[len("/api/ann/"):])
lbl = OUT_LBL_DIR / (Path(name).stem + ".txt")
if not lbl.exists():
self.send_json([])
else:
anns = []
for line in lbl.read_text().splitlines():
parts = line.strip().split()
if len(parts) == 5:
cls, cx, cy, w, h = int(parts[0]), *map(float, parts[1:])
anns.append({"cls": cls, "cx": cx, "cy": cy, "w": w, "h": h})
self.send_json(anns)
elif path.path.startswith("/tiles/"):
name = urllib.parse.unquote(path.path[len("/tiles/"):])
img_path = TILES_DIR / name
if not img_path.exists():
self.send_response(404); self.end_headers(); return
data = img_path.read_bytes()
self.send_response(200)
self.send_header("Content-Type", "image/jpeg")
self.send_header("Content-Length", str(len(data)))
self.end_headers()
self.wfile.write(data)
else:
self.send_response(404); self.end_headers()
def do_POST(self):
if self.path.startswith("/api/ann/"):
name = urllib.parse.unquote(self.path[len("/api/ann/"):])
length = int(self.headers.get("Content-Length", 0))
body = json.loads(self.rfile.read(length))
# Ulož label
lbl = OUT_LBL_DIR / (Path(name).stem + ".txt")
lines = [f"{a['cls']} {a['cx']:.6f} {a['cy']:.6f} {a['w']:.6f} {a['h']:.6f}" for a in body]
lbl.write_text("\n".join(lines) + ("\n" if lines else ""))
# Zkopíruj obrázek pokud ještě není
dst = OUT_IMG_DIR / name
if not dst.exists():
shutil.copy2(TILES_DIR / name, dst)
self.send_json({"ok": True})
else:
self.send_response(404); self.end_headers()
def do_OPTIONS(self):
self.send_response(200)
self.send_header("Access-Control-Allow-Origin", "*")
self.send_header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
self.send_header("Access-Control-Allow-Headers", "Content-Type")
self.end_headers()
if __name__ == "__main__":
import webbrowser, threading
port = 8765
server = HTTPServer(("127.0.0.1", port), Handler)
print(f"Anotátor běží na http://127.0.0.1:{port}")
print(f"Anotace → {OUT_LBL_DIR}")
print(f"Obrázky → {OUT_IMG_DIR}")
print("Ctrl+C pro zastavení")
threading.Timer(0.5, lambda: webbrowser.open(f"http://127.0.0.1:{port}")).start()
try:
server.serve_forever()
except KeyboardInterrupt:
print("\nZastaveno.")