[Windows Phone 8] Nokia Imaging SDK 2: Editando imágenes

Hola a todos!

Continuamos la serie de artículos sobre el SDK de trabajo con imágenes publicado por Nokia. Si te perdiste los anteriores, puedes verlos aquí:

En este segundo artículo vamos a empezar a trabajar con imágenes. Este SDK nos facilita la edición de las mismas, permitiéndonos rotar y recortar nuestras fotos de una forma rápida y sencilla.

Antes de comenzar a trabajar con el SDK en sí mismo, vamos a repasar la estructura de proyecto que utilizaremos en este y otros ejemplos (la que usamos normalmente en todos los ejemplos de este blog: MVVM).

Estructura del proyecto

Vamos a aislar la lógica de trabajo con imágenes en un servicio llamado ImagingService. Nuestra ViewModel recibirá mediante inyección de dependencias, gracias a nuestro querido Autofac, una instancia de este servicio al que podrá llamar para:

  • Obtener una imagen de la galería.
  • Recortar una porción de imagen seleccionada previamente por el usuario.
  • Rotar una imagen 45 grados desde su orientación original.
  • Rotar una  imagen 270 grados mediante el enumerado de rotaciones.
  • Guardar una imagen.

Por lo tanto, en este proyecto trabajaremos entres niveles:

  • XAML: en el archivo MainPage.xaml definiremos nuestra interface de usuario. Como vamos a usar el ApplicationBar para ejecutar acciones, tendremos que usar code behind para llamar a nuestra viewmodel.
  • ViewModel: en el archivo VMMainPage definiremos nuestra lógica e invocaremos al servicio de imagen siempre que sea necesario.
  • Servicio: en el archivo ImagingService tendremos el código que realiza las acciones sobre la imagen.

Vamos a comenzar!

Sesión de edición y conceptos del SDK de imágenes de Nokia

El primer concepto que debemos tener claro, y que nos servirá para todas las acciones que realicemos con el SDK, es el concepto de sesión de edición.

Siempre que queramos, de alguna forma, trabajar con imágenes usando el SDK de Nokia, tendremos que hacerlo en el contexto de una sesión de edición, definida por la clase EditingSession. Esta clase implementa IDisposable, por lo que la forma recomendada de utilizarla es mediante un bloque using:

Clase EditingSession
using (EditingSession session = new EditingSession(bmp))
{

}

Al crear una nueva instancia de EditingSession debemos indicar la imagen sobre la que deseamos trabajar. Para llevar a cabo esto, podemos pasar al constructor una instancia de tipo IBuffer o una instancia de tipo Bitmap. El tipo Bitmap viene incluido con el SDK de Nokia, en el namespace Nokia.Graphics.Imaging y contiene información de los píxeles de una imagen. Como podremos ver más adelante, tendremos nuevos métodos de extensión que nos permitirán obtener un Bitmap a partir de un WriteableBitmap o un IBuffer a partir de un MemoryStream.

Una vez que hayamos creado nuestra sesión de edición podremos empezar a jugar con nuestra imagen. Básicamente todo el trabajo se realizará en dos pasos:

  • Aplicar modificaciones a la imagen, mediante recortes, filtros…
  • Usar un método de renderizado para aplicar de forma efectiva las modificaciones y obtener la imagen modificada.

Para llevar a cabo el segundo paso, EditingSession nos facilita tres métodos:

  • RenderToBitmapAsync
  • RenderToImageAsync
  • RenderToJpegAsync
  • RenderToWriteableBitmapAsync

Cada uno de estos métodos admite unos parámetros de entrada y nos ofrece un resultado, indicado en su nombre. De esta forma, RenderToBitmapAsync nos pide como parámetro de entrada un Bitmap sobre el que dibujará la imagen. RenderToImageAsync nos pide un control Image sobre el que dibujará y de forma opcional un valor del enumerado OutputOption. Este enumerado, contenido dentro del namespace Nokia.Graphics.Imaging, indica la forma en la que dibujaremos la imagen:

  • PreserveAspectRatio, que indica que se debe conservar el ratio de aspecto de la imagen original (4:3, 16:9…)
  • Stretch, que indica que se descarte el ratio de aspecto original y se ajuste la imagen a las nuevas dimensiones, aunque necesite ser deformada para ello.

RenderToJPegAsync tiene varias sobrecargas. Por defecto no requiere ningún parámetro de entrada y nos devolverá de forma asíncrona la nueva imagen en formato IBuffer. Podemos indicarle el tamaño final que deseamos que tenga la imagen y un valor del enumerado OutputOption. En su tercera sobrecarga podemos indicarle, además, la calidad que deseamos asignar a la imagen y un valor del enumerado OutputColorSpacing:

  • Yuv420, YUV con un subsampling chroma de 4:2:0. Esto es, por cada dos veces que se pasa por Y, se hace una pasada a U y cero a V.
  • Yuv422, YUV con un subsampling chroma de 4:2:2. Esto es, por cada dos veces que se pasa por Y, se pasa una vez por U y V. Este es el formato standard, por norma general en las cámaras Android.

Por último, pero no menos importante, RenderToWriteableBitmapAsync, nos permite volcar nuestra EditingSession a un WriteableBitmap, indicando de forma opcional también el enumerado OutputOption.

Y ahora que ya sabemos como funciona el concepto más básico del SDK de imágenes de Nokia, vamos a pasar a ver como aplicarlo…

Recorte de imágenes con el SDK de Nokia

Es realmente sencillo. Existen, de hecho, varias formas en las que podemos recortar una imagen. Podemos hacerlo antes de comenzar la sesión, al crear un Bitmap como fuente de la misma. O podemos hacerlo al renderizar la sesión mediante el método RenderToBitmapAsync, que en su tercera sobrecarga nos permite indicar un área de recorte.

En mi ejemplo estoy trabajando con WriteableBitmaps, por lo que es más cómodo usar el método RenderToWriteableBitmapAsync y realizar el corte antes de la sesión. Para ello, lo que vamos a hacer es usar el método extensor AsBitmap() para la clase WriteableBitmap, que viene incluido en el namespace Nokia.InteropServices.WindowsRuntime. Este método nos devuelve un Bitmap a partir de un WriteableBitmap. Lo que voy a hacer es crear un nuevo Bitmap y establecer mi WriteableBitmap, usando este método extensor, como su fuente. Esto me permitirá indicar el área del Bitmap original que deseo usar:

Creando un nuevo Bitmap
public async Task<WriteableBitmap> CropImageAsync(WriteableBitmap originalImg, Rect cropArea)
{
    Bitmap bmp = new Bitmap(originalImg.AsBitmap(), cropArea);

}

Con esta simple línea de código, he creado un nuevo Bitmap, al que le he insertado el bitmap original, mediante el método extensor AsBitmap(). Además le paso al constructor un segundo parámetro de tipo Windows.Foundation.Rect, que incluye la porción de la imagen a recortar y usar. Lo bueno de este método es que, si quiero aplicar un efecto o realizar más operaciones sobre la imagen recortada, solo lo voy a hacer sobre la porción recortada, con lo que el consumo de memoria será menor.

Tras crear nuestro bmp recortado, vamos a crear una nueva sesión que lo use como fuente de la imagen y que llame al método RenderToWriteableBitmapAsync:

Sesión para el recorte
using (EditingSession session = new EditingSession(bmp))
{
    WriteableBitmap newWBitmap = new WriteableBitmap((int)session.Dimensions.Width, (int)session.Dimensions.Height);
    await session.RenderToWriteableBitmapAsync(newWBitmap);
    newWBitmap.Invalidate();
    return newWBitmap;
}

Simplemente creamos una nueva instancia de WriteableBitmap, se la indicamos al método RenderToWriteableBitmapAsync, lo invalidamos y lo devolvemos. con esto podremos realizar el recorte de nuestra imagen sin problemas:

image

Como podemos ver, la forma de cortar una foto no nos ha dado ningún problema y lo que es mejor… ilustra perfectamente el uso del SDK de Nokia, tres pasos: Crear un sesión de edición, modificar la imagen, renderizar de nuevo la imagen.

A continuación vamos a ver como aplicar nuestro primer filtro… para realizar una rotación libre de la imagen y una rotación por pasos.

Rotación de imágenes

Para el recorte de imágenes, hemos visto que teníamos dos opciones: Hacerlo antes de crear la sesión o hacerlo al renderizar la sesión. Para la rotación de una imagen tendremos dos opciones también, aunque ambas se ejecutan en el mismo momento. Ambas opciones son filtros que aplicaremos a una sesión ya creada, por supuesto antes de renderizarla.

Aunque más adelante nos centraremos en los tipos de filtro y en el funcionamiento particular de cada uno, vamos a examinar como añadir un filtro a nuestra sesión y como funcionan en general la aplicación de un filtro en la sesión.

Una sesión puede tener uno o más filtros aplicados, de forma acumulada. Esto es, podemos aplicar un filtro de rotación y luego sobre esa imagen rotada, podemos aplicar un filtro de color, etc.. Los filtros se irán acumulando uno sobre otro, en el orden en el que los hallamos indicado.

Para añadir un filtro, usaremos el método AddFilter de la instancia de sesión. Este método recibe un parámetro de tipo IFilter. Para crear un filtro en concreto usaremos la clase FilterFactory, que contiene métodos para crear cada uno de los filtros existentes. En nuestro caso nos interesan dos en concreto: CreateFreeRotationFilter y CreateStepRotationFilter.

El efecto de estos dos filtros es básicamente el mismo: rotar nuestra imagen. Pero lo ejecutan de diferente forma. CreateFreeRotationFilter nos da todo el control. Podemos indicarle los grados de rotación de la imagen, entre 0 y 360 grados, y la forma en la que deseamos que redimensione la imagen tras la rotación, usando el enumerado RotationResizeMode:

  • FitInside, indica que la imagen se redimensionará para que entre dentro de las dimensiones originales. Si rotamos una imagen 30 grados, al quedar ladeada, sus dimensiones crecerán en altura y disminuirán en anchura. Con FitInside, se redimensiona la imagen para que ésta entre en las dimensiones originales.
  • FitOutside, indica que la imagen se redimensionará para que la imagen original entre dentro de la nueva imagen. Justo al contrario que el caso anterior.
  • Ignore, simplemente no se redimensionará la imagen.

Teniendo esto en cuenta, en nuestro ejemplo tendremos un botón que rote una imagen 45 grados. Lo primero que hacemos una vez creada la sesión, es calcular el nuevo tamaño de la imagen:

Calcular nuevo tamaño
private Size CalculateNewImageSize(EditingSession session)
{
    Size imgDimensions = new Size();
    imgDimensions.Width = session.Dimensions.Width;
    imgDimensions.Height = session.Dimensions.Width;
    if ((int)session.Dimensions.Width < session.Dimensions.Height)
    {
        imgDimensions.Width = session.Dimensions.Height;
        imgDimensions.Height = session.Dimensions.Height;
    }
    return imgDimensions;
}

Básicamente, obtenemos las dimensiones de la imagen en la sesión y decidimos entre quedarnos con el Width o con el Height. Al final usamos el mayor de ellos, tanto para el ancho como para la altura.

Una vez obtenido el tamaño final de nuestra imagen, vamos a aplicar el filtro que rotará la imagen y a dibujar el resultado en un nuevo WriteableBitmap:

CreateFreeRotationFilter
public async Task<WriteableBitmap> RotateImage(WriteableBitmap originalImg, double rotationDegrees)
{
    using (EditingSession session = new EditingSession(originalImg.AsBitmap()))
    {
        Size imgDimensions = CalculateNewImageSize(session);
        WriteableBitmap newWBitmap = new WriteableBitmap((int)imgDimensions.Width, (int)imgDimensions.Height);

        session.AddFilter(FilterFactory.CreateFreeRotationFilter(rotationDegrees, RotationResizeMode.FitOutside));
        await session.RenderToWriteableBitmapAsync(newWBitmap);
        newWBitmap.Invalidate();
        return newWBitmap;
    }
}

En este ejemplo podemos ver ya una sesión completa de edición. En primer lugar creamos la instancia de EditingSession dentro de un bloque using. Dentro, lo primero que hacemos es obtener el tamaño de nuestra imagen y crear un nuevo WriteableBitmap con esas dimensiones.

A continuación usamos el método AddFilter de nuestra sesión para añadir un nuevo filtro, usando la clase FilterFactory y el método CreateFreeRotationFilter. Este método recibe como primer parámetro los grados de rotación y como segundo parámetro el enumerado RotationResizeMode.

Tras añadir nuestro filtro, usamos el método RenderToWriteableBitmapAsync, indicando el nuevo WriteableBitmap que hemos creado, que a continuación invalidamos para forzar que se dibuje y lo devolvemos para poder mostrarlo.

¿Porqué usamos un nuevo WriteableBitmap y no, simplemente, sobre escribimos el existente? Al usar nuestro WriteableBitmap como fuente de la sesión, si intentamos escribir en él el resultado de la misma, la imagen se estropeará y se deformará al intentar leer y escribir en una misma instancia.

El uso del segundo filtro de rotación, CreateStepRotationFilter es muy parecido, si cabe mucho más sencillo:

CreateStepRotationFilter
public async Task<WriteableBitmap> RotateImageStep270(WriteableBitmap originalImg)
{
    using (EditingSession session = new EditingSession(originalImg.AsBitmap()))
    {
        Size imgDimensions = CalculateNewImageSize(session);

        session.AddFilter(FilterFactory.CreateStepRotationFilter(Rotation.Rotate270));

        WriteableBitmap newWBitmap = new WriteableBitmap((int)imgDimensions.Width, (int)imgDimensions.Height);
        await session.RenderToWriteableBitmapAsync(newWBitmap);
        newWBitmap.Invalidate();
        return newWBitmap;
    }
}

La única diferencia con el filtro libre, es que en este caso al método CreateStepRotationFilter solo tenemos que indicarle un valor del enumerado Rotation:

  • Rotate0, 0 grados de rotación.
  • Rotate90, 90 grados de rotación.
  • Rotate180, 180 grados de rotación.
  • Rotate270, 270 grados de rotación.

La rotación siempre se hará en el sentido de las agujas del reloj. Una vez que hemos aplicado el filtro, repetimos los pasos anteriores: renderizamos a un nuevo WriteableBitmap, lo invalidamos y lo devolvemos, el resultado a continuación:

image

Y voila! ya podemos recortar y rotar nuestras imágenes con un código muy simple. Ahora, veamos como también este SDK nos ayuda a la hora de guardar en disco las mismas…

Conversiones de tipos gracias a la EditingSession

Esta muy bien poder jugar con nuestras fotos, pero un paso final indispensable es poder guardar el resultado en nuestro dispositivo para poder disfrutar de él más tarde. En Windows Phone guardar una imagen en la librería de imágenes es relativamente sencillo gracias a la clase MediaLibrary en el namespace Microsoft.Xna.Framework. Aunque XNA no esté soportado “per se” para desarrolla juegos, si que se soporta el acceso a sus ensamblados desde una app wp8.

La parte complicada viene al obtener el tipo de datos que requiere MediaLibrary y su método SavePicture: un array de bytes. Hasta ahora hemos trabajado en nuestro ejemplo con WriteableBitmap solamente y no existe una forma sencilla de obtener un array de bytes desde el directamente. Lo que vamos a hacer es usar el método RenderToJpegAsync de EditingSession, para obtener un IBuffer, del que podemos obtener una MemoryStream que podemos leer para obtener los bytes:

Guardando una imagen
public async Task SavePicture(WriteableBitmap originalImg)
{
    using (MediaLibrary library = new MediaLibrary())
    {
        using (EditingSession session = new EditingSession(originalImg.AsBitmap()))
        {
            var buffer = await session.RenderToJpegAsync();
            buffer.AsStream().Position = 0;
            byte[] byteBufferToWrite = new byte[buffer.Length];
            await buffer.AsStream().ReadAsync(byteBufferToWrite, 0, (int)buffer.Length);
            library.SavePicture(DateTime.Now.Ticks.ToString(), byteBufferToWrite);
        }
    }
}

De esta forma, con solo unas pocas líneas de código podemos convertir nuestro WriteableBitmap en un array de bytes, que ya podemos guardar en nuestra librería de imágenes sin ningún problema.

Bonus track: selección de recorte en pantalla

Como bonus, si os descargáis el código del ejemplo, veréis que podéis dibujar un rectángulo azul sobre la imagen solo con arrastrar y soltar sobre la pantalla. Luego las coordenadas del rectángulo (X,Y) y su tamaño, definirán el área de recorte. Para dibujar el rectángulo, tenemos la imagen metida dentro de un ViewBox, junto con un Canvas que contiene un Border con tamaño 0. Manejamos los eventos ManipulationStarted y ManipulationDelta del Canvas para dibujar nuestra área de recorte:

Dibujando el área de recorte
private void InteractionCanvas_ManipulationStarted(object sender, ManipulationStartedEventArgs e)
{
    Canvas.SetTop(brdCrop, e.ManipulationOrigin.Y);
    Canvas.SetLeft(brdCrop, e.ManipulationOrigin.X);
}

private void InterationCanvas_manipulationDelta(object sender, ManipulationDeltaEventArgs e)
{
    if (e.CumulativeManipulation.Translation.X >= 0)
        brdCrop.Width = e.CumulativeManipulation.Translation.X;
    if (e.CumulativeManipulation.Translation.Y >= 0)
        brdCrop.Height = e.CumulativeManipulation.Translation.Y;
}

Con este sencillo código, en el evento ManipulationStarted posicionamos (X,Y) el borde. En el evento ManipulationDelta, si la traslación acumulada es mayor de 0, la asignamos a la anchura y la altura del mismo. Se podría mejorar mucho, por ejemplo controlando los valores negativos para desplazar el punto superior izquierdo del borde y hacerlo crecer en otra dirección, pero creo que como ejemplo ilustrativo, vale.

Veréis que este código está directamente en code behind, sin hacer uso de la ViewModel… ¿Es que me he vuelto loco? No, simplemente este código es totalmente perteneciente a la vista. Podríamos usar EventToCommand para llamar a un comando, modificar las propiedades X, Y, Width y Height en nuestra ViewModel y enlazarlas al borde… pero no creo que aportase nada en absoluto y me parece que esa parte de lógica es perteneciente a la vista. (Admito discusiones en este aspecto y si queréis, podemos hacer un ejemplo de como sería este mismo código usando totalmente MVVM, dejadlo en los comentarios)

Y… fundido a negro… corten!

Y con esto llegamos al final del segundo artículo de esta serie sobre Nokia Imaging SDK. En el próximo veremos más a fondo los diferentes tipos de filtros y funcionalidades como el undo infinito sobre los filtros aplicados a una imagen. Hasta entonces, os dejo el proyecto de ejemplo que he usado en este artículo.

DISCLAIMER: Os doy el código para que hagáis con el todo lo que queráis, pero las fotos de este artículo son de mi preciosa mujer ™ y esas no os las presto, ni las fotos ni a ella… Por cierto, a ver quien adivina con que dispositivo se han sacado dichas fotos.

Un saludo y Happy Coding!

Un comentario sobre “[Windows Phone 8] Nokia Imaging SDK 2: Editando imágenes”

Responder a anonymous Cancelar respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *