Zuschnitt als Regel statt als gespeichertes Bild

Die Sichtung merkt sich jetzt, WO der Vogel im Foto sitzt (vier Zahlen); die
Anzeige schneidet daraus bei jedem Aufruf. Eine geaenderte Regel wirkt damit
sofort auf den ganzen Bestand. Altbestand faellt auf das alte Bild zurueck.
This commit is contained in:
christian committed 2026-10-14 18:41:52 +00:00
1 parent f572c20080
commit a97f066e92
14 files changed
+421 -19

No files matched your search

+55
View File
@@ -714,6 +714,33 @@ class Sighting(models.Model):
votes = models.JSONField(default=dict, blank=True)
mean_bird_score = models.FloatField(default=0.0)
# 🔑 **Wo der Vogel im Bild sitzt — als vier Zahlen, nicht als zweites
# Bild.**
#
# Bis zum 14.10.2026 speicherte das Projekt neben dem Foto einen fertig
# geschnittenen Ausschnitt. Der Zuschnitt war damit für immer festgelegt:
# Eine bessere Regel half nur neuen Sichtungen, und alle vorhandenen
# mussten einzeln nachgezogen werden.
#
# Aufgefallen ist es an Handyfotos. Dort ist der Vogel klein im Bild, der
# enge Ausschnitt entsprechend schmal und hoch — und in einer quadratischen
# Kachel blieb davon die Bauchmitte übrig, formatfüllend und unscharf.
# Christians Urteil, und es war das richtige: *„Mir geht es darum, dass ein
# Nutzer das so nicht verwenden kann."*
#
# Mit den Koordinaten ist der Zuschnitt eine **Regel statt einer Datei**:
# Die Anzeige schneidet bei jedem Aufruf neu aus dem Original. Eine
# geänderte Regel wirkt sofort auf den gesamten Bestand, Website und App
# dürfen verschieden schneiden, und niemand braucht den Detector dafür.
#
# Werte sind relativ (0..1), damit sie eine Verkleinerung des Originals
# überleben. Leer heißt: von vor dieser Änderung — dann gilt weiter das
# gespeicherte ``crop``.
box_x0 = models.FloatField("Vogel: links", null=True, blank=True)
box_y0 = models.FloatField("Vogel: oben", null=True, blank=True)
box_x1 = models.FloatField("Vogel: rechts", null=True, blank=True)
box_y1 = models.FloatField("Vogel: unten", null=True, blank=True)
is_first_of_species = models.BooleanField(default=False)
xp_awarded = models.PositiveIntegerField(default=0)
@@ -743,6 +770,34 @@ class Sighting(models.Model):
"""Handkorrektur schlägt Modell."""
return self.confirmed_species or self.species
@property
def vogel_box(self) -> tuple[float, float, float, float] | None:
"""Wo der Vogel sitzt, relativ — oder ``None`` bei Altbestand."""
werte = (self.box_x0, self.box_y0, self.box_x1, self.box_y1)
if any(w is None for w in werte):
return None
x0, y0, x1, y1 = werte
if x1 <= x0 or y1 <= y0:
return None
return (x0, y0, x1, y1)
@property
def kachel_url(self) -> str:
"""Das Bild für eine Kachel: aus dem Original geschnitten.
🔑 Rückfall auf das gespeicherte ``crop`` für Sichtungen von vor der
Umstellung, danach auf das Original. So bleibt jede alte Sammlung
anzeigbar, ohne dass jemand etwas nachziehen muss.
"""
from .zuschnitt import adresse
neu = adresse(self)
if neu:
return neu
if self.crop:
return self.crop.url
return self.image.url if self.image else ""
@property
def duration_s(self) -> float:
return (self.ended_at - self.started_at).total_seconds()