OCR识别护照(一):JavaFX搭建可视化界面
文件选择
首先在MainController.java中将需要用到的组件与main.fxml中的组件对应起来。再为需要展示变化数据的两个组件filePath和tipLabel准备两个StringProperty属性filePathText和tipText与之对应。
//选择的文件
@FXML
private TextArea filePath;
//选择文件按钮
@FXML
private Button fileButton;
//开始识别按钮
@FXML
private Button scanButton;
//进度展示label
@FXML
private Label tipLabel;
//结果表
@FXML
private TableView resultTable;
private List<File> filePathChoosed;
private SimpleStringProperty filePathText = new SimpleStringProperty();
private SimpleStringProperty tipText = new SimpleStringProperty();
将组件与数据绑定,这样当filePathText和tipText变化时,filePath和tipLabel组件就会随之展示不同的数据。
filePath.textProperty().bind(filePathText);
tipLabel.textProperty().bind(tipText);
为选择文件按钮添加点击事件,设置允许的文件类型png、jpg、bmp和pdf。使用fileChooser.showOpenMultipleDialog()这样用户可以一次性选择多个文件。
fileButton.setOnMouseClicked(new EventHandler<MouseEvent>() {
@Override
public void handle(MouseEvent event) {
try {
FileChooser fileChooser = new FileChooser();
fileChooser.setTitle("选择文件");
//设置可选择的文件类型
fileChooser.getExtensionFilters().addAll(
new FileChooser.ExtensionFilter("护照文件", "*.png","*.jpg","*.bmp","*.pdf")
);
//允许选择多个文件
filePathChoosed = fileChooser.showOpenMultipleDialog(fileButton.getScene().getWindow());
if(filePathChoosed != null && filePathChoosed.size() != 0){
StringBuilder sb = new StringBuilder();
for(File file: filePathChoosed){
sb.append(file.getPath() + "\n");
}
//将选择的文件展示到文本域中
filePathText.set(sb.toString());
}
} catch (Exception e) {
logger.error(e.getMessage());
}
}
});

使用腾讯AI——卡证文字识别
百度、阿里、腾讯和华为等都有AI开放平台,这个工具使用腾讯AI,主要是因为腾讯的免费调用次数最多!
腾讯OCR体验地址:https://cloud.tencent.com/act/event/ocrdemo?dt=1
注册腾讯云账号后,即可获取免费的AI资源包,其中卡证类识别有每月1000次的调用量,而且调用失败不计算使用量。

废话不多说,直接看API使用文档 https://cloud.tencent.com/document/api/866/37657。这里我们使用护照识别(港澳台地区及境外护照)这个接口,因为我们只需要获取护照中的MRZ码即可,不需要区分中国护照和其他护照。
maven引入腾讯云sdk
<!--腾讯云SDK -->
<dependency>
<groupId>com.tencentcloudapi</groupId>
<artifactId>tencentcloud-sdk-java</artifactId>
<version>3.1.322</version>
</dependency>
根据SDK示例,编写护照识别代码
package org.xy.passportScanner;
import com.tencentcloudapi.common.Credential;
import com.tencentcloudapi.common.exception.TencentCloudSDKException;
import com.tencentcloudapi.common.profile.ClientProfile;
import com.tencentcloudapi.common.profile.HttpProfile;
import com.tencentcloudapi.ocr.v20181119.OcrClient;
import com.tencentcloudapi.ocr.v20181119.models.MLIDPassportOCRRequest;
import com.tencentcloudapi.ocr.v20181119.models.MLIDPassportOCRResponse;
import org.xy.passportScanner.config.GlobalData;
import java.util.Base64;
public class TencentOcr {
private static final String region = "ap-guangzhou";
/**
* @description
* @author xy
* @date 2022/04/13 15:10
* @param image
* @param extension
* @return com.tencentcloudapi.ocr.v20181119.models.MLIDPassportOCRResponse
*/
public static MLIDPassportOCRResponse apiMLIDPassportOCR(byte[] image, String extension){
try {
Credential cred = new Credential(GlobalData.settings.getTencentSecretId(), GlobalData.settings.getTencentSecretKey());
// 实例化一个http选项,可选的,没有特殊需求可以跳过
HttpProfile httpProfile = new HttpProfile();
httpProfile.setEndpoint("ocr.tencentcloudapi.com");
// 实例化一个client选项,可选的,没有特殊需求可以跳过
ClientProfile clientProfile = new ClientProfile();
clientProfile.setHttpProfile(httpProfile);
// 实例化要请求产品的client对象,clientProfile是可选的
OcrClient client = new OcrClient(cred, region, clientProfile);
// 实例化一个请求对象,每个接口都会对应一个request对象
MLIDPassportOCRRequest req = new MLIDPassportOCRRequest();
//Image转Base64格式
String base64 = Base64.getEncoder().encodeToString(image);
req.setImageBase64("data:image/"+ extension +";base64," + base64);
// 返回的resp是一个MLIDPassportOCRResponse的实例,与请求对象对应
MLIDPassportOCRResponse resp = client.MLIDPassportOCR(req);
// 输出json格式的字符串回包
System.out.println(MLIDPassportOCRResponse.toJsonString(resp));
return resp;
} catch (TencentCloudSDKException e) {
System.out.println(e.toString());
return null;
}
}
}
PDF转图片
引入pdfbox处理pdf文件,
<!--pdf转图片,pdfbox-->
<dependency>
<groupId>org.apache.pdfbox</groupId>
<artifactId>pdfbox</artifactId>
<version>3.0.0-alpha2</version>
</dependency>
<dependency>
<groupId>org.apache.pdfbox</groupId>
<artifactId>fontbox</artifactId>
<version>3.0.0-alpha2</version>
</dependency>
逻辑也很简单,读取pdf文件,并将每一页都转成图片,然后调用腾讯OCR接口进行识别。需要注意的是腾讯OCR接口对图片大小有限制,所以renderer.renderImageWithDPI(i, 150)这里需要注意调整DPI。
package org.xy.passportScanner.utils;
import org.apache.pdfbox.Loader;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.rendering.PDFRenderer;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
public class PdfUtil {
public static List<BufferedImage> pdfToImage(File file){
List<BufferedImage> result = null;
try {
PDDocument doc = Loader.loadPDF(file);
PDFRenderer renderer = new PDFRenderer(doc);
int pageCount = doc.getNumberOfPages();
if (pageCount > 0){
result = new ArrayList<>();
}
for(int i=0;i<pageCount;i++){
BufferedImage image = renderer.renderImageWithDPI(i, 150);
result.add(image);
}
} catch (IOException e) {
e.printStackTrace();
}
return result;
}
}
为开始识别按钮添加点击事件,对用户选中的文件进行识别。
这里要注意的是,我们用了一个tipLabel来展示文件识别进度,识别线程中需要动态修改界面内容的话,直接修改tipText的值是没用的,需要用javafx提供的Platform.runLater()来修改界面展示内容。
Platform.runLater(() -> tipText.set(tip));
完整识别点击事件代码如下:
scanButton.setOnMouseClicked(new EventHandler<MouseEvent>(){
@Override
public void handle(MouseEvent event) {
try {
if(filePathChoosed == null || filePathChoosed.size() == 0){
tipText.set("请选择要识别的文件");
return;
}
data.clear();
int fileNum = filePathChoosed.size();
new Thread(()->{
for(int i = 0; i < fileNum; i++){
File file = filePathChoosed.get(i);
String tip = (i + 1) + "/" + fileNum + " 正在识别:" + file.getPath();
Platform.runLater(() -> {
tipText.set(tip);
});
try {
String filePath = "file://" + file.getPath();
String fileName = file.getName();
String extension = fileName.substring(fileName.lastIndexOf(".")+1);
if(extension.equalsIgnoreCase("pdf")){
List<BufferedImage> pdfImages = PdfUtil.pdfToImage(file);
for(BufferedImage image : pdfImages){
boolean success = false;
int rotate = 0;
while(rotate++<4){
image = RotateImage.Rotate(image, rotate*90);
ByteArrayOutputStream imageIO = new ByteArrayOutputStream();
ImageIO.write(image, "PNG", imageIO);
MLIDPassportOCRResponse ocrResponse = TencentOcr.apiMLIDPassportOCR(imageIO.toByteArray(), "png");
if(ocrResponse == null){
continue;
}else{
success = true;
data.add(new PassportBean(""+(i+1), filePath, ocrResponse.getID(), ocrResponse.getName(), ocrResponse.getSex(), formatDate(ocrResponse.getDateOfBirth()), formatDate(ocrResponse.getDateOfExpiration()), ocrResponse.getIssuingCountry(), ocrResponse.getNationality(), ""));
break;
}
}
if(!success){
PassportBean pb = new PassportBean();
pb.setId(""+(i+1));
pb.setFile(filePath);
pb.setComments("OCR识别失败");
data.add(pb);
}
}
}else{
boolean success = false;
int rotate = 0;
BufferedImage image = ImageIO.read(file);
while(rotate++<4){
image = RotateImage.Rotate(image, rotate*90);
ByteArrayOutputStream imageIO = new ByteArrayOutputStream();
ImageIO.write(image, "PNG", imageIO);
MLIDPassportOCRResponse ocrResponse = TencentOcr.apiMLIDPassportOCR(imageIO.toByteArray(), "png");
if(ocrResponse != null){
success = true;
data.add(new PassportBean(""+(i+1), filePath, ocrResponse.getID(), ocrResponse.getName(), ocrResponse.getSex(), formatDate(ocrResponse.getDateOfBirth()), formatDate(ocrResponse.getDateOfExpiration()), ocrResponse.getIssuingCountry(), ocrResponse.getNationality(), ""));
break;
}
}
if(!success){
PassportBean pb = new PassportBean();
pb.setId(""+(i+1));
pb.setFile(filePath);
pb.setComments("OCR识别失败");
data.add(pb);
}
}
} catch (Exception e) {
logger.error("OCR识别异常", e);
}
}
Platform.runLater(() -> tipText.set(fileNum + "个文件已识别完成"));
}).start();
} catch (Exception e) {
logger.error(e.getMessage());
}
}
});
结果数据导出
这里我并没用直接将tableView中的数据导出到excel文件,因为用户很多时候是需要将识别到的某些内容复制到其他系统界面中。因此采用tableView内容允许复制的方法来实现数据导出(tableView默认是无法复制的)。
private void copyToClipboard(){
ObservableList<TablePosition> posList = resultTable.getSelectionModel().getSelectedCells();
int old_r = -1;
StringBuilder clipboardString = new StringBuilder();
for (TablePosition p : posList) {
int r = p.getRow();
int c = p.getColumn();
Object cell = p.getTableColumn().getCellData(r);
if (cell == null)
cell = "";
if (old_r == r)
clipboardString.append('\t');
else if (old_r != -1)
clipboardString.append('\n');
clipboardString.append(cell);
old_r = r;
}
final ClipboardContent content = new ClipboardContent();
content.putString(clipboardString.toString());
Clipboard.getSystemClipboard().setContent(content);
}
看一下上面的代码,允许用户选择多个cell,并在用户选择复制后在同一行的cell中添加tab,在不同行的内容间添加\n换行,这样当用户选择多行内容时,也可以直接粘贴到excel表中!
提供两种复制数据的方式。
1、 右键弹出复制菜单
//初始化结果表
resultTable.setItems(data);
resultTable.getSelectionModel().setCellSelectionEnabled(true);
resultTable.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
MenuItem item = new MenuItem("复制");
item.setOnAction(new EventHandler<ActionEvent>() {
@Override
public void handle(ActionEvent event) {
copyToClipboard();
}
});
ContextMenu menu = new ContextMenu();
menu.getItems().add(item);
resultTable.setContextMenu(menu);
2、ctrl+c快捷键复制
KeyCodeCombination keyCodeCopy = new KeyCodeCombination(KeyCode.C, KeyCombination.CONTROL_ANY);
resultTable.setOnKeyPressed(event -> {
if (keyCodeCopy.match(event)) {
copyToClipboard();
}
});
演示:

护照识别中

识别完成

多选,右键显示“复制”菜单

复制到excel文件中,可以自动分列换行