8 puntos por GN⁺ 6 일 전 | 2 comentarios | Compartir por WhatsApp
  • Diferencia entre el método estándar de dividir entre 255 y el método alternativo de sumar un sesgo de 0.5 y dividir entre 256 al convertir colores enteros de 8 bits a punto flotante
  • El método de 255 mapea el entero 0 a 0.0 y 255 a 1.0, por lo que es fácil manejar directamente el negro y el blanco, y además coincide con la conversión de UNORM a float usada por la GPU
  • El método de 256 usa (img + 0.5) / 256.0 para colocar cada valor en el centro de su intervalo, lo que puede simplificar el tratamiento de bordes en tareas como el dithering, pero como 0 no es 0.0, la lógica de procesamiento queda atada a entradas de 8 bits
  • En el método de 255, los intervalos de los extremos tienen la mitad del ancho, así que si vuelves a redondear a 8 bits un número aleatorio uniforme en [0, 1], 0 y 255 aparecen con la mitad de frecuencia que los demás valores, aunque la conversión real de ida y vuelta de imágenes sigue funcionando sin pérdidas
  • Si vas a procesar imágenes de terceros, la respuesta correcta es normalizar con 255; solo vale la pena considerar el método de 256 si controlas tanto el guardado como la carga

Planteamiento del problema

  • En un programa que recibe una imagen, la convierte a punto flotante, la procesa y luego la vuelve a guardar como color de 8 bits, el punto en discusión es el método de conversión entre enteros y punto flotante
  • Existen dos enfoques
    • Método estándar (dividir entre 255): pixels = img / 255.0 → procesamiento → output = np.trunc(result * 255 + 0.5)
    • Método alternativo (dividir entre 256): pixels = (img + 0.5) / 256.0 → procesamiento → output = np.trunc(result * 256)
    • En ambos casos, antes de la conversión final de tipo, el valor se limita al rango 0~255: output.clip(0, 255).astype(np.uint8)
  • El método estándar mapea el entero 0 a 0.0 y 255 a 1.0, y es igual al método de conversión de UNORM a float de la GPU
  • El método alternativo suma un sesgo de 0.5, de modo que el entero 0 se mapea a 0.5/256 = 0.001953125
    • Por eso, si no conoces esa constante, no puedes detectar píxeles negros
    • Incluso si haces cálculos en punto flotante, la lógica queda atada a entradas de 8 bits
    • En el método estándar, siempre se puede asumir que el negro es 0.0

Objeciones a 255.0

  • Si dibujas el método estándar sobre la recta numérica, se ve algo extraño
  • Hay bins más pequeños en ambos extremos

    • Los bins de los extremos en la fórmula estándar sobresalen fuera del rango [0,1], dando una forma “estirada” al rango
    • Al convertir de punto flotante de vuelta a entero, el ancho de los bins de los extremos es solo la mitad del de los demás
      • Eso hace que al algoritmo le cueste más “producir” valores extremos
      • Si generas ruido uniforme en [0,1] y lo redondeas con la fórmula estándar, los valores 0 y 255 aparecen con la mitad de frecuencia que otros enteros
    • Si observas el histograma de un millón de números aleatorios uniformes, se puede verificar que la frecuencia de 0 y 255 es la mitad de la de los otros bins
    • Aun así, es difícil imaginar una situación real en la que ese sesgo para evitar extremos sea un problema
      • La imagen original sigue haciendo el recorrido de ida y vuelta sin pérdidas (uint8 → float → uint8)
      • Incluso resultados que se salen ligeramente de 0.0 o 1.0 se redondean al bin correcto y la distribución de salida se equilibra
      • Ejemplo: si durante el procesamiento se resta 0.005 al color, con el método estándar el negro cae por debajo de 0, mientras que con el alternativo sigue siendo positivo, pero en ambos casos el resultado final es el entero 0
  • Inexactitud

    • Los valores en punto flotante del método estándar no son exactos; por ejemplo, 128/255.0 ≈ 0.501961, mientras que 128/256.0 = 0.5
    • El error de redondeo hace que la distancia entre valores de punto flotante varíe ligeramente, pero el error es tan pequeño que no representa un problema real
      • Un flotante de 32 bits tiene una mantisa de 23 bits, y el error está en el nivel del bit menos significativo, por debajo de 2⁻²³
      • Un error relativo de 0.00001% no tiene importancia ni siquiera en procesamiento de imagen sofisticado; la inexactitud es un problema estético, no técnico
  • Valores que no pertenecen al rango entero

    • El método alternativo coloca cada valor en punto flotante exactamente en medio de dos enteros
      • Como no se puede saber el valor cuantizado original, tomar el punto medio entre dos enteros consecutivos es un compromiso razonable como estimación
    • Existe el argumento de que esto facilita el dithering (la entrada de blog de Andrew Kesler de 2015, "Converting Color Depth")
      • Se puede agregar ruido sin preocuparse por casos límite
      • En cambio, los valores extremos incómodos de la fórmula estándar requieren un tratamiento cuidadoso para mantener la consistencia en la distribución del ruido

Dos tipos de cuantizadores

  • Ambos enfoques pueden verse como dos tipos de cuantizador escalar uniforme
  • Según el artículo de Wikipedia sobre cuantización, los cuantizadores uniformes para datos de entrada con signo se clasifican en dos tipos
    • mid-tread: mapea 0 al nivel de reconstrucción con valor 0 (la huella del escalón)
    • mid-riser: mapea 0 al umbral de clasificación con valor 0 (la contrahuella del escalón)
    • Wikipedia cita como fuente un artículo de 1977 (Allen Gresho, "Quantization")
  • Fórmulas del cuantizador (L es el número de niveles de salida, por ejemplo 256)
    • Cuantizador escalonado mid-tread: codificación k = trunc(xL + 0.5), decodificación yₖ = k/L
    • Cuantizador escalonado mid-riser: codificación k = trunc(xL), decodificación yₖ = (k+0.5)/L
  • Aplicado a estos dos métodos
    • La fórmula estándar = mid-tread (L=255)
    • La fórmula alternativa = mid-riser (L=256)
  • El método estándar usa mid-tread para una entrada sin signo junto con el código L=255, una combinación que no es óptima para entradas de 8 bits
    • Es una elección por conveniencia de programación para mapear los extremos a 0.0 y 1.0
  • Mayor error de cuantización, pero en la práctica no

    • Si fuera un sistema que codifica un número real con distribución uniforme x∈[0,1] como entero de 8 bits y luego lo reconstruye como real, la fórmula estándar desperdicia ancho de banda
      • El rango representable del método estándar es [-0.5/255, 255.5/255], más amplio de lo necesario para una entrada en [0,1], lo que incrementa el error de reconstrucción
      • Según el cálculo del usuario de StackOverflow Peter Mudrievskij, el error absoluto medio es 1/1020 con divisor 255 y 1/1024 con divisor 256, por lo que dividir entre 256 es teóricamente un poco más preciso
    • Pero en la práctica no se está haciendo ese tipo de reconstrucción
      • La premisa es cargar una imagen RGB de 8 bits, procesarla y volver a guardarla; al guardarla no se puede controlar el método de cuantización, y la información perdida desaparece para siempre
      • Si una imagen se guardó multiplicando y redondeando con la fórmula estándar, cargarla luego dividiendo entre 256 no recupera precisión
      • La supuesta ventaja de menor error de reconstrucción solo tiene sentido cuando controlas tanto el guardado como la carga
    • Si cargas una imagen de otra persona con la fórmula alternativa, en realidad introduces más error
      • Lo más probable es que haya sido cuantizada con la fórmula estándar, así que decodificarla con una escala incorrecta es teóricamente inexacto
      • En la práctica, como el color no es una medición absoluta, esto equivale a procesar dentro de un rango un poco más pequeño y con un pequeño desplazamiento
    • No se deben mezclar las etapas de codificación y decodificación de ambos cuantizadores; es una forma común de terminar con código roto

Conclusión

  • Si vas a procesar imágenes que te dio otra persona, debes normalizar los valores RGB con 255
    • Los temores sobre valores de punto flotante inexactos o sobre un error de reconstrucción abstracto no son una buena razón para elegir la alternativa
  • Si controlas tanto el guardado como la carga de la imagen, no necesitas mapear 0 a 0 y no te molesta atar el código de procesamiento al rango dinámico de 8 bits, entonces puedes dividir entre 256 para ganar un poco más de precisión
    • Eso sí, ten en cuenta que un colega podría cargar la imagen con la fórmula estándar y arruinar el plan

Otras opiniones

2 comentarios

 
GN⁺ 6 일 전
Opiniones de Hacker News
  • Qué significan exactamente los valores de color casi no importa cuando son 8 bits por componente. El error que aparece por usar 255 o 256 como denominador es muy pequeño, y para notar la diferencia habría que tener muy buena percepción del color y pegarse mucho a la pantalla; además, los monitores y las pantallas de los teléfonos normalmente ni siquiera están calibrados.
    Pero si generas una señal VGA con un microcontrolador y solo tienes 8 pines de salida de color (3 para rojo, 3 para verde y 2 para azul), sí se vuelve bastante problemático. Ahí el valor de color es literalmente el nivel de voltaje de 0V a 0.7V que hay que enviar al monitor VGA.
    El canal azul se mapea como 0→0V, 1→0.23V, 2→0.47V, 3→0.7V, y rojo/verde como 0→0V, 1→0.1V, …, 7→0.7V. Si excluyes los extremos, los voltajes del azul no coinciden en absoluto con los de rojo/verde, así que no puedes ver un gris puro, y hasta el color más cercano queda con un ligero tinte azul o amarillo según la dirección de la diferencia.
    Además, casi todos los gradientes que mezclan azul con otros canales también se ven desalineados. Por ejemplo, los colores más cercanos sobre la línea que va de rojo puro a blanco puro se ven un poco anaranjados o morados.
    Aquí está el código para salida VGA de color de 8 bits con framebuffer doble de 320x240 en Raspberry Pi Pico 2: https://github.com/moefh/pico-vga-8bit-demo

    • Recuerdo que de niño, viendo una pantalla CRT con ruido, notaba unas tenues líneas azules y amarillas en los bordes. Siempre me pregunté por qué justo esos dos colores; si la causa es la misma, apenas ahora entiendo por qué.
    • Falta la corrección gamma. Antes de convertir el valor de 0 a 255 en voltaje, la PC normalmente eleva ese valor a la potencia 2.2.
      Eso hace que la diferencia entre valores pequeños y grandes se note muchísimo más: 2^2.2 = 4.595, 255^2.2 = 196,964.699
    • Para este problema, lo mejor parece ser el dithering temporal. La modulación delta-sigma por píxel se puede hacer con bastante facilidad.
      Si cambia a 30Hz, a la gente probablemente le cueste distinguir entre un azul leve y un amarillo leve.
    • Supongo que por eso en los 80 eran tan comunes los colores RGBI.
  • Como argumento a favor de 255, basta ver el caso extremo de una imagen en blanco y negro. Con un solo bit, 0 es negro y 1 es blanco.
    Parece bastante claro que 0 debe mapearse a 0.0 y 1 a 1.0. Es blanco y negro, no gris claro (0.25) y gris oscuro (0.75). Es decir, una imagen en blanco y negro se normaliza por 1, no por 2.
    Con 2 bits, normalmente 0=negro, 1=gris claro, 2=gris oscuro, 3=blanco, así que lo natural es mapearlos a 0.0, 0.33, 0.66 y 1.0. El negro debe ser negro, el blanco debe ser blanco y el espaciado debe ser uniforme, así que se normaliza por 3.
    Si extiendes esa lógica a 8 bits, terminas normalizando por 255. Aunque la diferencia sea muy pequeña con 8 bits, negro debe ser 0.0 y blanco debe ser 1.0.
    Si en cambio usas normalización por 256 con 8 bits, el rango de salida pasa a depender de la cantidad de bits. Con 1 bit sería [0.25, 0.75], con 2 bits [0.125, 0.875], etc. Normalmente lo que quieres es más matices al aumentar los bits, no cambiar el contraste.

  • Fue un texto que realmente da para pensar, y en lo personal me hizo replantear algunas suposiciones que tenía.
    Desde una formación en ingeniería eléctrica, me cuesta aceptar la presentación de “dos tipos de cuantizadores” del artículo. Es rigurosa en lo matemático, pero no es una explicación basada en sistemas reales.
    En un ADC siempre existe inherentemente una incertidumbre de cuantización de ±1/2 LSB. La característica de transferencia siempre es de muestreo mid-tread, y al menos yo no he visto contraejemplos. Esto vale tanto para ADC bipolares como unipolares.
    El código más bajo corresponde al voltaje negativo de referencia y el más alto al positivo. La gráfica de transferencia muestra, como en el artículo, que las bandas extrema superior e inferior tienen efectivamente un ancho de 1/2 LSB.
    En sistemas unipolares no puedes representar exactamente el voltaje intermedio; en otras palabras, aparece el problema del gris. En sistemas bipolares, 0V es el valor N/2 del mid-tread, pero eso no significa que existan “256 intervalos”.
    Por eso yo seguiría usando (VREF+ - VREF-) * k / (2^N - 1). O sea, estoy de acuerdo con normalizar por 255. Al final es como el error de los postes de una cerca: hay N valores, pero N-1 intervalos. Si hay menos intervalos que valores, entonces un intervalo tiene que repartirse entre dos valores, y por eso aparece un intervalo de 1/2 LSB en los extremos.

    • Toda la documentación de ADC que he visto dice que no se puede representar la escala completa positiva. Por ejemplo, en un ADC de 8 bits ±1V, -128 significa -1V y +127 significa 127/128=0.99219V.
      La transición de 126 a 127 ocurre a una distancia de 1.5 LSB del extremo positivo del rango completo. Una diferencia de 1 LSB implica 1/128=0.00781V, no 2/255=0.00784V.
      Pero en la práctica, si lo importante son el voltaje y la incertidumbre, casi nunca importa esa diferencia. Hay sesgo en el voltaje de referencia y también errores de linealidad. Un LSB no coincide exactamente ni con 1/128 ni con 2/255, y entonces hacen falta parámetros de calibración.
  • Esto se parece a la diferencia, en cálculo científico, entre muestras centradas en nodo y centradas en celda, visto en una dimensión. Hay que decidir si el valor está en el centro del intervalo (o del triángulo/tetraedro) o en el borde del intervalo (o en el vértice del triángulo/tetraedro).
    En cálculo científico no tiene sentido empezar a procesar datos sin saber cómo deben interpretarse los valores. En procesamiento de audio también, si solo recibes un flujo de enteros, necesitas saber qué intención de representación tienen esos enteros —por ejemplo, si usan codificación mu-law o lineal— para poder hacer cálculos sobre la señal original. Uno esperaría que los metadatos asociados al valor dieran esa respuesta.
    Pero con valores de píxel de 8 bits, si no hay metadatos adecuados en el formato de archivo para transmitir esa intención de representación, quedas a la deriva y no existe una respuesta correcta. Como dice el autor, no se le puede reprochar que elija lo que le da mejores resultados en su caso de uso, pero sí se puede señalar que bits sin contexto terminan perdiendo significado.

    • Eso me recordó el valor de normalización usado en la cuantización de imágenes satelitales Sentinel-2 level-2 de la ESA.
      A grandes rasgos es algo así: el número digital DN=0 se reserva como valor “NO_DATA”, y cuando DN está en el rango [1; 1;215-1], el valor de reflectancia L2A SR es L2A_SRi = (L2A_DNi + BOA_ADD_OFFSETi) / QUANTIFICATION_VALUE.
      https://sentiwiki.copernicus.eu/web/s2-products
  • Aquí hay un error al asumir que existen 256 niveles de 0 a 255. En realidad, hay 256 valores que pueden representarse con 8 bits, y hay 255 intervalos entre 0 (negro) y 255 (blanco puro)
    Así que dividir entre 255 no tiene nada de malo. Claro, 128 no es exactamente un gris medio, y los valores cuantizados de 8 bits entre 0 y 255 casi siempre están en sRGB, no en un espacio perceptual lineal
    En las APIs modernas también aparece una confusión parecida al manejar posiciones de muestreo, porque la posición se especifica como coordenadas y no como centro de píxel

    • La API de BeOS se basaba en el centro del píxel. Aunque ya a nadie le importará
  • Visto algebraicamente, la respuesta es clara: f(x) -> [0, 255]
    Si no se cumple f(n * 0) == n * f(0), empiezan a pasar cosas raras. Por ejemplo, si f(x) -> [0, 255], entonces f(0) + f(0) + f(0) = 0 + 0 + 0 = 0 = f(0)
    En cambio, si f(x) -> [0.5/8, 7.5/8], entonces f(0) + f(0) + f(0) = 0.5/8 + 0.5/8 + 0.5/8 = 1.5/8 != f(0)
    Si eliges lo segundo, no puedes esperar que un cálculo hecho del lado de x y otro hecho del lado de f(x) coincidan. Es decir, se rompe la correspondencia algebraica

  • Quiero defender la solución de +0.5. Primero, no me gustan los intervalos de medio tamaño en los bordes, y segundo, una representación basada en 255 normalmente no es HDR sino una imagen SDR
    Los valores RGB representan luminancia respecto a un estado de adaptación, y el “0” de una escena diurna no es “luminancia 0”. Es apenas cerca de 0.001 veces el punto más brillante, y aun así son millones de fotones, muchísimo más que 0
    En cierto sentido, el ojo experimenta el contraste como una escala deslizante, y no existe un 0 absoluto dentro del sistema. Por ejemplo, los sistemas de broadcast históricamente usaron 16~235 como rango de luminancia SDR. Considero que la lógica de “tiene que existir un 0 sí o sí” introduce sesgo, y creo que en la mayoría de los casos no hace falta un 0

    • Desde la experiencia de haber hecho mucho procesamiento y renderizado de imágenes para VFX, parece que se está olvidando que después viene una transformación de espacio de color. En SDR antiguo, por ejemplo, se convertía al Rec.709 lineal de sRGB; en formatos más recientes, a gamas más amplias. Así que la compresión del rango dinámico ocurre después de la carga
      Además, una gran parte de los flujos de trabajo de procesamiento y composición de imágenes, estén bien o mal, asumen que 0 significa 0. Por eso muchas veces se considera que en 8 bits, 0u se mapea a 0.0f y 255 a 1.0f. Si en una máscara o un alfa el valor 0 queda apenas por encima de 0.0, algún código en alguna parte terminará enmascarando otras operaciones con un umbral duro de 0.0 y aparecerán artefactos. A la inversa, si en el alfa 255 deja de ser exactamente 1.0f, después de la premultiplicación el objeto quedará apenas transparente
      Lo mismo puede pasar cuando, por culpa del +0.5, en el enmascarado 254 pasa a ser 1.0f
    • El texto se enfoca en RGB, pero el mismo problema de cuantización existe en todo tipo de señales que se mapean entre una representación discreta y una continua
      La clave no es si se representan 0 fotones, sino si se maximiza la información almacenada en 1 byte. Idealmente, no deberíamos usar menos el valor de byte 0, ni tampoco introducir un sesgo en los datos que debían caer en el bucket 0. Incluso en un espacio de color que va de brillante a muy brillante, todos los bytes deberían representar fragmentos del mismo tamaño del rango de brillo
    • Que los sistemas de broadcast hayan usado históricamente 16~235 como rango de luminancia SDR es precisamente el problema. Lamentablemente, incluso el HDMI “moderno” todavía arrastra esta práctica extraña, así que si la pantalla y la fuente no se ponen de acuerdo, la imagen se ve lavada o los negros quedan aplastados
    • Ambas soluciones suman 0.5. La diferencia está en qué parte del proceso ocurre eso
    • Es una idea interesante, pero da la sensación de que te mueve el piso. Desde el punto de vista de un programa de procesamiento, el negro de antes (0.0) y el blanco (1.0) pasan a ser un gris muy oscuro y un gris muy brillante
  • Si una regla llega hasta las 12 pulgadas, deberías normalizar por la longitud L, no por la cantidad de puntos sobre la regla, que serían 13

    • Esa analogía me confunde. No sé si la “regla” es una regla de 255 pulgadas con 256 puntos marcados de 0 a 255, o una regla de 256 pulgadas con 256 intervalos de 1 pulgada, de modo que L = 256×1
    • Si lo que en verdad quieres contar son postes de cerca, entonces el error del poste de cerca no es un error
    • Sí, pero >> 8 es mucho más rápido
    • ¿Quién decidió que los números representan puntos? También podrían representar los intervalos entre puntos
    • ¿Soy yo el tonto? ¿El 0 no empieza en el punto de inicio?
  • Fue un artículo agradable de leer porque trata un tema en el que no pensaba desde hacía tiempo. Me hizo recordar momentos en desarrollo de juegos donde la lógica del juego usaba matemáticas de punto flotante pero el pixel art había que dibujarlo en coordenadas enteras
    En varios lugares intentamos usar algo parecido a +0.5 para que se viera menos raro. Sobre todo cuando había una cámara en movimiento, y la cámara también había que recortarla
    También fue interesante el texto de Jonathan Blow de 2002 enlazado abajo [1]. La visualización del primer artículo ayuda mucho cuando uno quiere profundizar más
    [1] https://web.archive.org/web/20240706043551/https://number-no...

 
GN⁺ 6 일 전
Opiniones en Lobste.rs
  • Se ve desprolijo, pero es correcto: el valor correcto es 255
    Si no resulta intuitivo, se puede ver con un caso degenerado de 2 bits. Si los únicos valores enteros posibles son 0, 1, 2 y 3, al calcular toda la conversión entero→punto flotante, para evitar comportamientos raros donde negro/blanco no sean negro/blanco o donde los intervalos sean claramente desiguales, el resultado sería 0.0, 0.33..., 0.66..., 1.0
    Por lo tanto, la conversión inversa no consiste en multiplicar por 4 (2^2), sino en multiplicar por 3
    • La primera parte es correcta, pero de ahí no se sigue que “la conversión inversa debe multiplicar por 3 y no por 4”
      La conversión inversa requiere cuantización (redondeo), y justo ahí es donde se rompe la simetría
      Si se crea un gradiente uniforme de números reales en el rango 0..=1 y se cuantiza a 0, 1, 2 y 3, se puede ver que multiplicar por 3 produce un resultado desigual. Con ×3 y round(), 1 y 2 quedan sobrerrepresentados; con ×3 y luego floor o ceil, 0 o 3 se pliegan como singularidades, haciendo que el gradiente parezca usar solo 3 de los 4 colores
      La lógica de /3 y ×3 puede parecer razonable al convertir valores exactos de ida y vuelta, pero los valores intermedios dependen mucho de la elección del redondeo, y eso importa en cuanto se empieza a procesar datos
      La única forma de lograr proporciones enteras uniformes es multiplicar por (4-ε) y truncar hacia abajo, lo cual equivale a ×4, floor() y clamp(). Se siente como un error raro de diferencia de 1 o de ε, pero intuitivamente es la solución que mejor se ve
  • El título me confundió bastante. No sé si fue a propósito, pero al final parece más bien “¿0..1 corresponde a [0..255.0] o a [0.5..255.5]?”
    Para mí la respuesta siempre fue “obviamente” [0.0..255.0], pero quizá no sea algo obvio para todo el mundo
    En el artículo se dice que los intervalos “extremos” tienen solo la mitad de capacidad que los demás, pero tampoco creo que ese encuadre sea correcto
    Si no existen valores fuera de [0..1], que se vea como un intervalo más angosto es un artefacto del renderizado. Solo se renderiza más angosto porque se recortaron los buckets sabiendo que no hay valores fuera del rango
    En cambio, si sí existen valores fuera de [0..1], entonces ese rango es infinito. El artículo reconoce esto último, pero no lo primero
    En cuanto se acepta lo primero, el comportamiento correcto parece evidente, aunque el simple hecho de que exista un artículo así también significa objetivamente que no es un tema tan “evidente” :D
    • Si de verdad te parece obvio 0…255.0, entonces ¿qué rango de valores de punto flotante debería volver al entero 0 y qué valores deberían volver al entero 255?
      Si 0..<1 va al entero 0 y 254>..255.0 va al entero 255, entonces 128 queda absorbido. Probablemente querrías que 127.5..128.5 se convierta en 128, pero entonces ¿a dónde deberían ir esas mitades?
      Si desplazas un poco todo el rango para que 128 encaje, entonces 0..0.99609375 se mapea al entero 0
  • El enfoque estándar también parece haber surgido porque la gente naturalmente termina llamando a round()
    Como a la gente ese método le resulta bastante natural, parece haberse vuelto el estándar por su simplicidad
  • Me pregunto si podría servir el enfoque opuesto a lo que se intentaba lograr con 256. Es decir, enviar 0.0 a 0, 1.0 a 255, y mapear los demás valores de punto flotante entre 1 y 254
    uint8_t output = 0.0f >= result  
                     ? 0  
                     : 1.0f <= result  
                     ? 255  
                     : 1 + 253*result;  
    
    Estaría bien que durante el procesamiento también se mantuvieran negro como negro y blanco como blanco
    • Si haces eso, 0 y 255 se quedan con una porción mayor del intervalo unitario que los demás números. Aproximadamente 0.8%, es decir, 255/253
  • La primera imagen se ve rota en mi entorno
    • Soy el autor del artículo. ¿Quieres decir que el archivo de imagen está dañado? Lo comprimí con pngcrush. ¿O quieres decir que hay algo mal con el contenido de la imagen?