Modificar los pixeles en un BitmapImage utilizando BitmapEncoder | C# | WinRT

En un post anterior vimos como acceder directamente a los pixeles de una imagen, esto en resumen es así:

var localFolder = Package.Current.InstalledLocation;  
var folder = await localFolder.GetFolderAsync("Assets");  
var imgfile = await folder.GetFileAsync("conejo.bmp");

var filestream = await imgfile.OpenReadAsync();  
var decoder = await BitmapDecoder.CreateAsync(filestream);  
var pxDataProvider = await decoder.GetPixelDataAsync();  
byte[] pxData = pxDataProvider.DetachPixelData();  

Para una explicación detallada revisar :
Acceder a los pixeles de una imagen en Apps de WinRT

Esto nos permite acceder a la información de pixeles en forma de un array de bytes.

Una vez modificada la información del array de bytes básicamente tenemos un nuevo Bitmap, así que hay que asignar este byte[] a un objeto tipo BitmapImage.

Para esta tarea debemos hacer uso de la clase BitmapEncoder, es de esperarse revisando el post anterior.

Para crear una instancia de BitmapEncoder se requiere indicar cual es el tipo de imagen (Guid) que se va a encodificar, usualmente BMP que es el formato más plano y es la operación más rápida ya que no tiene compresión, así que ese parámetro lo establecemos como BitmapEncoder.BmpEncoderId, el segundo parámetro es un IRandomAccessStream, lo peor que podemos hacer es crear un archivo en disco, SIEMPRE sugiero usar un stream en memoria y una vez finalizada la manipulación si volcarlo a un archivo de disco si es necesario.

Para crear un stream de acceso aleatorio en memoria se debe hacer lo siguiente:

var bmpstream = new InMemoryRandomAccessStream();

De tal forma que se puede crear el encoder de la siguiente forma:

var encoder = await BitmapEncoder.CreateAsync(BitmapEncoder.BmpEncoderId, bmpstream);

En ese stream se escribiran los bytes modificados, haciendo uso del método BitmapEncoder.SetPixelData, es importante notar que este tiene varios parámetros

  • El formato en el que vienen los pixeles, usualmente BGRA (blue, green, red, alpha)
  • La forma en que se interpretan los valores alpha, por ahora manejaremos "Ignore"
  • Ancho y alto del bitmap
  • Puntos por pulgada del bitmap, el estándar es 96
  • Los bytes / pixeles a escribir en el stream

Una vez se halla copiado la info en el stream se debe llamar el método FlushAsync para asegurarse que no han quedado datos en el buffer intermedio.

var bmpstream = new InMemoryRandomAccessStream();  
var encoder = await BitmapEncoder.CreateAsync(BitmapEncoder.BmpEncoderId, bmpstream);  
encoder.SetPixelData(BitmapPixelFormat.Bgra8, BitmapAlphaMode.Ignore, decoder.PixelWidth, decoder.PixelHeight, 96, 96, pxData);  
await encoder.FlushAsync();  

Ya esta todo listo, ahora hay que crear un Objeto BitmapData al cual se le debe copiar la información del stream, esto ultimo se hace por medio del método SetSource, para finalmente asignar el objetoi BitmapData como Source de un objeto Image y así hacerlo visible.

var bd = new BitmapImage();
await bd.SetSourceAsync(bmpstream);
imgBitmap.Source = bd;

Ta ta raaaaan! eso es todo...

NO, No lo es. La vida no es fácil, pero al menos es divertida, el resultado del código anterior es un error en tiempo de ejecución.

HRESULT: 0x88982F50

The component cannot be found. (Exception from HRESULT: 0x88982F50

Esta clarísimo, cualquiera con 10 años de experiencia sabe de que se trata, claro 10 años de experiencia en espiritismo, jeroglíficos y criptografía.

Yo no tengo esa experiencia y apuesto a que ninguno de ustedes, así que llego el momento de sacar el supersayayin desarrollador que tenemos dentro.

Porqué puede fallar? lo primero que hay que descartar es que hayamos hecho algo mal, no se preocupen, ya revise y esta bien.

De todos los posibles caminos que podemos tomar, y son muchos, recomiendo comenzar a revisar de lo mas nuevo hacia atrás, después de revisar los parámetros seguimos con el array de bytes, al cual a estas alturas no le hemos hecho absolutamente nada, así que por el momento queda en la lista de no sospechosos.

Retrocediendo un poco más llegamos al stream, y bueno el stream es un objeto directamente involucrado en el error, y lo único que le hemos hecho es copiarle el array de bytes, habrá quedado bien la copia?. Lo primero que se me ocurre es revisar el tamaño del stream, este debe ser del mismo tamaño del array de bytes, lógicamente , así que utilizo las ayudas de Visual Studio para revisar estos valores

Watch Debug

1´440.054 oops... valor muy extraño a primera vista, ya que para este caso utilice una imagen de 800 x 600, lo cual implica que usando 4 bytes por color el tamaño debería ser 1'920.000 que es justo el tamaño del array de bytes.

Pensémoslo un poco, recuerdan esto unas líneas más arriba? BitmapAlphaMode.Ignore, si pues en el propio código le he dicho que ignore el canal alpha, asi que no tengo 4 bytes por pixel sino tres, estas cuentas me dan 1'440.000 wow... estamos cerca!

De donde salen los otros 54 bytes? para el más conocedor o sin oficio de 8 de la noche a 3 de la mañana ( creanme habemos varios ) es evidente...

Los 54 bytes son el tamaño del encabezado de un archivo BMP y recuerdan esto también unas líneas mas arriba? BitmapEncoder.BmpEncoderId , pues claro! estamos encodidificando todo como bmp! GENIAL nos cuadraron las cuentas!, pero eso no soluciona el error, que será?

Revisando de nuevo la imagen encontramos algo más, y muy sospechoso... el tamaño y la posición estan exactamente igual... o sea que el stream tiene el apuntador de la posición actual justo al final del archivo...
OMFG!... puede ser que... ooohh siii... oooohhhh SIIII.

Tengo el primer sospechoso, si el stream esta posicionado al final puede ser que al establecer el source del BitmapImage esta operación comience desde el final y por ende no hay más datos. Me armo de valor y con ayuda del método Seek posiciono el puntero de nuevo en el inicio.

bmpstream.Seek(0);

Lo ejecuto de nuevo y.... Funciona!

Imagen cargada

Este es el código completo

//Cargar bitmap desde disco
var localFolder = Package.Current.InstalledLocation;  
var imgfile = await (await localFolder.GetFolderAsync("Assets")).GetFileAsync("conejo.bmp");

//extraer los pixeles
var filestream = await imgfile.OpenReadAsync();  
var decoder = await BitmapDecoder.CreateAsync(filestream);  
var pxDataProvider = await decoder.GetPixelDataAsync();  
byte[] pxData = pxDataProvider.DetachPixelData();

//hacer algo con los pixeles
//....

//encodificar los pixeles
var bmpstream = new InMemoryRandomAccessStream();  
var encoder = await BitmapEncoder.CreateAsync(BitmapEncoder.BmpEncoderId, bmpstream);  
encoder.SetPixelData(BitmapPixelFormat.Bgra8, BitmapAlphaMode.Ignore, decoder.PixelWidth, decoder.PixelHeight, 96, 96, pxData);

await encoder.FlushAsync();  
bmpstream.Seek(0);

//crear un BitmapImage y mostrarlo en pantalla
var bd = new BitmapImage();  
await bd.SetSourceAsync(bmpstream);

imgBitmap.Source = bd;  
Espero sus comentarios y no duden en compartirlo ;)

Comparte este artículo

comments powered by Disqus