AR相机的实现

来源:互联网 发布:网络大电影营销 编辑:程序博客网 时间:2024/05/19 01:31

首先需要声明的是,我并不是原创每个概念,而是在前人的基础上做了一个实现,原理如图:

AR红包

上面的图片来自AR红包Android端实现原理

上面的难点我觉得可能是处理一帧图像的方法,这个方法有两个,分别是Camera和Camera2提供的,先看看Camera2的示例:

  // 开始预览,主要是camera.createCaptureSession这段代码很重要,创建会话      private void startPreview(CameraDevice camera) throws CameraAccessException {          SurfaceTexture texture = mPreviewView.getSurfaceTexture();  //      这里设置的就是预览大小          texture.setDefaultBufferSize(mPreviewSize.getWidth(), mPreviewSize.getHeight());</span>          Surface surface = new Surface(texture);          try {              // 设置捕获请求为预览,这里还有拍照啊,录像等              mPreviewBuilder = camera.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);          } catch (CameraAccessException e) {              e.printStackTrace();          }  //      就是在这里,通过这个set(key,value)方法,设置曝光啊,自动聚焦等参数!! 如下举例:  //      mPreviewBuilder.set(CaptureRequest.CONTROL_AE_MODE,CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH);      mImageReader = ImageReader.newInstance(mSurfaceView.getWidth(), mSurfaceView.getHeight(), ImageFormat.JPEG/*此处还有很多格式,比如我所用到YUV等*/, 2/*最大的图片数,mImageReader里能获取到图片数,但是实际中是2+1张图片,就是多一张*/);           mImageReader.setOnImageAvailableListener(mOnImageAvailableListener, mHandler);         // 这里一定分别add两个surface,一个Textureview的,一个ImageReader的,如果没add,会造成没摄像头预览,或者没有ImageReader的那个回调!!          mPreviewBuilder.addTarget(surface);      mPreviewBuilder.addTarget(mImageReader.getSurface());          mCameraDevice.createCaptureSession(Arrays.asList(surface, mImageReader.getSurface()),mSessionStateCallback, mHandler);      }      private CameraCaptureSession.StateCallback mSessionStateCallback = new CameraCaptureSession.StateCallback() {          @Override          public void onConfigured(CameraCaptureSession session) {              try {                  updatePreview(session);              } catch (CameraAccessException e) {                  e.printStackTrace();              }          }          @Override          public void onConfigureFailed(CameraCaptureSession session) {          }      };      private void updatePreview(CameraCaptureSession session) throws CameraAccessException {          session.setRepeatingRequest(mPreviewBuilder.build(), null, mHandler);      }      private ImageReader.OnImageAvailableListener mOnImageAvailableListener              = new ImageReader.OnImageAvailableListener() {          /**          *  当有一张图片可用时会回调此方法,但有一点一定要注意:          *  一定要调用 reader.acquireNextImage()和close()方法,否则画面就会卡住!!!!!我被这个坑坑了好久!!!          *    很多人可能写Demo就在这里打一个Log,结果卡住了,或者方法不能一直被回调。          **/          @Override          public void onImageAvailable(ImageReader reader) {               Image img = reader.acquireNextImage();               /**               *  因为Camera2并没有Camera1的Priview回调!!!所以该怎么能到预览图像的byte[]呢?就是在这里了!!!我找了好久的办法!!!               **/                ByteBuffer buffer = img.getPlanes()[0].getBuffer();                byte[] data = new byte[buffer.remaining()];                buffer.get(data);                    img.close();              }           };   }  

上述方法也可以实现图像的处理,但是我使用红米以及小米5在测试过程中发现卡顿非常严重,尽管检查了图片处理的线程是子线程,但是每秒钟大概处理6张预览图的时候还是依然卡顿,因此我排除了这个方法。

除了Camera2的方法以外,还有Camera的方法,而原理介绍中已经讲解了方法,如下:

三、获取一帧预览图像

Camera.setOneShotPreviewCallback(PreviewCallback cb)方法可以获取一帧预览图像,数据会返回到PreviewCallback 回调中的 onPreviewFrame(byte[] data,Camera camera)方法中,data即为帧数据。

由于我已经在Camera2测试了,因此我并没有直接使用这个方法来测试,而是参考了Zxing的二维码扫描这个库,因为这个二维码的处理逻辑其实是非常相似的,只是Zxing是解析二维码,而我们在藏红包的时候需要做的是处理奇数(单数)张图片与偶数(双数)张图片是否相似?

这里我是这样实现:通过拿到的Bitmap来加入一个集合(因为后面如果满足相似要求的话需要用到),接下来判断集合的长度 size 对2取余(就是除以2的余数)是否等于1,是的话,则是单数,否则的话就是双数。

这里比较简单,代码稍后一起贴出来。

接下来关于图片相似度方法,一般都是深度学习的算法,或者在知乎上面找到了一些建议,不过对于通过图片的简单像素做对比,基本上是不能做比较的,后面找到了一位大拿通过实现另一位大咖 阮一峰的文章,(相似图片搜索原理或者Android实现图片相似度),测试以后终于可以实现了。
首先,我先说一下我参考的是一个比较简单的ZXing demo ,(点击这里),通过代码分析,我只是简单的在解析的包里做的处理,代码如下:

包名:package com.breadykid.searchitem.scan.decode类:DecodeHandlerString lastHashValue,currentHashValue = null;    /**     * 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) {        Size size = activity.getCameraManager().getPreviewSize();        Log.e(TAG,"decode");        // 这里需要将获取的data翻转一下,因为相机默认拿的的横屏的数据        byte[] rotatedData = new byte[data.length];        for (int y = 0; y < size.height; y++) {            for (int x = 0; x < size.width; x++)                rotatedData[x * size.height + size.height - y - 1] = data[x + y * size.width];        }        for (int i = 0; i < rotatedData.length; i++) {        }        // 宽高也要调整        int tmp = size.width;        size.width = size.height;        size.height = tmp;        //Bitmap的生成        YuvImage image = new YuvImage(data, ImageFormat.NV21, size.width, size.height, 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());        bmpData.add(bmp);        LUtil.e("decodeData");//      decodeData(bmp);        //奇数        if(bmpData.size()%2 == 1){            lastHashValue = BitmapCompare.bitmapCompare(bmp);        }else{//偶数            currentHashValue = BitmapCompare.bitmapCompare(bmp);            //相似度            int diff = BitmapCompare.diff(lastHashValue,currentHashValue);            Log.e(TAG,"相识度:"+diff);            bmpData.clear();            currentHashValue = lastHashValue = null;            if(diff == 0){                //相似度次数                bmpCount++;            }        }        PlanarYUVLuminanceSource source = null;        if(bmpCount==3){            //返回的数据  待加工            source = buildLuminanceSource(rotatedData, size.width, size.height);        }        Handler handler = activity.getHandler();        if (source != null) {            // Don't log the barcode contents for security.            if (handler != null) {                Message message = Message.obtain(handler, R.id.decode_succeeded);                Bundle bundle = new Bundle();                //返回的数据加工成Bitmap                bundleThumbnail(source, bundle);                message.setData(bundle);                message.sendToTarget();            }        } else {            if (handler != null) {                Message message = Message.obtain(handler, R.id.decode_failed);                message.sendToTarget();            }        }    }/** * 图片相似度计算 * Created by 魏兴 on 2017/9/25. */public class BitmapCompare {    /**     * 获取图片哈希值     * @param bitmap     * @return     */    public static String bitmapCompare(Bitmap bitmap){        String result = null;        bitmap = convertGreyImg(bitmap);        int avg = getAvg(bitmap);        String binary = getBinary(bitmap,avg);        result = binaryString2hexString(binary);        return result;    }    /**     * 简化色彩     * @param img     * @return     */    public static Bitmap convertGreyImg(Bitmap img) {        int width = img.getWidth();         //获取位图的宽        int height = img.getHeight();       //获取位图的高        int[] pixels = new int[width * height]; //通过位图的大小创建像素点数组        img.getPixels(pixels, 0, width, 0, 0, width, height);        int alpha = 0xFF << 24;        for (int i = 0; i < height; i++) {            for (int j = 0; j < width; j++) {                int original = pixels[width * i + j];                int red = ((original & 0x00FF0000) >> 16);                int green = ((original & 0x0000FF00) >> 8);                int blue = (original & 0x000000FF);                int grey = (int) ((float) red * 0.3 + (float) green * 0.59 + (float) blue * 0.11);                grey = alpha | (grey << 16) | (grey << 8) | grey;                pixels[width * i + j] = grey;            }        }        Bitmap result = Bitmap.createBitmap(width, height, Bitmap.Config.RGB_565);        result.setPixels(pixels, 0, width, 0, 0, width, height);        return result;    }    /**     * 计算像素平均值     * @param img     * @return     */    public static int getAvg(Bitmap img) {        int width = img.getWidth();        int height = img.getHeight();        int[] pixels = new int[width * height];        img.getPixels(pixels, 0, width, 0, 0, width, height);        int avgPixel = 0;        for (int pixel : pixels) {            avgPixel += pixel;        }        return avgPixel / pixels.length;    }    /**     * 计算像素灰度     * @param img     * @param average     * @return     */    public static String getBinary(Bitmap img, int average) {        StringBuilder sb = new StringBuilder();        int width = img.getWidth();        int height = img.getHeight();        int[] pixels = new int[width * height];        img.getPixels(pixels, 0, width, 0, 0, width, height);        for (int i = 0; i < height; i++) {            for (int j = 0; j < width; j++) {                int original = pixels[width * i + j];                if (original >= average) {                    pixels[width * i + j] = 1;                } else {                    pixels[width * i + j] = 0;                }                sb.append(pixels[width * i + j]);            }        }        return sb.toString();    }    /**     * 计算哈希值     * @param bString     * @return     */    public static String binaryString2hexString(String bString) {        if (bString == null || bString.equals("") || bString.length() % 8 != 0)            return null;        StringBuilder sb = new StringBuilder();        int iTmp;        for (int i = 0; i < bString.length(); i += 4) {            iTmp = 0;            for (int j = 0; j < 4; j++) {                iTmp += Integer.parseInt(bString.substring(i + j, i + j + 1)) << (4 - j - 1);            }            sb.append(Integer.toHexString(iTmp));        }        return sb.toString();    }    /**     * 比较哈希值     * @param s1     * @param s2     */    public static int diff(String s1, String s2) {        char[] s1s = s1.toCharArray();        char[] s2s = s2.toCharArray();        int diffNum = 0;        for (int i = 0; i<s1s.length; i++) {            if (s1s[i] != s2s[i]) {                diffNum++;            }        }        return diffNum;    }    /**     * bitmap转为base64     * @param bitmap     * @return     */    public static String bitmapToBase64(Bitmap bitmap) {        String result = null;        ByteArrayOutputStream baos = null;        try {            if (bitmap != null) {                baos = new ByteArrayOutputStream();                bitmap.compress(Bitmap.CompressFormat.JPEG, 100, baos);                baos.flush();                baos.close();                byte[] bitmapBytes = baos.toByteArray();                result = Base64.encodeToString(bitmapBytes, Base64.DEFAULT);            }        } catch (IOException e) {            e.printStackTrace();        } finally {            try {                if (baos != null) {                    baos.flush();                    baos.close();                }            } catch (IOException e) {                e.printStackTrace();            }        }        return result;    }    /**     * base64转为bitmap     * @param base64Data     * @return     */    public static Bitmap base64ToBitmap(String base64Data) {        byte[] bytes = Base64.decode(base64Data, Base64.DEFAULT);        return BitmapFactory.decodeByteArray(bytes, 0, bytes.length);    }}

处理了上面的代码,现在我们可以像扫描二维码一样去调用CaptureActivity,当然返回的时候是就不要去获取解析的内容了,我们只要获取图片就好了。

 @Override    protected void onActivityResult(int requestCode, int resultCode, Intent data) {        super.onActivityResult(requestCode, resultCode, data);        if(requestCode == QUEST_AR_CODE){            if(resultCode ==RESULT_OK){                Bundle bundle = data.getExtras();                byte[] bmpData = bundle.getByteArray(DecodeThread.BARCODE_BITMAP);                int width = bundle.getInt("width");                int height = bundle.getInt("height");                Bitmap moneyBmp = BitmapFactory.decodeByteArray(bmpData,0,bmpData.length);//                File dirFile = FileUtil.createFileDir(this,"ARMoney");//                imgpath = FileUtil.saveBitmap(dirFile,"temp.png",moneyBmp);                //base64字符串                imgpath = BitmapCompare.bitmapToBase64(moneyBmp);                if(imgpath==null){                    Toast.makeText(this,"红包保存失败,请再次尝试",Toast.LENGTH_SHORT).show();                }else{                    tvImg.setText(etMoney.getText()+"元");                    tvImg.setVisibility(View.VISIBLE);                    etMoney.setVisibility(View.GONE);                    btnSend.setVisibility(View.GONE);                    tvUnit.setVisibility(View.GONE);                }            }else if(resultCode == RESULT_CANCELED){                LUtil.e("自动取消");            }else{                Toast.makeText(this,"AR红包隐藏失败",Toast.LENGTH_SHORT).show();            }        }    }

通过上面的代码,我们就实现了AR红包的藏,扫的话已经没有问题了,只是逻辑的实现而已。本来打算通过这个项目彻底了解一番Camera2的相关知识点,这点未达成;本来打算彻底学习图片的一些基础知识,这里也没有达成,只是简单的Ctrl+C 、Ctrl+V。这里意外学到的东西是关于scheme协议的使用、系统自带分享的使用(不能同时分享图片和文本),然后就是关于AppCompatActivity的全屏以及API 19 的沉浸式状态栏的实现。简单的说就是深度的东西没有学到,零碎的基础倒是一点一点的在积累。

参考文章:(文中均有链接)

AR红包Android端实现原理
Android实现图片相似度
ZXing简化

源码

正在优化中