Android基于百度ocr识别相机预览回调onPreviewF
百度ocr的官方的示例有的都是点击拍照的,那要是直接从相机onPreviewFrame回调中获取实时数据呢?目前的需求就是实时扫描检测有没有对应文字,识别到了展示AR模型,好问题来了,这样的话肯定不能连续拍照,因为takePicture调用一次卡顿一次,这样的话就成了ppt了,所以只能从onPreviewFrame入手。
onPreviewFrame
onPreviewFrame是会实时返回相机的数据,
@Override
public void onPreviewFrame(byte[] data, Camera camera) {
}
但是遗憾的是这里面的byte[]是yuv,弄过ffmpeg的同学都知道手机屏幕支持的就是yuv。那么我们要转成我们通用的图片格式。第一时间就想到了YuvImage 这个系统提供的工具类,所以一顿乱搞,写出了如下代码:
YuvImage yuvImage = new YuvImage(data, ImageFormat.NV21, width, height, null);
ByteArrayOutputStream os = new ByteArrayOutputStream(data.length);
int fixWidth=width;
if (height*4!=width*3)
fixWidth=height*4/3;
if (yuvImage.compressToJpeg(new Rect((width-fixWidth)/2, 0, fixWidth, height), 75, os)) {
byte[] tmp = os.toByteArray();
}
一运行,华为P9崩溃,其他手机保存出来的图片不是一片绿就是一片红,而且搜索解决答案的时候还碰到了这篇文章
https://blog.csdn.net/q979713444/article/details/80446404
虽然我没验证,但至少前人踩过了坑啊。决定弃坑。
于是又找到了ScriptIntrinsicYuvToRGB这个工具类。
https://blog.csdn.net/qq1137830424/article/details/81980673
public class NV21ToBitmap {
private RenderScript rs;
private ScriptIntrinsicYuvToRGB yuvToRgbIntrinsic;
private Type.Builder yuvType, rgbaType;
private Allocation in, out;
public NV21ToBitmap(Context context) {
rs = RenderScript.create(context);
yuvToRgbIntrinsic = ScriptIntrinsicYuvToRGB.create(rs, Element.U8_4(rs));
}
public byte[] nv21ToBitmap(byte[] nv21, int width, int height) {
if (yuvType == null) {
yuvType = new Type.Builder(rs, Element.U8(rs)).setX(nv21.length);
in = Allocation.createTyped(rs, yuvType.create(), Allocation.USAGE_SCRIPT);
rgbaType = new Type.Builder(rs, Element.RGBA_8888(rs)).setX(width).setY(height);
out = Allocation.createTyped(rs, rgbaType.create(), Allocation.USAGE_SCRIPT);
}
in.copyFrom(nv21);
yuvToRgbIntrinsic.setInput(in);
yuvToRgbIntrinsic.forEach(out);
Bitmap bmpout = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
// byte[] bytes = new byte[];
out.copyTo(bmpout);
Bitmap rotatedBitmap = getRotatedBitmap(bmpout, 90);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
rotatedBitmap.compress(Bitmap.CompressFormat.JPEG, 75, baos);
byte[] data = baos.toByteArray();
// out.copyTo(bytes);
return data;
}
public Bitmap getRotatedBitmap(Bitmap bitmap, int rotation) {
Matrix matrix = new Matrix();
matrix.postRotate(rotation);
return Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(),
bitmap.getHeight(), matrix, false);
}
public File byteToFile(byte[] bytes, String path) {
File localFile = null;
try {
// 根据绝对路径初始化文件
localFile = new File(path);
if (!localFile.exists()) {
localFile.createNewFile();
}
// 输出流
OutputStream os = new FileOutputStream(localFile);
os.write(bytes);
os.close();
} catch (Exception e) {
e.printStackTrace();
}
return localFile;
}
}
getRotatedBitmap是我对作者的改进,因为不旋转90度的话图片是横向的。
初始化ocr
public class OCRService {
private volatile static OCRService instence;
private boolean hasGotToken;
public synchronized static OCRService getInstence() {
if(instence == null){
synchronized (OCRService.class){
if(instence == null){
instence = new OCRService();
}
}
}
return instence;
}
public OCRService(){
}
/**
* 用明文ak,sk初始化
*/
public void initAccessTokenWithAkSk(Context context) {
OCR.getInstance(context).initAccessTokenWithAkSk(new OnResultListener<AccessToken>() {
@Override
public void onResult(AccessToken result) {
String token = result.getAccessToken();
hasGotToken = true;
}
@Override
public void onError(OCRError error) {
((Activity)context).runOnUiThread(new Runnable() {
@Override
public void run() {
error.printStackTrace();
Toast.makeText(context, "AK,SK方式获取token失败"+error.getMessage(), Toast.LENGTH_SHORT).show();
}
});
}
}, ((Activity)context).getApplicationContext(), "DIr4b4G94yWH2KpO757k6DqV", "KveP20sb53fQ5GU2hQbDx1mkhaBVIMyz");
}
public boolean checkTokenStatus(Context context) {
if (!hasGotToken) {
Toast.makeText(context, "token还未成功获取", Toast.LENGTH_LONG).show();
}
return hasGotToken;
}
}
上面是将ocr代码统一放入一个类管理。然后再activity中一开始就初始化他。
OCRService.getInstence().initAccessTokenWithAkSk(OCRActivity.this);
最后在相机回调中写识别代码
@Override
public void onPreviewFrame(byte[] data, Camera camera) {
.......
if (!OCRService.getInstence().checkTokenStatus(OCRActivity.this)) {
return;
}
if(!mIsRun){
return;
}
mIsRun = false;
Camera.Size pictureSize = cameraUtils.getParameters().getPictureSize();
byte[] bytes = nv21ToBitmap.nv21ToBitmap(data, pictureSize.width, pictureSize.height);
File file = nv21ToBitmap.byteToFile(bytes, Environment.getExternalStorageDirectory().getPath() + File.separator + "shpd.jpg");
if(file != null){
OcrRequestParams param = new OcrRequestParams();
param.setImageFile(file);
OCR.getInstance(OCRActivity.this).recognizeHandwriting(param, new OnResultListener<OcrResponseResult>() {
@Override
public void onResult(OcrResponseResult result) {
Log.d("yanjin","ocr-----result="+result.getJsonRes());
}
@Override
public void onError(OCRError error) {
Log.d("yanjin","ocr-----error="+error.getMessage());
}
});
}
}
这里本人做的是点击一下按钮识别一次,通过mIsRun来控制,如果要做成实时识别也只要做一个标志位,默认为true,进入到onPreviewFrame里面的第一行代码就判断是否识别,如果不能就直接返回,如果可以就设置为false后执行识别代码,然后再ocr的回调,成功和失败都将标志位复原为true,这样基本能完成,如果涉及到多线程,最好写成同步方法,标志位也用volatile修饰,尽量解决线程冲突问题。