HEIC-Fotos annehmen und den Ausschnitt weiter fassen
iPhones fotografieren HEIC; ohne pillow-heif wies die Bildpruefung jedes Foto ab. Dazu: ein erkannter Vogel ohne bestimmbare Art wird nicht mehr als "kein Vogel" gemeldet, und der Ausschnitt fuer die Sammlung ist quadratisch.
This commit is contained in:
1 parent
dbec78aa7a
commit
f572c20080
9 files changed
+285
-8
No files matched your search
@@ -68,6 +68,50 @@ class Box:
|
||||
return img
|
||||
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
|
||||
def area(self) -> float:
|
||||
return max(0.0, self.x1 - self.x0) * max(0.0, self.y1 - self.y0)
|
||||
|
||||
@@ -88,17 +88,41 @@ class Warteschlange:
|
||||
|
||||
# -- Bearbeiten -------------------------------------------------------
|
||||
|
||||
def _melden(self, foto_id: int, ausschnitt, result) -> None:
|
||||
def _melden(self, foto_id: int, ausschnitt, result, box=None) -> None:
|
||||
import requests
|
||||
|
||||
# ⚠️ ``Result.best`` ist eine Property, kein Aufruf — ``result.best()``
|
||||
# scheitert erst zur Laufzeit und genau dann, wenn ein Vogel
|
||||
# erkannt wurde.
|
||||
bester = result.best if result else None
|
||||
|
||||
# 🔴 **„Vogel ja, Art nein" ist etwas anderes als „kein Vogel" — und
|
||||
# ging hier verloren.**
|
||||
#
|
||||
# ``Pipeline.run`` gibt in DREI Fällen ``result = None`` zurück: kein
|
||||
# Fund, Beinahe-Treffer unter der Schwelle, und — der interessante —
|
||||
# ein sicher erkannter Vogel, dessen Art keine heimische ist. Die Box
|
||||
# unterscheidet die Fälle, nur hat sie vorher niemand angesehen.
|
||||
#
|
||||
# Folge am 14.10.2026: Ein Foto mit einer Meise im Gegenlicht wurde
|
||||
# mit 93 % als Vogel erkannt und kam in der App als „Kein Vogel
|
||||
# erkannt. Auf dem Foto war nichts zu finden" an. Christian hat
|
||||
# daraufhin zugeschnitten — gegen ein Problem, das nie der Zuschnitt
|
||||
# war. **Eine falsche Auskunft ist teurer als keine.**
|
||||
#
|
||||
# ``box.score`` ist genau die Zahl, die „da ist ein Vogel" bedeutet;
|
||||
# sie gehört in ``mean_bird_score``, dessen Sinn der Server selbst so
|
||||
# beschreibt: Vogelwert 0 heißt „auf keinem Bild war ein Vogel".
|
||||
vogelwert = 0.0
|
||||
if result is not None:
|
||||
vogelwert = result.bird_score
|
||||
elif box is not None and self.pipeline.zaehlt_als_vogel(box):
|
||||
vogelwert = box.score
|
||||
|
||||
nutzlast = {
|
||||
"species": bester.species.slug if bester else None,
|
||||
"confidence": round(bester.confidence, 4) if bester else 0.0,
|
||||
"mean_bird_score": round(result.bird_score, 4) if result else 0.0,
|
||||
"mean_bird_score": round(vogelwert, 4),
|
||||
"votes": {g.species.slug: round(g.confidence, 4)
|
||||
for g in (result.guesses[:5] if result else [])},
|
||||
}
|
||||
@@ -161,8 +185,11 @@ class Warteschlange:
|
||||
box, result = self.pipeline.run(bild)
|
||||
# Den Ausschnitt mitschicken, damit die Sammlung dasselbe Bild
|
||||
# zeigt wie bei einer Sichtung von der Kamera.
|
||||
ausschnitt = box.crop(bild) if box is not None else None
|
||||
self._melden(foto["id"], ausschnitt, result)
|
||||
# ⚠️ `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)
|
||||
fertig += 1
|
||||
except Exception as exc: # absichtlich breit, siehe Docstring
|
||||
print(f"[warteschlange] Foto {foto['id']} übersprungen: {exc}")
|
||||
|
||||
+4
-1
@@ -221,12 +221,15 @@ class Watcher:
|
||||
# Bestes Foto: Schärfe, gewichtet mit der Detektor-Sicherheit. Ein
|
||||
# gestochen scharfes Bild von einem halb verdeckten Vogel nützt weniger
|
||||
# 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)
|
||||
score = sharpness(crop) * (0.5 + 0.5 * box.score)
|
||||
if score > self._best_sharp:
|
||||
self._best_sharp = score
|
||||
self._best_image = frame.copy()
|
||||
self._best_crop = crop
|
||||
self._best_crop = box.anzeige_ausschnitt(frame)
|
||||
|
||||
def _vote(self, box: Box, result: Result) -> None:
|
||||
# Gewicht: wie sicher ist der Detektor, wie groß ist der Vogel im Bild,
|
||||
|
||||
Reference in new issue
Block a user