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

本文介绍了如何使用 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 Windows.UI.Xaml.Media.Imaging;

使用 BitmapDecoder 从图像文件中创建 SoftwareBitmap

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

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

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

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

将 SoftwareBitmap 保存到具有 BitmapEncoder 的文件

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

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

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

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

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

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


    }
}

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

使用带有 XAML 图像控件的 SoftwareBitmap

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

<Image x:Name="imageControl"/>

当前,Image 控件仅支持使用 BGRA8 编码和预乘 alpha 或不带 alpha 的通道的图像。 在尝试显示某个图像前,进行测试以确保它具有正确的格式,如果没有,则使用 SoftwareBitmap 静态 Convert 方法将该图像转换为受支持的格式。

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

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;

还可以使用 SoftwareBitmapSourceSoftwareBitmap 设置为 ImageBrushImageSource

从 WriteableBitmap 创建 SoftwareBitmap

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

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

以编程方式创建或编辑 SoftwareBitmap

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

SoftwareBitmap 使用 COM 互操作来公开包含像素数据的原始缓冲区。

若要使用 COM 互操作,必须在项目中包含对 System.Runtime.InteropServices 命名空间的引用。

using System.Runtime.InteropServices;

通过在命名空间内添加以下代码,初始化 IMemoryBufferByteAccess COM 接口。

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

使用所需的像素格式和大小创建一个新的 SoftwareBitmap。 或者,使用要编辑其像素数据的现有 SoftwareBitmap。 调用 SoftwareBitmap.LockBuffer 以获取表示像素数据缓冲区的 BitmapBuffer 类的实例。 将 BitmapBuffer 转换为 IMemoryBufferByteAccess COM 接口,然后调用 IMemoryBufferByteAccess.GetBuffer 以使用数据填充字节数组。 使用 BitmapBuffer.GetPlaneDescription 方法获取 BitmapPlaneDescription 对象,该对象将有助于针对每个像素将偏移计算到缓冲区中。

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

此方法访问以 Windows 运行时类型为基础的原始缓冲区,因此必须使用 unsafe 关键字进行声明。 你还必须使用 Microsoft Visual Studio 配置你的项目,以允许通过以下操作编译不安全的代码:打开项目的“属性”页面、单击“生成”属性页,然后选中“允许不安全代码”复选框。

从 Direct3D 图面创建 SoftwareBitmap

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

using Windows.Graphics.DirectX.Direct3D11;

调用 CreateCopyFromSurfaceAsync 以从图面创建一个新的 SoftwareBitmap。 顾名思义,新的 SoftwareBitmap 具有图像数据的单独副本。 修改 SoftwareBitmap 将不会对 Direct3D 图面产生任何影响。

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

将 SoftwareBitmap 转换为不同的像素格式

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

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

转换图像文件的代码

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

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