domingo, 24 de septiembre de 2017

Ingeniería inversa de curvas RGB con R

En este artículo vamos a usar el cálculo de curvas RGB como excusa para practicar el procesado matricial de imágenes con R. Una curva RGB es una transformación aplicada a los valores RGB (rojo, verde y azul) de una imagen para alterar su apariencia, de modo que cada uno de los posibles niveles de entrada es sustituido por un nivel de salida siguiendo una función predefinida en cada canal.



Si a la imagen anterior le aplicamos desde Photoshop las siguientes curvas:



Obtenemos un resultado donde puede verse que las zonas más luminosas han adquirido dominantes amarillentas (por defecto de componente azul), mientras que las sombras se han azulado al aplicarse el proceso contrario en ellas.



Imaginemos ahora que como dato solo tuviéramos las imágenes inicial y final. Cargando ambas en arrays y realizando la comparación píxel a píxel entre los valores de entrada y salida, podremos obtener las curvas RGB que dieron lugar a la transformación.

Como alternativa a un bucle que recorra píxel a píxel cada imagen, se han usado las cualidades vectoriales de R para hacer el cálculo de forma diferente. Un bucle mucho más corto recorre cada uno de los posibles niveles discretos de entrada [0..255], empleando la potente función which() que devuelve en un vector los índices de la matriz (imagen origen) cuyos valores casan con cada nivel de entrada. Obtenidos estos índices leemos los niveles que la imagen final tiene en esas localizaciones, cargando el promedio en la posición correspondiente de un vector curva[].

    for (canal in 1:3) {
      for (inputx in 1:256) {
        indices=which(antesint[,,canal]==inputx)
        curva[inputx,canal]=
          mean(despues[,,canal][indices])
      }
    }


Las curvas calculadas por ingeniería inversa son idénticas a las originales aplicadas en Photoshop como era de esperar.



Ahora podemos aplicar estas curvas desde R a cualquier otra fotografía, con el fin de obtener una apariencia similar en cuanto a tonalidades de color a la del primer procesado.




Aunque la notación es un poco ofuscada, me ha maravillado la facilidad con que la vectorización en R permite aplicar los valores de la curva (codificada en forma de tres vectores discretos) a la imagen almacenada en un array:

    for (canal in 1:3) procesado[,,canal]=
      curva[,canal][procesado[,,canal]]

La operación se hace individualmente en cada uno de los tres canales RGB (de ese bucle no he logrado librarme), pero para cada uno de ellos la línea de código anterior sustituye de una tacada todos los valores del array de imagen procesado[] por sus correspondientes transformaciones almacenadas en forma de tabla lookup en los vectores de curva[]. Es decir, los niveles de la imagen original actúan como punteros directos a sus valores destino, determinando la posición de la curva desde la que se leerá cada dato final.

Errores de redondeo al margen, el proceso de aplicar curvas RGB a una fotografía es reversible. Es decir, puede calcularse un conjunto de curvas que deshaga la transformación restaurando la imagen original, aunque solo si las curvas de ida son estrictamente monótonas (crecientes o decrecientes). En caso contrario habrá niveles para los que se hará imposible recuperar los valores de partida.

En el siguiente ejemplo vemos que la no monotonía estricta de la curva azul provoca que a más de un nivel de entrada le corresponda el mismo nivel de salida, dando lugar a una indeterminación que hace imposible el camino de vuelta.



Todo lo comentado está bien como ejercicio teórico, pero tiene una aplicación práctica limitada ya que requiere disponer de la imagen original para contrastarla con la imagen destino.

Pero y si en una aplicación la imagen inicial la pudiéramos estimar a partir de la imagen final?. Esta circunstancia se da en el virado digital de fotografías en blanco y negro, proceso consistente en aplicar una dominante de color a imágenes en blanco y negro por ejemplo para emular los tonos fotográficos clásicos (sepia, platinotipo, cianotipo,...).

A partir de una imagen virada con unas curvas RGB "secretas":



Podemos generar fácilmente una versión en blanco y negro combinando sus valores RGB y usarla como imagen de entrada. Así con el proceso visto se obtendrían las curvas RGB que modelan el virado:



Estas curvas aplicadas sobre cualquier imagen en blanco y negro de nuestra elección, la dotarán del aspecto de esa imagen que encontramos por Internet y cuyos tonos nos gustaban tanto.




Como ejercicio final he aplicado el método visto a un par de filtros Instagram, donde disponer de la imagen previa a la aplicación del filtro permite calcular las curvas RGB con facilidad.





Inspeccionando las gráficas sabemos que los dos filtros introducen componentes cálidas, especialmente 'Lord Kelvin', y adicionalmente el filtro '1977' añade dominantes magenta por defecto de verde. Ambos desplazan niveles nulos o muy bajos a valores mucho mayores lo que produce ese aspecto de "sombras lavadas".

Pese a lo precario de las muestras usadas, aplicar las curvas a la imagen original da un resultado difícilmente distinguible de la salida genuina de Instagram. Esto confirma que las curvas RGB son un buen modelo para los filtros analizados.



El código R que realiza todas las acciones puede encontrarse en tonehacker.R. Las dos implementaciones matriciales comentadas aparecen precedidas de alternativas menos eficientes basadas bucles.

2 comentarios:

  1. Estupendo artículo. Hace tiempo que vengo dando vueltas al manejo directo de los datos digitales con programación - en mi caso con Fortran. Pero el problema que nunca he sabido superar es cómo acceder al archivo original de datos digitales - en definitiva, un archivo de datos numérico. ¿Hay alguna manera de hacerlo, sobre todo al archivo raw, y si no en su caso al jpg?. Y después de manipularlo, ¿cómo se traduce a un formato que lo entienda un visor estándar de imágenes?.
    Gracias, Ignacio M.

    ResponderEliminar
    Respuestas
    1. Para acceder a los datos de cualquier archivo RAW te recomiendo el programa de línea de comandos DCRAW. En sí es un revelador RAW, pero puede hacer desde un revelado RAW completamente neutro, hasta una extracción RAW pura, lo que te permite acceder directamente a los valores numéricos que generó el sensor. Genera archivos estándar TIFF, no creo que tengas problema en encontrar una librería FORTRAN que los lea. Aquí tienes un tutorial:

      http://www.guillermoluijk.com/tutorial/dcraw/index.htm

      Salu2!

      Eliminar

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.