16OctEfecto “Fade Out” con C++ e ImageMagick

Llevo algunas semanas realizando una aplicación en C++ con las librerías de tratamiento de imagen de ImageMagick y me he decidido a hacer un tutorial sobre algo que he aprendido desarrollando dicha aplicación. En este breve tutorial pretendo explicaros cómo implementar el conocido efecto “desvanecer” (Fade Out) utilizando el lenguaje de programación C++ y trabajando sobre Linux.

Voy a saltarme la explicación sobre cómo instalar las librerías de desarrollo de ImageMagick ya que varía ligeramente de una distribución a otra y no suele ser muy complicado (teniendo en cuenta que ImageMagick suele estar presente en los repositorios más comunes y accesible desde gestores de paquetes como Synaptic). De todas formas si os surge alguna duda utilizad los comentarios.

Para simplificar la aplicación, ésta carecerá de interfaz gráfica y se ejecutará por terminal. El input del programa serán dos imágenes del mismo tamaño y un número entero positivo que indicará el número de imágenes intermedias que queremos que genere el programa como resultado.

Antes de empezar a poneros código fuente os comentaré la estructura de directorios del proyecto. Llamaremos al proyecto SFFadeOut (de Segmentation Fault Fade Out), por lo que la carpeta contenedora tendrá ese nombre. Dentro crearemos 4 directorios más:

  • /bin: para los ficheros binarios
  • /inc: para los headers (.h) a incluir en el proyecto
  • /obj: para los .o
  • /src: para los ficheros de código fuente

Ahora sí, ¡veamos el código!

En primer lugar os dejo el código de los 2 ficheros que conforman la clase Imagen. Esta clase se encargará de almacenar la información de los píxeles así como de tener algunos métodos y operadores para facilitar el trabajo. Primero os dejo el fichero Imagen.h que estará en /SFFadeOut/inc:

#ifndef IMAGEN_H
#define IMAGEN_H

class Imagen
{
 public:
  int dim[2];
  double * datos;
  Imagen();
  Imagen(int fil, int col);
  Imagen(int fil, int col, double val);
  ~Imagen();

  void  operator=(Imagen &im2);

  int fils(){return dim[0];}
  int cols(){return dim[1];}

  inline double & operator()(int i, int j)
  {
    return(datos[i*dim[1]+j]);
  };

};

Imagen & lee(char * archima, int campo);
int compara_dims(Imagen & im1, Imagen & im2);
void escribe(char * archima, Imagen & imR, Imagen & imG, Imagen & imB);

#endif

En éste fichero se encuentra la definición estática de la clase. Podéis ver los atributos de la clase, los constructores, un par de operadores y métodos propios de la clase. A continuación os dejo su correspondiente Imagen.cpp:

#include <Imagen.h>
#include <stdio.h>
#include <math.h>

Imagen::Imagen()
{
  dim[0]=0;
  dim[1]=0;
  datos=new double[1];
}

Imagen::Imagen(int fil, int col)
{
  dim[0]=fil;
  dim[1]=col;
  datos=new double[fil*col];
}

Imagen::Imagen(int fil, int col, double val)
{
  dim[0]=fil;
  dim[1]=col;
  datos=new double[fil*col];
  for(int i=0; i<fil*col;i++)
    datos[i]=val;
}

Imagen::~Imagen()
{
  delete[] datos;
}

void Imagen::operator=(Imagen  &im2)
{
  // este operador no duplica la memoria //
  int fil2=im2.fils();
  int col2=im2.cols();
  if(dim[0]!=fil2 || dim[1]!=col2)
  {
		dim[0]=fil2;
		dim[1]=col2;
		delete[] datos;
		datos=new double[fil2*col2];
	}

  int largo=dim[0]*dim[1];
  for(int i=0; i<largo; i++)
    datos[i]=im2.datos[i];

  return;
}

Si os habéis dado cuenta, hay algunas funciones definidas en el header que no están implementadas en el .cpp. El motivo es que vamos a crear otro fichero llamado inout.cpp en el que incluir las funciones de lectura y escritura de imágenes (y también la función para comparar las dimesiones de las imágenes).

#include <Imagen.h>
#include <stdio.h>

#include <Magick++.h>
#include <iostream>
using namespace std;
using namespace Magick;

Imagen  & lee(char * archima, int campo)
{
  string filename(archima);
  Image image;
  image.read(filename);
  Geometry g = image.size();
  int ancho = g.width();
  int alto= g.height();
  double c=1.0/257.0;
  Imagen * im=new Imagen(alto,ancho,(double)0.0);

  for(int i=0;i<alto;i++)
    for(int j=0;j<ancho;j++)
      switch(campo)
      {
        case 0:
          (*im)(i,j)=c*( double (image.pixelColor(j,i).redQuantum()));
          break;
        case 1:
          (*im)(i,j)=c*( double(image.pixelColor(j,i).greenQuantum()));
          break;
        case 2:
          (*im)(i,j)=c*( double(image.pixelColor(j,i).blueQuantum()));
          break;
        default:
          fprintf(stderr,"\n Error en inout.cpp al leer archivo \n");
          break;
      }

   return(*im);
};

int compara_dims(Imagen & im1, Imagen & im2)
{
  if(im1.fils()!=im2.fils() | im1.cols()!=im2.cols())
    return 0;
  return 1;
};

void escribe(char * archima, Imagen & imR, Imagen & imG, Imagen & imB)
{
    int ancho=imR.cols();
    int alto=imR.fils();
  if(compara_dims(imR,imG)!=1 | compara_dims(imG,imB)!=1 )
    {
      fprintf(stderr,"\n Error al escribir en inout.cpp: imagenes R,G,B con distintas dimensiones. \n");
      return;
    }

  Geometry geom(ancho,alto);
  Image im(geom,"white");

  for(int i=0;i<alto;i++)
    for(int j=0;j<ancho;j++)
      {
        double R=imR(i,j);
        double G=imG(i,j);
        double B=imB(i,j);
        R/=255.0;
        G/=255.0;
        B/=255.0;
        ColorRGB col(R,G,B);
              im.pixelColor(j,i,col);
      }

  im.write(archima);
  return;
};

Explicar todo el código anterior es bastante complejo por lo que pretendo que lo toméis como una API que supone una capa más de abstracción sobre la librería de ImageMagick. Ahora sí que os dejo la parte interesante, el fichero fadeout.cpp en el cual está el main del programa. Lo primero que realiza es comprobar si son correctos los parámetros de entrada y en caso contrario alertar al usuario. A continuación lee las imágenes y las almacena en objetos de la clase Imagen anteriormente detallada. Crea también un objeto para las imágenes resultantes e intera para cada píxel de cada una de las imágenes resultantes n, realizando el fundido (FadeOut).

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
#include <sstream>

#include <Imagen.h>
#include <Magick++.h>
#include <iostream>
using namespace std;
using namespace Magick;

void uso()
{
  fprintf(stderr,"Uso: prueba imagen0 imagen1 n \n");
  exit(1);
};

char * getImagename(int i){
	stringstream temp;

	temp << "imagen_resultante_" << i << ".tiff";
	string resultName = temp.str();

	char *cstr = new char [resultName.size()+1];
  strcpy (cstr, resultName.c_str());

	return cstr;

}

main(int argc,char **argv)
{

  //verifica que la cantidad de argumentos es correcta
  if(argc<4)
    uso();

  //almacena los argumentos en sus correspondientes variables
  argv++;
  char * nimagen0 =*argv++;
  char * nimagen1 =*argv++;
  int n=atoi(*argv++);

  //lee la primera imagen de entrada
  Imagen im0R=lee(nimagen0,0);
  Imagen im0G=lee(nimagen0,1);
  Imagen im0B=lee(nimagen0,2);

  //lee la segunda imagen de entrada
  Imagen im1R=lee(nimagen1,0);
  Imagen im1G=lee(nimagen1,1);
  Imagen im1B=lee(nimagen1,2);

	int cont;
 	int ancho=im0R.cols();
  int alto=im0R.fils();

	// crea un objeto Imagen para la imagen resultante
	Imagen resultR(ancho, alto);
	Imagen resultG(ancho, alto);
	Imagen resultB(ancho, alto);

	// la primera imagen resultante es la imagen de inicio
  escribe("imagen_resultante_0.tiff",im0R,im0G,im0B);

 	// bucle para generar n imágenes resultantes
	for(cont=0; cont <n; cont++){
		// doble bucle que recorre los píxeles
	  for(int i=0;i<alto;i++){
	    for(int j=0;j<ancho;j++){

				// realiza el fundido
      	resultR(i,j) = (float)(((float)(n+1)-(cont+1)) / (float)(n+1)) * im0R(i,j) + (float)((float)(cont+1) / (float)(n+1)) * im1R(i,j);
				resultG(i,j) = (float)(((float)(n+1)-(cont+1)) / (float)(n+1)) * im0G(i,j) + (float)((float)(cont+1) / (float)(n+1)) * im1G(i,j);
				resultB(i,j) = (float)(((float)(n+1)-(cont+1)) / (float)(n+1)) * im0B(i,j) + (float)((float)(cont+1) / (float)(n+1)) * im1B(i,j);
	    }
	  }
		// escribe la imagen resultante n
		escribe(getImagename(cont+1),resultR,resultG,resultB);
	}

	// escribe la imagen final
	escribe(getImagename(cont+1),im1R,im1G,im1B);

	fprintf(stdout,"\n\n Final del programa \n\n");
}

El cuerpo de los bucles es el que realiza el fundido propiamente dicho aplicando la fórmula:

Fórmula para implementar el fundido.

Fórmula para implementar el fundido.

Para facilitar un poco más la tarea de compilar el programa os dejo también el Makefile que yo utilizo:

HOME = ..

TARGETS = $(SFFADEOUT)

OBJECTS = $(OBJSFFADEOUT)

###############################
# esto es general

CPP = g++

incdir1 = $(HOME)/inc
incdir2 = /usr/include/ImageMagick
#incdir2 = /usr/include/GraphicsMagick

SRC= $(HOME)/src
OBJ = $(HOME)/obj
bindir=$(HOME)/bin

INCLUDES = -I$(incdir2) -I$(incdir1)

CPPFLAGS = -g -O3

LIBS = -DHAVE_CONFIG_H -D_REENTRANT -D_FILE_OFFSET_BITS=64 -I/usr/include/magick -I/X11 -L/usr/lib -L/usr/X11R6/lib -lMagick++ -ltiff -ljpeg -lpng -lbz2 -lz -lpthread -lm -ldl -lxml2

##########################################
# SFFADEOUT

SFFADEOUT = sffadeout
OBJSFFADEOUT = fadeout.o Imagen.o inout.o

installsffadeout: $(bindir)/$(SFFADEOUT)
$(bindir)/sffadeout: $(OBJ)/sffadeout
	-rm $(bindir)/sffadeout
	cp -p $(OBJ)/sffadeout $(bindir)/sffadeout

sffadeout: $(OBJ)/sffadeout

$(OBJ)/sffadeout: $(addprefix $(OBJ)/, $(OBJSFFADEOUT))
	$(CPP) $(INCLUDES) $(CPPFLAGS) -o $(addprefix $(OBJ)/,$(SFFADEOUT)) $(addprefix $(OBJ)/, $(OBJSFFADEOUT)) -lpthread $(LIBS) $(MesaLibs)

#########################################
# reglas generales

$(OBJ)/%.o : %.cpp
	$(CPP) -c $(INCLUDES) $(CPPFLAGS)  $&amp;lt; -o $@

.PHONY: prueba

all: directorios $(TARGETS)

clean:
	-rm $(TARGETS)
	-rm $(OBJ)/*.o

cleanall: clean

build: clean all

install: $(subst $(OBJ),$(bindir),$(TARGETS))

directorios:
	@if [ ! -d $(bindir) ]; then mkdir $(bindir); fi
	@if [ ! -d $(OBJ) ]; then mkdir $(OBJ); fi

Utilizando el comando make sólo deberéis navegar hasta la ruta adecuada y ejecutar el Makefile:

make -k sffadeout

Recordad que para que todo funcione correctamente debéis ser fieles a la estructura que os he comentado al inicio del post. Es decir, al final debéis tener una estructura de:

  • /SFFadeOut
    • /bin
    • /inc
      • Imagen.h
    • /obj
    • /src
      • fadeout.cpp
      • Imagen.cpp
      • inout.cpp
      • Makefile

Una vez compilado necesitáis un par de imágenes del mismo tamaño para provar si realmente funciona. Siendo fiel a mis anteriores artículos sobre procesamiento de imagen os animo a provar con la imagen de Lenna y una imagen negra.

Podéis ejecutar el programa con la instrucción (si las imágenes están en el directorio /SFFadeOut):

./obj/sffadeout ../lenna.png ../negro.jpg 4

El resultado con n=4 debería dar las siguientes imágenes:

Resultado con n=4.

Resultado con n=4.

Os dejo además un zip con todo el código estructurado en los directorios correctamente y con las imágenes de prueba que os he comentado. Para obtener mejores resultados probad con una n más alta y si queréis ver el fundido en acción os recomiendo crear un GIF animado.

Espero que os haya parecido interesante y no demasiado complicado. Seguro que hay fallos y mejoras así que os animo a todos a que expreséis vuestra opinión, sugerencias, problemas y demás en los comentarios.

Salu2!

Fuentes

Marcelo Bertalmío – http://www.tecn.upf.es/~mbertalmio/

One Response and Counting...

  • Bitacoras.com

    October 16th 2009

    Información Bitacoras.com…

    Valora en Bitacoras.com: No hay resumen disponible para esta anotación…