This commit is contained in:
knso committed 2026-09-15 21:05:18 +02:00
1 parent 6a056262a8
commit a55dca5fb6
11 files changed
+132 -25

No files matched your search

+51 -1
View File
@@ -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)])
+32 -2
View File
@@ -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))