使用 OpenCV 处理位图

本文介绍如何使用 SoftwareBitmap 类,该类用于表示图像,并且被许多不同的 Windows 运行时 API 使用。同时,本文还介绍如何利用开源计算机视觉库(OpenCV),这是一个本机代码库,提供多种图像处理算法。

本文中的示例将引导你创建可从 UWP 应用使用的本机代码 Windows 运行时组件,包括使用 C# 创建的应用。 此帮助程序组件将公开单个方法,Blur,这将使用 OpenCV 的模糊图像处理功能。 该组件实现私有方法,这些方法可获取指向基础图像数据缓冲区的指针,该缓冲区可以直接由 OpenCV 库使用,因此,可以轻松地扩展帮助程序组件来实现其他 OpenCV 处理功能。

注释

本文详述的 OpenCVHelper 组件所使用的技术要求处理的图像数据驻留在 CPU 内存中,而不是 GPU 内存中。 因此,对于允许请求图像的内存位置(如 MediaCapture 类)的 API,应指定 CPU 内存。

为 OpenCV 互操作创建辅助 Windows 运行时组件

1. 向解决方案中添加一个新的本机代码的 Windows 运行时组件项目

  1. 通过在解决方案资源管理器中右键单击解决方案并选择 “添加”>“新建项目”,将新项目添加到 Visual Studio 中的解决方案。
  2. Visual C++ 类别下,选择 Windows 运行时组件(通用 Windows)。 对于此示例,将项目命名为“OpenCVBridge”,然后单击“确定”
  3. “新建 Windows 通用项目”对话框中,选择应用的目标和最低操作系统版本,然后单击“确定”
  4. 右键单击解决方案资源管理器中自动生成的文件Class1.cpp,然后选择“ 删除”,当确认对话框弹出时,选择“ 删除”。 然后删除 Class1.h 头文件。
  5. 右键单击 OpenCVBridge 项目图标,然后选择“Add->类...”。在“添加类 对话框中,输入 类名 字段中的”OpenCVHelper“,然后单击 ”确定“。 将在后面的步骤中将代码添加到创建的类文件中。

2. 将 OpenCV NuGet 包添加到组件项目

  1. 在解决方案资源管理器中右键单击 OpenCVBridge 项目图标,然后选择 管理 NuGet 包...
  2. 当 NuGet 包管理器对话框打开时,选择 “浏览”选项卡并在搜索框中键入“OpenCV.Win”。
  3. 选择“OpenCV.Win.Core”,然后单击 安装。 在“预览”对话框中,单击 “确定”
  4. 使用相同的过程安装“OpenCV.Win.ImgProc”包。

注释

OpenCV.Win.Core 和 OpenCV.Win.ImgProc 不会定期更新,也不会通过应用商店符合性检查,因此这些包仅用于试验。

3. 实现 OpenCVHelper 类

将以下代码粘贴到 OpenCVHelper.h 头文件中。 此代码包括已安装的 CoreImgProc 包的 OpenCV 头文件,并声明了以下步骤中显示的三种方法。

#pragma once

// OpenCVHelper.h
#include <opencv2\core\core.hpp>
#include <opencv2\imgproc\imgproc.hpp>


namespace OpenCVBridge
{
    public ref class OpenCVHelper sealed
    {
    public:
        OpenCVHelper() {}

        void Blur(
            Windows::Graphics::Imaging::SoftwareBitmap^ input,
            Windows::Graphics::Imaging::SoftwareBitmap^ output);


    private:
        // helper functions for getting a cv::Mat from SoftwareBitmap
        bool TryConvert(Windows::Graphics::Imaging::SoftwareBitmap^ from, cv::Mat& convertedMat);
        bool GetPointerToPixelData(Windows::Graphics::Imaging::SoftwareBitmap^ bitmap,
            unsigned char** pPixelData, unsigned int* capacity);
    };
}

删除OpenCVHelper.cpp文件的现有内容,然后添加以下 include 指令。

#include "pch.h"
#include "OpenCVHelper.h"
#include "MemoryBuffer.h"

在 include 指令之后,使用 指令添加以下

using namespace OpenCVBridge;
using namespace Platform;
using namespace Windows::Graphics::Imaging;
using namespace Windows::Foundation;
using namespace Microsoft::WRL;
using namespace cv;

接下来,将方法 GetPointerToPixelData 添加到OpenCVHelper.cpp。 此方法使用 SoftwareBitmap,通过一系列转换,将像素数据表示为 COM 接口。通过该接口,我们可以获取一个指向底层数据缓冲区的指针,作为 char 数组。

首先,通过调用 LockBuffer获取包含像素数据的 BitmapBuffer,然后请求读/写缓冲区,以便 OpenCV 库可以修改该像素数据。 调用 CreateReference 以获取 IMemoryBufferReference 对象。 接下来,将 IMemoryBufferByteAccess 接口转换为所有 Windows 运行时类的基础接口 IInspectable,然后调用 QueryInterface 方法,以获取 IMemoryBufferByteAccess COM 接口,从而允许我们将像素数据缓冲区作为 字符数组 获取。 最后,通过调用 IMemoryBufferByteAccess::GetBuffer来填充 字符 数组。 如果此方法中的任何转换步骤失败,该方法将返回 false,指示无法继续进一步处理。

bool OpenCVHelper::GetPointerToPixelData(SoftwareBitmap^ bitmap, unsigned char** pPixelData, unsigned int* capacity)
{
    BitmapBuffer^ bmpBuffer = bitmap->LockBuffer(BitmapBufferAccessMode::ReadWrite);
    IMemoryBufferReference^ reference = bmpBuffer->CreateReference();

    ComPtr<IMemoryBufferByteAccess> pBufferByteAccess;
    if ((reinterpret_cast<IInspectable*>(reference)->QueryInterface(IID_PPV_ARGS(&pBufferByteAccess))) != S_OK)
    {
        return false;
    }

    if (pBufferByteAccess->GetBuffer(pPixelData, capacity) != S_OK)
    {
        return false;
    }
    return true;
}

接下来,添加如下所示的 TryConvert 方法。 此方法采用 SoftwareBitmap,并尝试将其转换为 Mat 对象,这是 OpenCV 用来表示图像数据缓冲区的矩阵对象。 此方法调用上面定义的 GetPointerToPixelData 方法,以获取像素数据缓冲区的 字符 数组表示形式。 如果成功,将调用 Mat 类的构造函数,传入从源 SoftwareBitmap 对象获取的像素宽度和高度。

注释

此示例将CV_8UC4常量指定为创建的 Mat 对象的像素格式。 这意味着传入此方法的 SoftwareBitmap 必须具有 BitmapPixelFormat 属性值 BGRA8,且预乘 alpha(等效于 CV_8UC4)才能使用此示例。

为了能够在同一个由 SoftwareBitmap 引用的数据像素数据缓冲区上进行进一步处理,而不是创建此缓冲区的副本,该方法返回了一个创建的 Mat 对象的浅拷贝。

bool OpenCVHelper::TryConvert(SoftwareBitmap^ from, Mat& convertedMat)
{
    unsigned char* pPixels = nullptr;
    unsigned int capacity = 0;
    if (!GetPointerToPixelData(from, &pPixels, &capacity))
    {
        return false;
    }

    Mat mat(from->PixelHeight,
        from->PixelWidth,
        CV_8UC4, // assume input SoftwareBitmap is BGRA8
        (void*)pPixels);
    
    // shallow copy because we want convertedMat.data = pPixels
    // don't use .copyTo or .clone
    convertedMat = mat;
    return true;
}

最后,此示例帮助程序类实现单个图像处理方法,Blur,该方法只使用上面定义的 TryConvert 方法检索表示源位图和模糊操作的目标位图的 Mat 对象,然后从 OpenCV ImgProc 库中调用 模糊 方法。 模糊 的另一个参数指定 X 和 Y 方向的模糊效果的大小。

void OpenCVHelper::Blur(SoftwareBitmap^ input, SoftwareBitmap^ output)
{
    Mat inputMat, outputMat;
    if (!(TryConvert(input, inputMat) && TryConvert(output, outputMat)))
    {
        return;
    }
    blur(inputMat, outputMat, cv::Size(15, 15));
}

使用辅助组件的简单 SoftwareBitmap OpenCV 示例

创建 OpenCVBridge 组件后,可以创建一个简单的 C# 应用,该应用使用 OpenCV 模糊 方法修改 SoftwareBitmap。 若要从 UWP 应用访问 Windows 运行时组件,必须先添加对该组件的引用。 在解决方案资源管理器中,右键单击 UWP 应用项目下的 引用 节点,然后选择 添加引用...。在“引用管理器”对话框中,选择“项目解决方案。 选中 OpenCVBridge 项目旁边的框,然后单击“确定”

下面的示例代码允许用户选择图像文件,然后使用 BitmapDecoder 生成图像的 SoftwareBitmap 表示形式。 有关使用 SoftwareBitmap的详细信息,请参阅 创建、编辑和保存位图图像

如本文前面所述,OpenCVHelper 类要求所有提供的 SoftwareBitmap 图像都使用具有预乘 alpha 值的 BGRA8 像素格式进行编码,因此,如果图像尚未采用此格式,示例代码将调用 Convert 将图像转换为预期格式。

接下来,将创建一个 SoftwareBitmap,用作模糊操作的目标。 输入图像属性用作构造函数的参数,以创建具有匹配格式的位图。

将创建 OpenCVHelper 的新实例,并调用 Blur 方法,传入源位图和目标位图。 最后,将创建一个 SoftwareBitmapSource,以便将输出图像分配给 XAML Image 控件。

除了默认项目模板包含的命名空间外,此示例代码还使用以下命名空间中的 API。

using Windows.Graphics.Imaging;
using Windows.Storage;
using Windows.Storage.Pickers;
using Windows.Storage.Streams;
using Windows.UI.Xaml.Media.Imaging;
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;
}

SoftwareBitmap inputBitmap;
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
    inputBitmap = await decoder.GetSoftwareBitmapAsync();
}

if (inputBitmap.BitmapPixelFormat != BitmapPixelFormat.Bgra8
            || inputBitmap.BitmapAlphaMode != BitmapAlphaMode.Premultiplied)
{
    inputBitmap = SoftwareBitmap.Convert(inputBitmap, BitmapPixelFormat.Bgra8, BitmapAlphaMode.Premultiplied);
}
    
SoftwareBitmap outputBitmap = new SoftwareBitmap(inputBitmap.BitmapPixelFormat, inputBitmap.PixelWidth, inputBitmap.PixelHeight, BitmapAlphaMode.Premultiplied);


var helper = new OpenCVBridge.OpenCVHelper();
helper.Blur(inputBitmap, outputBitmap);

var bitmapSource = new SoftwareBitmapSource();
await bitmapSource.SetBitmapAsync(outputBitmap);
imageControl.Source = bitmapSource;