关注公众号,发现CV技术之美
导言:
? ? ? 本文将会实测七牛云的人脸识别 API,从应用开发者的角度来验证这套 API 的可用性。文中会选用人脸比对、权威人脸比对和人脸检测这3个API来进行测试。
本文使用 PHP 作为编程语言,需要安装 Composer,这是 PHP 开发中广泛使用的依赖库管理工具,相当于 JavaScript 里的 npm。七牛云提供的官方 PHP SDK 也是通过 Composer 来安装的。
为了尽可能排除不相关的细节,代码是以命令行方式来运行的,每个 API 对应一个 PHP 代码文件。
预备工作
1. 建立工作目录并安装依赖
首先建立一个目录存放项目代码,并进入该目录,后续所有的操作都在该目录下:
mkdir?qiniu-api
cd?qiniu-api
接下去用 Composer 安装依赖包,除了安装七牛云官方提供的 SDK 外,我们还要安装一个 HTTP 库,用来发送 API 请求。虽然用原生 PHP 函数也能发送 API 请求,但用库的好处是库为我们处理了很多细节,让我们不用为此操心,而且库封装后的发送代码很简洁,即便不熟悉 PHP 的开发者也容易读懂。在本文中,我们选用 Symfony 的 HTTP Client 库来发送 API 请求。
用以下的命令来安装上述的 2 个库:
composer?require?qiniu/php-sdk
composer?require?symfony/http-client
安装成功后,目录内会生成 composer.json 和 composer.lock 两个文件,库的代码放在生成的 vendors 目录下。我们无需关心这些生成的代码。
2. 获取七牛云账号的密钥
注册或登录七牛云的账户,在“个人中心”的“密钥管理”里,七牛云会自动生成两对 Access Key 和 Secret Key,选择其中一对,复制出来。
人脸比对
这个 API 主要的功能是接收 2 张发送过来的照片,根据照片上人脸的相似度给出一个分值,用以判断 2 张照片中的是否为同一人。
新建一个名为 face-compare.php 的文件,这里的代码主要分为 3 部分,第 1 部分校验命令行传入的参数并返回结构化后的命令行参数数据,第 2 部分根据命令行参数来准备发送到 API 的请求数据,第 3 部分为实际调用 API。这 3 部分会分别放在 3 个函数中,以便于理解。为了强调 API 本身的调用流程,我们对代码不会做过多的封装,完善的错误处理也不会涉及。
在开始编写那些函数之前,先在代码里做一些预备工作,具体请看代码里的注释:
<?php
//?解决自动加载问题,之后引用的类会被自动加载
require_once?__DIR__?.?'/vendor/autoload.php';
//?指定使用特定命名空间下的类
use?Qiniu\Auth;
use?Symfony\Component\HttpClient\HttpClient;
//?定义两个常量,分别对应上文提到的七牛云里复制出的Access?Key和Secret?Key,这里需要替换成你实际的key
//?实际项目中,这些值可能是从配置文件或环境变量中读取
define('ACCESS_KEY',?'your_own_access_key');
define('SECRET_KEY',?'your_own_secret_key');
接着定义 3 个与人脸比对 API 相关的变量,这些变量的值都是文档里定义的。
//?API的调用地址
$apiUrl?=?'https://face-compare.qiniuapi.com/facecompare';
//?提交到API时用到的HTTP方法,本文中涉及的3个API都是该方法
$method?=?'POST';
//?发送到API的请求body的mime类型,本文中涉及的3个API都是使用该类型
$contentType?=?'application/json';
我们定义第一个函数,用来对参数做最基本的检验:
/**
?*?@param?int????$argc?命令行参数的数量
?*?@param?array??$argv?存储命令行参数的数组
?*?@return?array?命令行里得到的数据
?*/
function?getInputs(int?$argc,?array?$argv):?array
{
????//?确保参数数量正确(参数分别是:php脚本名、第1张图片路径、第2张图片路径,所以总共是3个参数)
????if?($argc?!==?3)?{
????????throw?new?\Exception('请提供2张图片的路径。');
????}
????//?确保传入的2张图片路径对应真实存在的文件
????if?(!file_exists($argv[1])?||?!file_exists($argv[2]))?{
????????throw?new?\Exception('请确保2张图片文件存在。');
????}
????//?以数组形式返回2张图片的路径
????return?[
????????'image_1'?=>?$argv[1],
????????'image_2'?=>?$argv[2],
????];
}
然后定义第 2 个函数,用于生成发送 API 的 HTTP 请求的 header 和 body。按照文档,必须的 header 有 2个,一个是 Content-Type,另一个是 Authorization。Content-Type 固定为 application/json;而 Authorization 的值需要根据七牛云的签名算法来生成,相对比较复杂,好在官方提供的 SDK 里提供了 Auth 类,可以直接调用来生成这个 header 的值。
/**
?*?@param?string?$apiUrl?API地址
?*?@param?string?$method?调用API的HTTP方法
?*?@param?string?$contentType?API请求body的mime类型?
?*?@param?array??$inputs?前一个函数返回的数组
?*?@return?array?发送HTTP请求用到的headers和body
?*/
function?composeRequestOptions(string?$apiUrl,?string?$method,?string?$contentType,?array?$inputs):?array?{
????//?对2张图片内容进行base64编码,放入一个文档要求的结构,并生成json格式的数据作为请求的body
????$body?=?json_encode([
????????'data_uri_a'?=>?encodeMediaToBase64($inputs['image_1']),
????????'data_uri_b'?=>?encodeMediaToBase64($inputs['image_2']),
????]);
????//?调用官方SDK来生成凭证的HTTP头
????$auth?=?new?Auth(ACCESS_KEY,?SECRET_KEY);
????//?$authHeader的值为诸如['Authorization'?=>?'Qiniu?QNJi_bYJlmO5LeY08FfoNj9w_r7...']的数组
????$authHeader?=?$auth->authorizationV2($apiUrl,?$method,?$body,?$contentType);
????//?请求头里除了包含mime类型,也要包含凭证,所以这里把两个头合并在一个数组里
????$headers?=?array_merge(
??????['Content-Type'?=>?$contentType],
??????$authHeader
????);
????//?返回包含header和body的数组
????return?[
????????'headers'?=>?$headers,
????????'body'?=>?$body,
????];
}
这里我们调用了 Auth 类的 authorizationV2 方法,传入 HTTP 请求里用到的一些参数(API 地址、方法、body 和 mime 类型)即可得到凭证。如果查看 SDK 的代码,会看到 Auth 类还有一个 authorization 方法(签名略有不同),经实测该方法也能返回正确的凭证。
另外,这里也用到了一个工具函数 encodeMediaToBase64,它的作用是根据参数里的路径读取文件内容,并编码成base64格式:
/**
?*?@param?string??$mediaPath?文件在本地的路径
?*?@return?string?base64字符串
?*/
function?encodeMediaToBase64(string?$mediaPath):?string?{
????$mediaData?=?file_get_contents($mediaPath);
????return?base64_encode($mediaData);
}
有了第 2 个方法返回的 header 和 body,我们就能发送调用 API 请求了,这是第 3 个函数的作用:
/**
?*?@param?string?$apiUrl?API地址
?*?@param?string?$method?调用API的HTTP方法
?*?@param?array??$requestOptions?包含请求header和body
?*?@return?array?API返回的应答body,转成了数组格式
?*/
function?callApi(string?$apiUrl,?string?$method,?array?$requestOptions):?array?{
????//?创建发送客户端
????$client?=?HttpClient::create();
????//?发送请求
????$response?=?$client->request($method,?$apiUrl,?$requestOptions);
????//?这里获得的应答是json格式
????$content?=?$response->getContent();
????//?把json转为数组并返回
????return?json_decode($content,?true);
}
当以上 3 个函数完成后,只需要调用它们即可,最后在标准输出中打印结果:
$inputs?=?getInputs($argc,?$argv);
$requestOptions?=?composeRequestOptions($apiUrl,?$method,?$contentType,?$inputs);
$responseBody?=?callApi($apiUrl,?$method,?$requestOptions);
//?打印输出API返回的结果
print_r($responseBody);
第 1 个函数调用里用到的 和argv 是 PHP 在命令行运行时自动提供的,分别对应命令行的参数个数和参数的具体值构成的数组,这和 C 语言里 main 函数里的参数相似。
笔者在网上找了 5 张国外明星的照片,其中 2 张为 Matt Damon 不同年龄的照片,以及 1 张 Leonardo Dicaprio 和 1 张 Morgan Freeman 的照片,共 4 张图作为测试照片,放在当前目录下的 images 目录里,图片如下:
Matt Damon-young
Matt?Damon-old
Leonardo?Dicaprio
Morgan Freeman
实际运行结果如下:
# Matt Damon的2张图片比对,相似度得分为100:
php?face-compare.php?images/matt-damon-young.jpg?images/matt-damon-old.jpg
Array
(
????[session_id]?=>?20210915084600UrzDdFAvk5
????[Errorcode]?=>?0
????[Errormsg]?=>?OK
????[similarity]?=>?100
)
# Matt Damon和Leonardo Dicaprio图片比对,相似度得分68:
php?face-compare.php?images/matt-damon-young.jpg?images/leonardo-dicaprio.jpg
Array
(
????[session_id]?=>?20210915084715c8YXlSJrK6
????[Errorcode]?=>?0
????[Errormsg]?=>?OK
????[similarity]?=>?68
)
#?如果将Matt Damon和Morgan Freeman对比,两者除了相貌外,年龄肤色也差别很大,得分为0:
php?face-compare.php?images/matt-damon-young.jpg?images/morgan-freeman.jpg
Array
(
????[session_id]?=>?20210915084829S6leTB1V2R
????[Errorcode]?=>?0
????[Errormsg]?=>?OK
????[similarity]?=>?0
)
以上结果和笔者预期比较符合。
权威人脸比对
该 API 用于将发送过来的姓名、身份照号和照片这些身份信息来和官方的身份证数据库进行比对,从而判别提供的身份信息是否属实。
新建一个名为 face-hdphoto-auth.php 的文件,代码的结构和人脸比对完全一样,也是分为 3 部分,差别主要是 API 地址和命令行接收的参数不同。为了简洁起见,不再把完整的代码列出,只是列出不一样的部分。
一开始的 3 个变量中,API 地址改为如下,其它 2 个变量值不变:
$apiUrl?=?'https://face-hdphotoauth.qiniuapi.com/hdphoto_auth';
第 1 个函数仍然用于检查命令行参数,这里需要接收 3 个输入:姓名、身份照号和照片路径。这个函数里,我们对输入做一些简单的检查,然后返回数据。
function?getInputs(int?$argc,?array?$argv):?array
{
????if?($argc?!==?4)?{
????????throw?new?\Exception('请提供姓名、身份证号和1张照片的路径。');
????}
????//?检查18位身份证号的格式
????if?(!preg_match('/^\d{17}[\dX]$/',?$argv[2]))?{
????????throw?new?\Exception('请提供18位身份证号。');
????}
????if?(!file_exists($argv[3]))?{
????????throw?new?\Exception('请确保1张照片文件存在。');
????}
????//?以数组形式返回姓名、身份照号和图片路径
????return?[
????????'name'?=>?$argv[1],
????????'id'?=>?$argv[2],
????????'image'?=>?$argv[3],
????];
}
第 2 个函数里只有一开始的 $body 数据有所不同。
function?composeRequestOptions(string?$apiUrl,?string?$method,?string?$contentType,?array?$inputs):?array?{
????$body?=?json_encode([
????????'realname'?=>?$inputs['name'],
????????'idcard'?=>?$inputs['id'],
????????'data_uri'?=>?encodeMediaToBase64($inputs['image']),
????]);
??//?其余部分和人脸比对里的函数完全一致
}
第 3 个函数和最后的函数调用部分完全一样。
通过以下方式来调用此 PHP 脚本:
#?用真实存在的名字和身份证号代替下面的数据,并使用此人的照片
php face-hdphoto-auth.php 张三?000000000000000000 images/photo-for-id-auth.jpg
笔者用自己的身份信息,再加上两张自己的照片,做了实测比对:
#?使用了自己一张近期的照片,相似度为96.92
Array
(
????[session_id]?=>?20210914013725tA3iTEoS09
????[Errorcode]?=>?0
????[Errormsg]?=>?OK
????[similarity]?=>?96.92
)
#?使用了自己一张十几年前的照片,相似度为81.88
Array
(
????[session_id]?=>?20210914013752cJy9ZYtMge
????[Errorcode]?=>?0
????[Errormsg]?=>?OK
????[similarity]?=>?81.88
)
笔者也故意测试了一些错误情况,如果身份证号存在对但名字不对,API 会正常返回,但 similarity 字段数值为 0:
#?故意把名字写错,但身份照号是真实存在的,会得到如下数据:
Array
(
????[session_id]?=>?20210914015325r6i8WjT2KB
????[Errorcode]?=>?0
????[Errormsg]?=>?OK
????[similarity]?=>?0
)
如果名字和身份证号都正确,但照片不是本人时,经实测有 2 种情况:如果 API 判断照片与本人有一定程度相符,会返回正常结果,但 similarity 里的份数较低;如果系统判断出压根不是一个人,直接返回了 400 错误。
笔者也尝试使用了一张用软件随便做的图片(不含人脸),得到了 Errormsg 为 PHOTO_NOT_ACCEPTED 的应答,与文档描述相符。
人脸检测
该 API 接收一张图片,来检查图片中出现的人脸,允许图片里有多个脸。
新建一个名为 face-detect.php 的文件,代码的结构和上面两个文件对完全一样,也是分为 3 部分,这里也只列出不同的部分。
一开始的 3 个变量中,API 地址改为如下,其它 2 个变量值不变:
$apiUrl?=?'https://face-detect.qiniuapi.com/facedetect';
第 1 个函数仍然用于检查命令行参数,这里只需要接收 1 个输入:图片文件的地址。
function?getInputs(int?$argc,?array?$argv):?array
{
????if?($argc?!==?2)?{
????????throw?new?\Exception('请提供1张图片的路径。');
????}
????if?(!file_exists($argv[1]))?{
????????throw?new?\Exception('请确保1张图片文件存在。');
????}
????return?[
????????'image'?=>?$argv[1],
????];
}
第 2 个函数里只有一开始的 $body 数据有所不同。
function?composeRequestOptions(string?$apiUrl,?string?$method,?string?$contentType,?array?$inputs):?array?{
????$body?=?json_encode([
????????'image_b64'?=>?encodeMediaToBase64($inputs['image']),
????]);
??//?其余部分和人脸比对里的函数完全一致
}
第 3 个函数和最后的函数调用部分完全一样。
我们用一张网上找的家庭合照来测试:
php?face-detect.php?images/family.jpg
返回的结果里信息量比较大,以下是一个节选的版本:
Array
(
????[num_face]?=>?3
????[rotate_angle]?=>?0
????[face]?=>?Array
????????(
????????????[0]?=>?Array
????????????????(
????????????????????[score]?=>?99.48
????????????????????[x]?=>?147
????????????????????[y]?=>?74
????????????????????[width]?=>?162
????????????????????[height]?=>?162
????????????????????[pitch]?=>?-1.87
????????????????????[yaw]?=>?-1.55
????????????????????[roll]?=>?2.25
????????????????????[eye]?=>?0
????????????????????[mouth]?=>?0
????????????????????[blur]?=>?100
????????????????????[gender]?=>?M
????????????????????[age]?=>?35
????????????????????[illumination]?=>?72.84
????????????????????[face_shape]?=>?Array(...)
????????????????????[completeness]?=>?100
????????????????????[area]?=>?26103
????????????????????[facesize]?=>?100
????????????????????[quality]?=>?92.58
????????????????????[face_aligned_b64]?=>?...
????????????????)
????????????[1]?=>?Array(...)
????????????[2]?=>?Array(...)
????????)
?
????[errorcode]?=>?0
????[errormsg]?=>?OK
????[session_id]?=>?20210917012718zjLc9tSBNu
)
可以看到,API 正确判断出了人脸的个数、每个人的性别和年龄、甚至每个脸部在三维中的旋转角度(当然,年龄只要比较接近就行,另外小孩子的性别可能更容易误判,但这也很正常);具体到每一个脸,API 还返回了脸部关键点的坐标。更详细的说明请参照文档。
小结
本文编写了 3 个命令行运行的 PHP 代码,来实测调用七牛云的 API。为了精简,代码里省去了很多和演示无关的部分。在实际项目中,需要对代码进行进一步的重构和封装,除去重复代码;由于发送网络请求会受制于网络的不稳定,需要增加 API 调用时异常处理(超时、返回错误代码等);也要对提交到 API 的数据进行更严格的校验。
经过实测,就案例里用到的 3 个 API 来说,总的感觉还是比较可靠的,可以逐步应用到生产环境中。当然,推测七牛云内部也在不断优化更新 API 背后的算法,相信之后这套 API 的可靠性还会得到进一步提升。
除了人脸核验,七牛云智能多媒体服务还提供了票证识别、多媒体处理、智能风控等功能,感兴趣的小伙伴可以体验一下,点击阅读原文即可跳转到官网页面~
戳下面的原文阅读,更有料!