Android 线上问题排查经历以及Camera初步学习总结

场景:接触Camera是今年3月份的时候,当时公司一个项目的后台审核业务员反馈:部分用户上传的证件照片比较模糊,看不太清楚。由于照片来源是APP端边框识别所得的照片,在经过测试复现后,我接手了该问题。 

复现:与测试负责人一起经过初步真机排查,发现该问题只在部分Android老机型中比较明显,如华为P9、OPPO R9等,这也容易理解,毕竟这5年前的手机像素确实不太行。(注:项目中的边框识别是2-3年前,公司开发人员使用github上一个名为card-io-lib开源项目的aar包)。

排查经过:

①:首先是对项目中调用“边框识别”返回照片处理的代码进行分析,发现组内开发者在使用Bitmap.compress方法处理返回的照片流数组时,对第二个quality参数设置了70(注:100为不压缩);于是将其改为了100进行尝试,结果却是我草率了,几乎没有起作用。。。。。

②:按第一步排查的情况,可以初步断定是该边框识别库内部逻辑造成的;由于项目中使用的是aar包,而没有项目源码;于是从github中找到了该库的项目源码,费了好大劲才将这年代久远的项目跑起来。也是在这之中,开始了解并学习了Android Camera相关知识点。

③:经过层层定位,其项目中进行边框识别页面名为CardIOActivity,而负责具体识别逻辑由CardScanner自定义View完成,该组件继承相机预览回调、相机自动聚焦回调、SurfaceHolder回调三个接口,具体的回调方法在后面解析。

 ④:主要的图像数据处理在Camera的onPreviewFrame回调中,大致原理以及步骤如下:

        1. Camera的onPreviewFrame回调一帧一帧持续输出图像流;

        2. 将图像流数据,由native层方法进行边框识别,主要通过opencv实现;

        3. 跳帧处理,如果上一帧还没解析完,会将当前回调所得的帧直接丢弃;

        4. 识别成功后,直接输出该帧的图片流数据。

⑤:猜测与验证:在排查过程中,使用华为P9相机拍照与识别所得的照片进行对比,发现相机拍照所得的照片明明十分清楚,而识别的咋就不行呢?这里猜想:应该是持续输出帧方法中的图像数据量比较小,又或是原生相机有算法优化加持等等。

        于是对基于华为P9,打印持续输出帧字节流数组长度,一帧图像约为92w(这个输出数据长度与开启预览前,Camera.addCallbackBuffer()方法所设置的缓冲区有关,因而输出的数据量都一样,相关方法在后面结合该开源项目进行分析);为了验证拍照的数据量有多大,使用Camera的takePicture方法进行测试,发现拍摄一张图片的获得的图像数据长度约为135w,好家伙,这差距一目了然。。。。

 ⑥:在看到对比结果后,我想到了一种粗暴而不失优雅的解决方案:在边框识别回调成功后,直接调用takePicture方法拍摄一张图片作为输出图。

@Override
    public void onPreviewFrame(byte[] data, Camera camera) {

        if (data == null) {
            return;
        }

        if (processingInProgress) {
            // return frame buffer to pool
            numFramesSkipped++;
            if (camera != null) {
                camera.addCallbackBuffer(data);
            }
            return;
        }
        processingInProgress = true;

        // TODO: eliminate this foolishness and measure/layout properly.
        if (mFirstPreviewFrame) {
            mFirstPreviewFrame = false;
            mFrameOrientation = ORIENTATION_PORTRAIT;
            mScanActivityRef.get().onFirstFrame();
        }

        //检测信息类,在native会对该类进行赋值,供后续判断
        DetectionInfo dInfo = new DetectionInfo();

        /** pika **/
        nScanFrame(data, mPreviewWidth, mPreviewHeight, mFrameOrientation, dInfo, detectedBitmap, mScanExpiry);

        boolean sufficientFocus = (dInfo.focusScore >= MIN_FOCUS_SCORE);
        Log.i("FlashTest", "onPreviewFrame:图像数组长度---"+data.length);
        if (!sufficientFocus) {
            triggerAutoFocus(false);
        } else if (dInfo.detected()) {
            //识别成功后,调用相机拍照,提高照片清晰度
            mCamera.takePicture(null, null, new Camera.PictureCallback() {
                @Override
                public void onPictureTaken(byte[] bytes, Camera it) {
                    Log.i("FlashTest", "onPictureTaken:拍照长度---"+bytes.length);
                    mScanActivityRef.get().onCardTakePicture(bytes);
                }
            });
//            mScanActivityRef.get().onCardDetected(detectedBitmap, dInfo);
        }
        // give the image buffer back to the camera, AFTER we're done reading
        // the image.
        if (camera != null) {
            camera.addCallbackBuffer(data);
        }
        processingInProgress = false;
    }

⑤:结果对比展示,效果拔群:

⑥:优化:

        图片优化:由于该图片上传至后端时,后端要用第三方API进行OCR识别,而OCR识别有5MB大小限制,对所编写的onCardTakePicture方法添加压缩逻辑,主要通过Bitmap实现压缩,阈值设置为21w(由OPPO R9这款老坦克手机的拍照数据流长度 + 2w冗余)。

/**
     * 扫描后拍照,提高照片清晰度
     * @param bytes
     */
    void onCardTakePicture(byte[] bytes){
        try {
            Vibrator vibrator = (Vibrator) getSystemService(Context.VIBRATOR_SERVICE);
            vibrator.vibrate(VIBRATE_PATTERN, -1);
        } catch (SecurityException e) {
            Log.e(Util.PUBLIC_LOG_TAG,
                    "Could not activate vibration feedback. Please add <uses-permission android:name=\"android.permission.VIBRATE\" /> to your application's manifest.");
        } catch (Exception e) {
            Log.w(Util.PUBLIC_LOG_TAG, "Exception while attempting to vibrate: ", e);
        }
        mCardScanner.pauseScanning();
        mUIBar.setVisibility(View.INVISIBLE);

        //原始数据长度
        int picSteamLength = bytes.length;

        Log.i("Flash=======", "原始图片流长度picSteamLength: "+picSteamLength);

        //使用bitmap对流进行压缩 与 旋转
        BitmapFactory.Options ontain = new BitmapFactory.Options();


        if (picSteamLength > PIC_COMPRESS_THRESHOLD) {
            //达到压缩阈值
            ontain.inSampleSize = 2;//等比缩放权值
            //颜色从默认的RGBA变为RGB
            ontain.inPreferredConfig = Bitmap.Config.RGB_565;
        }

        //旋转90度
        Bitmap mBitmap = Util.rotateBitmap(
                BitmapFactory.decodeByteArray(bytes, 0, bytes.length, ontain),
                90);

        //所得照片回调
        Intent dataIntent = new Intent();
        if (mBitmap != null) {
            ByteArrayOutputStream picBytes = new ByteArrayOutputStream();
            mBitmap.compress(
                    picSteamLength > PIC_COMPRESS_THRESHOLD ? Bitmap.CompressFormat.JPEG: Bitmap.CompressFormat.PNG,
                    picSteamLength > PIC_COMPRESS_THRESHOLD ? 70 : 100,
                    picBytes);
            Log.i("Flash=======", "是否到压缩阈值21w: "+(picSteamLength > PIC_COMPRESS_THRESHOLD));
            Log.i("Flash=======", "输出的图片流长度: "+picBytes.toByteArray().length);
            dataIntent.putExtra(CardIOActivity.EXTRA_CAPTURED_CARD_IMAGE, picBytes.toByteArray());
        }
        setResultAndFinish(RESULT_SCAN_SUPPRESSED, dataIntent);
    }

        包体积优化:由于目前大部分手机的CPU架构基本为armeabi-v7a或arm64-v8a,x86与x86_64很少见到,因而修改项目中的Android.mk文件,在打aar包时剔除对x86、x86_64两个架构的so库的编译。

⑦:以上就是我对线上这个问题的排查与解决全过程,排查、解决、优化,前后大约花了一周时间。4月份上线后到目前为止,再未反馈模糊问题。整体写的可能有点繁琐,但感觉值得记录一下。下面开始对Camera的相关方法进行总结分析。

===================================分割线================================

Camera相关接口、方法分析:

①:Camera.PreviewCallback接口:实现方法:onPreviewFrame,该方法能持续返回Camera帧数据,主要用来配合SurfaceView来实现相机实时预览;

/** @deprecated */
    @Deprecated
    public interface PreviewCallback {
        void onPreviewFrame(byte[] var1, Camera var2);
    }

②:Camera.AutoFocusCallBack:实现方法:onAutoFocus,返回聚焦结果值;

/** @deprecated */
    @Deprecated
    public interface AutoFocusCallback {
        void onAutoFocus(boolean var1, Camera var2);
    }

③:SurfaceHolder.CallBack:三个实现方法分别用于监听SurfaceView的状态,具体作用看方法名就能明白了。在Camera结合SurfaceView做相机预览功能时,由于Camera的onPreviewFrame不会自动触发,需要手动调用Camera.startPreview()方法,因而通过该方法实现。SurfaceHolder相当于是连接Camera与SurfaceView的桥梁。

public interface Callback {
        void surfaceCreated(SurfaceHolder var1);

        void surfaceChanged(SurfaceHolder var1, int var2, int var3, int var4);

        void surfaceDestroyed(SurfaceHolder var1);
    }

④:Camera.addCallbackBuffer设置缓冲区,如:通过计算每一帧需要的大小,开辟一块区域用于放置帧数据,

    Camera.Parameters parameters = mCamera.getParameters();

    //华为p9,通过打印为17,查表ImageFormat对象可得对应相机预览格式为NV21
    int previewFormat = parameters.getPreviewFormat();
    // Log.i("FlashTest", "previewFormat: " + previewFormat);

    // ImageFormat.getBitsPerPixel, 获取该图片格式下,每个像素点需要的bit数,通过网上资料,NV21  1像素为12位(bit)
    //获取当前相机的预览图像格式,计算每个像素点所需要的字节数,
    int bytesPerPixel = ImageFormat.getBitsPerPixel(previewFormat) / 8;
    //缓冲区大小,预览宽度 * 高度 * 每像素所需字节数 * 3, 这个3应该是一个冗余值吧
    int bufferSize = mPreviewWidth * mPreviewHeight * bytesPerPixel * 3;

    mPreviewBuffer = new byte[bufferSize];
    mCamera.addCallbackBuffer(mPreviewBuffer);

⑤:Camera.setParameters,该方法用于配置相机参数,如果不设置,就默认配置,下面代码是设置了预览图的尺寸大小。

    Camera.Parameters parameters = mCamera.getParameters();
    //获取Camera所支持的预览尺寸,这里如果支持640*480就设置为该尺寸,否则就用列表第0个
    List<Size> supportedPreviewSizes = parameters.getSupportedPreviewSizes();
    if (supportedPreviewSizes != null) {
        Size previewSize = null;
        for (Size s : supportedPreviewSizes) {
            if (s.width == 640 && s.height == 480) {
                previewSize = s;
                break;
            }
        }
        if (previewSize == null) {
            previewSize = supportedPreviewSizes.get(0);
            previewSize.width = mPreviewWidth;
            previewSize.height = mPreviewHeight;
        }
    }
    parameters.setPreviewSize(mPreviewWidth, mPreviewHeight);
    mCamera.setParameters(parameters);

⑥:Camera.setPreviewCallback(this)和setPreviewCallbackWithBuffer(this),在预览开启前设置。前者没一预览帧都会在内存中开启新的缓冲区,效率低;后者,在预览回调onPreviewFrame方法中,配合Camera.addCallbackBuffer()方法能对预览前开辟的缓冲区进行复用,每一帧输出都会在当前缓冲区内,以达到节约内存的目的

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值