Mostrando entradas con la etiqueta Procesamiento de imágenes. Mostrar todas las entradas
Mostrando entradas con la etiqueta Procesamiento de imágenes. Mostrar todas las entradas

viernes, 18 de diciembre de 2009

Tratamiento de imágenes en Java, Parte 2: Controlador y Modelo


¡Felices fiestas!

¡Saludos nuevamente blogueros!

Ahora terminaremos con el código que quedó pendiente en el artículo anterior, que consiste en la programación del Controlador y el Modelo de nuestra aplicación.

El Controlador tendrá la obligación de manejar los eventos que ocurran en la interfaz de usuario, de modo que recibirá las acciones del usuario en el menú de opciones y las paletas de edición, los cuales ya creamos en el artículo anterior.

Por otro lado, el Modelo tendrá responsabilidades a más bajo nivel, encargándose de implementar la carga, la edición y el guardado de las imágenes. Separando las diferentes funcionalidades en dos capas de nivel distinto, como si de las capas de Negocio y Acceso a datos se tratara.

El Controlador

El Controlador de la aplicación consistirá en una única clase que implementará las interfaces ActionListener y ChangeListener, para implementar en control de eventos del menú y de las barras JSlider respectivamente.

El código es el siguiente:

Controlador.java
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.JMenuItem;
import javax.swing.JSlider;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.JPanel;
/**
* @Desc Clase que implementa la gestión de evento en la interfaz de usuario
* @author Beto González
*
*/
public class Controlador implements ActionListener, ChangeListener{
ManejadorDeImagenes manejador;
PanelSwing panel;

public Controlador(PanelSwing panel) {
this.panel = panel;
manejador = new ManejadorDeImagenes();
}
/**
* @Desc Método que capturará los eventos ocurridos en el menú principal del sistema
*/
public void actionPerformed(ActionEvent ie) {
JMenuItem i = (JMenuItem)ie.getSource();
if(i.getText() == "Abrir"){
boolean estado = manejador.cargaArchivoDeImagen(panel, panel.lienzo);
if(estado) {
panel.guardar.setEnabled(true);
panel.brillo.setEnabled(true);
panel.color.setEnabled(true);
panel.escala.setEnabled(true);
panel.esqueInf.show(panel.panelBajo, "carta1");
}
}
else if(i.getText() == "Guardar")
    manejador.guardaArchivoDeImagen(panel);
else if(i.getText() == "Salir")
System.exit(0);
else if(i.getText() == "Ajustar Brillo")
{
manejador.restableceImagen(panel.lienzo);
panel.jslBrillo.setValue(0);
panel.esqueInf.show(panel.panelBajo, "carta2");
}
else if(i.getText() == "Ajustar Colores")
{
manejador.restableceImagen(panel.lienzo);
panel.esqueInf.show(panel.panelBajo, "carta3");
}
else if(i.getText() == "Escala de Grises")
{
panel.esqueInf.show(panel.panelBajo, "carta1");
manejador.muestraEscalaDeGrises(panel.lienzo);
}
}
/**
* @Desc Método que captarará los eventos ocurridos en los componentes JSlider de la interfaz de usuario
*/
public void stateChanged(ChangeEvent e)
{
JSlider slider = (JSlider) e.getSource();
if(slider == panel.jslBrillo)
manejador.muestraBrillo(panel.lienzo, slider.getValue());
else if(slider == panel.jslRojo) {
manejador.muestraColores(panel.lienzo, slider.getValue(), panel.jslVerde.getValue(), panel.jslAzul.getValue());
}
else if(slider == panel.jslVerde)
manejador.muestraColores(panel.lienzo, panel.jslRojo.getValue(), slider.getValue(), panel.jslAzul.getValue());
else if(slider == panel.jslAzul)
manejador.muestraColores(panel.lienzo, panel.jslRojo.getValue(), panel.jslVerde.getValue(), slider.getValue());
}
}


Como podemos ver, en la implementación del método actionPerformed() lleva a cabo las acciones relacionadas con el menú de opciones, tanto para las acciones de la opción "Archivo" (Abrir, Guardar y Salir) como para la "Edición" (Ajustar Brillo, Ajustar Colores, Escala de Grises). Las opciones de edición solo se activarán una vez que un archivo de imagen haya sido cargado por el editor, para lo cual se apoyará en un objeto de la clase ManejadorDeImagenes, la cual veremos más adelante.

En la implementación del método stateChanged(), se reciben las acciones ocurridas en los controles de tipo JSlider, de forma que podremos capturar el valor del brillo o los colores que el usuario capture en la interface, y llamará a alguno de los métodos del objeto de la clase ManejadorDeImagenes para aplicar estos cambios a las imagenes cargadas.

El Modelo

Como ya explicamos antes, el modelo consistirá estará separado en dos capas, que en la programación veremos expresado como dos clases diferentes. La primera ManejadorDeImagenes, que equivaldrá a la capa de negocio en las aplicaciones de manejo de datos. Esta clase será un intermediario entre el Controlador y la Vista de la aplicación con el nivel de Acceso a datos, que en este caso consiste en la apertura, modificación y salvado de las imágenes. Estas operaciones de bajo nivel serán llevadas a cabo por la clase ProcesadorDeImagenes, la cual estará completamente abstraída del resto de la aplicación, ya que en ella ocurrirán las operaciones a nivel de pixeles sobre las imágenes, y además poseerá métodos para cargar una imagen desde el archivo y salvar las imágenes resultantes del procesamiento hacia un archivo nuevo.

El código de la primera de estas clases aparece a continuación:

ManejadorDeImagenes.java
import java.awt.*;
import javax.swing.JFileChooser;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
/**
* @Desc Clase del nivel de la capa de negocios, que implementa las operaciones que son llamadas desde el Controlador de la aplicación
* para poder cargar las imagenes, alamacenarlas y modificaralas, apoyandose en un objeto la clase de más bajo nivel, es decir ProcesadorDeImagenes
* @author Beto González
*
*/
public class ManejadorDeImagenes {
ProcesadorDeImagenes procesador;
boolean editado = false;
// Constructor de la clase
public ManejadorDeImagenes() {
procesador = new ProcesadorDeImagenes();
}
/**
* @Desc Método que lleva a cabo la carga de un archivo de imagen
* @param contenedor
* @param lienzo
* @return
*/
public boolean cargaArchivoDeImagen(JPanel contenedor, PanelDeImagen lienzo)
{
String nombreArchivo = "";
boolean estado = true;
if(editado)
{
//new MsgError("Confirmacion","Aqui debemos pedir confirmación",200,180);
int resultado = JOptionPane.showConfirmDialog((Component)null, "¿Deseas guardar los cambios en este documento?","Confirmación",JOptionPane.YES_NO_OPTION);
if(resultado==JOptionPane.YES_OPTION)
guardaArchivoDeImagen(contenedor);
}
JFileChooser selector = new JFileChooser();
selector.addChoosableFileFilter(new FiltrodeArchivo("gif","Archivos Gif"));
String lista[] = {"jpeg","jpg"};
selector.addChoosableFileFilter(new FiltrodeArchivo(lista,"Archivos JPEG"));
selector.setDialogTitle("Abrir archivo de imagen");
selector.setDialogType(JFileChooser.OPEN_DIALOG);
int resultado = selector.showOpenDialog(null);
if(resultado == JFileChooser.APPROVE_OPTION)
{
nombreArchivo = selector.getSelectedFile().getName();
String ruta = selector.getSelectedFile().getPath();
Image imagen = procesador.cargaImagen(ruta, nombreArchivo);
lienzo.estableceImagen(imagen);
lienzo.repaint();
editado = false;
}
else
estado = false;
return estado;
}
/**
* @Desc Método que lleva a cabo la operación de salvar el archivo de imagen cargado
* @param contenedor
* @return
*/
public boolean guardaArchivoDeImagen(JPanel contenedor)
{
boolean estado = true;
JFileChooser selector = new JFileChooser();
selector.addChoosableFileFilter(new FiltrodeArchivo("gif","Archivos Gif"));
String lista[] = {"jpeg","jpg"};
selector.addChoosableFileFilter(new FiltrodeArchivo(lista,"Archivos JPEG"));
selector.setDialogTitle("Guardar archivo de imagen");
selector.setDialogType(JFileChooser.SAVE_DIALOG);
int resultado = selector.showSaveDialog(contenedor);
if(resultado == JFileChooser.APPROVE_OPTION)
{
//guardar archivo en la ruta especificada
String nombreArchivo = selector.getSelectedFile().getName();
String ruta = selector.getSelectedFile().getPath();
estado = procesador.guardaImagen(ruta, nombreArchivo);
if(!estado)
JOptionPane.showMessageDialog((Component)null,"Error del sistema : "+procesador.devuelveMensajeDeError(),"Error de Imagen",JOptionPane.OK_OPTION);
editado = false;
}
else
estado = false;
return estado;
}
/**
* @Desc Método que lleva a cabo la transformación de la imagen cargada a una imagen de escala de grises y la despliega en pantalla
* @param lienzo
*/
public void muestraEscalaDeGrises(PanelDeImagen lienzo)
{
procesador.escalaDeGrises();
lienzo.estableceImagen(procesador.devuelveImagenModificada());
lienzo.repaint();
}
/**
* @Desc Método que lleva a cabo la modificación del brillo de la imagen cargada y despliega la imagen resultante en pantalla
* @param lienzo
* @param valor
*/
public void muestraBrillo(PanelDeImagen lienzo, int valor)
{
procesador.modificaBrillo(valor);
lienzo.estableceImagen(procesador.devuelveImagenModificada());
lienzo.repaint();
editado = true;
}
/**
* @Desc @Desc Método que lleva a cabo la modificación de los colores de la imagen cargada y despliega la imagen resultante en pantalla
* @param lienzo
* @param rojo
* @param verde
* @param azul
*/
public void muestraColores(PanelDeImagen lienzo, int rojo, int verde, int azul)
{
procesador.modificaColor(rojo,verde,azul);
lienzo.estableceImagen(procesador.devuelveImagenModificada());
lienzo.repaint();
editado = true;
}
/**
* @Desc Método que coloca en la pantalla la imagen original que se cargó con el método cargarArchivoDeImagen
* @param lienzo
*/
public void restableceImagen(PanelDeImagen lienzo)
{
lienzo.estableceImagen(procesador.devuelveImagenBase());
lienzo.repaint();
editado = false;
}
}


Como vemos en los métodos cargaArchivoDeImagen() y guardaArchivoDeImagen(), la funcionalidad para elegir los archivos que serán cargados y guardados se facilita con el uso de la clase JFileChooser, la cual permite llamar a una ventana de selección de archivos, con la cual resulta más agradable esta tarea.

También podemos apreciar que se utiliza un objeto de la clase FiltroDeArchivo, para establecer el filtro que tendrá el objeto selector de la clase JFileChooser. De esta forma nos aseguramos que solo puedan ser seleccionadas imágenes de tipo JPEG y GIF. El código de esta clase auxiliar aparece a continuación:

FiltroDeArchivo.java
import java.io.*;
/**
* @Desc Clase que permite crear un filtro de archivos para utilizarlo con un selector de archivos
* @author Beto González
*
*/
public class FiltrodeArchivo extends javax.swing.filechooser.FileFilter{
String descrip = "";
String listTipos[];
FiltrodeArchivo(String tipo, String descripcion)
{
listTipos = new String[1];
listTipos[0] = tipo;
descrip = descripcion;
}
FiltrodeArchivo(String listTipos[], String descripcion)
{
this.listTipos = listTipos;
descrip = descripcion;
}
public boolean accept(File fileobj){
boolean extIgual = false;
String extension = "";
int ind=0;
if(fileobj.getPath().lastIndexOf(".") > 0)
{
extension = fileobj.getPath().substring(fileobj.getPath().lastIndexOf(".")+1).toLowerCase();
}
if(extension!="")
{
while(ind<listTipos.length && !extIgual)
{
extIgual = extension.equals(listTipos[ind].toLowerCase());
ind++;
}
return extIgual;
}
else
return fileobj.isDirectory();
}
public String getDescription()
{
if (descrip != "")
return descrip.concat(concatTipos());
else
return "";
}
private String concatTipos()
{
StringBuffer tipos = new StringBuffer(" (*."+listTipos[0]);
for(int i=1;i<listTipos.length;i++)
tipos.append(",*."+listTipos[i]);
tipos.append(")");
return tipos.toString();
}
}


Tanto estos dos métodos como el resto serán llamados desde el Controlador de la aplicación para cada acción que el usuario vaya realizando en la interfaz gráfica. De modo que el método muestraEscalaDeGrises() será llamado cuando el usuario seleccione la opcion "Escala de Grises" en el menú principal. Mientas que el método muestraBrillo() es llamado cuando el usuario realice un cambio en el brillo de la imagen, utilizando la barra JSlider jslBrillo para ajustar el brillo. Algo similar ocurrirá con el método muestraColores(), que se llamará cuando alguna de las barras JSlider de colores (jslRojo, jslVerde, jslAzul) sea utilizada. Estos métodos además llevan a cabo la actualización en la pantalla de las imágenes una vez que son modificadas. En el caso de que el usuario aumente el brillo o modifique los colores, la acción ocurrirá tan rápido que no podrá darse cuenta de todo el trabajo que la PC llevó a cabo para cambiar la apariencia de la imagen capturada.

El método restableceImagen() se utiliza para volver a colocar en la pantalla la imagen original que fue cargada, lo cual es simplemente una forma de asegurarnos que las tres acciones de edición (aumentar brillo, modificar colores y convertir a escala de grises) siempre ocurran sobre la misma imagen que fue cargada.

El código de la clase ProcesadorDeImagenes a la que nos hemos referido aparece a continuación:

ProcesadorDeImagenes.java
import java.awt.Canvas;
import java.awt.image.*;
import java.awt.Image;
import java.awt.Toolkit;
import java.io.IOException;
import java.io.File;
import java.awt.Graphics;
import java.awt.image.BufferedImage;
import javax.imageio.ImageIO;
/**
* @Desc Clase que implementa el procesamiento básico de imágenes digitales
* @author Beto González
*
*/
public class ProcesadorDeImagenes extends Canvas {
Image imagenBase;
Image imagenModificada;
String mensajeDeError = "";
String tipoDeImagen = "";
/**
* @Desc Método que permite agregar una imagen al procesador que es recibida como parámetro
* @param imagen
*/
public void estableceImagen(Image imagen)
{
imagenBase = imagen;
imagenModificada = null;
tipoDeImagen = (String)imagenBase.getProperty("type", this);
}
/**
* @Desc Método que permite agregar una imagen al procesador directamente desde un archivo de imagen
* @param imagen
*/
public Image cargaImagen(String ruta, String nombreDeArchivo)
{
imagenBase = Toolkit.getDefaultToolkit().getImage(ruta);
imagenModificada = null;
String[] partes = null;
partes = nombreDeArchivo.split("\\.");
int tope = partes.length;
if(tope > 1)
tipoDeImagen = partes[tope - 1];
return imagenBase;
}
/**
* @Desc Método que modifica el brillo de la imagen base contenida a partir del valor de intesidad recibido
* @param intensidad
* @return Verdadero si todo salió bien, falso en caso de error
*/
public boolean modificaBrillo(int intensidad)
{
boolean estado = true;
int p, rojo, verde, azul;
int a = imagenBase.getWidth(this);  //Ancho
    int h = imagenBase.getHeight(this); //Alto
    int totalDePixeles = a * h;   
int pixeles[] = new int[totalDePixeles];   //Arreglo de pixeles
PixelGrabber pg = new PixelGrabber(imagenBase,0,0,a,h,pixeles,0,a);
try
{
pg.grabPixels();
for(int i = 0; i < totalDePixeles; i++)
{
p = pixeles[i]; //Valor de un pixel
rojo = (0xff & (p>>16)) + intensidad;  //Desplaza el entero p 16 bits a la derecha y aplica la operacion AND a los primeros 8 bits
verde = (0xff & (p>>8)) + intensidad;  //Desplaza el entero p 8 bits a la derecha  y aplica la operacion AND a los siguientes 8 bits
azul = (0xff & p) + intensidad;        //Aplica la operacion AND a los siguientes 8 bits
if(rojo>255) rojo=255;
if(verde>255) verde=255;
if(azul>255) azul=255;
if(rojo<0) rojo=0;
if(verde<0) verde=0;
if(azul<0) azul=0;
pixeles[i]=(0xff000000|rojo<<16|verde<<8|azul);
}
imagenModificada  = createImage(new MemoryImageSource(a,h,pixeles,0,a));
}catch(InterruptedException e)
{
//JOptionPane.showMessageDialog((Component)null,"Error del sistema : "+e.getMessage(),"Error de Imagen",JOptionPane.OK_OPTION);
estado = false;
this.mensajeDeError = e.getMessage();
}
return estado;
}
/**
* @Desc Método que convierte la imagen base contenida en una imagen a escala de grises
* @return Verdadero si todo salió bien, falso en caso de error
*/
public boolean escalaDeGrises()
{
    boolean estado = true;
int p, promedio, rojo, verde, azul;
int a = imagenBase.getWidth(this);  //Ancho
    int h = imagenBase.getHeight(this); //Alto
    int totalDePixeles = a * h;
int pixeles[] = new int[totalDePixeles];   //Arreglo de pixeles
PixelGrabber pg = new PixelGrabber(imagenBase,0,0,a,h,pixeles,0,a);
try
{
pg.grabPixels();
for(int i = 0; i < totalDePixeles; i++)
{
p = pixeles[i]; //Valor de un pixel
rojo = (0xff & (p>>16));  //Desplaza el entero p 16 bits a la derecha y aplica la operacion AND a los primeros 8 bits
verde = (0xff & (p>>8));  //Desplaza el entero p 8 bits a la derecha  y aplica la operacion AND a los siguientes 8 bits
azul = (0xff & p) ;        //Aplica la operacion AND a los siguientes 8 bits
promedio = (int) ((rojo+verde+azul)/3);
pixeles[i]=(0xff000000|promedio<<16|promedio<<8|promedio);
}
imagenModificada  = createImage(new MemoryImageSource(a,h,pixeles,0,a));
}catch(InterruptedException e)
{
//JOptionPane.showMessageDialog((Component)null,"Error del sistema : "+e.getMessage(),"Error de Imagen",JOptionPane.OK_OPTION);
estado = false;
this.mensajeDeError = e.getMessage();
}
return estado;
}
/**
* @Desc Método que modifica los colores de la imagen base contenida a partir de los valores de intensidad de los colores rojo, verde y amarillo recibidos
* @param iRojo
* @param iVerde
* @param iAzul
* @return Verdadero si todo salió bien, falso en caso de error
*/
public boolean modificaColor(int iRojo, int iVerde, int iAzul)
{
boolean estado = true;
int p, rojo=0, verde=0, azul=0;
int a = imagenBase.getWidth(this);  //Ancho
    int h = imagenBase.getHeight(this); //Alto
    int totalDePixeles = a * h;
int pixeles[] = new int[totalDePixeles];   //Arreglo de pixeles
PixelGrabber pg = new PixelGrabber(imagenBase,0,0,a,h,pixeles,0,a);
try
{
pg.grabPixels();
for(int i = 0;i<(a*h);i++)
{
p = pixeles[i]; //Valor de un pixel
rojo = (0xff & (p>>16)) + iRojo;
verde = (0xff & (p>>8)) + iVerde;
azul = (0xff & p) + iAzul;
if(rojo>255) rojo=255;
if(verde>255) verde=255;
if(azul>255) azul=255;
if(rojo<0) rojo=0;
if(verde<0) verde=0;
if(azul<0) azul=0;
pixeles[i]=(0xff000000|rojo<<16|verde<<8|azul);
}
imagenModificada = createImage(new MemoryImageSource(a,h,pixeles,0,a));
}catch(InterruptedException e)
{
estado = false;
this.mensajeDeError = e.getMessage();
}
return estado;
}
/**
* @Desc Método que almacena la imagen contenida en una archivo de imagen, de acuerdo a la información del archivo que recibe como parámetro
* @param ruta
* @param nombreDeArchivo
* @param tipoDeImagen
* @return
*/
public boolean guardaImagen(String ruta, String nombreDeArchivo, String tipoDeImagen)
{
boolean estado = true;
BufferedImage imagen = creaBufferedImage((imagenModificada != null) ? imagenModificada : imagenBase);
try {
ImageIO.write(imagen, tipoDeImagen, new File(ruta));
} catch (IOException e) {
estado = false;
this.mensajeDeError = e.getMessage();
}
return estado;
}
/**
* @Desc Versión por defecto del método guardaImagen
* @param ruta
* @param nombreDeArchivo
* @return
*/
public boolean guardaImagen(String ruta, String nombreDeArchivo)
{
String[] partes = null;
partes = nombreDeArchivo.split("\\.");
int tope = partes.length;
if(tope > 1)
tipoDeImagen = partes[tope - 1];
return guardaImagen(ruta, nombreDeArchivo, tipoDeImagen);
}
/**
* @Desc Método que devuelve la imagen modificada por el procesador en un objeto de la clase Image
* @return
*/
public Image devuelveImagenModificada()
{
return imagenModificada;
}
/**
* @Desc Método que devuelve la imagen base dentro de un objeto de la clase Image
* @return
*/
public Image devuelveImagenBase()
{
return imagenBase;
}
/**
* @Desc Método que retora el último mensaje de error producido por los métodos de la clase
* @return
*/
public String devuelveMensajeDeError()
{
return mensajeDeError;
}
/**
* @Desc Versión por defecto del método creaBufferedImage
* @param imageIn
* @return El objeto BufferedImage
*/
public BufferedImage creaBufferedImage(Image imagenDeEntrada) {
return creaBufferedImage(imagenDeEntrada, BufferedImage.TYPE_INT_RGB);
}
/**
* @Desc Método para convertir un objeto Image a un objeto BufferedImage
* @param imageIn
* @param imageType
* @return El objeto BufferedImage
*/
public BufferedImage creaBufferedImage(Image imagenDeEntrada, int imageType) {
    BufferedImage bufferedImageDeSalida = new BufferedImage(imagenDeEntrada.getWidth(this), imagenDeEntrada.getHeight(this), imageType);
    Graphics g = bufferedImageDeSalida.getGraphics();
    g.drawImage(imagenDeEntrada, 0, 0, null);
    return bufferedImageDeSalida;
}
}


Como ya mencionamos esta clase trabajo al nivel más bajo de la aplicación directamente con los pixeles de las imagenes. Para lograr esto utiliza dos objetos de las clase Image (imagenBase e imagenModificada), para almacenar tanto las imágenes que son leídas tanto como la imagen que resulta del procesamiento (respectivamente). Los métodos estableceImagen() y cargarImagen() le permiten tener una imagen sobre la cual llevar a cabo el procesamiento que ocurre en los otros métodos.

Los métodos modificaBrillo(), escalaDeGrises() y modificaColores() llevan a cabo los cambios en los pixeles de la imagen, aplicando una serie de operaciones a cada uno de sus valores de intensidad de color, almacenando los valores de los pixeles dentro de un arreglo de número enteros y recorriéndolo dentro de un ciclo for. De esta forma, cada uno de los pixeles es tratado como un número entero, para el cual los valores de la intensidad del color estarán contenidos en sus primeros tres grupos de 8 bits. Es decir, los primero 8 bits menos significativos del entero representarán la intensidad del azul, los siguientes 8 bits los del verde y los 8 bits que le siguen los del rojo. Es por esto que en el código vemos que se lleva a cabo un desplazamiento a nivel de bits del valor de un pixel, para junto con la operación AND obtener los 8 bits del valor de cada uno de los tres colores del pixel:

p = pixeles[i]; //Valor de un pixel
rojo = (0xff & (p>>16)) + intensidad; //Desplaza el entero p 16 bits a la derecha y aplica la operacion AND a los primeros 8 bits
verde = (0xff & (p>>8)) + intensidad; //Desplaza el entero p 8 bits a la derecha y aplica la operacion AND a los siguientes 8 bits
azul = (0xff & p) + intensidad; //Aplica la operacion AND a los siguientes 8 bits


El código anterior es el que encontramos en el método modificaBrillo(), y lo que vemos que se hace es justamente sumarle una misma cantidad a cada uno de los valores de color del pixel para de esta forma obtener un pixel más iluminado, para después almacenar este pixel obtenido dentro del arreglo de pixeles.

En los otros dos métodos se sigue una lógica parecida, con la única diferencia de que para modificaColores, se aumentará la intensidad de los colores de los pixeles con un valor distinto para cada color, mientras que en escalaDeGrises se promediará el valor de los tres colores de cada pixel y se almacenará el mismo resultado dentro de los tres valores de color del pixel, para de esta forma obtener el equivalente en escala de grises del color original.

Implementación final

Ahora bien, visto el código del Controlador y el Modelo de la aplicación, solo nos queda juntarlo todo con la Vista para echar a correr el programa final. Para hacer eso, tendremos que modificar el código de la clase EditorImg que realizamos en el artículo anterior, agregando las líneas que se remarcan en el siguiente código:

EditorImg.java
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
/**
* Descripción: Esta clase implementa un editor básico de imagenes .jpg y .gif,
* utilizando componentes Swing y clases de la libreria image.
* @author Alberto González Espinoza
* @version 1.0
* @category Multimedia
*/
public class EditorImg extends JFrame{
static final long serialVersionUID=10000;
PanelSwing panel;
Controlador controlador;
public static void main(String[] args) {
// TODO Auto-generated method stub
EditorImg editor = new EditorImg();
editor.setBounds(120, 120, 800, 600);
editor.setVisible(true);
editor.setDefaultCloseOperation(DISPOSE_ON_CLOSE);
editor.addWindowListener(new WindowAdapter()
{
public void WindowCloser(WindowEvent e)
{
System.exit(0);
}
});
}

/**
* @Desc Constructor de la clase
*/
EditorImg() {
super("Editor básico de imagenes");
Container contentPane = getContentPane();
panel = new PanelSwing(this);
controlador = new Controlador(panel);
panel.abrir.addActionListener(controlador);
panel.guardar.addActionListener(controlador);
panel.salir.addActionListener(controlador);
panel.brillo.addActionListener(controlador);
panel.escala.addActionListener(controlador);
panel.color.addActionListener(controlador);
panel.jslBrillo.addChangeListener(controlador);
panel.jslRojo.addChangeListener(controlador);
panel.jslVerde.addChangeListener(controlador);
panel.jslAzul.addChangeListener(controlador);

contentPane.add(panel);
}
}


Y como vemos, lo que se hace es simplemente agregar el Controlador a la aplicación, instanciando un objeto de la clase Controlador, y asignando la gestión de eventos del menú principal y de los componentes JSlider a este objeto.

Una vez listo el paso anterior, podemos probar correr el programa y deberemos ver la misma pantalla que en el artículo anterior, con la diferencia de que ahora ya podremos cargar, editar y modificar las imágenes.

Por ejemplo, así es como se visualiza el cargado de las imágenes:

Aquí vemos una imagen ya cargada:

Si modificamos los colores:

O el brillo:

Y aquí como se ve en escala de grises:

Al final solo tenemos que salvar la imagen para concluir con el proceso de edición.


Bien, el código está totalmente a su disposición y es obviamente propenso a mejoras :D, ya que las funciones de procesamiento que aquí implementamos son algunas de las que se consideran básicas, pero con estos principios es posible incursionar en conceptos más avanzados de procesamiento de imágenes, en particular le veo mucho potencial a la clase ProcesadorDeImagenes para seguirla desarrollando e incluir un amplio abanico de operaciones de procesamiento de imágenes digitales, y que gracias a la abstracción de su diseño, pueda ser llevada a otras aplicaciones de forma transparente.

Y por hoy es todo, posiblemente este sea el último post de este año 2009, ya que la próxima semana ya estaré de vacaciones y tendré mi agenda un poco ocupada, pero espero que para el año que entra tendré mayor tiempo y decisión XD para seguir explorando las posibilidades de las herramientas de software libre.

Saludos desde la cabaña y hasta la próxima.

martes, 15 de septiembre de 2009

Tratamiento de imágenes en Java, Parte 1: Introducción y Vista


¡Vamos a ponerle color a Java!


Hola de nuevo blogueros, después de una larga sequía de varios meses ahora traigo el código fuente para la creación de un sencillo editor de imágenes en Java, el cual servirá para dos propósitos, como son el ilustrar el potencial del manejo de archivos e imágenes con Java y conocer algunos componentes adicionales de las interfaces de usuario de Swing.

Sobre la edición de imágenes

Las imágenes digitales consisten en un conjunto de valores numéricos ordenados en forma de arreglos o matrices, cada uno de estos valores representa el contenido del color de un pixel.


Zoom a una imagen digital con el que se aprecian sus pixeles

Los pixeles son los puntos de color que se despliegan en la pantalla al reproducir una imagen en nuestra PC. Al observar estos puntos en conjunto podemos apreciar la imagen en su totalidad, la cual no es otra cosa que una representación digital de una imagen del mundo real, pero que nuestro cerebro interpreta como si se tratara de la imagen original.

Podemos distinguir tres tipos de imágenes por las características de sus pixeles, estás son las las imágenes en escala de grises, las imágenes en blanco y negro y las imágenes a color.


Imagen a escala de grises

A nivel de programación encontramos que lo que las diferencia es la forma en la que varían los valores de los pixeles y cómo son interpretados por nuestra PC.

En las imágenes a color tenemos un conjunto de ternas de valores para representar el color de cada pixel que será visualizado. Así, en una imagen a color un pixel está formado en realidad por tres valores que representan la intensidad de los colores rojo, verde y azul. Al combinar estos tres valores se puede obtener un color diferente a los tres originales, y haciéndolos variar en su intensidad podemos obtener todo un gran conjunto de colores para dibujar. La forma en la que estos colores son combinados obedece al modelo de color RGB, que es el que se utiliza más frecuentemente con las imágenes digitales.


Mezcla activa de colores

En cambio, con las imágenes en escala de grises cada pixel representa un valor equivalente a una graduación de gris. Tal y como dice el artículo enlazado, "las imágenes representadas de este tipo están compuestas de sombras de grises, que van desde el negro más profundo variando gradualmente en intensidad de grises hasta llegar al blanco."

Una de las características interesantes del procesamiento de imagen es que el posible convertir una imagen a color a una imagen equivalente en escala de grises, aplicando un sencillo algoritmo de procesamiento a sus pixeles. Esto se logra calculando el promedio de las ternas de valores de cada uno de los pixeles de la imagen, y reemplazando los tres valores de cada pixel por el promedio obtenido, de forma que los tres colores tengan un mismo valor de intensidad.

Los pixeles de las imágenes en blanco y negro, son similares a las de escala de grises, con la diferencia de que éstos solo pueden contener uno de dos valores de color posibles, el 255 (blanco) y el 0 (negro).

Otra característica importante, es que si modificamos los valores de intensidad de los pixeles de una imagen, podemos hacer que varíen tanto su brillo como su color. Por ejemplo, para aumentar el brillo de una imagen, podemos sumarle un valor escalar a cada una de las ternas de colores de sus pixeles. Mientras que si deseáramos hacer que variara su color, podemos cambiar la intensidad de una de las tres ternas de color de cada uno de sus pixeles, por ejemplo para hacer más azul una imagen podemos sumarle un escalar a todos los valores de azul de sus pixeles.

Teniendo clara esta diferencia entre los tipos de imagen y lo que es un pixel, ahora podemos pasar a la programación de nuestra aplicación, la cual consistirá en una ventana con un menú de opciones, que permitirá cargar archivos de imágenes de nuestros sistema y gracias a una serie de componentes para interfaces gráficas, darles un pequeño tratamiento a estas imágenes. El programa además tendrá la capacidad para salvar las imágenes una vez que hayan sido modificadas por el usuario.


Manos a la obra

Nuevamente nos basaremos en el esquema MVC (Modelo, Vista, Controlador) para la construcción del software, comenzando con la parte de la vista para nuestra ventana y nuestros paneles de opciones.

Componentes de la interfaz de usuario:

JSlider. Utilizaremos este componente para crear una pequeña paleta de intensidad de colores y otra de intensidad del brillo, de forma que podamos modificar con los tonos de rojo, verde y azul de las imágenes, así como el brillo que estas proyectan.
JPanel. Utilizaremos este componente primero, para crear todos los paneles en los que se cargarán los componentes, y además crearemos una clase llamada PanelDeImagen que extenderá a la clase JPanel, para que funcione como un lienzo sobre el cual dibujaremos las imágenes que carguemos y editemos en la aplicación.
JScrollPane. Ya que el tamaño de las imágenes puede varias respecto del tamaño de la ventana de nuestra aplicación, utilizaremos un panel de tipo JScrollPane, el cual tiene la característica de hacer aparecer barras de desplazamiento en sus bordes cuando su contenido excede a su tamaño en pantalla, de forma que podemos cargar imágenes más grandes que nuestra ventana sin problemas.
JMenuBar, JMenu y JMenuItem. Estos componentes nos permitirán crear una barra de menú en el panel superior de nuestra aplicación, el cual contendrá las opciones relacionadas con los archivos, como son abrir un archivo de imagen, guardar la imagen editada, salir de la aplicación. También permitirán activar los paneles que contendrán las opciones para la edición de nuestras imágenes.
JFileChooser. Este componente adicional, nos auxiliará para hacer más sencilla la selección y el guardado de archivos de imágenes, ya que con los objetos de esta clase podemos llamar una ventana de selección de archivos, que nos permitirá elegir los archivos que editaremos en nuestra aplicación de forma visual.

La Vista

El código para implementar la vista de la aplicación aparecerá a continuación, separando cada una de las clases en un apartado.

Código para la implementación de la ventana de la aplicación:

EditorImg.java
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
/**
* Descripción: Esta clase implementa un editor básico de imágenes .jpg y .gif,
* utilizando componentes Swing y una serie de clases para el manejo y procesamiento de imagenes digitales.
* @author Beto González
* @version 1.0
* @category Multimedia
*/
public class EditorImg extends JFrame{
static final long serialVersionUID=10000;
PanelSwing panel;
public static void main(String[] args) {
// TODO Auto-generated method stub
EditorImg editor = new EditorImg();
editor.setBounds(120, 120, 800, 600);
editor.setVisible(true);
editor.setDefaultCloseOperation(DISPOSE_ON_CLOSE);
editor.addWindowListener(new WindowAdapter()
{
public void WindowCloser(WindowEvent e)
{
System.exit(0);
}
});
}

/**
* @Desc Constructor de la clase
*/
EditorImg() {
super("Editor básico de imagenes");
Container contentPane = getContentPane();
panel = new PanelSwing(this);
contentPane.add(panel);
}
}


Código para implementar el lienzo sobre el que se dibujará la imagen:

PanelDeImagen.java
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Image;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
/**
* @Desc Clase que extiende a la clase JPanel que se utiliza para crear un panel que permita visualizar imágenes en su interior
* @author Beto González
*
*/
public class PanelDeImagen extends JPanel{
static final long serialVersionUID=10000;
Image img;
Dimension tamaño;
JScrollPane base;
/**
* @Desc Constructor de la clase
*/
PanelDeImagen()
{
setBackground(Color.white);
}
/**
* @Desc Método a través del cual la clase recibe el objeto de la imagen que será visualizada en su interior
* @param i
*/
public void estableceImagen(Image i)
{
img = i;
}
/**
* @Desc Método a través del cual la clase obtiene la referencia hacia el panel en el cual se encuentra contenido.
* @param i
*/
public void estableceBase(JScrollPane contenedor)
{
base = contenedor;
}
/**
* @Desc Método extendido que es llamado cada ves que un objeto de esta clase llama al método repaint(). A este le agregamos
* una funcionalidad adicional que le permite redimencionar el panel que contiene la imagen de acuerdo a las dimensiones de
* ésta
*/
public void paintComponent(Graphics g)
{
super.paintComponent(g);
if (img != null)
{
if(base != null)
{
setSize(new Dimension(base.getWidth()-10,base.getHeight()-10));
setPreferredSize(new Dimension(base.getWidth()-10,base.getHeight()-10));
}
tamaño = new Dimension(getWidth(),getHeight());
int x = tamaño.width - img.getWidth(this);
while (x < 0)
{
tamaño.setSize(tamaño.width+1, tamaño.height);
x = tamaño.width - img.getWidth(this);
}
if(x > 0)
  x = (int) x/2;
int y = tamaño.height - img.getHeight(this);
while (y < 0)
{
tamaño.setSize(tamaño.width, tamaño.height+1);
y = tamaño.height - img.getHeight(this);
}
if(y > 0)
  y = (int) y/2;

if(!getSize().equals(tamaño))
{
setSize(tamaño);
setPreferredSize(tamaño);
}
g.drawImage(img, x, y, this);
}
}
}


La funcionalidad de los objetos de la clase PanelDeImagen consistirá en que recibirán una imagen por medio del método estableceImagen() y la dibujarán en su panel al llamar al método repaint() que heredaron de la clase JPanel. Ya que en esta implementación el panel de las imagenes estará contenida dentro de un objeto JScrollPane, es que además contamos con el método estableceBase(), con el cual podemos guardar una referencia a este panel. Además, debido a que las dimensiones de las imagenes cargada por la aplicación pueden, es que extendimos al método paintComponent(), el cuál además de encargarse de dibujar la imagen dentro del panel, modificará las dimensiones de este y del panel JScrollPane que lo contenga, de forma que se adapten al tamaño de las imágenes y estas puedan ser visualizadas en su totalidad en pantalla.

La siguiente clase implementa la totalidad de los componentes de la interfaz gráfica de la aplicación:

PanelSwing.java
import java.awt.*;
import javax.swing.*;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
/**
* @Desc Clase utilizada para crear los componentes de la interfaz gráfica de la aplicación
* @author Beto González
*
*/
public class PanelSwing extends JPanel {
static final long serialVersionUID = 10000;
String nombreArchivo, ruta;
JMenuBar barraMenu;
JMenu menuArchivo, menuEdicion;
JMenuItem abrir, guardar, salir, brillo, color, escala;
JScrollPane panelDespl;
JPanel panelBajo, panelBrillo, panelColor, panelVacio;
int altura = 80;
Image imagen;
Image imgAux;
EditorImg editor;
PanelDeImagen lienzo;
JSlider jslBrillo, jslRojo, jslVerde, jslAzul;
JLabel lblRojo, lblVerde, lblAzul;
CardLayout esqueInf;
/**
* @Desc Constructor de la clase
* @param editor
*/
PanelSwing(EditorImg editor)
{
this.editor = editor;
this.setLayout(new BorderLayout());
barraMenu = new JMenuBar();
FlowLayout f = new FlowLayout();
f.setAlignment(FlowLayout.LEFT);
barraMenu.setLayout(f);
menuArchivo = new JMenu("Archivo");
menuEdicion = new JMenu("Edición");
abrir = menuArchivo.add("Abrir");
guardar = menuArchivo.add("Guardar");
guardar.setEnabled(false);
menuArchivo.addSeparator();
salir = menuArchivo.add("Salir");
brillo = menuEdicion.add("Ajustar Brillo");
color = menuEdicion.add("Ajustar Colores");
escala = menuEdicion.add("Escala de Grises");
brillo.setEnabled(false);
color.setEnabled(false);
escala.setEnabled(false);
barraMenu.add(menuArchivo);
barraMenu.add(menuEdicion);
this.add("North",barraMenu); //Agregamos la barra de menu
creapanelCentral(); //Creamos el panel en el que se mostrara la imagen seleccionada
creapanelBajo(); //Creamos el panel en el que se mostraran los controles para manipular la imagen
}
/**
* @Desc Método que crea el contenido del panel central de la ventana
*/
private void creapanelCentral()
{
lienzo = new PanelDeImagen();
panelDespl = new JScrollPane(lienzo);
lienzo.estableceBase(panelDespl);
add("Center",panelDespl);
}
/**
* @Desc Método que crea el contenido del panel inferior de la ventana
*/
private void creapanelBajo()
{
panelBajo = new JPanel();
esqueInf = new CardLayout();
panelBajo.setLayout(esqueInf);
panelBajo.setPreferredSize(new Dimension(this.getWidth(),altura));
jslBrillo = new JSlider(SwingConstants.HORIZONTAL,0,100,0);
jslBrillo.setPaintTicks(true);
jslBrillo.setPaintLabels(true);
jslBrillo.setMajorTickSpacing(10);
jslBrillo.setMinorTickSpacing(5);
panelColor = new JPanel();
panelVacio = new JPanel();
panelBrillo = new JPanel(new BorderLayout());
panelBrillo.add("Center", new JLabel("Puedes ajustar el brillo de la imagen",JLabel.CENTER));
panelBrillo.add("South",jslBrillo);
panelBajo.add("carta1", panelVacio);
panelBajo.add("carta2", panelBrillo);
creaPaletas();
esqueInf.show(panelBajo, "carta1");
this.add("South",panelBajo);
}
/**
* @Desc Método que crea el contenido del panel inferior de la ventana
*/
private void creaPaletas()
{
GridBagLayout gridbag = new GridBagLayout();
GridBagConstraints constrain = new GridBagConstraints();
panelColor.setLayout(gridbag);
lblRojo = new JLabel("Rojo");
lblVerde = new JLabel("Verde");
lblAzul = new JLabel("Azul");
constrain.gridx = 0; constrain.gridy = 0;
constrain.gridheight = 1; constrain.gridwidth = 2;
gridbag.setConstraints(lblRojo, constrain);
panelColor.add(lblRojo);
constrain.gridx = 2; constrain.gridy = 0;
gridbag.setConstraints(lblVerde, constrain);
panelColor.add(lblVerde);
constrain.gridx = 4; constrain.gridy = 0;
gridbag.setConstraints(lblAzul, constrain);
panelColor.add(lblAzul);
jslRojo = new JSlider(SwingConstants.HORIZONTAL,0,50,0);
jslVerde = new JSlider(SwingConstants.HORIZONTAL,0,50,0);
jslAzul = new JSlider(SwingConstants.HORIZONTAL,0,50,0);
constrain.gridx = 0; constrain.gridy = 1;
constrain.gridheight = 1; constrain.gridwidth = 2;
gridbag.setConstraints(jslRojo, constrain);
panelColor.add(jslRojo);
constrain.gridx = 2; constrain.gridy = 1;
gridbag.setConstraints(jslVerde, constrain);
panelColor.add(jslVerde);
constrain.gridx = 4; constrain.gridy = 1;
gridbag.setConstraints(jslAzul, constrain);
panelColor.add(jslAzul);
panelBajo.add("carta3", panelColor);
}
}


Hasta este punto hemos completado la parte de la Vista de la aplicación, y nos quedan pendientes el Controlador y el Modelo. Sin embargo, para poder apreciar realmente la funcionalidad de las clases anteriores es necesario probarlas, echando a correr el programa, de forma que podamos visualizarlas y obtener un resultado como el siguiente:



En esta captura, podemos apreciar los tres paneles principales de la aplicación, el menú de opciones en la parte superior, el panel central que contiene los paneles de los componentes de las clases JScrollPane y PanelDeImagen, y finalmente el panel inferior, en el cual se desplegarán los componentes visuales para la edición de las imagenes, una vez que el Controlador y el Modelo estén trabajando.

En el siguiente artículo concluiremos con esta implementación.

Hasta pronto.