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

En este artículo se 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 convertir fácilmente entre diferentes formatos de píxeles y modos alfa, y permite el acceso de bajo nivel a los datos de píxeles. Además, SoftwareBitmap es una interfaz común que usan 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 SoftwareBitmap de un VideoFrame.

  • FaceDetector permite detectar caras en un SoftwareBitmap.

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

using Windows.Storage;
using Windows.Storage.Pickers;
using Windows.Storage.Streams;
using Windows.Graphics.Imaging;
using Microsoft.UI.Xaml.Media.Imaging;
using System.Runtime.InteropServices.WindowsRuntime;

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

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

private async Task<StorageFile?> PickInputFileAsync()
{
    var picker = new FileOpenPicker();

    // Initialize the picker with the window handle (required for WinUI 3).
    var hwnd = WinRT.Interop.WindowNative.GetWindowHandle(App.MainWindow);
    WinRT.Interop.InitializeWithWindow.Initialize(picker, hwnd);

    picker.ViewMode = PickerViewMode.Thumbnail;
    picker.SuggestedStartLocation = PickerLocationId.PicturesLibrary;
    picker.FileTypeFilter.Add(".jpg");
    picker.FileTypeFilter.Add(".jpeg");
    picker.FileTypeFilter.Add(".png");
    picker.FileTypeFilter.Add(".bmp");

    return await picker.PickSingleFileAsync();
}

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

private async Task<SoftwareBitmap> CreateSoftwareBitmapFromFileAsync(StorageFile file)
{
    using IRandomAccessStream stream = await file.OpenAsync(FileAccessMode.Read);

    // Create a decoder from the image file.
    BitmapDecoder decoder = await BitmapDecoder.CreateAsync(stream);

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

    return softwareBitmap;
}

Guardar un objeto SoftwareBitmap en un archivo con BitmapEncoder

Para guardar un Objeto SoftwareBitmap en un archivo, obtenga una instancia de StorageFile en la que se guardará la imagen. En este ejemplo se usa un FileSavePicker para permitir al usuario seleccionar un archivo de salida.

private async Task<StorageFile?> PickOutputFileAsync()
{
    var picker = new FileSavePicker();

    // Initialize the picker with the window handle (required for WinUI 3).
    var hwnd = WinRT.Interop.WindowNative.GetWindowHandle(App.MainWindow);
    WinRT.Interop.InitializeWithWindow.Initialize(picker, hwnd);

    picker.SuggestedStartLocation = PickerLocationId.PicturesLibrary;
    picker.SuggestedFileName = "output";
    picker.FileTypeChoices.Add("JPEG Image", new List<string> { ".jpg" });
    picker.FileTypeChoices.Add("PNG Image", new List<string> { ".png" });

    return await picker.PickSaveFileAsync();
}

Llame al método OpenAsync del objeto StorageFile para obtener una secuencia de acceso aleatorio a la que se escribirá la imagen. Llame 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.

Use el método SetSoftwareBitmap para establecer la imagen que se codificará. Puede 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. Tenga en cuenta que no todos los formatos de archivo admiten miniaturas, por lo que si usa esta característica, debe detectar el error de operación no compatible que se producirá si no se admiten miniaturas.

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

private async Task SaveSoftwareBitmapToFileAsync(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 (optional).
    encoder.BitmapTransform.ScaledWidth = (uint)softwareBitmap.PixelWidth;
    encoder.BitmapTransform.ScaledHeight = (uint)softwareBitmap.PixelHeight;
    encoder.BitmapTransform.InterpolationMode = BitmapInterpolationMode.Fant;
    encoder.IsThumbnailGenerated = true;

    try
    {
        await encoder.FlushAsync();
    }
    catch (Exception ex)
    {
        const int WINCODEC_ERR_UNSUPPORTEDOPERATION = unchecked((int)0x88982F81);
        switch (ex.HResult)
        {
            case WINCODEC_ERR_UNSUPPORTEDOPERATION:
                // If the encoder does not support thumbnail generation,
                // disable it and try again.
                encoder.IsThumbnailGenerated = false;
                break;

            default:
                throw;
        }
    }

    if (!encoder.IsThumbnailGenerated)
    {
        await encoder.FlushAsync();
    }
}

Puede especificar opciones de codificación adicionales cuando cree el BitmapEncoder creando un nuevo objeto BitmapPropertySet y rellenándolo con uno o varios objetos BitmapTypedValue que representen la configuración del codificador. Para obtener una lista de las opciones de codificador admitidas, consulte Referencia de opciones de BitmapEncoder.

private async Task SaveWithEncodingOptionsAsync(SoftwareBitmap softwareBitmap, StorageFile outputFile)
{
    using IRandomAccessStream stream = await outputFile.OpenAsync(FileAccessMode.ReadWrite);

    // Create encoding options with a specific image quality.
    var propertySet = new BitmapPropertySet();
    var qualityValue = new BitmapTypedValue(0.9, Windows.Foundation.PropertyType.Single);
    propertySet.Add("ImageQuality", qualityValue);

    // Create the encoder with the encoding options.
    BitmapEncoder encoder = await BitmapEncoder.CreateAsync(
        BitmapEncoder.JpegEncoderId, stream, propertySet);

    encoder.SetSoftwareBitmap(softwareBitmap);
    await encoder.FlushAsync();
}

Usar SoftwareBitmap con un control de imagen XAML

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

<Image x:Name="imageControl"
       Grid.Row="2"
       Stretch="Uniform"/>

Actualmente, el control Image solo admite imágenes con codificación BGRA8 y canal alfa premultiplicado o sin alfa. Antes de intentar mostrar una imagen, compruebe que tenga el formato correcto y, de no ser así, use el método estático Convert de SoftwareBitmap para convertir la imagen al formato compatible.

Cree un nuevo objeto SoftwareBitmapSource . Establezca el contenido del objeto de origen llamando a SetBitmapAsync y pasando un SoftwareBitmap. A continuación, puede establecer la propiedad Source del control Image en el objeto SoftwareBitmapSource recién creado.

private async Task DisplaySoftwareBitmapAsync(SoftwareBitmap softwareBitmap)
{
    // SoftwareBitmap must be Bgra8 with premultiplied or no alpha
    // to display in a XAML Image control.
    if (softwareBitmap.BitmapPixelFormat != BitmapPixelFormat.Bgra8 ||
        softwareBitmap.BitmapAlphaMode == BitmapAlphaMode.Straight)
    {
        softwareBitmap = SoftwareBitmap.Convert(
            softwareBitmap, BitmapPixelFormat.Bgra8, BitmapAlphaMode.Premultiplied);
    }

    // In WinUI 3, SoftwareBitmapSource is in Microsoft.UI.Xaml.Media.Imaging.
    var source = new SoftwareBitmapSource();
    await source.SetBitmapAsync(softwareBitmap);

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

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

Crear un objeto SoftwareBitmap a partir de writeableBitmap

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

private SoftwareBitmap ConvertWriteableBitmapToSoftwareBitmap(WriteableBitmap writeableBitmap)
{
    // Create a SoftwareBitmap from the WriteableBitmap's pixel buffer.
    SoftwareBitmap softwareBitmap = SoftwareBitmap.CreateCopyFromBuffer(
        writeableBitmap.PixelBuffer,
        BitmapPixelFormat.Bgra8,
        writeableBitmap.PixelWidth,
        writeableBitmap.PixelHeight);

    return softwareBitmap;
}

Crear o editar un objeto SoftwareBitmap mediante programación

Hasta ahora, este tema ha tratado el trabajo con archivos de imagen. También puede 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.

Use el método CopyFromBuffer para rellenar un Objeto SoftwareBitmap de una matriz de bytes y CopyToBuffer para copiar datos de píxeles en una matriz de bytes para leer o modificar. Para usar el método de extensión AsBuffer para encapsular una matriz de bytes como un objeto IBuffer, incluya el espacio de nombres System.Runtime.InteropServices.WindowsRuntime (que se incluye en las instrucciones SnippetNamespaces using al principio de este artículo).

Cree un nuevo SoftwareBitmap con el formato de píxel y el tamaño que desee. Asigne una matriz de bytes lo suficientemente grande como para contener los datos de píxeles, rellenarlos con los valores deseados y, a continuación, llamar a CopyFromBuffer para escribir los datos en el mapa de bits.

private SoftwareBitmap CreateGradientBitmap(int width, int height)
{
    // Create a new SoftwareBitmap programmatically.
    var softwareBitmap = new SoftwareBitmap(
        BitmapPixelFormat.Bgra8, width, height, BitmapAlphaMode.Premultiplied);

    // Allocate a byte array for the pixel data.
    int bytesPerPixel = 4; // BGRA8
    byte[] pixelData = new byte[width * height * bytesPerPixel];

    // Fill the bitmap with a gradient pattern.
    for (int row = 0; row < height; row++)
    {
        for (int col = 0; col < width; col++)
        {
            int pixelIndex = (row * width + col) * bytesPerPixel;

            // Blue channel: gradient left to right.
            pixelData[pixelIndex + 0] = (byte)((double)col / width * 255);
            // Green channel: gradient top to bottom.
            pixelData[pixelIndex + 1] = (byte)((double)row / height * 255);
            // Red channel: inverse diagonal gradient.
            pixelData[pixelIndex + 2] = (byte)(255 - (((double)(col + row)
                / (width + height)) * 255));
            // Alpha channel: fully opaque.
            pixelData[pixelIndex + 3] = 255;
        }
    }

    // Copy the pixel data into the SoftwareBitmap.
    softwareBitmap.CopyFromBuffer(pixelData.AsBuffer());

    return softwareBitmap;
}

Crear un objeto SoftwareBitmap a partir de una superficie de Direct3D

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

using Windows.Graphics.DirectX.Direct3D11;

Llame 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 Task<SoftwareBitmap> CreateBitmapFromSurfaceAsync(IDirect3DSurface surface)
{
    // Create a SoftwareBitmap from a Direct3D surface.
    SoftwareBitmap softwareBitmap =
        await SoftwareBitmap.CreateCopyFromSurfaceAsync(surface);

    return softwareBitmap;
}

Convertir un objeto SoftwareBitmap en un formato de píxel diferente

La clase SoftwareBitmap proporciona el método estático Convert, que le permite crear fácilmente un nuevo SoftwareBitmap que usa el formato de píxel y el modo alfa que especifique a partir de un softwareBitmap existente. Tenga 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.

private SoftwareBitmap ConvertBitmapPixelFormat(SoftwareBitmap softwareBitmap)
{
    // Convert the pixel format and alpha mode of the SoftwareBitmap.
    SoftwareBitmap convertedBitmap = SoftwareBitmap.Convert(
        softwareBitmap,
        BitmapPixelFormat.Bgra8,
        BitmapAlphaMode.Premultiplied);

    return convertedBitmap;
}

Transcodificación de un archivo de imagen

Puede transcodificar un archivo de imagen directamente desde un BitmapDecoder a un BitmapEncoder. Cree un IRandomAccessStream desde el archivo que se va a transcodificar. Cree un BitmapDecoder a partir de un flujo de entrada. Cree un nuevo InMemoryRandomAccessStream para que el codificador escriba en él y llame a BitmapEncoder.CreateForTranscodingAsync, pasándole la secuencia en memoria y el objeto decodificador. No se admiten las opciones de codificación al transcodificar; en su lugar, debe usar CreateAsync. Las propiedades del archivo de imagen de entrada que no establezca específicamente en el codificador se escribirán en el archivo de salida sin cambios. Llame a FlushAsync para que el codificador codifique en el flujo en memoria. Por último, busque la secuencia de archivos y la secuencia en memoria al principio y llame a CopyAsync para escribir la secuencia en memoria en la secuencia de archivos.

private async Task TranscodeImageFileAsync(StorageFile inputFile, StorageFile outputFile)
{
    using IRandomAccessStream inputStream =
        await inputFile.OpenAsync(FileAccessMode.Read);
    using IRandomAccessStream outputStream =
        await outputFile.OpenAsync(FileAccessMode.ReadWrite);

    // Create a decoder for the input file.
    BitmapDecoder decoder = await BitmapDecoder.CreateAsync(inputStream);

    // Create an encoder for transcoding to the output file.
    BitmapEncoder encoder =
        await BitmapEncoder.CreateForTranscodingAsync(outputStream, decoder);

    // Optionally apply transforms during transcoding.
    encoder.BitmapTransform.ScaledWidth = decoder.PixelWidth / 2;
    encoder.BitmapTransform.ScaledHeight = decoder.PixelHeight / 2;
    encoder.BitmapTransform.InterpolationMode = BitmapInterpolationMode.Fant;

    await encoder.FlushAsync();
}