Was ist ein geeigneter Weg, um Dithergeräusche zu unterdrücken?

9

Beim Reduzieren der Farbtiefe und beim Dithering mit einem 2-Bit-Rauschen (mit n =] 0,5,1,5 [und Ausgabe = Etage (Eingabe * (2 ^ Bit-1) + n)) werden die Enden des Wertebereichs (Eingaben 0,0 und 1,0) angezeigt ) sind laut. Es wäre wünschenswert, wenn sie einfarbig wären.

Beispiel: https://www.shadertoy.com/view/llsfz4

Rauschgradient (oben wird der shadertoy Screenshot eines Gradienten und beide Enden , die darstellend sollten jeweils festen , weißen und schwarzen sein, sind aber laut statt)

Das Problem kann natürlich gelöst werden, indem nur der Wertebereich komprimiert wird, sodass die Enden immer auf einzelne Werte gerundet werden. Das fühlt sich allerdings etwas hackig an und ich frage mich, ob es eine Möglichkeit gibt, dies "richtig" umzusetzen?

hotmultimedia
quelle
Aus irgendeinem Grund lief das Shadertoy nicht in meinem Browser. Könnten Sie ein oder mehrere einfache Bilder posten, um zu demonstrieren, was Sie meinen?
Simon F
1
Sollte es nicht eher n =] - 1, 1 [sein?
JarkkoL
@JarkkoL Nun, die Formel zum Konvertieren von Gleitkomma in Ganzzahl lautet output = floor (Eingabe * intmax + n), wobei n = 0,5 ohne Rauschen ist, weil Sie (zum Beispiel)> = 0,5 aufrunden möchten, aber <0,5 nach unten. Deshalb ist das Rauschen bei 0,5 "zentriert".
Hotmultimedia
@ SimonF hinzugefügt Bild des Shadertoy
hotmultimedia
1
Es scheint, dass Sie die Ausgabe abgeschnitten haben, anstatt sie zu runden (wie es GPUs tun) - stattdessen erhalten Sie zumindest die richtigen Weißtöne: shadertoy.com/view/MlsfD7 (Bild: i.stack.imgur.com/kxQWl.png )
Mikkel Gjoel

Antworten:

8

TL; DR: 2 * 1LSB-Dreieck-PDF-Dithering bricht in Randfällen bei 0 und 1 aufgrund von Klemmung. Eine Lösung besteht darin, in diesen Randfällen ein einheitliches 1-Bit-Dithering durchzuführen.

Ich füge eine zweite Antwort hinzu, da dies etwas komplizierter ausfiel, als ich ursprünglich gedacht hatte. Es scheint, dass dieses Problem ein "TODO: muss geklemmt werden?" in meinem Code seit ich von normalisiertem zu dreieckigem Dithering gewechselt bin ... im Jahr 2012. Es fühlt sich gut an, es endlich anzusehen :) Vollständiger Code für die im Beitrag verwendeten Lösungen / Bilder: https://www.shadertoy.com/view/llXfzS

Hier ist zunächst das Problem, das wir bei der Quantisierung eines Signals auf 3 Bit mit 2 * 1LSB-Dreieck-PDF-Dithering betrachten:

Ausgänge - im Wesentlichen, was hotmultimedia zeigte.

Mit zunehmendem Kontrast wird der in der Frage beschriebene Effekt deutlich: Die Ausgabe wird in den Randfällen nicht auf Schwarz / Weiß gemittelt (und reicht tatsächlich weit über 0/1 hinaus, bevor Sie dies tun).

Geben Sie hier die Bildbeschreibung ein

Das Betrachten eines Diagramms bietet etwas mehr Einblick:

Geben Sie hier die Bildbeschreibung ein (graue Linien markieren 0/1, auch in grau ist das Signal, das wir ausgeben möchten, gelbe Linie ist der Durchschnitt der geditherten / quantisierten Ausgabe, rot ist der Fehler (Signalmittelwert)).

Interessanterweise ist die durchschnittliche Ausgabe nicht nur nicht 0/1 an den Grenzen, sondern auch nicht linear (wahrscheinlich aufgrund des dreieckigen PDF des Rauschens). Wenn man das untere Ende betrachtet, ist es intuitiv sinnvoll, warum der Ausgang divergiert: Wenn das Dither-Signal negative Werte enthält, ändert der Clamp-On-Ausgang den Wert der unteren Dither-Teile des Ausgangs (dh der negativen Werte) Erhöhung des Durchschnittswertes. Eine Abbildung scheint in Ordnung zu sein (einheitliches, symmetrisches 2LSB-Dithering, Durchschnitt immer noch in Gelb):

Geben Sie hier die Bildbeschreibung ein

Wenn wir nur ein normalisiertes 1LSB-Dithering verwenden, gibt es überhaupt keine Probleme an den Kantenfällen, aber dann verlieren wir natürlich die schönen Eigenschaften des dreieckigen Ditherings (siehe z . B. diese Präsentation ).

Geben Sie hier die Bildbeschreibung ein

Eine (pragmatische, empirische) Lösung (Hack) besteht dann darin, auf [-0,5; 0,5 [gleichmäßiges Dithering für den Randfall zurückzugreifen:

float dithertri = (rnd.x + rnd.y - 1.0); //note: symmetric, triangular dither, [-1;1[
float dithernorm = rnd.x - 0.5; //note: symmetric, uniform dither [-0.5;0.5[

float sizt_lo = clamp( v/(0.5/7.0), 0.0, 1.0 );
float sizt_hi = 1.0 - clamp( (v-6.5/7.0)/(1.0-6.5/7.0), 0.0, 1.0 );

dither = lerp( dithernorm, dithertri, min(sizt_lo, sizt_hi) );

Dadurch werden die Randfälle repariert, während das dreieckige Dithering für den verbleibenden Bereich erhalten bleibt:

Geben Sie hier die Bildbeschreibung ein

Um Ihre Frage nicht zu beantworten: Ich weiß nicht, ob es eine mathematisch fundiertere Lösung gibt, und ich bin ebenso gespannt darauf, was Masters of Past getan hat :) Bis dahin haben wir zumindest diesen schrecklichen Hack, um unseren Code funktionsfähig zu halten.

BEARBEITEN
Ich sollte wahrscheinlich den in der Frage gegebenen Problemumgehungsvorschlag zum einfachen Komprimieren des Signals behandeln. Da der Durchschnitt in den Randfällen nicht linear ist, führt das einfache Komprimieren des Eingangssignals nicht zu einem perfekten Ergebnis - obwohl die Endpunkte festgelegt werden: Geben Sie hier die Bildbeschreibung ein

Verweise

Mikkel Gjoel
quelle
Es ist erstaunlich, dass der Lerp an den Rändern ein perfekt aussehendes Ergebnis liefert. Ich würde zumindest eine kleine Abweichung erwarten: P
Alan Wolfe
Ja, ich war auch positiv überrascht :) Ich glaube, es funktioniert, weil wir die Dither-Größe linear verkleinern, mit der gleichen Geschwindigkeit, mit der das Signal abnimmt. Dann stimmt zumindest die Skala überein ... aber ich stimme zu, dass es interessant ist, dass das direkte Mischen der Verteilungen keine negativen Nebenwirkungen zu haben scheint.
Mikkel Gjoel
@MikkelGjoel Leider ist Ihre Überzeugung aufgrund eines Fehlers in Ihrem Code falsch. Sie haben dasselbe RNG für beide dithertriund dithernormanstelle eines unabhängigen wieder verwendet. Sobald Sie alle Berechnungen durchgearbeitet und alle Begriffe abgebrochen haben, werden Sie feststellen, dass Sie überhaupt nicht lerping! Stattdessen wirkt der Code wie ein harter Cutoff und v < 0.5 / depth || v > 1 - 0.5/depthwechselt sofort zur dortigen Gleichverteilung. Nicht, dass es Ihnen das schöne Zittern nimmt, es ist nur unnötig kompliziert. Das Beheben des Fehlers ist tatsächlich schlecht, Sie werden mit einem schlimmeren Zittern enden. Verwenden Sie einfach einen harten Cutoff.
Orlp
Nachdem ich noch tiefer gegraben habe, habe ich ein weiteres Problem in Ihrem Shadertoy gefunden, bei dem Sie keine Gammakorrektur durchführen, während Sie Samples mitteln (Sie mitteln im sRGB-Raum, der nicht linear ist). Wenn Sie mit Gamma angemessen umgehen, stellen wir fest, dass wir leider noch nicht fertig sind. Wir müssen unser Rauschen formen, um mit der Gammakorrektur fertig zu werden. Hier ist ein Shadertoy, der das Problem anzeigt : shadertoy.com/view/3tf3Dn . Ich habe eine Reihe von Dingen ausprobiert und konnte sie nicht zum Laufen bringen. Deshalb habe ich hier eine Frage gestellt: computergraphics.stackexchange.com/questions/8793/… .
Orlp
3

Ich bin nicht sicher, ob ich Ihre Frage vollständig beantworten kann, aber ich werde einige Gedanken hinzufügen und vielleicht können wir gemeinsam eine Antwort finden :)

Erstens ist mir die Grundlage der Frage etwas unklar: Warum halten Sie es für wünschenswert, sauberes Schwarz / Weiß zu haben, wenn jede andere Farbe Rauschen aufweist? Das ideale Ergebnis nach dem Dithering ist Ihr ursprüngliches Signal mit völlig gleichmäßigem Rauschen. Wenn Schwarz und Weiß unterschiedlich sind, wird Ihr Rauschen signalabhängig (was jedoch in Ordnung sein kann, da es ohnehin dort auftritt, wo Farben geklemmt werden).

Es gibt jedoch Situationen, in denen Rauschen in Weiß oder Schwarz ein Problem darstellt (mir sind keine Anwendungsfälle bekannt, bei denen sowohl Schwarz als auch Weiß gleichzeitig "sauber" sein müssen): Beim Rendern eines additiv gemischten Partikels als Quad mit Bei einer Textur möchten Sie nicht, dass im gesamten Quad Rauschen hinzugefügt wird, da dies auch außerhalb der Textur angezeigt wird. Eine Lösung besteht darin, das Rauschen zu kompensieren, anstatt [-0,5; 1,5 [[-2,0; 0,0 [(dh 2 Bit Rauschen subtrahieren)) zu addieren. Dies ist eine ziemlich empirische Lösung, aber mir ist kein korrekterer Ansatz bekannt. Wenn Sie darüber nachdenken, möchten Sie wahrscheinlich auch Ihr Signal verstärken, um die verlorene Intensität auszugleichen ...

Etwas verwandt hielt Timothy Lottes einen GDC-Vortrag über die Formung von Rauschen in den Teilen des Spektrums, in denen es am dringendsten benötigt wird, und reduzierte das Rauschen am hellen Ende des Spektrums: http://32ipi028l5q82yhj72224m8j-wpengine.netdna-ssl.com/wp- content / uploads / 2016/03 / GdcVdrLottes.pdf

Mikkel Gjoel
quelle
(Entschuldigung, ich habe versehentlich die Eingabetaste gedrückt und das Bearbeitungslimit ist abgelaufen.) Der Anwendungsfall im Beispiel ist eine der Situationen, in denen es darauf ankommt: Rendern eines Gleitkomma-Graustufenbilds auf einem 3-Bit-Anzeigegerät. Hier ändert sich die Intensität stark, indem nur das LSB geändert wird. Ich versuche zu verstehen, ob es einen "richtigen Weg" gibt, Endwerte Volltonfarben zuzuordnen, z. B. den Wertebereich zu komprimieren und die Endwerte zu sättigen. Und was ist die mathematische Erklärung dafür? In der Beispielformel erzeugt der Eingabewert 1.0 keine Ausgabe, die im Durchschnitt 7 beträgt, und das stört mich.
hotmultimedia
1

Ich habe Mikkel Gjoels Idee, mit dreieckigem Rauschen zu zittern, zu einer einfachen Funktion vereinfacht, die nur einen einzigen RNG-Aufruf benötigt. Ich habe alle unnötigen Teile entfernt, damit es ziemlich lesbar und verständlich sein sollte, was los ist:

// Dithers and quantizes color value c in [0, 1] to the given color depth.
// It's expected that rng contains a uniform random variable on [0, 1].
uint dither_quantize(float c, uint depth, float rng) {
    float cmax = float(depth) - 1.0;
    float ci = c * cmax;

    float d;
    if (ci < 0.5 || ci >= cmax - 0.5) {
        // Uniform distribution on [-0.5, 0.5] for edges.
        d = rng - 0.5;
    } else {
        // Symmetric triangular distribution on [-1, 1].
        d = (rng < 0.5) ? sqrt(2.0 * rng) - 1.0 : 1.0 - sqrt(2.0 - 2.0*rng);
    }

    return uint(clamp(ci + d + 0.5, 0.0, cmax));
}

Für die Idee und den Kontext verweise ich Sie auf die Antwort von Mikkel Gjoel.

orlp
quelle
0

Ich folgte den Links zu dieser ausgezeichneten Frage zusammen mit dem Shadertoy-Beispiel.

Ich habe einige Fragen zur vorgeschlagenen Lösung:

  1. Was ist der Wert v? Ist es ein RGB-Signal im Bereich von 0,0 bis 1,0 (schwarz bis weiß), das wir quantisieren möchten? Ist es ein einzelner Schwimmer? (und wenn ja, wie haben Sie es aus dem ursprünglichen RGB-Signal berechnet?)
  2. Was ist die Quelle der „magischen Zahl“ 0,5 / 7,0? Ich nehme an, es ist das halbe Fach, aber ich würde erwarten, dass die Größe des durch 8 Bits dargestellten Fachs 1,0 / 255,0 beträgt, also war ich überrascht, 0,5 / 0,7 zu ​​sehen. Haben Sie etwas dagegen zu erklären, wie Sie diese Zahlen abgeleitet haben? Was vermisse ich?
  3. Ich gehe davon aus, dass die Dreiecksverteilung im Bereich [-1,1] liegt und die Uniform in [-0,5,0,5] liegt („halbes Bit“, weil wir uns der Kante nähern und nicht überschießen wollen - ist das? die Logik, die Sie angewendet haben?)
  4. Die einheitlichen und dreieckigen Zufallsvariablen sollten unabhängig sein. Hab ich recht?

Gute Arbeit! Ich möchte sehen, dass ich Ihren Gedankengang richtig verstanden habe. Vielen Dank!

zrizi
quelle