【教程】从人脸检测与比对,实测七牛云人脸核验 API

导言:
本文将会实测七牛云的人脸识别 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 的可靠性还会得到进一步提升。
(0)

相关推荐