Java 项目实战: 交友项目 - 百度人脸识别入门案例

概述

交友项目的头像上传要做一道校验:上传的图片里必须有一张人脸,风景图、多人合照都不算合规头像。这件事可以自己训模型,但更快的是直接调百度 AI 开放平台的人脸检测接口。本文不走封装,就用一个测试类把官方 Java SDK 跑通,把 AppID/API Key/Secret Key、image_type、face_field、error_code、face_num 这几个概念在一次真实调用里全部落地。

纲要

  • 能力定位:人脸检测在头像上传链路里的位置
  • 接入前准备:注册开放平台 → 创建应用 → 三把钥匙 → 开通人脸检测 → 额度与计费量级
  • 引入依赖:工程里真实的 Maven 坐标,以及它被声明在哪个模块
  • 初始化客户端:new AipFace(AppID, APIKey, SecretKey)、超时设置、access_token 由谁维护
  • 发起检测:client.detect(image, imageType, options) 三个入参逐个说明
    • image + image_type:URL 与 BASE64 两种方式的代码对比与取舍
    • options:face_field / max_face_num / face_type / liveness_control 的作用
  • 解析返回:error_code、error_msg、face_num、face_list 的读取方式
  • 业务判定:为什么是 error_code == 0 且 face_num == 1
  • 完整可运行示例:本地图片 → Base64 → detect → 打印
  • 三个对照实验:自拍 / 风景图 / 多人合照
  • 常见返回码与网络环境排查
  • 安全与合规提醒

人脸检测在业务链路里的位置

头像上传不是一个"上传即保存"的动作,中间夹了一次第三方校验。用户在客户端选图 → app-server 把图片传到 OSS 拿到公网 URL → 拿这个 URL 去问百度"这图里有几张脸" → 判定通过才落库。
百度人脸检测 阿里云 OSS 项目 app-server 百度人脸检测 阿里云 OSS 项目 app-server #mermaid-svg-dtI0kj2YWcJSvzh1{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-dtI0kj2YWcJSvzh1 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-dtI0kj2YWcJSvzh1 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-dtI0kj2YWcJSvzh1 .error-icon{fill:#552222;}#mermaid-svg-dtI0kj2YWcJSvzh1 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-dtI0kj2YWcJSvzh1 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-dtI0kj2YWcJSvzh1 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-dtI0kj2YWcJSvzh1 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-dtI0kj2YWcJSvzh1 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-dtI0kj2YWcJSvzh1 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-dtI0kj2YWcJSvzh1 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-dtI0kj2YWcJSvzh1 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-dtI0kj2YWcJSvzh1 .marker.cross{stroke:#333333;}#mermaid-svg-dtI0kj2YWcJSvzh1 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-dtI0kj2YWcJSvzh1 p{margin:0;}#mermaid-svg-dtI0kj2YWcJSvzh1 .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-dtI0kj2YWcJSvzh1 text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-dtI0kj2YWcJSvzh1 .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-dtI0kj2YWcJSvzh1 .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-dtI0kj2YWcJSvzh1 .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-dtI0kj2YWcJSvzh1 .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-dtI0kj2YWcJSvzh1 #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-dtI0kj2YWcJSvzh1 .sequenceNumber{fill:white;}#mermaid-svg-dtI0kj2YWcJSvzh1 #sequencenumber{fill:#333;}#mermaid-svg-dtI0kj2YWcJSvzh1 #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-dtI0kj2YWcJSvzh1 .messageText{fill:#333;stroke:none;}#mermaid-svg-dtI0kj2YWcJSvzh1 .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-dtI0kj2YWcJSvzh1 .labelText,#mermaid-svg-dtI0kj2YWcJSvzh1 .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-dtI0kj2YWcJSvzh1 .loopText,#mermaid-svg-dtI0kj2YWcJSvzh1 .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-dtI0kj2YWcJSvzh1 .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-dtI0kj2YWcJSvzh1 .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-dtI0kj2YWcJSvzh1 .noteText,#mermaid-svg-dtI0kj2YWcJSvzh1 .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-dtI0kj2YWcJSvzh1 .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-dtI0kj2YWcJSvzh1 .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-dtI0kj2YWcJSvzh1 .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-dtI0kj2YWcJSvzh1 .actorPopupMenu{position:absolute;}#mermaid-svg-dtI0kj2YWcJSvzh1 .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-dtI0kj2YWcJSvzh1 .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-dtI0kj2YWcJSvzh1 .actor-man circle,#mermaid-svg-dtI0kj2YWcJSvzh1 line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-dtI0kj2YWcJSvzh1 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} putObject 上传头像 返回公网 URL detect(image=url, image_type=URL, options) 按 URL 下载图片 图片字节流 JSON(error_code, error_msg, face_num, face_list) error_code==0 且 face_num==1 → 允许保存

这里有个顺序上的依赖:走 URL 方式时,百度是从它自己的机房去下载这张图的,所以图片必须先落到一个公网可访问的地址上。私有读的 Bucket、内网地址、带鉴权签名的临时链接,百度都拉不到------这会在返回码里体现出来,后面排查表里会提。

接入前准备

百度人脸识别属于百度 AI 开放平台的能力,走的是"应用 + 密钥"模式,和阿里云短信、OSS 那套是一个套路。区别在于百度的人脸接口是 SDK 直连(SDK 内部自己换 token),不需要你在控制台配模板或签名。

步骤 做什么 产出 / 说明
注册登录 打开 https://ai.baidu.com/tech/face,支持百度账号或百度云账号登录 一个可用的开发者账号
创建应用 控制台 → 人工智能 → 人脸识别 → 创建应用,填应用名、归属行业 应用列表里的三条字符串
拿三把钥匙 应用详情页复制 AppID、API Key、Secret Key 后续代码里的 APP_ID / API_KEY / SECRET_KEY
开通能力 确认「人脸检测」已在应用默认能力里(人脸识别类应用一般默认开通) 调用失败时会返回权限相关错误码
了解额度 新应用通常有一批免费调用量,同时受 QPS 与每日总量两个维度限制 具体数值与计费单价随政策调整,以控制台「概览 / 计费」页为准

有三点容易踩:

  • AppID 是纯数字,API Key / Secret Key 是字母数字混合串,三者都区分大小写,复制时别把首尾空格带进去。
  • 免费额度是双重限制:日调用量用完,当天就直接报限额错误;单机并发高一点会触发 QPS 限额,这两个错误码不一样,排查方向也不同。
  • 百度云老账号的 Key 体系叫法不同:API_KEY 对应控制台的 Access Key ID,SECRET_KEY 对应 Access Key Secret。讲义里专门提醒过这一点。

引入依赖

工程里真实的坐标是 com.baidu.aip:java-sdk:4.8.0。

xml 复制代码
<!-- 百度人脸识别API -->
<dependency>
    <groupId>com.baidu.aip</groupId>
    <artifactId>java-sdk</artifactId>
    <version>4.8.0</version>
</dependency>

这段依赖声明位置值得说清楚 。视频里说"咱们项目不需要额外引依赖,第一天都引好了",实读代码可以看到它其实写在 mf-autoconfig/pom.xml 里:

xml 复制代码
<project>
    <artifactId>mf-autoconfig</artifactId>
    <dependencies>
        <!--阿里云核心API-->
        <dependency>
            <groupId>com.aliyun</groupId>
            <artifactId>aliyun-java-sdk-core</artifactId>
            <version>4.5.3</version>
        </dependency>
        <!--阿里云oss存储API-->
        <dependency>
            <groupId>com.aliyun.oss</groupId>
            <artifactId>aliyun-sdk-oss</artifactId>
            <version>3.10.2</version>
        </dependency>
        <!--百度人脸识别API-->
        <dependency>
            <groupId>com.baidu.aip</groupId>
            <artifactId>java-sdk</artifactId>
            <version>4.8.0</version>
        </dependency>
    </dependencies>
</project>

而 mf-app-server/pom.xml 依赖了 mf-autoconfig,百度 SDK 是顺着这条依赖传递进测试类的 。父 pom(mf)里并没有这个坐标。所以严格讲不是"第一天在父 pom 引好",而是"引在 autoconfig 模块里、app-server 传递拿到了"。如果你把这段代码单独放到一个干净工程里跑,自己补上这个依赖即可。

工程结构大致是这样,测试类放在 mf-app-server 的 test 目录下:

tree 复制代码
mf-test/
├── pom.xml                        # 父 pom:Spring Boot 2.3.9.RELEASE,JDK 8,依赖管理
├── mf-autoconfig/
│   └── pom.xml                    # 百度 SDK 4.8.0 声明在这里
├── mf-model/
├── mf-commons/
├── mf-dubbo/
└── mf-app-server/
    ├── pom.xml                    # 依赖 mf-autoconfig,SDK 由它传递进来
    └── src/test/java/com/wb/test/
        └── FaceTest.java          # 本文的入门案例

SDK 自身带了 org.json,所以代码里的 JSONObject 直接可用,不用另外引 json 包。

初始化客户端

AipFace 就是与百度人脸接口对话的入口,构造三个参数即可。

java 复制代码
package com.wb.test;

import com.baidu.aip.face.AipFace;

public class AipFaceClientDemo {

    // 控制台应用详情页复制过来的三项,换成你自己的
    public static final String APP_ID = "你的 AppID";
    public static final String API_KEY = "你的 API Key";
    public static final String SECRET_KEY = "你的 Secret Key";

    public static void main(String[] args) {
        // 初始化一个 AipFace,建议做成单例复用
        AipFace client = new AipFace(APP_ID, API_KEY, SECRET_KEY);

        // 可选:设置网络连接参数
        client.setConnectionTimeoutInMillis(2000);   // 建立连接超时:2s
        client.setSocketTimeoutInMillis(60000);      // 数据传输超时:60s

        // 可选:走公司代理时才需要,二选一
        // client.setHttpProxy("proxy_host", 8080);
        // client.setSocketProxy("proxy_host", 8080);
    }
}

鉴权不用自己管。 百度的调用链是"用 API Key / Secret Key 换 access_token,再拿 token 调业务接口",access_token 默认 30 天有效。这套逻辑 SDK 已经在内部做掉了:它在需要时自动去换 token、自己缓存、过期了自己刷新,AipFace 实例内部维护这层状态。

由此推出一条实践建议:AipFace 不要在每次调用时 new 一个。频繁 new 会让 SDK 反复去换 token,既慢又容易在并发下触发鉴权类限流。工程最终的做法是把它做成 Spring 单例 Bean(下一篇会讲),入门阶段在一个测试类里用一个静态实例或成员变量就够。

两个超时的取值也不是随手写的:连接超时 2s 是"连不上就快速失败",传输超时给到 60s 是因为百度服务端要去下载图片(走 URL 方式时)或上传 Base64 数据,图片大一点耗时会长。两者反着配(连接给 60s、传输给 2s)会导致小图也频繁超时。

发起人脸检测

核心只有一个方法:client.detect(image, imageType, options)。

java 复制代码
JSONObject res = client.detect(image, imageType, options);

三个入参分别是"图片内容""图片怎么传进来""要检测出什么"。前两个是本文最关键的知识点。

image 与 image_type:URL 还是 BASE64

image_type 决定 image 这个字符串到底是什么。人脸检测支持三种取值,日常只用前两种:

image_type image 里放什么 前提条件 适用场景
URL 图片的公网可访问地址 百度服务器能直接下载到这张图 图片已经在 OSS/CDN 上,有稳定公网地址
BASE64 图片 base64 编码后的字符串 本地有文件或内存里有字节流 图片不便暴露到公网、内网服务、临时文件
FACE_TOKEN 人脸图片的唯一标识 这张图之前被检测过,拿到了 token 复用此前检测结果,避免重复传图

URL 方式,就是讲义和工程里的写法:

java 复制代码
package com.wb.test;

import com.baidu.aip.face.AipFace;
import org.json.JSONObject;

import java.util.HashMap;

public class FaceTestByUrl {

    public static final String APP_ID = "你的 AppID";
    public static final String API_KEY = "你的 API Key";
    public static final String SECRET_KEY = "你的 Secret Key";

    public static void main(String[] args) {
        AipFace client = new AipFace(APP_ID, API_KEY, SECRET_KEY);
        client.setConnectionTimeoutInMillis(2000);
        client.setSocketTimeoutInMillis(60000);

        // image 是图片的公网 URL
        String image = "https://your-bucket.oss-cn-beijing.aliyuncs.com/2021/04/19/xxxx.jpg";
        String imageType = "URL";

        HashMap<String, String> options = new HashMap<String, String>();
        options.put("face_field", "age");
        options.put("max_face_num", "2");
        options.put("face_type", "LIVE");
        options.put("liveness_control", "LOW");

        JSONObject res = client.detect(image, imageType, options);
        System.out.println(res.toString(2));
    }
}

注意这里的 URL 必须是匿名可读的。OSS 默认私有读的 Bucket,直接拼出来的地址用在浏览器里能打开(因为控制台带的签名)但在百度那边会 403;这种情况要么给 Bucket 开放公共读,要么改用 BASE64。这条是"代码看着没问题、结果一直报错"的高频原因。

BASE64 方式,把图片读成字节数组再编码:

java 复制代码
package com.wb.test;

import com.baidu.aip.face.AipFace;
import org.json.JSONObject;

import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Base64;
import java.util.HashMap;

public class FaceTestByBase64 {

    public static final String APP_ID = "你的 AppID";
    public static final String API_KEY = "你的 API Key";
    public static final String SECRET_KEY = "你的 Secret Key";

    public static void main(String[] args) throws Exception {
        AipFace client = new AipFace(APP_ID, API_KEY, SECRET_KEY);
        client.setConnectionTimeoutInMillis(2000);
        client.setSocketTimeoutInMillis(60000);

        // 1、读本地图片,编码成 base64 字符串
        String path = "C:/Users/yourname/Desktop/avatar.jpg";
        byte[] bytes = Files.readAllBytes(Paths.get(path));
        String image = Base64.getEncoder().encodeToString(bytes);

        // 2、imageType 改成 BASE64
        String imageType = "BASE64";

        HashMap<String, String> options = new HashMap<String, String>();
        options.put("face_field", "age");
        options.put("max_face_num", "2");
        options.put("face_type", "LIVE");
        options.put("liveness_control", "LOW");

        JSONObject res = client.detect(image, imageType, options);
        System.out.println(res.toString(2));
    }
}

两种方式怎么选,看的是"图片现在在哪":

  • 图片已经躺在 OSS 上、地址公网可达 → 用 URL,业务代码最清爽,一行字符串。
  • 图片还在内存里、或者桶是私有读、或者是用户刚上传的临时文件 → 用 BASE64,多一步读文件 + 编码,但不受公网可达性约束。
  • 代价上,Base64 会把体积放大约三分之一(4/3 的编码膨胀)。官方给的限制是图片总数据大小小于 10M、base64 编码后的数据不超过 2M,所以头像这种小图随便用,扫描件级别的大图走 Base64 就容易顶到上限,这时候老老实实先传 OSS 再用 URL。

options:控制返回什么、检测几张

options 是一个 HashMap<String, String>,传 null 表示全用默认值。

java 复制代码
HashMap<String, String> options = new HashMap<String, String>();
options.put("face_field", "age");
options.put("max_face_num", "2");
options.put("face_type", "LIVE");
options.put("liveness_control", "LOW");
参数 取值 作用
face_field age、expression、face_shape、gender、glasses、landmark、landmark150、quality、eye_status、emotion、face_type、mask、spoofing,逗号分隔 指定额外返回哪些属性。不传时只返回 face_token、人脸框位置、置信度和旋转角
max_face_num 默认 1,最大 120 最多处理几张人脸;超出的脸直接不返回
face_type LIVE(生活照,默认)、IDCARD(身份证芯片照)、WATERMARK(带水印证件照)、CERT(证件照) 告诉算法这是什么类型的图,影响检测策略
liveness_control NONE(默认)、LOW、NORMAL、HIGH 活体检测严格度。等级越高越能挡照片翻拍,但真人也可能被误拒
face_sort_type 0 按人脸面积从大到小,1 按离图片中心从近到远,默认 0 决定 face_list 的排序,也就决定了 face_list[0] 是谁

头像校验场景的实际取值建议:face_field 只取你要用的(比如 age,beauty),取多了白白拉长响应;max_face_num 保持小值------反正 face_num > 1 就要判不合规,多检测没有意义;liveness_control 开 LOW 足够,因为这里只关心"是不是人脸",不做身份核验,开高了反而把正常自拍挡掉。

有个编译期的坑:官方文档的示例把 options 写成 HashMap<String, Object>,而 detect 的签名收的是 HashMap<String, String>,照着文档抄会编译不过------值统一写成字符串字面量。

解析返回结果

detect 返回 org.json.JSONObject,错误信息和数据在同一个对象里:

字段 类型 含义
error_code int 0 表示正常返回,非 0 见官方错误码表
error_msg string 成功时是 SUCCESS,失败时是错误描述
log_id long 本次请求的日志号,报工单时提供它
face_num int 检测到的人脸数量
face_list array 人脸信息列表,长度与 face_num 一致

face_list 里每一张脸包含这些字段:

字段 含义
face_token 这张人脸的唯一标识,同一张图多次检测得到同一个值
location 人脸框位置:left / top / width / height / rotation
face_probability 是人脸的置信度,范围 0~1
angle 三维旋转角:yaw(左右)/ pitch(俯仰)/ roll(平面内旋转)
age、beauty、expression、gender 等 由 face_field 决定是否返回

一次真实检测成功的返回长这样(字段名取自百度官方返回结构,数值是示例值):

json 复制代码
{
  "error_code": 0,
  "error_msg": "SUCCESS",
  "log_id": 2120123456789,
  "face_num": 1,
  "face_list": [
    {
      "face_token": "35235asfas21421fakghktyfdgh68bio",
      "location": {
        "left": 117,
        "top": 131,
        "width": 172,
        "height": 170,
        "rotation": 4
      },
      "face_probability": 1,
      "angle": {
        "yaw": -0.35,
        "pitch": 1.91,
        "roll": 2.30
      },
      "age": 29.29,
      "beauty": 61.34,
      "expression": {
        "type": "smile",
        "probability": 0.55
      },
      "gender": {
        "type": "male",
        "probability": 0.99
      }
    }
  ]
}

face_field 的可选列表会随文档版本调整(beauty、race 在历史文档里出现过),实际以控制台当前的文档为准;不写进 face_field 的字段不会出现在 face_list 里,拿的时候要么先判断存在,要么把它加进 face_field。

判定逻辑:error_code 为 0 且 face_num 为 1

只看一个 error_code == 0 是不够的,那只能说明"这次调用成功了"。真正决定头像是否合规的是 face_num:
#mermaid-svg-NmIl8yytiIPOYlMQ{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-NmIl8yytiIPOYlMQ .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-NmIl8yytiIPOYlMQ .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-NmIl8yytiIPOYlMQ .error-icon{fill:#552222;}#mermaid-svg-NmIl8yytiIPOYlMQ .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-NmIl8yytiIPOYlMQ .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-NmIl8yytiIPOYlMQ .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-NmIl8yytiIPOYlMQ .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-NmIl8yytiIPOYlMQ .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-NmIl8yytiIPOYlMQ .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-NmIl8yytiIPOYlMQ .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-NmIl8yytiIPOYlMQ .marker{fill:#333333;stroke:#333333;}#mermaid-svg-NmIl8yytiIPOYlMQ .marker.cross{stroke:#333333;}#mermaid-svg-NmIl8yytiIPOYlMQ svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-NmIl8yytiIPOYlMQ p{margin:0;}#mermaid-svg-NmIl8yytiIPOYlMQ .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-NmIl8yytiIPOYlMQ .cluster-label text{fill:#333;}#mermaid-svg-NmIl8yytiIPOYlMQ .cluster-label span{color:#333;}#mermaid-svg-NmIl8yytiIPOYlMQ .cluster-label span p{background-color:transparent;}#mermaid-svg-NmIl8yytiIPOYlMQ .label text,#mermaid-svg-NmIl8yytiIPOYlMQ span{fill:#333;color:#333;}#mermaid-svg-NmIl8yytiIPOYlMQ .node rect,#mermaid-svg-NmIl8yytiIPOYlMQ .node circle,#mermaid-svg-NmIl8yytiIPOYlMQ .node ellipse,#mermaid-svg-NmIl8yytiIPOYlMQ .node polygon,#mermaid-svg-NmIl8yytiIPOYlMQ .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-NmIl8yytiIPOYlMQ .rough-node .label text,#mermaid-svg-NmIl8yytiIPOYlMQ .node .label text,#mermaid-svg-NmIl8yytiIPOYlMQ .image-shape .label,#mermaid-svg-NmIl8yytiIPOYlMQ .icon-shape .label{text-anchor:middle;}#mermaid-svg-NmIl8yytiIPOYlMQ .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-NmIl8yytiIPOYlMQ .rough-node .label,#mermaid-svg-NmIl8yytiIPOYlMQ .node .label,#mermaid-svg-NmIl8yytiIPOYlMQ .image-shape .label,#mermaid-svg-NmIl8yytiIPOYlMQ .icon-shape .label{text-align:center;}#mermaid-svg-NmIl8yytiIPOYlMQ .node.clickable{cursor:pointer;}#mermaid-svg-NmIl8yytiIPOYlMQ .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-NmIl8yytiIPOYlMQ .arrowheadPath{fill:#333333;}#mermaid-svg-NmIl8yytiIPOYlMQ .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-NmIl8yytiIPOYlMQ .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-NmIl8yytiIPOYlMQ .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-NmIl8yytiIPOYlMQ .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-NmIl8yytiIPOYlMQ .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-NmIl8yytiIPOYlMQ .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-NmIl8yytiIPOYlMQ .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-NmIl8yytiIPOYlMQ .cluster text{fill:#333;}#mermaid-svg-NmIl8yytiIPOYlMQ .cluster span{color:#333;}#mermaid-svg-NmIl8yytiIPOYlMQ div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-NmIl8yytiIPOYlMQ .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-NmIl8yytiIPOYlMQ rect.text{fill:none;stroke-width:0;}#mermaid-svg-NmIl8yytiIPOYlMQ .icon-shape,#mermaid-svg-NmIl8yytiIPOYlMQ .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-NmIl8yytiIPOYlMQ .icon-shape p,#mermaid-svg-NmIl8yytiIPOYlMQ .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-NmIl8yytiIPOYlMQ .icon-shape .label rect,#mermaid-svg-NmIl8yytiIPOYlMQ .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-NmIl8yytiIPOYlMQ .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-NmIl8yytiIPOYlMQ .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-NmIl8yytiIPOYlMQ :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 否
是
否
是
读取图片(URL 或本地文件转 Base64)
client.detect(image, imageType, options)
error_code 等于 0 ?
调用失败:鉴权 / 网络 / 图片 / 额度问题
face_num 等于 1 ?
不合规:face_num=0 无人脸,face_num>1 多人合照
合规头像,允许保存到用户资料

为什么必须等于 1。 face_num = 0 是风景图、背影、纯色图,没有脸可存;face_num > 1 是多人合照,用户拿一张合影当头像,平台既无法确定展示的是哪一位,也不利于后续的比对类功能。只有"恰好一张脸"才放行。工程里的完整判定就两行:

java 复制代码
Integer errorCode = (Integer) res.get("error_code");
Integer faceNum = (Integer) res.get("face_num");

// 调用成功 且 恰好检测到一张人脸
boolean legalAvatar = (errorCode == 0) && (faceNum == 1);

顺带区分一个很容易混淆的情况:error_code = 0 但 face_num = 0 不是调用失败,而是"调用成功,但这张图里没人脸",这种在业务上属于"图片不合格",要给用户提示"头像需包含清晰人脸",而不是报"服务异常"。反过来,非 0 的 error_code 才是真的链路出问题了------这两类错误在接口返回给前端时应该分开,混在一起会让用户看到莫名其妙的错误信息。

完整可运行示例

下面这个测试类把"读本地文件 → Base64 编码 → detect → 解析 → 判定"串成一条完整链路,可以直接跑。工程里 JUnit 4.12 和 spring-boot-starter-test 已经在父 pom 里,写 @Test 即可。

java 复制代码
package com.wb.test;

import com.baidu.aip.face.AipFace;
import org.json.JSONObject;
import org.junit.Test;

import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Base64;
import java.util.HashMap;

public class FaceCheckTest {

    // 换成控制台应用详情页里的三项
    private static final String APP_ID = "你的 AppID";
    private static final String API_KEY = "你的 API Key";
    private static final String SECRET_KEY = "你的 Secret Key";

    /**
     * 检测本地图片中是否只有一张人脸
     * true:合规头像;false:无人脸或多张人脸
     */
    public boolean detectSingleFace(String imagePath) throws Exception {
        // 1、构造客户端(实际项目中应复用同一个实例)
        AipFace client = new AipFace(APP_ID, API_KEY, SECRET_KEY);
        client.setConnectionTimeoutInMillis(2000);
        client.setSocketTimeoutInMillis(60000);

        // 2、读文件并做 base64 编码
        byte[] bytes = Files.readAllBytes(Paths.get(imagePath));
        String image = Base64.getEncoder().encodeToString(bytes);

        // 3、检测参数
        HashMap<String, String> options = new HashMap<String, String>();
        options.put("face_field", "age,beauty,expression");
        options.put("max_face_num", "1");
        options.put("face_type", "LIVE");
        options.put("liveness_control", "LOW");

        // 4、调用人脸检测
        JSONObject res = client.detect(image, "BASE64", options);
        System.out.println(res.toString(2));

        // 5、解析结果
        int errorCode = res.getInt("error_code");
        if (errorCode != 0) {
            System.out.println("调用失败,error_code=" + errorCode
                    + ",error_msg=" + res.getString("error_msg"));
            return false;
        }

        int faceNum = res.getInt("face_num");
        System.out.println("检测到人脸数:" + faceNum);
        return faceNum == 1;
    }

    @Test
    public void testLocalImage() throws Exception {
        // 本地图片:写成绝对路径最稳,Windows 用正斜杠或双反斜杠
        String imagePath = "C:/Users/yourname/Desktop/avatar.jpg";

        boolean legal = detectSingleFace(imagePath);
        System.out.println("是否合规头像:" + legal);
    }
}

路径写法提醒。 Paths.get 拿到的是相对 JVM 工作目录的路径,而 Maven 跑测试时的工作目录是模块根目录 (这里是 mf-app-server),不是 classpath,也不是你 IDE 里打开的那个文件夹。两个常见结果:

  • 写成相对路径 src/test/resources/avatar.jpg,在 IDE 里点绿箭头能跑通,命令行 mvn test 却报 java.nio.file.NoSuchFileException------因为两者的工作目录不一样。想跨环境稳定,用绝对路径,或者用 getClass().getClassLoader().getResource("avatar.jpg").getFile() 从 classpath 读。
  • Windows 路径里的 \ 在 Java 字符串中要转义成 \\,"C:\\Users\\...\\avatar.jpg";漏一个反斜杠后面的字符就被当成转义序列,编译期就报错。

三个对照实验

判断这套代码是否真的在干活,用同一段代码换三张图跑一遍就够了。这三个结果能把"检测"这个能力讲明白。

实验 准备的图片 预期 error_code 预期 face_num 业务结论
自拍 一张本人清晰正脸照 0 1 合规,允许保存
风景 纯风景 / 无人的图片 222202(图片中没有人脸) 无该字段或 0 不合规,提示"请上传含人脸的图片"
合照 两人及以上的合影 0 > 1 不合规,提示"头像只能包含一张人脸"

第一组和第二组是最直观的对照:代码一字未改,只换了图片内容,返回的 error_code 就从 0 变成了 222202。这说明"有没有人脸"这件事是服务端算法给出的结论,不是你本地判断出来的------很多人第一次调不通时会怀疑代码写错了,其实是图片本身没有脸。

第三组是头像校验里最容易被忽略的一种:调用成功 (error_code = 0),但 face_num = 2。如果判定条件只写了 error_code == 0,这张合影就会被当成合规头像存进库里。这也是为什么判定必须带上 face_num == 1。

用本地图片跑三个实验时,只需要换 imagePath 的值;用 OSS 上的图跑,则把 image 换成对应的 URL、imageType 改成 "URL"。讲义和工程里就是这么做的:先把图传到 OSS,贴 URL,第一次贴的是无人的图,返回 222202,第二次贴的是人脸图,返回 error_code=0、face_num=1。

常见返回码与排查

error_code 非 0 时别急着改代码,先按这个表定位。具体错误码数值以百度官方文档的「错误信息」章节为准,下面列的只是高频项的语义归类。

error_code 类别 含义 常见原因 处理方向
鉴权类(110 / 111 等) access_token 无效或已过期 密钥写错、应用被停用、本机时间偏差过大导致签名失效 核对三把钥匙;校准系统时间;SDK 会自动重取 token,仍失败就查应用状态
222202 pic not has face,图片中没有人脸 传了风景图、背影图、纯色图 换成含人脸的图再试,属于正常业务返回
222203 image check fail,无法解析人脸 图片损坏、格式异常、人脸过小或严重遮挡 换图;确认图片能被正常打开
222001 / 222013 / 222015 等 参数缺失或格式错误 image 为空、image_type 拼错、max_face_num 传了非数字 逐项核对入参拼写与类型
图片下载失败 百度拉不到 URL 指向的图 URL 非公网可达、Bucket 私有读、内网地址、链接带过期签名 改用公共读地址,或换成 BASE64 方式
图片尺寸 / 大小超限 总数据超过 10M,或 base64 编码后超过 2M 传了扫描件级别的大图 压缩后再传,或先传 OSS 用 URL 方式
SDK 本地错误(SDK100~SDK109) 图片大小超限、边长不合要求、读文件失败、连接超时、格式不支持 请求还没发出去就被 SDK 拦下 看具体是哪一个,读文件失败多半是路径写错
额度类(4 / 17 / 18) 集群超限额、日请求量超限额、QPS 超限额 当天免费量用尽,或并发压高 等额度重置、降并发、或按官方计费文档开通付费

网络与环境上还有几个和 OSS 那篇同源的坑:

  • 企业网络 / 代理 :公司内网直连不到百度接口时,detect 会抛连接超时。SDK 提供了 setHttpProxy / setSocketProxy,二选一,都配上反而容易出问题。
  • 本机时间偏差 :签名校验依赖时间戳,机器时间比真实时间差太多会直接鉴权失败。这和 OSS 的 RequestTimeTooSkewed 是同一类问题------现象都是"代码没动过,昨天还能跑"。开一下系统自动校时即可。
  • SDK 与 JDK 版本 :本文用的 4.8.0 在 JDK 8 上是稳的,SDK 官方支持 1.7+。工程本身是 JDK 8 + Spring Boot 2.3.9。
  • 密钥硬编码:上面示例里的三把钥匙写在常量里,只适合本地验证。真要提交代码,请外置到配置文件或环境变量,别把密钥提交进仓库。

安全与合规

人脸属于敏感个人信息,这类接口的使用有几条底线:

  • 三把钥匙按"最小权限"放置,只给需要调用人脸能力的服务,测试和生产用不同的应用。
  • 测试时只用自己或已获授权的人脸图片,别拿同事、朋友、网上随便找的人脸照片测------上传到第三方接口就是一次数据出境。
  • 测试图用完就删,不要长期留在 OSS 或本地磁盘里。

API 速览

用法 签名 / 参数 说明
构造客户端 new AipFace(APP_ID, API_KEY, SECRET_KEY) 建议单例复用,内部维护 access_token
连接超时 client.setConnectionTimeoutInMillis(2000) 建立连接超时,单位毫秒
传输超时 client.setSocketTimeoutInMillis(60000) 数据传输超时,单位毫秒
代理 client.setHttpProxy(host, port) / client.setSocketProxy(host, port) 二选一
人脸检测 client.detect(image, imageType, options) 返回 org.json.JSONObject
图片类型 "URL" / "BASE64" / "FACE_TOKEN" 决定 image 放什么
结果字段 error_code、error_msg、face_num、face_list[].face_token 等 error_code == 0 表示调用成功

官方文档

总结

本文只做了一件事:把百度人脸检测 SDK 在一段可运行的代码里跑通。几个要点值得记住:

  • 依赖坐标是 com.baidu.aip:java-sdk:4.8.0,工程里声明在 mf-autoconfig 模块,app-server 通过传递依赖拿到。
  • 初始化只需三把钥匙,access_token 由 SDK 内部自动换取和刷新,业务代码不用管鉴权,但要复用 AipFace 实例。
  • image_type 的两种常用取值分清场景:URL 要求图片公网可达,简单但对桶的读权限有要求;BASE64 无公网要求,代价是体积膨胀三分之一且有 2M 上限。
  • 业务判定是两个条件的与:error_code == 0 说明调用成功,face_num == 1 才说明是合规的单张人脸。
  • error_code != 0 和 error_code == 0 但 face_num == 0 是两回事,前者是链路故障,后者是图片不合格,接口返回给前端时要分开。

现在这段代码还把 AipFace 的创建、参数拼装、结果判定全写在一个测试类里,任何要校验头像的服务都得抄一遍。下一篇《百度人脸识别:抽取自动装配组件》把它抽成 mf-autoconfig 里的 AipFaceProperties + AipFaceTemplate,顺带澄清一处被讲错的注入问题------那里会用一个最小复现说明,为什么"在 @Bean 方法里 new 对象"并不等于"注入不生效"。

相关推荐
wuminyu3 小时前
JMM学习总结:深入理解happens-before关系
java·linux·c语言·jvm·c++
电商API_180079052473 小时前
反向海淘多币种结算:汇率该在哪个时点被冻结
java·开发语言·数据库·跨境电商·1688 api
开开心心就好3 小时前
多份文档怎么批量打印?支持多种格式
java·服务器·开发语言·pdf·ocr·散列表·启发式算法
Pniubi4 小时前
模拟一个充值送token怎么保证幂等?一致性?原子性?
java
企业数字化笔记4 小时前
多租户配额怎么控制?用量口径、并发预留与超额处理的实现方法
java·数据库·spring boot
巴勒个啦5 小时前
Prompt Engineering 实战:5 个让 AI 生成生产级业务代码的工程模式
java·前端
倚楼听风语5 小时前
Kotlin 协程与 suspend 原理详解
java·算法·kotlin
开开心心就好5 小时前
文件被占用删不掉?解锁工具强制删除
java·运维·服务器·开发语言·前端·pdf·ocr
weixin_460443565 小时前
Java+Linux私有化训考平台:服务、数据库与终端部署结构解析
java·linux·数据库·私有化部署·企业培训考试系统·企业考试系统