Convertir de Yuv a Rgb en Android


Si habéis trabajado con la cámara de Android e intentáis hacer efectos en tiempo real, seguro que os suena el formato "Yuv". Y si no os ayudáis de alguna librería como OpenCV, os tocará picar algo de código.

La lógica básica para usar la cámara que propone Android es simple: abrís la cámara, la conectáis a un "Surface View" y a través de "Callbacks" y métodos de la cámara, podéis hacer fotos, algunos ajustes y filtros (bastante limitados antes de "Ice Cream Sandwich"), etc.

Para los que esto no es suficiente, la cámara permite obtener previsualizaciones en tiempo real, que las podéis usar para pintar directamente en el "Surface View" de vuestra aplicación (o guardarlas en el dispositivo, calcular parámetros, etc).  Un ejemplo de como hacerlo, lo describo en ésta entrada.

El problema, es que probablemente querréis trabajar en el espacio RGB (de 8 bits por canal) y lo que te ofrece Android por defecto es el espacio de color "Yuv" (en concreto NV21 semiplanar).  Android os ofrece dos opciones:
La otra opción es hacer vosotros mismos la conversión de Yuv a RGB. Yo me enfrenté a esta opción, y basándome en la información disponible en Wikipedia:YUV, un mail de David Manpearl, y varios otros posts (que probablemente 
ofrezcan mejores soluciones), llegué al siguiente código:

/**
  * Convierte de un stream en formato Yuv (NV21) al formato RGB (32 bits).
  * 
  * @param out array de componentes RGB
  * @param in array de componentes Yuv en formato "semiplanar"
  * @param width 
  * @param height
  * @param rotated indica si el array de entrada está rotado
  * @throws NullPointerException
  * @throws IllegalArgumentException
  */
 public void yuv2rgb(int[] out, byte[] in, int width, int height, boolean rotated) 
   throws NullPointerException, IllegalArgumentException 
 { 
  final int size = width * height; 

  // Comprobar los parámetros de entrada
  if(out == null) throw new NullPointerException("buffer 'out' == null"); 
  if(out.length < size) throw new IllegalArgumentException("buffer 'out' length < " + size); 
  if(in == null) throw new NullPointerException("buffer 'in' == null"); 
  if(in.length < (size * 3 / 2)) throw new IllegalArgumentException("buffer 'in' length != " + in.length + " < " + (size * 3/ 2)); 

  // YCrCb
  int Y, Cr = 0, Cb = 0;
  
  // Variables auxiliares
  int Rn = 0, Gn = 0, Bn = 0;
  for(int j = 0, pixPtr = 0, cOff0 = size - width; j < height; j++) { 
   if((j & 0x1) == 0)
    cOff0 += width;
   int pixPos = height - 1 - j; 
   for(int i = 0, cOff = cOff0; i < width; i++, cOff++, pixPtr++, pixPos += height) { 
    
    // Obtener Y
    Y = 0xff & in[pixPtr]; // 0xff es por el signo
    
    // Obtener Cr y Cb
    if((pixPtr & 0x1) == 0) { 
     Cr = in[cOff]; 
     if(Cr < 0) Cr += 127; else Cr -= 128;
     Cb = in[cOff + 1]; 
     if(Cb < 0) Cb += 127; else Cb -= 128; 

     Bn = Cb + (Cb >> 1) + (Cb >> 2) + (Cb >> 6);
     Gn = - (Cb >> 2) + (Cb >> 4) + (Cb >> 5) - (Cr >> 1) + (Cr >> 3) + (Cr >> 4) + (Cr >> 5);
     Rn = Cr + (Cr >> 2) + (Cr >> 3) + (Cr >> 5);
    } 

    // Se calculan los componentes RGB
    int R = Y + Rn; 
    if(R < 0) R = 0; else if(R > 255) R = 255; 
    int B = Y + Bn; 
    if(B < 0) B = 0; else if(B > 255) B = 255; 
    int G = Y + Gn; 
    if(G < 0) G = 0; else if(G > 255) G = 255; 

    // En este punto del código se podría aplicar algún filtro 
    // a partir de los componentes por separado de la imagen.
    // Por ejemplo se podrían intercambiar 2 componentes o eliminar uno.
    int rgb = 0xff000000 | (R << 16) | (G << 8) | B;

    // Según la opción se rellena el buffer de salida aplicando la transformación o no
    if(rotated)
     out[pixPos] = rgb; 
    else
     out[pixPtr] = rgb;
   } 
  } 
 }

Admite optimizaciones ,probablemente tenga algún error (lo iré actualizando) y admito comentarios. Pero es Java puro, 
incluye un flag para realizar la rotación in situ de la imagen (si no, habría que rotar el Canvas al pintarlo), y es lo suficientemente 
rápido (50ms en un Samsung Galaxy GT i-9000) para jugar un rato.

La rotación me pareció interesante, porque se pierden bastantes milisegundos al rotar la imagen posteriormente. 
En este caso hay que llamar a este método con los parámetros "width" y "height" intercambiados. Realmente es un "hack" 
para el caso concreto que se explica en esta entrada y que se puede obviar para casos más generales poniendo el flag "rotated" 
a "false".

En la entrada Previsualización de la cámara de Android en tiempo real, describo como podéis utilizar éste código.

Otras opciones que he probado son: