Ausschnitt fuer die Sammlung weiter fassen
Christian ueber seine erste Amsel: "Der Zuschnitt der Fotos ist auch etwas merkwuerdig." Er hatte recht -- in der Kachel war nur Gefieder zu sehen, Kopf und Schwanz lagen ausserhalb. URSACHE: Derselbe enge Ausschnitt diente zwei Zwecken. Fuer den Klassifikator ist eng richtig, jedes Stueck Hintergrund verwaessert die Bestimmung. Fuer die Sammlung ist eng falsch: Dort steht das Bild in einer fast quadratischen Kachel, und die schneidet weg, was ueber das Quadrat hinausragt. Bei einem Vogel im Hochformat -- und so sitzt fast jeder -- bleibt die Bauchmitte uebrig, formatfuellend und unscharf. Neu: Box.anzeige_ausschnitt, quadratisch und mit mehr Rand. Was die Kachel abschneidet, ist jetzt Umgebung statt Vogel. `crop` bleibt unveraendert -- die Artbestimmung ist gemessen und soll so bleiben. An Christians echter Amsel geprueft: vorher 446x857 (Verhaeltnis 0,52), nachher 1176x1175. Im Vergleichsbild sitzt der ganze Vogel auf dem First statt einer Nahaufnahme des Bauchs. 5 Tests, auch fuer Voegel am Bildrand (sonst sitzt der Vogel ausserhalb seiner eigenen Kachel). 136 Detector-Tests gruen.
This commit is contained in:
1 parent
f7184d193c
commit
93f209ca12
4 files changed
+124
-2
No files matched your search
@@ -68,6 +68,50 @@ class Box:
|
|||||||
return img
|
return img
|
||||||
return img.crop((x0, y0, x1, y1))
|
return img.crop((x0, y0, x1, y1))
|
||||||
|
|
||||||
|
def anzeige_ausschnitt(self, img: Image.Image, pad: float = 0.35) -> Image.Image:
|
||||||
|
"""Derselbe Vogel, aber als Bild zum Ansehen statt zum Bestimmen.
|
||||||
|
|
||||||
|
🔑 **Der Klassifikator und das Auge wollen Verschiedenes.** ``crop``
|
||||||
|
liegt eng am Vogel, weil jedes Stück Hintergrund die Bestimmung
|
||||||
|
verwässert. Für die Sammlung ist genau das falsch: Dort steht der
|
||||||
|
Ausschnitt in einer fast quadratischen Kachel, und die schneidet links
|
||||||
|
und rechts weg, was über das Quadrat hinausragt.
|
||||||
|
|
||||||
|
Bei einem Vogel im Hochformat — und so sitzt fast jeder — bleibt davon
|
||||||
|
die Bauchmitte übrig, formatfüllend und unscharf. Christian am
|
||||||
|
14.10.2026 über seine erste Amsel: *„Der Zuschnitt der Fotos ist auch
|
||||||
|
etwas merkwürdig."* Er hatte recht: Vom Vogel war nur Gefieder zu
|
||||||
|
sehen, Kopf und Schwanz lagen außerhalb.
|
||||||
|
|
||||||
|
Deshalb hier mehr Rand **und** ein Ausschnitt in Richtung Quadrat: Was
|
||||||
|
die Kachel abschneidet, ist dann Umgebung statt Vogel.
|
||||||
|
|
||||||
|
⚠️ Absichtlich eine eigene Methode. ``crop`` hier zu ändern hieße, die
|
||||||
|
Artbestimmung zu ändern — und die ist gemessen und soll so bleiben.
|
||||||
|
"""
|
||||||
|
w, h = img.size
|
||||||
|
bx0, by0 = self.x0 * w, self.y0 * h
|
||||||
|
bx1, by1 = self.x1 * w, self.y1 * h
|
||||||
|
bw, bh = bx1 - bx0, by1 - by0
|
||||||
|
if bw <= 0 or bh <= 0:
|
||||||
|
return img
|
||||||
|
|
||||||
|
# Erst Rand, dann die kürzere Seite auf die längere ziehen.
|
||||||
|
seite = max(bw, bh) * (1 + 2 * pad)
|
||||||
|
mx, my = (bx0 + bx1) / 2, (by0 + by1) / 2
|
||||||
|
|
||||||
|
# ⚠️ Nicht über das Bild hinaus, und die Mitte nachführen statt den
|
||||||
|
# Ausschnitt zu stauchen — sonst sitzt ein Vogel am Bildrand
|
||||||
|
# außermittig in seiner eigenen Kachel.
|
||||||
|
seite = min(seite, float(w), float(h))
|
||||||
|
mx = min(max(mx, seite / 2), w - seite / 2)
|
||||||
|
my = min(max(my, seite / 2), h - seite / 2)
|
||||||
|
|
||||||
|
return img.crop((
|
||||||
|
int(mx - seite / 2), int(my - seite / 2),
|
||||||
|
int(mx + seite / 2), int(my + seite / 2),
|
||||||
|
))
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def area(self) -> float:
|
def area(self) -> float:
|
||||||
return max(0.0, self.x1 - self.x0) * max(0.0, self.y1 - self.y0)
|
return max(0.0, self.x1 - self.x0) * max(0.0, self.y1 - self.y0)
|
||||||
|
|||||||
@@ -185,7 +185,10 @@ class Warteschlange:
|
|||||||
box, result = self.pipeline.run(bild)
|
box, result = self.pipeline.run(bild)
|
||||||
# Den Ausschnitt mitschicken, damit die Sammlung dasselbe Bild
|
# Den Ausschnitt mitschicken, damit die Sammlung dasselbe Bild
|
||||||
# zeigt wie bei einer Sichtung von der Kamera.
|
# zeigt wie bei einer Sichtung von der Kamera.
|
||||||
ausschnitt = box.crop(bild) if box is not None else None
|
# ⚠️ `anzeige_ausschnitt`, nicht `crop`: Der enge Schnitt ist
|
||||||
|
# fuer den Klassifikator gedacht; in der fast quadratischen
|
||||||
|
# Kachel der Sammlung bleibt davon nur die Bauchmitte uebrig.
|
||||||
|
ausschnitt = box.anzeige_ausschnitt(bild) if box is not None else None
|
||||||
self._melden(foto["id"], ausschnitt, result, box)
|
self._melden(foto["id"], ausschnitt, result, box)
|
||||||
fertig += 1
|
fertig += 1
|
||||||
except Exception as exc: # absichtlich breit, siehe Docstring
|
except Exception as exc: # absichtlich breit, siehe Docstring
|
||||||
|
|||||||
+4
-1
@@ -221,12 +221,15 @@ class Watcher:
|
|||||||
# Bestes Foto: Schärfe, gewichtet mit der Detektor-Sicherheit. Ein
|
# Bestes Foto: Schärfe, gewichtet mit der Detektor-Sicherheit. Ein
|
||||||
# gestochen scharfes Bild von einem halb verdeckten Vogel nützt weniger
|
# gestochen scharfes Bild von einem halb verdeckten Vogel nützt weniger
|
||||||
# als ein leicht weicheres, auf dem er ganz zu sehen ist.
|
# als ein leicht weicheres, auf dem er ganz zu sehen ist.
|
||||||
|
# Schaerfe am ENGEN Ausschnitt messen: Umgebung soll die Bewertung
|
||||||
|
# nicht verwaessern. Gespeichert wird dann der weitere -- siehe
|
||||||
|
# Box.anzeige_ausschnitt.
|
||||||
crop = box.crop(frame)
|
crop = box.crop(frame)
|
||||||
score = sharpness(crop) * (0.5 + 0.5 * box.score)
|
score = sharpness(crop) * (0.5 + 0.5 * box.score)
|
||||||
if score > self._best_sharp:
|
if score > self._best_sharp:
|
||||||
self._best_sharp = score
|
self._best_sharp = score
|
||||||
self._best_image = frame.copy()
|
self._best_image = frame.copy()
|
||||||
self._best_crop = crop
|
self._best_crop = box.anzeige_ausschnitt(frame)
|
||||||
|
|
||||||
def _vote(self, box: Box, result: Result) -> None:
|
def _vote(self, box: Box, result: Result) -> None:
|
||||||
# Gewicht: wie sicher ist der Detektor, wie groß ist der Vogel im Bild,
|
# Gewicht: wie sicher ist der Detektor, wie groß ist der Vogel im Bild,
|
||||||
|
|||||||
@@ -885,3 +885,75 @@ class UebernimmTests(unittest.TestCase):
|
|||||||
from detector.species_de import catalog
|
from detector.species_de import catalog
|
||||||
|
|
||||||
self.assertEqual(self.pipeline.uebernimm(arten=catalog({"garten"})), [])
|
self.assertEqual(self.pipeline.uebernimm(arten=catalog({"garten"})), [])
|
||||||
|
|
||||||
|
|
||||||
|
class AnzeigeAusschnittTests(unittest.TestCase):
|
||||||
|
"""🔑 Der Klassifikator und das Auge wollen Verschiedenes.
|
||||||
|
|
||||||
|
``crop`` liegt eng am Vogel, weil Hintergrund die Bestimmung verwaessert.
|
||||||
|
In der fast quadratischen Kachel der Sammlung schneidet genau das links
|
||||||
|
und rechts den Vogel weg — bei einem Vogel im Hochformat bleibt die
|
||||||
|
Bauchmitte uebrig. Christian am 14.10.2026: „Der Zuschnitt der Fotos ist
|
||||||
|
auch etwas merkwuerdig."
|
||||||
|
"""
|
||||||
|
|
||||||
|
def bild(self, groesse=(1800, 2400)):
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
|
return Image.new("RGB", groesse, (120, 160, 200))
|
||||||
|
|
||||||
|
def test_anzeige_ist_quadratisch(self):
|
||||||
|
from detector.classify import Box
|
||||||
|
|
||||||
|
# Ein hochkant sitzender Vogel, schmal und hoch.
|
||||||
|
box = Box(x0=0.40, y0=0.30, x1=0.52, y1=0.62, score=0.9)
|
||||||
|
im = self.bild()
|
||||||
|
eng = box.crop(im)
|
||||||
|
anzeige = box.anzeige_ausschnitt(im)
|
||||||
|
|
||||||
|
self.assertLess(eng.width / eng.height, 0.7, "Testbox ist nicht hochkant")
|
||||||
|
self.assertAlmostEqual(anzeige.width / anzeige.height, 1.0, places=1)
|
||||||
|
|
||||||
|
def test_anzeige_enthaelt_den_ganzen_vogel(self):
|
||||||
|
from detector.classify import Box
|
||||||
|
|
||||||
|
box = Box(x0=0.40, y0=0.30, x1=0.52, y1=0.62, score=0.9)
|
||||||
|
im = self.bild()
|
||||||
|
anzeige = box.anzeige_ausschnitt(im)
|
||||||
|
# Mindestens so hoch wie der Vogel plus Rand.
|
||||||
|
vogel_hoehe = (0.62 - 0.30) * im.height
|
||||||
|
self.assertGreater(anzeige.height, vogel_hoehe)
|
||||||
|
|
||||||
|
def test_crop_bleibt_unveraendert_eng(self):
|
||||||
|
"""Die Artbestimmung ist gemessen und darf sich nicht mitaendern."""
|
||||||
|
from detector.classify import Box
|
||||||
|
|
||||||
|
box = Box(x0=0.40, y0=0.30, x1=0.52, y1=0.62, score=0.9)
|
||||||
|
im = self.bild()
|
||||||
|
eng = box.crop(im)
|
||||||
|
erwartet_breite = (0.52 - 0.40) * im.width * 1.24 # pad 0.12 beidseits
|
||||||
|
self.assertAlmostEqual(eng.width, erwartet_breite, delta=3)
|
||||||
|
|
||||||
|
def test_am_bildrand_bleibt_der_ausschnitt_im_bild(self):
|
||||||
|
"""⚠️ Sonst sitzt ein Vogel am Rand ausserhalb seiner eigenen Kachel."""
|
||||||
|
from detector.classify import Box
|
||||||
|
|
||||||
|
im = self.bild()
|
||||||
|
for box in (
|
||||||
|
Box(x0=0.00, y0=0.00, x1=0.08, y1=0.10, score=0.9),
|
||||||
|
Box(x0=0.92, y0=0.90, x1=1.00, y1=1.00, score=0.9),
|
||||||
|
):
|
||||||
|
with self.subTest(box=box):
|
||||||
|
a = box.anzeige_ausschnitt(im)
|
||||||
|
self.assertGreater(a.width, 0)
|
||||||
|
self.assertLessEqual(a.width, im.width)
|
||||||
|
self.assertLessEqual(a.height, im.height)
|
||||||
|
self.assertAlmostEqual(a.width / a.height, 1.0, places=1)
|
||||||
|
|
||||||
|
def test_winziges_bild_sprengt_nichts(self):
|
||||||
|
from detector.classify import Box
|
||||||
|
|
||||||
|
im = self.bild((60, 40))
|
||||||
|
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)
|
||||||
Reference in new issue
Block a user