Quelle est la bonne façon de limiter le bruit de tramage?

9

Lors de la réduction de la profondeur de couleur et du tramage avec un bruit de 2 bits (avec n =] 0,5,1,5 [et sortie = étage (entrée * (2 ^ bits-1) + n)), les extrémités de la plage de valeurs (entrées 0,0 et 1,0 ) sont bruyants. Il serait souhaitable qu'ils soient de couleur unie.

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

Dégradé de bruit (ci-dessus est une capture d'écran du shadertoy, illustrant un dégradé et les deux extrémités qui devraient être respectivement blanches et noires, mais qui sont plutôt bruyantes)

Le problème peut bien sûr être résolu en compressant simplement la plage de valeurs de sorte que les extrémités soient toujours arrondies à des valeurs uniques. Cela semble un peu un hack cependant, et je me demande s'il existe un moyen de mettre en œuvre cela "correctement"?

hotmultimedia
la source
Pour une raison quelconque, le shadertoy n'a pas fonctionné sur mon navigateur. Pourriez-vous publier une / des images simples pour montrer ce que vous voulez dire?
Simon F
1
Cela ne devrait-il pas ressembler davantage à n =] - 1, 1 [?
JarkkoL
@JarkkoL Eh bien, la formule de conversion de virgule flottante en entier est sortie = étage (entrée * intmax + n), où n = 0,5 sans bruit, parce que vous voulez (par exemple)> = 0,5 arrondir, mais <0,5 abaisser. C'est pourquoi le bruit est "centré" à 0,5.
hotmultimedia
@SimonF a ajouté l'image du shadertoy
hotmultimedia
1
Il semble que vous tronquiez la sortie plutôt que de l'arrondir (comme le font les GPU) - arrondir à la place, au moins vous obtenez les blancs appropriés: shadertoy.com/view/MlsfD7 (image: i.stack.imgur.com/kxQWl.png )
Mikkel Gjoel

Réponses:

8

TL; DR: 2 * 1LSB coupures de tramage triangulaire-pdf dans les boîtiers électroniques à 0 et 1 en raison du serrage. Une solution consiste à lerp à un tramage uniforme 1 bit dans ces edgecases.

J'ajoute une deuxième réponse, car cela s'est avéré un peu plus compliqué que je ne le pensais à l'origine. Il semble que ce problème ait été un "TODO: besoins de serrage?" dans mon code depuis que je suis passé du tramage normalisé au tramage triangulaire ... en 2012. Ça fait du bien de le regarder enfin :) Code complet pour la solution / les images utilisées tout au long du post: https://www.shadertoy.com/view/llXfzS

Tout d'abord, voici le problème que nous examinons, lors de la quantification d'un signal à 3 bits avec un tramage PDF triangulaire 2 * 1LSB:

les sorties - essentiellement ce que hotmultimedia a montré.

En augmentant le contraste, l'effet décrit dans la question devient apparent: la sortie ne passe pas en moyenne au noir / blanc dans les boîtiers électroniques (et s'étend en fait bien au-delà de 0/1 avant de le faire).

entrez la description de l'image ici

Regarder un graphique donne un peu plus d'informations:

entrez la description de l'image ici (les lignes grises marquent 0/1, également en gris le signal que nous essayons d'émettre, la ligne jaune est la moyenne de la sortie tramée / quantifiée, le rouge est l'erreur (moyenne du signal)).

Fait intéressant, non seulement la sortie moyenne n'est pas 0/1 aux limites, mais elle n'est pas non plus linéaire (probablement en raison du pdf triangulaire du bruit). En regardant l'extrémité inférieure, il est logique de comprendre pourquoi la sortie diverge: lorsque le signal tramé commence à inclure des valeurs négatives, le serrage sur la sortie modifie la valeur des parties inférieures tramées de la sortie (c'est-à-dire les valeurs négatives), ce qui augmenter la valeur de la moyenne. Une illustration semble être en ordre (tramage 2LSB uniforme et symétrique, toujours en jaune):

entrez la description de l'image ici

Maintenant, si nous utilisons simplement un tramage normalisé 1LSB, il n'y a aucun problème au niveau des bords-cas, mais bien sûr, nous perdons les belles propriétés du tramage triangulaire (voir par exemple cette présentation ).

entrez la description de l'image ici

Une solution (pragmatique, empirique) (hack) consiste alors à revenir à [-0,5; 0,5 [tramage uniforme pour la casse):

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) );

Ce qui corrige les edgecases tout en gardant le tramage triangulaire intact pour la plage restante:

entrez la description de l'image ici

Donc, pour ne pas répondre à votre question: je ne sais pas s'il existe une solution plus solide mathématiquement, et je suis tout aussi désireux de savoir ce que Masters of Past a fait :) Jusque-là, au moins nous avons cet horrible hack pour faire fonctionner notre code.

EDIT
Je devrais probablement couvrir la solution de contournement-suggestion donnée dans la question, sur la compression simple du signal. Étant donné que la moyenne n'est pas linéaire dans les cas particuliers, la simple compression du signal d'entrée ne produit pas un résultat parfait - bien qu'elle fixe les points de terminaison: entrez la description de l'image ici

Références

Mikkel Gjoel
la source
Il est étonnant que le lerp sur les bords donne un résultat parfait. Je m'attendrais à au moins une petite déviation: P
Alan Wolfe
Oui, j'ai aussi été positivement surpris :) Je pense que cela fonctionne parce que nous réduisons la magnitude de tramage de manière linéaire, au même rythme que le signal diminue. Donc, au moins l'échelle correspond ... mais je conviens qu'il est intéressant de constater que le mélange direct des distributions ne semble pas avoir d'effets secondaires négatifs.
Mikkel Gjoel
@MikkelGjoel Malheureusement, votre croyance est incorrecte en raison d'un bogue dans votre code. Vous avez réutilisé le même RNG pour les deux dithertriet dithernormau lieu d'un RNG indépendant. Une fois que vous aurez étudié toutes les mathématiques et annulé tous les termes, vous constaterez que vous ne lerpez pas du tout! Au lieu de cela, le code agit comme une coupure stricte v < 0.5 / depth || v > 1 - 0.5/depth, basculant instantanément vers la distribution uniforme. Non pas que cela enlève le joli tramage que vous avez, c'est juste inutilement compliqué. La correction du bug est en fait mauvaise, vous vous retrouverez avec un pire tramage. Utilisez simplement une coupure ferme.
orlp
Après avoir creusé encore plus, j'ai trouvé un autre problème dans votre shadertoy où vous ne faites pas de correction gamma lors de la moyenne des échantillons (vous faites la moyenne dans l'espace sRGB qui n'est pas linéaire). Si vous gérez le gamma de manière appropriée, nous constatons que malheureusement nous n'avons pas encore terminé. Nous devons façonner notre bruit pour faire face à la correction gamma. Voici un shadertoy affichant le problème: shadertoy.com/view/3tf3Dn . J'ai essayé un tas de choses et je n'ai pas pu le faire fonctionner, j'ai donc posté une question ici: computergraphics.stackexchange.com/questions/8793/… .
orlp
3

Je ne suis pas sûr de pouvoir répondre pleinement à votre question, mais j'ajouterai quelques réflexions et nous pourrons peut-être arriver à une réponse ensemble :)

Premièrement, le fondement de la question n'est pas clair pour moi: pourquoi considérez-vous qu'il est souhaitable d'avoir un noir / blanc propre lorsque toutes les autres couleurs ont du bruit? Le résultat idéal après tramage est votre signal d'origine avec un bruit entièrement uniforme. Si le noir et le blanc sont différents, votre bruit dépend du signal (ce qui pourrait être bien, car il se produit quand les couleurs sont de toute façon bloquées).

Cela dit, il y a des situations où le bruit en blanc ou en noir pose un problème (je ne suis pas au courant des cas où le noir et le blanc doivent être "propres" simultanément): lors du rendu d'une particule mélangée additivement comme un quadruple avec une texture, vous ne voulez pas de bruit ajouté dans tout le quad, car cela s'afficherait également à l'extérieur de la texture. Une solution consiste à compenser le bruit, donc plutôt que d'ajouter [-0,5; 1,5 [vous ajoutez [-2,0; 0,0 [(c'est-à-dire soustrayez 2 bits de bruit). C'est une solution assez empirique, mais je ne suis pas au courant d'une approche plus correcte. En y réfléchissant, vous voudrez probablement également augmenter votre signal pour compenser l'intensité perdue ...

Quelque peu apparenté, Timothy Lottes a fait une présentation au GDC sur la mise en forme du bruit aux parties du spectre là où il est le plus nécessaire, en réduisant le bruit dans la partie brillante du spectre: http://32ipi028l5q82yhj72224m8j-wpengine.netdna-ssl.com/wp- contenu / téléchargements / 2016/03 / GdcVdrLottes.pdf

Mikkel Gjoel
la source
(désolé, j'ai appuyé sur Entrée accidentellement et le délai de modification a expiré) Le cas d'utilisation dans l'exemple est l'une des situations où cela importerait: rendre une image en niveaux de gris à virgule flottante sur un périphérique d'affichage 3 bits. Ici, l'intensité change considérablement en changeant simplement le LSB. J'essaie de comprendre s'il existe une "bonne façon" de faire correspondre les valeurs finales aux couleurs unies, comme la compression de la plage de valeurs et la saturation des valeurs finales. Et quelle est l'explication mathématique de cela? Dans l'exemple de formule, une valeur d'entrée de 1,0 ne produit pas une sortie moyenne de 7, et c'est ce qui me dérange.
hotmultimedia
1

J'ai simplifié l'idée de Mikkel Gjoel de tramage avec bruit triangulaire en une fonction simple qui n'a besoin que d'un seul appel RNG. J'ai supprimé tous les bits inutiles, ce qui devrait être assez lisible et compréhensible:

// 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));
}

Pour l'idée et le contexte, je vous renvoie à la réponse de Mikkel Gjoel.

orlp
la source
0

J'ai suivi les liens vers cette excellente question avec l'exemple shadertoy.

J'ai quelques questions sur la solution suggérée:

  1. Quelle est la valeur v? Est-ce un signal RVB compris entre 0,0 et 1,0 (du noir au blanc) que nous voulons quantifier? S'agit-il d'un seul flotteur? (et si oui, comment l'avez-vous calculé à partir du signal RVB d'origine?)
  2. Quelle est la source du «nombre magique» 0,5 / 7,0? Je suppose que c'est le demi-bac, mais je m'attendrais à ce que la taille du bac représenté par 8 bits soit de 1,0 / 255,0, j'ai donc été surpris de voir 0,5 / 0,7. Cela vous dérange d'expliquer comment vous avez dérivé ces chiffres. Qu'est-ce que je rate?
  3. Je suppose que la distribution du triangle est dans la plage [-1,1] et l'uniforme est dans [-0,5,0,5] ("demi-bit" parce que nous nous rapprochons du bord et nous ne voulons pas dépasser) - est-ce que la logique que vous avez appliquée?)
  4. Les variables aléatoires uniformes et triangulaires doivent être indépendantes. Ai-je raison?

Bon travail! Je veux voir que j'ai bien compris votre ligne de pensée. Merci!

zrizi
la source