Android NDK开发:实战案例-电动车牌号识别(自定义相机及自定义遮罩控件)

目录

相关文章

Android NDK开发:实战案例-电动车牌号识别(介绍)
利用PorterDuffXfermode绘制图片文字

自定义相机

代码展示
public class ScanningCameraView extends SurfaceView implements SurfaceHolder.Callback,Camera.PreviewCallback{
    private Camera mCamera;//相机
    private boolean isSupportAutoFocus;//是否支持自动对焦
    private int screenHeight;//屏幕的高度
    private int screenWidth;//屏幕的宽度
    private boolean isPreviewing;//是否在预览
    private IdentifyCallBack identifyCallBack;//扫描成功的回调函数
    private boolean isScanning =false;
    Handler handler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            if (msg.what == 1){
                if(identifyCallBack!=null){
                    identifyCallBack.onIdentifyImage((Bitmap) msg.obj);
                }
            }
        }
    };
    public ScanningCameraView(Context context) {
        super(context);
        init();
    }

    public ScanningCameraView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public ScanningCameraView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }
    private void init() {
        //获取屏幕分辨率
        DisplayMetrics dm = getContext().getResources().getDisplayMetrics();
        screenWidth = dm.heightPixels;
        screenHeight = dm.widthPixels;
        isSupportAutoFocus = getContext().getPackageManager().hasSystemFeature(
                PackageManager.FEATURE_CAMERA_AUTOFOCUS);
        getHolder().addCallback(this);
    }
    public void setIdentifyCallBack(IdentifyCallBack identifyCallBack) {
        this.identifyCallBack = identifyCallBack;
    }
    /**
     * 开灯
     */
    public void openLight(){
        Camera.Parameters parameters = mCamera.getParameters();
        //打开闪光灯
        parameters.setFlashMode(Camera.Parameters.FLASH_MODE_TORCH);//开启
        mCamera.setParameters(parameters);
    }

    /**
     * 关灯
     */
    public void closeLight(){
        Camera.Parameters parameters = mCamera.getParameters();
        //打开闪光灯
        parameters.setFlashMode(Camera.Parameters.FLASH_MODE_OFF);//开启
        mCamera.setParameters(parameters);
    }
    /**
     * Camera帧数据回调用
     */
    @Override
    public void onPreviewFrame(final byte[] data, final Camera camera) {
        camera.addCallbackBuffer(data);
        new Thread(new Runnable() {
            @Override
            public void run() {
                //识别中不处理其他帧数据
                if (!isScanning) {
                    isScanning = true;
                    try {
                        //获取Camera预览尺寸
                        Camera.Size size = camera.getParameters().getPreviewSize();
                        //将帧数据转为bitmap
                        YuvImage image = new YuvImage(data, ImageFormat.NV21, size.width, size.height, null);
                        if (image != null) {
                            ByteArrayOutputStream stream = new ByteArrayOutputStream();
                            image.compressToJpeg(new Rect(0, 0, size.width, size.height), 80, stream);
                            Bitmap bmp = BitmapFactory.decodeByteArray(stream.toByteArray(), 0, stream.size());
                            Bitmap bitmap = cutImage(bmp);//获取遮罩处图像
                            Message message = handler.obtainMessage();
                            message.what = 1;
                            message.obj = bitmap;
                            handler.sendMessage(message);
                            isScanning = false;
                        }
                    } catch (Exception ex) {
                        isScanning = false;
                    }
                }
            }
        }
        ).start();

    }
    /**
     * 摄像头自动聚焦
     */
    Camera.AutoFocusCallback autoFocusCB = new Camera.AutoFocusCallback() {
        public void onAutoFocus(boolean success, Camera camera) {
            postDelayed(doAutoFocus, 500);
        }
    };
    private Runnable doAutoFocus = new Runnable() {
        public void run() {
            if (mCamera != null) {
                try {
                    mCamera.autoFocus(autoFocusCB);
                } catch (Exception e) {
                }
            }
        }
    };
    /**
     * 裁剪照片
     *
     * @return
     */
    private Bitmap cutImage(Bitmap bitmap) {
        int h = bitmap.getWidth();
        int w = bitmap.getHeight();
        int clipw = w/5*3;//这里根据遮罩的比例进行裁剪
        int cliph = (int) (clipw*1.93f);
        int x = (w - clipw) / 2;
        int y = (h - cliph) / 2;
        return Bitmap.createBitmap(bitmap, y, x,cliph, clipw);
    }
    /**
     * 打开指定摄像头
     */
    public void openCamera() {
        Camera.CameraInfo cameraInfo = new Camera.CameraInfo();
        for (int cameraId = 0; cameraId < Camera.getNumberOfCameras(); cameraId++) {
            Camera.getCameraInfo(cameraId, cameraInfo);
            if (cameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_BACK) {
                try {
                    mCamera = Camera.open(cameraId);
                } catch (Exception e) {
                    if (mCamera != null) {
                        mCamera.release();
                        mCamera = null;
                    }
                }
                break;
            }
        }
    }
    @Override
    public void surfaceCreated(SurfaceHolder holder) {
        try {
            releaseCamera();
            openCamera();
            initCamera();
        }catch (Exception e){
            mCamera = null;
        }
    }

    /**
     * 加载相机配置
     */
    private void initCamera() {
        try {
            mCamera.setPreviewDisplay(getHolder());//当前控件显示相机数据
            mCamera.setDisplayOrientation(90);//调整预览角度
            setCameraParameters();
            startPreview();//打开相机
        }catch (Exception e){
            releaseCamera();
        }
    }

    /**
     * 配置相机参数
     */
    private void setCameraParameters() {
        Camera.Parameters parameters = mCamera.getParameters();
        List sizes = parameters.getSupportedPreviewSizes();
        double scale = (double) getWidth() / getHeight();
        Camera.Size cameraSize = SortCameraSizeUtil.getCameraSize(sizes, scale);
        if(cameraSize != null){
            screenWidth = cameraSize.width;
            screenHeight = cameraSize.height;
        }else {
            //确定前面定义的预览宽高是camera支持的,不支持取就更大的
            for (int i = 0; i < sizes.size(); i++) {
                if ((sizes.get(i).width >= screenWidth && sizes.get(i).height >= screenHeight) || i == sizes.size() - 1) {
                    screenWidth = sizes.get(i).width;
                    screenHeight = sizes.get(i).height;
                    break;
                }
            }
        }
        //设置最终确定的预览大小
        parameters.setPreviewSize(screenWidth, screenHeight);//设置预览分辨率
        parameters.setPictureSize(screenWidth, screenHeight);//设置拍照图片的分辨率
        mCamera.setParameters(parameters);
    }
    /**
     * 释放相机
     */
    private void releaseCamera() {
        if(mCamera!=null){
            stopPreview();
            mCamera.setPreviewCallback(null);
            mCamera.release();
            mCamera=null;
        }
    }
    /**
     * 停止预览
     */
    private void stopPreview() {
        if (mCamera != null && isPreviewing) {
            mCamera.stopPreview();
            isPreviewing = false;
        }
    }

    /**
     * 开始预览
     */
    public void startPreview() {
        if (mCamera != null) {
            mCamera.addCallbackBuffer(new byte[((screenWidth * screenHeight) * ImageFormat.getBitsPerPixel(ImageFormat.NV21)) / 8]);
            mCamera.setPreviewCallbackWithBuffer(this);
            mCamera.startPreview();
            if(isSupportAutoFocus) {
                mCamera.autoFocus(autoFocusCB);
            }
            isPreviewing = true;
        }
    }
    @Override
    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
        stopPreview();
        initCamera();
    }

    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {
        releaseCamera();
    }
    public interface IdentifyCallBack{
        /**
         * 回调扫描的车牌区域
         */
        void onIdentifyImage(Bitmap bitmap);
    }
}

重点讲解

●调整预览角度
如果不进行预览角度的调整,自定义相机展现的画面会是横着的,因此需要将角度旋转90度。

mCamera.setDisplayOrientation(90);//调整预览角度

●获取最佳比例的分辨率
这里是根据这个自定义相机控件的宽高比与相机支持的各个分辨率的宽高比对比来进行筛选的,找到那个差值最小的分辨率即可,代码如下:

 /**
     * 配置相机参数
     */
    private void setCameraParameters() {
        Camera.Parameters parameters = mCamera.getParameters();
        //获取相机支持的分辨率
        List sizes = parameters.getSupportedPreviewSizes();
        //自定义相机控件的宽高比
        double scale = (double) getWidth() / getHeight();
        //通过对比取出差值最小的那个分辨率
        Camera.Size cameraSize = SortCameraSizeUtil.getCameraSize(sizes, scale);
        if(cameraSize != null){
            screenWidth = cameraSize.width;
            screenHeight = cameraSize.height;
        }else {
            //如果找不到就取一个最清晰的
            //确定前面定义的预览宽高是camera支持的,不支持取就更大的
            for (int i = 0; i < sizes.size(); i++) {
                if ((sizes.get(i).width >= screenWidth && sizes.get(i).height >= screenHeight) || i == sizes.size() - 1) {
                    screenWidth = sizes.get(i).width;
                    screenHeight = sizes.get(i).height;
                    break;
                }
            }
        }
        //设置最终确定的预览大小
        parameters.setPreviewSize(screenWidth, screenHeight);//设置预览分辨率
        parameters.setPictureSize(screenWidth, screenHeight);//设置拍照图片的分辨率
        mCamera.setParameters(parameters);
    }
 /**
     * 返回宽高比差值最小的Size
     * @param sizes 系统支持的Camera的Size
     * @param showViewRatio 当前自定义的相机控件展示的宽高的比值
     * @return
     */
    public static Camera.Size getCameraSize(List sizes,double showViewRatio){
        ArrayList sortSizeBeans = new ArrayList<>();
        for (Camera.Size size : sizes) {
            if(size != null){
                //获取当前遍历到的分辨率的宽高比(注意由于相机的角度问题,这里是高比宽)
                double scale = (double) size.height / size.width;
                //获取到两个比值的差值的绝对值并存储起来
                sortSizeBeans.add(new CameraSizeBean(size,Math.abs(showViewRatio - scale)));
            }
        }
        //对集合进行排序,差值最小的排上面
        Collections.sort(sortSizeBeans);
        if(sortSizeBeans.size() > 0){
            //将差值最小的返回
            return sortSizeBeans.get(0).getSize();
        }else {
            return null;
        }
    }

●自定义相机帧数据的回调
由于我们实现的是扫描识别并不是拍照识别,因此我们需要获取相机的每一帧图像,并进行处理,这就需要我们进行相应的配置,代码如下:

mCamera.addCallbackBuffer(new byte[((screenWidth * screenHeight) * ImageFormat.getBitsPerPixel(ImageFormat.NV21)) / 8]);
mCamera.setPreviewCallbackWithBuffer(this);

回调函数如下:

/**
     * Camera帧数据回调用
     */
    @Override
    public void onPreviewFrame(final byte[] data, final Camera camera) {
        camera.addCallbackBuffer(data);
        new Thread(new Runnable() {
            @Override
            public void run() {
                //识别中不处理其他帧数据
                if (!isScanning) {
                    isScanning = true;
                    try {
                        //获取Camera预览尺寸
                        Camera.Size size = camera.getParameters().getPreviewSize();
                        //将帧数据转为bitmap
                        YuvImage image = new YuvImage(data, ImageFormat.NV21, size.width, size.height, null);
                        if (image != null) {
                            ByteArrayOutputStream stream = new ByteArrayOutputStream();
                            image.compressToJpeg(new Rect(0, 0, size.width, size.height), 80, stream);
                            Bitmap bmp = BitmapFactory.decodeByteArray(stream.toByteArray(), 0, stream.size());
                            Bitmap bitmap = cutImage(bmp);//获取遮罩处图像
                            Message message = handler.obtainMessage();
                            message.what = 1;
                            message.obj = bitmap;
                            handler.sendMessage(message);
                            isScanning = false;
                        }
                    } catch (Exception ex) {
                        isScanning = false;
                    }
                }
            }
        }
        ).start();

    }

自定义遮罩

代码展示
public class ScanningMaskView extends View {
    private float mMaskWidth;//中间透明部分的宽度
    private float mMaskHeight;//中间透明部分的高度
    private Paint mPaintMask;//遮罩画笔
    private Paint mPaintText;//文字画笔
    private Paint mPaintMaskStrok;//遮罩描边画笔
    private float mTextSize = 30;//文字大小
    private Path mMaskPath;//遮罩透明部分路径
    private String mTopTripStr = "请扫描电动车牌号";
    private String mBottomTripStr = "请保持光线充足";

    public ScanningMaskView(Context context) {
        super(context);
        init();
    }

    public ScanningMaskView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public ScanningMaskView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    /**
     * 当控件大小改变的时候动态调整遮罩的大小
     * @param w
     * @param h
     * @param oldw
     * @param oldh
     */
    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        mMaskWidth = w/5*3;//遮罩透明部分的宽度为控件宽度的3/5
        mMaskHeight = mMaskWidth*1.93f;//遮罩透明部分的高度根据车牌比例算出
        mMaskPath.reset();
        mTextSize = w/20;
        mPaintText.setTextSize(mTextSize);//设置文字大小
        float left = (w-mMaskWidth)/2;
        float top = (h-mMaskHeight)/2;
        float right = left + mMaskWidth;
        float bottom = top + mMaskHeight;
        mMaskPath.addRoundRect(new RectF(left,top,right,bottom),10,10, Path.Direction.CW);
        invalidate();
    }
    private void init(){
        //关闭硬件加速
        setLayerType(LAYER_TYPE_SOFTWARE,null);

        mPaintMask = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaintMask.setStyle(Paint.Style.FILL);
        mPaintMask.setColor(Color.BLACK);
        mPaintMask.setAlpha(160);//设置半透明

        mPaintText = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaintText.setStrokeWidth(3);
        mPaintText.setColor(Color.WHITE);//设置文字颜色
        mPaintText.setTextAlign(Paint.Align.CENTER);//文字水平居中

        mPaintMaskStrok = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaintMaskStrok.setColor(Color.WHITE);
        mPaintMaskStrok.setStyle(Paint.Style.STROKE);
        mPaintMaskStrok.setStrokeWidth(3);

        mMaskPath = new Path();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.save();//离屏绘制
        canvas.drawRect(0,0,getWidth(),getHeight(),mPaintMask);//绘制整个控件大小遮罩
        mPaintMask.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));//将透明部分抠出来
        canvas.drawPath(mMaskPath,mPaintMask);
        mPaintMask.setXfermode(null);//清除混合模式
        canvas.restore();
        canvas.drawPath(mMaskPath,mPaintMaskStrok);//绘制遮罩描边
        canvas.save();
        canvas.translate(getWidth()/2,getHeight()/2);
        canvas.rotate(90);//旋转90度绘制文字
        canvas.drawText(mTopTripStr,0,-((mMaskWidth/2)+((getWidth()-mMaskWidth)/4)),mPaintText);
        canvas.drawText(mBottomTripStr,0,(mMaskWidth/2)+((getWidth()-mMaskWidth)/4),mPaintText);
        canvas.restore();
    }
}

重点讲解

●遮罩透明部分
遮罩透明部分主要是用了混合模式来实现的,具体就不多做解释了,这里我有一个小案例:利用PorterDuffXfermode绘制图片文字,想了解的同学可以点一下看看,或是自己百度了解下也可以。
●透明部分比例


透明部分的比例,以竖着的方式来看,宽度为屏幕宽度的3/5,高度为宽度的1.93倍,这个1.93并不是随便写的数据,而是因为1.93差不多也是电动车牌的宽高比,对应代码如下:

mMaskWidth = w/5*3;//遮罩透明部分的宽度为控件宽度的3/5
mMaskHeight = mMaskWidth*1.93f;//遮罩透明部分的高度根据车牌比例算出

●车牌号区域裁剪
车牌号部分的裁剪其实就是遮罩透明区域的裁剪,既然我们知道了透明部分区域的计算方式那么裁剪就很简单了,这部分代码在自定义相机里,代码如下:

   /**
     * 裁剪照片
     *
     * @return
     */
    private Bitmap cutImage(Bitmap bitmap) {
        int h = bitmap.getWidth();
        int w = bitmap.getHeight();
        int clipw = w/5*3;//这里根据遮罩的比例进行裁剪
        int cliph = (int) (clipw*1.93f);
        int x = (w - clipw) / 2;
        int y = (h - cliph) / 2;
        return Bitmap.createBitmap(bitmap, y, x,cliph, clipw);
    }

扫描动画实现

扫描动画我是使用TranslateAnimation来实现的,就是利用TranslateAnimation使扫描线从竖着的方向来看从右向左移动,到底之后就反转执行动画,而那条线就是背景为椭圆shape的View拉长之后看着就像中间粗两边细的线,代码如下:

/**
     * 动画设置
     */
    void setAnimation() {
        TranslateAnimation mAnimation = new TranslateAnimation(TranslateAnimation.RELATIVE_TO_PARENT, 0.99f,TranslateAnimation.RELATIVE_TO_PARENT, 0.01f,TranslateAnimation.ABSOLUTE, 0f,TranslateAnimation.ABSOLUTE,0f);
        mAnimation.setDuration(5000);
        mAnimation.setRepeatMode(Animation.REVERSE);// 设置反方向执行
        mAnimation.setRepeatCount(Animation.INFINITE);
        viewScanningline.setAnimation(mAnimation);
        mAnimation.start();
    }

而执行动画的区域是使用了百分百布局来进行固定的,使其移动的轨迹刚好在遮罩透明区域,代码如下:



    
    
    
    
        
        
    
    
    

实现效果

注意事项

●权限问题
别忘了权限的添加:

    
    
    

另外如果是Android6.0及以上别忘了动态申请权限,这里我为了省事所以没进行动态申请而是直接手动开启的权限。
●项目类型
我这里创建的是Native C++项目

案例源码

https://gitee.com/itfitness/LicenseOcrDemo

你可能感兴趣的:(Android NDK开发:实战案例-电动车牌号识别(自定义相机及自定义遮罩控件))