Android实现扫一扫识别图像数字(使用训练的库拍照查看扫描结果)(下)
- 关于
- 效果图
- 第一步,添加我们的训练库
- 编写扫描框控件
- 新建扫码界面ScannerActivity.java
- 关于二维码拍照的代码
- 图片的优化以及解码
- 获取页面是否活动的时间帮助类
- 使用Tesseract-OCR
- 自定义弹窗
- 修改ScannerActivity.java
关于
最近在整理电脑上面的项目,想起之前有研究学习图像数字识别功能实现,只记录了上篇,然后下半篇忘记写了,现在在回来看一下,有的地方自己也有些生疏了,总之也是借鉴了网上一部分。本篇代码内容较多,**不想细看的话可以直接跳到末尾,我会将源码放出来。**想知道如何训练数据的可以参考上一篇博文《Android实现扫一扫识别图像数字(镂空图像数字Tesseract训练)(上)》
效果图
也可以下载apk试一下效果:
链接:https://pan.baidu.com/s/1TA2o4ABt3bnqjTAA1gIpjQ
提取码:1234
当然了,识别效果不是每次都正确的,因为训练库里面的数据不够庞大,所以误差率还是不小的,不然如果想要正式使用的话,有大量的训练数据就没关系了
第一步,添加我们的训练库
在res/下新建raw文件夹,将我们上篇训练的num.traineddata拷贝进去:
这里我把我的训练好的数据放到网盘里:
链接:https://pan.baidu.com/s/1ekvpF6nZbPfNOxJGpOTjOg
提取码:1234
编写扫描框控件
public final class ScannerFinderView extends RelativeLayout {
private static final int[] SCANNER_ALPHA = { 0, 64, 128, 192, 255, 192, 128, 64 };
private static final long ANIMATION_DELAY = 100L;
private static final int OPAQUE = 0xFF;
private static final int MIN_FOCUS_BOX_WIDTH = 50;
private static final int MIN_FOCUS_BOX_HEIGHT = 50;
private static final int MIN_FOCUS_BOX_TOP = 200;
private static Point ScrRes;
private int top;
private Paint mPaint;
private int mScannerAlpha;
private int mMaskColor;
private int mFrameColor;
private int mLaserColor;
private int mTextColor;
private int mFocusThick;
private int mAngleThick;
private int mAngleLength;
private Rect mFrameRect; //绘制的Rect
private Rect mRect; //返回的Rect
public ScannerFinderView(Context context) {
this(context, null);
}
public ScannerFinderView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public ScannerFinderView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mPaint = new Paint();
mPaint.setAntiAlias(true);
Resources resources = getResources();
mMaskColor = resources.getColor(R.color.finder_mask);
mFrameColor = resources.getColor(R.color.finder_frame);
mLaserColor = resources.getColor(R.color.finder_laser);
mTextColor = resources.getColor(R.color.white);
mFocusThick = 1;
mAngleThick = 8;
mAngleLength = 40;
mScannerAlpha = 0;
init(context);
this.setOnTouchListener(getTouchListener());
}
private void init(Context context) {
if (isInEditMode()) {
return;
}
// 需要调用下面的方法才会执行onDraw方法
setWillNotDraw(false);
if (mFrameRect == null) {
ScrRes = ScreenUtils.getScreenResolution(context);
int width = ScrRes.x * 3 / 5;
int height = width;
width = width == 0
? MIN_FOCUS_BOX_WIDTH
: width < MIN_FOCUS_BOX_WIDTH ? MIN_FOCUS_BOX_WIDTH : width;
height = height == 0
? MIN_FOCUS_BOX_HEIGHT
: height < MIN_FOCUS_BOX_HEIGHT ? MIN_FOCUS_BOX_HEIGHT : height;
int left = (ScrRes.x - width) / 2;
int top = (ScrRes.y - height) / 5;
this.top = top; //记录初始距离上方距离
mFrameRect = new Rect(left, top, left + width, top + height);
mRect = mFrameRect;
}
}
public Rect getRect() {
return mRect;
}
@Override
public void onDraw(Canvas canvas) {
if (isInEditMode()) {
return;
}
Rect frame = mFrameRect;
if (frame == null) {
return;
}
int width = canvas.getWidth();
int height = canvas.getHeight();
// 绘制焦点框外边的暗色背景
mPaint.setColor(mMaskColor);
canvas.drawRect(0, 0, width, frame.top, mPaint);
canvas.drawRect(0, frame.top, frame.left, frame.bottom + 1, mPaint);
canvas.drawRect(frame.right + 1, frame.top, width, frame.bottom + 1, mPaint);
canvas.drawRect(0, frame.bottom + 1, width, height, mPaint);
drawFocusRect(canvas, frame);
drawAngle(canvas, frame);
drawText(canvas, frame);
drawLaser(canvas, frame);
}
/**
* 画聚焦框,白色的
*
* @param canvas
* @param rect
*/
private void drawFocusRect(Canvas canvas, Rect rect) {
// 绘制焦点框(黑色)
mPaint.setColor(mFrameColor);
// 上
canvas.drawRect(rect.left + mAngleLength, rect.top, rect.right - mAngleLength, rect.top + mFocusThick, mPaint);
// 左
canvas.drawRect(rect.left, rect.top + mAngleLength, rect.left + mFocusThick, rect.bottom - mAngleLength,
mPaint);
// 右
canvas.drawRect(rect.right - mFocusThick, rect.top + mAngleLength, rect.right, rect.bottom - mAngleLength,
mPaint);
// 下
canvas.drawRect(rect.left + mAngleLength, rect.bottom - mFocusThick, rect.right - mAngleLength, rect.bottom,
mPaint);
}
/**
* 画四个角
*
* @param canvas
* @param rect
*/
private void drawAngle(Canvas canvas, Rect rect) {
mPaint.setColor(mLaserColor);
mPaint.setAlpha(OPAQUE);
mPaint.setStyle(Paint.Style.FILL);
mPaint.setStrokeWidth(mAngleThick);
int left = rect.left;
int top = rect.top;
int right = rect.right;
int bottom = rect.bottom;
// 左上角
canvas.drawRect(left, top, left + mAngleLength, top + mAngleThick, mPaint);
canvas.drawRect(left, top, left + mAngleThick, top + mAngleLength, mPaint);
// 右上角
canvas.drawRect(right - mAngleLength, top, right, top + mAngleThick, mPaint);
canvas.drawRect(right - mAngleThick, top, right, top + mAngleLength, mPaint);
// 左下角
canvas.drawRect(left, bottom - mAngleLength, left + mAngleThick, bottom, mPaint);
canvas.drawRect(left, bottom - mAngleThick, left + mAngleLength, bottom, mPaint);
// 右下角
canvas.drawRect(right - mAngleLength, bottom - mAngleThick, right, bottom, mPaint);
canvas.drawRect(right - mAngleThick, bottom - mAngleLength, right, bottom, mPaint);
}
private void drawText(Canvas canvas, Rect rect) {
int margin = 40;
mPaint.setColor(mTextColor);
mPaint.setTextSize(getResources().getDimension(R.dimen.text_size_13sp)); //13dp
String text = getResources().getString(R.string.auto_scan_notification); //<stringname="auto_scan_notification">将扫描内容放入框内,即可自动扫描</string>
Paint.FontMetrics fontMetrics = mPaint.getFontMetrics();
float fontTotalHeight = fontMetrics.bottom - fontMetrics.top;
float offY = fontTotalHeight / 2 - fontMetrics.bottom;
float newY = rect.bottom + margin + offY;
float left = (ScreenUtils.getScreenWidth() - mPaint.getTextSize() * text.length()) / 2;
canvas.drawText(text, left, newY, mPaint);
}
private void drawLaser(Canvas canvas, Rect rect) {
// 绘制焦点框内固定的一条扫描线
mPaint.setColor(mLaserColor);
mPaint.setAlpha(SCANNER_ALPHA[mScannerAlpha]);
mScannerAlpha = (mScannerAlpha + 1) % SCANNER_ALPHA.length;
int middle = rect.height() / 2 + rect.top;
canvas.drawRect(rect.left + 2, middle - 1, rect.right - 1, middle + 2, mPaint);
mHandler.sendEmptyMessageDelayed(1, ANIMATION_DELAY);
}
private Handler mHandler = new Handler(){
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
invalidate();
}
};
private OnTouchListener touchListener;
private OnTouchListener getTouchListener() {
if (touchListener == null){
touchListener = new OnTouchListener() {
int lastX = -1;
int lastY = -1;
@Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
lastX = -1;
lastY = -1;
return true;
case MotionEvent.ACTION_MOVE:
int currentX = (int) event.getX();
int currentY = (int) event.getY();
try {
Rect rect = mFrameRect;
final int BUFFER = 60;
if (lastX >= 0) {
boolean currentXLeft = currentX >= rect.left - BUFFER && currentX <= rect.left + BUFFER;
boolean currentXRight = currentX >= rect.right - BUFFER && currentX <= rect.right + BUFFER;
boolean lastXLeft = lastX >= rect.left - BUFFER && lastX <= rect.left + BUFFER;
boolean lastXRight = lastX >= rect.right - BUFFER && lastX <= rect.right + BUFFER;
boolean currentYTop = currentY <= rect.top + BUFFER && currentY >= rect.top - BUFFER;
boolean currentYBottom = currentY <= rect.bottom + BUFFER && currentY >= rect.bottom - BUFFER;
boolean lastYTop = lastY <= rect.top + BUFFER && lastY >= rect.top - BUFFER;
boolean lastYBottom = lastY <= rect.bottom + BUFFER && lastY >= rect.bottom - BUFFER;
boolean XLeft = currentXLeft || lastXLeft;
boolean XRight = currentXRight || lastXRight;
boolean YTop = currentYTop || lastYTop;
boolean YBottom = currentYBottom || lastYBottom;
boolean YTopBottom = (currentY <= rect.bottom && currentY >= rect.top)
|| (lastY <= rect.bottom && lastY >= rect.top);
boolean XLeftRight = (currentX <= rect.right && currentX >= rect.left)
|| (lastX <= rect.right && lastX >= rect.left);
//右上角
if (XLeft && YTop) {
updateBoxRect(2 * (lastX - currentX), (lastY - currentY), true);
//左上角
} else if (XRight && YTop) {
updateBoxRect(2 * (currentX - lastX), (lastY - currentY), true);
//右下角
} else if (XLeft && YBottom) {
updateBoxRect(2 * (lastX - currentX), (currentY - lastY), false);
//左下角
} else if (XRight && YBottom) {
updateBoxRect(2 * (currentX - lastX), (currentY - lastY), false);
//左侧
} else if (XLeft && YTopBottom) {
updateBoxRect(2 * (lastX - currentX), 0, false);
//右侧
} else if (XRight && YTopBottom) {
updateBoxRect(2 * (currentX - lastX), 0, false);
//上方
} else if (YTop && XLeftRight) {
updateBoxRect(0, (lastY - currentY), true);
//下方
} else if (YBottom && XLeftRight) {
updateBoxRect(0, (currentY - lastY), false);
}
}
} catch (NullPointerException e) {
e.printStackTrace();
}
v.invalidate();
lastX = currentX;
lastY = currentY;
return true;
case MotionEvent.ACTION_UP:
//移除之前的刷新
mHandler.removeMessages(1);
//松手时对外更新
mRect = mFrameRect;
lastX = -1;
lastY = -1;
return true;
default:
}
return false;
}
};
}
return touchListener;
}
private void updateBoxRect(int dW, int dH, boolean isUpward) {
int newWidth = (mFrameRect.width() + dW > ScrRes.x - 4 || mFrameRect.width() + dW < MIN_FOCUS_BOX_WIDTH)
? 0 : mFrameRect.width() + dW;
//限制扫描框最大高度不超过屏幕宽度
int newHeight = (mFrameRect.height() + dH > ScrRes.x || mFrameRect.height() + dH < MIN_FOCUS_BOX_HEIGHT)
? 0 : mFrameRect.height() + dH;
int leftOffset = (ScrRes.x - newWidth) / 2;
if (isUpward){
this.top -= dH;
}
int topOffset = this.top;
if (topOffset < MIN_FOCUS_BOX_TOP){
this.top = MIN_FOCUS_BOX_TOP;
return;
}
if (topOffset + newHeight > MIN_FOCUS_BOX_TOP + ScrRes.x){
return;
}
if (newWidth < MIN_FOCUS_BOX_WIDTH || newHeight < MIN_FOCUS_BOX_HEIGHT){
return;
}
mFrameRect = new Rect(leftOffset, topOffset, leftOffset + newWidth, topOffset + newHeight);
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
mHandler.removeMessages(1);
}
}
对应的颜色代码:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="white">#FFFFFFFF</color>
<color name="finder_mask">#80000000</color>
<color name="finder_frame">#FFFFFFFF</color>
<color name="finder_laser">#0db8f6</color>
</resources>
获取屏幕尺寸的帮助类ScreenUtils:
/**
* ScreenUtils
*/
public class ScreenUtils {
private ScreenUtils() {
throw new AssertionError();
}
/**
* 获取屏幕宽度
*
* @return
*/
public static int getScreenWidth() {
Context context = MyApplication.sAppContext;
DisplayMetrics dm = context.getResources().getDisplayMetrics();
return dm.widthPixels;
}
/**
* 获取屏幕高度
*
* @return
*/
public static int getScreenHeight() {
Context context = MyApplication.sAppContext;
DisplayMetrics dm = context.getResources().getDisplayMetrics();
return dm.heightPixels;
}
public static Point getScreenResolution(Context context) {
WindowManager manager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
Display display = manager.getDefaultDisplay();
int width = display.getWidth();
int height = display.getHeight();
return new Point(width, height);
}
}
新建扫码界面ScannerActivity.java
新建一个页面ScannerActivity,其中ScannerActivity.xml布局如下:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ViewStub
android:id="@+id/qr_code_view_stub"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"/>
<TextView
android:id="@+id/qr_code_header_bar"
android:layout_width="match_parent"
android:gravity="center"
android:layout_height="@dimen/title_bar_height"
android:background="@android:color/black"
android:text="@string/title_activity_scan_qr_code"
android:textColor="@color/white"
android:textSize="18sp" />
<com.zl.tesseract.scanner.view.ScannerFinderView
android:id="@+id/qr_code_view_finder"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_centerInParent="true"
android:visibility="gone"/>
<View
android:layout_below="@id/qr_code_header_bar"
android:id="@+id/qr_code_view_background"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/black"
android:visibility="gone"/>
<Switch
android:id="@+id/switch1"
android:text="@string/is_qr_code_scanner"
android:layout_margin="20dp"
android:layout_alignParentBottom="true"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<Switch
android:id="@+id/switch2"
android:text="@string/is_open_flashlight"
android:layout_margin="20dp"
android:layout_alignParentRight="true"
android:layout_alignParentEnd="true"
android:layout_centerHorizontal="true"
android:layout_alignParentBottom="true"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</RelativeLayout>
为方便阅读,我将strings.xml代码贴一下:
<resources>
<string name="app_name">Tesseract-OCR-Scanner</string>
<!--扫一扫-->
<string name="title_activity_scan_qr_code">扫一扫</string>
<string name="close">关闭</string>
<string name="auto_scan_notification">将扫描内容放入框内,即可自动扫描</string>
<string name="notification">提示</string>
<string name="positive_button_confirm">确认</string>
<string name="could_not_read_qr_code_from_scanner">对不起,无法打开扫出的内容</string>
<string name="camera_not_found">未检测到相机</string>
<string name="is_qr_code_scanner">是否扫码 </string>
<string name="is_open_flashlight">是否打开闪光灯 </string>
<string name="take_photos">拍照识别数字</string>
</resources>
修改ScannerActivity.java代码:
在进入页面首先需要判断是否有拍照和存储权限:
@Override
public void onCreate(Bundle savedInstanceState) {
requestWindowFeature(Window.FEATURE_NO_TITLE);
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_scanner);
if (ContextCompat.checkSelfPermission(this, CAMERA) != PackageManager.PERMISSION_GRANTED ||
ContextCompat.checkSelfPermission(this, READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this, new String[]{CAMERA, READ_EXTERNAL_STORAGE}, 100);
} else {
initView();
initData();
}
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == 100){
boolean permissionGranted = true;
for (int i : grantResults) {
if (i != PackageManager.PERMISSION_GRANTED) {
permissionGranted = false;
}
}
if (permissionGranted){
initView();
initData();
}else {
// 无权限退出
finish();
}
}
}
还要在AndroidManifest.xml配置文件中添加权限清单:
<uses-permission android:name="android.permission.CAMERA"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.VIBRATE"/>
<uses-feature
android:name="android.hardware.camera"
android:required="false"/>
<uses-feature
android:name="android.hardware.camera.autofocus"
android:required="false"/>
<uses-feature
android:name="android.hardware.camera.flash"
android:required="false"/>
关于二维码拍照的代码
封装Camera服务对象,并预览和解码管理类
/**
* This object wraps the Camera service object and expects to be the only one talking to it. The implementation
* encapsulates the steps needed to take preview-sized images, which are used for both preview and decoding.
*/
public final class CameraManager {
private static CameraManager sCameraManager;
private final CameraConfigurationManager mConfigManager;
/**
* Preview frames are delivered here, which we pass on to the registered handler. Make sure to clear the handler so
* it will only receive one message.
*/
//预览相机查看到的内容回调
private final PreviewCallback mPreviewCallback;
/** Auto-focus callbacks arrive here, and are dispatched to the Handler which requested them. */
//自动聚焦回调
private final AutoFocusCallback mAutoFocusCallback;
private Camera mCamera;
private boolean mInitialized;
private boolean mPreviewing;
private boolean useAutoFocus;
private CameraManager() {
this.mConfigManager = new CameraConfigurationManager();
mPreviewCallback = new PreviewCallback(mConfigManager);
mAutoFocusCallback = new AutoFocusCallback();
}
/**
* Initializes this static object with the Context of the calling Activity.
*/
public static void init() {
if (sCameraManager == null) {
sCameraManager = new CameraManager();
}
}
/**
* Gets the CameraManager singleton instance.
*
* @return A reference to the CameraManager singleton.
*/
public static CameraManager get() {
return sCameraManager;
}
/**
* Opens the mCamera driver and initializes the hardware parameters.
*
* @param holder The surface object which the mCamera will draw preview frames into.
* @throws IOException Indicates the mCamera driver failed to open.
*/
public boolean openDriver(SurfaceHolder holder) throws IOException {
if (mCamera == null) {
try {
mCamera = Camera.open();
if (mCamera != null) {
// setParameters 是针对魅族MX5做的。MX5通过Camera.open()拿到的Camera 对象不为null
Camera.Parameters mParameters = mCamera.getParameters();
mCamera.setParameters(mParameters);
mCamera.setPreviewDisplay(holder);
String currentFocusMode = mCamera.getParameters().getFocusMode();
useAutoFocus = FOCUS_MODES_CALLING_AF.contains(currentFocusMode);
if (!mInitialized) {
mInitialized = true;
mConfigManager.initFromCameraParameters(mCamera);
}
mConfigManager.setDesiredCameraParameters(mCamera);
return true;
}
} catch (Exception e) {
e.printStackTrace();
}
}
return false;
}
/**
* Closes the camera driver if still in use.
*/
public boolean closeDriver() {
if (mCamera != null) {
try {
mCamera.release();
mInitialized = false;
mPreviewing = false;
mCamera = null;
return true;
} catch (Exception e) {
e.printStackTrace();
}
}
return false;
}
/**
* 打开或关闭闪光灯
*
* @param open 控制是否打开
* @return 打开或关闭失败,则返回false。
*/
public boolean setFlashLight(boolean open) {
if (mCamera == null || !mPreviewing) {
return false;
}
Camera.Parameters parameters = mCamera.getParameters();
if (parameters == null) {
return false;
}
List<String> flashModes = parameters.getSupportedFlashModes();
// Check if camera flash exists
if (null == flashModes || 0 == flashModes.size()) {
// Use the screen as a flashlight (next best thing)
return false;
}
String flashMode = parameters.getFlashMode();
if (open) {
if (Camera.Parameters.FLASH_MODE_TORCH.equals(flashMode)) {
return true;
}
// Turn on the flash
if (flashModes.contains(Camera.Parameters.FLASH_MODE_TORCH)) {
parameters.setFlashMode(Camera.Parameters.FLASH_MODE_TORCH);
mCamera.setParameters(parameters);
return true;
} else {
return false;
}
} else {
if (Camera.Parameters.FLASH_MODE_OFF.equals(flashMode)) {
return true;
}
// Turn on the flash
if (flashModes.contains(Camera.Parameters.FLASH_MODE_OFF)) {
parameters.setFlashMode(Camera.Parameters.FLASH_MODE_OFF);
mCamera.setParameters(parameters);
return true;
} else {
return false;
}
}
}
/**
* Asks the mCamera hardware to begin drawing preview frames to the screen.
*/
public boolean startPreview() {
if (mCamera != null && !mPreviewing) {
try {
mCamera.startPreview();
mPreviewing = true;
return true;
} catch (Exception e) {
e.printStackTrace();
}
}
return false;
}
/**
* Tells the mCamera to stop drawing preview frames.
*/
public boolean stopPreview() {
if (mCamera != null && mPreviewing) {
try {
// 停止预览时把callback移除.
mCamera.setOneShotPreviewCallback(null);
mCamera.stopPreview();
mPreviewCallback.setHandler(null, 0);
mAutoFocusCallback.setHandler(null, 0);
mPreviewing = false;
return true;
} catch (Exception e) {
e.printStackTrace();
}
}
return false;
}
/**
* A single preview frame will be returned to the handler supplied. The data will arrive as byte[] in the
* message.obj field, with width and height encoded as message.arg1 and message.arg2, respectively.
*
* @param handler The handler to send the message to.
* @param message The what field of the message to be sent.
*/
public void requestPreviewFrame(Handler handler, int message) {
if (mCamera != null && mPreviewing) {
mPreviewCallback.setHandler(handler, message);
mCamera.setOneShotPreviewCallback(mPreviewCallback);
}
}
/**
* Asks the mCamera hardware to perform an autofocus.
*
* @param handler The Handler to notify when the autofocus completes.
* @param message The message to deliver.
*/
public void requestAutoFocus(Handler handler, int message) {
if (mCamera != null && mPreviewing) {
mAutoFocusCallback.setHandler(handler, message);
// Log.d(TAG, "Requesting auto-focus callback");
if (useAutoFocus) {
try {
mCamera.autoFocus(mAutoFocusCallback);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
/**
* 通过调用底层摄像头api拍照
*
* @param shutterCallback 图像捕获时间的回调
* @param jpegPictureCallback JEPG图片回调
* @param rawPictureCallback,原始图像回调
*/
public void takeShot(Camera.ShutterCallback shutterCallback,
Camera.PictureCallback rawPictureCallback,
Camera.PictureCallback jpegPictureCallback ){
mCamera.takePicture(shutterCallback, rawPictureCallback, jpegPictureCallback);
}
private static final Collection<String> FOCUS_MODES_CALLING_AF;
static {
FOCUS_MODES_CALLING_AF = new ArrayList<String>(2);
FOCUS_MODES_CALLING_AF.add(Camera.Parameters.FOCUS_MODE_AUTO);
FOCUS_MODES_CALLING_AF.add(Camera.Parameters.FOCUS_MODE_MACRO);
}
}
预览回调方法
final class PreviewCallback implements Camera.PreviewCallback {
private static final String TAG = PreviewCallback.class.getName();
//相机的参数的管理方法
private final CameraConfigurationManager mConfigManager;
private Handler mPreviewHandler;
private int mPreviewMessage;
PreviewCallback(CameraConfigurationManager configManager) {
this.mConfigManager = configManager;
}
void setHandler(Handler previewHandler, int previewMessage) {
this.mPreviewHandler = previewHandler;
this.mPreviewMessage = previewMessage;
}
@Override
public void onPreviewFrame(byte[] data, Camera camera) {
//获取分辨率
Point point = mConfigManager.getCameraResolution();
if (mPreviewHandler != null) {
Message message =
mPreviewHandler.obtainMessage(mPreviewMessage, point.x, point.y,
data);
message.sendToTarget();
mPreviewHandler = null;
} else {
Log.v(TAG, "no handler callback.");
}
}
}
相机参数设置封装方法类CameraConfigurationManager.java
**
* 设置相机的参数信息,获取最佳的预览界面
*
*/
public final class CameraConfigurationManager {
private static final String TAG = "CameraConfiguration";
// 屏幕分辨率
private Point screenResolution;
// 相机分辨率
private Point cameraResolution;
public void initFromCameraParameters(Camera camera) {
// 需要判断摄像头是否支持缩放
Camera.Parameters parameters = camera.getParameters();
if (parameters.isZoomSupported()) {
// 设置成最大倍数的1/10,基本符合远近需求
parameters.setZoom(parameters.getMaxZoom() / 10);
}
if (parameters.getMaxNumFocusAreas() > 0) {
List focusAreas = new ArrayList();
Rect focusRect = new Rect(-900, -900, 900, 0);
focusAreas.add(new Camera.Area(focusRect, 1000));
parameters.setFocusAreas(focusAreas);
}
WindowManager manager = (WindowManager) MyApplication.sAppContext.getSystemService(Context.WINDOW_SERVICE);
Display display = manager.getDefaultDisplay();
// Point theScreenResolution = getDisplaySize(display);
// theScreenResolution = getDisplaySize(display);
screenResolution = getDisplaySize(display);
Log.i(TAG, "Screen resolution: " + screenResolution);
/** 因为换成了竖屏显示,所以不替换屏幕宽高得出的预览图是变形的 */
Point screenResolutionForCamera = new Point();
screenResolutionForCamera.x = screenResolution.x;
screenResolutionForCamera.y = screenResolution.y;
if (screenResolution.x < screenResolution.y) {
screenResolutionForCamera.x = screenResolution.y;
screenResolutionForCamera.y = screenResolution.x;
}
cameraResolution = CameraConfigurationUtils.findBestPreviewSizeValue(parameters, screenResolutionForCamera);
Log.i(TAG, "Camera resolution x: " + cameraResolution.x);
Log.i(TAG, "Camera resolution y: " + cameraResolution.y);
}
@SuppressWarnings("deprecation")
@SuppressLint("NewApi")
private Point getDisplaySize(final Display display) {
final Point point = new Point();
try {
display.getSize(point);
} catch (NoSuchMethodError ignore) {
point.x = display.getWidth();
point.y = display.getHeight();
}
return point;
}
public void setDesiredCameraParameters(Camera camera) {
Camera.Parameters parameters = camera.getParameters();
if (parameters == null) {
Log.w(TAG, "Device error: no camera parameters are available. Proceeding without configuration.");
return;
}
Log.i(TAG, "Initial camera parameters: " + parameters.flatten());
// if (safeMode) {
// Log.w(TAG, "In camera config safe mode -- most settings will not be honored");
// }
parameters.setPreviewSize(cameraResolution.x, cameraResolution.y);
camera.setParameters(parameters);
Camera.Parameters afterParameters = camera.getParameters();
Camera.Size afterSize = afterParameters.getPreviewSize();
if (afterSize != null && (cameraResolution.x != afterSize.width || cameraResolution.y != afterSize.height)) {
Log.w(TAG, "Camera said it supported preview size " + cameraResolution.x + 'x' + cameraResolution.y + ", but after setting it, preview size is " + afterSize.width + 'x' + afterSize.height);
cameraResolution.x = afterSize.width;
cameraResolution.y = afterSize.height;
}
/** 设置相机预览为竖屏 */
camera.setDisplayOrientation(90);
}
public Point getCameraResolution() {
return cameraResolution;
}
public Point getScreenResolution() {
return screenResolution;
}
}
获取最佳预览界面大小CameraConfigurationUtils.java
/**
* Utility methods for configuring the Android camera.
*
* @author Sean Owen
*/
@SuppressWarnings("deprecation") // camera APIs
public final class CameraConfigurationUtils {
private static final String TAG = "CameraConfiguration";
private static final int MIN_PREVIEW_PIXELS = 480 * 320; // normal screen
private static final double MAX_ASPECT_DISTORTION = 0.15;
public static Point findBestPreviewSizeValue(Camera.Parameters parameters, Point screenResolution) {
List<Camera.Size> rawSupportedSizes = parameters.getSupportedPreviewSizes();
if (rawSupportedSizes == null) {
Log.w(TAG, "Device returned no supported preview sizes; using default");
Camera.Size defaultSize = parameters.getPreviewSize();
if (defaultSize == null) {
throw new IllegalStateException("Parameters contained no preview size!");
}
return new Point(defaultSize.width, defaultSize.height);
}
if (Log.isLoggable(TAG, Log.INFO)) {
StringBuilder previewSizesString = new StringBuilder();
for (Camera.Size size : rawSupportedSizes) {
previewSizesString.append(size.width).append('x').append(size.height).append(' ');
}
Log.i(TAG, "Supported preview sizes: " + previewSizesString);
}
// double screenAspectRatio = screenResolution.x / (double) screenResolution.y;
// Find a suitable size, with max resolution
// int maxResolution = 0;
Camera.Size maxResPreviewSize = null;
int diff = Integer.MAX_VALUE;
for (Camera.Size size : rawSupportedSizes) {
int realWidth = size.width;
int realHeight = size.height;
int resolution = realWidth * realHeight;
if (resolution < MIN_PREVIEW_PIXELS) {
continue;
}
boolean isCandidatePortrait = realWidth < realHeight;
int maybeFlippedWidth = isCandidatePortrait ? realHeight : realWidth;
int maybeFlippedHeight = isCandidatePortrait ? realWidth : realHeight;
// double aspectRatio = maybeFlippedWidth / (double) maybeFlippedHeight;
// double distortion = Math.abs(aspectRatio - screenAspectRatio);
// if (distortion > MAX_ASPECT_DISTORTION) {
// continue;
// }
if (maybeFlippedWidth == screenResolution.x && maybeFlippedHeight == screenResolution.y) {
Point exactPoint = new Point(realWidth, realHeight);
Log.i(TAG, "Found preview size exactly matching screen size: " + exactPoint);
return exactPoint;
}
int newDiff = Math.abs(maybeFlippedWidth - screenResolution.x) + Math.abs(maybeFlippedHeight - screenResolution.y);
if (newDiff < diff) {
maxResPreviewSize = size;
diff = newDiff;
}
// Resolution is suitable; record the one with max resolution
// if (resolution > maxResolution) {
// maxResolution = resolution;
// maxResPreviewSize = size;
// }
}
// If no exact match, use largest preview size. This was not a great idea on older devices because
// of the additional computation needed. We're likely to get here on newer Android 4+ devices, where
// the CPU is much more powerful.
if (maxResPreviewSize != null) {
Point largestSize = new Point(maxResPreviewSize.width, maxResPreviewSize.height);
Log.i(TAG, "Using largest suitable preview size: " + largestSize);
return largestSize;
}
// If there is nothing at all suitable, return current preview size
Camera.Size defaultPreview = parameters.getPreviewSize();
if (defaultPreview == null) {
throw new IllegalStateException("Parameters contained no preview size!");
}
Point defaultSize = new Point(defaultPreview.width, defaultPreview.height);
Log.i(TAG, "No suitable preview sizes, using default: " + defaultSize);
return defaultSize;
}
}
自动对焦方法回调AutoFocusCallback.java
final class AutoFocusCallback implements Camera.AutoFocusCallback {
private static final String TAG = AutoFocusCallback.class.getName();
private static final long AUTO_FOCUS_INTERVAL_MS = 1300L; //自动对焦时间
private Handler mAutoFocusHandler;
private int mAutoFocusMessage;
void setHandler(Handler autoFocusHandler, int autoFocusMessage) {
this.mAutoFocusHandler = autoFocusHandler;
this.mAutoFocusMessage = autoFocusMessage;
}
@Override
public void onAutoFocus(boolean success, Camera camera) {
if (mAutoFocusHandler != null) {
Message message = mAutoFocusHandler.obtainMessage(mAutoFocusMessage, success);
mAutoFocusHandler.sendMessageDelayed(message, AUTO_FOCUS_INTERVAL_MS);
mAutoFocusHandler = null;
} else {
Log.v(TAG, "Got auto-focus callback, but no handler for it");
}
}
}
图片的优化以及解码
添加常用图片格式以及算法优化,使用到了Zxing
关于zxing网上一堆源码,直接搬过来即可,或者也可以到文章末尾下载
final class DecodeHandler extends Handler {
private final ScannerActivity mActivity;
private final MultiFormatReader mMultiFormatReader;
private final Map<DecodeHintType, Object> mHints;
private byte[] mRotatedData;
DecodeHandler(ScannerActivity activity) {
this.mActivity = activity;
mMultiFormatReader = new MultiFormatReader();
mHints = new Hashtable<>();
mHints.put(DecodeHintType.CHARACTER_SET, "utf-8");
mHints.put(DecodeHintType.TRY_HARDER, Boolean.TRUE);
Collection<BarcodeFormat> barcodeFormats = new ArrayList<>();
barcodeFormats.add(BarcodeFormat.CODE_39);
barcodeFormats.add(BarcodeFormat.CODE_128); // 快递单常用格式39,128
barcodeFormats.add(BarcodeFormat.QR_CODE); //扫描格式自行添加
mHints.put(DecodeHintType.POSSIBLE_FORMATS, barcodeFormats);
}
@Override
public void handleMessage(Message message) {
switch (message.what) {
case R.id.decode:
decode((byte[]) message.obj, message.arg1, message.arg2);
break;
case R.id.quit:
Looper looper = Looper.myLooper();
if (null != looper) {
looper.quit();
}
break;
}
}
/**
* Decode the data within the viewfinder rectangle, and time how long it took. For efficiency, reuse the same reader
* objects from one decode to the next.
*
* @param data The YUV preview frame.
* @param width The width of the preview frame.
* @param height The height of the preview frame.
*/
private void decode(byte[] data, int width, int height) {
if (null == mRotatedData) {
mRotatedData = new byte[width * height];
} else {
if (mRotatedData.length < width * height) {
mRotatedData = new byte[width * height];
}
}
Arrays.fill(mRotatedData, (byte) 0);
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
if (x + y * width >= data.length) {
break;
}
mRotatedData[x * height + height - y - 1] = data[x + y * width];
}
}
int tmp = width; // Here we are swapping, that's the difference to #11
width = height;
height = tmp;
Result rawResult = null;
try {
Rect rect = mActivity.getCropRect();
if (rect == null) {
return;
}
PlanarYUVLuminanceSource source = new PlanarYUVLuminanceSource(mRotatedData, width, height, rect.left, rect.top, rect.width(), rect.height(), false);
if (mActivity.isQRCode()){
/*
HybridBinarizer算法使用了更高级的算法,针对渐变图像更优,也就是准确率高。
但使用GlobalHistogramBinarizer识别效率确实比HybridBinarizer要高一些。
*/
rawResult = mMultiFormatReader.decode(new BinaryBitmap(new GlobalHistogramBinarizer(source)), mHints);
if (rawResult == null) {
rawResult = mMultiFormatReader.decode(new BinaryBitmap(new HybridBinarizer(source)), mHints);
}
}else{
TessEngine tessEngine = TessEngine.Generate();
Bitmap bitmap = source.renderCroppedGreyscaleBitmap();
String result = tessEngine.detectText(bitmap);
if(!TextUtils.isEmpty(result)){
rawResult = new Result(result, null, null, null);
rawResult.setBitmap(bitmap);
}
}
} catch (Exception ignored) {
} finally {
mMultiFormatReader.reset();
}
if (rawResult != null) {
Message message = Message.obtain(mActivity.getCaptureActivityHandler(), R.id.decode_succeeded, rawResult);
message.sendToTarget();
} else {
Message message = Message.obtain(mActivity.getCaptureActivityHandler(), R.id.decode_failed);
message.sendToTarget();
}
}
}
处理与相机所匹配的状态的线程CaptureActivityHandler.java
/**
* This class handles all the messaging which comprises the state machine for capture.
*/
public final class CaptureActivityHandler extends Handler {
private static final String TAG = CaptureActivityHandler.class.getName();
private final ScannerActivity mActivity;
private final DecodeThread mDecodeThread;
private State mState;
public CaptureActivityHandler(ScannerActivity activity) {
this.mActivity = activity;
mDecodeThread = new DecodeThread(activity);
mDecodeThread.start();
mState = State.SUCCESS;
// Start ourselves capturing previews and decoding.
restartPreviewAndDecode();
}
@Override
public void handleMessage(Message message) {
switch (message.what) {
case R.id.auto_focus:
// Log.d(TAG, "Got auto-focus message");
// When one auto focus pass finishes, start another. This is the closest thing to
// continuous AF. It does seem to hunt a bit, but I'm not sure what else to do.
if (mState == State.PREVIEW) {
CameraManager.get().requestAutoFocus(this, R.id.auto_focus);
}
break;
case R.id.decode_succeeded:
Log.e(TAG, "Got decode succeeded message");
mState = State.SUCCESS;
mActivity.handleDecode((Result) message.obj);
break;
case R.id.decode_failed:
// We're decoding as fast as possible, so when one decode fails, start another.
mState = State.PREVIEW;
CameraManager.get().requestPreviewFrame(mDecodeThread.getHandler(), R.id.decode);
break;
}
}
public void quitSynchronously() {
mState = State.DONE;
CameraManager.get().stopPreview();
Message quit = Message.obtain(mDecodeThread.getHandler(), R.id.quit);
quit.sendToTarget();
try {
mDecodeThread.join();
} catch (InterruptedException e) {
// continue
}
// Be absolutely sure we don't send any queued up messages
removeMessages(R.id.decode_succeeded);
removeMessages(R.id.decode_failed);
}
public void restartPreviewAndDecode() {
if (mState != State.PREVIEW) {
CameraManager.get().startPreview();
mState = State.PREVIEW;
CameraManager.get().requestPreviewFrame(mDecodeThread.getHandler(), R.id.decode);
CameraManager.get().requestAutoFocus(this, R.id.auto_focus);
}
}
private enum State {
PREVIEW, SUCCESS, DONE
}
public void onPause() {
mState = State.DONE;
CameraManager.get().stopPreview();
}
}
用于解码的线程DecodeThread.java
/**
* This thread does all the heavy lifting of decoding the images.
*/
final class DecodeThread extends Thread {
private final ScannerActivity mActivity;
private final CountDownLatch mHandlerInitLatch;
private Handler mHandler;
DecodeThread(ScannerActivity activity) {
this.mActivity = activity;
mHandlerInitLatch = new CountDownLatch(1);
}
Handler getHandler() {
try {
mHandlerInitLatch.await();
} catch (InterruptedException ie) {
// continue?
}
return mHandler;
}
@Override
public void run() {
Looper.prepare();
mHandler = new DecodeHandler(mActivity);
mHandlerInitLatch.countDown();
Looper.loop();
}
}
图片解析二维码回调方法
/**
* 图片解析二维码回调方法
*/
public interface DecodeImageCallback {
void decodeSucceed(Result result);
void decodeFail(int type, String reason);
}
解析图像二维码线程
/**
*
* 解析图像二维码线程
*/
public class DecodeImageThread implements Runnable {
private static final int MAX_PICTURE_PIXEL = 256;
private byte[] mData;
private int mWidth;
private int mHeight;
private String mImgPath;
private DecodeImageCallback mCallback;
public DecodeImageThread(String imgPath, DecodeImageCallback callback) {
this.mImgPath = imgPath;
this.mCallback = callback;
}
@Override
public void run() {
if (null == mData) {
if (!TextUtils.isEmpty(mImgPath)) {
Bitmap bitmap = QrUtils.decodeSampledBitmapFromFile(mImgPath, MAX_PICTURE_PIXEL, MAX_PICTURE_PIXEL);
this.mData = QrUtils.getYUV420sp(bitmap.getWidth(), bitmap.getHeight(), bitmap);
this.mWidth = bitmap.getWidth();
this.mHeight = bitmap.getHeight();
}
}
if (mData == null || mData.length == 0 || mWidth == 0 || mHeight == 0) {
if (null != mCallback) {
mCallback.decodeFail(0, "No image data");
}
return;
}
final Result result = QrUtils.decodeImage(mData, mWidth, mHeight);
if (null != mCallback) {
if (null != result) {
mCallback.decodeSucceed(result);
} else {
mCallback.decodeFail(0, "Decode image failed.");
}
}
}
}
二维码解析管理
/**
* 二维码解析管理。
*/
public class DecodeManager {
public void showCouldNotReadQrCodeFromScanner(Context context, final OnRefreshCameraListener listener) {
new AlertDialog.Builder(context).setTitle(R.string.notification)
.setMessage(R.string.could_not_read_qr_code_from_scanner)
.setPositiveButton(R.string.close, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.dismiss();
if (listener != null) {
listener.refresh();
}
}
})
.show();
}
public interface OnRefreshCameraListener {
void refresh();
}
}
二维码相关功能类
/**
* 二维码相关功能类
*/
public class QrUtils {
private static byte[] yuvs;
/**
* YUV420sp
*
* @param inputWidth
* @param inputHeight
* @param scaled
* @return
*/
public static byte[] getYUV420sp(int inputWidth, int inputHeight, Bitmap scaled) {
int[] argb = new int[inputWidth * inputHeight];
scaled.getPixels(argb, 0, inputWidth, 0, 0, inputWidth, inputHeight);
/**
* 需要转换成偶数的像素点,否则编码YUV420的时候有可能导致分配的空间大小不够而溢出。
*/
int requiredWidth = inputWidth % 2 == 0 ? inputWidth : inputWidth + 1;
int requiredHeight = inputHeight % 2 == 0 ? inputHeight : inputHeight + 1;
int byteLength = requiredWidth * requiredHeight * 3 / 2;
if (yuvs == null || yuvs.length < byteLength) {
yuvs = new byte[byteLength];
} else {
Arrays.fill(yuvs, (byte) 0);
}
encodeYUV420SP(yuvs, argb, inputWidth, inputHeight);
scaled.recycle();
return yuvs;
}
/**
* RGB转YUV420sp
*
* @param yuv420sp inputWidth * inputHeight * 3 / 2
* @param argb inputWidth * inputHeight
* @param width
* @param height
*/
private static void encodeYUV420SP(byte[] yuv420sp, int[] argb, int width, int height) {
// 帧图片的像素大小
final int frameSize = width * height;
// ---YUV数据---
int Y, U, V;
// Y的index从0开始
int yIndex = 0;
// UV的index从frameSize开始
int uvIndex = frameSize;
// ---颜色数据---
// int a, R, G, B;
int R, G, B;
//
int argbIndex = 0;
//
// ---循环所有像素点,RGB转YUV---
for (int j = 0; j < height; j++) {
for (int i = 0; i < width; i++) {
// a is not used obviously
// a = (argb[argbIndex] & 0xff000000) >> 24;
R = (argb[argbIndex] & 0xff0000) >> 16;
G = (argb[argbIndex] & 0xff00) >> 8;
B = (argb[argbIndex] & 0xff);
//
argbIndex++;
// well known RGB to YUV algorithm
Y = ((66 * R + 129 * G + 25 * B + 128) >> 8) + 16;
U = ((-38 * R - 74 * G + 112 * B + 128) >> 8) + 128;
V = ((112 * R - 94 * G - 18 * B + 128) >> 8) + 128;
//
Y = Math.max(0, Math.min(Y, 255));
U = Math.max(0, Math.min(U, 255));
V = Math.max(0, Math.min(V, 255));
// NV21 has a plane of Y and interleaved planes of VU each sampled by a factor of 2
// meaning for every 4 Y pixels there are 1 V and 1 U. Note the sampling is every other
// pixel AND every other scanline.
// ---Y---
yuv420sp[yIndex++] = (byte) Y;
// ---UV---
if ((j % 2 == 0) && (i % 2 == 0)) {
//
yuv420sp[uvIndex++] = (byte) V;
//
yuv420sp[uvIndex++] = (byte) U;
}
}
}
}
public static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {
// Raw height and width of image
final int height = options.outHeight;
final int width = options.outWidth;
int inSampleSize = 1;
if (height > reqHeight || width > reqWidth) {
final int halfHeight = height / 2;
final int halfWidth = width / 2;
// Calculate the largest inSampleSize value that is a power of 2 and keeps both
// height and width larger than the requested height and width.
while ((halfHeight / inSampleSize) > reqHeight && (halfWidth / inSampleSize) > reqWidth) {
inSampleSize *= 2;
}
}
return inSampleSize;
}
public static Bitmap decodeSampledBitmapFromFile(String imgPath, int reqWidth, int reqHeight) {
// First decode with inJustDecodeBounds=true to check dimensions
final BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeFile(imgPath, options);
// Calculate inSampleSize
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
// Decode bitmap with inSampleSize set
options.inJustDecodeBounds = false;
return BitmapFactory.decodeFile(imgPath, options);
}
/**
* Decode the data within the viewfinder rectangle, and time how long it took. For efficiency, reuse the same reader
* objects from one decode to the next.
*/
public static Result decodeImage(byte[] data, int width, int height) {
// 处理
Result result = null;
try {
Hashtable<DecodeHintType, Object> hints = new Hashtable<DecodeHintType, Object>();
hints.put(DecodeHintType.CHARACTER_SET, "utf-8");
hints.put(DecodeHintType.TRY_HARDER, Boolean.TRUE);
hints.put(DecodeHintType.POSSIBLE_FORMATS, BarcodeFormat.QR_CODE);
PlanarYUVLuminanceSource source =
new PlanarYUVLuminanceSource(data, width, height, 0, 0, width, height, false);
/**
* HybridBinarizer算法使用了更高级的算法,但使用GlobalHistogramBinarizer识别效率确实比HybridBinarizer要高一些。
*
* GlobalHistogram算法:(http://kuangjianwei.blog.163.com/blog/static/190088953201361015055110/)
*
* 二值化的关键就是定义出黑白的界限,我们的图像已经转化为了灰度图像,每个点都是由一个灰度值来表示,就需要定义出一个灰度值,大于这个值就为白(0),低于这个值就为黑(1)。
* 在GlobalHistogramBinarizer中,是从图像中均匀取5行(覆盖整个图像高度),每行取中间五分之四作为样本;以灰度值为X轴,每个灰度值的像素个数为Y轴建立一个直方图,
* 从直方图中取点数最多的一个灰度值,然后再去给其他的灰度值进行分数计算,按照点数乘以与最多点数灰度值的距离的平方来进行打分,选分数最高的一个灰度值。接下来在这两个灰度值中间选取一个区分界限,
* 取的原则是尽量靠近中间并且要点数越少越好。界限有了以后就容易了,与整幅图像的每个点进行比较,如果灰度值比界限小的就是黑,在新的矩阵中将该点置1,其余的就是白,为0。
*/
BinaryBitmap bitmap1 = new BinaryBitmap(new GlobalHistogramBinarizer(source));
// BinaryBitmap bitmap1 = new BinaryBitmap(new HybridBinarizer(source));
QRCodeReader reader2 = new QRCodeReader();
result = reader2.decode(bitmap1, hints);
} catch (ReaderException e) {
}
return result;
}
}
正则校验以及图片转换
public class Tools {
public static Bitmap rotateBitmap(Bitmap source, float angle) {
Matrix matrix = new Matrix();
matrix.postRotate(angle);
return Bitmap.createBitmap(source, 0, 0, source.getWidth(), source.getHeight(), matrix, true);
}
public static Bitmap preRotateBitmap(Bitmap source, float angle) {
Matrix matrix = new Matrix();
matrix.preRotate(angle);
return Bitmap.createBitmap(source, 0, 0, source.getWidth(), source.getHeight(), matrix, false);
}
public enum ScalingLogic {
CROP, FIT
}
public static int calculateSampleSize(int srcWidth, int srcHeight, int dstWidth, int dstHeight,
ScalingLogic scalingLogic) {
if (scalingLogic == ScalingLogic.FIT) {
final float srcAspect = (float) srcWidth / (float) srcHeight;
final float dstAspect = (float) dstWidth / (float) dstHeight;
if (srcAspect > dstAspect) {
return srcWidth / dstWidth;
} else {
return srcHeight / dstHeight;
}
} else {
final float srcAspect = (float) srcWidth / (float) srcHeight;
final float dstAspect = (float) dstWidth / (float) dstHeight;
if (srcAspect > dstAspect) {
return srcHeight / dstHeight;
} else {
return srcWidth / dstWidth;
}
}
}
public static Bitmap decodeByteArray(byte[] bytes, int dstWidth, int dstHeight,
ScalingLogic scalingLogic) {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeByteArray(bytes, 0, bytes.length, options);
options.inJustDecodeBounds = false;
options.inSampleSize = calculateSampleSize(options.outWidth, options.outHeight, dstWidth,
dstHeight, scalingLogic);
Bitmap unscaledBitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length, options);
return unscaledBitmap;
}
public static Rect calculateSrcRect(int srcWidth, int srcHeight, int dstWidth, int dstHeight,
ScalingLogic scalingLogic) {
if (scalingLogic == ScalingLogic.CROP) {
final float srcAspect = (float) srcWidth / (float) srcHeight;
final float dstAspect = (float) dstWidth / (float) dstHeight;
if (srcAspect > dstAspect) {
final int srcRectWidth = (int) (srcHeight * dstAspect);
final int srcRectLeft = (srcWidth - srcRectWidth) / 2;
return new Rect(srcRectLeft, 0, srcRectLeft + srcRectWidth, srcHeight);
} else {
final int srcRectHeight = (int) (srcWidth / dstAspect);
final int scrRectTop = (int) (srcHeight - srcRectHeight) / 2;
return new Rect(0, scrRectTop, srcWidth, scrRectTop + srcRectHeight);
}
} else {
return new Rect(0, 0, srcWidth, srcHeight);
}
}
public static Rect calculateDstRect(int srcWidth, int srcHeight, int dstWidth, int dstHeight,
ScalingLogic scalingLogic) {
if (scalingLogic == ScalingLogic.FIT) {
final float srcAspect = (float) srcWidth / (float) srcHeight;
final float dstAspect = (float) dstWidth / (float) dstHeight;
if (srcAspect > dstAspect) {
return new Rect(0, 0, dstWidth, (int) (dstWidth / srcAspect));
} else {
return new Rect(0, 0, (int) (dstHeight * srcAspect), dstHeight);
}
} else {
return new Rect(0, 0, dstWidth, dstHeight);
}
}
public static Bitmap createScaledBitmap(Bitmap unscaledBitmap, int dstWidth, int dstHeight,
ScalingLogic scalingLogic) {
Rect srcRect = calculateSrcRect(unscaledBitmap.getWidth(), unscaledBitmap.getHeight(),
dstWidth, dstHeight, scalingLogic);
Rect dstRect = calculateDstRect(unscaledBitmap.getWidth(), unscaledBitmap.getHeight(),
dstWidth, dstHeight, scalingLogic);
Bitmap scaledBitmap = Bitmap.createBitmap(dstRect.width(), dstRect.height(),
Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(scaledBitmap);
canvas.drawBitmap(unscaledBitmap, srcRect, dstRect, new Paint(Paint.FILTER_BITMAP_FLAG));
return scaledBitmap;
}
public static Bitmap getFocusedBitmap(Context context, Camera camera, byte[] data, Rect box){
Point ScrRes = ScreenUtils.getScreenResolution(context);
Point CamRes = CameraConfigurationUtils.findBestPreviewSizeValue(camera.getParameters(), ScrRes);
int SW = ScrRes.x;
int SH = ScrRes.y;
int RW = box.width();
int RH = box.height();
int RL = box.left;
int RT = box.top;
float RSW = (float) (RW * Math.pow(SW, -1));
float RSH = (float) (RH * Math.pow(SH, -1));
float RSL = (float) (RL * Math.pow(SW, -1));
float RST = (float) (RT * Math.pow(SH, -1));
float k = 0.5f;
int CW = CamRes.x;
int CH = CamRes.y;
int X = (int) (k * CW);
int Y = (int) (k * CH);
Bitmap unscaledBitmap = Tools.decodeByteArray(data, X, Y, ScalingLogic.CROP);
Bitmap bmp = Tools.createScaledBitmap(unscaledBitmap, X, Y, ScalingLogic.CROP);
unscaledBitmap.recycle();
if (CW > CH){
bmp = Tools.rotateBitmap(bmp, 90);
}
int BW = bmp.getWidth();
int BH = bmp.getHeight();
int RBL = (int) (RSL * BW);
int RBT = (int) (RST * BH);
int RBW = (int) (RSW * BW);
int RBH = (int) (RSH * BH);
Bitmap res = Bitmap.createBitmap(bmp, RBL, RBT, RBW, RBH);
bmp.recycle();
return res;
}
// private static Pattern pattern = Pattern.compile("(1|861)\\d{10}$*");
private static Pattern pattern = Pattern.compile("[^(0-9)]");
/**
* 获取为数字的数据
* @param chin 获取的字符串
* @return 返回数字
*/
public static String filterNumber(String chin)
{
chin = chin.replaceAll("[^(0-9)]", "");
return chin;
}
private static StringBuilder bf = new StringBuilder();
public static String getTelNum(String sParam){
if(TextUtils.isEmpty(sParam)){
return "";
}
Matcher matcher = pattern.matcher(sParam.trim());
bf.delete(0, bf.length());
while (matcher.find()) {
bf.append(matcher.group()).append("\n");
}
int len = bf.length();
if (len > 0) {
bf.deleteCharAt(len - 1);
}
return bf.toString();
}
}
获取页面是否活动的时间帮助类
用于初始化camera的时候的前置判断
public final class InactivityTimer {
private static final int INACTIVITY_DELAY_SECONDS = 5 * 60;
private final ScheduledExecutorService inactivityTimer =
Executors.newSingleThreadScheduledExecutor(new DaemonThreadFactory());
private final Activity activity;
private ScheduledFuture<?> inactivityFuture = null;
public InactivityTimer(Activity activity) {
this.activity = activity;
onActivity();
}
public void onActivity() {
cancel();
//在限定时间调用退出程序功能
inactivityFuture =
inactivityTimer.schedule(new FinishListener(activity), INACTIVITY_DELAY_SECONDS, TimeUnit.SECONDS);
}
private void cancel() {
if (inactivityFuture != null) {
inactivityFuture.cancel(true);
inactivityFuture = null;
}
}
public void shutdown() {
cancel();
inactivityTimer.shutdown();
}
private static final class DaemonThreadFactory implements ThreadFactory {
public Thread newThread(@NonNull Runnable runnable) {
Thread thread = new Thread(runnable);
thread.setDaemon(true);
return thread;
}
}
}
退出app方法类
/**
* Simple listener used to exit the app in a few cases.
*/
public final class FinishListener
implements DialogInterface.OnClickListener, DialogInterface.OnCancelListener, Runnable {
private final Activity mActivityToFinish;
public FinishListener(Activity activityToFinish) {
this.mActivityToFinish = activityToFinish;
}
public void onCancel(DialogInterface dialogInterface) {
run();
}
public void onClick(DialogInterface dialogInterface, int i) {
run();
}
public void run() {
mActivityToFinish.finish();
}
}
使用Tesseract-OCR
首先是训练数据的data管理
public class TessDataManager {
static final String TAG = "DBG_" + TessDataManager.class.getName();
private static final String tessdir = "tesseract";
private static final String subdir = "tessdata";
private static final String filename = "num.traineddata";
private static String trainedDataPath;
private static String tesseractFolder;
public static String getTesseractFolder() {
return tesseractFolder;
}
public static String getTrainedDataPath(){
return initiated ? trainedDataPath : null;
}
private static boolean initiated;
public static void initTessTrainedData(Context context){
if(initiated){
return;
}
File appFolder = context.getFilesDir();
File folder = new File(appFolder, tessdir);
if(!folder.exists()){
folder.mkdir();
}
tesseractFolder = folder.getAbsolutePath();
File subfolder = new File(folder, subdir);
if(!subfolder.exists()){
subfolder.mkdir();
}
File file = new File(subfolder, filename);
trainedDataPath = file.getAbsolutePath();
Log.d(TAG, "Trained data filepath: " + trainedDataPath);
if(!file.exists()) {
try {
FileOutputStream fileOutputStream;
byte[] bytes = readRawTrainingData(context);
if (bytes == null){
return;
}
fileOutputStream = new FileOutputStream(file);
fileOutputStream.write(bytes);
fileOutputStream.close();
initiated = true;
Log.d(TAG, "Prepared training data file");
} catch (FileNotFoundException e) {
Log.e(TAG, "Error opening training data file\n" + e.getMessage());
} catch (IOException e) {
Log.e(TAG, "Error opening training data file\n" + e.getMessage());
}
}
else{
initiated = true;
}
}
private static byte[] readRawTrainingData(Context context){
try {
InputStream fileInputStream = context.getResources()
.openRawResource(R.raw.num);
ByteArrayOutputStream bos = new ByteArrayOutputStream();
byte[] b = new byte[1024];
int bytesRead;
while (( bytesRead = fileInputStream.read(b))!=-1){
bos.write(b, 0, bytesRead);
}
fileInputStream.close();
return bos.toByteArray();
} catch (FileNotFoundException e) {
Log.e(TAG, "Error reading raw training data file\n"+e.getMessage());
return null;
} catch (IOException e) {
Log.e(TAG, "Error reading raw training data file\n" + e.getMessage());
}
return null;
}
}
解析拍照数字线程
/**
*
* 解析拍照数字线程
*/
public class TesseractThread implements Runnable {
private Bitmap mBitmap;
private TesseractCallback mCallback;
public TesseractThread(Bitmap mBitmap, TesseractCallback callback) {
this.mBitmap = mBitmap;
this.mCallback = callback;
}
@Override
public void run() {
if (mBitmap == null && null != mCallback) {
mCallback.fail();
return;
}
mCallback.succeed(TessEngine.Generate().detectText(mBitmap));
}
}
图片解析数字回调方法
/**
* 图片解析数字回调方法
*/
public interface TesseractCallback {
void succeed(String result);
void fail();
}
OCR识别设置
public class TessEngine {
static final String TAG = "DBG_" + TessEngine.class.getName();
private TessEngine(){
}
public static TessEngine Generate() {
return new TessEngine();
}
public String detectText(Bitmap bitmap) {
Log.d(TAG, "Initialization of TessBaseApi");
TessDataManager.initTessTrainedData(MyApplication.sAppContext);
TessBaseAPI tessBaseAPI = new TessBaseAPI();
String path = TessDataManager.getTesseractFolder();
Log.d(TAG, "Tess folder: " + path);
tessBaseAPI.setDebug(true);
tessBaseAPI.init(path, "num");
// 白名单
/* tessBaseAPI.setVariable(TessBaseAPI.VAR_CHAR_WHITELIST, "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789");
// 黑名单
tessBaseAPI.setVariable(TessBaseAPI.VAR_CHAR_BLACKLIST, "!@#$%^&*()_+=-[]}{;:'\"\\|~`,./<>?");
tessBaseAPI.setPageSegMode(TessBaseAPI.PageSegMode.PSM_AUTO_OSD);
Log.d(TAG, "Ended initialization of TessEngine");
Log.d(TAG, "Running inspection on bitmap");
tessBaseAPI.setImage(bitmap);
String inspection = tessBaseAPI.getHOCRText(0);
Log.d(TAG, "Confidence values: " + tessBaseAPI.meanConfidence());
tessBaseAPI.end();
System.gc();
return Tools.getTelNum(inspection);*/
tessBaseAPI.setVariable(TessBaseAPI.VAR_CHAR_WHITELIST, "0123456789");
// tessBaseAPI.setVariable(TessBaseAPI.VAR_CHAR_WHITELIST, "0123456789");
// 黑名单
tessBaseAPI.setVariable(TessBaseAPI.VAR_CHAR_BLACKLIST, "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz!@#$%^&*()_+=-[]}{;:'\"\\|~`,./<>?");
tessBaseAPI.setVariable("classify_bln_numeric_mode", "1");
// tessBaseAPI.setPageSegMode(TessBaseAPI.PageSegMode.PSM_AUTO_OSD);
tessBaseAPI.setPageSegMode(TessBaseAPI.PageSegMode.PSM_SINGLE_LINE);
Log.d(TAG, "Ended initialization of TessEngine");
Log.d(TAG, "Running inspection on bitmap");
tessBaseAPI.setImage(bitmap);
String inspection = tessBaseAPI.getUTF8Text();
Log.d(TAG, "Confidence values: " + tessBaseAPI.meanConfidence());
tessBaseAPI.end();
System.gc();
return Tools.filterNumber(inspection);
}
}
自定义弹窗
public class ImageDialog extends Dialog {
private Bitmap bmp;
private String title;
public ImageDialog(@NonNull Context context) {
super(context);
}
public ImageDialog addBitmap(Bitmap bmp) {
if (bmp != null){
this.bmp = bmp;
}
return this;
}
public ImageDialog addTitle(String title) {
if (title != null){
this.title = title;
}
return this;
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.image_dialog);
ImageView imageView = (ImageView)findViewById(R.id.image_dialog_imageView);
TextView textView = (TextView)findViewById(R.id.image_dialog_textView);
if (bmp != null){
imageView.setImageBitmap(bmp);
}
if(title!=null){
textView.setText(this.title);
}
}
@Override
public void dismiss() {
bmp.recycle();
bmp = null;
System.gc();
super.dismiss();
}
}
对应的布局文件
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="200dp">
<TextView
android:layout_margin="10dp"
android:text="识别结果"
android:gravity="center"
android:layout_width="match_parent"
android:layout_height="20dp"/>
<ImageView
android:layout_width="250dp"
android:layout_height="120dp"
android:id="@+id/image_dialog_imageView"
android:layout_gravity="center"/>
<TextView
android:textColor="@color/white"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="20dp"
android:id="@+id/image_dialog_textView"
android:layout_gravity="center_horizontal|top"
android:gravity="center"
android:textSize="16sp"
android:layout_margin="10dp" />
</LinearLayout>
修改ScannerActivity.java
隐藏的布局文件layout_surface_view.xml:
<?xml version="1.0" encoding="utf-8"?>
<SurfaceView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
/>
我就直接贴出ScannerActivity.java代码来了:
public class ScannerActivity extends AppCompatActivity implements Callback, Camera.PictureCallback, Camera.ShutterCallback{
private CaptureActivityHandler mCaptureActivityHandler;
private boolean mHasSurface;
private InactivityTimer mInactivityTimer;
private ScannerFinderView mQrCodeFinderView;
private SurfaceView mSurfaceView;
private ViewStub mSurfaceViewStub;
private DecodeManager mDecodeManager = new DecodeManager();
private Switch switch1;
private ProgressDialog progressDialog;
private Bitmap bmp;
@Override
public void onCreate(Bundle savedInstanceState) {
requestWindowFeature(Window.FEATURE_NO_TITLE);
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_scanner);
if (ContextCompat.checkSelfPermission(this, CAMERA) != PackageManager.PERMISSION_GRANTED ||
ContextCompat.checkSelfPermission(this, READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this, new String[]{CAMERA, READ_EXTERNAL_STORAGE}, 100);
} else {
initView();
initData();
}
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == 100){
boolean permissionGranted = true;
for (int i : grantResults) {
if (i != PackageManager.PERMISSION_GRANTED) {
permissionGranted = false;
}
}
if (permissionGranted){
initView();
initData();
}else {
// 无权限退出
finish();
}
}
}
private void initView() {
mQrCodeFinderView = (ScannerFinderView) findViewById(R.id.qr_code_view_finder);
mSurfaceViewStub = (ViewStub) findViewById(R.id.qr_code_view_stub);
switch1 = (Switch) findViewById(R.id.switch1);
mHasSurface = false;
Switch switch2 = (Switch) findViewById(R.id.switch2);
switch2.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
CameraManager.get().setFlashLight(isChecked);
}
});
}
public Rect getCropRect() {
return mQrCodeFinderView.getRect();
}
public boolean isQRCode() {
return switch1.isChecked();
}
private void initData() {
mInactivityTimer = new InactivityTimer(this);
}
@Override
protected void onResume() {
super.onResume();
if (mInactivityTimer != null){
CameraManager.init();
initCamera();
}
}
private void initCamera() {
if (null == mSurfaceView) {
mSurfaceViewStub.setLayoutResource(R.layout.layout_surface_view);
mSurfaceView = (SurfaceView) mSurfaceViewStub.inflate();
}
SurfaceHolder surfaceHolder = mSurfaceView.getHolder();
if (mHasSurface) {
initCamera(surfaceHolder);
} else {
surfaceHolder.addCallback(this);
// 防止sdk8的设备初始化预览异常(可去除,本项目最小16)
surfaceHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
}
}
@Override
protected void onPause() {
if (mCaptureActivityHandler != null) {
try {
mCaptureActivityHandler.quitSynchronously();
mCaptureActivityHandler = null;
if (null != mSurfaceView && !mHasSurface) {
mSurfaceView.getHolder().removeCallback(this);
}
CameraManager.get().closeDriver();
} catch (Exception e) {
// 关闭摄像头失败的情况下,最好退出该Activity,否则下次初始化的时候会显示摄像头已占用.
finish();
}
}
super.onPause();
}
@Override
protected void onDestroy() {
if (null != mInactivityTimer) {
mInactivityTimer.shutdown();
}
super.onDestroy();
}
/**
* Handler scan result
*
* @param result
*/
public void handleDecode(Result result) {
mInactivityTimer.onActivity();
if (null == result) {
mDecodeManager.showCouldNotReadQrCodeFromScanner(this, new DecodeManager.OnRefreshCameraListener() {
@Override
public void refresh() {
restartPreview();
}
});
} else {
handleResult(result);
}
}
private void initCamera(SurfaceHolder surfaceHolder) {
try {
if (!CameraManager.get().openDriver(surfaceHolder)) {
return;
}
} catch (IOException e) {
// 基本不会出现相机不存在的情况
Toast.makeText(this, getString(R.string.camera_not_found), Toast.LENGTH_SHORT).show();
finish();
return;
} catch (RuntimeException re) {
re.printStackTrace();
return;
}
mQrCodeFinderView.setVisibility(View.VISIBLE);
findViewById(R.id.qr_code_view_background).setVisibility(View.GONE);
if (mCaptureActivityHandler == null) {
mCaptureActivityHandler = new CaptureActivityHandler(this);
}
}
public void restartPreview() {
if (null != mCaptureActivityHandler) {
try {
mCaptureActivityHandler.restartPreviewAndDecode();
} catch (Exception e) {
e.printStackTrace();
}
}
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {}
@Override
public void surfaceCreated(SurfaceHolder holder) {
if (!mHasSurface) {
mHasSurface = true;
initCamera(holder);
}
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
mHasSurface = false;
}
public Handler getCaptureActivityHandler() {
return mCaptureActivityHandler;
}
private void handleResult(Result result) {
if (TextUtils.isEmpty(result.getText())) {
mDecodeManager.showCouldNotReadQrCodeFromScanner(this, new DecodeManager.OnRefreshCameraListener() {
@Override
public void refresh() {
restartPreview();
}
});
} else {
Vibrator vibrator = (Vibrator) this.getSystemService(Context.VIBRATOR_SERVICE);
vibrator.vibrate(200L);
if (switch1.isChecked()) {
qrSucceed(result.getText());
} else {
phoneSucceed(result.getText(), result.getBitmap());
}
}
}
@Override
public void onPictureTaken(byte[] data, Camera camera) {
if (data == null) {
return;
}
mCaptureActivityHandler.onPause();
bmp = null;
bmp = Tools.getFocusedBitmap(this, camera, data, getCropRect());
TesseractThread mTesseractThread = new TesseractThread(bmp, new TesseractCallback() {
@Override
public void succeed(String result) {
Message message = Message.obtain();
message.what = 0;
message.obj = result;
mHandler.sendMessage(message);
}
@Override
public void fail() {
Message message = Message.obtain();
message.what = 1;
mHandler.sendMessage(message);
}
});
Thread thread = new Thread(mTesseractThread);
thread.start();
}
@Override
public void onShutter() {}
private void qrSucceed(String result){
AlertDialog dialog = new AlertDialog.Builder(this).setTitle(R.string.notification)
.setMessage(result)
.setPositiveButton(R.string.positive_button_confirm, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.dismiss();
restartPreview();
}
})
.show();
dialog.setOnDismissListener(new DialogInterface.OnDismissListener() {
@Override
public void onDismiss(DialogInterface dialog) {
restartPreview();
}
});
}
private void phoneSucceed(String result, Bitmap bitmap){
ImageDialog dialog = new ImageDialog(this);
dialog.addBitmap(bitmap);
dialog.addTitle(TextUtils.isEmpty(result) ? "未识别到手机号码" : result);
dialog.show();
dialog.setOnDismissListener(new DialogInterface.OnDismissListener() {
@Override
public void onDismiss(DialogInterface dialog) {
restartPreview();
}
});
}
private Handler mHandler = new Handler(){
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
cancelProgressDialog();
switch (msg.what){
case 0:
phoneSucceed((String) msg.obj, bmp);
break;
case 1:
Toast.makeText(ScannerActivity.this, "无法识别", Toast.LENGTH_SHORT).show();
break;
default:
break;
}
}
};
public void buildProgressDialog() {
if (progressDialog == null) {
progressDialog = new ProgressDialog(this);
progressDialog.setProgressStyle(ProgressDialog.STYLE_SPINNER);
}
progressDialog.setMessage("识别中...");
progressDialog.setCancelable(true);
progressDialog.show();
}
public void cancelProgressDialog() {
if (progressDialog != null){
if (progressDialog.isShowing()) {
progressDialog.dismiss();
}
}
}
}