AR相机的实现

Posted Vicent_9920

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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
类:DecodeHandler


String 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简化

源码

正在优化中

以上是关于AR相机的实现的主要内容,如果未能解决你的问题,请参考以下文章

单数行灰色,双数行白色

N个同学排队,1,2,1,2报数,单数退出,双数一排,1,2,1,2报数,单数退出,最后剩下的人原站哪?

求一SQL语句,把日期单数前面加0

求问JS高手,我想通过遍历一个数组然后做replace,把把数组里的单数位置的替换成双数位置的。

双数日期下插入文本内容

AR 相机扫描效果实现