Zugeschnittene Fotos ohne Sucher bestimmen

Ein Handyfoto ist das Gegenteil einer Kameraaufnahme: Jemand zielt auf den
Vogel. Unterhalb von rund einem Viertel Bildflaeche findet SSD-MobileNet ihn
nicht mehr (12 % Flaeche: 38 % gefunden, 6 %: 10 %, 2 %: 0 %) — und auf einem
Zuschnitt, der nur noch aus Vogel besteht, meldet es sich ueberhaupt nicht.

run_zuschnitt() ueberspringt den Sucher. Gemessen an schwierigen Aufnahmen:
aus 'nichts gefunden' wird Haussperling 93 %.

Dabei faellt der Sucher als Torwaechter weg. Die erste Fassung liess
min_bird_score (0.05) entscheiden und gab fuer eine graue Wand Fischadler
20 % zurueck; ueber 148 Vogelbilder und 71 Hintergrundausschnitte kommen bei
dieser Schwelle 82 % der Hintergruende durch. Das Tor ist jetzt die
Sicherheit des besten Treffers (0.45: 96 % der Voegel, 14 % Hintergrund)
zusammen mit min_bird_score.

Alle Messungen stehen in docs/handyfotos.md, auch die dort als verworfen
dokumentierte Kachelsuche.

Die Pruefbilder liegen bewusst NICHT hier: Dieses Repo ist oeffentlich, die
Aufnahmen sind privat. Sie liegen in der Nextcloud unter
Projekte/Vogelalbum/Pruefbilder; 'pruefbilder/' steht in der .gitignore.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
christianandClaude Opus 5 committed 2026-10-24 17:34:52 +00:00
1 parent c11fe9ab04
commit dc7db4b603
4 files changed
+252

No files matched your search

+70
View File
@@ -956,3 +956,73 @@ class AnzeigeAusschnittTests(unittest.TestCase):
a = Box(x0=0.3, y0=0.3, x1=0.7, y1=0.7, score=0.9).anzeige_ausschnitt(im)
self.assertLessEqual(a.width, 60)
self.assertLessEqual(a.height, 40)
class ZuschnittTests(unittest.TestCase):
"""Das Tor fuer von Hand zugeschnittene Bilder.
🔑 Ohne Modelle gebaut: Geprueft wird die **Entscheidung**, nicht die
Erkennung. Genau dort lag der Fehler — die erste Fassung liess
``min_bird_score`` allein entscheiden und gab fuer eine graue Wand
„Fischadler 20 %" zurueck.
"""
def pipeline(self, sicherheit: float, bird_score: float):
"""Eine Pipeline, die ohne LiteRT auskommt.
``Pipeline.__new__`` statt ``Pipeline(...)``: Der Konstruktor laedt
beide Modelle, und die haben mit dieser Frage nichts zu tun.
"""
from detector.classify import Guess, Pipeline, Result
from detector.species_de import BY_SCIENTIFIC
art = BY_SCIENTIFIC["Parus major"]
class FakeClassifier:
def classify(self, img):
return Result(
guesses=[Guess(species=art, confidence=sicherheit)],
raw_top=[],
ms=1.0,
bird_score=bird_score,
)
pipe = Pipeline.__new__(Pipeline)
pipe.classifier = FakeClassifier()
pipe.min_bird_score = 0.05
pipe.min_zuschnitt_sicherheit = 0.45
return pipe
def test_sicherer_treffer_geht_durch(self):
ergebnis = self.pipeline(0.93, 0.11).run_zuschnitt(Image.new("RGB", (80, 80)))
self.assertIsNotNone(ergebnis)
self.assertEqual(ergebnis.best.species.german, "Kohlmeise")
def test_unsicherer_treffer_wird_verworfen(self):
"""Der Fall „graue Wand": bird_score reicht, Sicherheit nicht.
Gemessen ueber 71 Hintergrundausschnitte: Bei 0.05 bird_score kaemen
82 Prozent davon durch. Die Sicherheit ist hier das Tor.
"""
self.assertIsNone(
self.pipeline(0.20, 0.16).run_zuschnitt(Image.new("RGB", (80, 80)))
)
def test_fremde_art_wird_verworfen(self):
"""Sicher erkannt, aber keine heimische Art — dafuer bleibt
bird_score zustaendig."""
self.assertIsNone(
self.pipeline(0.95, 0.01).run_zuschnitt(Image.new("RGB", (80, 80)))
)
def test_beide_bedingungen_noetig(self):
"""Keine der beiden Schwellen allein darf genuegen."""
knapp_drueber = self.pipeline(0.46, 0.06)
self.assertIsNotNone(knapp_drueber.run_zuschnitt(Image.new("RGB", (80, 80))))
for sicherheit, bird_score in ((0.44, 0.06), (0.46, 0.04)):
with self.subTest(sicherheit=sicherheit, bird_score=bird_score):
self.assertIsNone(
self.pipeline(sicherheit, bird_score).run_zuschnitt(
Image.new("RGB", (80, 80))
)
)