Ruling 43: beruehren sich Schriftzug und echte Flaeche (z.B. eine Mole), bilden sie eine Komponente und bleiben beide stehen - dieselbe vorsichtige Richtung wie bei Beschriftungen auf dem Festland, lieber ein Buchstabe zu viel als eine Mole zu wenig. Ruling 44: ein Tippfehler in LABEL_MAX_NATURAL (0.2 statt 0.02) faellt jetzt in einem Test auf, der knapp oberhalb der Schwelle liegt. MAINLAND_MIN_SIZE-Kommentar auf zwei Saetze gekuerzt (Stil wie LAND_REACH_PX/PALM_VERGE), Wortdopplung im Docstring entfernt. Kein Verhalten geaendert: 41 statt 39 Tests, beide neuen Tests waren vor jeder Aenderung bereits gruen. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
81 lines
4.0 KiB
Python
81 lines
4.0 KiB
Python
import numpy as np
|
|
from leonida.classify import CLASSES, PALETTE, class_shares, classify, drop_map_labels
|
|
|
|
def block(color, size=8):
|
|
return np.full((size, size, 3), color, dtype=np.uint8)
|
|
|
|
def test_pure_colors_hit_their_class():
|
|
for name, color in PALETTE.items():
|
|
labels = classify(block(color))
|
|
assert (labels == CLASSES[name]).all(), name
|
|
|
|
def test_jpeg_noise_still_classifies():
|
|
rng = np.random.default_rng(0)
|
|
noisy = block(PALETTE["strasse"], 32).astype(np.int16)
|
|
noisy += rng.integers(-10, 11, noisy.shape) # JPEG-Rauschen
|
|
labels = classify(np.clip(noisy, 0, 255).astype(np.uint8))
|
|
assert (labels == CLASSES["strasse"]).mean() > 0.98
|
|
|
|
def test_unknown_pixels_are_filled_from_neighbours():
|
|
img = block(PALETTE["stadt"], 16)
|
|
img[7:9, 7:9] = (255, 0, 255) # Beschriftung, in keiner Klasse
|
|
labels = classify(img)
|
|
assert (labels == CLASSES["stadt"]).all()
|
|
|
|
def test_reports_the_share_of_unknown_pixels():
|
|
img = block(PALETTE["stadt"], 16)
|
|
img[7:9, 7:9] = (255, 0, 255) # 4 von 256 Pixeln passen zu keiner Farbe
|
|
labels, share = classify(img, report_unknown=True)
|
|
assert (labels == CLASSES["stadt"]).all()
|
|
assert abs(share - 4 / 256) < 1e-9
|
|
|
|
def test_shares_sum_to_one():
|
|
img = np.concatenate([block(PALETTE["wasser"], 8), block(PALETTE["land"], 8)], axis=1)
|
|
shares = class_shares(classify(img))
|
|
assert abs(sum(shares.values()) - 1.0) < 1e-9
|
|
assert abs(shares["wasser"] - 0.5) < 1e-9
|
|
|
|
def test_letter_shaped_island_becomes_water():
|
|
"""Ein Buchstabe im Wasser ist Beschriftung, keine Stadt: keine natuerlichen Merkmale."""
|
|
labels = np.full((200, 200), CLASSES["wasser"], dtype=np.uint8)
|
|
labels[40:60, 40:120] = CLASSES["stadt"] # Querbalken
|
|
labels[40:120, 40:60] = CLASSES["strasse"] # Laengsbalken -> zusammen ein "L"
|
|
out = drop_map_labels(labels)
|
|
assert (out == CLASSES["wasser"]).all()
|
|
|
|
def test_real_island_survives():
|
|
"""Eine Insel mit Gruen und Sand bleibt, auch wenn sie klein und vom Festland getrennt ist."""
|
|
labels = np.full((200, 200), CLASSES["wasser"], dtype=np.uint8)
|
|
labels[40:120, 40:120] = CLASSES["stadt"]
|
|
labels[50:70, 50:70] = CLASSES["gruen"]
|
|
labels[100:115, 45:115] = CLASSES["sand"]
|
|
out = drop_map_labels(labels)
|
|
assert (out[40:120, 40:120] != CLASSES["wasser"]).all()
|
|
|
|
def test_mainland_is_never_dropped():
|
|
"""Die groesste Nicht-Wasser-Komponente bleibt immer, auch voellig ohne Gruen und Sand."""
|
|
labels = np.full((200, 200), CLASSES["wasser"], dtype=np.uint8)
|
|
labels[10:190, 10:100] = CLASSES["stadt"]
|
|
out = drop_map_labels(labels)
|
|
assert (out[10:190, 10:100] == CLASSES["stadt"]).all()
|
|
|
|
def test_lettering_touching_a_real_area_is_kept():
|
|
"""Beruehren sich Schriftzug und echte Flaeche, bilden sie eine Komponente, und es bleibt
|
|
beides stehen. Die vorsichtige Richtung: lieber ein Buchstabe zu viel als eine Mole zu
|
|
wenig. Dieselbe Entscheidung wie bei den Beschriftungen auf dem Festland."""
|
|
labels = np.full((200, 200), CLASSES["wasser"], dtype=np.uint8)
|
|
labels[40:60, 40:120] = CLASSES["strasse"] # Schriftzug, fuer sich genommen 0 % natuerlich
|
|
labels[60:90, 100:140] = CLASSES["stadt"] # Mole, beruehrt ihn an der Kante
|
|
labels[70:85, 110:135] = CLASSES["gruen"] # ... und traegt Gruen: echte Flaeche
|
|
out = drop_map_labels(labels)
|
|
assert (out[40:60, 40:120] == CLASSES["strasse"]).all()
|
|
|
|
def test_five_percent_of_green_already_counts_as_a_real_area():
|
|
"""Die Grenze liegt bei 2 %. Mit 5 % Gruen muss die Flaeche bleiben - daran faellt auch
|
|
ein Tippfehler in LABEL_MAX_NATURAL auf (0.2 statt 0.02), der sonst durchginge."""
|
|
labels = np.full((200, 200), CLASSES["wasser"], dtype=np.uint8)
|
|
labels[40:140, 40:140] = CLASSES["stadt"] # 10.000 px, unter MAINLAND_MIN_SIZE
|
|
labels[40:62, 40:63] = CLASSES["gruen"] # 506 px, also gut 5 %
|
|
out = drop_map_labels(labels)
|
|
assert (out[40:140, 40:140] != CLASSES["wasser"]).all()
|