245 lines
8.6 KiB
Python
245 lines
8.6 KiB
Python
import numpy as np
|
|
from scipy import ndimage as ndi
|
|
|
|
from leonida.classify import CLASSES
|
|
from leonida.inpaint import fill_nearest, is_thin, rebuild
|
|
|
|
STADT, PARK, WATER = CLASSES["stadt"], CLASSES["gruen"], CLASSES["wasser"]
|
|
ROAD, PATH, BUILDING = CLASSES["strasse"], CLASSES["weg"], CLASSES["gebaeude"]
|
|
|
|
|
|
def city(h=200, w=300):
|
|
return np.full((h, w), STADT, dtype=np.uint8)
|
|
|
|
|
|
def box_mask(shape, rows, cols):
|
|
mask = np.zeros(shape, dtype=bool)
|
|
mask[rows, cols] = True
|
|
return mask
|
|
|
|
|
|
def scribble(labels, mask):
|
|
"""Schrift simulieren: unter der Maske steht Unsinn, der nichts mit der Karte zu tun hat."""
|
|
labels[mask] = BUILDING
|
|
labels[mask & (np.indices(labels.shape).sum(axis=0) % 3 == 0)] = ROAD
|
|
|
|
|
|
def test_fill_nearest_keeps_a_road_running_along_a_grid_line():
|
|
labels = city(60, 100)
|
|
labels[:, 40:60] = ROAD
|
|
band = box_mask(labels.shape, slice(None), slice(47, 53))
|
|
labels[band] = BUILDING
|
|
fill_nearest(labels, band, band)
|
|
assert (labels[:, 40:60] == ROAD).all()
|
|
|
|
|
|
def test_fill_nearest_closes_a_grid_slit_in_a_park():
|
|
labels = np.full((60, 100), PARK, dtype=np.uint8)
|
|
band = box_mask(labels.shape, slice(28, 34), slice(None))
|
|
labels[band] = ROAD
|
|
fill_nearest(labels, band, band)
|
|
assert (labels == PARK).all()
|
|
|
|
|
|
def test_thin_means_at_most_four_metres_thick():
|
|
assert is_thin(box_mask((40, 40), slice(10, 14), slice(5, 35)))
|
|
assert not is_thin(box_mask((40, 40), slice(10, 15), slice(5, 35)))
|
|
|
|
|
|
def test_straight_road_runs_through_a_text_band_in_the_same_width():
|
|
labels = city()
|
|
labels[:, 140:160] = ROAD
|
|
mask = box_mask(labels.shape, slice(80, 120), slice(60, 240))
|
|
scribble(labels, mask)
|
|
result = rebuild(labels, mask)
|
|
assert result.stats.connected == 1
|
|
assert (labels[80:120, 141:159] == ROAD).all()
|
|
assert not (labels[80:120, 60:138] == ROAD).any()
|
|
assert not (labels[80:120, 162:240] == ROAD).any()
|
|
|
|
|
|
def test_a_slight_offset_is_bridged_smoothly():
|
|
labels = city()
|
|
labels[:100, 130:150] = ROAD
|
|
labels[100:, 138:158] = ROAD
|
|
mask = box_mask(labels.shape, slice(80, 120), slice(60, 240))
|
|
scribble(labels, mask)
|
|
result = rebuild(labels, mask)
|
|
assert result.stats.connected == 1
|
|
roads, count = ndi.label(labels == ROAD)
|
|
assert count == 1 # oben und unten hängen jetzt zusammen
|
|
assert 16 <= (labels[100] == ROAD).sum() <= 24 # in der Mitte etwa straßenbreit
|
|
|
|
|
|
def test_roads_across_visible_ground_are_not_joined():
|
|
labels = city()
|
|
labels[90:110, :60] = ROAD
|
|
labels[90:110, 240:] = ROAD
|
|
mask = box_mask(labels.shape, slice(70, 130), slice(60, 140)) | box_mask(labels.shape, slice(70, 130), slice(180, 240))
|
|
mask |= box_mask(labels.shape, slice(70, 75), slice(140, 180)) # schmale Brücke: eine Maske
|
|
scribble(labels, mask)
|
|
result = rebuild(labels, mask)
|
|
assert result.stats.connected == 0
|
|
assert result.stats.dead_ends == 2
|
|
assert (labels[90:110, 141:179] == STADT).all() # sichtbarer Boden dazwischen bleibt
|
|
assert not (labels[mask] == ROAD).any() # Sackgassen werden nicht gemalt
|
|
|
|
|
|
def test_a_corner_under_the_mask_has_no_overhang():
|
|
labels = city()
|
|
labels[:80, 100:120] = ROAD
|
|
labels[130:150, 181:] = ROAD
|
|
mask = box_mask(labels.shape, slice(80, 180), slice(60, 181))
|
|
scribble(labels, mask)
|
|
result = rebuild(labels, mask)
|
|
assert result.stats.extended == 2
|
|
assert (labels[80:130, 102:118] == ROAD).all()
|
|
assert (labels[132:148, 122:181] == ROAD).all()
|
|
assert not (labels[153:180, 100:120] == ROAD).any()
|
|
assert not (labels[130:150, 60:97] == ROAD).any()
|
|
|
|
|
|
def test_a_side_street_joins_a_hidden_through_road():
|
|
labels = city()
|
|
labels[:80, 140:160] = ROAD
|
|
labels[130:150, :] = ROAD
|
|
mask = box_mask(labels.shape, slice(80, 180), slice(60, 240))
|
|
scribble(labels, mask)
|
|
result = rebuild(labels, mask)
|
|
assert result.stats.connected == 1 and result.stats.extended == 1
|
|
assert (labels[80:131, 142:158] == ROAD).all()
|
|
assert not (labels[153:180, 140:160] == ROAD).any()
|
|
|
|
|
|
def test_a_road_clipped_on_one_side_keeps_its_full_width():
|
|
labels = city()
|
|
labels[:, 140:160] = ROAD
|
|
mask = box_mask(labels.shape, slice(80, 120), slice(150, 240))
|
|
scribble(labels, mask)
|
|
rebuild(labels, mask)
|
|
assert (labels[80:120, 140:159] == ROAD).all()
|
|
assert not (labels[80:120, 162:240] == ROAD).any()
|
|
|
|
|
|
def test_a_path_stays_a_path_and_the_street_wins_the_crossing():
|
|
labels = city()
|
|
labels[:, 145:150] = PATH
|
|
labels[95:115, :] = ROAD
|
|
mask = box_mask(labels.shape, slice(60, 140), slice(100, 200))
|
|
scribble(labels, mask)
|
|
rebuild(labels, mask)
|
|
assert (labels[97:113, 145:150] == ROAD).all()
|
|
assert (labels[62:90, 146:149] == PATH).all()
|
|
assert (labels[120:138, 146:149] == PATH).all()
|
|
|
|
|
|
def test_park_and_water_under_text_stay_what_they_are():
|
|
labels = city()
|
|
labels[:, :150] = PARK
|
|
labels[:, 250:] = WATER
|
|
mask = box_mask(labels.shape, slice(80, 120), slice(100, 280))
|
|
scribble(labels, mask)
|
|
rebuild(labels, mask)
|
|
assert (labels[80:120, 100:149] == PARK).all()
|
|
assert (labels[80:120, 151:249] == STADT).all()
|
|
assert (labels[80:120, 251:280] == WATER).all()
|
|
|
|
|
|
def test_edge_crumbs_are_no_fill_source():
|
|
labels = np.full((200, 300), PARK, dtype=np.uint8)
|
|
labels[74:78, 150:154] = CLASSES["sand"] # 16 m² Kantenglättung
|
|
mask = box_mask(labels.shape, slice(80, 120), slice(100, 200))
|
|
scribble(labels, mask)
|
|
rebuild(labels, mask)
|
|
assert (labels[mask] == PARK).all()
|
|
|
|
|
|
def test_a_road_running_off_the_map_under_text_is_kept_to_the_edge():
|
|
labels = city()
|
|
labels[:, 140:160] = ROAD
|
|
mask = box_mask(labels.shape, slice(0, 30), slice(60, 240))
|
|
scribble(labels, mask)
|
|
result = rebuild(labels, mask)
|
|
assert result.stats.extended == 1
|
|
assert (labels[0:30, 142:158] == ROAD).all()
|
|
|
|
|
|
def test_a_bitten_building_is_closed_but_not_extended():
|
|
labels = city()
|
|
labels[50:90, 50:110] = BUILDING
|
|
mask = box_mask(labels.shape, slice(70, 110), slice(90, 150))
|
|
scribble(labels, mask)
|
|
result = rebuild(labels, mask)
|
|
assert result.stats.closed == 1
|
|
assert (labels[70:90, 90:110] == BUILDING).all()
|
|
assert not (labels[92:110, 90:150] == BUILDING).any()
|
|
assert not (labels[70:90, 112:150] == BUILDING).any()
|
|
|
|
|
|
def block_city():
|
|
"""Ein Block zwischen vier Straßen, links sichtbar bebaut, rechts verdeckt."""
|
|
labels = city(300, 400)
|
|
labels[20:40, :] = ROAD
|
|
labels[260:280, :] = ROAD
|
|
labels[:, 20:40] = ROAD
|
|
labels[:, 360:380] = ROAD
|
|
for top in (60, 100, 140, 180):
|
|
for left in (60, 120):
|
|
labels[top:top + 20, left:left + 30] = BUILDING
|
|
mask = box_mask(labels.shape, slice(60, 241), slice(220, 341))
|
|
return labels, mask
|
|
|
|
|
|
def test_a_hidden_block_part_gets_the_density_of_the_visible_part():
|
|
labels, mask = block_city()
|
|
visible_density = 8 * 600 / (220 * 320 - mask.sum())
|
|
scribble(labels, mask)
|
|
result = rebuild(labels, mask)
|
|
added = (labels[mask] == BUILDING).sum()
|
|
assert result.stats.added == len(result.buildings) >= 3
|
|
assert abs(added - visible_density * mask.sum()) <= 600 + 2 * (20 + 30) # ein Vorlage-Rechteck genau
|
|
|
|
|
|
def test_added_buildings_keep_setback_gaps_and_stay_in_the_hidden_part():
|
|
labels, mask = block_city()
|
|
scribble(labels, mask)
|
|
result = rebuild(labels, mask)
|
|
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)
|
|
assert count == len(result.buildings)
|
|
for k in range(1, count + 1):
|
|
others = new & (pieces != k)
|
|
assert ndi.distance_transform_edt(~others)[pieces == k].min() >= 3
|
|
|
|
|
|
def test_no_buildings_are_added_in_a_park():
|
|
labels, mask = block_city()
|
|
labels[40:260, 200:360] = PARK
|
|
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
|
|
labels[60:80, 60:90] = BUILDING
|
|
labels[100:120, 60:90] = BUILDING
|
|
scribble(labels, mask)
|
|
result = rebuild(labels, mask)
|
|
assert result.stats.added == 0
|
|
assert result.stats.note == "zu wenig Nachbarn"
|
|
|
|
|
|
def test_rebuild_is_deterministic():
|
|
first, mask = block_city()
|
|
second = first.copy()
|
|
scribble(first, mask)
|
|
scribble(second, mask)
|
|
a, b = rebuild(first, mask), rebuild(second, mask)
|
|
assert (first == second).all()
|
|
assert a.buildings == b.buildings
|