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; //绘制的Rectprivate Rect mRect; //返回的Rectpublic 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;}@Overridepublic 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));  //13dpString 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(){@Overridepublic 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;@Overridepublic 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);}@Overrideprotected 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"><ViewStubandroid:id="@+id/qr_code_view_stub"android:layout_width="match_parent"android:layout_height="match_parent"android:layout_gravity="center"/><TextViewandroid: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.ScannerFinderViewandroid:id="@+id/qr_code_view_finder"android:layout_width="match_parent"android:layout_height="match_parent"android:layout_centerInParent="true"android:visibility="gone"/><Viewandroid: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"/><Switchandroid: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"/><Switchandroid: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代码:
在进入页面首先需要判断是否有拍照和存储权限:

    @Overridepublic 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();}}@Overridepublic 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-featureandroid:name="android.hardware.camera"android:required="false"/><uses-featureandroid:name="android.hardware.camera.autofocus"android:required="false"/><uses-featureandroid: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 对象不为nullCamera.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 existsif (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 flashif (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 flashif (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;}@Overridepublic 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 screenprivate 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 sizeCamera.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;}@Overridepublic 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,128barcodeFormats.add(BarcodeFormat.QR_CODE); //扫描格式自行添加mHints.put(DecodeHintType.POSSIBLE_FORMATS, barcodeFormats);}@Overridepublic 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 #11width = 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();}@Overridepublic 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 messagesremoveMessages(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;}@Overridepublic 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;}@Overridepublic 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() {@Overridepublic 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 algorithmY = ((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 imagefinal 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 dimensionsfinal BitmapFactory.Options options = new BitmapFactory.Options();options.inJustDecodeBounds = true;BitmapFactory.decodeFile(imgPath, options);// Calculate inSampleSizeoptions.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);// Decode bitmap with inSampleSize setoptions.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 FinishListenerimplements 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;}@Overridepublic 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;}@Overrideprotected 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);}}@Overridepublic 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"><TextViewandroid:layout_margin="10dp"android:text="识别结果"android:gravity="center"android:layout_width="match_parent"android:layout_height="20dp"/><ImageViewandroid:layout_width="250dp"android:layout_height="120dp"android:id="@+id/image_dialog_imageView"android:layout_gravity="center"/><TextViewandroid: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;@Overridepublic 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();}}@Overridepublic 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() {@Overridepublic 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);}@Overrideprotected 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);}}@Overrideprotected 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();}@Overrideprotected 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() {@Overridepublic 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();}}}@Overridepublic void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {}@Overridepublic void surfaceCreated(SurfaceHolder holder) {if (!mHasSurface) {mHasSurface = true;initCamera(holder);}}@Overridepublic 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() {@Overridepublic 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());}}}@Overridepublic 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() {@Overridepublic void succeed(String result) {Message message = Message.obtain();message.what = 0;message.obj = result;mHandler.sendMessage(message);}@Overridepublic void fail() {Message message = Message.obtain();message.what = 1;mHandler.sendMessage(message);}});Thread thread = new Thread(mTesseractThread);thread.start();}@Overridepublic 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() {@Overridepublic void onClick(DialogInterface dialog, int which) {dialog.dismiss();restartPreview();}}).show();dialog.setOnDismissListener(new DialogInterface.OnDismissListener() {@Overridepublic 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() {@Overridepublic void onDismiss(DialogInterface dialog) {restartPreview();}});}private Handler mHandler = new Handler(){@Overridepublic 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();}}}}  

关于zxing的源码如下:
链接:https://pan.baidu.com/s/1313DB223Gr3MvbC8XwXaXw
提取码:1234

关于项目的源码参考资源Tesseract-OCR-Scanner-master.7z

Android实现扫一扫识别图像数字(使用训练的库拍照查看扫描结果)(下)相关推荐

  1. Android实现扫一扫识别图像数字(镂空图像数字Tesseract训练)(上)

    Android实现扫一扫识别图像数字(镂空图像数字训练)(上) 关于 需要的工具以及安装运行步骤如下 1.安装tesseract 2.下载使用jTessBoxEditor与素材准备 3.开始操作 步骤 ...

  2. python识别图像数字诊断模块_opencv+python 机读卡识别

    长按上图识别二维码报名济南源创会 摘要: 通过随意一张机读卡的照片,识别其中选择题题号,选项,以及相关数字识别.这个系列的解决方案不止一种,调参的方法也是各种各样,反正能够满足需求就极好了 1.预处理 ...

  3. 微信技术应用2大核心:语音和扫一扫

        我们时常听到对张小龙关于微信设计的研究,但很少注意到微信背后的技术团队.在早期版本中,由于主打信息沟通功能,微信技术上并无亮点,直到 4.3 版本之后,语音识别.扫一扫功能陆续的加入,新技术加 ...

  4. 安卓实现扫一扫识别数字

    本文已授权微信公众号:鸿洋(hongyangAndroid)原创首发. 公司业务需求,需要做手机号码的识别.所以有了此篇文章,现在就将实现过程分享给大家. 1.准备工作 首先实现识别数字等字符,我们要 ...

  5. Android扫车牌号识别技术SDK

    Android扫车牌号识别技术SDK Android扫车牌号识别技术SDK描述 Android扫车牌号识别技术SDK是我公司开发的基于移动平台的车牌识别软件开发包,支持android.iOS等多种主流 ...

  6. android 带手电筒的扫一扫(1 可以自动打开手电筒,2 可以自动对焦,增加识别率)

    android 扫一扫 功能: 1 可以类似于摩拜单车的扫一扫,自动打开手电筒图标让我们打开手电筒. 2 可以自动聚焦 解释说明: 功能1 加上了手电筒的效果 说明 在项目中 CaptureActiv ...

  7. Android二维码多码识别,相册选择二维码,自定义扫码界面

    现在很多App都有扫码识别二维码的场景,最新的扫码已经支持全屏扫码.从相册选取二维码识别,以下使用基于Zxing封装的一个库:https://github.com/maning0303/MNZXing ...

  8. Android TensorFlow Lite 深度学习识别手写数字mnist demo

    一. TensorFlow Lite TensorFlow Lite介绍.jpeg TensorFlow Lite特性.jpeg TensorFlow Lite使用.jpeg TensorFlow L ...

  9. 发票扫一扫,OCR识别功能

    现如今硬件发展突飞猛进,移动端OCR识别的功能借助强大的摄像头的像素做的非常成熟,而且都不是人工手动拍照来完成的,都是通过Android.iOS平台扫一扫自动ocr识别完成的,比如说,移动端扫一扫发票 ...

最新文章

  1. eas账号是什么意思_刚开始做抖音带货和好物推荐,如何布局抖音种草账号矩阵?...
  2. Proxy与NAT有什么区别
  3. liunx(3)-内核模块编写与系统调用
  4. loj10200. 「一本通 6.2 练习 3」Goldbach's Conjecture
  5. C#连接数据库SQL(2005)
  6. LeetCode 107. 二叉树的层次遍历 II(队列)
  7. jdbc之连接Oracle的基本步骤
  8. SaltStack 第一板块入门介绍 [1]
  9. mysql控制台操作
  10. STM32中断与事件
  11. (转)基于Metronic的Bootstrap开发框架经验总结(7)--数据的导入、导出及附件的查看处理...
  12. 使用内存映射文件来共享数据
  13. 如何做实时监控?—— 参考 Spring Boot 实现
  14. Atitit 分布式之道 之常见的分布式技术 1. 第十二章基于对象的分布式系统 1 1.1. Corba dcom 2 2. 第11章 分布式文件系统 - 2 2.1.  常见的分布式文件系统有,G
  15. 量子计算机与GIS,量子计算机系列---开篇,原理
  16. 牛股轮回另类可能:未来的牛股在哪?
  17. jflash添加芯片_Jflash用于烧录
  18. LEACH路由协议MATLAB仿真代码
  19. 【网络设备】单臂路由和STP配置及理论
  20. 网站Banner的代码

热门文章

  1. Module and Component
  2. python中必须要会的四大高级数据类型(字符,元组,列表,字典)
  3. matlab stats里的f值,MATLAB 回归分析regress,nlinfit,stepwise函数
  4. JAVA 多用户商城系统b2b2c-Spring Cloud Stream 介绍
  5. beeline连接hive的两种方式
  6. instagram忘记密码怎么解决_如何找回ins密码
  7. DDD的常见问题、争论以及局限性
  8. 【java8】LocalDateTime、LocalDate与LocalTime的基本使用
  9. 超市商品管理系统设计
  10. 什么是码元?什么是比特?