start
This commit is contained in:
1 parent
6a056262a8
commit
a55dca5fb6
11 files changed
+132
-25
No files matched your search
@@ -173,8 +173,8 @@ Spieler.
|
||||
- **Autos:** vier SUVs an Straßenpunkten nahe dem Start, Konfiguration aus der Showcase
|
||||
übernommen. Ihre Chunks bleiben geladen, weil jedes Fahrzeug in jedem Frame Physik rechnet.
|
||||
- **Figuren:** Modul aus `showcase.js` nach `src/characters/`, inklusive Delta-Retargeting
|
||||
und ans Mesh angepasster Hitbox. Start ist der Rasen am Südrand des Parks von Vice Beach bei X 1600, Y 690 (three.js: x = 1600, z = -690) — ursprünglich X 1699, Y 743 auf einer Buchstabenkontur von „(MIAMI BEACH)“, versetzt mit dem Beschriftungs-Plan —,
|
||||
abgeleitet aus dem Schwerpunkt der 259 Ocean-Beach-Landmarks im Ausschnitt.
|
||||
und ans Mesh angepasster Hitbox. Start ist der Rasen am Südrand des Parks von Vice Beach bei X 1600, Y 690 (three.js: x = 1600, z = -690) — ursprünglich X 1699, Y 743, abgeleitet aus dem Schwerpunkt der 259 Ocean-Beach-Landmarks im Ausschnitt,
|
||||
auf einer Buchstabenkontur von „(MIAMI BEACH)“, versetzt mit dem Beschriftungs-Plan.
|
||||
- **Debug-Panel:** Figurenauswahl, Chunk-Grenzen, Hitbox, Kartenbild auf dem Boden,
|
||||
Sichtweite, Höhenfaktor, Sprung zu einem der 54 Wahrzeichen.
|
||||
- **Namensnennung im HUD:** Kartendaten und Landmarks von map.gtadb.org (CC BY 4.0,
|
||||
|
||||
@@ -41,10 +41,10 @@ Gemessen an der Vorlage (`tools/extract/cache`, YANIS v15) und den ausgelieferte
|
||||
## 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
|
||||
Kacheln → classify → drop_map_labels → remove_annotations → drop_map_labels → 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.
|
||||
`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 und läuft nach `remove_annotations` ein zweites Mal, für Inselreste, die erst das Entfernen im Wasser hinterlässt.
|
||||
|
||||
| Einheit | Aufgabe |
|
||||
|---|---|
|
||||
@@ -95,6 +95,8 @@ reason = "knallrotes Gebäude in A10, laut Nutzer echt"
|
||||
|
||||
Der Rahmen soll die Beschriftung mit mindestens 8 m Luft umschließen; `--propose` schlägt Rahmen mit 10 m Luft vor. Wo keine Farbregel sicher greift — blasse Schrift, winzige Straßennamen in Gebäudegrau, Piktogramme aus dünnen dunklen Strichen —, bekommt die Stelle einen engen `region`-Rahmen: dann wird der ganze Rahmen neu aufgebaut.
|
||||
|
||||
Interstate-Schilder mit rotem Banner stehen als `region` in der Liste, weil keine Farbregel das dunkelrote Banner sicher trifft (ΔE 20,5 zu `spekulativ`) und der Rest sonst als spekulatives Gebäude auf der Fahrbahn stünde.
|
||||
|
||||
## Erkennung je Art
|
||||
|
||||
Je Eintrag entsteht eine Maske in fünf Schritten:
|
||||
@@ -121,9 +123,11 @@ Die **Gitternetz-Maske** sind die Streifen aus `[grid]`, auf den Ausschnitt besc
|
||||
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. Gefüllt wird in Streifen von 10 m beiderseits jeder Linie.
|
||||
2. **Eintragsmasken:** Alle Eintragsmasken werden vereinigt und in zusammenhängende Masken zerlegt; überlappende Einträge landen so in derselben Maske. Je Maske:
|
||||
2. **Eintragsmasken:** Alle Eintragsmasken werden vereinigt und in zusammenhängende Masken zerlegt; überlappende Einträge landen so in derselben Maske. Zuerst wird jede Maske aufgebaut:
|
||||
- **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 in einem Fenster aus dem Hüllrechteck der Maske plus 60 m, danach Gebäude ergänzen im Nachbarschaftsfenster (plus 150 m).
|
||||
- **sonst:** Straßen und Wege → Flächen → Gebäude schließen in einem Fenster aus dem Hüllrechteck der Maske plus 60 m.
|
||||
|
||||
Erst danach werden für alle nicht dünnen Masken, in derselben Reihenfolge, Gebäude ergänzt im Nachbarschaftsfenster (plus 150 m). Sonst zählten die Buchstabenkonturen noch nicht aufgebauter Masken als Straße und teilten Blöcke, und wo Gebäude entstehen, hinge von der Reihenfolge der Masken ab.
|
||||
|
||||
### Straßen und Wege
|
||||
|
||||
|
||||
@@ -156,7 +156,7 @@ Die Namensnennung steht unten links und reicht auf schmalen Fenstern bis in die
|
||||
- Ein Test, der die Namen mit der tatsächlichen Lage verbindet statt nur mit den Indizes: der Downtown-Bezugspunkt aus `tools/extract/data/districts.json` bei Welt `(−511, 254)` ergibt über `chunkForWorld` `(i = 1, j = 8)` und damit `B9`; der höchste Turm, das Southeast Financial Center bei `(−461, −460)`, ergibt `C12`. An der bestehenden Funktion nachgerechnet. (Korrigiert nach Task 4: `(−511, 254)` ist der Mittelpunkt der westlichen Hochhäuser, nicht der Hochhauskern selbst.)
|
||||
|
||||
**Ohne Browser (vitest), für `mapMath.js`:**
|
||||
- `worldToMap` und `mapToWorld` an bekannten Punkten: dem früheren Spawn `(1699, 3, −743)` und alle vier Ecken des Ausschnitts; dazu Hin- und Rückweg ergibt wieder den Ausgangspunkt. Ein Test, der eine gespiegelte Achse sicher fängt.
|
||||
- `worldToMap` und `mapToWorld` an bekannten Punkten: dem früheren Spawn `(1699, 3, −743)` und allen vier Ecken des Ausschnitts; dazu Hin- und Rückweg ergibt wieder den Ausgangspunkt. Ein Test, der eine gespiegelte Achse sicher fängt.
|
||||
- Zoomgrenzen: nie kleiner als „ganzer Ausschnitt", nie größer als vierfach; Verschiebung bleibt innerhalb des Kartenrands.
|
||||
- Klick gegen Ziehen an der 4-px-Schwelle, knapp darunter und knapp darüber, und ein Ziehen, das zum Startpunkt zurückkehrt.
|
||||
- `nearestWalkable` an einer künstlichen Maske: begehbarer Treffer landet in der Mitte seines Pixels; ein Gebäudepixel neben einer Straße findet die Straße; nur Wasser im Umkreis ergibt kein Ergebnis; ein Treffer knapp außerhalb von 20 m zählt nicht.
|
||||
|
||||
+1
-1
Submodule three-player-controller updated: ac396825a7...028c652449.
@@ -38,7 +38,7 @@ box = [[-885, 2045], [-833, 1995]]
|
||||
|
||||
[[annotation]]
|
||||
name = "Schild 97 in B2"
|
||||
kind = "symbol"
|
||||
kind = "region"
|
||||
box = [[-551, 2199], [-497, 2147]]
|
||||
|
||||
[[annotation]]
|
||||
@@ -153,7 +153,7 @@ box = [[-861, 1572], [-797, 1521]]
|
||||
|
||||
[[annotation]]
|
||||
name = "Schild 97 in B4"
|
||||
kind = "symbol"
|
||||
kind = "region"
|
||||
box = [[-652, 1601], [-592, 1539]]
|
||||
|
||||
[[annotation]]
|
||||
@@ -238,7 +238,7 @@ box = [[1440, 1336], [1515, 1289]]
|
||||
|
||||
[[annotation]]
|
||||
name = "Schild 97 in A6"
|
||||
kind = "symbol"
|
||||
kind = "region"
|
||||
box = [[-869, 1230], [-817, 1184]]
|
||||
|
||||
[[annotation]]
|
||||
@@ -278,7 +278,7 @@ box = [[-1000, 830], [-945, 774]]
|
||||
|
||||
[[annotation]]
|
||||
name = "Schild 97 in A7"
|
||||
kind = "symbol"
|
||||
kind = "region"
|
||||
box = [[-945, 933], [-893, 883]]
|
||||
|
||||
[[annotation]]
|
||||
@@ -338,7 +338,7 @@ box = [[-670, 530], [-613, 490]]
|
||||
|
||||
[[annotation]]
|
||||
name = "Schild 404 in B8"
|
||||
kind = "symbol"
|
||||
kind = "region"
|
||||
box = [[-626, 618], [-567, 575]]
|
||||
|
||||
[[annotation]]
|
||||
@@ -478,7 +478,7 @@ box = [[1905, -60], [1934, -131]]
|
||||
|
||||
[[annotation]]
|
||||
name = "Schild 397 in B12"
|
||||
kind = "symbol"
|
||||
kind = "region"
|
||||
box = [[-685, -346], [-625, -397]]
|
||||
|
||||
[[annotation]]
|
||||
@@ -493,7 +493,7 @@ box = [[-313, -398], [-169, -433]]
|
||||
|
||||
[[annotation]]
|
||||
name = "Schild 397 in D12"
|
||||
kind = "symbol"
|
||||
kind = "region"
|
||||
box = [[-146, -410], [-87, -460]]
|
||||
|
||||
[[annotation]]
|
||||
|
||||
@@ -24,7 +24,7 @@ from skimage.morphology import disk
|
||||
|
||||
from .classify import PALETTE
|
||||
from .coords import SLICE, chunk_for_world, chunk_name
|
||||
from .inpaint import RebuildStats, fill_nearest, is_thin, rebuild
|
||||
from .inpaint import RebuildStats, add_buildings, fill_nearest, is_thin, rebuild
|
||||
|
||||
KINDS = ("text_white", "text_red", "marker", "symbol", "street_name", "region")
|
||||
MARGIN = {"text_white": 7, "text_red": 7, "marker": 3, "symbol": 4, "street_name": 1, "region": 0}
|
||||
@@ -406,7 +406,7 @@ def remove_annotations(mosaic: np.ndarray, labels: np.ndarray, annotations: Anno
|
||||
owners.setdefault(int(cid), entry)
|
||||
touched.append(ids)
|
||||
|
||||
generated = []
|
||||
generated, rebuilt = [], []
|
||||
h, w = labels.shape
|
||||
for cid, sl in enumerate(ndi.find_objects(components), start=1):
|
||||
if sl is None:
|
||||
@@ -419,9 +419,17 @@ def remove_annotations(mosaic: np.ndarray, labels: np.ndarray, annotations: Anno
|
||||
continue
|
||||
mask = np.zeros(labels.shape, dtype=bool)
|
||||
mask[sl] = piece
|
||||
result = rebuild(labels, mask, union)
|
||||
result = rebuild(labels, mask, union, add=False)
|
||||
owner = owners[cid]
|
||||
owner.stats = result.stats if owner.stats is None else _merged(owner.stats, result.stats)
|
||||
rebuilt.append((cid, sl))
|
||||
# Ergänzt wird erst, wenn alle Masken aufgebaut sind: sonst zählten Buchstabenkonturen noch nicht
|
||||
# aufgebauter Masken als Straße und teilten Blöcke, und das Ergebnis hinge von der Reihenfolge ab.
|
||||
for cid, sl in rebuilt:
|
||||
mask = np.zeros(labels.shape, dtype=bool)
|
||||
mask[sl] = components[sl] == cid
|
||||
result = add_buildings(labels, mask, union)
|
||||
owners[cid].stats = _merged(owners[cid].stats, result.stats)
|
||||
generated += [Polygon([(SLICE["x0"] + c, SLICE["y1"] - r) for r, c in corners])
|
||||
for corners in result.buildings]
|
||||
for entry, ids in zip(entries, touched):
|
||||
|
||||
@@ -90,12 +90,14 @@ def is_thin(mask: np.ndarray) -> bool:
|
||||
return float(ndi.distance_transform_edt(np.pad(mask, 1)).max()) <= THIN_DEPTH
|
||||
|
||||
|
||||
def rebuild(labels: np.ndarray, mask: np.ndarray, blocked: np.ndarray | None = None) -> RebuildResult:
|
||||
def rebuild(labels: np.ndarray, mask: np.ndarray, blocked: np.ndarray | None = None,
|
||||
add: bool = True) -> RebuildResult:
|
||||
"""Baut das Klassenbild unter einer zusammenhängenden Maske wieder auf.
|
||||
|
||||
blocked sind alle Pixel, die nicht als Quelle taugen — alle Beschriftungsmasken; ohne
|
||||
Angabe nur diese Maske. Reihenfolge wie in der Spec: Straßen und Wege, Flächen,
|
||||
Gebäude schließen, Gebäude ergänzen.
|
||||
Gebäude schließen, Gebäude ergänzen. Mit add=False endet der Aufbau nach dem Schließen;
|
||||
ergänzt wird dann über add_buildings, sobald alle Masken aufgebaut sind.
|
||||
"""
|
||||
blocked = mask if blocked is None else blocked
|
||||
stats = RebuildStats()
|
||||
@@ -107,6 +109,14 @@ def rebuild(labels: np.ndarray, mask: np.ndarray, blocked: np.ndarray | None = N
|
||||
_stamp(L, M, painted, road, points, halves)
|
||||
_fill_areas(L, M & ~painted, B)
|
||||
_close_buildings(L, M, B, painted, stats)
|
||||
corners = _add_buildings(labels, mask, blocked, stats) if add else []
|
||||
return RebuildResult(stats, corners)
|
||||
|
||||
|
||||
def add_buildings(labels: np.ndarray, mask: np.ndarray, blocked: np.ndarray | None = None) -> RebuildResult:
|
||||
"""Ergänzt Gebäude unter einer schon aufgebauten Maske; die Statistik trägt nur added und note."""
|
||||
blocked = mask if blocked is None else blocked
|
||||
stats = RebuildStats()
|
||||
corners = _add_buildings(labels, mask, blocked, stats)
|
||||
return RebuildResult(stats, corners)
|
||||
|
||||
@@ -535,13 +545,17 @@ def _window(mask, margin):
|
||||
|
||||
|
||||
def _rectangle(rows, cols) -> Polygon:
|
||||
"""Gedrehtes Hüllrechteck um Pixel (Zeilen, Spalten) als Polygon in (Spalte, Zeile), mit Pixelkante."""
|
||||
"""Gedrehtes Hüllrechteck um Pixel (Zeilen, Spalten) als Polygon in (Spalte, Zeile), mit Pixelkante.
|
||||
|
||||
Liegen die Pixel auf einer Linie, ist das Hüllrechteck eine Linie; eckige Enden machen daraus
|
||||
wieder ein Rechteck mit vier Ecken statt einer Kapsel.
|
||||
"""
|
||||
points = MultiPoint(np.column_stack([cols, rows]).astype(float))
|
||||
with warnings.catch_warnings():
|
||||
# Siehe buildings.regularize(): harmlose GEOS-Warnung bei achsenparallelen Formen.
|
||||
warnings.filterwarnings("ignore", message=r".*oriented_envelope.*", category=RuntimeWarning)
|
||||
envelope = points.minimum_rotated_rectangle
|
||||
return envelope.buffer(0.5, join_style="mitre")
|
||||
return envelope.buffer(0.5, cap_style="square", join_style="mitre")
|
||||
|
||||
|
||||
def _sides(rect: Polygon) -> tuple[float, float]:
|
||||
|
||||
@@ -25,6 +25,7 @@ def main() -> None:
|
||||
labels = drop_map_labels(labels)
|
||||
removal = remove_annotations(mosaic, labels, load_annotations(Path("data/annotations.toml")))
|
||||
labels = removal.labels
|
||||
labels = drop_map_labels(labels) # Inselreste, die erst das Entfernen im Wasser hinterlässt
|
||||
print(removal.report())
|
||||
if unknown_share > 0.05:
|
||||
raise SystemExit(f"{unknown_share:.1%} der Pixel unbekannt. Palette kalibrieren, nicht die Grenze anheben.")
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import numpy as np
|
||||
from scipy import ndimage as ndi
|
||||
from shapely.geometry import box
|
||||
|
||||
from leonida.annotations import (Annotation, AnnotationList, Grid, annotation_mask, candidates, grid_mask,
|
||||
@@ -6,7 +7,7 @@ from leonida.annotations import (Annotation, AnnotationList, Grid, annotation_ma
|
||||
from leonida.classify import CLASSES, classify
|
||||
|
||||
PARK, ROAD, URBAN = (192, 216, 128), (80, 80, 80), (216, 216, 216)
|
||||
WHITE, OUTLINE = (248, 248, 248), (80, 88, 96)
|
||||
WHITE, OUTLINE, BUILDING = (248, 248, 248), (80, 88, 96), (176, 176, 176)
|
||||
|
||||
|
||||
def picture(h=200, w=300, colour=URBAN):
|
||||
@@ -154,6 +155,24 @@ def test_the_grid_is_closed_before_anything_is_rebuilt():
|
||||
assert removal.grid_lines == 7 # Spalten 0, 100, 200, 300 und Zeilen 0, 100, 200
|
||||
|
||||
|
||||
def test_a_grid_line_through_an_entry_leaves_no_strip_in_it():
|
||||
img = picture(colour=PARK)
|
||||
img[:, 98:102] = ROAD # Gitterlinie, als Straße gelesen, quer durch den Schriftzug
|
||||
letter(img, (80, 120), (60, 140))
|
||||
annotations = AnnotationList([entry("text_white", (60, 141), (40, 161))], grid=Grid(100, 6))
|
||||
removal = remove_annotations(img, classify(img), annotations)
|
||||
assert (removal.labels == CLASSES["land"]).all() # erst danach aufgebaut, liefe dort ein Straßenstreifen
|
||||
|
||||
|
||||
def test_a_thin_mask_takes_the_nearest_class_and_leaves_no_slit():
|
||||
img = picture()
|
||||
img[60:140, 100:200] = BUILDING
|
||||
line = entry("region", (99, 102), (90, 211), name="Zeigerlinie") # 3 m breit, quer über das Gebäude
|
||||
removal = remove_annotations(img, classify(img), AnnotationList([line]))
|
||||
assert (removal.labels[60:140, 100:200] == CLASSES["gebaeude"]).all()
|
||||
assert (removal.labels[99:102, 90:100] == CLASSES["stadt"]).all()
|
||||
|
||||
|
||||
def test_entries_sharing_one_mask_are_reported_together():
|
||||
img = picture()
|
||||
img[:, 140:160] = ROAD
|
||||
@@ -165,6 +184,37 @@ def test_entries_sharing_one_mask_are_reported_together():
|
||||
assert "#2 Rechts · A1 · Maske" in report and "gemeinsam mit #1" in report
|
||||
|
||||
|
||||
class ReversedComponents:
|
||||
"""scipy.ndimage für annotations.py, nur nummeriert label() die Flächen rückwärts."""
|
||||
|
||||
def __getattr__(self, name):
|
||||
return getattr(ndi, name)
|
||||
|
||||
@staticmethod
|
||||
def label(image, structure=None):
|
||||
numbers, count = ndi.label(image, structure=structure)
|
||||
return np.where(numbers > 0, count + 1 - numbers, 0), count
|
||||
|
||||
|
||||
def test_added_buildings_do_not_depend_on_the_order_of_the_masks(monkeypatch):
|
||||
img = picture(300, 400)
|
||||
img[20:40, :] = ROAD
|
||||
img[260:280, :] = ROAD
|
||||
img[:, 20:40] = ROAD
|
||||
img[:, 360:380] = ROAD
|
||||
for top in (60, 100, 140, 180):
|
||||
for left in (60, 120):
|
||||
img[top:top + 20, left:left + 30] = BUILDING # links sichtbar bebaut
|
||||
letter(img, (43, 257), (180, 190)) # Kontur in Straßenfarbe teilt den Block bis zum Aufbau
|
||||
annotations = AnnotationList([entry("text_white", (30, 271), (165, 206), index=1, name="Balken"),
|
||||
entry("region", (60, 241), (220, 341), index=2, name="Rest")])
|
||||
forward = remove_annotations(img, classify(img), annotations)
|
||||
monkeypatch.setattr("leonida.annotations.ndi", ReversedComponents())
|
||||
backward = remove_annotations(img, classify(img), annotations)
|
||||
assert forward.generated # im verdeckten Teil rechts entstehen Gebäude
|
||||
assert (forward.labels == backward.labels).all()
|
||||
|
||||
|
||||
def test_buildings_mostly_inside_added_rectangles_are_marked():
|
||||
heights = [{"height": 9.0, "source": "district"}, {"height": 12.0, "source": "district"}]
|
||||
marked = mark_generated([box(1, 1, 9, 9), box(6, 0, 16, 10)], heights, [box(0, 0, 10, 10)])
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import numpy as np
|
||||
import pytest
|
||||
from scipy import ndimage as ndi
|
||||
|
||||
from leonida.classify import CLASSES
|
||||
from leonida.inpaint import fill_nearest, is_thin, rebuild
|
||||
from leonida.inpaint import _rectangle, _sides, fill_nearest, is_thin, rebuild
|
||||
|
||||
STADT, PARK, WATER = CLASSES["stadt"], CLASSES["gruen"], CLASSES["wasser"]
|
||||
ROAD, PATH, BUILDING = CLASSES["strasse"], CLASSES["weg"], CLASSES["gebaeude"]
|
||||
@@ -133,6 +134,18 @@ def test_a_path_stays_a_path_and_the_street_wins_the_crossing():
|
||||
assert (labels[120:138, 146:149] == PATH).all()
|
||||
|
||||
|
||||
def test_a_path_runs_through_to_a_stub_too_short_for_a_direction():
|
||||
labels = city()
|
||||
labels[:80, 145:150] = PATH
|
||||
labels[120:134, 155:160] = PATH # kurzes, versetztes Stück: kein Straßenende
|
||||
mask = box_mask(labels.shape, slice(80, 120), slice(100, 200))
|
||||
scribble(labels, mask)
|
||||
result = rebuild(labels, mask)
|
||||
assert (result.stats.connected, result.stats.extended, result.stats.dead_ends) == (1, 0, 0)
|
||||
paths, count = ndi.label(labels == PATH)
|
||||
assert count == 1 # oberer Weg und Stück hängen zusammen
|
||||
|
||||
|
||||
def test_park_and_water_under_text_stay_what_they_are():
|
||||
labels = city()
|
||||
labels[:, :150] = PARK
|
||||
@@ -203,9 +216,12 @@ def test_a_hidden_block_part_gets_the_density_of_the_visible_part():
|
||||
def test_added_buildings_keep_setback_gaps_and_stay_in_the_hidden_part():
|
||||
labels, mask = block_city()
|
||||
scribble(labels, mask)
|
||||
before = labels.copy()
|
||||
result = rebuild(labels, mask)
|
||||
assert (labels[~mask] == before[~mask]).all()
|
||||
for corners in result.buildings: # Maske: Pixel 60–240 × 220–340 samt Pixelkante
|
||||
assert all(59.5 <= r <= 240.5 and 219.5 <= c <= 340.5 for r, c in corners)
|
||||
new = (labels == BUILDING) & mask
|
||||
assert not (new & ~mask).any()
|
||||
road_distance = ndi.distance_transform_edt(~np.isin(labels, [ROAD, PATH]))
|
||||
assert road_distance[new].min() >= 15 # Straßenabstand der Nachbarn, auf 15 m begrenzt
|
||||
pieces, count = ndi.label(new)
|
||||
@@ -223,6 +239,14 @@ def test_no_buildings_are_added_in_a_park():
|
||||
assert result.stats.added == 0
|
||||
|
||||
|
||||
def test_no_buildings_are_added_in_water():
|
||||
labels, mask = block_city()
|
||||
labels[40:260, 200:360] = WATER
|
||||
scribble(labels, mask)
|
||||
result = rebuild(labels, mask)
|
||||
assert result.stats.added == 0
|
||||
|
||||
|
||||
def test_without_three_neighbours_nothing_is_added():
|
||||
labels, mask = block_city()
|
||||
labels[labels == BUILDING] = STADT
|
||||
@@ -242,3 +266,9 @@ def test_rebuild_is_deterministic():
|
||||
a, b = rebuild(first, mask), rebuild(second, mask)
|
||||
assert (first == second).all()
|
||||
assert a.buildings == b.buildings
|
||||
|
||||
|
||||
def test_pixels_on_one_line_give_a_rectangle_with_four_corners():
|
||||
rect = _rectangle(np.full(10, 5), np.arange(10, 20)) # 10 Pixel in einer Zeile
|
||||
assert len(rect.exterior.coords) == 5 # vier Ecken und der Schlusspunkt
|
||||
assert _sides(rect) == pytest.approx((1.0, 10.0))
|
||||
Binary file not shown.
Reference in new issue
Block a user