sábado, 19 de septiembre de 2026

Ingeniería inversa de procesado de imagen con LUTs

En 'Ingeniería inversa de procesado de imagen con redes neuronales' replicábanos un procesado de imagen con un modelo hiper compacto, la red neuronal. En este artículo lograremos el mismo objetivo con la filosofía contraria, codificando la transformación por fuerza bruta mediante una enorme tabla de correspondencias o LUT (Look Up Table). El enfoque es el mismo que en 'Ingeniería inversa de curvas RGB con R', solo que ahora con muchos más grados de libertad.

Existen varios tipos de LUT en procesado de imagen, pero nos centraremos en las más comunes y polivalentes: las LUTs 3D. En ellas se codifica para una serie de valores RGB de partida la salida RGB correspondiente a cada entrada. Las LUT pueden definirse de la longitud (resolución) que deseemos, lo que determinará el número de entradas que tendrán y teóricamente la precisión conseguida.

Las LUTs codifican transformaciones de color solo dependientes de los niveles RGB origen, independientemente de los valores encontrados en píxeles vecinos o de su localización dentro de la imagen. Por ello no sirven para modelar procesados locales (enfoque, ruido, contraste local,...) ni tampoco para correcciones dependientes de la posición dentro de la imagen (viñeteo, aberraciones cromáticas,...). Son puras correspondencias de un color A a un color B.

Las LUTs 3D pueden almacenarse en dos formatos diferentes:

  • .cube: archivo de texto plano en el que, tras una breve cabecera descriptiva, se codifican numéricamente en 3 columnas las ternas RGB de salida a las que corresponderá cada combinación RGB de entrada contemplada, normalizadas entre 0 y 1.
  • HaldCLUT: archivo de imagen (PNG o TIFF) en el cual de forma directa e intuitiva podemos ver los colores destino que la LUT va a generar a partir de las posibles combinaciones RGB de entrada.

Si nos fijamos, los valores RGB de entrada a los que corresponden las salidas no aparecen por ningún lado. Esto es debido a que el formato LUT asume que los valores de entrada se muestrean de forma equiespaciada, variando los niveles R, G y B de forma ordenada y conocida. Así basta acudir a la localización del dato de salida: fila en el fichero .cube o posición del píxel en el PNG/TIFF, para saber a qué color origen corresponde.

No es posible codificar infinitas combinaciones de color con un número finito de entradas, por lo que los valores no encontrados directamente en la LUT se deberán interpolar al hacer uso de la misma. En consecuencia el resultado de aplicar una LUT sobre una imagen dependerá de la forma en que el software de procesado realice estas interpolaciones, aunque con LUTs suficientemente densas las diferencias deberían ser mínimas.

El formato HaldCLUT, además de ser potencialmente menos preciso (enteros de 8 o 16 bits vs los decimales en coma flotante de .cube), tiene una importante limitación, y es que debe poderse codificar en una imagen cuadrada, donde cada píxel se asocia a un valor RGB de entrada. Eso significa que no todas las resoluciones de LUT son susceptibles de codificarse en HaldCLUT, solo aquellas cuyo número de entradas sea cuadrado perfecto (raíz entera):

Fuente: resoluciones de LUT compatibles con el formato HaldCLUT

HaldCLUT tiene como ventajas que es mucho más compacto que .cube (unos 10-40KB vs más de 1MB para una LUT 36x36x36), y que presenta el procesado de forma muy visual como una imagen en color, sobre la que pueden hacerse directamente procesados y al guardar tendremos una nueva LUT que incorpore las novedades.

Con ayuda de Gemini construimos tres funciones relativas a LUTs:

  • build_lut(): función que a partir de dos imágenes (antes/después), almacena en una LUT de resolución parametrizable y en formatos .cube y HaldCLUT el mapeo de ternas RGB que lleva de la imagen origen a la procesada.
  • apply_lut(): función que a partir de una LUT en formato .cube o HaldCLUT, permite aplicarla a cualquier imagen arbitraria. Esto nos servirá para validar la precisión de la LUT.
  • plot_lut(): visualización en 3D de las traslaciones de color RGB que realiza una LUT en formato .cube, dibujando los colores destino dentro del cubo RGB origen.
~~~

Como primera prueba tomamos el procesado agresivo de Photoshop (curvas RGB, desaturación y rotación de tono) que ya modelamos en su día con una red neuronal. Para obtener el antes/después con que alimentar la función creadora de la LUT, aplicamos dicho procesado a una imagen sintética que contiene los 17 millones de colores disponibles en 8 bits (a la izquierda la original y a la derecha el procesado):

Hemos usado una LUT de resolución de 36x36x36 (36 valores diferentes por canal), aunque la más habitual es la LUT 33x33x33, para poder tener la versión también en HaldCLUT:


A simple vista se hace imposible distinguir el procesado de Photoshop y el producido al aplicar la LUT (hacer clic para ver en alta resolución):

El error promedio en la escala de 8 bits se sitúa en solo 0,37 niveles, siendo la distribución así:

~~~

Vamos con un segundo ejemplo, en este caso la conversión a BN por defecto de Photoshop. No tenemos ni idea de cómo realiza internamente Photoshop esta conversión a BN, pero con la LUT estaremos en disposición de aplicársela a cualquier imagen sin necesitar el programa.

La correspondencia con la imagen sintética y la LUT en formato HaldCLUT:

El resultado de nuevo es indistinguible del procesado en Photoshop (hacer clic para ver en alta resolución):

El error promedio en la escala de 8 bits se sitúa para esta LUT en solo 0,13 niveles, siendo la distribución así:

~~~

Como último caso de uso vamos a calcular LUTs de conversión entre espacios de color. Asignamos nuestra imagen sintética en Photoshop a los espacios sRGB y Adobe RGB, realizando sendas conversiones a ProPhoto RGB. Las LUTs que modelen los procesados serán una réplica de las conversiones internas de Photoshop entre esas dos parejas de espacios de color. Aplicar las LUTs sobre cualquier imagen resulta ser una muy buena aproximación de las conversiones realizadas por el motor de gestión de color de Photoshop.

Resulta interesante la visualización en 3D de las LUTs porque representa de manera muy intuitiva cómo de amplios son los espacios sRGB y Adobe RGB respecto a ProPhoto RGB. El cubo máximo corresponde a todos los posibles colores (combinaciones RGB) de ProPhoto RGB, y las mallas representan aquellos colores que existen respectivamente dentro de sRGB y de Adobe RGB. Lógicamente ambos son un subconjunto de ProPhoto RGB, siendo Adobe RGB un subconjunto más amplio que sRGB (hacer clic para ver en alta resolución):

~~~

La función de visualizacion plot_lut() es bastante parametrizable: densidad de la malla, información a presentar, etc... Las 8 esferas de color que podemos ver representan los puntos en el espacio color destino que ocupan los colores puros o vértices del cubo RGB origen: negro, blanco, rojo, verde, azul y las 3 combinaciones binarias.

En forma de línea gruesa se representa los colores destino que la LUT atribuye a una escala de gris. Cuando más se aleje esa línea de la diagonal perfecta más estará coloreando la LUT ciertos tonos de gris.

Por último existe la opción, usada arriba en la conversión a BN, de dibujar trazos de segmento que unen los colores origen con sus destinos tras aplicar la LUT.

Aquí la visualización para el primer ejercicio en color (hacer clic para ver en alta resolución):

~~~

Repositorio con el código R: GitHub.

No hay comentarios:

Publicar un comentario

Por claridad del blog, por favor trata de utilizar una sintaxis lo más correcta posible y no abusar del uso de emoticonos, mayúsculas y similares.