赞
踩
飞浆官网:
飞桨PaddlePaddle-源于产业实践的开源深度学习平台 这里有使用PaddleOCR的安装过程,这个最麻烦的就是安装过程,我在安装过程中遇到各种问题,后续会写一篇笔记记录这个PaddleOCR的安装过程, 这里直接先使用着先,这里主要记录java调用PaddleOCR中基于PaddleHub Serving的服务部署接口,我这里使用的是windows10,部署paddleOCR的基于PaddleHub Serving的服务部署,然后通过Java请求部署提供的接口进行身份证的文字识别。要完成部署,需要下载对应的文件,具体可以到下面的gitee和GitHub上下载。
gitee:
PaddleOCR: 基于飞桨的OCR工具库,包含总模型仅8.6M的超轻量级中文OCR,单模型支持中英文数字组合识别、竖排文本识别、长文本识别。同时支持多种文本检测、文本识别的训练算法。 (gitee.com)
github:
GitHub - PaddlePaddle/PaddleOCR: Awesome multilingual OCR toolkits based on PaddlePaddle (practical ultra lightweight OCR system, support 80+ languages recognition, provide data annotation and synthesis tools, support training and deployment among server, mobile, embedded and IoT devices)
这个PaddleOCR的安装和部署过程,后续会写一篇文章的(PaddleOCR在windows中的使用_m0_62317155的博客-CSDN博客 安装和部署过程在这里,看这篇文章就好了)。这里不是重点,这里只要记录Java调用启动PaddleOCR的基于PaddleHub Serving的服务部署的接口实现对身份证文字的识别。启动成功之后,可以看到如下的信息
然后使用postman或者apifox发送请求,然后试一试,有没有启动成功。我这里使用的是apifox,感觉这个比postman好用。使用apifox请求接口传入图片需要,变成base64位的图片,可以在这个网站将图片转成base64位
图片转换base64编码 在线图片Base64编码转换工具 iP138在线工具 如下面这一张图片,然后,将图片放在上面的在线网站,转图片为base64位的图片
注:图片来源于网络,如有不对的地方,请联系删除一下
从GIF图中,可以看到能够识别出来身份证的结果了
这里使用的是springboot2.7.10 + jdk11,下面就是一个简单的springboot的web项目,什么maven依赖都不需要添加,只是一个简单的springboot 的web项目就行了。
就这么一个简单的springboot 的web项目就行了,这里使用lombok,是考虑到可以会记录日志。所以这个加不加都行!
结果如下所示,代码在GIF的后面,完整的Java代码在后面,具体看代码就行了。
项目目录如下所示:
项目中的pom.xml如下所示:
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.10</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.example</groupId> <artifactId>paddle-ocr-java</artifactId> <version>0.0.1-SNAPSHOT</version> <name>paddle-ocr-java</name> <description>paddle-ocr-java</description> <properties> <java.version>11</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <excludes> <exclude> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </exclude> </excludes> </configuration> </plugin> </plugins> </build> </project>
Controller的代码
package com.example.controller; import com.example.utils.IdCardOcrUtils; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; import java.io.IOException; import java.util.Map; @RestController public class PaddleOcrTest { @PostMapping("/orctest") public Map<String, String> ocrTest(MultipartFile file) { try { byte[] bytes = file.getBytes(); Map<String, String> userInfoMap = IdCardOcrUtils.getStringStringMap(bytes); return userInfoMap; } catch (IOException e) { e.printStackTrace(); return null; } } }
身份证识别的utils
这个工具类可以写在service中的,先用service定义一个接口,然后再serviceImpl中实现这个工具类的功能。毕竟按照三层结构,Controller + service + dao层的,这个标准模式好一点。这里变成工具类时因为,懒!毕竟是笔记,不想搞得项目目录太复杂。
package com.example.utils; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.util.Base64Utils; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import org.springframework.web.client.RestTemplate; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; public class IdCardOcrUtils { private IdCardOcrUtils() { } /** * 身份证完整信息识别 * * @param bytes 输入流,的bytes数组 * @return 身份证信息 */ public static Map<String, String> getStringStringMap(byte[] bytes) { StringBuilder result = new StringBuilder(); HttpHeaders headers = new HttpHeaders(); //设置请求头格式 headers.setContentType(MediaType.APPLICATION_JSON); //构建请求参数 MultiValueMap<String, String> map = new LinkedMultiValueMap<String, String>(); //添加请求参数images,并将Base64编码的图片传入 map.add("images", ImageToBase64(bytes)); //构建请求 HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<MultiValueMap<String, String>>(map, headers); RestTemplate restTemplate = new RestTemplate(); //发送请求, springboot内置的restTemplate Map json = restTemplate.postForEntity("http://127.0.0.1:8868/predict/ocr_system", request, Map.class).getBody(); System.out.println(json); List<List<Map>> jsons = (List<List<Map>>) json.get("results"); System.out.println(jsons); for (int i = 0; i < jsons.get(0).size(); i++) { System.out.println("当前的文字是:" + jsons.get(0).get(i).get("text")); // 这里光靠这个trim()有些空格是去除不掉的,所以还需要使用替换这个,双重保险 result.append(jsons.get(0).get(i).get("text").toString().trim().replace(" ", "")); } String trim = result.toString().trim(); System.out.println("=================拼接后的文字是========================="); System.out.println(trim); System.out.println("=======================接下来就是使用正则表达提取文字信息了==============================="); List<Map> maps = jsons.get(0); String name = predictName(maps); if (name.equals("") || name == null) { name = fullName(trim); } System.out.println("姓名:" + name); String nation = national(maps); System.out.println("民族:" + nation); String address = address(maps); System.out.println("地址:" + address); String cardNumber = cardNumber(maps); System.out.println("身份证号:" + cardNumber); String sex = sex(cardNumber); System.out.println("性别:" + sex); String birthday = birthday(cardNumber); System.out.println("出生:" + birthday); // return json1; Map<String, String> userInfoMap = new HashMap<>(); userInfoMap.put("name", name); userInfoMap.put("nation", nation); userInfoMap.put("address", address); userInfoMap.put("cardNumber", cardNumber); userInfoMap.put("sex", sex); userInfoMap.put("birthday", birthday); return userInfoMap; } // 上面的方法,使用了static修饰,下面的方法,也需要使用static修饰,这里使用 // private修饰的话,在其他类中直接通过IdCardOcrUtils.predictName()这个就访问不到了, 或者protected修饰, // 不然其他类访问不就行了吗? // 这里唯一能通过IdCardOcrUtils.方法名,访问的是public修饰的方法 /** * 获取身份证姓名 * * @param maps 识别的结果集合 * @return 姓名 */ private static String predictName(List<Map> maps) { String name = ""; for (Map map : maps) { String str = map.get("text").toString().trim().replace(" ", ""); if (str.contains("姓名") || str.contains("名")) { String pattern = ".*名[\\u4e00-\\u9fa5]{1,4}"; Pattern r = Pattern.compile(pattern); Matcher m = r.matcher(str); if (m.matches()) { name = str.substring(str.indexOf("名") + 1); } } } return name; } /** * 为了防止第一次得到的名字为空,以后是遇到什么情况就解决什么情况就行了 * * @param result panddleOCR扫描得到的结果拼接: * 如:姓名韦小宝性别男民族汉出生1654年12月20日住址北京市东城区景山前街4号紫禁城敬事房公民身份证号码11204416541220243X * @return */ private static String fullName(String result) { String name = ""; if (result.contains("性") || result.contains("性别")) { String str = result.substring(0, result.lastIndexOf("性")); String pattern = ".*名[\\u4e00-\\u9fa5]{1,4}"; Pattern r = Pattern.compile(pattern); Matcher m = r.matcher(str); if (m.matches()) { name = str.substring(str.indexOf("名") + 1); } } return name; } /** * 获取民族 * * @param maps 识别的结果集合 * @return 民族信息 */ private static String national(List<Map> maps) { String nation = ""; for (Map map : maps) { String str = map.get("text").toString(); String pattern = ".*民族[\u4e00-\u9fa5]{1,4}"; Pattern r = Pattern.compile(pattern); Matcher m = r.matcher(str); if (m.matches()) { nation = str.substring(str.indexOf("族") + 1); } } return nation; } /** * 获取身份证地址 * * @param maps 识别的结果集合 * @return 身份证地址信息 */ private static String address(List<Map> maps) { String address = ""; StringBuilder addressJoin = new StringBuilder(); for (Map map : maps) { String str = map.get("text").toString().trim().replace(" ", ""); if (str.contains("住址") || str.contains("址") || str.contains("省") || str.contains("市") || str.contains("县") || str.contains("街") || str.contains("乡") || str.contains("村") || str.contains("镇") || str.contains("区") || str.contains("城") || str.contains("组") || str.contains("号") || str.contains("幢") || str.contains("室") ) { addressJoin.append(str); } } String s = addressJoin.toString(); if (s.contains("省") || s.contains("县") || s.contains("住址") || s.contains("址") || s.contains("公民身份证")) { // 通过这里的截取可以知道,即使是名字中有上述的那些字段,也不要紧,因为这个ocr识别是一行一行来的,所以名字的会在地址这两个字 // 前面,除非是名字中也有地址的”地“或者”址“字,这个还可以使用lastIndexOf()来从后往左找,也可以在一定程度上避免这个。 // 具体看后面的截图,就知道了 address = s.substring(s.indexOf("址") + 1, s.indexOf("公民身份证")); } else { address = s; } return address; } /** * 获取身份证号 * * @param maps ocr识别的内容列表 * @return 身份证号码 */ private static String cardNumber(List<Map> maps) { String cardNumber = ""; for (Map map : maps) { String str = map.get("text").toString().trim().replace(" ", ""); // 之里注意了,这里的双斜杆,是因为这里是java,\会转义,所以使用双鞋干\\,去掉试一试就知道了 String pattern = "\\d{17}[\\d|x|X]|\\d{15}"; Pattern r = Pattern.compile(pattern); Matcher m = r.matcher(str); if (m.matches()) { cardNumber = str; } } return cardNumber; } /** * 二代身份证18位 * 这里之所以这样做,是因为如果直接从里面截取,也可以,但是从打印的内容中,有时候 * 性别性别男,是在同一行,有些照片是 * 性 * 别 * 男 * 等,如果单纯是使用字符串的str.contains("男") ==》 然后返回性别男, * str.contains("女") ==> 然后返回性别女 * 这个万姓名中有男字,地址中有男字,等。而这个人的性别是女。这是可能会按照识别顺序 * 排序之后,识别的是地址的男字,所以这里直接从身份证倒数第二位的奇偶性判断男女更加准确一点 * 从身份证号码中提取性别 * * @param cardNumber 身份证号码,二代身份证18位 * @return 性别 */ private static String sex(String cardNumber) { String sex = ""; // 取倒身份证倒数第二位的数字的奇偶性判断性别,二代身份证18位 String substring = cardNumber.substring(cardNumber.length() - 2, cardNumber.length() - 1); int parseInt = Integer.parseInt(substring); if (parseInt % 2 == 0) { sex = "女"; } else { sex = "男"; } return sex; } /** * 从身份证中获取出生信息 * * @param cardNumber 二代身份证,18位 * @return 出生日期 */ private static String birthday(String cardNumber) { String birthday = ""; String date = cardNumber.substring(6, 14); String year = date.substring(0, 4); String month = date.substring(4, 6); String day = date.substring(6, 8); birthday = year + "年" + month + "月" + day + "日"; return birthday; } /** * 获取图片的base64位 * @param data 图片变成byte数组 * @return 图片的base64为内容 */ private static String ImageToBase64(byte[] data) { // 直接调用springboot内置的springframework内置的犯法 String encodeToString = Base64Utils.encodeToString(data); return encodeToString; } }
传入如下三张图片
注:图片来源于网络,如有不对的地方,请联系删除一下
注:图片来源于网络,如有不对的地方,请联系删除一下
结果如下所示:
从GIF中,可以看图片识别的结果,apifox的内容,因为这里的性别是从身份证倒数第二位的奇偶性判断的,所以!!!
性别这里不用管它,这个身份证号就是乱填的,所以性别不对很正常的
代码中idea打印的内容,至于这里的idea打印的内容,具体看代码就知道了。
因为这里的性别是从身份证倒数第二位的奇偶性判断的,所以!!!
性别这里不用管它,这个身份证号就是乱填的,所以性别不对很正常的
这里进行了一些,处理,防止当PaddleOCR的hubserving部署的服务停止时,Java项目出现异常报错,返回的结果出现问题,对后面的程序造成影响,结果如下图所示:
进行处理之后,代码在后面
Controller的代码:
package com.example.controller; import com.example.utils.IdCardOcrUtils; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; import java.io.IOException; import java.util.Map; @RestController public class PaddleOcrTest { @PostMapping("/orctest") public Map<String, String> ocrTest(MultipartFile file) { try { byte[] bytes = file.getBytes(); Map<String, String> userInfoMap = IdCardOcrUtils.getStringStringMap(bytes); return userInfoMap; } catch (IOException e) { e.printStackTrace(); return null; } } }
工具类
package com.example.utils; // import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.util.Base64Utils; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import org.springframework.web.client.RestClientException; import org.springframework.web.client.RestTemplate; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; // @Slf4j public class IdCardOcrUtils { private IdCardOcrUtils() { } /** * 身份证完整信息识别 * * @param bytes 输入流,的bytes数组 * @return 身份证信息 */ public static Map<String, String> getStringStringMap(byte[] bytes) { try { StringBuilder result = new StringBuilder(); HttpHeaders headers = new HttpHeaders(); //设置请求头格式 headers.setContentType(MediaType.APPLICATION_JSON); //构建请求参数 MultiValueMap<String, String> map = new LinkedMultiValueMap<String, String>(); //添加请求参数images,并将Base64编码的图片传入 map.add("images", ImageToBase64(bytes)); //构建请求 HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<MultiValueMap<String, String>>(map, headers); RestTemplate restTemplate = new RestTemplate(); //发送请求, springboot内置的restTemplate Map json = restTemplate.postForEntity("http://127.0.0.1:8868/predict/ocr_system", request, Map.class).getBody(); System.out.println(json); List<List<Map>> jsons = (List<List<Map>>) json.get("results"); System.out.println(jsons); for (int i = 0; i < jsons.get(0).size(); i++) { System.out.println("当前的文字是:" + jsons.get(0).get(i).get("text")); // 这里光靠这个trim()有些空格是去除不掉的,所以还需要使用替换这个,双重保险 result.append(jsons.get(0).get(i).get("text").toString().trim().replace(" ", "")); } String trim = result.toString().trim(); System.out.println("=================拼接后的文字是========================="); System.out.println(trim); System.out.println("=======================接下来就是使用正则表达提取文字信息了==============================="); List<Map> maps = jsons.get(0); String name = predictName(maps); if (name.equals("") || name == null) { name = fullName(trim); } System.out.println("姓名:" + name); String nation = national(maps); System.out.println("民族:" + nation); String address = address(maps); System.out.println("地址:" + address); String cardNumber = cardNumber(maps); System.out.println("身份证号:" + cardNumber); String sex = sex(cardNumber); System.out.println("性别:" + sex); String birthday = birthday(cardNumber); System.out.println("出生:" + birthday); // return json1; Map<String, String> userInfoMap = new HashMap<>(); userInfoMap.put("name", name); userInfoMap.put("nation", nation); userInfoMap.put("address", address); userInfoMap.put("cardNumber", cardNumber); userInfoMap.put("sex", sex); userInfoMap.put("birthday", birthday); return userInfoMap; } catch (RestClientException e) { // log.info("请启动身份证识别服务部署!!!以下报错并不会影响运行,所以这个异常不需要特别关心它,这个异常已经处理了!!!"); // System.out.println("请启动身份证识别服务部署!!!以下报错并不会影响运行,所以这个异常不需要特别关心它,这个异常已经处理了!!!"); e.printStackTrace(); // Map<String, String> maps = new HashMap<>(); // maps.put("names", ""); return null; } } // 上面的方法,使用了static修饰,下面的方法,也需要使用static修饰,这里使用 // private修饰的话,在其他类中直接通过IdCardOcrUtils.predictName()这个就访问不到了, 或者protected修饰, // 不然其他类访问不就行了吗? // 这里唯一能通过IdCardOcrUtils.方法名,访问的是public修饰的方法 /** * 获取身份证姓名 * * @param maps 识别的结果集合 * @return 姓名 */ private static String predictName(List<Map> maps) { String name = ""; for (Map map : maps) { String str = map.get("text").toString().trim().replace(" ", ""); if (str.contains("姓名") || str.contains("名")) { String pattern = ".*名[\\u4e00-\\u9fa5]{1,4}"; Pattern r = Pattern.compile(pattern); Matcher m = r.matcher(str); if (m.matches()) { name = str.substring(str.indexOf("名") + 1); } } } return name; } /** * 为了防止第一次得到的名字为空,以后是遇到什么情况就解决什么情况就行了 * * @param result panddleOCR扫描得到的结果拼接: * 如:姓名韦小宝性别男民族汉出生1654年12月20日住址北京市东城区景山前街4号紫禁城敬事房公民身份证号码11204416541220243X * @return */ private static String fullName(String result) { String name = ""; if (result.contains("性") || result.contains("性别")) { String str = result.substring(0, result.lastIndexOf("性")); String pattern = ".*名[\\u4e00-\\u9fa5]{1,4}"; Pattern r = Pattern.compile(pattern); Matcher m = r.matcher(str); if (m.matches()) { name = str.substring(str.indexOf("名") + 1); } } return name; } /** * 获取民族 * * @param maps 识别的结果集合 * @return 民族信息 */ private static String national(List<Map> maps) { String nation = ""; for (Map map : maps) { String str = map.get("text").toString(); String pattern = ".*民族[\u4e00-\u9fa5]{1,4}"; Pattern r = Pattern.compile(pattern); Matcher m = r.matcher(str); if (m.matches()) { nation = str.substring(str.indexOf("族") + 1); } } return nation; } /** * 获取身份证地址 * * @param maps 识别的结果集合 * @return 身份证地址信息 */ private static String address(List<Map> maps) { String address = ""; StringBuilder addressJoin = new StringBuilder(); for (Map map : maps) { String str = map.get("text").toString().trim().replace(" ", ""); if (str.contains("住址") || str.contains("址") || str.contains("省") || str.contains("市") || str.contains("县") || str.contains("街") || str.contains("乡") || str.contains("村") || str.contains("镇") || str.contains("区") || str.contains("城") || str.contains("组") || str.contains("号") || str.contains("幢") || str.contains("室") ) { addressJoin.append(str); } } String s = addressJoin.toString(); if (s.contains("省") || s.contains("县") || s.contains("住址") || s.contains("址") || s.contains("公民身份证")) { // 通过这里的截取可以知道,即使是名字中有上述的那些字段,也不要紧,因为这个ocr识别是一行一行来的,所以名字的会在地址这两个字 // 前面,除非是名字中也有地址的”地“或者”址“字,这个还可以使用lastIndexOf()来从后往左找,也可以在一定程度上避免这个。 // 具体看后面的截图,就知道了 address = s.substring(s.indexOf("址") + 1, s.indexOf("公民身份证")); } else { address = s; } return address; } /** * 获取身份证号 * * @param maps ocr识别的内容列表 * @return 身份证号码 */ private static String cardNumber(List<Map> maps) { String cardNumber = ""; for (Map map : maps) { String str = map.get("text").toString().trim().replace(" ", ""); // 之里注意了,这里的双斜杆,是因为这里是java,\会转义,所以使用双鞋干\\,去掉试一试就知道了 String pattern = "\\d{17}[\\d|x|X]|\\d{15}"; Pattern r = Pattern.compile(pattern); Matcher m = r.matcher(str); if (m.matches()) { cardNumber = str; } } return cardNumber; } /** * 二代身份证18位 * 这里之所以这样做,是因为如果直接从里面截取,也可以,但是从打印的内容中,有时候 * 性别性别男,是在同一行,有些照片是 * 性 * 别 * 男 * 等,如果单纯是使用字符串的str.contains("男") ==》 然后返回性别男, * str.contains("女") ==> 然后返回性别女 * 这个万姓名中有男字,地址中有男字,等。而这个人的性别是女。这是可能会按照识别顺序 * 排序之后,识别的是地址的男字,所以这里直接从身份证倒数第二位的奇偶性判断男女更加准确一点 * 从身份证号码中提取性别 * * @param cardNumber 身份证号码,二代身份证18位 * @return 性别 */ private static String sex(String cardNumber) { String sex = ""; // 取倒身份证倒数第二位的数字的奇偶性判断性别,二代身份证18位 String substring = cardNumber.substring(cardNumber.length() - 2, cardNumber.length() - 1); int parseInt = Integer.parseInt(substring); if (parseInt % 2 == 0) { sex = "女"; } else { sex = "男"; } return sex; } /** * 从身份证中获取出生信息 * * @param cardNumber 二代身份证,18位 * @return 出生日期 */ private static String birthday(String cardNumber) { String birthday = ""; String date = cardNumber.substring(6, 14); String year = date.substring(0, 4); String month = date.substring(4, 6); String day = date.substring(6, 8); birthday = year + "年" + month + "月" + day + "日"; return birthday; } /** * 获取图片的base64位 * * @param data 图片变成byte数组 * @return 图片的base64为内容 */ private static String ImageToBase64(byte[] data) { // 直接调用springboot内置的springframework内置的犯法 String encodeToString = Base64Utils.encodeToString(data); return encodeToString; } }
启动PaddleOCR的hubserving部署时
代码在码云:Java + PaddleOCR身份证识别: 这个是使用百度的Java + PaddleOCR识别身份证信息!!! - Gitee.com
介绍 — Vue.js (vuejs.org), 我今天去vue2官网看的时候,发现有一个公告,说vue2停止准备停止更新了。看这里就知道了Vue.js (vuejs.org)。
element UI的官网如下所示组件 | Element
注意了这里只是实现功能而已,至于样式什么的我就不设置了,这个毕竟是一个示例,没必要写得这么详细,如果是样式问题,可以看着修改就行了。这里样式就不考虑了,只考虑实现功能!!!注意了这里的Java实现了身份证的正反面识别,代码中的前端只实现了正面识别的展示,反面识别同样的道理,可以看自己的需求添加身份证发面识别的展示,这里就不写出来了,跟正面识别用一个道理!!!
pom.xml,跟上面一样,并没有变
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.10</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.example</groupId> <artifactId>paddle-ocr-java</artifactId> <version>0.0.1-SNAPSHOT</version> <name>paddle-ocr-java</name> <description>paddle-ocr-java</description> <properties> <java.version>11</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <excludes> <exclude> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </exclude> </excludes> </configuration> </plugin> </plugins> </build> </project>
PaddleOcrTest.java
package com.example.controller; import com.example.utils.IdCardOcrUtils; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; import java.io.IOException; import java.util.Map; @RestController public class PaddleOcrTest { /** * 身份证正面识别 * @param file 文件名 * @return 身份证正面信息的Map集合,包括姓名、性别、民族、住址、出生、身份证号码 */ @PostMapping("/orctest") public Map<String, String> ocrTest(MultipartFile file) { try { byte[] bytes = file.getBytes(); // 这里可以考虑将前端页面上传的文件保存到文件夹中,返回图片的访问地址给前端。 // 也可考虑转成base64位,把base64位返回给前端。 // 或者在前端那里直接将图片转base64位,然后将base64位的图片赋值到对应的字段中,提交后保存到数据库中 // 然后用户在前端点击提交用户信息时,将对应的信息保存到数据库中 Map<String, String> userInfoMap = IdCardOcrUtils.getStringStringMap(bytes); // userInfoMap.put("imgUrl", "图片的访问地址或者图片的base64位"); return userInfoMap; } catch (IOException e) { e.printStackTrace(); return null; } } /** * 身份证反面识别功能 * @param file 传入的文件 * @return 身份证反面信息,Map集合,包括身份证反面的:签发机关、有效期限 */ @RequestMapping("/ocrfanmian") public Map<String, String> ocrFanMian(MultipartFile file) { try { byte[] bytes = file.getBytes(); // 这里可以考虑将前端页面上传的文件保存到文件夹中,返回图片的访问地址给前端。 // 也可考虑转成base64位,把base64位返回给前端。 // 或者在前端那里直接将图片转base64位,然后将base64位的图片赋值到对应的字段中,提交后保存到数据库中 // 然后用户在前端点击提交用户信息时,将对应的信息保存到数据库中 Map<String, String> fanmianInfo = IdCardOcrUtils.getFanMian(bytes); // fanmianInfo.put("imgUrl", "图片的访问地址或者图片的base64位"); return fanmianInfo; } catch (IOException e) { e.printStackTrace(); return null; } } }
IdCardOcrUtils.java
package com.example.utils; // import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.util.Base64Utils; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import org.springframework.web.client.RestClientException; import org.springframework.web.client.RestTemplate; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; // @Slf4j public class IdCardOcrUtils { private IdCardOcrUtils() { } /** * 身份证正面完整信息识别 * * @param bytes 输入流,的bytes数组 * @return 身份证正面信息的Map集合,包括姓名、性别、民族、住址、出生、身份证号码 */ public static Map<String, String> getStringStringMap(byte[] bytes) { try { StringBuilder result = new StringBuilder(); HttpHeaders headers = new HttpHeaders(); //设置请求头格式 headers.setContentType(MediaType.APPLICATION_JSON); //构建请求参数 MultiValueMap<String, String> map = new LinkedMultiValueMap<String, String>(); //添加请求参数images,并将Base64编码的图片传入 map.add("images", ImageToBase64(bytes)); //构建请求 HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<MultiValueMap<String, String>>(map, headers); RestTemplate restTemplate = new RestTemplate(); //发送请求, springboot内置的restTemplate Map json = restTemplate.postForEntity("http://127.0.0.1:8868/predict/ocr_system", request, Map.class).getBody(); System.out.println(json); List<List<Map>> jsons = (List<List<Map>>) json.get("results"); System.out.println(jsons); for (int i = 0; i < jsons.get(0).size(); i++) { System.out.println("当前的文字是:" + jsons.get(0).get(i).get("text")); // 这里光靠这个trim()有些空格是去除不掉的,所以还需要使用替换这个,双重保险 result.append(jsons.get(0).get(i).get("text").toString().trim().replace(" ", "")); } String trim = result.toString().trim(); System.out.println("=================拼接后的文字是========================="); System.out.println(trim); System.out.println("=======================接下来就是使用正则表达提取文字信息了==============================="); List<Map> maps = jsons.get(0); String name = predictName(maps); if (name.equals("") || name == null) { name = fullName(trim); } System.out.println("姓名:" + name); String nation = national(maps); System.out.println("民族:" + nation); String address = address(maps); System.out.println("地址:" + address); String cardNumber = cardNumber(maps); System.out.println("身份证号:" + cardNumber); String sex = sex(cardNumber); System.out.println("性别:" + sex); String birthday = birthday(cardNumber); System.out.println("出生:" + birthday); // return json1; Map<String, String> userInfoMap = new HashMap<>(); userInfoMap.put("name", name); userInfoMap.put("nation", nation); userInfoMap.put("address", address); userInfoMap.put("cardNumber", cardNumber); userInfoMap.put("sex", sex); userInfoMap.put("birthday", birthday); return userInfoMap; } catch (RestClientException e) { // log.info("请启动身份证识别服务部署!!!以下报错并不会影响运行,所以这个异常不需要特别关心它,这个异常已经处理了!!!"); // System.out.println("请启动身份证识别服务部署!!!以下报错并不会影响运行,所以这个异常不需要特别关心它,这个异常已经处理了!!!"); e.printStackTrace(); // Map<String, String> maps = new HashMap<>(); // maps.put("names", ""); return null; } } /** * 身份证反面识别 * * @param bytes 图片的byte字节数组 * @return 身份证反面信息,Map集合,包括身份证反面的:签发机关、有效期限 */ public static Map<String, String> getFanMian(byte[] bytes) { try { StringBuilder result = new StringBuilder(); HttpHeaders headers = new HttpHeaders(); //设置请求头格式 headers.setContentType(MediaType.APPLICATION_JSON); //构建请求参数 MultiValueMap<String, String> map = new LinkedMultiValueMap<String, String>(); //添加请求参数images,并将Base64编码的图片传入 map.add("images", ImageToBase64(bytes)); //构建请求 HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<MultiValueMap<String, String>>(map, headers); RestTemplate restTemplate = new RestTemplate(); //发送请求, springboot内置的restTemplate Map json = restTemplate.postForEntity("http://127.0.0.1:8868/predict/ocr_system", request, Map.class).getBody(); System.out.println(json); List<List<Map>> jsons = (List<List<Map>>) json.get("results"); System.out.println(jsons); for (int i = 0; i < jsons.get(0).size(); i++) { System.out.println("当前的文字是:" + jsons.get(0).get(i).get("text")); // 这里光靠这个trim()有些空格是去除不掉的,所以还需要使用替换这个,双重保险 result.append(jsons.get(0).get(i).get("text").toString().trim().replace(" ", "")); } String trim = result.toString().trim(); List<Map> maps = jsons.get(0); // 身份证反面签发机关 String qianFaJiGuan = qianFaJiGuan(maps); // 身份证反面有效期限 String youXiaoQiXian = youXiaoQiXian(maps); Map<String, String> mapsInfo = new HashMap<>(); mapsInfo.put("qianFaJiGuan", qianFaJiGuan); mapsInfo.put("youXiaoQiXian", youXiaoQiXian); // maps.put("flag", "back"); 本来想放一个标记的,用来标记正反面 return mapsInfo; } catch (RestClientException e) { e.printStackTrace(); return null; } } // 下面代码中有好多地方使用到了正则表达式 // 上面的方法,使用了static修饰,下面的方法,也需要使用static修饰,这里使用 // private修饰的话,在其他类中直接通过IdCardOcrUtils.predictName()这个就访问不到了, 或者protected修饰, // 不然其他类访问不就行了吗? // 这里唯一能通过IdCardOcrUtils.方法名,访问的是public修饰的方法 /** * 获取身份证姓名 * * @param maps 识别的结果集合 * @return 姓名 */ private static String predictName(List<Map> maps) { String name = ""; for (Map map : maps) { String str = map.get("text").toString().trim().replace(" ", ""); if (str.contains("姓名") || str.contains("名")) { String pattern = ".*名[\\u4e00-\\u9fa5]{1,4}"; Pattern r = Pattern.compile(pattern); Matcher m = r.matcher(str); if (m.matches()) { name = str.substring(str.indexOf("名") + 1); } } } return name; } /** * 为了防止第一次得到的名字为空,以后是遇到什么情况就解决什么情况就行了 * * @param result panddleOCR扫描得到的结果拼接: * 如:姓名韦小宝性别男民族汉出生1654年12月20日住址北京市东城区景山前街4号紫禁城敬事房公民身份证号码11204416541220243X * @return */ private static String fullName(String result) { String name = ""; if (result.contains("性") || result.contains("性别")) { String str = result.substring(0, result.lastIndexOf("性")); String pattern = ".*名[\\u4e00-\\u9fa5]{1,4}"; Pattern r = Pattern.compile(pattern); Matcher m = r.matcher(str); if (m.matches()) { name = str.substring(str.indexOf("名") + 1); } } return name; } /** * 获取民族 * * @param maps 识别的结果集合 * @return 民族信息 */ private static String national(List<Map> maps) { String nation = ""; for (Map map : maps) { String str = map.get("text").toString(); String pattern = ".*民族[\u4e00-\u9fa5]{1,4}"; Pattern r = Pattern.compile(pattern); Matcher m = r.matcher(str); if (m.matches()) { nation = str.substring(str.indexOf("族") + 1); } } return nation; } /** * 获取身份证地址 * * @param maps 识别的结果集合 * @return 身份证地址信息 */ private static String address(List<Map> maps) { String address = ""; StringBuilder addressJoin = new StringBuilder(); for (Map map : maps) { String str = map.get("text").toString().trim().replace(" ", ""); // 看身份证地址那一栏,具体可以看一下自己的身份证,几乎都包含这些字,具体可以自己debugger看一下就知道了 // 具体可以自己debugger看一下就知道了 if (str.contains("住址") || str.contains("址") || str.contains("省") || str.contains("市") || str.contains("县") || str.contains("街") || str.contains("乡") || str.contains("村") || str.contains("镇") || str.contains("区") || str.contains("城") || str.contains("组") || str.contains("号") || str.contains("幢") || str.contains("室") ) { addressJoin.append(str); } } String s = addressJoin.toString(); if (s.contains("省") || s.contains("县") || s.contains("住址") || s.contains("址") || s.contains("公民身份证")) { // 通过这里的截取可以知道,即使是名字中有上述的那些字段,也不要紧,因为这个ocr识别是一行一行来的,所以名字的会在地址这两个字 // 前面,除非是名字中也有地址的”地“或者”址“字,这个还可以使用lastIndexOf()来从后往左找,也可以在一定程度上避免这个。 // 具体看后面的截图,就知道了 address = s.substring(s.indexOf("址") + 1, s.indexOf("公民身份证")); } else { address = s; } return address; } /** * 获取身份证号 * * @param maps ocr识别的内容列表 * @return 身份证号码 */ private static String cardNumber(List<Map> maps) { String cardNumber = ""; for (Map map : maps) { String str = map.get("text").toString().trim().replace(" ", ""); // 之里注意了,这里的双斜杆,是因为这里是java,\会转义,所以使用双鞋干\\,去掉试一试就知道了 String pattern = "\\d{17}[\\d|x|X]|\\d{15}"; Pattern r = Pattern.compile(pattern); Matcher m = r.matcher(str); if (m.matches()) { cardNumber = str; } } return cardNumber; } /** * 二代身份证18位 * 这里之所以这样做,是因为如果直接从里面截取,也可以,但是从打印的内容中,有时候 * 性别性别男,是在同一行,有些照片是 * 性 * 别 * 男 * 等,如果单纯是使用字符串的str.contains("男") ==》 然后返回性别男, * str.contains("女") ==> 然后返回性别女 * 这个万姓名中有男字,地址中有男字,等。而这个人的性别是女。这是可能会按照识别顺序 * 排序之后,识别的是地址的男字,所以这里直接从身份证倒数第二位的奇偶性判断男女更加准确一点 * 从身份证号码中提取性别 * * @param cardNumber 身份证号码,二代身份证18位 * @return 性别 */ private static String sex(String cardNumber) { String sex = ""; // 取倒身份证倒数第二位的数字的奇偶性判断性别,二代身份证18位 String substring = cardNumber.substring(cardNumber.length() - 2, cardNumber.length() - 1); int parseInt = Integer.parseInt(substring); if (parseInt % 2 == 0) { sex = "女"; } else { sex = "男"; } return sex; } /** * 从身份证中获取出生信息 * * @param cardNumber 二代身份证,18位 * @return 出生日期 */ private static String birthday(String cardNumber) { String birthday = ""; String date = cardNumber.substring(6, 14); String year = date.substring(0, 4); String month = date.substring(4, 6); String day = date.substring(6, 8); birthday = year + "年" + month + "月" + day + "日"; return birthday; } /** * 获取图片的base64位 * * @param data 图片变成byte数组 * @return 图片的base64为内容 */ private static String ImageToBase64(byte[] data) { // 直接调用springboot内置的springframework内置的犯法 String encodeToString = Base64Utils.encodeToString(data); return encodeToString; } /** * 获取身份证反面信息的签发机关 * * @param maps ocr识别的内容列表 * @return 身份证反面的签发机关 */ private static String qianFaJiGuan(List<Map> maps) { String qianFaJiGuan = ""; for (Map map : maps) { String str = map.get("text").toString().trim().replace(" ", ""); if (str.contains("公安局")) { // 为什么要有这一步,是因为,有时候身份证的签发机关(这四个字)和XXX公安局,是在一起并且是同一行的, // 如图片比较正的时候,识别得到的结果是:签发机关XXX公安局, // 如果图片是歪的,识别到的结果,签发机关和XXX公安局不在用一行的 // 具体那一张稍微正一点的图片和一张歪一点的图片,debugger,这里看一下就知道了 if (str.contains("签发机关")) { // String为引用类型 str = str.replace("签发机关", ""); } String pattern = ".*公安局"; Pattern r = Pattern.compile(pattern); Matcher m = r.matcher(str); if (m.matches()) { qianFaJiGuan = str; } } } return qianFaJiGuan; } /** * 身份证反面有效期识别 * * @param maps ocr识别的内容列表 * @return 身份证的有效期 */ private static String youXiaoQiXian(List<Map> maps) { String youXiaoQiXian = ""; for (Map map : maps) { String str = map.get("text").toString().trim().replace(" ", ""); // 为什么要有这一步,是因为,有时候身份证的有效期限(这四个字)和日期是在一起并且是同一行的, // 如图片比较正的时候,识别得到的结果是:效期期限2016.02.01-2026.02.01 // 如果图片是歪的,识别到的结果,有效期限和日期不在用一行的 // 具体那一张稍微正一点的图片和一张歪一点的图片,debugger,这里看一下就知道了 if (str.contains("有效期限")) { // String为引用类型 str = str.replace("有效期限", ""); } String pattern = "\\d{4}(\\-|\\/|.)\\d{1,2}\\1\\d{1,2}-\\d{4}(\\-|\\/|.)\\d{1,2}\\1\\d{1,2}"; Pattern r = Pattern.compile(pattern); Matcher m = r.matcher(str); if (m.matches()) { youXiaoQiXian = str; } } return youXiaoQiXian; } }
vue-element-ocr_test.html,的代码如下所示,下面所示的代码中因为引入的是在线的vue和element UI,所以运行代码的时候需要联网
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>PaddleOCR身份证识别</title> <!-- 在线引入vue,具体看官网:https://v2.cn.vuejs.org/v2/guide/ --> <script src="https://cdn.jsdelivr.net/npm/vue@2"></script> <!-- 下面是在线引入element ui的样式和组件库,具体看官网:https://element.eleme.cn/#/zh-CN/component/installation --> <!-- 引入样式 --> <link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css"> <!-- 引入组件库 --> <script src="https://unpkg.com/element-ui/lib/index.js"></script> </head> <body> <div id="app"> <el-row> <!-- 注意了,这里是element UI的组件库用法,不同的组件库上传文件的配置或者返回的结果是不一样的,这里只是举一个例子 --> <!-- 如vant和element UI组件库上传文件的操作也是不一样的,看对应的官网然后进行相对应的开发就可以了 --> <el-upload action="http://localhost:8080/orctest" list-type="picture-card" :on-preview="handlePictureCardPreview" :on-remove="handleRemove" :on-success="handleSuccess"> <i class="el-icon-plus"></i> </el-upload> <el-dialog :visible.sync="dialogVisible"> <img width="100%" :src="dialogImageUrl" alt=""> </el-dialog> </el-row> <el-row style="width: 400px; margin-top: 10px;"> <!-- 这里只是一个示例,就不考虑表单验证之类的了 --> <el-form ref="form" :model="userInfoForm" label-width="80px"> <el-form-item label="姓名"> <el-input v-model="userInfoForm.name"></el-input> </el-form-item> <el-form-item label="民族"> <el-input v-model="userInfoForm.nation"></el-input> </el-form-item> <el-form-item label="性别"> <el-radio-group v-model="userInfoForm.sex"> <el-radio label="男"></el-radio> <el-radio label="女"></el-radio> </el-radio-group> </el-form-item> <el-form-item label="住址"> <el-input type="textarea" v-model="userInfoForm.address"></el-input> </el-form-item> <el-form-item label="身份证号"> <el-input v-model="userInfoForm.cardNumber"></el-input> </el-form-item> <el-form-item> <el-button type="primary" @click="onSubmit">提交表单</el-button> <el-button>取消</el-button> </el-form-item> </el-form> </el-row> </div> <script> var app = new Vue({ el: '#app', data: { userInfoForm: { name: undefined, nation: undefined, address: undefined, cardNumber: '', sex: undefined, birthday: '' }, dialogImageUrl: '', dialogVisible: false }, methods: { // element UI中el-upload图片上传成功时的回调,详情看官网 // https://element.eleme.cn/#/zh-CN/component/upload handleSuccess(response, file, fileList) { console.log(response) this.userInfoForm = response }, handleRemove(file, fileList) { console.log(file, fileList); if (fileList.length == 0) { this.userInfoForm = { name: undefined, nation: undefined, address: undefined, cardNumber: undefined, sex: undefined, birthday: undefined } } }, handlePictureCardPreview(file) { this.dialogImageUrl = file.url; this.dialogVisible = true; }, // 发送axios请求,将表单数据保存的数据库中 onSubmit() { // 这里可以发送axios请求,将表单数据保存到数据库中 } } }) </script> </body> </html>
结果如下面的GIF图所示
这里可以不看的,我在上面的代码已经考虑过了。为什么要写这个呢,我之前是想着直接把所有内容拼接在一起:如下图所示的idea打印的结果:
但是这里有问题,具体看后面的解释!!!然后根据身份证的特定字段如:姓名、性别、出生、住址、公民身份证号码。这些固定的字段,截取内容,如使用Java对str.substring(str.indexOf(“姓名”) ,str.idnexOf(“性别”))的字符串的截取,就可以得到对应的名字。具体看idea打印的内容截图就知道了,特别是下面画红线的内容。但是后面发现歪了一些的图片(只要不是歪得太离谱)的文字位置不一定对,就像下面划线的内容,OCR得到的结果不一定是按照身份证上面的顺序排列文字的,所以我就在代码中考虑了使用正则表达式的匹配的方式了。这样就不需要知道位置了。这里写这个是单存记录一下这个过程。
正图图片:
apifox的内容:因为这里的性别是从身份证倒数第二位的奇偶性判断的,所以!!!
性别这里不用管它,这个身份证号就是乱填的,所以性别不对很正常的
idea打印的内容如下所示:因为这里的性别是从身份证倒数第二位的奇偶性判断的,所以!!!
性别这里不用管它,这个身份证号就是乱填的,所以性别不对很正常的
偏移的图片:
apifox的内容:因为这里的性别是从身份证倒数第二位的奇偶性判断的,所以!!!
性别这里不用管它,这个身份证号就是乱填的,所以性别不对很正常的
idea打印的内容:因为这里的性别是从身份证倒数第二位的奇偶性判断的,所以!!!
性别这里不用管它,这个身份证号就是乱填的,所以性别不对很正常的
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。