创建、编辑和保存位图图像

本文介绍如何使用 BitmapDecoderBitmapEncoder 加载和保存图像文件,以及如何使用 SoftwareBitmap 对象来表示位图图像。

SoftwareBitmap 类是一种通用 API,可以从多个源(包括图像文件、WriteableBitmap 对象、Direct3D 图面和代码)创建。 SoftwareBitmap 允许在不同的像素格式和 alpha 模式之间轻松转换,并允许对像素数据进行低级别访问。 此外,SoftwareBitmap是Windows的多个功能使用的常见接口,包括:

  • CapturedFrame允许将相机捕获的帧作为 SoftwareBitmap

  • VideoFrame使你能够获取VideoFrameSoftwareBitmap表示形式。

  • FaceDetector允许检测SoftwareBitmap中的人脸。

本文中的示例代码使用以下命名空间中的 API。

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;

使用 BitmapDecoder 从图像文件创建 SoftwareBitmap

若要从文件创建 SoftwareBitmap,请获取包含图像数据的 StorageFile实例。 此示例使用 FileOpenPicker 允许用户选择图像文件。

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();
}

调用 StorageFile 对象的 OpenAsync 方法以获取包含图像数据的随机访问流。 调用静态方法 BitmapDecoder.CreateAsync以获取指定流的 BitmapDecoder 类的实例。 调用 GetSoftwareBitmapAsync以获取包含图像的 SoftwareBitmap 对象。

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;
}

使用 BitmapEncoder 将 SoftwareBitmap 保存到文件

若要将 SoftwareBitmap 保存到文件,请获取将图像保存到的 StorageFile 实例。 此示例使用 FileSavePicker 允许用户选择输出文件。

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();
}

调用 StorageFile 对象的 OpenAsync 方法以获取要向其写入映像的随机访问流。 调用静态方法 BitmapEncoder.CreateAsync以获取指定流的 BitmapEncoder 类的实例。 CreateAsync 的第一个参数是表示应用于对图像进行编码的编解码器的 GUID。 BitmapEncoder 类公开一个属性,其中包含编码器支持的每个编解码器的 ID,例如 JpegEncoderId

使用 SetSoftwareBitmap 方法设置要编码的图像。 可以设置 BitmapTransform 属性的值,以在对图像进行编码时应用基本转换。 IsThumbnailGenerated 属性确定缩略图是否由编码器生成。 请注意,并非所有文件格式都支持缩略图,因此,如果使用此功能,则应捕获在不支持缩略图时引发的不受支持的操作错误。

调用 FlushAsync ,使编码器将图像数据写入指定文件。

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();
    }
}

在创建 BitmapEncoder 时,可以通过创建新的 BitmapPropertySet 对象,并用一个或多个表示编码器设置的 BitmapTypedValue 对象填充它,来指定附加编码选项。 有关支持的编码器选项的列表,请参阅 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();
}

将 SoftwareBitmap 与 XAML 图像控件配合使用

若要使用 图像 控件在 XAML 页面中显示图像,请先在 XAML 页面中定义 图像 控件。

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

目前, 图像 控件仅支持使用 BGRA8 编码和预乘或无 alpha 通道的图像。 在尝试显示图像之前,请测试以确保其格式正确,如果不是,请使用 SoftwareBitmap 静态 Convert 方法将图像转换为受支持的格式。

创建新的 SoftwareBitmapSource 对象。 通过调用 SetBitmapAsync 来设置源对象的内容,并传入 SoftwareBitmap。 然后,可以将 Image 控件的 Source 属性设置为新创建的 SoftwareBitmapSource

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;
}

还可以使用 SoftwareBitmapSourceSoftwareBitmap 设置为 ImageBrushImageSource

从可写位图创建软件位图

可以通过调用 SoftwareBitmap.CreateCopyFromBuffer 并提供 WriteableBitmapPixelBuffer 属性来设置像素数据,从现有的 WriteableBitmap 创建 SoftwareBitmap。 第二个参数允许指定新创建的 SoftwareBitmap 的像素格式。 可以使用 WriteableBitmapPixelWidthPixelHeight 属性来指定新图像的尺寸。

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;
}

以编程方式创建或编辑 SoftwareBitmap

到目前为止,本主题已解决使用图像文件的问题。 还可以在代码中以编程方式创建新的 SoftwareBitmap ,并使用相同的技术来访问和修改 SoftwareBitmap 的像素数据。

使用 CopyFromBuffer 方法使用字节数组填充 SoftwareBitmap,并使用 CopyToBuffer 将像素数据复制到字节数组中以供读取或修改。 若要使用 AsBuffer 扩展方法将字节数组包装为 IBuffer,请包含 System.Runtime.InteropServices.WindowsRuntime 命名空间(本文顶部的 SnippetNamespaces using 指令中已包含该命名空间)。

使用所需的像素格式和大小创建新的 SoftwareBitmap 。 分配足够大的字节数组来保存像素数据,用所需的值填充它,然后调用 CopyFromBuffer 将数据写入位图。

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;
}

从 Direct3D 图面创建 SoftwareBitmap

若要从 Direct3D 图面创建 SoftwareBitmap 对象,必须包含 Windows。项目中的 Graphics.DirectX.Direct3D11 命名空间。

using Windows.Graphics.DirectX.Direct3D11;

调用 CreateCopyFromSurfaceAsync,从该表面创建新的 SoftwareBitmap。 如名称所示,新的 SoftwareBitmap 具有图像数据的单独副本。 对 SoftwareBitmap 的修改不会对 Direct3D 图面产生任何影响。

private async Task<SoftwareBitmap> CreateBitmapFromSurfaceAsync(IDirect3DSurface surface)
{
    // Create a SoftwareBitmap from a Direct3D surface.
    SoftwareBitmap softwareBitmap =
        await SoftwareBitmap.CreateCopyFromSurfaceAsync(surface);

    return softwareBitmap;
}

将 SoftwareBitmap 转换为其他像素格式

SoftwareBitmap 类提供了静态方法 Convert,可用于轻松创建一个新的 SoftwareBitmap,该地图使用从现有 SoftwareBitmap 指定的像素格式和 alpha 模式。 请注意,新创建的位图具有图像数据的单独副本。 对新位图的修改不会影响源位图。

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;
}

对图像文件进行转码

可以直接将图像文件从 BitmapDecoder 转码为 BitmapEncoder。 从要转码的文件创建 IRandomAccessStream。 从输入流创建新的 BitmapDecoder 。 创建一个新的 InMemoryRandomAccessStream 供编码器写入,然后调用 BitmapEncoder.CreateForTranscodingAsync,传入该内存流和解码器对象。 转码时不支持编码选项;应改用 CreateAsync。 未在编码器上专门设置的输入图像文件中的任何属性都将未更改地写入输出文件。 调用 FlushAsync ,使编码器编码为内存中流。 最后,将文件流和内存流都定位到开头,并调用 CopyAsync 将内存流写入文件流。

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();
}