使用百度文字识别实现身份证自动审核

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/qq_34845394/article/details/86351251

使用百度文字识别实现身份证自动审核

前言
先来说说为什么要做这个东西,这段时间公司的注册用户直线上升,而我们的项目是一个虚拟币交易平台,需要用户经过实名认证才能去交易,之前项目中的实名认证是人工去审核的,每天审核几千上万个,把人搞死了,所以就急需一个自动去审核的功能。

实现思路
我最开始想的是去找一个识别身份证上面的文字的接口,然后把识别出来的姓名和身份证号和用户填写的去比较,完全符合就给他通过,然后我就去找接口了,最后决定使用百度的接口了

一、创建应用

去百度云创建一个应用,创建应用之前是需要认证,认证很简单,按照他提示的一步一步去搞就行了,然后就可以创建应用了,如下:

image.png

其中APP_IDAPI_KEYSECRET_KEY是等会需要用到的,其实所有api都需要这个,阿里的也一样,我们可以看到这一个应用就可以使用很多api:

image.png

二、创建测试案例测试

有下面这么多,这里我们只需要使用身份证识别就可以了,但是这里有个问题,就是身份证识别每天只能免费使用500次,而我们每天的审核量远远不止500条,虽然我刚开始是那这个身份证的接口做的一个测试案例的,识别效果还不错,返回的是JSON格式的数据,如下:

待识别的证件照【正面】:

1.jpg

正面识别结果:

0 [main] INFO com.baidu.aip.client.BaseClient  - get access_token success. current state: STATE_AIP_AUTH_OK
3 [main] DEBUG com.baidu.aip.client.BaseClient  - current state after check priviledge: STATE_TRUE_AIP_USER
{
  "log_id": 8156922741304040753,
  "words_result": {
    "姓名": {
      "words": "卜振",
      "location": {
        "top": 236,
        "left": 611,
        "width": 111,
        "height": 53
      }
    },
    "民族": {
      "words": "汉",
      "location": {
        "top": 345,
        "left": 832,
        "width": 31,
        "height": 39
      }
    },
    "住址": {
      "words": "安徵省颖上县颖河乡马海村卜老庄队317号",
      "location": {
        "top": 534,
        "left": 588,
        "width": 483,
        "height": 104
      }
    },
    "公民身份号码": {
      "words": "342128197603154716",
      "location": {
        "top": 774,
        "left": 778,
        "width": 644,
        "height": 48
      }
    },
    "出生": {
      "words": "19760315",
      "location": {
        "top": 434,
        "left": 591,
        "width": 380,
        "height": 42
      }
    },
    "性别": {
      "words": "男",
      "location": {
        "top": 342,
        "left": 589,
        "width": 36,
        "height": 44
      }
    }
  },
  "words_result_num": 6,
  "image_status": "normal",
  "direction": 0
}

待识别的证件照【反面】:

扫描二维码关注公众号,回复: 4883640 查看本文章

3.jpg

反面识别结果:

0 [main] INFO com.baidu.aip.client.BaseClient  - get access_token success. current state: STATE_AIP_AUTH_OK
3 [main] DEBUG com.baidu.aip.client.BaseClient  - current state after check priviledge: STATE_TRUE_AIP_USER
{
  "log_id": 329680296446600997,
  "words_result": {
    "失效日期": {
      "words": "20340725",
      "location": {
        "top": 803,
        "left": 934,
        "width": 232,
        "height": 55
      }
    },
    "签发机关": {
      "words": "永州市公安局零陵分局",
      "location": {
        "top": 691,
        "left": 629,
        "width": 501,
        "height": 66
      }
    },
    "签发日期": {
      "words": "20140725",
      "location": {
        "top": 817,
        "left": 635,
        "width": 254,
        "height": 54
      }
    }
  },
  "words_result_num": 3,
  "image_status": "normal",
  "direction": 0
}

可以看到识别还是挺准确的,测试代码如下:

package com.ssh.test;

import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;

import org.json.JSONObject;

import com.baidu.aip.ocr.AipOcr;

public class Test {
	//设置APPID/AK/SK
	public static final String APP_ID = "11634304";
	public static final String API_KEY = "wXsELDayARPIbfjnhsunlRWC";
	public static final String SECRET_KEY = "EgC8Ug26uP8FGQXdC2HxkbdMsUrTF3m8";

	public static void sample(AipOcr client) {
	    // 传入可选参数调用接口
	    HashMap<String, String> options = new HashMap<String, String>();
	    options.put("detect_direction", "true");
	    options.put("detect_risk", "false");

	    //front:身份证含照片的一面;back:身份证带国徽的一面
	    String idCardSide = "back";

	    // 参数为本地图片路径
	    String image = "D://idcard/3.jpg";
	    JSONObject res = client.idcard(image, idCardSide, options);
	    //System.out.println(res.toString(2));

	    byte[] data = null;
		try {
			InputStream inputStream = null;
			inputStream = new FileInputStream(image);
			data = new byte[inputStream.available()];
			inputStream.read(data);
			inputStream.close();
		} catch (IOException e) {
			e.printStackTrace();
		}
	    
	    // 参数为本地图片二进制数组
	    byte[] file = data;
	    res = client.idcard(file, idCardSide, options);
	    System.out.println(res.toString(2));

	}
	
	public static void main(String[] args) {
		// 初始化一个AipOcr
        AipOcr client = new AipOcr(APP_ID, API_KEY, SECRET_KEY);

        // 可选:设置网络连接参数
        client.setConnectionTimeoutInMillis(2000);
        client.setSocketTimeoutInMillis(60000);

        // 可选:设置代理服务器地址, http和socket二选一,或者均不设置
        //client.setHttpProxy("proxy_host", proxy_port);  // 设置http代理
       // client.setSocketProxy("proxy_host", proxy_port);  // 设置socket代理
		
        sample(client);
	}
}


其实也可以去看百度云官方的案例,代码也得很详细,用之前先导jar包,具体看这里:http://ai.baidu.com/docs#/OCR-Java-SDK/top。

其实这样已经实现了身份证识别,但是仔细一想,每张身份证需要调用两次,那500次就只够250个人使用,太少了,然后我就试着去找其他的接口实现了,下面就来讲讲我是用什么来实现的。

三、使用通用文字识别

通用文字识别每天可以使用5W次,对于我们的项目是足够了,所以我就决定使用他了,
这个通用文字识别返回的数据如下:

识别结果:

2018-08-14 08:56:58 [ INFO | com.baidu.aip.client.BaseClient:209 ]:get access_token success. current state: STATE_AIP_AUTH_OK
0 [main] INFO com.baidu.aip.client.BaseClient  - get access_token success. current state: STATE_AIP_AUTH_OK
{"log_id":4969158594526202744,"words_result":[{"probability":{"average":0.986142,"min":0.969767,"variance":1.35E-4},"words":"名凌伟忠"},{"probability":{"average":0.95937,"min":0.834893,"variance":0.003532},"words":"性新男民族汉"},{"probability":{"average":0.99802,"min":0.99302,"variance":6.0E-6},"words":"出生1968年7月8日"},{"probability":{"average":0.99679,"min":0.982513,"variance":3.2E-5},"words":"住址江苏省苏州市平江区陆洞"},{"probability":{"average":0.996753,"min":0.988016,"variance":2.5E-5},"words":"桥45号"},{"probability":{"average":0.996695,"min":0.94748,"variance":1.1E-4},"words":"公民身份号码320511196807080012"}],"words_result_num":6,"language":3,"direction":3}
最后提取出来的文字=======>名凌伟忠性新男民族汉出生196878日住址江苏省苏州市平江区陆洞桥45号公民身份号码320511196807080012
正面验证通过!

我们可以看到这个通用文字识别出来的结果是一段一段的,我们需要把我们需要的文字提取出来,提取过程看下面的代码:

package com.tradeplatform.common.util;

import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.regex.Pattern;

import org.apache.commons.lang3.StringUtils;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import com.baidu.aip.ocr.AipOcr;

/**
 * 文字识别工具类
 * @author asus
 *
 */
public class OcrUtils {

	//设置APPID/AK/SK
	public static final String APP_ID = "11636969";
	public static final String API_KEY = "BEPxcrMej7by6EnY6rGPjHsi";
	public static final String SECRET_KEY = "pSe8nI2WWHbtQr7Phhr02ICq4m5yWiIY";


	public static void main(String[] args) {

		for (int i = 0; i < 1; i++) {
//			System.err.println(idCardValidate("郑志刚","420112195711032714","http://47.75.43.140:8888/group1/M00/02/95/rB_34FtpWleADkp4AAI06gxuaIs788.jpg"));
			System.err.println(idCardValidate("凌伟忠","320511196807080012","https://admin.gdae.xin//group1/M00/02/DA/rB_34Ftr7RGADpSwAAEe1K-ijzY703.jpg"));
		}

	}

	/**
	 * 身份证文字识别
	 * 说明:
	 * <p>
	 * 创建人: LGQ <br>
	 * 创建时间: 2018年8月6日 下午4:43:14 <br>
	 * <p>
	 * 修改人: <br>
	 * 修改时间: <br>
	 * 修改备注: <br>
	 * </p>
	 * @throws Exception 
	 */
	private static HashMap<String, String> options;
	public static String idCardValidate(String trueName, String idCardNO, String imgSrc) {

		if (StringUtils.isNotBlank(idCardNO)) {
			idCardNO = idCardNO.toUpperCase();
		}
		
		// 传入可选参数调用接口
		if (null == options) {
			options = new HashMap<String, String>();
			options.put("language_type", "CHN_ENG");
			options.put("detect_direction", "true");
			options.put("detect_language", "true");
			options.put("probability", "true");
		}

		URL url = null;
		byte[] data = null;
		try {
			url = new URL(imgSrc);

			DataInputStream dataInputStream = new DataInputStream(url.openStream());

			ByteArrayOutputStream output = new ByteArrayOutputStream();

			byte[] buffer = new byte[1024];
			int length;

			while ((length = dataInputStream.read(buffer)) > 0) {
				output.write(buffer, 0, length);
			}
			data = output.toByteArray();

			dataInputStream.close();
		} catch (MalformedURLException e) {
			return "照片URL协议、格式或者路径错误!";

		} catch (IOException e) {
			return "照片转换IO流异常!";
		}


		if (null == data) {
			return "获取的照片为空!";
		}
		JSONObject res = getAipOcr().basicGeneral(data, options);
		
		System.err.println(res);

		// 通用文字识别, 图片参数为远程url图片
		try {
			if (null != res) {
				//用于保存全部识别出来的文字
				StringBuilder rest = new StringBuilder();
				JSONArray jsonArray = res.getJSONArray("words_result");
				//使用循环把需要的文字全部提取出来
				for(int i = 0;i < jsonArray.length(); i++) {
					JSONObject jsonObject = (JSONObject) jsonArray.get(i);
					rest.append(jsonObject.getString("words"));
				}

				if(StringUtils.isNotBlank(rest.append("").toString())) {
					//最后提取出来的文字
					String restStr = rest.toString();
					System.err.println("最后提取出来的文字=======>" + restStr);
					
					//判断是正面照还是反面照
					if (StringUtils.isBlank(trueName) && StringUtils.isBlank(idCardNO) && restStr.contains("居民身份证")) {
						//反面
						//截取过期时间
						try {
							String expiryTime = restStr.split("\\-")[1];
							if(StringUtils.isNotBlank(expiryTime)) {
								if("长期".equals(expiryTime)) {
									return "反面验证通过!";
								}else {
									try {
										StringBuilder s = new StringBuilder();

										char[] expiryChar = expiryTime.toCharArray();

										for (char c : expiryChar) {
											if ('0' <= c && c <= '9') {
												s.append(c);
											}
										}
										Long idcardDate = new SimpleDateFormat("yyyyMMdd").parse(s.toString()).getTime();
										Long currDate = new Date().getTime();
										Long vlid = idcardDate - currDate;
										if(vlid >= 0) {
											return "反面验证通过!";
										}else {
											return "身份证过期!";
										}
									} catch (ParseException e) {
										return "无法识别!";
									}
								}
							}else {
								return "无法识别!";
							}
						} catch (Exception e) {
							return "无法识别!";
						}
						
					} else if (restStr.contains("出生") && restStr.contains("年")) {
						//正面
						/**
						 * 判断证件号是否匹配
						 */
						if (!restStr.contains(trueName)) {
							return "姓名不匹配!";

						}else if(idCardNO.length() != 18 || !restStr.contains(idCardNO) ){
							return "证件号不匹配!";

						}else if((idCardNO.length() != 18 || !restStr.contains(idCardNO)) && !restStr.contains(trueName)){
							return "证件号、姓名不匹配!";

						}else {
							try {
								/**
								 * 判断年龄是否符合要求
								 */
								//获取索引
								int sIndex = restStr.indexOf("出生");

								int eIndex = restStr.indexOf("年");

								//截取到年
								String year = restStr.substring(sIndex + 2, eIndex);

								//判断截取到的是不是4个纯数字
								Pattern pattern = Pattern.compile("^[-\\+]?[\\d]*$");  
								if (pattern.matcher(year.trim()).matches()) {
									//是纯数字
									//判断年龄是否符合18-70
									Calendar date = Calendar.getInstance();
									//当前年份
									int currYear = date.get(Calendar.YEAR);
									//年龄
									int age = currYear - Integer.valueOf(year.trim());

									if (age < 18) {
										return "年龄小于18岁!";
									}

									if (age > 70) {
										return "年龄大于70岁!";
									}
								}else {
									return "无法识别!";
								}

								//整段字符串包含全部通过
								if(idCardNO.length() == 18 && restStr.contains(trueName) && restStr.contains(idCardNO)) {
									return "正面验证通过!";
								}
							} catch (Exception e) {
								return "无法识别!";
							}
						}
					}else {
						return "无法识别!";
					}
				}
			}
		} catch (JSONException e) {
			return "无法识别!";
		}
		return "无法识别!";
	}

	/**
	 * 获取AipOcr对象
	 * 说明:
	 * <p>
	 * 创建人: LGQ <br>
	 * 创建时间: 2018年8月6日 下午3:59:21 <br>
	 * <p>
	 * 修改人: <br>
	 * 修改时间: <br>
	 * 修改备注: <br>
	 */
	private static AipOcr client;
	private static AipOcr getAipOcr() {
		if(client == null) {
			// 初始化一个AipOcr
			// 可选:设置网络连接参数
			client = new AipOcr(APP_ID, API_KEY, SECRET_KEY);
			client.setConnectionTimeoutInMillis(2000);
			client.setSocketTimeoutInMillis(60000);
		}
		return client;
	}
}

上面的这一段代码需要注意:

try {
	url = new URL(imgSrc);

	DataInputStream dataInputStream = new DataInputStream(url.openStream());

	ByteArrayOutputStream output = new ByteArrayOutputStream();

	byte[] buffer = new byte[1024];
	int length;

	while ((length = dataInputStream.read(buffer)) > 0) {
		output.write(buffer, 0, length);
	}
	data = output.toByteArray();

	dataInputStream.close();
} catch (MalformedURLException e) {
	return "照片URL协议、格式或者路径错误!";

} catch (IOException e) {
	return "照片转换IO流异常!";
}


if (null == data) {
	return "获取的照片为空!";
}
JSONObject res = getAipOcr().basicGeneral(data, options);

我们为什么不直接把图片的url传过去呢,而是去读取这个图片地址,将其转换成字节流呢,因为这个接口不支持htpps,我们项目中的图片都是带https的。

四、使用

识别工具类已经做好了,我们怎么去使用,刚开始我是想在审核的时候去进行识别,就是运营人员点击审核按钮,就去识别对应人的信息,后来我发现这个点击过程有点慢,点击一下需要等待1-2秒左右,可能就是将图片转换成字节流这个过程很耗时,在这里我来说说为什么不做成全自动审核,就是完全不需要人去管,因为机器识别始终不可能比人眼识别精准,可能会出现识别错误的情况,目前我还没有经过大量测试,还不清楚识别率高不高。所以只能先做成半自动吧,后来我就想去用一个任务定时去审核,然后把自动审核的结果存起来,当运营人员去审核的时候就不会出现卡顿的情况,因为是提前审核好的,运营人员审核的时候只需要去关心自动审核没有通过的(因为肯能会识别错),如果是自动审核通过了那就是通过了,下面贴出自动审核定时器的代码:

package com.tradeplatform.platform.user.job;

import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.commons.collections4.CollectionUtils;
import org.apache.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.Trigger;
import org.springframework.scheduling.TriggerContext;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;
import org.springframework.scheduling.support.CronTrigger;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

import com.tradeplatform.common.util.ApiConstant;
import com.tradeplatform.common.util.OcrUtils;
import com.tradeplatform.common.util.SysConfig;
import com.tradeplatform.platform.trade.mapper.TradeConfigMapper;
import com.tradeplatform.platform.trade.service.TradeConfigService;
import com.tradeplatform.platform.user.mapper.UserMapper;
import com.tradeplatform.trade.api.trade.utils.TradeConstants;
import com.tradeplatform.trade.api.user.entity.User;

/**
 * 实名认证自动审核任务
 * @author asus
 *
 */
@Component
@EnableScheduling
@Transactional
public class AutoVerifyTask implements SchedulingConfigurer {

	
	private Logger log = Logger.getLogger(getClass());


	private TradeConfigMapper tradeConfigMapper;
	
	@Autowired
	private UserMapper userMapper;
	
	@Autowired
	private TradeConfigService configService;
	

	/**
	 * 查询出需要自动审核的实名认证
	 * <p>
	 * 创建人:lgq <br>
	 * 创建时间:2018年5月31日 下午1:55:01 <br>
	 * <p>
	 * 修改人: <br>
	 * 修改时间: <br>
	 * 修改备注: <br>
	 * </p>
	 * 
	 * @return
	 */
	public List<User> getNeedAutoVerify() {
		//每次自动审核条数
		String autoVerifyNum = configService.getConfig(TradeConstants.TRADE_CONFIG_AUTO_VERIFY_NUM);
		
		//查询出需要自动审核的用户
		List<User> autoVerifyUsers = userMapper.selAutoVerifyUsers(Long.valueOf(autoVerifyNum));
		
		return autoVerifyUsers;
	}

	
	private String cron;
	
	@Autowired
	public AutoVerifyTask(TradeConfigMapper tradeConfigMapper) {
		// 获取每几分钟执行一次自动审核(单位:分钟)
		this.tradeConfigMapper = tradeConfigMapper;
		String interval = tradeConfigMapper.getConfigByCode(TradeConstants.TRADE_CONFIG_AUTO_VERIFY_TIME_INTERVAL).getConfigValue();
		//0 0 3 */1 * ? 
		this.cron = "0 */" + interval + " * * * ?";
		log.info("cron表达式==========>" + cron);
		//每十秒钟执行一次
		//cron = "*/10 * * * * ?";
		//每十分钟执行一次
		//cron = "0 */10 * * * ?";
	}  
	
	//记录自动审核任务执行次数
	private int num = 1;
	
	@Override
	public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
		taskRegistrar.addTriggerTask(new Runnable() {  
            @Override  
            public void run() {  
            	//自动审核是否开启
            	String autoVerify = configService.getConfig(TradeConstants.TRADE_CONFIG_OPEN_AUTO_VERIFY);
            	if (autoVerify.equals("1")) {
            		log.warn("----------自动审核已开启,开始进行第" + num + "次自动审核---------");
            		
            		//服务内网IP,用于加载身份证前面
            		// 公测环境、生产环境才使用服务器内网IP
            		String idCardHost;
            		
            		if (SysConfig.environment >= ApiConstant.Environment.OBT) {
            			idCardHost = "http://47.75.43.140:8888";
            		}else {
            			idCardHost = "http://192.168.0.189:8888";
					}
            		
            		//查询出需要自动审核的用户
            		List<User> autoVerifyUsers = getNeedAutoVerify();
            		
            		//为空不循环
            		if (CollectionUtils.isNotEmpty(autoVerifyUsers)) {
            			
            			log.info("需要审核的用户数量:" + autoVerifyUsers.size());
            			
            			//循环审核
                		for (int i = 0; i < autoVerifyUsers.size(); i ++) {
                			
                			User user = autoVerifyUsers.get(i);
                			
                			log.info("第" + (i + 1) + "个审核的用户信息:" + user);
                			
                			//获取自动审核结果
                			//正面照审核结果
                			String frontVerifyResult = OcrUtils.idCardValidate(user.getTrueName(), user.getIdCard(), idCardHost + user.getFront());
                			
                			log.info("正面照审核结果:" + frontVerifyResult);
                			
                			//反面照审核结果
                			String versoVerifyResult = OcrUtils.idCardValidate("", "", idCardHost + user.getVerso());
                			
                			log.info("反面照审核结果:" + versoVerifyResult);
                			
                			//修改自动审核描述
                			Map<String, Object> map = new HashMap<>();
                			map.put("desc", "正:" + frontVerifyResult + "|反:" + versoVerifyResult);
                			map.put("id", user.getId());
                			
                			userMapper.updateVerifyDesc(map);
                			
                			try {
								Thread.sleep(1000);
							} catch (InterruptedException e) {
								e.printStackTrace();
							}
                			
    					}
                		
                		num ++;
                		
                		log.warn("----------自动审核完成---------");
					} else {
						log.warn("----------没有需要自动审核的用户---------");
					}
            		
            	} else {
					log.warn("----------自动审核未开启---------");
				}
            }  
        }, new Trigger() {
            @Override  
            public Date nextExecutionTime(TriggerContext triggerContext) {  
                // 任务触发,可修改任务的执行周期  
                CronTrigger trigger = new CronTrigger(cron);  
                Date nextExec = trigger.nextExecutionTime(triggerContext);  
                return nextExec;  
            }  
        });  
	}
	
	
}

定时器不会用的看这里:

http://note.youdao.com/noteshare?id=09c1497aa12dbd944ee9c5cc103eb07c

因为系统需要,在后台设置了3个参数,分别是:是否开启自动审核、自动审核任务执行时间间隔、每次审核条数,有了这3个参数就可以动态的去控制自动审核了。

经过测试可以发现每分钟去请求百度识别接口20-30次是没有问题的,上面代码中每次识别之前我都让现成睡眠了1s,就是怕接口识别不过来加上的。

使用定时器去做的好处就是不管用户什么提交实名认证,任务都会去自动把他审核掉,提前审核好,然后将结果保存至数据库,最后运营人员看到的界面就是这样的:

image.png

运营人员即可根据提示去进行审核,虽然还是手动的,但是比纯手动还是快了不少的。

猜你喜欢

转载自blog.csdn.net/qq_34845394/article/details/86351251