起因
前几天有个朋友问我有没有什么不需要登录QQ的OCR软件,我一琢磨:Steam上那个ShareX不就挺好的。
结果下载下来一看,旧版(13.X)用的是ocr.space的免费服务,新版是直接调用的本地OCR,总结下来就是一句话:识别不精准,需求达不到。
于是怒(不是)而写了一个对接百度OCR的Java程序,和大家一起分享。
(另:因为这个东西既涉及到对ShareX的反编译(修改OCR地址),又涉及到一个小Java项目的编写,但是考虑到ShareX是开源软件,而且修改的部分只占很小一部分,更多的是Java项目的实现思考,所以最后还是决定放在编程语言区……如果放错了区,本人万分抱歉)
(另2:在写的时候有一些问题,也希望和大家一起探讨~)
确定思路和对象
因为我实在懒得编译原版代码,于是我从ocr.space的入手,因为它是靠网络API来进行OCR识别,偷梁换柱比较方便。
(不是,我点了个保存草稿,怎么给我发出去了,算了那我接着写好了)
先用Dnspy进行一番查看,考虑到是OCR,我们直接搜Ocr,搜索到一个OcrSpace
毕竟不是商业软件,这逻辑一眼就能看懂。我们跑去ocr.space 的网站 https://ocr.space/OCRAPI , 不费吹灰之力我们就能得到它的API信息,请求和返回都有。
既然我们要用自己的OCR做OCR识别,(而我还不擅长C#,算了我怎么好像什么都不擅长)那么我们遵循这样一个思想:
让软件认为返回值是原本的接口返回,而实际上的OCR工作,由百度等其他接口实现。
粗俗点说就是:软件不动,我们改我们的服务器的项目,让服务器的返回值和原本的接口返回值保持一致(而OCR的内容由百度等其他接口实现)。
编写代码
先用Idea创建一个SpringBoot项目,引入fastjson,引入Baidu SDK。
<!-- https://mvnrepository.com/artifact/com.alibaba/fastjson -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>2.0.17</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.baidu.aip/java-sdk -->
<dependency>
<groupId>com.baidu.aip</groupId>
<artifactId>java-sdk</artifactId>
<version>4.16.12</version>
<exclusions>
<exclusion>
<artifactId>slf4j-simple</artifactId>
<groupId>org.slf4j</groupId>
</exclusion>
</exclusions>
</dependency>
然后我们观察一下dnspy里的接口:
Dictionary<string, string> dictionary = new Dictionary<string, string>();
dictionary.Add("apikey", this.APIKey);
dictionary.Add("language", this.Language.ToString());
dictionary.Add("isOverlayRequired", this.Overlay.ToString());
UploadResult uploadResult = base.SendRequestFile("https://apipro1.ocr.space/parse/image", stream, fileName, "file", dictionary, null, null, HttpMethod.POST, "multipart/form-data", null);
我们不难看出传值方式为POST,然后是multipart/form-data,显然传输的是二进制文件流,有了这些回到ocr.space查看接口定义,对应一下不难得到:
于是我们先写一个controller放着:
@RequestMapping("/select_ocr")
public ocrSpaceResponse selectOcr(HttpServletRequest request,
@RequestParam(value = "apikey") String apikey,
@RequestParam(value = "language") String language,
@RequestParam(value = "isOverlayRequired") String required,
@RequestParam(value = "file") MultipartFile file
) {}
之后我们创建一个service,用spring管理service对象:
public class BaiduAPI {
public static final String APP_ID = "";
public static final String API_KEY = "";
public static final String SECRET_KEY = "";
public static final String DATE_FORMAT = "yyyy-MM-dd HH:mm:ss";
AipOcr client;
public BaiduAPI()
{
client = new AipOcr(APP_ID, API_KEY, SECRET_KEY);
// 可选:设置网络连接参数
client.setConnectionTimeoutInMillis(2000);
client.setSocketTimeoutInMillis(60000);
}
multipart file先转换成base64,这样方便我们处理。
(结果根据百度SDK的说法,给百度SDK传值要么传图片地址,要么传图片数组,那么我们还需要一个base64转byte[]数组的代码。网上百度一下,就能找到对应的需求:
package com.example.demo.tools;
import com.example.demo.Demo1Application;
import org.slf4j.LoggerFactory;
import org.springframework.web.multipart.MultipartFile;
import sun.misc.BASE64Decoder;
import sun.misc.BASE64Encoder;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
public class ImageToBase64 {
private static final org.slf4j.Logger logger = LoggerFactory.getLogger(Demo1Application.class);
/**
* 将MultipartFile 图片文件编码为base64
* @Param file
* @return
* @throws Exception
*/
public static String generateBase64(MultipartFile file){
if (file == null || file.isEmpty()) {
throw new RuntimeException("图片不能为空!");
}
String fileName = file.getOriginalFilename();
String fileType = fileName.substring(fileName.lastIndexOf("."));
String contentType = file.getContentType();
byte[] imageBytes = null;
String base64EncoderImg="";
try {
imageBytes = file.getBytes();
BASE64Encoder base64Encoder =new BASE64Encoder();
/**
* 1.Java使用BASE64Encoder 需要添加图片头("data:" + contentType + ";base64,"),
* 其中contentType是文件的内容格式。
* 2.Java中在使用BASE64Enconder().encode()会出现字符串换行问题,这是因为RFC 822中规定,
* 每72个字符中加一个换行符号,这样会造成在使用base64字符串时出现问题,
* 所以我们在使用时要先用replaceAll("[\\s*\t\n\r]", "")解决换行的问题。
*/
base64EncoderImg = "data:" + contentType + ";base64," + base64Encoder.encode(imageBytes);
base64EncoderImg = base64EncoderImg.replaceAll("[\\s*\t\n\r]", "");
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
//logger.info("当前图片为:" + base64EncoderImg);
return base64EncoderImg;
}
public static byte[] base64ToImgByteArray(String base64){
try{
sun.misc.BASE64Decoder decoder = new sun.misc.BASE64Decoder();
//因为参数base64来源不一样,需要将附件数据替换清空掉。如果此入参来自canvas.toDataURL("image/png");
base64 = base64.replaceAll("data:image/png;base64,", "");
//base64解码并转为二进制数组
byte[] bytes = decoder.decodeBuffer(base64);
for (int i = 0; i < bytes.length; ++i) {
if (bytes[i] < 0) {// 调整异常数据
bytes[i] += 256;
}
}
return bytes;
}
catch (Exception e)
{
return null;
}
}
//这里对参数属性,参数来自html img.src或者canvas.toDataURL("image/png");
//如果是其他类型的图片请做base64 = base64.replaceAll("data:image/png;base64,", "");里面的png修改
}
接下来把ocr.space的接口返回和百度接口的返回JSON拖到GSONFormatplus里,生成对应的对象:(对象名分别是ocrSpaceResponse和BaiduResponse)
之后编写获取百度OCR的代码:
public ocrSpaceResponse getBaiduResult(String base64Pic,String language)
{
HashMap<String,String> options = new HashMap<>();
//options.put("detect_language", "true");
//这里的JSONObject并不是fastjson,而是org.json,所以还要转换一下,才能用到GsonFormatplus的对象里
JSONObject json = client.basicGeneral(base64ToImgByteArray(base64Pic),options);
com.alibaba.fastjson.JSONObject c = com.alibaba.fastjson.JSONObject.parseObject(json.toString());
//这里
BaiduResponse response1 = com.alibaba.fastjson.JSONObject.toJavaObject(c, BaiduResponse.class);
if(response1.getErrorMsg()==null)
{
StringBuilder wordback = new StringBuilder();
for(BaiduResponse.WordsResultDTO word:response1.getWordsResult())
{
//拼凑数据,因为百度返回和OCRSPACE对换行的处理不一样,这样就换成了OcrSpace的换行方式
wordback.append(word.getWords()).append("\r\n");
}
return CreateResponse.createResponse(wordback);
}
else {
return null;
}
}
修改一下client.basicGeneral,改成client.basicAccurateGeneral就是百度高精度了。
再看一眼这里的代码:
if (!uploadResult.IsSuccess)
{
uploadResult = base.SendRequestFile("https://apipro2.ocr.space/parse/image", stream, fileName, "file", dictionary, null, null, HttpMethod.POST, "multipart/form-data", null);
}
if (uploadResult.IsSuccess)
{
return JsonConvert.DeserializeObject<OCRSpaceResponse>(uploadResult.Response);
}
这个!IsSuccess就代表第一个接口失败。也是,毕竟百度免费SDK额度有限,用完了高精度的怎么办呢,再用低精度的就好。(毕竟加一起2000条,一个月用完属于是有点难度)
于是首先创建一个报错的异常
package com.example.demo.exceptions;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;
@ResponseStatus(code= HttpStatus.INTERNAL_SERVER_ERROR,reason="Failed so use another!")
public class ServerException extends Exception {
}
再从这里,检测到如果为空就抛出异常
@RequestMapping("/baidu_high_ocr")
public ocrSpaceResponse baidu_high_ocr(HttpServletRequest request,
@RequestParam(value = "apikey") String apikey,
@RequestParam(value = "language") String language,
@RequestParam(value = "isOverlayRequired") String required,
@RequestParam(value = "file") MultipartFile file
) throws ServerException {
logger.info("使用百度高精度");
String base = ImageToBase64.generateBase64(file);
ocrSpaceResponse response = baidu.getBaiduHighResult(base, "schinese");
if (response == null) {
throw new ServerException();
}
return response;
}
之后只需要修改一下dnspy这里的地址,再启动程序即可。(我的Springboot开的端口是8082)
public OCRSpaceResponse DoOCR(Stream stream, string fileName)
{
Dictionary<string, string> dictionary = new Dictionary<string, string>();
dictionary.Add("apikey", this.APIKey);
dictionary.Add("language", this.Language.ToString());
dictionary.Add("isOverlayRequired", this.Overlay.ToString());
UploadResult uploadResult = base.SendRequestFile("http://127.0.0.1:8082/baidu_low_ocr", stream, fileName, "file", dictionary, null, null, HttpMethod.POST, "multipart/form-data", null);
if (!uploadResult.IsSuccess)
{
uploadResult = base.SendRequestFile("http://127.0.0.1:8082/baidu_high_ocr", stream, fileName, "file", dictionary, null, null, HttpMethod.POST, "multipart/form-data", null);
}
if (uploadResult.IsSuccess)
{
return JsonConvert.DeserializeObject<OCRSpaceResponse>(uploadResult.Response);
}
return null;
}
来一张成功的截图:
最后有几个问题想和各位探讨一下:
- 在使用BaiduSdk的时候,它的JSON和我们的JSON库不一样,我是把它转换成了json文本,然后再用自己用的JSON库转换一次,才能让它直接赋值给对象。有没有比较好的方案去解决这个问题?
- 可以看到BaiduSDK的key值等是写死的(具体看我项目文件),有没有比较合适的手段让他从文件中读写?之前尝试过使用Springboot的application.properties但是赋值就变成空了(@Value注解)
还望各位大佬不吝赐教,小弟在此感谢各位。
demo1.zip
(128.49 KB, 下载次数: 75)