Files
gta6/docs/superpowers/specs/2026-09-13-beschriftungen-design.md
T
knsoandClaude Opus 5 57ffb5953a docs: Entwurf zum Entfernen der Kartenbeschriftungen
Alle Beschriftungen der Vorlage (Schriftzüge, Marker, Schilder, Symbole,
Straßennamen, Gitternetz) werden vor der Vektorisierung aus dem Klassenbild
entfernt und darunter wiederaufgebaut: Straßen laufen durch, Flächen
schließen sich, angebissene Gebäude werden geschlossen, verdeckte Blockteile
bekommen passende, als ergänzt markierte Gebäude. Ersetzt task_999fed4a.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 16:33:23 +02:00

20 KiB
Raw Blame History

Kartenbeschriftungen entfernen — Entwurf

Datum: 2026-09-13 Zusammenhang: Der Nutzer sah auf der Minimap in J7–L8 den Schriftzug „VICE BEACH (MIAMI BEACH)“ als Straßen- und Stadtflächen-Geometrie und wollte ihn samt dem, was darunter liegen müsste, bereinigt haben. Nach der Bestandsaufnahme entschied er: alle Beschriftungen der Vorlage. Dieser Entwurf ersetzt die Beschriftungs-Aufgabe task_999fed4a, deren Sitzung nie etwas geliefert hat (Arbeitsbaum pensive-bell-3a3d9d unverändert). Baut auf: docs/superpowers/specs/2026-09-12-leonida-vice-slice-design.md (Pipeline), docs/superpowers/specs/2026-09-13-minimap-design.md (Kartenbild), Konventionen aus docs/superpowers/specs/2026-09-13-feinkalibrierung-design.md.

Ziel

Die Pipeline entfernt jede Beschriftung der Vorlage, bevor sie daraus Geometrie macht, und baut auf, was darunter liegen müsste: Straßen und Wege laufen durch, Flächen schließen sich, angebissene Gebäude werden geschlossen, verdeckte Blockteile bekommen passende Gebäude. Weil der Eingriff vor der Vektorisierung sitzt, entstehen Land, Straßen, Mittellinien, Palmen, Höhen, Kollision und Minimap daraus wie bisher und passen zueinander; jeder künftige Lauf behält das Ergebnis.

Befund

Gemessen an der Vorlage (tools/extract/cache, YANIS v15) und den ausgelieferten Daten:

  • 94 Fundstellen, davon 88 heute als Geometrie sichtbar. Sechs im offenen Wasser hat drop_map_labels schon entfernt (VICE CITY (MIAMI), CATALAN BAY (BISCAYNE BAY), (FERRY ISLAND), S2/71, T2/71, fast ganz T2/38/T2/37).
    • 24 Schriftzüge: weiße Füllung (248, 248, 248) mit dunkler Kontur, rote Füllung (248, 112, 112) mit dunkelroter Kontur.
    • Schilder 10, 3, 97, 561, 404, 397; „?“-Kreise; schwarze Piktogramme (Golf-Fähnchen, Tennisschläger, Stern, Haus, Schuh, Kreuz).
    • Marker wie T1/41, T2/65, L1/36, S2/32, A2/05: kräftige Farben außerhalb der Kartenpalette mit Nadel und Zeigerlinie.
    • Straßennamen „Catalan Blvd“, „Shore Dr“, „Shore Ct“: helle Schrift auf der Fahrbahn.
  • Gitternetz alle 500 m: 4 px breite, abgedunkelte Linien bei Pixelzeile und -spalte 498–501, 998–1001 usw. und an den Rändern. Über Stadtfläche wird es als Gebäude klassifiziert, fällt als zu dünn weg und hinterlässt 4 m breite Schlitze in den Stadtflächen.
  • Folgeschäden in L8: Marker S2/32 ergab ein Brückenstück mit Kollision mitten in der Stadt (285 m², Welt x 1911 / y 628), die Marker T2/65 und T2/51 zwei Löcher in der Landplatte (268 m² bei x 1918 / y 593, 172 m² bei x 1912 / y 620).
  • Unter großen Schriftzügen (Maske = Füllung + 7 m): VICE BEACH 10 Straßenenden, kein angeschnittenes Gebäude ≥ 60 m²; CROSSTOWN 9 Straßenenden, 8 angeschnittene Gebäude; STOCKYARD 12 und 2; BAYSIDE 6 und 3; OCEAN BEACH 5 und 2; BOHEMIA 0 und 1.
  • Farbgleiche echte Flächen: rosa Gebäudekomplex D2, rote Parkwege C6 und C11, lachsrote Rechtecke B8 und B9, knallrote Gebäude A10, K10 und L10, rote spekulative Autobahnrampen A12. Eine Farbsuche über die ganze Karte würde sie mit löschen.
  • Keine beschriftungsfreie Vorlage: gtadb führt nur andere Rekonstruktionen, keinen Label-Layer (Ruling 33).
  • Reproduzierbar: run.py erzeugt die ausgelieferten 144 Chunk-Dateien und manifest.json in 23 s Byte für Byte identisch neu.

Entscheidungen des Nutzers

Frage Entscheidung
Umfang Alle Beschriftungen: Schriftzüge, Marker, Schilder, Symbole, Straßennamen, Gitternetz — dazu weitere Überreste, die der Nutzer auf der Karte markiert
Verdeckte Blockteile Plausible Gebäude ergänzen, in den Daten als ergänzt markiert
Ansatz Liste aller Stellen, automatische Erkennung und automatischer Wiederaufbau; einzelne Ausreißer später über corrections.toml der Feinkalibrierung
Probe vorab Keine
Knallrote Flächen A10, K10, L10 Echte Gebäude, bleiben stehen
Einbau, Liste und Erkennung · Wiederaufbau · Fehlerfälle und Prüfung Abschnitte 1 bis 3 bestätigt

Einbau in die Pipeline

Kacheln → classify → drop_map_labels → remove_annotations → Farbanteil-Prüfung → Land, Straßen, Gebäude, Höhen, Palmen → Markierung ergänzter Gebäude → Chunks → Minimap

remove_annotations ändert ausschließlich das Klassenbild — welches Pixel Wasser, Land, Park, Sand, Stadtfläche, Gebäude, Straße, Weg oder spekulativ ist. Alles danach bleibt Code von heute. drop_map_labels bleibt als Absicherung für Schrift im offenen Wasser.

Einheit Aufgabe
tools/extract/data/annotations.toml Die Liste. Kommentare erlaubt, Koordinaten in Welt-x und Welt-y (Meter), wie die Minimap sie anzeigt.
tools/extract/leonida/annotations.py Liste lesen und prüfen, Maske je Eintrag bilden, Gitternetz-Maske, remove_annotations(mosaic, labels, annotations) mit Bericht.
tools/extract/leonida/inpaint.py Wiederaufbau auf dem Klassenbild: dünne Masken, Straßen und Wege, Flächen, Gebäude schließen, Gebäude ergänzen.
tools/extract/leonida/coords.py chunk_for_world(x, y) -> (i, j), Spiegel von chunkForWorld in src/world/coords.js; aus dem Feinkalibrierungs-Plan vorgezogen, mit demselben Test.
tools/extract/find_annotations.py --propose: Vorschläge für die Liste. --check: Vollständigkeit. --review: Prüfbilder.
tools/extract/run.py Ruft remove_annotations auf, druckt den Bericht, markiert ergänzte Gebäude, erzeugt am Ende map.png, map-walkable.png und map.json über render_map aus overview.py.

Fehlt annotations.toml oder enthält sie nur Kommentare, gibt es keine Einträge und kein Gitternetz — der Lauf entspricht dann dem heutigen.

Die Liste

[grid]
spacing = 500    # Meter zwischen den Gitterlinien der Vorlage
width   = 6      # Meter, Linie samt Kantenglättung

[[annotation]]
name = "VICE BEACH (MIAMI BEACH)"
kind = "text_white"
box  = [[1350, 870], [1900, 705]]    # zwei gegenüberliegende Ecken

[[annotation]]
name   = "Rest am Marker S2/32"
kind   = "region"
polygon = [[1905, 640], [1925, 640], [1925, 615], [1905, 615]]
reason = "vom Nutzer markiert"

[[keep]]
box    = [[-856, 194], [-794, -1]]
reason = "knallrotes Gebäude in A10, laut Nutzer echt"

Felder

Tabelle Feld Pflicht Bedeutung
[grid] spacing, width beide, wenn die Tabelle da ist Zahlen > 0. Linien liegen auf x0 + k·spacing und y1 − k·spacing des Ausschnitts, Streifenbreite width mittig darauf.
[[annotation]] name ja Text der Beschriftung oder kurze Beschreibung.
kind ja text_white, text_red, marker, symbol, street_name, region.
box ja, bei region box oder polygon Zwei Ecken [x, y], beliebige Reihenfolge, innerhalb des Ausschnitts.
polygon nur bei region ≥ 3 Punkte, gültiges Vieleck.
reason nein Freitext.
[[keep]] box ja Stelle, die wie Beschriftung aussieht, aber echt ist. Wirkt nur auf find_annotations.py --check.
reason ja Warum sie echt ist.

Der Rahmen soll die Beschriftung mit mindestens 8 m Luft umschließen; --propose schlägt Rahmen mit 10 m Luft vor.

Erkennung je Art

Je Eintrag entsteht eine Maske in fünf Schritten:

  1. Kandidatenpixel nach der Farbregel der Art, nur innerhalb des Rahmens.
  2. Zusammenhängende Kandidaten (8er-Nachbarschaft), die den Rahmenrand berühren, fallen weg — sie gehören zu etwas, das über den Rahmen hinausreicht, also zur echten Karte. Rahmenkanten, die auf dem Rand des Ausschnitts liegen, zählen nicht als Rand.
  3. Wachsen um den Rand der Art (Scheibe).
  4. Eingeschlossene Löcher bis 400 m² füllen (Buchstabeninnenräume, Innenraum von Schildern).
  5. Auf den Rahmen beschneiden.
kind Farbregel (RGB der Vorlage) Rand
text_white min(R, G, B) ≥ 236 und max − min ≤ 14 7 m
text_red ΔE zu (248, 112, 112) < 18 — euklidischer Abstand in CIELAB wie in classify.py 7 m
marker HSV-Sättigung ≥ 0,6, Helligkeit ≥ 0,5 (Skala 0–1) und ΔE ≥ 25 zu jeder Palettenfarbe aus classify.py — oder fast schwarz: max(R, G, B) ≤ 45 3 m
symbol wie text_white oder wie marker 3 m
street_name min(R, G, B) ≥ 150 und max − min ≤ 20 1 m
region ganzer Rahmen oder ganzes Vieleck, keine Farbregel, kein Schritt 2 bis 4 0 m

Die Gitternetz-Maske sind die Streifen aus [grid], auf den Ausschnitt beschnitten.

Wiederaufbau

Reihenfolge in inpaint.py:

  1. Gitternetz: Jedes Pixel der Gitternetz-Maske, das in keiner Eintragsmaske liegt, übernimmt die Klasse des nächsten Pixels außerhalb aller Masken, gleich welcher Klasse.
  2. Eintragsmasken: Alle Eintragsmasken werden vereinigt und in zusammenhängende Masken zerlegt; überlappende Einträge landen so in derselben Maske. Je Maske:
    • dünn (größter Abstand eines Maskenpixels zum Maskenrand ≤ 2 m — Straßennamen, Zeigerlinien): wie das Gitternetz.
    • sonst: Straßen und Wege → Flächen → Gebäude schließen → Gebäude ergänzen, jeweils in einem Fenster aus dem Hüllrechteck der Maske plus 60 m.

Straßen und Wege

Je Straßenklasse getrennt, in der Reihenfolge weg, spekulativ, strasse — was später gemalt wird, gewinnt.

  • Messen: Die sichtbaren Pixel der Klasse (außerhalb aller Masken) werden mit _skeleton_graph und _skeleton_paths aus vectors.py zu Mittellinien. Vertraut wird nur Mittellinienpunkten mit mindestens 10 m Abstand zur Maske. Wo ein Pfad in diese 10-m-Zone eintritt, entsteht ein Straßenende: Ort = letzter vertrauter Punkt, Richtung = von dem Punkt 15 m weiter zurück zum Ende (bei kürzerem vertrauten Stück dessen ganze Länge; unter 5 m kein Ende), Halbbreite = Median der Distanztransformation entlang dieser 15 m. Ein Ende zählt nur, wenn der Strahl von dort in seiner Richtung die Maske innerhalb von 20 m erreicht — eine Straße, die bloß an der Maske vorbeiführt, erzeugt keine Enden.
  • Verbinden: Zwei Enden a, b derselben Maske passen, wenn die Richtung jedes Endes höchstens 20° von der Verbindungslinie abweicht, der seitliche Versatz jedes Endes zur Linie des anderen höchstens max(2 · größere Halbbreite, 0,15 · Abstand) beträgt und die Halbbreiten sich höchstens um den Faktor 1,6 unterscheiden. Kosten: Summe der beiden Winkel in Grad plus 100 · Versatz / Abstand. Paare werden aufsteigend nach Kosten vergeben, jedes Ende höchstens einmal. Ein Paar wird als kubische Hermite-Kurve gemalt, die an beiden Enden deren Richtung aufnimmt; die Halbbreite geht linear von a nach b über.
  • Weiterführen: Ein Ende ohne Partner läuft geradeaus. Treffer zählen erst ab dem ersten Maskenpixel — vorher läuft der Strahl noch über die eigene sichtbare Straße. Er endet am ersten dieser Treffer: an der Mittellinie eines gemalten Paares (T-Kreuzung), am Schnittpunkt mit einem anderen weitergeführten Ende (beide enden dort — Ecke ohne Überstand), außerhalb der Maske auf einem Pixel der eigenen Klasse oder strasse (angeschlossen), oder außerhalb der Maske auf einem anderen Pixel — dann am Maskenrand (Sackgasse). Endet es an einer Kreuzung, wird es um die eigene Halbbreite verlängert, damit die Ecke gefüllt ist.
  • Malen: Nur Pixel innerhalb der Maske werden gesetzt.

Flächen

Jedes Maskenpixel, das keine Straße bekommen hat, erhält die Klasse des nächsten Pixels außerhalb aller Masken, dessen Klasse wasser, land, gruen, sand oder stadt ist. Straßen und Gebäude wachsen dabei nicht.

Gebäude schließen

Ein Gebäudestück — zusammenhängende gebaeude-Pixel außerhalb aller Masken, ≥ 12 m², mit mindestens 3 Pixeln direkt an der Maske — wird geschlossen: Maskenpixel innerhalb seines gedrehten Hüllrechtecks (minimum_rotated_rectangle) werden gebaeude, außer sie sind schon Straße. Das Gebäude wird nie über sein Hüllrechteck hinaus verlängert. Rote spekulative Stücke werden nicht geschlossen, weil spekulativ zugleich Straßenklasse ist.

Gebäude ergänzen

  • Bebaubar sind Maskenpixel mit der Klasse stadt nach dem Flächenschritt, mindestens den Straßenabstand (siehe Vorlage) von jeder Straße und mindestens 3 m von jedem Gebäude entfernt — auch von gerade erst gesetzten.
  • Blöcke sind die zusammenhängenden Flächen (4er-Nachbarschaft) im Fenster, die weder Straße, Weg, spekulativ noch Wasser sind.
  • Nachbarschaft sind die Gebäudestücke außerhalb aller Masken, die höchstens 150 m von der Maske entfernt liegen. Gibt es weniger als drei, wird in dieser Maske nichts ergänzt; der Bericht nennt das.
  • Vorlage aus der Nachbarschaft: kurze Seite = Median der kurzen Hüllrechteckseiten, auf 6–40 m begrenzt; lange Seite = Median der langen, auf kurze Seite bis 60 m begrenzt; Straßenabstand = Median des Abstands der Gebäudepixel am Umriss zur nächsten Straße, auf 2–15 m begrenzt.
  • Dichte je Block: Anteil der Gebäudepixel am sichtbaren Teil des Blocks, sofern dieser mindestens 25 % des Blocks und 400 m² umfasst; sonst die Gebäudedichte der Nachbarschaft. Ziel = Dichte · verdeckte Blockfläche − dort schon geschlossene Gebäudefläche. Liegt das Ziel unter der halben Fläche eines Vorlage-Rechtecks, wird im Block nichts gesetzt.
  • Setzen: Ausrichtung entlang der langen Seite des gedrehten Hüllrechtecks des Blocks. Die Reihen laufen parallel dazu, beginnend an der Hüllrechteckseite mit den meisten Straßenpixeln, die erste im Straßenabstand, die weiteren im Abstand kurze Seite + 3 m. In jeder Reihe folgen Rechtecke der Vorlagengröße mit 3 m Lücke. Ein Rechteck wird gesetzt, wenn alle seine Pixel bebaubar sind; gesetzt wird in Reihen- und Positionsreihenfolge, bis das Ziel erreicht ist. Kein Zufall — gleiche Eingabe, gleiches Ergebnis.
  • Markierung: remove_annotations gibt die gesetzten Rechtecke als Weltpolygone zurück. run.py setzt nach der Höhenzuweisung source = "annotation" bei jedem Gebäude, das zu mindestens 50 % seiner Fläche in diesen Rechtecken liegt. Die Höhe bleibt die aus assign_heights. Der Browser liest source nicht.

Fehlerfälle

Formfehler — der Lauf bricht ab mit AnnotationError("Beschriftung N: …") beziehungsweise "Ausnahme N: …" für keep, mit Nummer und Feld:

  • kaputtes TOML, unbekannte Tabelle auf oberster Ebene;
  • unbekanntes kind, fehlendes Pflichtfeld, Feld, das die Tabelle nicht kennt (Tippfehlerschutz);
  • box nicht aus zwei Punkten mit je zwei Zahlen, eine Seite unter 2 m, außerhalb des Ausschnitts;
  • polygon mit weniger als drei Punkten oder sich selbst schneidend, box und polygon zugleich, polygon bei einer anderen Art als region;
  • [grid] mit fehlendem oder nicht positivem Wert.

Inhaltliche Probleme — Warnung, der Lauf geht weiter:

  • leer: Die Farbregel findet nach Schritt 2 kein Pixel. Eintrag übersprungen.
  • randvoll: Die Maske füllt mehr als 60 % des Rahmens — Rahmen zu eng oder Art falsch. Eintrag übersprungen; gilt nicht für region.
  • angeschnitten: Ein in Schritt 2 verworfener Kandidat hat mindestens 20 Pixel — vermutlich schneidet der Rahmen Schrift ab. Eintrag wird angewendet.

Die Farbanteil-Grenzen in run.py bleiben unverändert und prüfen das bereinigte Klassenbild.

Bericht

run.py druckt nach remove_annotations:

Beschriftungen: 91 angewendet, 0 übersprungen · Gitternetz: 14 Linien
  #17 VICE BEACH (MIAMI BEACH) · K7 · Maske 44.223 m² · Straßen 5 verbunden, 1 weitergeführt, 0 Sackgassen · Gebäude 0 geschlossen, 3 ergänzt
Warnungen:
  #52 L1/42 · C6 · angeschnitten: Rahmen prüfen

Der Chunk ist der des Rahmenmittelpunkts über chunk_for_world. Zählt eine Maske mehrere Einträge, stehen Straßen- und Gebäudezahlen beim ersten.

Werkzeug find_annotations.py

  • --propose durchsucht die ganze Vorlage mit den Farbregeln, fasst Treffer zu Fundstellen zusammen und schreibt einen Listenentwurf mit Rahmen (10 m Luft) und geschätzter Art nach debug/annotations/vorschlag.toml, dazu einen nummerierten Kontaktbogen. Die Liste in data/ wird von Hand daraus übernommen und benannt.
  • --check sucht genauso und endet mit Status 1, wenn der Mittelpunkt einer Fundstelle in keinem annotation- und keinem keep-Rahmen liegt; es nennt jede solche Stelle mit Chunk und Koordinate. Die Suchregeln werden an der heutigen Vorlage so eingestellt, dass sie die 94 Fundstellen dieses Entwurfs finden und keine Küstenlinie melden — der hellblaue Küstenverlauf und die grünen Uferstreifen erfüllen die reine Marker-Farbregel.
  • --review rendert je Eintrag debug/annotations/NNN-name.png: links die Vorlage mit Maskenumriss, rechts das Ergebnis aus den Chunk-Dateien in den Farben von overview.py, beschriftet mit Nummer, Name und Chunk; dazu debug/annotations/uebersicht.png.

Prüfung

Tests (pytest, künstliche Bilder, ohne Kachel-Cache):

  • Liste: fehlende oder kommentarleere Datei ergibt keine Einträge; jeder Formfehler nennt Nummer und Feld; keep ohne reason ist ein Formfehler.
  • Erkennung: weiße Schrift mit Kontur über Straße und Park wird samt Kontur erfasst; ein farbgleiches Stück am Rahmenrand bleibt; ein Buchstabeninnenraum wird gefüllt; region mit Vieleck maskiert genau das Vieleck; die Gitterstreifen liegen auf den richtigen Zeilen und Spalten; leer, randvoll und angeschnitten werden gemeldet.
  • Dünne Masken: eine Straße längs unter einer Gitterlinie bleibt durchgehend; ein Park mit Gitterlinie hat keinen Schlitz.
  • Straßen: eine gerade Straße durch ein Schriftband läuft in gleicher Breite weiter; eine leicht gekrümmte wird glatt verbunden; zwei Straßen mit Versatz über der Grenze werden nicht verbunden; eine Ecke unter der Maske entsteht ohne Überstand; eine T-Kreuzung schließt an; eine seitlich angeschnittene Straße behält ihre volle Breite; ein Weg bleibt Weg und wird an einer Kreuzung von der Straße überdeckt; ein Ende ohne Gegenüber wird als Sackgasse gezählt.
  • Flächen: Park unter Schrift bleibt Park, Wasser bleibt Wasser, Straßen und Gebäude wachsen nicht.
  • Gebäude: ein angebissenes Gebäude wird zum Rechteck geschlossen, aber nicht länger; ein verdeckter Blockteil erreicht die Dichte des sichtbaren Teils auf ein Vorlage-Rechteck genau; im Park und im Wasser entsteht nichts; Straßenabstand und 3-m-Lücken werden eingehalten; jedes Rechteck liegt ganz im verdeckten Teil; unter drei Nachbarn entsteht nichts; zweimal dieselbe Eingabe ergibt dasselbe Ergebnis.
  • Markierung: ein Gebäude mit ≥ 50 % Fläche in einem ergänzten Rechteck bekommt source = "annotation", eines mit weniger behält seine Quelle.
  • chunk_for_world: dieselben Beispiele wie tests/coords.test.js.

An den echten Daten, in dieser Reihenfolge:

  1. Nullauf: Ohne annotations.toml erzeugt run.py die ausgelieferten Chunk-Dateien, manifest.json und die Minimap-Dateien Byte für Byte identisch.
  2. Liste aus --propose erstellt, benannt, um keep und die vom Nutzer markierten Überreste ergänzt; --check endet mit Status 0.
  3. Lauf ohne Formfehler; jede Warnung ist behoben oder begründet.
  4. Prüfbilder: Claude sieht alle an und schickt dem Nutzer die großen Schriftzüge und jede zweifelhafte Stelle.
  5. Kennzahlen vorher und nachher: Brücken auf Land (das Stück in L8 ist weg), Löcher unter 2.000 m² in der Landplatte (die zwei in L8 sind weg), Gebäudezahl samt ergänzten, Farbanteile innerhalb der Grenzen.
  6. Im Spiel: Minimap zeigt keine Schrift mehr; Fahrt durch VICE BEACH und CROSSTOWN mit Screenshots.
  7. cd tools/extract && uv run pytest -q und npm test sauber.

Folgen für andere Dokumente

  • docs/superpowers/specs/2026-09-13-feinkalibrierung-design.md und docs/superpowers/plans/2026-09-13-feinkalibrierung.md: Die Ausführungsschranke nennt diesen Entwurf statt task_999fed4a. chunk_for_world ist dann vorhanden; Aufgabe 1 des Plans legt die Funktion nicht erneut an. Fehler des Wiederaufbaus an einzelnen Stellen werden dort über corrections.toml behoben.
  • docs/superpowers/specs/2026-09-13-minimap-design.md: Die Kartenerzeugung hängt jetzt am Ende von run.py; der Hinweis „folgt, sobald die Beschriftungs-Aufgabe gelandet ist“ entfällt.

Nicht Teil dieses Entwurfs

  • Handkorrekturen einzelner Stellen — Feinkalibrierung.
  • Die hellgelbe Linie um Downtown, die heute als Sand eingelesen wird: keine Beschriftung.
  • Änderungen im Browser.
  • Beschriftungen außerhalb des Ausschnitts vice-a.