Crear, editar y guardar imágenes de mapa de bits

Este artículo explica cómo cargar y guardar archivos de imagen mediante BitmapDecoder y BitmapEncoder y cómo usar el objeto SoftwareBitmap para representar imágenes de mapa de bits.

La clase SoftwareBitmap es una API versátil que se puede crear a partir de varios orígenes, incluidos archivos de imagen, objetos WriteableBitmap, superficies de Direct3D y código. SoftwareBitmap permite la fácil conversión entre diferentes formatos de píxeles y modos alfa, y el acceso de bajo nivel a los datos de píxeles. Además, SoftwareBitmap es una interfaz común que usa varias características de Windows, entre las que se incluyen:

  • CapturedFrame permite obtener fotogramas capturados por la cámara como SoftwareBitmap.

  • VideoFrame permite obtener una representación de SoftwareBitmap de un VideoFrame.

  • FaceDetector permite detectar caras en un objeto SoftwareBitmap.

El código de ejemplo de este artículo usa las interfaces de los siguientes espacios de nombres.

using Windows.Storage;
using Windows.Storage.Pickers;
using Windows.Storage.Streams;
using Windows.Graphics.Imaging;
using Windows.UI.Xaml.Media.Imaging;

Creación de un SoftwareBitmap a partir de un archivo de imagen con BitmapDecoder

Para crear un objeto SoftwareBitmap a partir de un archivo, obtén una instancia de StorageFile que contenga los datos de imagen. En este ejemplo se usa FileOpenPicker para permitir al usuario seleccionar un archivo de imagen.

FileOpenPicker fileOpenPicker = new FileOpenPicker();
fileOpenPicker.SuggestedStartLocation = PickerLocationId.PicturesLibrary;
fileOpenPicker.FileTypeFilter.Add(".jpg");
fileOpenPicker.ViewMode = PickerViewMode.Thumbnail;

var inputFile = await fileOpenPicker.PickSingleFileAsync();

if (inputFile == null)
{
    // The user cancelled the picking operation
    return;
}

Llama al método OpenAsync del objeto StorageFile para obtener una secuencia de acceso aleatorio que contenga los datos de la imagen. Llama al método estático BitmapDecoder.CreateAsync para obtener una instancia de la clase BitmapDecoder para la secuencia especificada. Llama a GetSoftwareBitmapAsync para obtener un objeto SoftwareBitmap que contenga la imagen.

SoftwareBitmap softwareBitmap;

using (IRandomAccessStream stream = await inputFile.OpenAsync(FileAccessMode.Read))
{
    // Create the decoder from the stream
    BitmapDecoder decoder = await BitmapDecoder.CreateAsync(stream);

    // Get the SoftwareBitmap representation of the file
    softwareBitmap = await decoder.GetSoftwareBitmapAsync();
}

Guardar un SoftwareBitmap en un archivo con BitmapEncoder

Para guardar un SoftwareBitmap en un archivo, obtén una instancia de StorageFile en la que se guardará la imagen. En este ejemplo se usa FileSavePicker para permitir al usuario seleccionar un archivo de resultados.

FileSavePicker fileSavePicker = new FileSavePicker();
fileSavePicker.SuggestedStartLocation = PickerLocationId.PicturesLibrary;
fileSavePicker.FileTypeChoices.Add("JPEG files", new List<string>() { ".jpg" });
fileSavePicker.SuggestedFileName = "image";

var outputFile = await fileSavePicker.PickSaveFileAsync();

if (outputFile == null)
{
    // The user cancelled the picking operation
    return;
}

Llama al método OpenAsync del objeto StorageFile para obtener una secuencia de acceso aleatorio en la que la imagen se escribirá. Llama al método estático BitmapEncoder.CreateAsync para obtener una instancia de la clase BitmapEncoder para la secuencia especificada. El primer parámetro para CreateAsync es un GUID que representa el códec que se debe usar para codificar la imagen. La clase BitmapEncoder expone una propiedad que contiene el identificador de cada códec admitido por el codificador, como JpegEncoderId.

Usa el método SetSoftwareBitmap para establecer la imagen que se codificará. Puedes establecer valores de la propiedad BitmapTransform para aplicar transformaciones básicas a la imagen mientras se codifica. La propiedad IsThumbnailGenerated determina si el codificador genera una miniatura. Ten en cuenta que no todos los formatos de archivo admiten miniaturas, por lo que si usas esta característica, debes detectar el error de operación no compatible que se producirá si no se admiten miniaturas.

Llama a FlushAsync para que el codificador escriba los datos de imagen en el archivo especificado.

private async void SaveSoftwareBitmapToFile(SoftwareBitmap softwareBitmap, StorageFile outputFile)
{
    using (IRandomAccessStream stream = await outputFile.OpenAsync(FileAccessMode.ReadWrite))
    {
        // Create an encoder with the desired format
        BitmapEncoder encoder = await BitmapEncoder.CreateAsync(BitmapEncoder.JpegEncoderId, stream);

        // Set the software bitmap
        encoder.SetSoftwareBitmap(softwareBitmap);

        // Set additional encoding parameters, if needed
        encoder.BitmapTransform.ScaledWidth = 320;
        encoder.BitmapTransform.ScaledHeight = 240;
        encoder.BitmapTransform.Rotation = Windows.Graphics.Imaging.BitmapRotation.Clockwise90Degrees;
        encoder.BitmapTransform.InterpolationMode = BitmapInterpolationMode.Fant;
        encoder.IsThumbnailGenerated = true;

        try
        {
            await encoder.FlushAsync();
        }
        catch (Exception err)
        {
            const int WINCODEC_ERR_UNSUPPORTEDOPERATION = unchecked((int)0x88982F81);
            switch (err.HResult)
            {
                case WINCODEC_ERR_UNSUPPORTEDOPERATION: 
                    // If the encoder does not support writing a thumbnail, then try again
                    // but disable thumbnail generation.
                    encoder.IsThumbnailGenerated = false;
                    break;
                default:
                    throw;
            }
        }

        if (encoder.IsThumbnailGenerated == false)
        {
            await encoder.FlushAsync();
        }


    }
}

Puedes especificar opciones de codificación adicionales al crear BitmapEncoder mediante la creación de un nuevo objeto BitmapPropertySet y rellenarlo con uno o varios objetos BitmapTypedValue que representan la configuración del codificador. Para obtener una lista de las opciones de codificador admitidas, consulta Referencia de opciones de BitmapEncoder.

var propertySet = new Windows.Graphics.Imaging.BitmapPropertySet();
var qualityValue = new Windows.Graphics.Imaging.BitmapTypedValue(
    1.0, // Maximum quality
    Windows.Foundation.PropertyType.Single
    );

propertySet.Add("ImageQuality", qualityValue);

await Windows.Graphics.Imaging.BitmapEncoder.CreateAsync(
    Windows.Graphics.Imaging.BitmapEncoder.JpegEncoderId,
    stream,
    propertySet
);

Usar SoftwareBitmap con un control de imagen XAML

Para mostrar una imagen dentro de una página XAML mediante el control Imagen, primero define un control Imagen en la página XAML.

<Image x:Name="imageControl"/>

Actualmente, el control Imagen solo admite imágenes que usan codificación BGRA8 y pre multiplicado o sin canal alfa. Antes de mostrar una imagen, pruébala para asegurarte de que tiene el formato correcto y, si no es así, usa el método Conversión estática SoftwareBitmap para convertir la imagen al formato admitido.

Crea un nuevo objeto SoftwareBitmapSource. Establece el contenido del objeto de origen llamando a SetBitmapAsync y pasando un objeto SoftwareBitmap. A continuación, puedes establecer la propiedad Origen del control Imagen en el objeto SoftwareBitmapSource recién creado.

if (softwareBitmap.BitmapPixelFormat != BitmapPixelFormat.Bgra8 ||
    softwareBitmap.BitmapAlphaMode == BitmapAlphaMode.Straight)
{
    softwareBitmap = SoftwareBitmap.Convert(softwareBitmap, BitmapPixelFormat.Bgra8, BitmapAlphaMode.Premultiplied);
}

var source = new SoftwareBitmapSource();
await source.SetBitmapAsync(softwareBitmap);

// Set the source of the Image control
imageControl.Source = source;

También puedes usar SoftwareBitmapSource para establecer un SoftwareBitmap como ImageSource para un ImageBrush.

Crear un objeto SoftwareBitmap a partir de WriteableBitmap

Puedes crear un SoftwareBitmap a partir de un WriteableBitmap llamando al SoftwareBitmap.CreateCopyFromBuffer y proporcionando la propiedad PixelBuffer de WriteableBitmap para establecer los datos de píxeles. El segundo argumento permite solicitar un formato de píxel para objeto WriteableBitmap recién creado. Puedes usar las propiedades PixelWidth y PixelHeight de WriteableBitmap para especificar las dimensiones de la nueva imagen.

SoftwareBitmap outputBitmap = SoftwareBitmap.CreateCopyFromBuffer(
    writeableBitmap.PixelBuffer,
    BitmapPixelFormat.Bgra8,
    writeableBitmap.PixelWidth,
    writeableBitmap.PixelHeight
);

Crear o editar un objeto SoftwareBitmap mediante programación

Hasta ahora, este tema ha tratado el trabajo con archivos de imagen. También puedes crear un nuevo SoftwareBitmap mediante programación en el código y usar la misma técnica para acceder a los datos de píxeles de SoftwareBitmap y modificarlos.

SoftwareBitmap usa la interoperabilidad COM para exponer el búfer sin procesar que contiene los datos de píxeles.

Para usar la interoperabilidad COM, debes incluir una referencia al espacio de nombres System.Runtime.InteropServices en tu proyecto.

using System.Runtime.InteropServices;

Inicializa la interfaz COM IMemoryBufferByteAccess agregando el código siguiente dentro del espacio de nombres.

[ComImport]
[Guid("5B0D3235-4DBA-4D44-865E-8F1D0E4FD04D")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
unsafe interface IMemoryBufferByteAccess
{
    void GetBuffer(out byte* buffer, out uint capacity);
}

Crea un nuevo SoftwareBitmap con formato de píxel y tamaño que desees. O bien, usa un SoftwareBitmap existente y edita los datos de píxeles. Llama a SoftwareBitmap.LockBuffer para obtener una instancia de la clase BitmapBuffer que representa el búfer de datos de píxeles. Convierte BitmapBuffer en la interfaz COM IMemoryBufferByteAccess y, a continuación, llama a IMemoryBufferByteAccess.GetBuffer para rellenar una matriz de bytes con datos. Utiliza el método BitmapBuffer.GetPlaneDescription para obtener un objeto BitmapPlaneDescription que te ayudará a calcular el desplazamiento en el búfer para cada píxel.

softwareBitmap = new SoftwareBitmap(BitmapPixelFormat.Bgra8, 100, 100, BitmapAlphaMode.Premultiplied);

using (BitmapBuffer buffer = softwareBitmap.LockBuffer(BitmapBufferAccessMode.Write))
{
    using (var reference = buffer.CreateReference())
    {
        byte* dataInBytes;
        uint capacity;
        ((IMemoryBufferByteAccess)reference).GetBuffer(out dataInBytes, out capacity);

        // Fill-in the BGRA plane
        BitmapPlaneDescription bufferLayout = buffer.GetPlaneDescription(0);
        for (int i = 0; i < bufferLayout.Height; i++)
        {
            for (int j = 0; j < bufferLayout.Width; j++)
            {

                byte value = (byte)((float)j / bufferLayout.Width * 255);
                dataInBytes[bufferLayout.StartIndex + bufferLayout.Stride * i + 4 * j + 0] = value;
                dataInBytes[bufferLayout.StartIndex + bufferLayout.Stride * i + 4 * j + 1] = value;
                dataInBytes[bufferLayout.StartIndex + bufferLayout.Stride * i + 4 * j + 2] = value;
                dataInBytes[bufferLayout.StartIndex + bufferLayout.Stride * i + 4 * j + 3] = (byte)255;
            }
        }
    }
}

Dado que este método accede al búfer sin procesar subyacente a los tipos de Windows Runtime, debes declararte mediante la palabra clave inseguro. También debes configurar el proyecto en Microsoft Visual Studio para permitir la compilación de código no seguro; para ello, abre la página Propiedades del proyecto, haz clic en la página de propiedades Compilar y activa la casilla Permitir código no seguro.

Crear un SoftwareBitmap a partir de una superficie de Direct3D

Para crear un objeto SoftwareBitmap a partir de una superficie de Direct3D, debes incluir el espacio de nombres Windows.Graphics.DirectX.Direct3D11 en el proyecto.

using Windows.Graphics.DirectX.Direct3D11;

Llama a CreateCopyFromSurfaceAsync para crear un nuevo SoftwareBitmap desde la superficie. Como indica el nombre, el nuevo SoftwareBitmap tiene una copia independiente de los datos de la imagen. Las modificaciones en SoftwareBitmap no tendrán ningún efecto en la superficie de Direct3D.

private async void CreateSoftwareBitmapFromSurface(IDirect3DSurface surface)
{
    softwareBitmap = await SoftwareBitmap.CreateCopyFromSurfaceAsync(surface);
}

Convertir un BitmapSource a un formato de píxeles diferente

La clase SoftwareBitmap proporciona el método estático Convert, que te permite crear fácilmente un nuevo SoftwareBitmap con el formato de píxel y el modo alfa especificado a partir de un softwareBitmap existente. Ten en cuenta que el mapa de bits recién creado tiene una copia independiente de los datos de imagen. Las modificaciones en el nuevo mapa de bits no afectarán al mapa de bits de origen.

SoftwareBitmap bitmapBGRA8 = SoftwareBitmap.Convert(softwareBitmap, BitmapPixelFormat.Bgra8, BitmapAlphaMode.Premultiplied);

Transcodificación de un archivo de imagen

Puedes transcodificar un archivo de imagen directamente desde un bitmapDecoder a un bitmapEncoder. Crea un IRandomAccessStream desde el archivo que se va a transcodificar. Crea un bitmapDecoder a partir del flujo de entrada. Crea un nuevo InMemoryRandomAccessStream para que el codificador escriba y llame al BitmapEncoder.CreateForTranscodingAsync, y pase la secuencia en memoria y el objeto de descodificador. Las opciones de codificación no funcionan al transcodificar; en su lugar, debes usar CreateAsync. Las propiedades del archivo de imagen de entrada que no se establezcan específicamente en el codificador se escribirán en el archivo de salida sin cambios. Llama a FlushAsync para que el codificador codifique en la secuencia en memoria. Por último, busca la secuencia de archivos y la secuencia en memoria al principio y llama a CopyAsync para escribir la secuencia en memoria en la secuencia de archivos.

private async void TranscodeImageFile(StorageFile imageFile)
{


    using (IRandomAccessStream fileStream = await imageFile.OpenAsync(FileAccessMode.ReadWrite))
    {
        BitmapDecoder decoder = await BitmapDecoder.CreateAsync(fileStream);

        var memStream = new Windows.Storage.Streams.InMemoryRandomAccessStream();
        BitmapEncoder encoder = await BitmapEncoder.CreateForTranscodingAsync(memStream, decoder);

        encoder.BitmapTransform.ScaledWidth = 320;
        encoder.BitmapTransform.ScaledHeight = 240;

        await encoder.FlushAsync();

        memStream.Seek(0);
        fileStream.Seek(0);
        fileStream.Size = 0;
        await RandomAccessStream.CopyAsync(memStream, fileStream);

        memStream.Dispose();
    }
}