Go y Golang

Algoritmo Floyd-Steinberg Dithering con Golang

En este post de programación con Go te voy a enseñar cómo aplicar el algoritmo Floyd-Steinberg Dithering a cualquier imagen, de modo que puedas convertir una imagen de color completo a una en blanco y negro respetando la iluminación.

Para que tengas una idea de lo que hace este algoritmo, mira la siguiente comparación donde aparece la imagen a color, en blanco y negro sin dithering y finalmente en blanco y negro pero con dithering:

Comparación algoritmo Floyd-Steinberg Dithering – Golang

Como puedes ver, al aplicarle el dithering aparecen más detalles en la imagen, dando la ilusión de iluminación. De hecho pareciera que está en escala de grises, pero no, en este caso el dithering solo convierte la imagen a blanco y negro, nada más.

Veamos entonces cómo implementar este algoritmo Floyd-Steinberg Dithering con Golang a cualquier imagen ya sea PNG o JPG.

Explicación del algoritmo

El funcionamiento del dithering es convertir la imagen a otro nivel de colores, por ejemplo, imagen en color a imagen en blanco y negro. El tramado o dithering no solo hace la conversión, también propaga la diferencia de conversión entre el pixel original y el nuevo.

Veamos un ejemplo a continuación, y después veremos la implementación en el lenguaje de programación Go. Toma en cuenta que en este ejemplo vamos a convertir una imagen a color, a una imagen en blanco y negro, pero el tramado sirve para más paletas de colores.

Convertir a escala de grises

Primero necesitamos convertir la imagen en color a imagen en escala de grises. Necesitamos hacerlo con la imagen completa, ya que necesitamos que todos sus pixeles estén en un nivel de gris.

Para convertir a escala de grises supongamos que tenemos un pixel con el valor RGB en 200, 150, 168. Primero lo convertimos a grises, así que aplicamos la ecuación de la luminancia y queda en: 0.299*r+0.587*g+0.114*b

Lo que da como resultado: 59.8+88.05+19.152=167.002, ahora aplicamos ese valor para los 3 niveles, por lo que el RGB que previamente era 200,150,168 se convierte en 167,167,167, pues ya está en un gris.

Hacemos lo mismo por cada pixel existente en la imagen de modo que toda la imagen quede en escala de grises y cada pixel tenga un RGB igualado.

Convertir a blanco y negro

Una vez que tenemos el valor en gris de un pixel, vamos a convertirlo a blanco o negro. Para ello hacemos una simple conversión: si el gris es más blanco, entonces el pixel resultante será blanco (255 en RGB). Si no, negro (0 en RGB). Y para ello definimos un umbral que está a la mitad.

Debido a que RGB tiene valores del 0 al 255, podemos definir el umbral como 127. Si el valor del pixel es menor a 127, lo convertimos en negro. De lo contrario, lo convertimos en blanco.

Nota importante: en este ejemplo estamos convirtiendo una imagen de color completo a blanco y negro. El Dithering sirve para más paletas de colores, no solo para blanco y negro.

Calcular error de cuantificación

Siguiendo con nuestro ejemplo, teníamos el RGB en 200, 150, 168. Se convirtió a 167,167,167 en grises y después, debido a que el 167 es mayor que 127, se convirtió en un pixel blanco (255). Si seguimos ese proceso por cada pixel, tendríamos la imagen en blanco y negro sin más; pero no todo termina ahí.

Justamente en este punto es donde aplicamos el Dithering, mismo que nos dice que debemos obtener la diferencia también llamada error de cuantificación, específicamente de la conversión entre el nivel de gris original y el nuevo nivel de blanco y negro.

Volvamos a los números de nuevo, teníamos el RGB 200, 150, 168. Lo convertimos a 167, 167, 167 y luego a un 255, 255, 255. El error de cuantificación es:

errorDeCuantificacion := nivelDeGrisOriginal - nivelBlancoONegro

Por lo que en este caso sería 167-255, que da como resultado -88.

Propagar error de cuantificación

Ese -88 es el error de cuantificación que debemos propagar a los pixeles vecinos siguiendo la siguiente distribución:

  • imagen[x + 1][y ] := imagen[x + 1][y ] + error de cuantificación * 7 / 16
  • imagen[x – 1][y + 1] := imagen[x – 1][y + 1] + error de cuantificación * 3 / 16
  • imagen[x ][y + 1] := imagen[x ][y + 1] + error de cuantificación * 5 / 16
  • imagen[x + 1][y + 1] := imagen[x + 1][y + 1] + error de cuantificación * 1 / 16

Por ejemplo, supongamos que estamos en el pixel 5,6 (x con valor de 5, y con valor de 6) y tenemos nuestro error de cuantificación en -88. Queda así:

  • imagen[6][6] := imagen[6][6 ] + (-88 * 7 / 16)
  • imagen[4][7] := imagen[4][7] + (-88 * 3 / 16)
  • imagen[5 ][7] := imagen[5][7] + (-88 * 5 / 16)
  • imagen[6][7] := imagen6][7] + (-88 * 1 / 16)

Es importante que recuerdes que en este caso la imagen ya debe estar en escala de grises porque vamos a sumar ese error de cuantificación a cada valor RGB, y se supone que para este punto cada pixel va a tener los mismos valores para R,G y B que va a ser un gris. Calculando tenemos:

  • imagen[6][6] := imagen[6][6 ] + (-38.5)
  • imagen[4][7] := imagen[4][7] + (-16.5)
  • imagen[5 ][7] := imagen[5][7] + (-27.5)
  • imagen[6][7] := imagen6][7] + (-5.5)

Supongamos que los valores de cada pixel vecino son los siguientes:

  • imagen[6][6] = 20,20,20
  • imagen[4][7] = 52,52,52
  • imagen[5 ][7] = 96,96,96
  • imagen[6][7] = 28,28,28

Al aplicar la fórmula queda así:

  • imagen[6][6] := 20 + (-38.5)
  • imagen[4][7] := 52 + (-16.5)
  • imagen[5 ][7] :=96 + (-27.5)
  • imagen[6][7] := 28 + (-5.5)

Y finalmente así:

  • imagen[6][6] = -18.5, -18.5, -18.5,
  • imagen[4][7] = 35.5, 35.5, 35.5,
  • imagen[5 ][7] = 68.5, 68.5, 68.5,
  • imagen[6][7] = 22.5, 22.5, 22.5

Obviamente no podemos tener pixeles con valores negativos o decimales, hay que hacer las revisiones correspondientes para convertir el 35.5 a 35, por ejemplo, y convertir el -18.5 a 0. Lo que quiero que notes es cómo cambiaron los valores. Estaban así:

  • imagen[6][6] = 20,20,20
  • imagen[4][7] = 52,52,52
  • imagen[5 ][7] = 96,96,96
  • imagen[6][7] = 28,28,28

Y ahora así:

  • imagen[6][6] = 0,0,0
  • imagen[4][7] = 35,35,35
  • imagen[5 ][7] = 68,68,68
  • imagen[6][7] = 22,22,22

Fíjate en que, debido a que nuestro pixel original tenía un valor de 167 (más blanco), el error de cuantificación fue de -88, lo que hizo que sus pixeles vecinos sean más oscuros de lo que eran.

Repetimos ese proceso por cada pixel y gracias al error de propagación se genera un efecto de suavizado e iluminación en la imagen resultante, ya que cuando le toque el turno de ser procesado al que fue un pixel vecino, su valor original habrá cambiado gracias al error de propagación del pixel previo.

Código fuente

Ya te expliqué el algoritmo, ahora veamos el código de Golang. La función del algoritmo Floyd-Steinberg Dithering en Go queda así:

func floydSteinbergDithering(imagenOriginal image.Image) *image.RGBA {
 ancho, alto := imagenOriginal.Bounds().Max.X, imagenOriginal.Bounds().Max.Y
 imagenConDithering := image.NewRGBA(image.Rect(0, 0, ancho, alto))
 for y := 0; y < alto; y++ {
  for x := 0; x < ancho; x++ {
   nivelDeGris := convertirAEscalaDeGrises(imagenOriginal.At(x, y))
   nivelDeGrisUint8 := uint32AUint8(nivelDeGris)
   imagenConDithering.Set(x, y, color.RGBA{
    R: nivelDeGrisUint8,
    G: nivelDeGrisUint8,
    B: nivelDeGrisUint8,
    A: NivelAlfaTotalmenteOpacoPara8Bits,
   })
  }

 }
 for y := 0; y < alto; y++ {
  for x := 0; x < ancho; x++ {
   // En este punto la imagen ya es gris, podemos
   // acceder a cualquier nivel RGB para obtener
   // su nivel de gris, pues los 3 son iguales.
   // Yo accedo al nivel R
   nivelDeGrisOriginal, _, _, _ := imagenConDithering.At(x, y).RGBA()
   nivelBlancoONegro := colorMasCercanoSegunNivelDeGris(nivelDeGrisOriginal)
   nivelBlancoONegroUint8 := uint32AUint8(nivelBlancoONegro)
   imagenConDithering.Set(x, y, color.RGBA{
    R: nivelBlancoONegroUint8,
    G: nivelBlancoONegroUint8,
    B: nivelBlancoONegroUint8,
    A: NivelAlfaTotalmenteOpacoPara8Bits,
   })
   errorDeCuantificacion := nivelDeGrisOriginal - nivelBlancoONegro
   propagarError(imagenConDithering, x, y, errorDeCuantificacion)
  }

 }
 return imagenConDithering
}

El recorrido de la imagen se hace desde 0,0, avanzando fila por fila. En este caso estoy convirtiendo la imagen a escala de grises en primer lugar, y después ya estoy aplicando el dithering obteniendo el nivel de gris original, convirtiendo dicho nivel a un nivel blanco o negro, calculando el error de cuantificación y propagando el error.

Todo el procesamiento se está haciendo con Golang sobre una image.Image. La siguiente función se encarga de convertir un pixel a escala de grises:

func convertirAEscalaDeGrises(c color.Color) uint32 {
 r, g, b, _ := c.RGBA()
 return uint32(0.299*float64(r) + 0.587*float64(g) + 0.114*float64(b))
}

Por cierto, es buen momento para aclarar que en Golang un RGBA tiene como máximo valor 65535, es decir, 65536 posibles combinaciones, aunque cada valor de RGBA es un uint32. Por lo tanto no podemos trabajar con cada nivel de color suponiendo que sus valores van del 0 al 255; hay que hacer las conversiones necesarias. Esta conversión solo aplica en este lenguaje, pero el algoritmo sigue siendo el mismo.

Aquí está la función que propaga el error de cuantificación para aplicar el Floyd-Steinberg Dithering:

func propagarError(imagenResultante *image.RGBA, x int, y int, errorDeCuantificacion uint32) {
 /*
  pixels[x + 1][y    ] := pixels[x + 1][y    ] + quant_error × 7 / 16
  pixels[x - 1][y + 1] := pixels[x - 1][y + 1] + quant_error × 3 / 16
  pixels[x    ][y + 1] := pixels[x    ][y + 1] + quant_error × 5 / 16
  pixels[x + 1][y + 1] := pixels[x + 1][y + 1] + quant_error × 1 / 16
 */ establecerNivelDeColorEnImagen(imagenResultante, x+1, y, calcular716(errorDeCuantificacion, imagenResultante.At(x+1, y)))
 establecerNivelDeColorEnImagen(imagenResultante, x-1, y+1, calcular316(errorDeCuantificacion, imagenResultante.At(x-1, y+1)))
 establecerNivelDeColorEnImagen(imagenResultante, x, y+1, calcular516(errorDeCuantificacion, imagenResultante.At(x, y+1)))
 establecerNivelDeColorEnImagen(imagenResultante, x+1, y+1, calcular116(errorDeCuantificacion, imagenResultante.At(x+1, y+1)))
}

Poniendo todo junto

Te acabo de mostrar las funciones más importantes para aplicar el tramado a una imagen con Go, pero el código completo lo voy a dejar en GitHub. Fíjate que solo debes invocar a la función demostrarDithering así:

func main() {
 nombreImagenEntrada := "lagartija.jpg"
 nombreImagenSalida := "convertida.png"
 log.Printf("Error al convertir: %v", demostrarDithering(nombreImagenEntrada, nombreImagenSalida))
}

Una vez que tengas el código fuente, configura el nombre de la imagen de entrada y salida, compila con go build, ejecuta el programa recién compilado y verás los resultados de aplicar el tramado a una imagen con Go. Un ejemplo de imagen ya lo has visto al inicio del post.

También te dejo con un vídeo explicando todo este algoritmo y probando con algunas imágenes.

He estado investigando esta técnica porque la voy a aplicar en mi plugin para impresoras térmicas para mejorar la calidad de las imágenes, aunque técnicamente hablando no es mejorar la calidad, es aplicar un efecto de iluminación a las imágenes en blanco y negro.

Estoy aquí para ayudarte 🤝💻


Estoy aquí para ayudarte en todo lo que necesites. Si requieres alguna modificación en lo presentado en este post, deseas asistencia con tu tarea, proyecto o precisas desarrollar un software a medida, no dudes en contactarme. Estoy comprometido a brindarte el apoyo necesario para que logres tus objetivos. Mi correo es parzibyte(arroba)gmail.com, estoy como@parzibyte en Telegram o en mi página de contacto

No te pierdas ninguno de mis posts 🚀🔔

Suscríbete a mi canal de Telegram para recibir una notificación cuando escriba un nuevo tutorial de programación.
parzibyte

Programador freelancer listo para trabajar contigo. Aplicaciones web, móviles y de escritorio. PHP, Java, Go, Python, JavaScript, Kotlin y más :) https://parzibyte.me/blog/software-creado-por-parzibyte/

Entradas recientes

Leer 10 mil números y ordenar con C

En el ejercicio de programación de hoy vamos a trabajar con ANSI C para leer…

3 semanas hace

Generador de números aleatorios online

Hoy te quiero compartir una herramienta en línea para generar números aleatorios directamente en el…

3 semanas hace

Comprimir PDF con Bot de Telegram

Comprimir un PDF con Telegram es posible gracias a los Bots. Anteriormente en mi blog…

3 semanas hace

MySQL – Guardar combinación de días de la semana

En este post te voy a enseñar a guardar solo algunos días de la semana…

1 mes hace

Guía de inicio rápido para impresora térmica

En este post te enseñaré a usar tu impresora térmica comenzando en el paso de…

1 mes hace

Solución a ERR_SSL_CIPHER_OPERATION_FAILED

Hoy voy a tratar de solucionar el error ERR_SSL_CIPHER_OPERATION_FAILED que aparece al usar npm install…

1 mes hace

Esta web usa cookies.