Con la llegada del fin de verano es posible que tengamos fotografías de las vacaciones, celebraciones o cualquier tipo de evento que se realiza para disfrutar del buen tiempo. A continuación iniciaremos una serie de números a modo de talleres prácticos para hablar del tratamiento de imágenes y fotografías.
A lo largo de esta serie iremos poniendo ejemplos y prácticas en Java que podréis ejecutar vosotros mismos. No hay un motivo definitivo por la elección de este lenguaje. Los mismos algoritmos se podrían realizar utilizando cualquier otro lenguaje. En este primer número vamos a tratar algunos fundamentos básicos sobre las imágenes.
Formatos
Nos podemos encontrar multitud de formatos para el almacenamiento de ficheros. Los más antiguos y rudimentarios son los mapas de bits como por ejemplo el formato .bmp. En este formato se almacena la información sin comprimir por lo que suelen ser muy grandes respecto a otros formatos. Como ventajas son más fáciles de tratar pues no precisan de un algoritmo de descompresión pudiendo acceder a los puntos directamente.
Existen otros formatos como gif, jpg, png o tiff que comprimen este mapa de bits y estaban orientados a ciertas necesidades. El formato que vamos a utilizar para realizar estas prácticas será el formato jpg ya que es bastante aconsejable para fotografías. Pero siempre hay que tener el cuenta que el tratamiento de la imagen se debe realizar sobre un mapa de bits descomprimido. Por ello los pasos que se suelen realizar son descomprimir el fichero, tratar el fichero y almacenar nuevamente comprimido.
Colores
Una imagen, a groso modo, se trata de una matriz de puntos. Siempre que deseemos acceder a un punto concreto deberemos utilizar una coordenada (x, y). Un punto contiene una serie de bits para representar el color. Esta cantidad de bits puede variar según la calidad de la imagen.
En la actualidad se suelen utilizar 24bits para fotografías en el que cada color primario (rojo, verde y azul) está representado por 8 bits. Puede darse el caso de que veamos opciones de imágenes a 32bits, pero en ese caso se añade un nuevo canal llamado alfa que representa la transparencia del punto.
Combinando estos colores primarios obtenemos todas las posibles tonalidades de colores. Cada color primario se representa con 1 byte así que el rango de valores posibles es entre 0 y 255. Cuando queremos ausencia de color pondremos el color 0 y si queremos que se pinte ese color en su tonalidad más fuerte pondremos 255.
También podemos obtener colores derivados a partir de combinaciones de tonalidades de los colores primarios. Por ejemplo, si aplicamos tonalidad máxima en rojo y en verde se obtiene el amarillo. El blanco se obtiene con la tonalidad máxima en los tres colores primarios y el negro con su ausencia.
Primera práctica – Filtro gris.
De momento en este primer post solo deseamos establecer unas bases para poder crear filtros más complejos en futuros números. Así que en este número escribiremos el código que abre todos los ficheros de un directorio, los procese con un filtro sencillo y los almacene en otro directorio.
El filtro sencillo que vamos a desarrollar es poner la imagen con tonalidades grises. Anteriormente comentamos que se podían crear derivados de colores utilizando los colores primarios. Para el caso de los grises se obtienen poniendo la misma tonalidad de colores primarios. De esa forma podemos obtener 256 tonalidades de grises, desde el color negro al color blanco.
Para empezar nuestra práctica vamos a crear un interface para el tratamiento de imágenes de manera que podremos crear diferentes filtros y almacenarlos para ejecutarlos de manera consecutiva. Aquí crearemos nuestro primer fichero:
package org.gbd.fotos;
import java.awt.image.BufferedImage;
public interface Filtro {
public abstract BufferedImage filtrar(BufferedImage bi);
}
El método filtrar será el encargado de ejecutar el filtro correspondiente. Este método recibe una imagen (BufferedImage) y devuelve una copia con el filtro aplicado. Hemos elegido BufferedImage por permitir de manera más sencilla a cada punto mediante coordenadas x,y.
A continuación vamos a crear un filtro que convierta una imagen en color a escala de grises. Para obtener la escala de grises debemos obtener de cada punto una tonalidad idéntica para asociarla a los tres colores primarios. En nuestro caso para obtener valor único vamos a realizar la media de las tres tonalidades de la imagen origen. Aquí tenéis 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 FiltroGris implements Filtro{
public BufferedImage filtrar(BufferedImage bi){
//Crea una copia del mismo tamaño que la imagen
BufferedImage biDestino =
GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice().getDefaultConfiguration()
.createCompatibleImage(bi.getWidth(), bi.getHeight(), Transparency.OPAQUE);
//Recorre las imagenes y obtiene el color de la imagen original y la almacena en el destino
for (int x=0;x < bi.getWidth();x++){
for (int y=0;y < bi.getHeight();y++){
//Obtiene el color
Color c1=new Color(bi.getRGB(x, y));
//Calcula la media de tonalidades
int med=(c1.getRed()+c1.getGreen()+c1.getBlue())/3;
//Almacena el color en la imagen destino
biDestino.setRGB(x, y, new Color(med,med,med).getRGB());
}
}
return biDestino;
}
}
En el anterior algoritmo crea una imagen de las mismas dimensiones que la imagen original. A continuación recorre las imágenes y obtiene la media de tonos de cada punto almacenando esta media en todos los tonos de la segunda imagen.
Aplicar el filtro en serie
En ocasiones puede que queramos aplicar un filtro en diferentes imágenes ya que quizá la cámara estaba sacando las fotos de una manera determinada y deseamos corregir todas las fotos que ha realizado. Ejecutar la aplicación foto a foto puede llegar a ser un incordio para el usuario. Por todo ello vamos a desarrollar un algoritmo que lea los ficheros de una carpeta y los almacene en otra aplicando los filtros que deseemos.
import java.awt.GraphicsEnvironment;
import java.awt.Image;
import java.awt.Toolkit;
import java.awt.Transparency;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import javax.imageio.ImageIO;
import javax.swing.ImageIcon;
import org.gbd.fotos.Filtro;
import org.gbd.fotos.FiltroGris;
public class Main {
public static void main(String[] args) {
if (args.length==0){
System.out.println("No ha introducido ningún parámetro. Sintaxis:");
System.out.println("java Main < carpeta > ");
System.exit(-1);
}
String rutaOrigen=args[0];
File dirOrigen=new File(rutaOrigen);
//Comprueba que sea un directorio
if (!dirOrigen.isDirectory()){
System.out.println("La ruta enviada no es un directorio");
System.exit(-1);
}
//Crea el directorio destino
File dirDestino=new File(rutaOrigen+"/modificado");
if (!dirDestino.exists()){
dirDestino.mkdir();
}
//Recorre los ficheros
File ficheros[]=dirOrigen.listFiles();
for (File fic:ficheros){
//Si no es directorio
if (!fic.isDirectory()){
try{
//Carga la imagen
Image ima=Toolkit.getDefaultToolkit().getImage(fic.getAbsolutePath());
ima = new ImageIcon(ima).getImage(); //Para poder utilizar el getWidth y getHeight y asegurarse la carga de la imagen
//Creación BufferedImage con la imagen
BufferedImage bi =
GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice().getDefaultConfiguration()
.createCompatibleImage(ima.getWidth(null), ima.getHeight(null), Transparency.OPAQUE);
bi.getGraphics().drawImage(ima, 0, 0, null);
// Almacenar los filtros que deseamos aplicar
ArrayList < Filtro > alFiltros=new ArrayList< Filtro >();
alFiltros.add(new FiltroGris());
// Ejecutar los filtros
for (Filtro f:alFiltros){
bi=f.filtrar(bi);
}
// Guardar la imagen final en la carpeta destino
try{
ImageIO.write(bi, "jpg", new File(dirDestino + "/" + fic.getName()));
System.out.println("Foto finalizada " + fic.getName());
}catch(IOException e){
System.out.println("Error al procesar foto");
e.printStackTrace();
}
}catch(Exception e){
System.out.println(fic.getAbsolutePath() + " no tratado");
}
}
}
}
}
Inicialmente realiza la lectura de los ficheros que contiene el directorio indicado como parámetro y genera la carpeta destino si no existe. A continuación almacenamos todos los filtros que queremos aplicar en un ArrayList y los ejecutamos. Finalmente almacena el resultado en la carpeta destino con el mismo nombre.
Conclusión
En este primer número hemos visto algunos fundamentos básicos sobre los colores y hemos creado un filtro sencillo que convierte la imagen a escala de grises. También hemos creado la base para aplicar una serie de filtros sobre todos los ficheros de una carpeta. En futuros números crearemos filtros más complejos para tratar nuestras fotografías.