65.9K
CodeProject 正在变化。 阅读更多。
Home

将 Windows Azure Face API 集成到 C++ 应用程序中

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.90/5 (14投票s)

2015年5月8日

CPOL

8分钟阅读

viewsIcon

33538

downloadIcon

575

了解如何使用 C++ REST SDK 将新的 Windows Azure 机器学习 API 集成到 C++ 应用程序中

引言

微软最近推出了一套名为“Project Oxford”的机器学习 API,其中包括面部检测和识别、语音识别和合成、视觉和自然语言理解等功能。面部 API 已通过一个小型应用程序得到演示,该应用程序病毒式传播:how-old.net。几天之内,数千万用户尝试了数亿张图片。即使年龄猜测功能并不那么好,但演示表明这些 API 可以轻松地集成到任何应用程序中。

Project Oxford 服务通过 RESTful API 公开,SDK 包括 .NET 和 Java(用于 Android)的 REST 包装器。文档提供了多种语言的示例:JavaScript、C#、PHP、Python、Ruby、Curl、Java、ObjC。由于在 C++ 中也可以使用 C++ REST SDK 来消费 RESTful 服务,因此我决定演示如何在 MFC 应用程序中集成面部 API。采用类似的方法也可以集成语音、视觉或自然语言理解。

注册服务

Project Oxford API 是 Windows Azure 服务,可免费使用(目前为测试版),但所有调用都必须使用分配给每个 Windows Azure 帐户的订阅密钥进行签名。要获取此密钥,您必须单独注册每个服务(面部、语音、视觉等)。

在 Windows Azure 门户中,您必须转到“市场”,选择“新建”,然后再次选择“市场”。

您将能够选择一个应用服务。搜索“面部 API”并选择它。

您必须选择一个计划(目前唯一可用的计划是免费的)、为服务命一个名以及其他信息。

在最后一页,查看购买并接受。

管理您的订阅密钥

服务配置完成后,您可以在“市场”下查看它。

使用“管理”命令查看(如有必要)不同应用服务的订阅密钥并重新生成。

从此处复制订阅密钥以与服务调用一起使用。

面部 API

Project Oxford API 此处 有文档记录,面部 API 的参考文档 此处 可用。

面部 API 提供的不只是面部检测功能。它允许将面部与人关联,将人分组,根据一个或多个输入面部识别组中的某人,等等。面部服务包括

  • 检测图像中的人脸
  • 验证两个面部是否代表同一个人
  • 通过面部识别组中的人
  • 根据面部相似性将面部列表分成组
  • 在面部列表中查找相似面部

在本文中,我们仅关注面部检测。面部检测是指识别图像中的人脸。它会返回人脸的位置、眼睛、鼻子和嘴的位置(称为面部地标),以及头部姿势、性别和年龄等附加属性。后两者是实验性功能,正如 how-old.net 所展示的,目前猜测年龄的准确性不高。

面部检测 API 有几个限制,包括

  • 支持的图像格式为 BMP、PNG、JPEG 和 GIF
  • 图像大小不得超过 4MB
  • 仅当人脸大于 36x36 像素且小于 4096x4096 像素时才能检测到人脸;但是,返回的最大人脸数量是 64,并且由于各种技术原因,并非所有面部都可能被检测到

可以通过 URL 指定图像,其内容类型为 JSON,或者将图像作为请求的一部分上传,内容类型为 application/octect-stream 来检测图像中的人脸。在本文中,我们将使用第二种选项。

检测 API 此处 有文档记录。对于下面的图像,当请求分析年龄、性别和头部姿势时(为简化起见忽略面部地标),服务会返回如下所示的 JSON 数据。

[
  {
    "faceId":"4ad67da7-c86b-4dc8-8565-a224cda71253",
    "faceRectangle":{
      "top":47,
      "left":53,
      "width":58,
      "height":58
    },
    "attributes":{
      "headPose":{
        "pitch":0.0,
        "roll":2.4,
        "yaw":-3.4
      },
      "gender":"male",
      "age":32
    }
  }
]

顺便说一句,在这种情况下,面部分析仅将年龄估错了几年(比实际年龄大),我预计这可能在合理的误差范围内。但是,如果图片尺寸较小,分析结果会不同,这次会更接近实际年龄(拍照时)。

演示 C++ 应用

为了演示这些 API 在 C++ 应用程序中的用法,我原型开发了一个简单的 MFC 应用程序,您可以在其中加载图像,运行面部检测,然后在图像上显示检测到的面部、年龄和性别。男性面部将用蓝色矩形标识,女性面部用红色矩形标识。

该应用程序非常简单:它允许您打开一个 BMP 或 JPEG 图像,然后将其绘制在窗口的客户区。没有调整大小或滚动功能,因为这只是一个演示。如果您有兴趣,可以查看附件的源代码,了解加载和绘制是如何完成的。

为了使用 Face REST API,我们需要使用 C++ REST SDK。它作为 NuGet 包提供,因此我使用了 Visual Studio 的 NuGet 包管理器来搜索并安装它。

请注意,cpprest 是一个聚合包,它将所有针对不同平台的独立包组合在一起。总大小超过 1GB,因此您可能只想下载 cpprestsdk.v120.windesktop.msvcstl.dyn.rt-dyn,这是您使用 Visual Studio 2013 为 Windows 开发所需的内容。(有关更多详细信息,请参阅 C++ Rest SDK 2.5.0 发布说明。)

C++ REST SDK 中我们将使用几个组件:http_client(用于连接 HTTP 服务)、json、异步文件流和任务。为了使用它们,我们需要包含几个头文件。

#include "cpprest\json.h"
#include "cpprest\http_client.h"
#include "cpprest\filestream.h"

using namespace concurrency;
using namespace concurrency::streams;

using namespace web;
using namespace web::http;
using namespace web::http::client;

为了获取图像中面部的分析结果,我们需要执行以下操作

  • 将图像加载到文件流中。
  • 当流可用时,使用 http_client 对象向检测 API 发送 HTTP POST 请求。
  • 当响应可用时,从中提取 JSON 内容(如果成功)。
  • 当结果 JSON 可用时,解析它并使用结果在面部上绘制矩形和年龄。

上述描述表示异步处理,这可以通过 PPL 任务编程模型实现。我们启动一个返回任务的操作,并为每个任务设置一个延续,该延续在当前任务完成后执行。总而言之,代码如下所示

void detect_faces(
  std::function<void(web::json::value)> success,
  std::function<void(const char*)> error,
  utility::string_t const & filename,
  utility::string_t const & subscriptionKey,
  bool const analyzesFaceLandmarks,
  bool const analyzesAge,
  bool const analyzesGender,
  bool const analyzesHeadPose)
{
  file_stream<unsigned char>::open_istream(filename)
     .then([=](pplx::task<basic_istream<unsigned char>> previousTask)
     {
        try
        {
           auto fileStream = previousTask.get();

           auto client = http_client{U("https://api.projectoxford.ai/face/v0/detections")};

           auto query = uri_builder()
              .append_query(U("analyzesFaceLandmarks"), 
                            analyzesFaceLandmarks ? "true" : "false")
              .append_query(U("analyzesAge"), analyzesAge ? "true" : "false")
              .append_query(U("analyzesGender"), analyzesGender ? "true" : "false")
              .append_query(U("analyzesHeadPose"), analyzesHeadPose ? "true" : "false")
              .append_query(U("subscription-key"), subscriptionKey)
              .to_string();

           client
              .request(methods::POST, query, fileStream)
              .then([fileStream, success](pplx::task<http_response> previousTask)
              {
                 fileStream.close();

                 return previousTask.get().extract_json();
              })
              .then([success, error](pplx::task<json::value> previousTask)
              {
                 try
                 {
                    success(previousTask.get());
                 }
                 catch(http_exception const & e)
                 {
                    error(e.what());
                 }
              });
        }
        catch(std::system_error const & e)
        {
           error(e.what());
        }
     });
}

detect_faces() 函数接受几个参数

  • 两个回调函数,一个用于成功,我们将传递回来的 JSON 值,另一个用于错误,我们将传递一个表示错误消息的 string
  • 要分析的图像在磁盘上的路径
  • 订阅密钥
  • 可选参数,指示要执行哪些附加分析

可以按如下方式调用该函数

auto doc = GetDocument();
auto path = doc->GetImagePath();
auto stdpath = path.GetBuffer(path.GetLength());
path.ReleaseBuffer();

auto error = [](const char* error){
  std::wostringstream ss;
  ss << error << std::endl;
  AfxMessageBox(ss.str().c_str()); };

auto werror = [](const wchar_t* error){
  std::wostringstream ss;
  ss << error << std::endl;
  AfxMessageBox(ss.str().c_str()); };

auto success = [this, werror](web::json::value object) {
  m_faces = faceapi::parse_face_result(object, werror);
  this->Invalidate();
};

faceapi::detect_faces(success, error, stdpath, 
         U("your-subscription-key"), false, true, true, true);  

请注意,您必须使用注册应用服务时获得的订阅密钥。

如果调用成功,我们将收到一个 JSON 响应,其外观类似于上面显示的示例。如果函数失败,我们也会收到一个 JSON 值,其中包含错误代码和消息。这样的消息可能看起来像这样

{
  "code":"InvalidImageSize",
  "message":"Image size is too small or too big."
}

parse_face_result() 函数解析我们收到的 JSON 值,以提取分析结果或错误消息,并返回一个 face 对象集合。

std::vector<faceapi::face> parse_face_result(
  web::json::value object,
  std::function<void(wchar_t const *)> error)
{
  std::vector<faceapi::face> faces;

  if(!object.is_null())
  {
     if(object.has_field(U("code")))
     {
        auto message = object.at(U("message")).as_string();
        error(message.c_str());
     }
     else 
     {
        auto arr = object.as_array();
        for(auto const & obj : arr)
        {
           try
           {
              auto face = faceapi::face{};
              face.faceId = obj.at(U("faceId")).as_string();

              auto const & fr = obj.at(U("faceRectangle"));
              face.faceRectangle.width = fr.at(U("width")).as_integer();
              face.faceRectangle.height = fr.at(U("height")).as_integer();
              face.faceRectangle.top = fr.at(U("top")).as_integer();
              face.faceRectangle.left = fr.at(U("left")).as_integer();

              auto const & attr = obj.at(U("attributes")).as_object();
              if(!attr.empty())
              {
                 face.attributes.age = attr.at(U("age")).as_integer();
                 face.attributes.gender = (attr.at(U("gender")).as_string() == U("male")) ? 
                                           faceapi::gender::male : faceapi::gender::female;

                 auto const & hpose = attr.at(U("headPose")).as_object();
                 if(!hpose.empty())
                 {
                    face.attributes.headPose.pitch = hpose.at(U("pitch")).as_double();
                    face.attributes.headPose.roll = hpose.at(U("roll")).as_double();
                    face.attributes.headPose.yaw = hpose.at(U("yaw")).as_double();
                 }
              }

              faces.push_back(face);
           }
           catch(std::exception const &)
           {
           }
        }
     }
  }

  return faces;
}

face 类型和其他类型定义如下

namespace faceapi
{
   struct face_rectangle
   {
      int width = 0;
      int height = 0;
      int left = 0;
      int top = 0;
   };

   struct face_landmark
   {
      double x = 0;
      double y = 0;
   };

   struct face_landmarks
   {
      face_landmark pupilLeft;
      face_landmark pupilRight;
      face_landmark noseTip;
      face_landmark mouthLeft;
      face_landmark mouthRight;
      face_landmark eyebrowLeftOuter;
      face_landmark eyebrowLeftInner;
      face_landmark eyeLeftOuter;
      face_landmark eyeLeftTop;
      face_landmark eyeLeftBottom;
      face_landmark eyeLeftInner;
      face_landmark eyebrowRightInner;
      face_landmark eyebrowRightOuter;
      face_landmark eyeRightInner;
      face_landmark eyeRightTop;
      face_landmark eyeRightBottom;
      face_landmark eyeRightOuter;
      face_landmark noseRootLeft;
      face_landmark noseRootRight;
      face_landmark noseLeftAlarTop;
      face_landmark noseRightAlarTop;
      face_landmark noseLeftAlarOutTip;
      face_landmark noseRightAlarOutTip;
      face_landmark upperLipTop;
      face_landmark upperLipBottom;
      face_landmark underLipTop;
      face_landmark underLipBottom;
   };

   struct head_pose 
   {
      double roll = 0;
      double yaw = 0;
      double pitch = 0;
   };

   enum class gender 
   {
      female,
      male
   };

   struct face_attributes 
   {
      int age = 0;
      gender gender = gender::female;
      head_pose headPose;
   };

   struct face
   {
      std::wstring faceId;
      face_rectangle faceRectangle;
      face_attributes attributes;
   };

   void detect_faces(
      std::function<void(web::json::value)> success, 
      std::function<void(char const *)> error,
      utility::string_t const & filename, 
      utility::string_t const & subscriptionKey,
      bool const analyzesFaceLandmarks = false,
      bool const analyzesAge = false,
      bool const analyzesGender = false,
      bool const analyzesHeadPose = false);

   std::vector<face> parse_face_result(
      web::json::value object,
      std::function<void(wchar_t const *)> error);
}

有了面部信息后,我们就可以在图像上绘制矩形和年龄文本。这在视图的 OnDraw() 方法中完成,但如果您想了解它是如何完成的,请查看源代码,因为它对于本文的目的并不重要。

下图显示了检测在包含许多人的图像上是如何工作的(图像来源:wikipedia

重构代码

detect_faces() 函数将一些函数作为参数,在成功或失败时调用它们。这可以重构为实际返回一个任务,然后我们为该任务设置一个延续,以便在结果可用时执行某些操作。此外,异常处理可以移出此任务到最后的延续,因为从任务体中逃逸的任何异常都会被捕获并在最后一个任务的 wait()get() 调用中重新抛出。因此,detect_faces() 函数可以重新实现如下

pplx::task<web::json::value> detect_faces_async(
  utility::string_t const & filename,
  utility::string_t const & subscriptionKey,
  bool const analyzesFaceLandmarks,
  bool const analyzesAge,
  bool const analyzesGender,
  bool const analyzesHeadPose,
  pplx::cancellation_token const & token)
{
  return file_stream<unsigned char>::open_istream(filename)
     .then([=](pplx::task<basic_istream<unsigned char>> previousTask)
     {
        if(!token.is_canceled())
        {
           auto fileStream = previousTask.get();

           auto client = http_client{U("https://api.projectoxford.ai/face/v0/detections")};

           auto query = uri_builder()
              .append_query(U("analyzesFaceLandmarks"), 
                            analyzesFaceLandmarks ? "true" : "false")
              .append_query(U("analyzesAge"), analyzesAge ? "true" : "false")
              .append_query(U("analyzesGender"), analyzesGender ? "true" : "false")
              .append_query(U("analyzesHeadPose"), analyzesHeadPose ? "true" : "false")
              .append_query(U("subscription-key"), subscriptionKey)
              .to_string();

           return client
              .request(methods::POST, query, fileStream, token)
              .then([fileStream](pplx::task<http_response> previousTask)
           {
              fileStream.close();

              return previousTask.get().extract_json();
           });
        }
        
        return pplx::task_from_result(json::value());
     });
}

在这种情况下,我们还需要将调用代码重构为如下

auto doc = GetDocument();
auto path = doc->GetImagePath();
auto stdpath = path.GetBuffer(path.GetLength());
path.ReleaseBuffer();

auto error = [](const char* error){
  std::wostringstream ss;
  ss << error << std::endl;
  AfxMessageBox(ss.str().c_str()); };

auto werror = [](const wchar_t* error){
  std::wostringstream ss;
  ss << error << std::endl;
  AfxMessageBox(ss.str().c_str()); };

auto success = [this, werror](web::json::value object) {
  m_faces = faceapi::parse_face_result(object, werror);
  this->Invalidate();
};

faceapi::detect_faces_async(stdpath, U("your-subscription-key"), false, true, true, true)
  .then([this, werror, error](pplx::task<web::json::value> previousTask) {
     try 
     {
        m_faces = faceapi::parse_face_result(previousTask.get(), werror);
        this->Invalidate();
     }
     catch(std::exception const & e)
     {
        error(e.what());
     }
  });

对于通过 URL 指定的图像中的面部检测,也可以实现类似的实现。在这种情况下,我们不再需要从磁盘加载文件,而是将 JSON 值传递到请求正文中。

pplx::task<web::json::value< detect_faces_from_url_async(
  utility::string_t const & url,
  utility::string_t const & subscriptionKey,
  bool const analyzesFaceLandmarks,
  bool const analyzesAge,
  bool const analyzesGender,
  bool const analyzesHeadPose,
  pplx::cancellation_token const & token)
{
  auto client = http_client{U("https://api.projectoxford.ai/face/v0/detections")};

  auto query = uri_builder()
     .append_query(U("analyzesFaceLandmarks"), analyzesFaceLandmarks ? "true" : "false")
     .append_query(U("analyzesAge"), analyzesAge ? "true" : "false")
     .append_query(U("analyzesGender"), analyzesGender ? "true" : "false")
     .append_query(U("analyzesHeadPose"), analyzesHeadPose ? "true" : "false")
     .append_query(U("subscription-key"), subscriptionKey)
     .to_string();

  auto content = web::json::value {};
  content[U("url")] = web::json::value(url);

  return client
     .request(methods::POST, query, content, token)
     .then([](pplx::task<http_response> previousTask)
  {
     return previousTask.get().extract_json();
  });
}

结论

Project Oxford 提供了一系列机器学习 API,这些 API 是免费的(尽管目前仍处于测试阶段),并且可以轻松集成到您的应用程序中。在本文中,我展示了开始使用这些 API 所需的操作,以及如何使用 C++ REST SDK 在 C++ 应用程序(带 MFC)中消费一些面部 API。

历史

  • 2015 年 5 月 8 日:初始版本
© . All rights reserved.