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<Camera.Size> 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<Camera.Size> 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<Camera.Size> sizes,double showViewRatio){
        ArrayList<CameraSizeBean> 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();
    }

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

<?xml version="1.0" encoding="utf-8"?>
<android.support.percent.PercentRelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <com.itfitness.licenseocrdemo.widget.camera.ScanningCameraView
        android:id="@+id/camera2"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        />
    <com.itfitness.licenseocrdemo.widget.mask.ScanningMaskView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        />
    <!--扫描线的区域-->
    <RelativeLayout
        android:id="@+id/layout_scanning"
        app:layout_aspectRatio="51.90%"
        app:layout_widthPercent="60%"
        android:layout_centerInParent="true">
        <!--扫描线-->
        <View
            android:id="@+id/view_scanningline"
            android:layout_alignParentLeft="true"
            android:background="@drawable/shape_scanningline"
            android:layout_width="2dp"
            android:layout_height="match_parent"/>
    </RelativeLayout>
    <ImageView
        android:id="@+id/img"
        android:scaleType="centerInside"
        android:layout_width="wrap_content"
        android:layout_height="90dp"
        />
    <ImageView
        android:id="@+id/img_light"
        android:layout_centerHorizontal="true"
        app:layout_marginTopPercent="86%"
        android:rotation="90"
        android:background="@drawable/selector_light"
        app:layout_aspectRatio="120%"
        app:layout_widthPercent="15%"
        />
</android.support.percent.PercentRelativeLayout>

实现效果

注意事项

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

    <uses-permission android:name="android.permission.CAMERA" />
    <uses-feature android:name="android.hardware.camera" />
    <uses-feature android:name="android.hardware.camera.autofocus" />

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

案例源码

https://gitee.com/itfitness/LicenseOcrDemo

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容