美文网首页Android应用开发那些事Android开发实践Android
Android CameraPreview快速适配 (人脸识别、

Android CameraPreview快速适配 (人脸识别、

作者: lfork | 来源:发表于2019-03-12 17:45 被阅读700次

    一、开发背景

    由于在项目中需要集成多个人脸识别SDK,然后这些SDK有些有自己的Camera API(但又不是特别独立),有的SDK甚至都没有像样的Camera API。所以我就自己写了一个通用的,用来抓取视频流的Camera1的代码封装。

    二、特色

    • 快速适配各类相机(物理旋转、镜像、各种角度旋转等)

    • 控件大小自动适配

    • 人脸位置映射

    • 使用缓冲机制获取视频流

    • 代码简单(一个Class)、引入方面,CameraPreview的完整代码在第五点

    1.png 2.png

    三、基本使用,炒鸡简单

    • 1、一个 Layout文件
    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        >
    
        <com.lfork.cameraimagecollect.camera.CameraPreview
            android:id="@+id/camera_preview"
            android:layout_width="300dp"
            android:layout_height="500dp"
          />
    
    </LinearLayout>
    
    • 2、恢复和停止 (默认是前置摄像头 适配设备oneplus 6)
    package com.lfork.cameraimagecollect.camera1
    
    import android.os.Bundle
    import android.support.v7.app.AppCompatActivity
    import com.lfork.cameraimagecollect.R
    import kotlinx.android.synthetic.main.activity_camera_texture.*
    
    
    class CameraTextureActivity : AppCompatActivity() {
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_camera_texture)
        }
    
        override fun onResume() {
            super.onResume()
            camera_preview.resumeCamera()
        }
    
        override fun onStop() {
            super.onStop()
            camera_preview.releaseCamera()
        }
    }
    
    
    • 3、相关的权限处理就需要自己完成了

    四、进阶使用

    • 1、设置预览回调帧 确保在resumeCamea()之前调用即可
    val previewFrameCallBack = Camera.PreviewCallback { data, _ ->
        Log.d("CameraTextureActivity", "preview camera byte data $data")
    }
    camera_preview.previewCallBack = previewFrameCallBack
    
    • 2、针对特定的相机进行适配
    val params = CameraPreview.Params()
    params.cameraId = 0
    params.isHorizontalMirrored = false
    params.previewRotation = 90f
    params.previewWidth = 640
    params.previewHeight = 480
    camera_preview.setupCameraParams(params)
    
    • 3、人脸位置映射
    /**
         * 原始图片(传到SDK里面进行处理的图片)中的坐标到预览View坐标中的映射。应用场景举例:预览页面显示人脸框。
         *
         * @param rectF 原始图中的坐标
         */
        fun mapFromOriginalRect(rectF: RectF, rawImgWeight: Int, rawImgHeight: Int)
    

    五、引入:一个class,拷贝到项目中即可

    package com.lfork.cameraimagecollect.camera
    
    import android.content.Context
    import android.graphics.Matrix
    import android.graphics.RectF
    import android.graphics.SurfaceTexture
    import android.hardware.Camera
    import android.os.Handler
    import android.os.Looper
    import android.support.annotation.AttrRes
    import android.util.AttributeSet
    import android.view.TextureView
    import android.widget.FrameLayout
    import java.io.IOException
    
    /**
     * 基于系统TextureView和Camera1实现的预览View;
     */
    class CameraPreview : FrameLayout,
        TextureView.SurfaceTextureListener {
    
        lateinit var textureView: TextureView
    
        /**
         * 设置预览的缩放类型
         * 缩放类型
         */
        var scaleType: ScaleType = ScaleType.CROP_INSIDE
    
        /**
         * 图片帧缩放类型。
         */
        enum class ScaleType {
            /**
             * 宽度与父控件一致,高度自适应
             */
            FIT_WIDTH,
            /**
             * 调试与父控件一致,宽度自适应
             */
            FIT_HEIGHT,
            /**
             * 全屏显示 ,保持显示比例,多余的部分会被裁剪掉。
             */
            CROP_INSIDE
        }
    
        private var surface: SurfaceTexture? = null
    
        var mCamera: Camera? = null
    
        private var params = Params()
    
    
        /**
         * 默认配置是一加6的前置摄像头
         */
        class Params {
    
            var cameraId = 1
    
            /**
             * 预览帧的旋转角度(顺时针)
             */
            var previewRotation = 90f
    
            /**
             *   经过Camera处理后(比如旋转)的PreviewFrame的图像宽度   比如1280*720旋转后就会变成720*1280
             */
            var frameWidth: Int = 0
    
            /**
             *   经过Camera处理后(比如旋转)的PreviewFrame的图像高度
             */
            var frameHeight: Int = 0
    
            /**
             * 是否对视频进行了镜像处理(水平镜像)
             */
            var isHorizontalMirrored = false
    
    
            /**
             * 屏幕是否是竖着摆放
             */
            var isPortrait = true
    
        }
    
        /**
         * UI线程处理
         */
        private val mHandler = Handler(Looper.getMainLooper())
    
        constructor(context: Context) : super(context) {
            init()
        }
    
        constructor(
            context: Context,
            attrs: AttributeSet?
        ) : super(context, attrs) {
            init()
        }
    
        constructor(
            context: Context, attrs: AttributeSet?,
            @AttrRes defStyleAttr: Int
        ) : super(context, attrs, defStyleAttr) {
            init()
        }
    
    
        private fun init() {
            textureView = TextureView(context)
            textureView.surfaceTextureListener = this
            addView(textureView)
        }
    
    
        private var mPreviewBuffer: ByteArray? = null
    
        /**
         * 设置视频流回调(带缓冲)
         */
        var previewCallBack: Camera.PreviewCallback? = null
    
    
        /**
         *  对特定的相机进行参数配置
         */
        fun setupCameraParams(params: Params) {
            this.params = params
        }
    
        /**
         * 根据现有的参数对相机进行设置
         */
        private fun applyParams() {
    
            if (mCamera == null) {
                mCamera = Camera.open(params.cameraId)  //1
            }
    
            val cameraParams = mCamera!!.parameters
    
            //使用默认参数
            if (params.frameHeight == 0 || params.frameWidth == 0) {
    
                val size = getCloselyPreSize(
                    params.isPortrait,
                    width,
                    height,
                    cameraParams.supportedPreviewSizes
                )
    
                if (size == null) {
                    releaseCamera()
                    return
                }
    
                params.frameWidth = size.width
                params.frameHeight = size.height
    
            }
    
            //预览相片的尺寸:相机传过来的  注意是横着的
            cameraParams.setPreviewSize(params.frameWidth, params.frameHeight)
    
            mCamera?.parameters = cameraParams
            mCamera?.setDisplayOrientation(params.previewRotation.toInt())
    
            if (params.isHorizontalMirrored) {
                scaleX = -1f
            }
    
            if (params.previewRotation % 360f == 90f || params.previewRotation % 360f == 270f) {
                //进行90度旋转后,需要进行宽高转换
                val temp = params.frameHeight
                params.frameHeight = params.frameWidth
                params.frameWidth = temp
            } else {
                //进行90度旋转后,需要进行宽高转换
                val temp = params.frameWidth
                params.frameWidth = params.frameHeight
                params.frameHeight = temp
            }
    
    
            //让视频根据控件大小进行适配
            val ratio = 1.0 * params.frameHeight / params.frameWidth
    
            //获取控件高度  注意都是 height/  width
            val deviceRatio = 1.0 * height / width
            if (ratio >= deviceRatio) {
                scaleType = ScaleType.FIT_WIDTH
            } else {
                scaleType = ScaleType.FIT_HEIGHT
            }
    
            mHandler.post { requestLayout() }
        }
    
        fun resumeCamera() {
            if (mCamera == null) {
                applyParams()
            }
            try {
                if (mPreviewBuffer == null) {
                    //不要二次设置mPreviewBuffer 否则可能会有画面延迟,原因还不知道
                    mPreviewBuffer = ByteArray(params.frameWidth * params.frameHeight * 2)
                }
                mCamera?.addCallbackBuffer(mPreviewBuffer);
                mCamera?.setPreviewCallbackWithBuffer { data, camera ->
                    previewCallBack?.onPreviewFrame(data, camera)
                    camera?.addCallbackBuffer(mPreviewBuffer)
                };
                mCamera?.setPreviewTexture(surface)
                mCamera?.startPreview()
    
            } catch (ioe: IOException) {
                // Something bad happened
                ioe.printStackTrace()
            }
        }
    
        fun releaseCamera() {
            mCamera?.setPreviewCallbackWithBuffer(null);
            mCamera?.addCallbackBuffer(null)
            mCamera?.setPreviewCallback(null);
            mCamera?.stopPreview()
            mCamera?.release()
            mCamera = null
        }
    
        fun onDestroy() {
            previewCallBack == null
        }
    
    
        /**
         * 对布局大小进行重绘
         */
        override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
            super.onLayout(changed, left, top, right, bottom)
            val selfWidth = width
            val selfHeight = height
            if (params.frameWidth == 0 || params.frameHeight == 0 || selfWidth == 0 || selfHeight == 0) {
                return
            }
            val scaleType = resolveScaleType()
            if (scaleType == ScaleType.FIT_HEIGHT) {
                val targetWith = params.frameWidth * selfHeight / params.frameHeight
                val delta = (targetWith - selfWidth) / 2
                textureView.layout(left - delta, top, right + delta, bottom)
            } else {
                val targetHeight = params.frameHeight * selfWidth / params.frameWidth
                val delta = (targetHeight - selfHeight) / 2
                textureView.layout(left, top - delta, right, bottom + delta)
            }
        }
    
    
        override fun onSurfaceTextureSizeChanged(surface: SurfaceTexture?, width: Int, height: Int) {
            //屏幕旋转的时候这个方法才会执行
        }
    
        override fun onSurfaceTextureUpdated(surface: SurfaceTexture?) {
        }
    
        override fun onSurfaceTextureDestroyed(surface: SurfaceTexture?): Boolean {
            surface?.release()
            releaseCamera()
            return true
        }
    
        override fun onSurfaceTextureAvailable(surface: SurfaceTexture?, width: Int, height: Int) {
            //这个方法只会执行一次
            this.surface = surface
    
            //在外面的onResume第一次调用resumeCamera()是打不开相机的,需要在这里调用才能开启第一次相机预览
            //因为onResume的时候还无法获取到当前控件的宽度
            resumeCamera()
        }
    
        /**
         * 通过对比得到与宽高比最接近的预览尺寸(如果有相同尺寸,优先选择)
         *
         * @param isPortrait 是否竖屏
         * @param surfaceWidth 需要被进行对比的原宽
         * @param surfaceHeight 需要被进行对比的原高
         * @param preSizeList 需要对比的预览尺寸列表
         * @return 得到与原宽高比例最接近的尺寸
         */
        private fun getCloselyPreSize(
            isPortrait: Boolean,
            surfaceWidth: Int,
            surfaceHeight: Int,
            preSizeList: List<Camera.Size>
        ): Camera.Size? {
            val reqTmpWidth: Int
            val reqTmpHeight: Int
            // 当屏幕为垂直的时候需要把宽高值进行调换,保证宽大于高
            if (isPortrait) {
                reqTmpWidth = surfaceHeight
                reqTmpHeight = surfaceWidth
            } else {
                reqTmpWidth = surfaceWidth
                reqTmpHeight = surfaceHeight
            }
            //先查找preview中是否存在与surfaceview相同宽高的尺寸
            for (size in preSizeList) {
                if (size.width == reqTmpWidth && size.height == reqTmpHeight) {
                    return size
                }
            }
    
            // 得到与传入的宽高比最接近的size
            val reqRatio = reqTmpWidth.toFloat() / reqTmpHeight
            var curRatio: Float
            var deltaRatio: Float
            var deltaRatioMin = java.lang.Float.MAX_VALUE
            var retSize: Camera.Size? = null
            for (size in preSizeList) {
                curRatio = size.width.toFloat() / size.height
                deltaRatio = Math.abs(reqRatio - curRatio)
                if (deltaRatio < deltaRatioMin) {
                    deltaRatioMin = deltaRatio
                    retSize = size
                }
            }
            return retSize
        }
    
    
        /**
         * 预览View中的坐标映射到,原始图片中。应用场景举例:裁剪框
         *
         * @param rect 预览View中的坐标
         */
        fun mapToOriginalRect(rect: RectF) {
            val selfWidth = width
            val selfHeight = height
            if (params.frameWidth == 0 || params.frameHeight == 0 || selfWidth == 0 || selfHeight == 0) {
                return
                // TODO
            }
            val matrix = Matrix()
            val scaleType = resolveScaleType()
            if (scaleType == ScaleType.FIT_HEIGHT) {
                val targetWith = params.frameWidth * selfHeight / params.frameHeight
                val delta = (targetWith - selfWidth) / 2
                val ratio = 1.0f * params.frameHeight / selfHeight
                matrix.postTranslate(delta.toFloat(), 0f)
                matrix.postScale(ratio, ratio)
            } else {
                val targetHeight = params.frameHeight * selfWidth / params.frameWidth
                val delta = (targetHeight - selfHeight) / 2
    
                val ratio = 1.0f * params.frameWidth / selfWidth
                matrix.postTranslate(0f, delta.toFloat())
                matrix.postScale(ratio, ratio)
            }
            matrix.mapRect(rect)
        }
    
        /**
         * 原始图片(传到SDK里面进行处理的图片)中的坐标到预览View坐标中的映射。应用场景举例:预览页面显示人脸框。
         *
         * @param rectF 原始图中的坐标
         */
        fun mapFromOriginalRect(rectF: RectF, rawImgWeight: Int, rawImgHeight: Int) {
            val selfWidth = width
            val selfHeight = height
            if (rawImgWeight == 0 || rawImgHeight == 0 || selfWidth == 0 || selfHeight == 0) {
                return
            }
    
            val matrix = Matrix()
    
            val scaleType = resolveScaleType()
            if (scaleType == ScaleType.FIT_HEIGHT) {
                val targetWith = rawImgWeight * selfHeight / rawImgHeight
                val delta = (targetWith - selfWidth) / 2
    
                val ratio = 1.0f * selfHeight / rawImgHeight
                matrix.postScale(ratio, ratio)
                matrix.postTranslate((-delta).toFloat(), 0f)
            } else {
                val targetHeight = rawImgHeight * selfWidth / rawImgWeight
                val delta = (targetHeight - selfHeight) / 2
    
                val ratio = 1.0f * selfWidth / rawImgWeight
    
                matrix.postScale(ratio, ratio)
                matrix.postTranslate(0f, (-delta).toFloat())
            }
            matrix.mapRect(rectF)
    
            if (params.isHorizontalMirrored) {
                val left = selfWidth - rectF.right
                val right = left + rectF.width()
                rectF.left = left
                rectF.right = right
            }
        }
    
        private fun resolveScaleType(): ScaleType {
            val selfRatio = 1.0f * width / height
            val targetRatio = 1.0f * params.frameWidth / params.frameHeight
    
            var scaleType = this.scaleType
            if (this.scaleType == ScaleType.CROP_INSIDE) {
                scaleType =
                    if (selfRatio > targetRatio) ScaleType.FIT_WIDTH else ScaleType.FIT_HEIGHT
            }
            return scaleType
        }
    
    }
    
    

    相关文章

      网友评论

        本文标题:Android CameraPreview快速适配 (人脸识别、

        本文链接:https://www.haomeiwen.com/subject/btgnpqtx.html