From a97f066e920fccc9a3df0cb4c498a39bacec2eb6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20G=C3=A4rtner?= Date: Wed, 14 Oct 2026 18:41:52 +0000 Subject: [PATCH] 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. --- detector/sink.py | 3 + detector/warteschlange.py | 5 + detector/watcher.py | 6 ++ web/dex/api.py | 27 +++++ web/dex/app_api.py | 12 ++- web/dex/migrations/0016_vogel_box.py | 33 ++++++ web/dex/models.py | 55 ++++++++++ web/dex/test_zuschnitt.py | 149 +++++++++++++++++++++++++++ web/dex/zuschnitt.py | 120 +++++++++++++++++++++ web/templates/dex/dex.html | 4 +- web/templates/dex/home.html | 16 +-- web/templates/dex/sightings.html | 4 +- web/templates/dex/species.html | 4 +- web/templates/dex/widget.html | 2 +- 14 files changed, 421 insertions(+), 19 deletions(-) create mode 100644 web/dex/migrations/0016_vogel_box.py create mode 100644 web/dex/test_zuschnitt.py create mode 100644 web/dex/zuschnitt.py diff --git a/detector/sink.py b/detector/sink.py index 0708f78..6f9806c 100644 --- a/detector/sink.py +++ b/detector/sink.py @@ -35,6 +35,9 @@ def payload_of(sighting: Sighting) -> dict: "frames_with_bird": sighting.frames_with_bird, "votes": {k: round(v, 4) for k, v in sighting.votes.items()}, "mean_bird_score": round(sighting.mean_bird_score, 4), + # Vier Zahlen statt eines zweiten Bildes: siehe web/dex/zuschnitt.py. + "box": ([round(w, 5) for w in sighting.best_box] + if getattr(sighting, "best_box", None) else None), } diff --git a/detector/warteschlange.py b/detector/warteschlange.py index 354696c..a5910fd 100644 --- a/detector/warteschlange.py +++ b/detector/warteschlange.py @@ -126,6 +126,11 @@ class Warteschlange: "votes": {g.species.slug: round(g.confidence, 4) for g in (result.guesses[:5] if result else [])}, } + # Wo der Vogel sitzt. Die Sammlung schneidet daraus selbst zu -- + # siehe web/dex/zuschnitt.py. + if box is not None: + nutzlast["box"] = [round(box.x0, 5), round(box.y0, 5), + round(box.x1, 5), round(box.y1, 5)] dateien = {} if ausschnitt is not None: dateien["crop"] = ("crop.jpg", _encode(ausschnitt, 640), "image/jpeg") diff --git a/detector/watcher.py b/detector/watcher.py index b778982..534f55d 100644 --- a/detector/watcher.py +++ b/detector/watcher.py @@ -36,6 +36,9 @@ class Sighting: best_crop: Image.Image | None best_sharpness: float mean_bird_score: float + # Wo der Vogel im besten Bild sitzt, relativ. Die Sammlung schneidet + # daraus selbst zu -- siehe web/dex/zuschnitt.py. + best_box: tuple[float, float, float, float] | None = None @property def duration_s(self) -> float: @@ -127,6 +130,7 @@ class Watcher: self._best_sharp = -1.0 self._best_image: Image.Image | None = None self._best_crop: Image.Image | None = None + self._best_box: tuple[float, float, float, float] | None = None self._bird_scores: list[float] = [] self._frame_i = 0 @@ -230,6 +234,7 @@ class Watcher: self._best_sharp = score self._best_image = frame.copy() self._best_crop = box.anzeige_ausschnitt(frame) + self._best_box = (box.x0, box.y0, box.x1, box.y1) def _vote(self, box: Box, result: Result) -> None: # Gewicht: wie sicher ist der Detektor, wie groß ist der Vogel im Bild, @@ -279,6 +284,7 @@ class Watcher: frames_with_bird=self._bird_frames, best_image=self._best_image, best_crop=self._best_crop, + best_box=self._best_box, best_sharpness=max(0.0, self._best_sharp), mean_bird_score=mittel, ) diff --git a/web/dex/api.py b/web/dex/api.py index f2660c4..31b0971 100644 --- a/web/dex/api.py +++ b/web/dex/api.py @@ -76,6 +76,30 @@ def config(request: HttpRequest) -> JsonResponse: return JsonResponse(kamera.konfiguration()) +def _box_uebernehmen(sighting, payload) -> None: + """Wo der Vogel im Bild sitzt, als vier relative Zahlen. + + 🔑 Damit ist der Zuschnitt eine Regel statt einer Datei: Die Anzeige + schneidet aus dem Original, und eine bessere Regel wirkt auf den ganzen + Bestand. Siehe ``dex/zuschnitt.py``. + + ⚠️ Nur vollständige und plausible Vierecke übernehmen. Ein halbes Viereck + ist keins, und eine vertauschte Kante ergäbe einen leeren Ausschnitt — + sichtbar würde das erst als graue Kachel, weit weg von hier. + """ + box = payload.get("box") + if not isinstance(box, (list, tuple)) or len(box) != 4: + return + try: + x0, y0, x1, y1 = (float(w) for w in box) + except (TypeError, ValueError): + return + if not (0.0 <= x0 < x1 <= 1.0 and 0.0 <= y0 < y1 <= 1.0): + return + sighting.box_x0, sighting.box_y0 = x0, y0 + sighting.box_x1, sighting.box_y1 = x1, y1 + + def _parse_dt(value: str | None) -> datetime | None: if not value: return None @@ -182,6 +206,8 @@ def ingest(request: HttpRequest) -> JsonResponse: # Der Token EINER Kamera genügt, um Dateien unter der öffentlichen # Medienadresse abzulegen — und ein kompromittierter Standort ist genau # der Fall, für den die Token je Kamera getrennt sind. Siehe bilder.py. + _box_uebernehmen(sighting, payload) + try: if "image" in request.FILES: sighting.image = saeubern(request.FILES["image"]) @@ -567,6 +593,7 @@ def bestimmt_melden(request: HttpRequest, sichtung_id: int) -> JsonResponse: # beruht: Artsicherheit 0 heißt „welche Art?", Vogelwert 0 heißt „gar kein # Vogel". Siehe die Begründung bei ``ingest``. sighting.frames_with_bird = 1 if (species or sighting.mean_bird_score > 0) else 0 + _box_uebernehmen(sighting, payload) sighting.wartet_auf_bestimmung = False if "crop" in request.FILES: sighting.crop = request.FILES["crop"] diff --git a/web/dex/app_api.py b/web/dex/app_api.py index 61371ce..3d0df03 100644 --- a/web/dex/app_api.py +++ b/web/dex/app_api.py @@ -136,9 +136,8 @@ def _art_kurz(request: HttpRequest, art: Species, eintrag: DexEntry | None) -> d "zuletzt": eintrag.last_seen.isoformat(), "neu": eintrag.is_new, "foto": ( - _bild(request, eintrag.best_sighting.crop) - or _bild(request, eintrag.best_sighting.image) - if eintrag.best_sighting + request.build_absolute_uri(eintrag.best_sighting.kachel_url) + if eintrag.best_sighting and eintrag.best_sighting.kachel_url else None ), } @@ -189,7 +188,12 @@ def _sichtung_kurz(request: HttpRequest, s: Sighting) -> dict: else None ), "foto": _bild(request, s.image), - "ausschnitt": _bild(request, s.crop), + # 🔑 Der Ausschnitt wird aus dem Original gerechnet, nicht als + # zweites Bild gespeichert — siehe dex/zuschnitt.py. Damit schneidet + # eine geänderte Regel sofort den ganzen Bestand neu, und die App + # bekommt dieselbe Kachel wie die Website. + "ausschnitt": (request.build_absolute_uri(s.kachel_url) + if s.kachel_url else None), } diff --git a/web/dex/migrations/0016_vogel_box.py b/web/dex/migrations/0016_vogel_box.py new file mode 100644 index 0000000..e7cac03 --- /dev/null +++ b/web/dex/migrations/0016_vogel_box.py @@ -0,0 +1,33 @@ +# Generated by Django 6.1 on 2026-10-14 18:25 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('dex', '0015_camera_min_mean_bird_score'), + ] + + operations = [ + migrations.AddField( + model_name='sighting', + name='box_x0', + field=models.FloatField(blank=True, null=True, verbose_name='Vogel: links'), + ), + migrations.AddField( + model_name='sighting', + name='box_x1', + field=models.FloatField(blank=True, null=True, verbose_name='Vogel: rechts'), + ), + migrations.AddField( + model_name='sighting', + name='box_y0', + field=models.FloatField(blank=True, null=True, verbose_name='Vogel: oben'), + ), + migrations.AddField( + model_name='sighting', + name='box_y1', + field=models.FloatField(blank=True, null=True, verbose_name='Vogel: unten'), + ), + ] diff --git a/web/dex/models.py b/web/dex/models.py index 566dd2c..fdf2053 100644 --- a/web/dex/models.py +++ b/web/dex/models.py @@ -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() diff --git a/web/dex/test_zuschnitt.py b/web/dex/test_zuschnitt.py new file mode 100644 index 0000000..0173b22 --- /dev/null +++ b/web/dex/test_zuschnitt.py @@ -0,0 +1,149 @@ +"""Tests für die Zuschnitt-Regel. + +🔑 Der Kern der Umstellung: Der Ausschnitt ist eine **Regel**, keine Datei. +Deshalb prüfen diese Tests nicht „ein Bild entsteht", sondern die Eigenschaft, +wegen der wir umgebaut haben — dass sich der Zuschnitt für den ganzen Bestand +ändern lässt, auch rückwirkend. +""" + +from __future__ import annotations + +import io +from datetime import timedelta + +from django.core.files.uploadedfile import SimpleUploadedFile +from django.test import TestCase +from django.utils import timezone +from PIL import Image + +from .models import Sighting +from .zuschnitt import quadrat + + +def foto(groesse=(1800, 2400)) -> SimpleUploadedFile: + im = Image.new("RGB", groesse, (120, 160, 200)) + puffer = io.BytesIO() + im.save(puffer, format="JPEG") + return SimpleUploadedFile("v.jpg", puffer.getvalue(), "image/jpeg") + + +class QuadratTests(TestCase): + """Die Rechnung allein — ohne Dateien, ohne Datenbank.""" + + def test_ergebnis_ist_quadratisch(self): + """Weil die Kachel quadratisch ist. Sonst schneidet sie den Vogel ab.""" + x0, y0, x1, y1 = quadrat((0.40, 0.30, 0.52, 0.62), 1800, 2400) + self.assertEqual(x1 - x0, y1 - y0) + + def test_der_vogel_passt_hinein(self): + box = (0.40, 0.30, 0.52, 0.62) + x0, y0, x1, y1 = quadrat(box, 1800, 2400) + self.assertLessEqual(x0, box[0] * 1800) + self.assertLessEqual(y0, box[1] * 2400) + self.assertGreaterEqual(x1, box[2] * 1800) + self.assertGreaterEqual(y1, box[3] * 2400) + + def test_am_rand_bleibt_das_fenster_im_bild(self): + """⚠️ Sonst sitzt ein Vogel am Bildrand ausserhalb seiner Kachel.""" + for box in ((0.0, 0.0, 0.08, 0.10), (0.92, 0.90, 1.0, 1.0)): + with self.subTest(box=box): + x0, y0, x1, y1 = quadrat(box, 1800, 2400) + self.assertGreaterEqual(x0, 0) + self.assertGreaterEqual(y0, 0) + self.assertLessEqual(x1, 1800) + self.assertLessEqual(y1, 2400) + self.assertEqual(x1 - x0, y1 - y0) + + def test_mehr_rand_heisst_groesseres_fenster(self): + """Das ist die Stellschraube, um die es bei der Umstellung ging.""" + eng = quadrat((0.4, 0.3, 0.5, 0.6), 1800, 2400, rand=0.05) + weit = quadrat((0.4, 0.3, 0.5, 0.6), 1800, 2400, rand=0.60) + self.assertGreater(weit[2] - weit[0], eng[2] - eng[0]) + + def test_fenster_wird_nie_groesser_als_das_bild(self): + x0, y0, x1, y1 = quadrat((0.0, 0.0, 1.0, 1.0), 1800, 2400, rand=2.0) + self.assertLessEqual(x1 - x0, 1800) + self.assertLessEqual(y1 - y0, 2400) + + +class KachelTests(TestCase): + def sichtung(self, **kw) -> Sighting: + jetzt = timezone.now() + s = Sighting(started_at=jetzt, ended_at=jetzt + timedelta(seconds=5), **kw) + s.image = foto() + s.save() + return s + + def test_mit_koordinaten_entsteht_eine_kachel(self): + s = self.sichtung(box_x0=0.40, box_y0=0.30, box_x1=0.52, box_y1=0.62) + self.assertIn("zuschnitte/", s.kachel_url) + + def test_die_kachel_ist_quadratisch(self): + s = self.sichtung(box_x0=0.40, box_y0=0.30, box_x1=0.52, box_y1=0.62) + from pathlib import Path + + from django.conf import settings + + pfad = Path(settings.MEDIA_ROOT) / s.kachel_url.split("/media/")[-1] + with Image.open(pfad) as im: + self.assertEqual(im.width, im.height) + + def test_ohne_koordinaten_gilt_der_alte_ausschnitt(self): + """Altbestand muss anzeigbar bleiben, ohne dass jemand nachzieht.""" + s = self.sichtung() + s.crop = foto((300, 300)) + s.save() + self.assertEqual(s.kachel_url, s.crop.url) + + def test_ohne_alles_bleibt_das_original(self): + s = self.sichtung() + self.assertEqual(s.kachel_url, s.image.url) + + def test_halbe_koordinaten_zaehlen_nicht(self): + """Ein halbes Viereck ist keins — sonst entstünde eine graue Kachel.""" + s = self.sichtung(box_x0=0.4, box_y0=0.3, box_x1=None, box_y1=0.62) + self.assertIsNone(s.vogel_box) + + def test_verdrehte_kanten_zaehlen_nicht(self): + s = self.sichtung(box_x0=0.6, box_y0=0.3, box_x1=0.4, box_y1=0.62) + self.assertIsNone(s.vogel_box) + + +class RegelAendernTests(TestCase): + """🔑 Die Eigenschaft, wegen der umgebaut wurde. + + Eine geänderte Regel muss den ganzen Bestand erfassen — nicht nur das, was + danach entsteht. Genau das konnte die alte Lösung nicht. + """ + + def test_mehr_rand_erzeugt_eine_neue_kachel_fuer_alte_sichtungen(self): + from . import zuschnitt + + jetzt = timezone.now() + s = Sighting(started_at=jetzt, ended_at=jetzt, + box_x0=0.40, box_y0=0.30, box_x1=0.52, box_y1=0.62) + s.image = foto() + s.save() + + vorher = s.kachel_url + + alt_rand, alt_regel = zuschnitt.RAND, zuschnitt.REGEL + try: + zuschnitt.RAND = 0.8 + zuschnitt.REGEL = alt_regel + 1 + nachher = s.kachel_url + finally: + zuschnitt.RAND, zuschnitt.REGEL = alt_rand, alt_regel + + # Andere Adresse, anderes Bild — ohne dass jemand die Sichtung angefasst hat. + self.assertNotEqual(vorher, nachher) + + from pathlib import Path + + from django.conf import settings + + def kante(url): + with Image.open(Path(settings.MEDIA_ROOT) / url.split("/media/")[-1]) as im: + return im.width + + self.assertGreater(kante(nachher), 0) diff --git a/web/dex/zuschnitt.py b/web/dex/zuschnitt.py new file mode 100644 index 0000000..ca2421c --- /dev/null +++ b/web/dex/zuschnitt.py @@ -0,0 +1,120 @@ +"""Wie ein Vogelfoto in einer Kachel aussieht. + +🔑 **Der Zuschnitt ist eine Regel, keine Datei.** Das Original liegt ohnehin +auf der Platte, und wo der Vogel darin sitzt, steht als vier Zahlen an der +Sichtung. Daraus lässt sich der Ausschnitt jederzeit neu rechnen — bei jedem +Aufruf, für jede Sichtung, auch für die von vorletztem Jahr. + +Vorher war es umgekehrt: Der Detector schnitt ein zweites Bild zu und legte es +daneben. Damit war der Zuschnitt für immer festgelegt. Eine bessere Regel half +nur neuen Sichtungen, alle vorhandenen hätte man einzeln nachziehen müssen — +und das auf jeder Anlage, die es irgendwo gibt. + +⚠️ **Der Unterschied ist nicht Bequemlichkeit, sondern Benutzbarkeit.** An +Handyfotos fiel auf, woran die alte Lösung scheitert: Dort ist der Vogel klein +im Bild, der enge Ausschnitt entsprechend schmal und hoch. In einer +quadratischen Kachel blieb davon die Bauchmitte übrig — formatfüllend und +unscharf, ohne Kopf und ohne Schwanz. + +Das Ergebnis wird als Datei zwischengespeichert, weil Zuschneiden Rechenzeit +kostet und ein Album viele Kacheln hat. **Der Zwischenspeicher ist +wegwerfbar**: Löscht man ihn, entsteht beim nächsten Aufruf alles neu — und +genau das ist der Weg, eine geänderte Regel auszurollen. +""" + +from __future__ import annotations + +import hashlib +from pathlib import Path + +from django.conf import settings +from PIL import Image + +# Wie viel Luft um den Vogel. 0.35 heißt: Die Kante des Ausschnitts ist 35 % +# der Vogelgröße von ihm entfernt. +# +# 🔑 Diese Zahl ist der ganze Sinn der Umstellung. Sie zu ändern und den +# Zwischenspeicher zu leeren ändert jede Kachel im Bestand. +RAND = 0.35 + +# Kantenlänge des erzeugten Bildes. Die Kacheln sind kleiner; größer zu +# speichern lohnt nur für Bildschirme mit hoher Punktdichte. +KANTE = 600 + +# Unterordner im Medienverzeichnis. Nicht ins Repo, nicht in die Sicherung — +# alles hier lässt sich aus Original und Koordinaten neu erzeugen. +ORDNER = "zuschnitte" + +# Steigt diese Zahl, bekommen alle Dateien neue Namen und werden neu +# gerechnet. So rollt man eine geänderte Regel aus, ohne etwas zu löschen. +REGEL = 1 + + +def _ziel(sichtung) -> Path: + kennung = hashlib.sha1( + f"{sichtung.pk}-{sichtung.image.name}-{RAND}-{KANTE}-{REGEL}".encode() + ).hexdigest()[:16] + return Path(settings.MEDIA_ROOT) / ORDNER / f"{sichtung.pk}-{kennung}.jpg" + + +def quadrat(box: tuple[float, float, float, float], breite: int, hoehe: int, + rand: float = RAND) -> tuple[int, int, int, int]: + """Aus relativen Vogel-Koordinaten ein quadratisches Fenster in Pixeln. + + ⚠️ **Die Mitte wird nachgeführt, nicht der Ausschnitt gestaucht.** Sitzt + der Vogel am Bildrand, verschiebt sich das Fenster nach innen, statt + schmaler zu werden — sonst säße er außermittig in seiner eigenen Kachel. + """ + x0, y0, x1, y1 = box + bx0, by0, bx1, by1 = x0 * breite, y0 * hoehe, x1 * breite, y1 * hoehe + bw, bh = max(1.0, bx1 - bx0), max(1.0, by1 - by0) + + seite = min(max(bw, bh) * (1 + 2 * rand), float(breite), float(hoehe)) + mx = min(max((bx0 + bx1) / 2, seite / 2), breite - seite / 2) + my = min(max((by0 + by1) / 2, seite / 2), hoehe - seite / 2) + return ( + int(mx - seite / 2), int(my - seite / 2), + int(mx + seite / 2), int(my + seite / 2), + ) + + +def adresse(sichtung) -> str | None: + """Adresse der Kachel für diese Sichtung — oder ``None``. + + Erzeugt das Bild beim ersten Abruf. Gibt es keine Koordinaten (Sichtungen + von vor der Umstellung), fällt die Vorlage auf das gespeicherte ``crop`` + zurück; dafür liefert diese Funktion ``None``. + """ + box = sichtung.vogel_box + if box is None or not sichtung.image: + return None + + ziel = _ziel(sichtung) + if not ziel.exists(): + try: + _erzeugen(sichtung, box, ziel) + except (OSError, ValueError): + # ⚠️ Ein fehlendes Original darf keine Seite zerreißen. Dann gilt + # der Rückfall, genau wie bei einer Sichtung ohne Koordinaten. + return None + + return f"{settings.MEDIA_URL}{ORDNER}/{ziel.name}" + + +def _erzeugen(sichtung, box, ziel: Path) -> None: + ziel.parent.mkdir(parents=True, exist_ok=True) + with sichtung.image.open("rb") as f: + bild = Image.open(f) + bild.load() + if bild.mode != "RGB": + bild = bild.convert("RGB") + + ausschnitt = bild.crop(quadrat(box, bild.width, bild.height)) + if ausschnitt.width > KANTE: + ausschnitt = ausschnitt.resize((KANTE, KANTE), Image.LANCZOS) + + # Erst daneben schreiben, dann umbenennen: Zwei gleichzeitige Abrufe + # dürfen sich nicht gegenseitig eine halbe Datei hinterlassen. + vorlaeufig = ziel.with_suffix(".teil") + ausschnitt.save(vorlaeufig, format="JPEG", quality=86, optimize=True) + vorlaeufig.replace(ziel) diff --git a/web/templates/dex/dex.html b/web/templates/dex/dex.html index 695cf34..0798118 100644 --- a/web/templates/dex/dex.html +++ b/web/templates/dex/dex.html @@ -70,8 +70,8 @@ {% if not card.found %}title="{% trans "Noch nicht gesichtet" %}"{% endif %}> {{ s.dex_nr }}
- {% if card.found and card.entry.best_sighting and card.entry.best_sighting.crop %} - {{ s.name }} + {% if card.found and card.entry.best_sighting and card.entry.best_sighting.kachel_url %} + {{ s.name }} {% elif s.reference_image %} {{ s.name }} {% endif %} diff --git a/web/templates/dex/home.html b/web/templates/dex/home.html index 7797393..fc8a558 100644 --- a/web/templates/dex/home.html +++ b/web/templates/dex/home.html @@ -52,8 +52,8 @@ {% if letzte.image %} {{ letzte.effective_species.name }} - {% elif letzte.crop %} - {{ letzte.effective_species.name }} + {% elif letzte.kachel_url %} + {{ letzte.effective_species.name }} {% endif %} {% trans "ZULETZT" %}
@@ -78,8 +78,8 @@ style="{{ entry.species.typ_vars }}"> {{ entry.species.dex_nr }}
- {% if entry.best_sighting and entry.best_sighting.crop %} - {{ entry.species.name }} + {% if entry.best_sighting and entry.best_sighting.kachel_url %} + {{ entry.species.name }} {% elif entry.species.reference_image %} {{ entry.species.name }} {% endif %} @@ -109,8 +109,8 @@