¿Cuántas palabras caben en una imagen?




Sofía no suele ser una persona muy expresiva, y difícilmente uno penetra su coraza para poder siquiera atisbar qué es lo que está pensando en cada momento. Ya sé que llegar al trabajo por la mañana temprano no infunde la mayor de las alegrías, pero Sofía parecía tener su propio rito de autoresignación consistente en sentarse tratando de no llamar demasiado la atención, colocarse sus cascos, abrir sus herramientas de trabajo que organizaba siempre de la misma manera en la pantalla de la derecha (era de las pocas programadoras que tenía dos monitores) y en la de la izquierda comenzaba a leer algún artículo o alguna web de noticias hasta que, cuando lo consideraba oportuno, volvía la cabeza al otro monitor y comenzaba a escribir código. El rito debe ser poderoso porque, probablemente es la programadora más brillante de la empresa.
El caso es que aquella precisa mañana, su rito cambió. En vez de colocarse los cascos y abrir una web, abrió un ejemplar del "Código de Da Vinci" de Dan Brown y se puso a leer. A mí, la verdad, es que ese libro no me entusiasmaba especialmente y puestos a elegir prefiero al Holmes de Conan Doyle o al Hércules Poirot de Agatha Christie. El caso es que no es el tipo de libro que esperaba ver leyendo a Sofía, así que me atreví a preguntar.

¿Qué tal el libro? ¿Te gusta? -pregunté.
No mucho, pero habla de criptografía y tenía curiosidad. Me ha llamado la atención la parte en la que el protagonista interpreta supuestos mensajes secretos de Da Vinci en su obra de La Última Cena. ¿No es curioso pensar que el pintor podría estar comunicándose con alguien que vive cientos de años después de su muerte?
Visto así sí que impresiona -dije- pero supongo que es lo que buscan todos los pintores al pintar un cuadro ¿no?
Si, pero imagina poder esconder información oculta en la obra y que pase desapercibida para el público general, pero no para el receptor del mensaje.
Pues sería muy interesante, pero sería más fácil enviarle una carta con algún código cifrado.
Si, pero si alguien la intercepta tiene ya una información valiosa: El emisor quiere enviar un mensaje secreto al receptor. Eso de por sí es ya una información útil para el enemigo. Se trata, pues, de enviar el mensaje sin levantar sospechas. Hoy en día lo tenemos bastante fácil con las imágenes digitales, ya no hay que pintar un cuadro. Hay una técnica detrás de este tipo de ocultación de la información llamada Esteganografía.

Te refieres a que se pueden almacenar datos en las cabeceras de los archivos de imágenes ¿no? en fin, eso no parece muy seguro -traté de parecer inteligente haciendo mis propias deducciones.
Para nada Alberto, te equivocas. Hablo de guardar información en la propia información de la imagen, en los píxeles que la componen.
Sofía me observó unos segundo como esperando a que cayera en la cuenta de cómo podía hacerse tal cosa, como si fuera evidente, pero por enésima vez vio defraudadas sus expectativas sobre mis capacidades.
Verás -prosiguió- una imagen digital de ordenador está compuesta por una cabecera, como tú bien dices, en la que se almacena información específica del formato en la que se almacena. La cabecera de una imagen JPG es diferente a la cabecera de una imagen PNG o GIF, por ejemplo. Tras la cabecera están los datos de la imagen propiamente dichos. Una imagen está compuesta por muchos píxeles, y cada pixel tiene un color. Pues bien, la clave está en cómo se almacena el color de cada pixel. El formato más común (aunque no el único) es codificar el color usando tres bytes (24 bits) en el que cada byte representa el nivel de cada componente de color (a saber: rojo, verde y azul).
Por ejemplo, un componente alto de rojo y verde, y un componente bajo de azul daría lugar al color amarillo. Podríamos codificarlo como Rojo:255, Verde:255, Azul:0. Recordemos que un byte (8 bits) pueden representar un rango de valores que va del 0 al 255 (sin signo).
En un archivo gráfico, generalmente los pixeles se almacenan como secuencias de tres bytes en formato RRVVAA (o RRGGBB de red, Green y Blue).


Vale, esto más o menos lo entiendo, pero no llego a comprender cómo puedo almacenar algo en un pixel. Para el color se usan los 24 bits y no queda ninguno sin usar donde almacenar nada sin que se vea modificado el color en sí.

Touché querido Watson, de eso precisamente se trata: de cambiar el color del pixel, pero tan ligeramente que el ojo humano sea incapaz de notar la diferencia. De hecho Alberto, ¿eres capaz de notar alguna diferencia de color entre los dos siguientes rectángulos de color rojo? (Sofía me mostró los cuadros en su pantalla).





Por más que me esforcé no fui capaz de ver diferencia alguna, así que me aventuré a decir: Son iguales.

Para nada -rió sofía. El primero tienen una componente roja de 255 (en binario 11111111) y la segunda de 254 (en binario 11111110). Fíjate que sólo hay una diferencia de un bit entre uno y otro color.
Entonces, si cambio un sólo bit en una componente de color, ¿no notaré la diferencia? -de nuevo traté de parecerme más a Holmes que a Watson.
No, si cambio el primer bit (01111111), por ejemplo, mira lo que pasa:





Ahora, la componente roja pasa a ser 127, que dista bastante del 255 original. Por lo tanto, si queremos que el color se mantenga visualmente igual, sólo podemos tocar el bit de más a la derecha, también llamado bit menos significativo.

Ya entiendo -dije. Se trata entonces de codificar la información que queremos ocultar alterando los últimos bits de cada componente de color ¿es así?
Elemental querido Alberto. Así es. Te pongo un ejemplo. Imagina que quieres esconder un mensaje que empieza por la letra A en una imagen. La letra A tiene el código ASCII 65 (10000001 en binario). Pues bien, necesitamos modificar el último bit de la componente roja del primer bit para que acabe en 1, el de la componente verde para que acabe en 0, y así sucesivamente. Como tenemos 8 bits, cada carácter necesitará tres píxeles (3 pixeles * 3 componentes de color = 9 componentes de color) Es decir, nos sobra un bit que, por mantener las cosas simples, no vamos a modificar.
El proceso sería pues el siguiente (Sofía garabateó sobre un papel):


Sofía rodeó de color rojo los bits que era necesario cambiar y me hizo notar como la componente azul del último pixel no la estábamos usando en el ejemplo, pero que podía usarse sin problema aunque complicaba un poco la programación. También me explicó que debían usarse formatos de archivos con compresión sin pérdidas, ya que formatos como el JPG pueden alterar las componentes de color originales.

Por cierto -continuó Sofía- puede almacenarse información "secreta" en otros tipos de archivos digitales, como vídeo o sonido. De hecho, en el caso del sonido el proceso es muy similar.

Lo cierto es que todo lo que me estaba contando Sofía empezaba a sonarme a buen argumento para una novela, pero como se me da mejor la programación que la literatura, decidí escribir un programa para poner todo en práctica en vez de tratar de hacerle sombra a Dan Brown. Tecleé dos programas, una llamado Encode.java que almacena un texto en una imagen y otro llamado Decode.java que realiza el proceso contrario. Reproduzco aquí los dos programas.

Encode.java

  1. import java.awt.Color;
  2. import java.awt.image.BufferedImage;
  3. import java.io.File;
  4. import javax.imageio.ImageIO;
  5.  
  6. public class Encode {
  7.     public static void main(String[] args) {
  8.         if (args.length<2) {
  9.             System.out.println("Formato: Encode imagen \"texto\"");
  10.             System.exit(0);
  11.         }
  12.         String img_in=args[0];
  13.         String message=args[1];
  14.        
  15.         message=message+"#";
  16.         // cargar imagen
  17.         BufferedImage img = null;
  18.         try {
  19.             img = ImageIO.read(new File(img_in));
  20.         } catch (Exception e) {
  21.             e.printStackTrace();
  22.         }
  23.        
  24.         if (message.length()>((img.getWidth()*img.getHeight())/3)) {
  25.             System.out.println("El mensaje es demasiado grande para la imagen.");
  26.             System.exit(0);
  27.         }
  28.         // almacenar mensaje
  29.         // cada tres pixels un caracter
  30.         int pix_index=0;
  31.         byte[] chars=null;
  32.         try {
  33.             chars=message.getBytes("ASCII");
  34.         } catch (Exception e) {
  35.             e.printStackTrace();
  36.         }
  37.         for (int i=0; i<chars.length; i++) {
  38.             for (int j=0; j<3; j++) {
  39.                 int ycoord=(int)(pix_index/img.getWidth());
  40.                 int xcoord=pix_index-(ycoord*img.getWidth());
  41.                 int c = img.getRGB(xcoord,ycoord);
  42.                 int red = (int)((c & 0x00ff0000) >> 16);
  43.                 int green = (int)((c & 0x0000ff00) >> 8);
  44.                 int blue = (int)(c & 0x000000ff);
  45.                 if (isBitSet(chars[i], (j*3)+0)) {
  46.                     red |= 1 << 0;
  47.                 } else {
  48.                     red &= ~(1 << 0);
  49.                 }
  50.                 if (isBitSet(chars[i], (j*3)+1)) {
  51.                     green |= 1 << 0;
  52.                 } else {
  53.                     green &= ~(1 << 0);
  54.                 }
  55.                 if (j<2) {  // el ultimo byte no lo usamos
  56.                     if (isBitSet(chars[i], (j*3)+2)) {
  57.                         blue |= 1 << 0;
  58.                     } else {
  59.                         blue &= ~(1 << 0);
  60.                     }          
  61.                 }
  62.                 Color color=new Color(red, green, blue);
  63.                 img.setRGB(xcoord, ycoord, color.getRGB());
  64.                 pix_index++;
  65.             }
  66.         }
  67.         // guardar imagen
  68.         try {
  69.             File outputfile = new File("encodedimage.png");
  70.             ImageIO.write(img, "png", outputfile);
  71.         } catch(Exception e) {
  72.             e.printStackTrace();
  73.         }
  74.     }
  75.    
  76.     private static Boolean isBitSet(byte b, int bit) {
  77.         return (b & (1 << bit)) != 0;
  78.     }
  79. }

Decode.java

  1. import java.awt.Color;
  2. import java.awt.image.BufferedImage;
  3. import java.io.File;
  4. import javax.imageio.ImageIO;
  5.  
  6. public class Decode {
  7.     public static void main(String[] args) {
  8.         if (args.length<1) {
  9.             System.out.println("Formato: Decode imagen");
  10.             System.exit(0);
  11.         }
  12.         String img_in=args[0];
  13.  
  14.         // cargar imagen
  15.         BufferedImage img = null;
  16.         try {
  17.             img = ImageIO.read(new File(img_in));
  18.         } catch (Exception e) {
  19.             e.printStackTrace();
  20.         }
  21.        
  22.         // obtener mensaje
  23.         for (int i=0; i<(img.getWidth()*img.getHeight())-3; i=i+3){
  24.             char car=' ';
  25.             // primer pixel
  26.             int ycoord=(int)(i/img.getWidth());
  27.             int xcoord=i-(ycoord*img.getWidth());
  28.             int c = img.getRGB(xcoord,ycoord);
  29.             int red = (int)((c & 0x00ff0000) >> 16);
  30.             int green = (int)((c & 0x0000ff00) >> 8);
  31.             int blue = (int)(c & 0x000000ff);
  32.             if (isBitSet((byte)red,0)) {
  33.                 car |= 1 << 0;
  34.             } else {
  35.                 car &= ~(1 << 0);
  36.             }
  37.             if (isBitSet((byte)green,0)) {
  38.                 car |= 1 << 1;
  39.             } else {
  40.                 car &= ~(1 << 1);
  41.             }
  42.             if (isBitSet((byte)blue,0)) {
  43.                 car |= 1 << 2;
  44.             } else {
  45.                 car &= ~(1 << 2);
  46.             }
  47.             // segundo pixel
  48.             ycoord=(int)((i+1)/img.getWidth());
  49.             xcoord=i+1-(ycoord*img.getWidth());
  50.             c = img.getRGB(xcoord,ycoord);
  51.             red = (int)((c & 0x00ff0000) >> 16);
  52.             green = (int)((c & 0x0000ff00) >> 8);
  53.             blue = (int)(c & 0x000000ff);
  54.             if (isBitSet((byte)red,0)) {
  55.                 car |= 1 << 3;
  56.             } else {
  57.                 car &= ~(1 << 3);
  58.             }
  59.             if (isBitSet((byte)green,0)) {
  60.                 car |= 1 << 4;
  61.             } else {
  62.                 car &= ~(1 << 4);
  63.             }
  64.             if (isBitSet((byte)blue,0)) {
  65.                 car |= 1 << 5;
  66.             } else {
  67.                 car &= ~(1 << 5);
  68.             }
  69.    
  70.             // tercer pixel
  71.             ycoord=(int)((i+2)/img.getWidth());
  72.             xcoord=i+2-(ycoord*img.getWidth());
  73.             c = img.getRGB(xcoord,ycoord);
  74.             red = (int)((c & 0x00ff0000) >> 16);
  75.             green = (int)((c & 0x0000ff00) >> 8);
  76.             blue = (int)(c & 0x000000ff);
  77.             if (isBitSet((byte)red,0)) {
  78.                 car |= 1 << 6;
  79.             } else {
  80.                 car &= ~(1 << 6);
  81.             }
  82.             if (isBitSet((byte)green,0)) {
  83.                 car |= 1 << 7;
  84.             } else {
  85.                 car &= ~(1 << 7);
  86.             }
  87.    
  88.             if (car=='#') {
  89.                 break;
  90.             }
  91.             System.out.print(car);
  92.         }
  93.     }
  94.    
  95.     private static Boolean isBitSet(byte b, int bit) {
  96.         return (b & (1 << bit)) != 0;
  97.     }
  98. }
  99.  

Bueno, el código no es especialmente elegante, pero parece que funcionará -dijo Sofía machacando de nuevo mi autoestima.
Para probar el programa usamos la siguiente imagen de una fotografía de un viaje a Dinamarca que había realizado Sofía.


Tras compilar los programas ejecutamos el comando: java Encode dinamarca.jpg "mensaje oculto"
Obtuvimos la siguiente imagen:


Como ves -dijo Sofía- son indistinguibles la una de la otra.
Tras ejecutar: java Decode encodedimage.png obtuvimos de nuevo el texto "mensaje oculto".

Bien, se me ocurren algunas utilidades para este programa -dije.
Si estas pensando en pasar mensajes ocultos dentro de imágenes a otra empresa para ganarte un sobresueldo, tendrás que refinar un poco el método, ya que, aunque codificada en la imagen, el texto no está encriptado y si alguien sospecha que hay un mensaje oculto será fácil obtenerlo. Aunque si lo que quieres es que Gaznápiro no los intercepte, creo que ni en mil años sería capaz.

3 comentarios:

  1. Acabo de descubrir este blog a traves del enlace de la wikipedia de la pagina de sdl.
    Me encata lo que escribes. Sigue asi!! Este blog me va a ser muy util.


    Por cierto, la foto parece Noruega. El punto mas alto de Dinamarca es una colina de 170 metros.

    ResponderEliminar
  2. Me alegra que te guste el blog Joan :-)
    Por cierto, creo que tienes razón en lo de que no es Dinamarca, un compañero me comentó que podía ser Gudvangen, en Noruega. A ver si alguien lo confirma.

    ResponderEliminar
  3. Falto la respuesta del título...
    Respuesta:
    Depende del número de caracteres de las palabras, aunque se podría saber con exactitud cuantos caracteres caben en una imagen, según el método explicado.
    Número de caracteres = (AnchoImagen * AltoImagen)/3

    El ancho y el alto de la imagen debe estar en pixeles.

    ResponderEliminar