Redimensionar imágenes, convertirlas a byte array y viceversa (con transparencia)
El título del post es algo largo, pero resume un problema que me volvía de cabeza desde hace un tiempo, y que no era capaz de resolver… hasta hoy.
Cuando trabajamos con imágenes en una aplicación suele ser muy común almacenarlas en una base de datos. En el caso que me ocupa, al ser imágenes con una resolución bastante alta, un requisito es que éstas deben almacenarse a distintas resoluciones. Sin embargo, antes de continuar con el tema permitidme un paréntesis:
<PARENTESIS MODE = “on”>
Sé que existen bastantes detractores de ésta práctica, que suelen preferir guardar las imágenes en disco, pero esto a mi juicio conlleva una serie de inconvenientes:
- Pérdida de atomicidad: Mezclamos un sistema transaccional con un sistema de ficheros no transaccional (y no, de momento no recomiendo usar transacciones en el sistema de ficheros, al menos si no queréis quedaros calvos en el proceso). De modo que como no disponemos de un mecanismo transaccional, debemos implementar mecanismos de sincronización ‘a manija’ entre la base de datos y el sistema de ficheros, con todo lo que conlleva.
- Problemas al hacer copias de seguridad: Ya que mediante el SQL Server agent podemos planificar copias periódicas de la base de datos, pero no de los ficheros asociados. Así pues, hay que copiar los ficheros manualmente o lanzando un proceso desde nuestra aplicación.
- También suele argumentarse que si guardamos las imágenes en la base de datos, el tamaño de la base de datos puede incrementarse mucho y degradarse el rendimiento (recordar que SQL Server Express ‘sólo’ admite bases de datos de hasta 10GB). Esto no es cierto, ya que desde la versión 2008 existe la posibilidad de utilizar FILESTREAM, que permite almacenar los datos de un campo en el sistema de ficheros, obteniendo así lo mejor de ambos mundos.
<PARENTESIS MODE= “off”>
Vale, sigamos con el tema.
Como os decía, en el proyecto que me ocupa actualmente un requisito muy importante es almacenar distintas resoluciones de una imagen en la base de datos mediante FILESTREAM. Para ello, hay que redimensionar cada una de las imágenes y convertirlas en un array de bytes, para luego almacenarlas en un campo de tipo BLOB, concretamente varbinary(MAX). Posteriormente cuando queremos recuperar una imagen, se lee el array de bytes y se transforma otra vez en imagen para visualizarla por pantalla, imprimirla, o lo que sea…
Redimensionar imagenes
Cuál es el problema entonces? Existen multitud de ejemplos en Internet acerca de cómo redimensionar imágenes:
public static Image ResizeImage(this Image oldImage, int targetSize)
{
Size newSize = calculateDimensions(oldImage.Size, targetSize);
using (Bitmap newImage = new Bitmap(newSize.Width,
newSize.Height, PixelFormat.Format24bppRgb))
{
using (Graphics canvas = Graphics.FromImage(newImage))
{
canvas.SmoothingMode = SmoothingMode.AntiAlias;
canvas.InterpolationMode = InterpolationMode.HighQualityBicubic;
canvas.PixelOffsetMode = PixelOffsetMode.HighQuality;
canvas.DrawImage(oldImage, new Rectangle(new Point(0, 0), newSize));
using (MemoryStream m = new MemoryStream())
{
newImage.Save(m, ImageFormat.Jpeg);
return (Image)newImage.Clone();
}
}
}
}
El código anterior funciona bien en casi todos los casos, pero no cuando la imagen a redimensionar contiene partes transparentes, ya que las partes transparentes aparecen en negro. Esto es así porque la información de transparencia de una imagen se almacena en el canal alfa, y en el código anterior al crear el nuevo Bitmap estamos usando explícitamente el valor ‘Format24bppRgb’ de la enumeración PixelFormat, que almacena 8 bits para cada color primario.
En su lugar, debemos utilizar el valor ‘Format32bppRgb’ que almacena 8 bits para cada color primario más 8 bits para el canal alfa. También podemos omitir el formato en el constructor y pasar sólo el ancho y alto, ya que por defecto se usará el valor ‘Format32bppRgb’ en caso que no sea suministrado.
De todos modos, el código anterior es sólo a efectos de ilustrar el ejemplo, ya que para redimensionar una imagen es mucho más sencillo usar el método ‘GetThumbnailImage’ de la clase ‘Image’:
public static Image ResizeImage(this Image oldImage, int targetSize)
{
Size newSize = calculateDimensions(oldImage.Size, targetSize);
return oldImage.GetThumbnailImage(newSize.Width, newSize.Height, () => false, IntPtr.Zero);
}
Convirtiendo imágenes a bytes y viceversa
También existen multitud de ejemplos acerca de convertir imágenes a matrices y a la inversa. Veamos algunos ejemplos:
1) Mediante un MemoryStream: en este ejemplo se vuelca la imagen en un stream en memoria, y posteriormente se transforma en un array.
public static byte[] ConvertImageToByteArray(System.Drawing.Image imageIn)
{
using (System.IO.MemoryStream ms = new System.IO.MemoryStream())
{
imageIn.Save(ms, ImageFormat.Jpeg);
return ms.ToArray();
}
}
public static Image ConvertByteArrayToImage(byte[] byteArrayIn)
{
using (System.IO.MemoryStream ms = new System.IO.MemoryStream(byteArrayIn))
{
Image returnImage = Image.FromStream(ms);
return returnImage;
}
}
Resultaría muy sencillo si no fuese porque al convertir la imagen a un array volvemos a tener el problema de las transparencias.
2) Otro método es utilizar código unsafe para copiar literalmente los bits de la imagen a un array:
private unsafe byte[] BmpToBytes_Unsafe(Bitmap bmp)
{
BitmapData bData = bmp.LockBits(new Rectangle(new Point(), bmp.Size),
ImageLockMode.ReadOnly,
PixelFormat.Format32bppArgb);
int byteCount = bData.Stride * bmp.Height;
byte[] bmpBytes = new byte[byteCount];
Marshal.Copy(bData.Scan0, bmpBytes, 0, byteCount);
bmp.UnlockBits(bData);
return bmpBytes;
}
private unsafe Bitmap BytesToBmp_Unsafe(byte[] bmpBytes, Size imageSize)
{
Bitmap bmp = new Bitmap(imageSize.Width, imageSize.Height);
BitmapData bData = bmp.LockBits(new Rectangle(new Point(), bmp.Size),
ImageLockMode.WriteOnly,
PixelFormat.Format32bppArgb);
Marshal.Copy(bmpBytes, 0, bData.Scan0, bmpBytes.Length);
bmp.UnlockBits(bData);
return bmp;
}
Sin duda éste método ofrece un mayor rendimiento, y además al especificar el formato ‘Format32bppArgb’ nos soluciona el problema de las transparencias, pero resulta que nos crea otro problema: Para posteriormente poder revertir el array a imagen necesitamos conocer el tamaño de la imagen original, y eso no es demasiado práctico.
AL final la solución ha sido mucho más simple y porque no, mucho más elegante: Usando un simple TypeConverter.ConvertTo:
public static byte[] ConvertImageToByteArray(System.Drawing.Image imageIn)
{
return (byte[])TypeDescriptor.GetConverter(imageIn).ConvertTo(imageIn, typeof(byte[]));
}
En fin, espero que si alguien ha estado en la misma situación que yo, al menos este post le resuelva un poco la vida 🙂
Saludos desde Andorra a punto de cerrar el año,
8 Responsesso far
Gran Post como siempre.
La verdad es que el trato de imagines siempre se hace un poco pesado :S
C# .NET no es potente para tratamiento de imágenes, no? si ya hay que utilizar métodos unsafe y la API de Windows para ello… para aplicaciones de bajo nivel, de sistemas, como antivirus o herramientas de utilidades (tipo CCleaner y tantas otras) .NET no parece adecuado ?? A ver si alguna voz oficial de Microsoft dice algo al respecto…
Hola preguntón,
¿Que C# no es potetnte para el tratamiento de imagenes? ¿Y lo dices porque en el post hay fragmentos de código unsafe?
Uhm… pero hombre, tu te has leído el post? Lo has entendido? No se… a lo mejor es que me he explicado fatal :-S, pero en resumidas cuentas el código necesario para redimensionar una imagen son 2 líneas de código y para transformar una imagen a array de bytes una línea de código. Sin unsafe ni API, puro Framework 100%.
Aún y así te diré que para tratamiento intensivo de imágenes no hay nada como C/C++, ya que en estos casos no hay nada más rápido que usar un array de punteros para acceder a los puntos de una imagen de mapa de bits.
La ventaja es que a diferencia de otros lenguajes, C# permite utilizar bloques de código unsafe para poder realizar operaciones de este tipo. Que más quieres?
Respecto a aplicaciones de bajo nivel (antivirus y demás), dices que no te parece apropiado en uso de C#… con que lo desarrollarías entonces? C++? Java (permite que me ría)? Que dices? O se lo preguntamos a los colegas @RodrigoCorral y @IbonLanda que trabaaron en Panda unos cuantos años?
No soy precisamente una voz oficial de Microsoft, pero espero que te sirva mi respuesta. Y si tienes más dudas no te cortes, pregunta, ok?
Saludos,
Al igual que Javier comenta, cada vez que un cliente se me pone a hablar de imágenes empiezo a temblar. Estoy con Lluís en el tema que C# es una herramienta genial para el tema de imágenes. No sé si realmente será muy potente, pero hasta el momento, con un par de líneas de código te permites el lujo de redimensionar, de meter una marca de agua personalizada, de cambiar la resolución… En definitiva, en mi caso, que no tengo ni idea del trabajo con imágenes, no he tenido nunca ningún problema para hacer las típicas aplicaciones que te permiten subir imágenes a un servidor y mostrarlas posteriormente.
Lo que sí que creo que siempre seguirá martirizándome es el apartado que Lluís ha colocado entre paréntesis, es decir, fichero suelto o dentro de la base de datos. Incluso creo recordar que la versión 2008 de SQL ha incorporado un nuevo tipo de campo que viene a suplir esta carencia que hemos tenido siempre con los ficheros binarios. En mi caso particular, el problema es que, por exigencias del guión, en los últimos trabajos estoy obligado a trabajar con MySQL y ya he tenido algún que otro problemilla con la base cuando intentas forzarla un poco almacendo este tipo de información.
@Preguntón: Releyendo mi respuesta creo que el tono no ha sido el más adecuado. Básicamente me refería a que no hay nda tan potente como C/C++ pero que C# te permite utilizar código unsafe en caso que sea necesario (que en el caso anterior no lo es). Por otro lado, para aplicaciones de bajo nivel lo que hay que hacer es utilizar la herramienta adecuada, si tienes alguna tarea de muy alta prioridad haz un componente C++ y llámalo desde tu aplicación, que puede estar hecha en el lenguaje que quieras.
@Jesús: La verdad es que salvo en un par de ocasiones (creando filtros de imágenes), no he tenido que utilizar código unsafe. Respecto al dilema dentro-fuera dela BD, debo decirte que FILESTREAM es una gozada, se encarga él solito e gestionar y sincronizar las transacciones con el sistema de ficheros, y tiene un recolector para ir liberando el espacio cuando se eliminan registros. Una gozada vaya! Siento que te lo pierdas por utilizar mySQL, que tiene sus cosas buenas, pero esto es una maravilla.
Pues la verdad es que el hecho de que la transaccionalidad en NTFS sea tan… digamos compleja, le quita mucho juego.
Si hacer una transacción en NTFS, fuese tan sencillo como usar System.Transactions y meter un FileStream allí dentro, entonces usaríamos cada cosa por lo que es, y no necesitaríamos inventos como FILESTREAM (y que nadie me malinterprete: no critico FILESTREAM, critico que sea necesario).
Saludos!
Pues si @Edu, así es. Una lástima que no sea más sencillo, porque tiempo han tenido de encapsularlo en el FWK :-/
Todavía recuerdo tu post de hace más o menos un año sobre TxF:
http://geeks.ms/blogs/etomas/archive/2009/11/25/txf-ntfs-transaccional.aspx
Era la época en la que le estaba dándo un ojo al tema y me fué de perlas 🙂
Jejejeee… yo también me lo estuve mirando para un proyectillo donde necesitábamos «asegurar» una coherencia de ficheros.
La verdad es que para esto todavía es fácil de usar, pero si lo quieres sincronizar con una transacción de base de datos… buf! Se complica el tema: es posible pero se deben empezar a usar objetos COM incomprensibles, no hay apenas documentación.
Y es una pena, porque el mecanismo es super potente y es una pena que en .NET 4 no venga «de serie»… 🙂
Saludos!