Blog / Engineering

Transparente PNGs brechen KI-Upscaler: der Bug mit dem dunklen Halo

· Bug-Studie, mit gemessenem Fix · Gegenstand: die ×4-Produktions-Pipeline

Gib ein PNG mit Transparenz (ein freigestelltes Foto, ein Sticker-Export) in fast jeden KI-Upscaler, und ein ganz bestimmter Defekt erscheint: ein dunkler Halo, der von jeder transparenten Kante ein paar Pixel weit ins Motiv kriecht, dahinter ein schwacher heller Überschwinger-Ring. Auf dunklen Vorschauen versteckt er sich, auf weißen Seiten sticht er ins Auge. Wir haben ihn auf unserer eigenen Pipeline reproduziert, seine genaue Form gemessen (ein Graben von −94 Luma, drei Modellpixel breit), ihn auf die Premultiplied-Alpha-Rundreise der Webplattform zurückgeführt und den Fix gemessen, den wir ausliefern: ein O(n)-Flood-Fill, der 98 % des Fehlers entfernt, bevor das Modell überhaupt läuft.

−93,9Luma-Fehler, erster Pixelringnaiver Pfad vs. saubere Referenz
≈3 pxGrabenbreite im Modellmaßstabdann ein +17-Überschwinger-Ring
63×Fehlerreduktion durch den Fix−62,9 → 1,1 im Mittel, 0–3-px-Band
O(n)Kosten des Fixesein BFS-Durchgang, keine zusätzliche Inferenz

Jede Zahl in dieser Notiz wurde mit dem Code der Produktions-Pipeline gemessen: dieselben Modelle, dieselbe Mathematik. Rohdaten pro Bild: e3_alpha.json.

1 · Woher das Schwarz kommt

Das 2D-Canvas des Webs speichert Pixel vormultipliziert: nicht (r, g, b, α), sondern (rα, gα, bα, α). Das Compositing liebt dieses Format (die Porter-Duff-Operatoren[1] werden zu einzelnen Multiply-Adds), aber es ist an genau einer Stelle verlustbehaftet. Pixel zurückzulesen heißt, die Farbe durch α zu teilen:

r = ⌊ rα / α ⌉, undefiniert bei α = 0(1)

Bei α = 0 ist das 0/0, und die Antwort der Plattform[2] lautet, dass es nichts mehr zurückzugewinnen gibt: Die Farbe wurde in dem Moment mit null multipliziert, in dem sie das Canvas berührte. getImageData liefert unter jedem vollständig transparenten Pixel RGB (0, 0, 0). Die transparente Fläche deines Freistellers ist stillschweigend zu einem See aus reinem Schwarz geworden.

Ein Mensch sieht den See nie: Alpha verbirgt ihn. Ein Super-Resolution-Netz ist kein Mensch: Es konsumiert drei RGB-Kanäle und kein Alpha, es sieht also eine Fotografie, eingebettet in Vantablack, und jedes Ausgabepixel nahe der Grenze wird aus einem rezeptiven Feld berechnet, das in den See eintaucht. Das Schwarz bleibt nicht an Ort und Stelle; es diffundiert durch den Faltungsstapel nach innen[3].

2 · Den Halo messen

naiv (Canvas-RGB=0)Alpha-Bleeding (unseres)
-100-75-50-2502513581216L1-Abstand ins Motiv (px, Modellmaßstab)Δ Luma vs. Referenzperfekt
Abbildung 1. Der Halo, profiliert. Naiver Pfad (violett): ein Graben von −94 Luma im ersten Pixelring innerhalb des Motivs, −90 bei 2 px, −54 bei 3 px, dann ein +17-Überschwinger-Ring bei 5 px, der bis ~13 px auf null abklingt; dunkle Kontamination durch ein endliches rezeptives Feld, vom GAN zu einer Kante geschärft. Produktionspfad (grün): nie schlechter als 1,5 Luma-Stufen. Abstände im Modellmaßstab; ×4 für Ausgabepixel.
Drei Tafeln: der freigestellte Input über einem Schachbrettmuster, die naive Hochskalierung mit dunklem Rand um das Oval und die saubere Hochskalierung mit Alpha-Bleeding
Abbildung 2. Input mit Transparenz (links), naiver Pfad (Mitte), Produktionspfad (rechts), über einen hellen Grund gelegt. Der Rand der mittleren Tafel liest sich in Vorschaugröße als subtile „schmutzige Kante“, und genau deshalb wird dieser Bug so oft ausgeliefert: Er versteckt sich, bis ein Nutzer den Sticker auf einen hellen Hintergrund legt.

Eine wirklich kontraintuitive Messung: Wir hatten erwartet, dass das Modell innerhalb des transparenten Bereichs Müll halluziniert, „die Leere verbessert“. Tut es nicht. Flaches Schwarz rein, flaches Schwarz raus (Gradientenenergie 0,1). Der Schaden ist nicht das, was das Modell in der Leere erfindet; es ist das, was die Leere dem Motiv über die Grenze hinweg antut. Das ist wichtig, weil es bedeutet, dass das Maskieren der Ausgabe dich nicht retten kann: Die beschädigten Pixel liegen innerhalb des sichtbaren Bereichs, unter α > 0.

3 · Der Fix: die Leere mit plausibler Farbe fluten

Wenn das Modell RGB unter α = 0 sehen muss, dann soll es RGB sein, das das Motiv fortsetzt. Vor der Inferenz führen wir Alpha-Bleeding aus: eine Breitensuche-Flutung mit mehreren Quellen, ausgehend von jedem nicht-transparenten Pixel, bei der jedes transparente Pixel die mittlere Farbe seiner bereits gefärbten 4-Nachbarn annimmt:

c(p) = mean{ c(q) : q ∈ N₄(p), q gefärbt }, p in BFS-Reihenfolge(2)

Die Warteschlange mit dem Randring zu seeden und FIFO abzuarbeiten, garantiert, dass jedes entnommene Pixel mindestens einen gefärbten Nachbarn hat (wer es eingereiht hat, war zuerst fertig), sodass ein einziger O(n)-Durchgang die gesamte Leere färbt: unbegrenzte Distanz, eingeschlossene Löcher inklusive. Alpha-Bytes werden nie angefasst; halbtransparente Pixel behalten ihre eigene Farbe. Das Modell sieht dann eine glatte Fortsetzung statt einer Klippe, und Alpha (separat hochskaliert) wird auf dem Weg nach draußen wieder angewendet. Gemessener Effekt: Der Fehler im 0–3-px-Band fällt von -62,9 auf 1,1 Luma-Stufen, eine 63-fache Reduktion, ohne messbare Laufzeitkosten.

Kantendetail der naiven Rekonstruktion und der mit Bleeding, plus eine verstärkte Differenz-Heatmap, die zeigt, wie der Halo an der Alpha-Grenze klebt
Abbildung 3. Kantendetail: der naive Pfad (links) neben dem Pfad mit Bleeding (Mitte) und rechts die |Differenz| zwischen den beiden Rekonstruktionen, ×8 verstärkt. Der Halo folgt der Alpha-Grenze und sonst nichts; abseits der Kante sind die Ausgaben identisch, was genau das Verhalten ist, für das der Fix entworfen wurde.

4 · Was das für deine Bilder bedeutet

Wenn du diese Seite benutzt: nichts; transparente PNGs nehmen den oben gemessenen Pfad in der Cloud-Pipeline wie in der Browser-Fallback-Engine, denn beide teilen sich dieselbe reine Bibliothek. Wenn du Bild-Tools baust: Behandle jedes reine RGB-Modell als allergisch gegen vormultiplizierte Nullen: Bleeding vor der Inferenz (die Flutung oben sind ~30 Zeilen ohne Abhängigkeiten), oder auf eine Volltonfarbe matten und die Verzerrung der Matte in Kauf nehmen. Und wenn du eine mysteriöse dunkle Kante in der Ausgabe von jemandes Enhancer diagnostizierst, kennst du jetzt den Fingerabdruck: ein Graben, ein paar Pixel breit, ein Überschwinger-Ring dahinter und eine perfekte Korrelation mit der Alpha-Grenze. Die drei Zahlen zum Merken: −94, 3 px, 63×.

Quellen

  1. Porter, T., & Duff, T. (1984). Compositing Digital Images. SIGGRAPH '84, Computer Graphics 18(3), 253–259. (The premultiplied-alpha compositing algebra.) dl.acm.org
  2. WHATWG HTML Living Standard, the 2D canvas element: premultiplied backing stores and the lossy getImageData round-trip for non-opaque pixels. html.spec.whatwg.org
  3. Wang, X., Xie, L., Dong, C., & Shan, Y. (2021). Real-ESRGAN: Training Real-World Blind Super-Resolution with Pure Synthetic Data. ICCV Workshops. arXiv:2107.10833. arxiv.org
  4. Lange, D. (1936). “Migrant Mother” (Destitute pea pickers in California), Farm Security Administration. Public domain, via Wikimedia Commons. commons.wikimedia.org
  5. ONNX Runtime: the inference engine both our server (CPU) and in-browser (WebGPU/WASM) engines run on. onnxruntime.ai