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






4.90/5 (14投票s)
了解如何使用 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
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 日:初始版本