Filme an ihren Trailern erkennen
Wie erkennt man einen Film, wenn man nur die Videodatei hat? Mein Konzept: Von bekannten Filmen werden Trailer in kompakte Bild-Fingerprints (perceptual Hashes) umgerechnet und in einer kleinen Datenbank abgelegt. Ein unbekannter Film wird anschließend Frame für Frame gegen diese Fingerprints geprüft, und der Film mit den meisten Treffern gewinnt. Dieser Beitrag beschreibt die Idee, die Bausteine und die ersten Messungen zur Robustheit der Fingerprints.
Ausgangslage
Wer eine größere Filmsammlung hat, kennt das Problem: Dateinamen wie film_final_2.mkv sagen nichts, Metadaten fehlen oder stimmen nicht. Eine automatische Zuordnung zu einem Eintrag in einer Filmdatenbank wäre praktisch, scheitert aber an genau diesen fehlenden Informationen.
Bei Musik löst Shazam das mit Audio-Fingerprints. Für Filme fehlt so ein Verfahren, und vor allem fehlt die Referenzdatenbank. Hier hilft eine einfache Beobachtung: Fast jeder Film hat einen öffentlich verfügbaren Trailer, und der besteht aus Szenen genau dieses Films. Die Trailer sind also die Referenz, die wir sowieso schon haben.
Das Konzept
Der Ablauf hat zwei Seiten, die dieselbe Verarbeitungskette benutzen:
- Index aufbauen: Trailer laden, Frames extrahieren, pro Frame einen 64-Bit-Hash berechnen und zusammen mit der Film-ID speichern.
- Film suchen: Den unbekannten Film durch dieselbe Kette schicken, die Hashes gegen den Index halten und die Treffer pro Film zählen.
Gespeichert werden nur Hashes und IDs. Keine Frames, keine Videos. Ein heruntergeladener Trailer wird nach dem Hashen sofort wieder gelöscht.
Verarbeitungskette
Die Frames kommen per Pipe aus ffmpeg. Jedes Frame durchläuft vier Schritte:
- Abtastung mit 2 Frames pro Sekunde beim Indizieren und 4 Frames pro Sekunde bei der Suche.
- Skalierung auf 720 Pixel Höhe und mittiger Zuschnitt auf 400x400 Pixel.
- Verkleinerung auf 32x32 Pixel in Graustufen.
- Frames mit sehr geringer Varianz (schwarz, einfarbige Titelkarten) werden verworfen, weil sie zu jedem Film passen würden.
Bei Trailern lässt man außerdem die ersten 5 und die letzten 10 Sekunden weg. Dort liegen Studiologos, Ratingkarten und Abspann, die in vielen Trailern gleich aussehen.
Was ist eine DCT?
Die DCT (Diskrete Kosinustransformation) zerlegt ein Bild nicht in Pixel, sondern in Muster unterschiedlicher Feinheit. Man kann sich das wie einen Equalizer vorstellen: Statt "wie laut ist es zu jedem Zeitpunkt" fragt man "wie viel Bass, wie viel Mitten, wie viel Höhen steckt drin". Ähnlich macht es auch Shazam zur Erkennung von Miusik. Beim Bild heißen die Bänder Frequenzen: Niedrige Frequenzen beschreiben große, weiche Flächen und Verläufe (grobe Helligkeitsverteilung), hohe Frequenzen feine Details, Kanten und Rauschen.
Dieselbe Idee steckt in JPEG. Dort wirft man die hohen Frequenzen zuerst weg, weil das Auge sie am wenigsten vermisst. Für Fingerprints ist das ideal: Kompressionsartefakte, Rauschen und leichte Farbänderungen betreffen vor allem die hohen Frequenzen. Die niedrigen, also die grobe Bildstruktur, bleiben zwischen Trailer und Vollfilm stabil.
Hash-Berechnung
Aus dem 32x32-Bild wird eine zweidimensionale DCT berechnet. Von den Koeffizienten behalte ich die 8x8 niedrigsten Frequenzen (64 Werte), ignoriere den Gleichanteil (die mittlere Helligkeit) und vergleiche jeden Wert mit dem Median. Größer als der Median ergibt eine 1, sonst eine 0. Das Ergebnis sind 64 Bit.
def phash(img):
"""64-Bit-pHash aus einem 32x32-Graustufenbild."""
d = DCT @ img.astype(float) @ DCT.T
low = d[:8, :8].flatten()
bits = low > np.median(low[1:]) # Gleichanteil ignorieren
h = 0
for b in bits:
h = (h << 1) | int(b)
return h
Ähnliche Bilder ergeben ähnliche Hashes. Wie ähnlich, misst man in Bits, in denen sich zwei Hashes unterscheiden (Hamming-Distanz).
Suche und Bewertung
Für jeden Hash des Eingangsvideos wird die Hamming-Distanz zu allen Einträgen im Index bestimmt (XOR, dann Bits zählen). Bis zu 10 Bit Unterschied gelten als Treffer. Zwei Regeln halten die Fehltreffer klein:
- Trifft ein Frame mehr als 50 Einträge, ist es zu generisch und wird ignoriert.
- Pro Film zählen nur verschiedene Trailer-Frames. Ein einzelner Eintrag, der von vielen Frames getroffen wird, erhöht die Wertung nur einmal.
Anschließend werden die Filme nach der Zahl ihrer Treffer sortiert (Voting). Ab 10 verschiedenen Treffern gilt ein Film als erkannt.
Aufbau des Index
Als Quelle für Film-IDs dient der öffentliche TMDB-Export. Er ist ohne API-Key abrufbar, enthält alle Filme und lässt sich nach Popularität sortieren. Die Trailer-Verweise kommen von der TMDB-API. Der Index wächst dann von den beliebtesten Filmen nach unten.
Weil zwei externe Dienste beteiligt sind, muss der Aufbau sich selbst bremsen. Vorgesehen sind höchstens 30 Anfragen pro Sekunde an TMDB (bei einem 429 ruht alles 10 Minuten), Downloads strikt nacheinander mit etwa 5 Sekunden Pause plus Zufall und eine Pause von 10 Minuten, wenn die Quelle blockt. Jeder Film wird sofort nach dem Hashen gespeichert, ein abgebrochener Lauf macht also beim nächsten Start dort weiter. Filme ohne Trailer oder mit gesperrtem Video landen mit Grund in einer eigenen Tabelle und werden nicht erneut versucht.
Erste Messungen
Die wichtigste Annahme des Konzepts ist, dass ein Hash robust gegenüber Kompression und Farbänderungen ist, aber zwei verschiedene Szenen auseinanderhält. Das habe ich als Erstes geprüft.
Versuchsaufbau: Ein zehnminütiger Ausschnitt eines Spielfilms (Quelle in 480p) wurde als Referenz mit H.264 neu kodiert. Daraus entstanden drei Varianten: 360p mit CRF 28, 240p mit CRF 35 (stark komprimiert) und eine Version mit veränderter Helligkeit, Kontrast und Sättigung. Alle Videos liefen mit 2 fps durch die oben beschriebene Kette. Gemessen wurde auf einem Apple M3 Pro. Von möglichen 1.200 Frames blieben 1.188 nach dem Varianzfilter übrig.
Verglichen habe ich pro Variante die Hashes derselben Zeitstempel mit der Referenz, dazu rund 18.000 zufällige Paare aus Frames, die mindestens 30 Sekunden auseinanderliegen.
Variante Ø Bits Median 95 % Max Anteil <= 10 Bit
360p, CRF 28 1,3 2 4 6 100 %
240p, CRF 35 2,6 2 6 14 99,9 %
Farbänderung 0,6 0 2 6 100 %
Anderer Zeitpunkt 31,4 - - - 0 % (min. 14)
Die Verteilung für die stark komprimierte Variante sieht so aus:
Distanz (Bits) gleiches Frame anderer Zeitpunkt
0-3 63,3 % 0,0 %
4-7 35,2 % 0,0 %
8-11 1,4 % 0,0 %
12-15 0,1 % 0,0 %
16-19 0,0 % 0,1 %
20-23 0,0 % 1,7 %
24-27 0,0 % 11,4 %
28-31 0,0 % 32,6 %
32-35 0,0 % 35,8 %
36-39 0,0 % 15,8 %
40-43 0,0 % 2,5 %
44 und mehr 0,0 % 0,2 %
Die beiden Verteilungen überlappen praktisch nicht. Gleiche Frames liegen fast alle unter 8 Bit, verschiedene Frames im Schnitt bei rund 31 Bit, was dem Erwartungswert für zufällige 64-Bit-Werte (32) entspricht. Die Schwelle von 10 Bit trennt beide Gruppen in diesem Versuch sauber.
Geschwindigkeit
Ebenfalls auf dem M3 Pro und mit dem zehnminütigen Ausschnitt:
Frames extrahieren und hashen, 2 fps 576 Frames/s (ca. 290x Echtzeit)
Frames extrahieren und hashen, 4 fps 1124 Frames/s (ca. 285x Echtzeit)
Reine Hash-Berechnung 28 µs pro Hash
Abgleich, 1 Mio. Hashes 14 ms pro Query-Frame (8 MB)
Abgleich, 10 Mio. Hashes 135 ms pro Query-Frame (80 MB)
Für den Abgleich habe ich zufällige Hashes verwendet, es geht dabei nur um die Rechenzeit. Hochgerechnet auf einen Film von gut 100 Minuten mit rund 24.000 Query-Frames sind das etwa 6 Minuten bei einer Million Hashes und knapp 55 Minuten bei zehn Millionen.
Grenzen und offene Punkte
- Die Stichprobe ist klein. Ein einzelner Ausschnitt eines einzelnen Films sagt wenig darüber, wie sich Animationsfilme, sehr dunkle Filme oder Filme mit Letterbox verhalten.
- Der lineare Scan skaliert nur begrenzt. Ab einigen Millionen Hashes wird die Suche spürbar langsam. Ein Index für Hamming-Nachbarschaften (zum Beispiel Multi-Index-Hashing) wäre der nächste Schritt.
- Trailer müssen Filmszenen enthalten. Material, das nur für den Trailer gedreht wurde, erzeugt keine Treffer. Filme ohne Trailer sind gar nicht erkennbar.
- Die Schwellwerte sind Startwerte. 10 Bit, 10 Stimmen und 50 Treffer pro Frame sind begründete Annahmen, aber noch nicht an einer großen Datenbank abgestimmt.
Das Konzept ist bewusst klein gehalten: ein Skript, eine SQLite-Datei, keine weiteren Dienste. Wenn du Ideen hast, wie sich die Trefferqualität verbessern ließe, oder Erfahrungen mit ähnlichen Verfahren, schreib mir gern.
Weiterführende Quellen
- Perceptual hashing (Wikipedia): Überblick über Verfahren, die ähnliche Inhalte auf ähnliche Hashes abbilden.
- Looks Like It (The Hacker Factor Blog): verständliche Beschreibung, wie Bild-Hashes funktionieren.
- pHash.org: Projektseite der pHash-Bibliothek für Bild-, Audio- und Video-Hashes.
- Discrete cosine transform (Wikipedia): die Mathematik hinter der DCT.
- JPEG (Wikipedia): zeigt, wie die DCT in der Bildkompression eingesetzt wird.
- Hamming distance (Wikipedia): das Abstandsmaß, mit dem Hashes verglichen werden.
- Acoustic fingerprint (Wikipedia): das Prinzip hinter Shazam und ähnlichen Diensten.
- Fast Exact Search in Hamming Space with Multi-Index Hashing (arXiv): Paper zum schnellen Suchen nach Hamming-Nachbarn, relevant für größere Indizes.
- TMDB: Daily ID Exports und TMDB: Rate Limiting: die Datenquelle und ihre Grenzen.
- FFmpeg Filters Documentation: Dokumentation zu den Filtern für Skalierung, Zuschnitt und Frame-Rate.