综述
最近工作中进行了OCR文本检测与识别开发,文本检测/识别顾名思义就是通过一张图片或图像数据提取其中的文本信息(图像->文字)。但是实际应用中涉及到的使用场景有很多,有些场景下如果只是简单的输出图像中的文字并不能很好的解决实际问题,仍然需要人为的挑选与整理这些文本信息,费时费力。如果能通过程序代码实现对这些混乱的文本信息进行整理与输出将起到事半功倍的效果。
下面我就以中国大陆护照识别为例,来讲解如何通过代码来实现对OCR识别输出的文本信息进行相关整理与关键信息提取,同时贴出关键代码分享给大家。
护照中的关键信息
以上是中国大陆护照的样本主页图片,从中我们可以看到的关键信息如下:
- 护照类型:P
- 护照号码:EF1260892
- 国家代码:CHN
- 姓名:证件样本
- 性别:女
- 出生日期:1985-03-20
- 出生地点:广东
- 签发日期:2019-01-18
- 签发地点:广东
- 签发有效期:2029-01-17
- 签发机关:中华人民共和国国家移民管理局
- MZR码第一行:POCHNZHENGJIAN<<YANGBEN<<<<<<<<<<<<<<<<<<<<<
- MZR码第二行:EF12608921CHN8503208F2901178NGKELMPONBPJB978
关键信息提取方法
从以上的示例样本中,我们整理出了需要提取的关键信息,下面我们就根据这些关键信息的特点来制定提取方法——正则表达式匹配法(相关语法与测试可参考正则表达式在线测试)。
首先我们需要提取两行MZR码的内容
MZR编码规则
MRZ码就是护照主页下方的两行机读码,每行44个字符(由0-9、A-Z和<组成),如下例:
POCHNZHANG<<SAN<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
G489476464CHN7304279M210126619203301<<<<<<16
从这两行码的构成我们可以总结出它们的规律性:
第一行:
只包含大写英文字母和<符号,码长度是44
正则表达式:
((?!.*[0-9])(?!.*[a-z])(?=.*[A-Z])(?=.*[<]))(.{44})
第二行:
包含大写英文字母、阿拉伯数字和<符号(可能包含),码长度是44
正则表达式:
(((?!.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[<]))|((?!.*[a-z])(?!.*[<])(?=.*[A-Z])(?=.*[0-9])))(.{44})
在完成提取MRZ码后,下面我们需要根据MRZ码的编码规则来进行相关信息的提取:
第一行关键信息解析:
- PO:代表护照类型
- CHN:国家代码
- 6-44位:39位持证人姓名,姓和名之间用<<隔开,最后用<补足39个字符
第二行关键信息解析:
- 1-9位:护照号码
- 10位:1-9位的验证码
- 11-13位:国家代码
- 14-19位:持证人生日
- 20位:14-19位生日的验证码
- 21位:持证人性别(M:男,F:女)
- 22-27位:护照有效期
- 28:护照有效期验证码
- 29-42位:个人代码,每个国家都不一样
- 43位:个人代码的验证码
- 44:1-10位、14-20位、22-43位的验证码
从以上两行MRZ码解析可以看出,我们所需要大部分关键信息都已包含在这里了,我们只需要通过一些字符截断的方法便可以获取这些关键信息了。
String mzr1Str = "POCHNZHANG<<SAN<<<<<<<<<<<<<<<<<<<<<<<<<<<<<";
String mzr2Str = "G489476464CHN7304279M210126619203301<<<<<<16";
- 提取护照类型
String passportType = mzr1Str.substring(0,2)
- 提取国家代码
String countryCode = mzr1Str.substring(2,5)
- 提取持证人姓名英文拼音
String[] mzr1ToNames = mzr1Str.split("<<");
String name = mzr1ToNames[0].substring(5)+" "+mzr1ToNames[1]);//姓与名通过空格隔开
如果想要提取中文姓名的话,正则表达式匹配的方式不再适用,需要通过将检测到的中文文本转为拼音并组成<拼音,中文>键值对的方式,并与英文拼音比对相同后输出对应中文字符。
import java.util.HashMap;
// 第三方中文转拼音开源库,详见:https://github.com/promeG/TinyPinyin
import com.github.promeg.pinyinhelper.Pinyin;
/**
* 提取字符串数组中中文字符拼音并与对应中文字符组成键值对
* @param outputStrArr 字符串混合数组
* @return <拼音, 中文>键值对HashMap
*/
public static HashMap getPassportPinyinChineseMapFromOutputStr(String[] outputStrArr){
HashMap pcHashMap = new HashMap();
for(int i=0;i<outputStrArr.length;i++){
String str = outputStrArr[i];
StringBuilder pinyin = new StringBuilder("");
StringBuilder chinese = new StringBuilder("");
for(int j=0;j<str.length();j++){
if(Pinyin.isChinese(str.charAt(j))){
pinyin.append(Pinyin.toPinyin(str.charAt(j)));
chinese.append(str.charAt(j));
}
}
if(!pinyin.toString().equals("")) {
pcHashMap.put(pinyin.toString(), chinese.toString());
}
}
if(pcHashMap.isEmpty()){
return null;
}else{
return pcHashMap;
}
}
- 提取护照号码
String passportNum = mzr2Str.substring(0,9);
- 提取持证人生日
String birthDate = null;
//需要判断两位数年份是否大于当前两位数年份
if(Integer.parseInt(mzr2Str.substring(13, 15))>22) {
birthDate = "19"+mzr2Str.substring(13, 19);
}else{
birthDate = "20"+mzr2Str.substring(13, 19);
}
- 提取持证人性别
String gender = mzr2Str.substring(20,21);
- 提取护照有效截止期
String validUtilDate = "20"+mzr2Str.substring(21,27);
另外,我们还需要提取出生地点、签发地点和签发机关的信息,这就需要通过正则表达式来提取了。
- 出生地点/签发地点:(构成规则:中文名称省份+分隔符/+拼音英文名称省份)
正则表达式:
(?!.*([中国]{2}))([\u4e00-\u9fa5]{2,})/(?!.*([CHINESE]{7}))([A-Z]{3,})
- 签发机关:(构成规则:固定机构中文名称)
正则表达式(目前整理的主要有三类,其他可以按编写规则自行补充):
[公安部出入境管理局]{9}|[外交部]{3}|[中华人民共和国国家移民管理局]{14}
至此,我们所有需要的护照关键信息便都可以提取到了,下面贴一下所有关键代码。
完整关键代码
- 护照关键信息实体类
public class PassPortInfoEntity {
private String pptType;//护照类型
private String pptNum;//护照号码
private String countryCode;//国家代码
private String name;//姓名
private String gander;//性别
private String birthLocate;//出生地点
private String birthDate;//出生日期
private String issueLocate;//签发地点
private String validUntil;//有效截止期
private String issueOrganization;//签发机关
private String mzr1;//MRZ1码
private String mzr2;//MRZ2码
public PassPortInfoEntity(){
pptType = null;
pptNum = null;
countryCode = null;
name = null;
gander = null;
birthLocate = null;
birthDate = null;
issueLocate = null;
validUntil = null;
issueOrganization = null;
mzr1 = null;
mzr2 = null;
}
public String getPptType() {
return pptType;
}
public void setPptType(String pptType) {
this.pptType = pptType;
}
public String getPptNum() {
return pptNum;
}
public void setPptNum(String pptNum) {
this.pptNum = pptNum;
}
public String getCountryCode() {
return countryCode;
}
public void setCountryCode(String countryCode) {
this.countryCode = countryCode;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getGander() {
return gander;
}
public void setGander(String gander) {
this.gander = gander;
}
public String getBirthLocate() {
return birthLocate;
}
public void setBirthLocate(String birthLocate) {
this.birthLocate = birthLocate;
}
public String getBirthDate() {
return birthDate;
}
public void setBirthDate(String birthDate) {
this.birthDate = birthDate;
}
public String getIssueLocate() {
return issueLocate;
}
public void setIssueLocate(String issueLocate) {
this.issueLocate = issueLocate;
}
public String getValidUntil() {
return validUntil;
}
public void setValidUntil(String validUntil) {
this.validUntil = validUntil;
}
public String getIssueOrganization() {
return issueOrganization;
}
public void setIssueOrganization(String issueOrganization) {
this.issueOrganization = issueOrganization;
}
public String getMzr1() {
return mzr1;
}
public void setMzr1(String mzr1) {
this.mzr1 = mzr1;
}
public String getMzr2() {
return mzr2;
}
public void setMzr2(String mzr2) {
this.mzr2 = mzr2;
}
}
- 关键信息正则表达式定义
//出生/签发地点
public final static String PassportLocationRegex = "(?!.*([中国]{2}))([\\u4e00-\\u9fa5]{2,})/(?!.*([CHINESE]{7}))([A-Z]{3,})";
//签发机关(公安部出入境管理局)
public final static String PassportIssueOrganizationRegex = "[公安部出入境管理局]{9}|[外交部]{3}|[中华人民共和国国家移民管理局]{14}";
//MZR1码
public final static String PassportMZR1Regex = "((?!.*[0-9])(?!.*[a-z])(?=.*[A-Z])(?=.*[<]))(.{44})";
//MZR2码
public final static String PassportMZR2Regex = "(((?!.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[<]))|((?!.*[a-z])(?!.*[<])(?=.*[A-Z])(?=.*[0-9])))(.{44})";
public final static String[] PassportInfoRegexes = {
PassportLocationRegex,
PassportIssueOrganizationRegex,
PassportMZR1Regex,
PassportMZR2Regex
};
- 正则匹配方法
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* 正则匹配
* @param originStr 原字符串
* @param targetRegex 正则表达式
* @return 匹配后字符串
*/
public static String RegularMatch(String originStr, String targetRegex) {
Pattern r = Pattern.compile(targetRegex);
Matcher m = r.matcher(originStr);
if(targetRegex.equals(PassportIssueOrganizationRegex)) {
//提取签发机关中文名称
if (m.find()) {
String[] nameStr = null;
if (originStr.contains("/")) {
nameStr = originStr.split("/");
return nameStr[0];
}else{
return originStr;
}
}
return "";
}else{
//提取对应匹配信息
if (m.matches()) {
return originStr;
} else {
return "";
}
}
}
- 护照主页字符串数组完整信息提取
/**
* 完整信息匹配
* @param outputStrArr 护照主页识别字符串数组
* @return 护照关键信息实体类
*/
public static PassPortInfoEntity getPassportKeyInfosFromOutputStr(String[] outputStrArr){
PassPortInfoEntity passPortInfoEntity = new PassPortInfoEntity();
//按正则表达式提取相应字符串
for(int i=0;i<PassportInfoRegexes.length;i++){
//提取所有符合当前正则表达式要求的字符串,多个字符串以空格隔开
StringBuilder matchStrs = new StringBuilder();
for(int j=0;j<outputStrArr.length;j++){
String matchTempStr = RegularMatch(outputStrArr[j].replaceAll(" ", ""), PassportInfoRegexes[i]);
if(!matchTempStr.equals("")) {
matchStrs = matchStrs.append(matchTempStr).append(" ");
}
}
if(i==0){
try {
String[] BirthIssueLocates = matchStrs.toString().split(" ");
String birthLocate = BirthIssueLocates[0];
String issueLocate = BirthIssueLocates[1];
if(BirthIssueLocates.length>2){
//提取出生地点
birthLocate = BirthIssueLocates[1];
//提取签发地点
issueLocate = BirthIssueLocates[2];
}
passPortInfoEntity.setBirthLocate(birthLocate);
passPortInfoEntity.setIssueLocate(issueLocate);
}catch (Exception e){}
}else if(i==1){
//提取签发机关信息
passPortInfoEntity.setIssueOrganization(matchStrs.toString().replaceAll(" ", ""));
}else if(i==2){
//提取第一行MRZ码
String mzr1Str = matchStrs.toString().replaceAll(" ", "");
passPortInfoEntity.setMzr1(mzr1Str);
if(mzr1Str.isEmpty() || mzr1Str.length()<44) continue;
//解析信息
//提取护照类型
passPortInfoEntity.setPptType(mzr1Str.substring(0,2));
//提取国家代码
passPortInfoEntity.setCountryCode(mzr1Str.substring(2,5));
String[] mzr1ToNames = mzr1Str.split("<<");
//提取姓名英文拼音
passPortInfoEntity.setName(mzr1ToNames[0].substring(5)+" "+mzr1ToNames[1]);
}else if(i==3){
//提取第二行MRZ码
String mzr2Str = matchStrs.toString().replaceAll(" ", "");
passPortInfoEntity.setMzr2(mzr2Str);
if(mzr2Str.isEmpty() || mzr2Str.length()<44) continue;
//解析信息
//提取护照号码
passPortInfoEntity.setPptNum(mzr2Str.substring(0,9));
//提取生日
if(Integer.parseInt(mzr2Str.substring(13, 15))>22) {
passPortInfoEntity.setBirthDate("19"+mzr2Str.substring(13, 19));
}else{
passPortInfoEntity.setBirthDate("20"+mzr2Str.substring(13, 19));
}
//提取性别
passPortInfoEntity.setGander(mzr2Str.substring(20,21));
//提取有效截止期
passPortInfoEntity.setValidUntil("20"+mzr2Str.substring(21,27));
}
}
return passPortInfoEntity;
}
- 中文姓名对应
//此处省略OCR识别输出文本信息方法
......
passPortInfoEntity = PassportRegularUtils.getPassportKeyInfosFromOutputStr(resultStrArr);
//此处省略其他关键信息提取
......
//中英文姓名提取与合成
HashMap pcHashMap = PassportRegularUtils.getPassportPinyinChineseMapFromOutputStr(resultStrArr);
String nameInfo = null;
if(pcHashMap!=null) {
Log.d(TAG, "pcHashMap="+pcHashMap.toString());
try {
String[] namePinyinStrs = passPortInfoEntity.getName().split(" ");
String firstNameChinese = (String) pcHashMap.get(namePinyinStrs[0]);
String lastNameChinese = (String) pcHashMap.get(namePinyinStrs[1]);
String fullNameChinese = (String) pcHashMap.get(namePinyinStrs[0] + namePinyinStrs[1]);
if (firstNameChinese != null && !firstNameChinese.equals("null") && lastNameChinese != null && !lastNameChinese.equals("null")) {
//适用姓与名分开打印的护照
nameInfo = firstNameChinese + lastNameChinese + " / " + passPortInfoEntity.getName();
} else if (fullNameChinese != null && !fullNameChinese.equals("null")) {
//适用姓与名合并打印的护照
nameInfo = fullNameChinese + " / " + passPortInfoEntity.getName();
} else {
//若未成功提取,则不打印中文姓名
nameInfo = passPortInfoEntity.getName();
}
}catch (Exception e){
nameInfo = passPortInfoEntity.getName();
}
}
总结
以上方法和代码主要是针对中国大陆地区护照主页关键信息的提取,如若需要提取其他国家护照的信息,可参照这一套方法来编写相关代码,都是相通的,如有错误还请各位大神指正。