8 Punkte von GN⁺ 3 일 전 | 2 Kommentare | Auf WhatsApp teilen
  • Unterschied zwischen der standardmäßigen Methode mit Division durch 255 und der alternativen Methode mit 0,5-Bias und Division durch 256, wenn 8-Bit-Ganzzahlfarben in Fließkommazahlen umgewandelt werden
  • Die 255-Methode bildet Ganzzahl 0 auf 0.0 und 255 auf 1.0 ab, sodass sich Schwarz und Weiß direkt behandeln lassen, und sie entspricht auch der UNORM-zu-Float-Konvertierung der GPU
  • Die 256-Methode platziert mit (img + 0.5) / 256.0 jeden Wert in der Mitte seines Intervalls, was bei Aufgaben wie Dithering die Behandlung von Randfällen vereinfachen kann, aber 0 nicht auf 0.0 abbildet und die Verarbeitungslogik damit an 8-Bit-Eingaben bindet
  • Bei der 255-Methode sind die Intervalle an beiden Enden nur halb so breit; rundet man gleichverteilte Zufallswerte aus [0, 1] zurück auf 8 Bit, treten 0 und 255 deshalb nur halb so häufig auf wie andere Werte, doch echte Bild-Roundtrips funktionieren trotzdem verlustfrei
  • Wenn man Bilder anderer verarbeitet, ist Normalisierung durch 255 die richtige Wahl; die 256-Methode ist nur dann erwägenswert, wenn man Speichern und Laden vollständig kontrolliert

Problemstellung

  • In einem Programm, das ein Bild einliest, in Fließkommazahlen umwandelt, verarbeitet und anschließend wieder als 8-Bit-Farben speichert, ist die Methode zur Integer-Float-Konvertierung der Streitpunkt
  • Es gibt zwei Ansätze
    • Standardmethode (durch 255 teilen): pixels = img / 255.0 → verarbeiten → output = np.trunc(result * 255 + 0.5)
    • Alternative Methode (durch 256 teilen): pixels = (img + 0.5) / 256.0 → verarbeiten → output = np.trunc(result * 256)
    • In beiden Fällen wird der Wert vor der finalen Typumwandlung auf 0 bis 255 begrenzt: output.clip(0, 255).astype(np.uint8)
  • Die Standardmethode bildet Ganzzahl 0 auf 0.0 und 255 auf 1.0 ab und entspricht der UNORM-zu-Float-Konvertierung der GPU
  • Die alternative Methode fügt einen 0,5-Bias hinzu, sodass Ganzzahl 0 auf 0.5/256 = 0.001953125 abgebildet wird
    • Dadurch lassen sich schwarze Pixel nicht erkennen, wenn man diese Konstante nicht kennt
    • Selbst bei Fließkommaberechnungen bleibt die Logik damit an 8-Bit-Eingaben gekoppelt
    • Bei der Standardmethode kann man Schwarz immer als 0.0 annehmen

Einwände gegen 255.0

  • Zeichnet man die Standardmethode auf der Zahlengeraden ein, wirkt sie etwas seltsam
  • Kleinere Bins an beiden Enden

    • Die End-Bins der Standardformel ragen über den Bereich [0,1] hinaus; der Bereich ist also „gestreckt“
    • Beim Zurückwandeln von Fließkomma in Integer sind die End-Bins nur halb so breit wie die anderen
      • Für einen Algorithmus wird es dadurch „schwieriger“, Extremwerte auszugeben
      • Erzeugt man gleichverteiltes Rauschen in [0,1] und rundet es mit der Standardformel, treten 0 und 255 nur halb so häufig auf wie andere Ganzzahlen
    • Ein Histogramm von einer Million gleichverteilten Zufallszahlen zeigt, dass die Bins für 0 und 255 nur halb so hoch sind wie die übrigen
    • Allerdings ist schwer vorstellbar, wann dieser Bias zur Vermeidung von Extremwerten in der Praxis wirklich problematisch ist
      • Das Originalbild kann weiterhin verlustfrei hin- und zurückgewandelt werden (uint8 → float → uint8)
      • Ergebnisse, die leicht unter 0.0 oder über 1.0 liegen, werden dennoch korrekt in den richtigen Bin gerundet, wodurch die Ausgabeverteilung wieder gleichmäßiger wird
      • Beispiel: Wenn im Verarbeitungsschritt 0.005 vom Farbwert abgezogen wird, fällt Schwarz bei der Standardmethode unter 0, bei der alternativen Methode bleibt es positiv, aber beide Methoden liefern am Ende Integer 0
  • Ungenauigkeit

    • Die Fließkommawerte der Standardmethode sind nicht exakt, z. B. ist 128/255.0 ≈ 0.501961, während 128/256.0 = 0.5
    • Durch Rundungsfehler variiert der Abstand zwischen Fließkommawerten minimal, aber der Fehler ist so klein, dass er praktisch keine Rolle spielt
      • 32-Bit-Fließkommazahlen haben eine 23-Bit-Mantisse, und der Fehler liegt auf dem Niveau des niederwertigsten Bits, also unter 2⁻²³
      • Ein relativer Fehler von 0.00001 % ist selbst für anspruchsvolle Bildverarbeitung bedeutungslos; die Ungenauigkeit ist eher ein ästhetisches als ein technisches Problem
  • Werte, die auf keiner Ganzzahl liegen

    • Die alternative Methode platziert jeden Fließkommawert genau zwischen zwei Ganzzahlen
      • Da der ursprüngliche quantisierte Wert unbekannt ist, ist der Mittelpunkt zweier benachbarter Ganzzahlen ein sinnvoller Kompromiss als Schätzwert
    • Es gibt die Behauptung, dass Dithering dadurch einfacher wird (Andrew Keslers Blogbeitrag von 2015, "Converting Color Depth")
      • Man kann Rauschen hinzufügen, ohne sich um Sonderfälle zu kümmern
      • Umgekehrt erfordern die unhandlichen Extremwerte der Standardformel eine sorgfältigere Behandlung, wenn die Rauschverteilung konsistent bleiben soll

Zwei Arten von Quantisierern

  • Die beiden Ansätze lassen sich als zwei Arten von uniformen skalaren Quantisierern auffassen
  • Laut dem Wikipedia-Artikel zur Quantisierung werden uniforme Quantisierer für vorzeichenbehaftete Eingangsdaten in zwei Typen eingeteilt
    • mid-tread: bildet 0 auf die Rekonstruktionsstufe 0 ab (die waagerechte Stufe)
    • mid-riser: bildet 0 auf die Klassifikationsschwelle 0 ab (die senkrechte Stufenkante)
    • Wikipedia verweist als Quelle auf eine Arbeit von 1977 (Allen Gresho, "Quantization")
  • Quantisiererformeln (L ist die Anzahl der Ausgabestufen, z. B. 256)
    • mid-tread-Treppenquantisierer: Kodierung k = trunc(xL + 0.5), Dekodierung yₖ = k/L
    • mid-riser-Treppenquantisierer: Kodierung k = trunc(xL), Dekodierung yₖ = (k+0.5)/L
  • Auf die beiden Methoden angewandt
    • Standardformel = mid-tread (L=255)
    • Alternative Formel = mid-riser (L=256)
  • Die Standardmethode ist eine Kombination aus mid-tread für vorzeichenlose Eingaben mit dem Code L=255 und damit nicht optimal für 8-Bit-Eingaben
    • Diese Wahl dient der praktischen Bequemlichkeit, die Endpunkte auf 0.0 und 1.0 abzubilden
  • Höherer Quantisierungsfehler, aber praktisch nicht relevant

    • Bei einem System, das gleichverteilte reelle Werte x∈[0,1] in 8-Bit-Integer kodiert und anschließend wieder als reelle Werte rekonstruiert, verschwendet die Standardformel Bandbreite
      • Der darstellbare Bereich der Standardmethode ist [-0.5/255, 255.5/255] und damit weiter als für Eingaben in [0,1] nötig, was den Rekonstruktionsfehler erhöht
      • Laut einer Berechnung des StackOverflow-Nutzers Peter Mudrievskij beträgt der mittlere absolute Fehler bei Division durch 255 1/1020, bei Division durch 256 1/1024; daher ist die Division durch 256 theoretisch etwas präziser
    • In der Praxis rekonstruiert man aber nicht auf diese Weise
      • Voraussetzung ist ein 8-Bit-RGB-Bild, das geladen, verarbeitet und wieder gespeichert wird; beim Speichern hat man die Quantisierungsmethode oft nicht unter Kontrolle, und verlorene Information bleibt dauerhaft verloren
      • Wenn das Bild mit der Standardformel skaliert und gerundet gespeichert wurde, lässt sich durch Division durch 256 beim Laden keine Präzision zurückholen
      • Der geringere Rekonstruktionsfehler ist nur relevant, wenn man sowohl Speichern als auch Laden kontrolliert
    • Lädt man Bilder anderer mit der alternativen Formel, erzeugt man sogar mehr Fehler
      • Diese Bilder wurden höchstwahrscheinlich mit der Standardformel quantisiert; eine Dekodierung mit der falschen Skalierung ist theoretisch ungenauer
      • Praktisch sind Farben allerdings keine absoluten Messwerte, sodass man nur in einem etwas kleineren Bereich mit einem kleinen Offset arbeitet
    • Man darf die Kodierungs- und Dekodierungsschritte der beiden Quantisierer nicht mischen; genau das führt oft zu kaputtem Code

Fazit

  • Wenn man ein Bild verarbeitet, das von jemand anderem stammt, sollte man RGB-Werte durch 255 normalisieren
    • Sorgen über ungenaue Fließkommawerte oder abstrakte Rekonstruktionsfehler sind kein guter Grund für die Alternative
  • Wenn man das Speichern und Laden vollständig kontrolliert, 0 nicht auf 0 abbilden muss und damit leben kann, den Verarbeitungscode an den 8-Bit-Dynamikbereich zu binden, kann man durch Division durch 256 etwas mehr Präzision erreichen
    • Man sollte aber bedenken, dass ein Kollege das Bild mit der Standardformel laden und den Plan damit zunichtemachen könnte

Andere Sichtweisen

  • Jonathan Blows Text von 2002 behandelt mid-riser- und mid-tread-Quantisierer ohne diese Namen zu verwenden; von dort stammt die Idee für die Diagramme
  • Andrew Keslers Blogbeitrag von 2015 verteidigt die alternative Formel
    • Allerdings wird dort mit einer Standardformel ohne Rundung verglichen, wodurch ein Großteil der Analyse hinfällig wird

2 Kommentare

 
GN⁺ 3 일 전
Hacker-News-Kommentare
  • Was ein Farbwert genau bedeutet, ist bei 8 Bit pro Komponente meist kein großes Thema. Der Fehler durch den Unterschied zwischen einem Nenner von 255 oder 256 ist sehr klein, und um ihn zu sehen, müsste man ein gutes Farbempfinden haben und sehr nah an den Bildschirm herangehen; außerdem sind Monitor- oder Smartphone-Displays in der Regel nicht kalibriert.
    Wenn man jedoch mit einem Mikrocontroller ein VGA-Signal erzeugt und nur 8 Pins für die Farbausgabe hat (3 Rot, 3 Grün, 2 Blau), wird es ziemlich knifflig. In diesem Fall sind die Farbwerte selbst die Spannungspegel von 0 V bis 0,7 V, die an den VGA-Monitor gesendet werden müssen.
    Der Blaukanal wird auf 0→0 V, 1→0,23 V, 2→0,47 V, 3→0,7 V abgebildet, Rot/Grün dagegen auf 0→0 V, 1→0,1 V, …, 7→0,7 V. Lässt man die beiden Endpunkte außen vor, passt keine einzige Blau-Spannung zu den Rot-/Grün-Spannungen, sodass man kein reines Grau sehen kann; selbst die nächstliegenden Farben haben je nach Richtung einen leichten Blau- oder Gelbstich.
    Außerdem wirken fast alle Verläufe, in denen Blau mit anderen Kanälen gemischt wird, verschoben. Zum Beispiel sehen die nächstliegenden Farben auf der Linie von reinem Rot zu reinem Weiß leicht orange oder violett aus.
    Hier ist Code für 8-Bit-Farb-VGA-Ausgabe mit einem doppelt gepufferten 320x240-Framebuffer auf dem Raspberry Pi Pico 2: https://github.com/moefh/pico-vga-8bit-demo

    • Ich erinnere mich noch daran, als Kind auf verrauschte CRT-Bildschirme geschaut und an den Rändern schwache blaue und gelbe Linien gesehen zu haben. Ich habe mich immer gefragt, warum ausgerechnet diese beiden Farben; falls es derselbe Grund ist, verstehe ich es jetzt endlich.
    • Gammakorrektur wurde vergessen. Bevor Werte im Bereich 0–255 in Spannungen umgewandelt werden, potenziert der PC sie normalerweise mit 2.2.
      Dadurch treten Unterschiede bei kleinen und großen Werten viel deutlicher hervor: 2^2.2 = 4.595, 255^2.2 = 196,964.699
    • Für dieses Problem scheint zeitliches Dithering am besten geeignet zu sein. Delta-Sigma-Modulation pro Pixel lässt sich recht einfach umsetzen.
      Wenn es mit 30 Hz wechselt, dürfte es für Menschen schwer sein, den Unterschied zwischen leicht bläulich und leicht gelblich zu erkennen.
    • Deshalb waren RGBI-Farben in den 80ern wohl so verbreitet.
  • Ein Argument für 255 ist der Extremfall eines Schwarzweißbilds. Bei einem einzelnen Bit ist 0 Schwarz und 1 Weiß.
    Dass 0 auf 0.0 und 1 auf 1.0 abgebildet werden sollte, ist ziemlich klar. Es ist schließlich Schwarzweiß und nicht Hellgrau (0.25) und Dunkelgrau (0.75). Ein Schwarzweißbild wird also nicht mit 2, sondern mit 1 normalisiert.
    Bei 2 Bit hat man normalerweise 0=Schwarz, 1=Hellgrau, 2=Dunkelgrau, 3=Weiß, daher ist eine Abbildung auf 0.0, 0.33, 0.66, 1.0 naheliegend. Schwarz soll Schwarz und Weiß soll Weiß sein, und die Abstände sollen gleich sein, also normalisiert man mit 3.
    Führt man diese Logik bis zu 8 Bit fort, landet man bei einer Normalisierung mit 255. Denn auch wenn der Unterschied bei 8 Bit sehr klein wird, sollten Schwarz 0.0 und Weiß 1.0 sein.
    Die andere Methode, bei 8 Bit mit 256 zu normalisieren, führt dazu, dass sich der Ausgabebereich je nach Bitzahl verändert. Bei 1 Bit wäre es [0.25, 0.75], bei 2 Bit [0.125, 0.875]. Gewünscht ist normalerweise, dass mit steigender Bitzahl mehr Nuancen hinzukommen, nicht dass sich der Kontrast ändert.

  • Das war wirklich ein Text, der zum Nachdenken anregt, und er hat mich dazu gebracht, einige meiner eigenen Annahmen noch einmal zu prüfen.
    Aus Sicht der Elektrotechnik fällt es mir schwer, der Darstellung von „zwei Arten von Quantisierern“ im Text zuzustimmen. Mathematisch ist sie sauber, aber sie beschreibt keine realen Systeme.
    ADCs haben immer eine inhärente Quantisierungsunsicherheit von ±1/2 LSB. Die Übertragungskennlinie ist immer eine Mid-Tread-Abtastung, jedenfalls habe ich noch nie ein Gegenbeispiel gesehen. Das gilt sowohl für bipolare als auch für unipolare ADCs.
    Der kleinste Code entspricht der negativen Referenzspannung und der größte Code der positiven Referenzspannung. Ein Diagramm der Übertragungskennlinie zeigt, dass das oberste und unterste Intervall, wie im Text dargestellt, effektiv eine Breite von 1/2 LSB haben.
    In unipolaren Systemen kann man Mittelspannungen nicht exakt darstellen; anders gesagt tritt wieder das Grau-Problem auf. In bipolaren Systemen ist 0 V der Wert N/2 einer Mid-Tread-Kennlinie, aber das bedeutet nicht, dass es „256 Intervalle“ gibt.
    Deshalb würde ich weiter (VREF+ - VREF-) * k / (2^N - 1) verwenden. Ich stimme also der Normalisierung mit 255 zu. Letztlich ist das derselbe Fencepost-Fehler: Es gibt N Werte, aber N-1 Intervalle. Wenn es weniger Intervalle als Werte gibt, muss ein Intervall auf zwei Werte verteilt werden, und deshalb entstehen an den Endpunkten 1/2-LSB-Intervalle.

    • In jeder ADC-Dokumentation, die ich gesehen habe, steht, dass positive Vollaussteuerung nicht darstellbar ist. Bei einem 8-Bit-ADC mit ±1 V bedeutet -128 zum Beispiel -1 V, und +127 bedeutet 127/128=0.99219 V.
      Der Übergang von 126 auf 127 erfolgt 1.5 LSB unterhalb der positiven Vollaussteuerung. Ein Unterschied von 1 LSB bedeutet 1/128=0.00781 V und nicht 2/255=0.00784 V.
      Aber wenn in der Praxis Spannung und Unsicherheit wichtig sind, ist dieser Unterschied meist ohnehin bedeutungslos. Die Referenzspannung hat einen Bias und es gibt Linearitätsfehler. 1 LSB entspricht weder exakt 1/128 noch exakt 2/255, und man braucht Parameter zur Kalibrierung.
  • Das ähnelt in wissenschaftlichen Berechnungen dem Unterschied zwischen node-centered und cell-centered Samples in einer Dimension. Man muss entscheiden, ob ein Wert in der Mitte eines Intervalls liegt (oder im Zentrum eines Dreiecks/Tetraeders) oder auf einer Intervallgrenze (oder an einem Eckpunkt eines Dreiecks/Tetraeders).
    In wissenschaftlichen Berechnungen ergibt es keinen Sinn, mit der Datenverarbeitung zu beginnen, ohne zu wissen, wie die Werte interpretiert werden sollen. Auch bei der Audiosignalverarbeitung gilt: Wenn man nur einen Integer-Stream erhält, muss man wissen, welche Darstellungsabsicht hinter diesen Integern steckt, etwa ob es sich um mu-law-Codierung oder lineare Codierung handelt, bevor man Berechnungen am Ursprungssignal durchführen kann. Man erwartet, dass die an den Werten hängenden Metadaten diese Antwort liefern.
    Bei 8-Bit-Pixelwerten treibt man jedoch orientierungslos dahin, wenn es keine brauchbaren Metadaten im Dateiformat gibt, die die Darstellungsabsicht übermitteln, und dann gibt es keine richtige Antwort. Wie der Autor sagt, kann man niemanden dafür kritisieren, die Variante zu wählen, die für den eigenen Zweck bessere Ergebnisse liefert; man kann aber darauf hinweisen, dass Bits ohne Kontext ihre Bedeutung verlieren.

    • Das erinnert mich an den Normalisierungswert, der bei der Quantisierung von ESA-Sentinel-2-Level-2-Satellitenbildern verwendet wird.
      Grob gesagt sieht es so aus: Die Digital Number DN=0 bleibt als Wert „NO_DATA“ reserviert, und wenn DN im Bereich [1; 1;215-1] liegt, dann ist der L2A-SR-Reflexionswert L2A_SRi = (L2A_DNi + BOA_ADD_OFFSETi) / QUANTIFICATION_VALUE.
      https://sentiwiki.copernicus.eu/web/s2-products
  • Hier steckt der Fehler, anzunehmen, es gebe 256 Stufen von 0 bis 255. Tatsächlich gibt es 256 mit 8 Bit darstellbare Werte, und zwischen 0 (Schwarz) und 255 (reines Weiß) liegen 255 Intervalle.
    Deshalb ist das Teilen durch 255 kein Problem. Natürlich ist 128 nicht exakt ein mittleres Grau, und quantisierte 8-Bit-Werte von 0–255 liegen fast immer nicht in einem linear wahrgenommenen Raum, sondern in sRGB.
    Eine ähnliche Verwirrung entsteht in modernen APIs beim Umgang mit Abtastpositionen, weil Positionen als Koordinaten und nicht als Pixelzentren angegeben werden.

    • Die BeOS-API orientiert sich an Pixelzentren. Inzwischen kümmert das wohl niemanden mehr.
  • Algebraisch betrachtet ist die Antwort eindeutig: f(x) -> [0, 255].
    Wenn nicht f(n * 0) == n * f(0) gilt, passieren seltsame Dinge. Wenn zum Beispiel f(x) -> [0, 255], dann ist f(0) + f(0) + f(0) = 0 + 0 + 0 = 0 = f(0).
    Dagegen gilt bei f(x) -> [0.5/8, 7.5/8]: f(0) + f(0) + f(0) = 0.5/8 + 0.5/8 + 0.5/8 = 1.5/8 != f(0).
    Wenn man Letzteres wählt, kann man nicht erwarten, dass Rechnungen auf der x-Seite und Rechnungen auf der f(x)-Seite zusammenpassen. Das heißt, die algebraische Entsprechung geht kaputt.

  • Ich würde die +0.5-Lösung unterstützen. Erstens gefallen mir die halb so großen Intervalle an den Rändern nicht, und zweitens sind 255-basierte Darstellungen normalerweise keine HDR-, sondern SDR-Bilder.
    RGB-Werte stehen für Luminanz relativ zu einem Adaptionszustand, und die „0“ in einer Tagesszene ist nicht „Luminanz 0“. Sie ist nur ungefähr das 0,001-Fache des hellsten Punkts, und das sind immer noch Millionen von Photonen, also weit mehr als 0.
    In gewisser Weise erlebt das Auge Kontrast auf einer gleitenden Skala, und im System gibt es kein absolutes 0. So verwendeten Rundfunksysteme historisch 16–235 als SDR-Luminanzbereich. Ich halte die Logik „es muss unbedingt eine 0 geben“ für verzerrend und denke, dass man in den meisten Fällen keine 0 braucht.

    • Aus der Perspektive von jemandem, der viel VFX-Bildverarbeitung und Rendering gemacht hat, scheint hier vergessen zu werden, dass danach Farbraumkonvertierungen stattfinden. Bei älterem SDR etwa in lineares Rec.709 aus sRGB, bei neueren Formaten in breitere Farbräume. Die Stauchung des Dynamikbereichs passiert also erst nach dem Laden.
      Außerdem setzen viele Workflows für Bildverarbeitung und Compositing, ob zu Recht oder nicht, voraus, dass 0 eben 0 bedeutet. Deshalb wird bei 8 Bit oft angenommen, dass 0u auf 0.0f und 255 auf 1.0f abgebildet wird. Wenn bei Masken oder Alpha der Wert 0 etwas größer als 0.0 wird, entstehen Artefakte, weil irgendwo Code mit einem harten Schwellenwert von 0.0 andere Operationen maskiert. Umgekehrt wird ein Objekt nach Premultiplikation minimal transparent, wenn 255 im Alpha-Kanal nicht mehr 1.0f ist.
      Dasselbe kann bei +0.5 passieren, wenn in einer Maske 254 zu 1.0f wird.
    • Der Artikel konzentriert sich auf RGB, aber dasselbe Quantisierungsproblem gibt es bei allen möglichen Signalen, die zwischen diskreter und kontinuierlicher Darstellung abgebildet werden.
      Entscheidend ist nicht, ob man 0 Photonen darstellt, sondern ob man die in 1 Byte gespeicherte Information maximiert. Idealerweise sollte man den Bytewert 0 nicht seltener verwenden und auch keine Verzerrung hinzufügen, sodass Daten, die in den 0. Bucket gehört hätten, verschoben werden. Selbst in einem Farbraum von hell bis sehr hell sollte jedes Byte ein gleich großes Stück des Helligkeitsbereichs repräsentieren.
    • Dass Rundfunksysteme historisch 16–235 als SDR-Luminanzbereich verwendet haben, ist gerade das Problem. Leider leidet sogar „modernes“ HDMI noch unter dieser seltsamen Praxis, sodass das Bild ausgewaschen wirkt oder Schwarz absäuft, wenn Display und Quelle sich nicht einig sind.
    • Beide Lösungen addieren 0.5. Der Unterschied ist, an welcher Stelle im Prozess das geschieht.
    • Interessante Idee, aber es fühlt sich an, als würde die Welt verrücktspielen. Aus Sicht der Verarbeitungsprogramme werden das frühere Schwarz (0.0) und Weiß (1.0) plötzlich zu sehr dunklem Grau und sehr hellem Grau.
  • Wenn ein Lineal bis 12 Zoll geht, sollte man mit der Länge L normalisieren und nicht mit 13, der Anzahl der Punkte auf dem Lineal.

    • Die Analogie verwirrt mich. Ich weiß nicht, ob das „Lineal“ ein 255-Zoll-Lineal mit 256 Markierungen von 0 bis 255 ist oder ein 256-Zoll-Lineal mit 256 Abschnitten zu je 1 Zoll, also L = 256×1.
    • Wenn man in Wirklichkeit Zaunpfosten zählen will, dann ist der Zaunpfahlfehler kein Fehler.
    • Stimmt, aber >> 8 ist viel schneller.
    • Wer sagt denn, dass die Zahlen Punkte darstellen? Sie könnten auch die Intervalle zwischen den Punkten darstellen.
    • Bin ich dumm, oder beginnt 0 nicht am Startpunkt?
  • Angenehmer Artikel über ein Thema, über das ich lange nicht nachgedacht hatte. Er hat mich an Momente in der Spieleentwicklung erinnert, in denen die Spiellogik Gleitkomma-Arithmetik nutzte, Pixel Art aber auf ganzzahligen Koordinaten gezeichnet werden musste.
    An einigen Stellen habe ich etwas Ähnliches wie +0.5 verwendet, damit es weniger seltsam aussah. Das galt besonders bei bewegter Kamera, und die Kamera selbst musste ebenfalls gerastert werden.
    Der unten verlinkte Text von Jonathan Blow aus dem Jahr 2002 [1] war auch interessant. Die Visualisierung im ersten Artikel hilft sehr, wenn man tiefer einsteigen will.
    [1] https://web.archive.org/web/20240706043551/https://number-no...

 
GN⁺ 3 일 전
Lobste.rs-Meinungen
  • Sieht unordentlich aus, ist aber richtig: 255 ist der korrekte Wert
    Falls es nicht intuitiv wirkt, hilft ein degenerierter 2-Bit-Fall. Wenn die einzigen möglichen ganzzahligen Werte 0, 1, 2 und 3 sind und man die Umwandlung von Integer zu Fließkomma vollständig durchrechnet, erhält man 0.0, 0.33..., 0.66..., 1.0, wenn man seltsames Verhalten vermeiden will, bei dem Schwarz/Weiß nicht Schwarz/Weiß ist oder die Abstände offensichtlich ungleichmäßig sind
    Daher erfolgt die Rückumwandlung durch Multiplikation mit 3, nicht mit 4 (2^2)
    • Der erste Teil stimmt, aber daraus folgt nicht, dass man bei der „Rückumwandlung mit 3 multiplizieren muss und nicht mit 4“
      Für die Rückumwandlung braucht man Quantisierung (Rundung), und genau dort wird die Symmetrie gebrochen
      Wenn man einen gleichmäßigen reellen Gradienten im Bereich 0..=1 erzeugt und ihn auf 0, 1, 2, 3 quantisiert, sieht man, dass Multiplikation mit 3 kein gleichmäßiges Ergebnis liefert. Nach ×3 und round() sind 1 und 2 überrepräsentiert; nach ×3 und floor oder ceil werden 0 oder 3 wie Sonderfälle an den Rand gefaltet, sodass der Gradient so wirkt, als nutze er nur 3 der 4 Farben
      Die /3- und ×3-Logik sieht für exaktes Hin-und-zurück-Konvertieren von Zahlen in Ordnung aus, aber Zwischenwerte hängen stark von der Rundungswahl ab und werden wichtig, sobald man mit der Datenverarbeitung beginnt
      Gleichmäßige Integer-Anteile erhält man nur mit Multiplikation mit (4-ε) und anschließendem Abrunden, was äquivalent zu ×4, floor() und clamp() ist. Es fühlt sich wie ein seltsamer Off-by-one- oder ε-Unterschied an, ist aber intuitiv die am besten aussehende Lösung
  • Der Titel war ziemlich verwirrend. Ich weiß nicht, ob das Absicht war, aber am Ende wirkt es eher wie die Frage: „Entspricht 0..1 dem Bereich [0..255.0] oder [0.5..255.5]?“
    Für mich war die Antwort immer „natürlich“ [0.0..255.0], aber offenbar ist das nicht für alle selbstverständlich
    Im Artikel wird gesagt, die „Extrem“-Intervalle hätten nur halb so viel Kapazität wie die anderen, aber auch dieses Framing halte ich für falsch
    Wenn es keine Werte außerhalb von [0..1] gibt, ist das scheinbar schmalere Intervall nur ein Artefakt der Darstellung. Es wird nur schmaler gerendert, weil man die Buckets anhand des Wissens abgeschnitten hat, dass es keine Werte außerhalb des Bereichs gibt
    Wenn es dagegen Werte außerhalb von [0..1] gibt, dann ist dieser Bereich unendlich. Der Artikel akzeptiert Letzteres, aber nicht Ersteres
    Sobald man Ersteres akzeptiert, scheint das korrekte Verhalten klar zu sein, aber dass es überhaupt solche Artikel gibt, zeigt objektiv auch, dass das Problem nicht wirklich „klar“ ist :D
    • Wenn 0…255.0 wirklich so selbstverständlich ist, welcher Bereich von Fließkommawerten sollte dann zu Integer 0 zurückkehren, und welcher zu Integer 255?
      Wenn 0..<1 zu Integer 0 wird und 254>..255.0 zu Integer 255, dann wird 128 verschluckt. Vermutlich soll 127.5..128.5 zu 128 werden, aber wohin sollen dann diese Hälften gehen?
      Wenn man alles ein wenig verschiebt, damit 128 passt, wird 0..0.99609375 auf Integer 0 gemappt
  • Auch der Standardansatz scheint daraus entstanden zu sein, dass Leute ganz natürlich round() aufrufen
    Diese Methode fühlt sich für viele offenbar ziemlich natürlich an, und dürfte deshalb wegen ihrer Einfachheit zum Standard geworden sein
  • Ich frage mich, ob auch die umgekehrte Variante dessen nützlich wäre, was man mit 256 erreichen wollte. Also 0.0 auf 0, 1.0 auf 255 und alle übrigen Fließkommawerte auf 1 bis 254 abzubilden
    uint8_t output = 0.0f >= result  
                     ? 0  
                     : 1.0f <= result  
                     ? 255  
                     : 1 + 253*result;  
    
    Es wäre schön, wenn Schwarz während der Verarbeitung Schwarz bliebe und Weiß Weiß
    • Dann bekommen 0 und 255 innerhalb des Einheitsintervalls einen größeren Anteil als die anderen Zahlen. Ungefähr 0,8 %, also 255/253
  • Das erste Bild scheint in meiner Umgebung kaputt dargestellt zu werden
    • Hier ist der Autor des Artikels. Meinst du, dass die Bilddatei beschädigt ist? Ich habe sie mit pngcrush komprimiert. Oder meinst du, dass inhaltlich etwas mit dem Bild nicht stimmt?