Continuamos con la serie sobre tratamiento de imágenes para mejorar las nuestras fotografías realizadas durante las vacaciones. En números anteriores establecimos unas bases para poder ejecutar cualquier filtro en serie poniendo un ejemplo sencillo para convertir a gris y en el segundo número realizamos un algoritmo que orientaba la foto intentando detectar el tono azul del cielo.
En este número vamos a desarrollar un filtro llamado balance de blanco o equilibrio del color. Este filtro se utiliza para conseguir una reproducción correcta del color sin que existan colores predominantes en la foto. Igualmente puede ser bastante útil para dar claridad a fotos oscuras u oscurecer las fotos claras encontrando una proporción de luz adecuada.
Para entender este filtro será necesario tratar algunos conceptos previos como el histograma y como podemos desplazar los tonos aplicando operaciones de suma o resta y así hacer la imagen más clara o oscura o aplicar multiplicadores para saturar más o menos el color.
Histograma
El histograma es un concepto muy sencillo de entender si se muestra mediante ejemplos. En números pasados hablamos que un canal (rojo, verde o azul) puede contener diferentes tonalidades desde lo más oscuro a lo más claro. En el caso de la codificación de 24 bits (8 bits por canal) existen 256 valores posibles [0-255].
El histograma consiste en la representar gráficamente cuantas veces se repite un mismo tono en una imagen. Para ello se cuenta cuantas veces aparece el mismo tono en la imagen. En el caso de 24bits dispondremos de 256 contadores por canal. Por ejemplo veamos este caso:
La anterior imagen es una foto con un balance de colores incorrectos. El color rojo predomina sobre la foto. Al representar el histograma se puede ver como la concentración de muestras del mismo tono se encuentra en tonos más claros en la grafica del canal rojo que en los canales verde y azul. Es decir, la foto tiene más puntos con valores altos en el canal rojo que en los verdes y azules.
También nos podemos encontrar con casos en los que la foto es muy oscura. Veamos el histograma de un caso así.
En el anterior histograma podemos ver como la mayoría de los puntos se encuentran en las partes más bajas de las gráficas (al principio). Es decir, tiene pocos valores con tonos altos en los tres canales por lo que es una foto muy oscura.
Modificar los tonos sobre todos los puntos aplicando fórmulas matemáticas modificará el tono general de la imagen o de los canales que deseemos. Una de estas operaciones es sumar o restar un valor fijo.
Al sumar un valor fijo sobre todos los tonos lo que realizamos es desplazar el grupo de puntos a tonos más claros. Esto producirá que nuestra imagen se aclare. Por lo contrario, si restamos un valor fijo sobre todos los puntos lo que provocamos es oscurecer la imagen.
En todo caso, tanto en la suma como en la resta, el contraste del color es el mismo. Para conseguir un mayor contraste entre los diferentes colores que pueda tener la imagen lo que necesitamos es conseguir la mayor distancia de tonos de la mayoría de puntos dentro del histograma pero siempre llevando una proporcionalidad.
Para conseguir este efecto deberíamos multiplicar los valores de los tonos por un factor X. Cuanto mayor sea este factor los puntos del histograma más se separarán y como consecuencia mayor contraste encontraremos en la foto. Sin embargo, hay que tener cuidado con el cálculo de este factor ya que una saturación excesiva puede hacer que nuestras fotos parezcan sacadas de unos dibujos animados :).
Equilibrio del color
El algoritmo de equilibrio de color aplica un factor multiplicador independiente sobre cada canal para de esta manera conseguir que tengamos una serie de puntos considerable en ambos extremos del histograma. De esta manera se consigue un contraste interesante haciendo que las fotos tomen más viveza. Este filtro trata los canales por separado consiguiendo que no haya ningún canal que sea predominante sobre los otros.
Para realizar este algoritmo almacenaremos tres histogramas, uno por cada canal y para obtenerlo contaremos cuantos puntos hay en cada tono [0-255] para cada canal [rgb]. Una vez tenemos el histograma de los tres canales, vamos a desechar el 1% de puntos en cada extremo. De esta manera evitamos que algún punto que pueda ser ruido o un pequeño objeto no apreciable no deje expandir los tonos por todo el espectro.
Una vez conocemos los rangos mínimo y máximo, eliminando el 1% de puntos (por cada lado), restaremos a todos los puntos un valor fijo que será el tono mínimo y lo multiplicaremos por un factor para expandir o saturar y cubrir el espectro 256/(max-min). Es decir, aplicaremos la siguiente fórmula tono=(tono-min)*(256/(max-min))
Finalmente hay que tener cuidado con los puntos que no hemos tenido en cuenta para el cálculo (1% por cada lado) ya que se saldrán del rango. Para ello delimitaremos un máximo=255 y un mínimo=0. Este sería el algoritmo:
package org.gbd.fotos;
import java.awt.Color;
import java.awt.GraphicsEnvironment;
import java.awt.Transparency;
import java.awt.image.BufferedImage;
public class FiltroBalanceBlanco implements Filtro{
public int porcentajeRuido=1;
public FiltroBalanceBlanco(){
}
public FiltroBalanceBlanco(int porcentajeRuido){
this.porcentajeRuido=porcentajeRuido;
}
public BufferedImage filtrar(BufferedImage bi) {
BufferedImage biDestino =
GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice().getDefaultConfiguration()
.createCompatibleImage(bi.getWidth(), bi.getHeight(), Transparency.OPAQUE);
int histograma[][]=new int[3][256];
int total=0;
//Calcula los histogramas de cada canal
for (int x=0;x < bi.getWidth();x++){
for (int y=0;y < bi.getHeight();y++){
Color c1=new Color(bi.getRGB(x, y));
int rgb[]={
c1.getRed(),
c1.getGreen(),
c1.getBlue()
};
for (int canal=0;canal < 3;canal++){
histograma[canal][rgb[canal]]++;
}
total++;
}
}
//Obtiene el tono mínimo y máximo eliminando el N% de puntos (por posibles ruidos en la foto).
int min[]={-1,-1,-1};
int acumMin[]={0, 0, 0};
int max[]={-1,-1,-1};
int acumMax[]={0, 0, 0};
for (int canal=0;canal < 3;canal++){
for (int tono=0;tono < 256;tono++){
if (min[canal]==-1){
acumMin[canal]+=histograma[canal][tono];
if (acumMin[canal] > total*porcentajeRuido/100){
min[canal]=tono;
}
}
if (max[canal]==-1){
acumMax[canal]+=histograma[canal][255-tono];
if (acumMax[canal] > total*porcentajeRuido/100){
max[canal]=255-tono;
}
}
}
}
//Genera el nuevo color estirando el 96% de los tonos en toda la imagen.
for (int x=0;x < bi.getWidth();x++){
for (int y=0;y < bi.getHeight();y++){
Color c1=new Color(bi.getRGB(x, y));
int rgb[]={
c1.getRed(),
c1.getGreen(),
c1.getBlue()
};
for (int canal=0;canal < 3;canal++){
double factor=256.0/(max[canal]-min[canal]);
rgb[canal]=(int)((rgb[canal]-min[canal])*factor);
rgb[canal]=Math.min(255, Math.max(0, rgb[canal]));
}
biDestino.setRGB(x, y, new Color(rgb[0],rgb[1],rgb[2]).getRGB());
}
}
return biDestino;
}
}
Si ejecutáis el algoritmo vereis resultados como estos.
En Genbeta Dev | Tratamiento de imágenes I: Escala de grises En Genbeta Dev | Tratamiento de imágenes II: Orientar imágenes según tono azul