Android CameraPreview快速適配 (人臉識(shí)別、相機(jī)視頻流)

一、開(kāi)發(fā)背景

由于在項(xiàng)目中需要集成多個(gè)人臉識(shí)別SDK,然后這些SDK有些有自己的Camera API(但又不是特別獨(dú)立),有的SDK甚至都沒(méi)有像樣的Camera API。所以我就自己寫了一個(gè)通用的,用來(lái)抓取視頻流的Camera1的代碼封裝。

二、特色

  • 快速適配各類相機(jī)(物理旋轉(zhuǎn)、鏡像、各種角度旋轉(zhuǎn)等)

  • 控件大小自動(dòng)適配

  • 人臉位置映射

  • 使用緩沖機(jī)制獲取視頻流

  • 代碼簡(jiǎn)單(一個(gè)Class)、引入方面,CameraPreview的完整代碼在第五點(diǎn)

1.png
2.png

三、基本使用,炒雞簡(jiǎn)單

  • 1、一個(gè) 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、恢復(fù)和停止 (默認(rèn)是前置攝像頭 適配設(shè)備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、相關(guān)的權(quán)限處理就需要自己完成了

四、進(jìn)階使用

  • 1、設(shè)置預(yù)覽回調(diào)幀 確保在resumeCamea()之前調(diào)用即可
val previewFrameCallBack = Camera.PreviewCallback { data, _ ->
    Log.d("CameraTextureActivity", "preview camera byte data $data")
}
camera_preview.previewCallBack = previewFrameCallBack
  • 2、針對(duì)特定的相機(jī)進(jìn)行適配
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里面進(jìn)行處理的圖片)中的坐標(biāo)到預(yù)覽View坐標(biāo)中的映射。應(yīng)用場(chǎng)景舉例:預(yù)覽頁(yè)面顯示人臉框。
     *
     * @param rectF 原始圖中的坐標(biāo)
     */
    fun mapFromOriginalRect(rectF: RectF, rawImgWeight: Int, rawImgHeight: Int)

五、引入:一個(gè)class,拷貝到項(xiàng)目中即可

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

/**
 * 基于系統(tǒng)TextureView和Camera1實(shí)現(xiàn)的預(yù)覽View;
 */
class CameraPreview : FrameLayout,
    TextureView.SurfaceTextureListener {

    lateinit var textureView: TextureView

    /**
     * 設(shè)置預(yù)覽的縮放類型
     * 縮放類型
     */
    var scaleType: ScaleType = ScaleType.CROP_INSIDE

    /**
     * 圖片幀縮放類型。
     */
    enum class ScaleType {
        /**
         * 寬度與父控件一致,高度自適應(yīng)
         */
        FIT_WIDTH,
        /**
         * 調(diào)試與父控件一致,寬度自適應(yīng)
         */
        FIT_HEIGHT,
        /**
         * 全屏顯示 ,保持顯示比例,多余的部分會(huì)被裁剪掉。
         */
        CROP_INSIDE
    }

    private var surface: SurfaceTexture? = null

    var mCamera: Camera? = null

    private var params = Params()


    /**
     * 默認(rèn)配置是一加6的前置攝像頭
     */
    class Params {

        var cameraId = 1

        /**
         * 預(yù)覽幀的旋轉(zhuǎn)角度(順時(shí)針)
         */
        var previewRotation = 90f

        /**
         *   經(jīng)過(guò)Camera處理后(比如旋轉(zhuǎn))的PreviewFrame的圖像寬度   比如1280*720旋轉(zhuǎn)后就會(huì)變成720*1280
         */
        var frameWidth: Int = 0

        /**
         *   經(jīng)過(guò)Camera處理后(比如旋轉(zhuǎn))的PreviewFrame的圖像高度
         */
        var frameHeight: Int = 0

        /**
         * 是否對(duì)視頻進(jìn)行了鏡像處理(水平鏡像)
         */
        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

    /**
     * 設(shè)置視頻流回調(diào)(帶緩沖)
     */
    var previewCallBack: Camera.PreviewCallback? = null


    /**
     *  對(duì)特定的相機(jī)進(jìn)行參數(shù)配置
     */
    fun setupCameraParams(params: Params) {
        this.params = params
    }

    /**
     * 根據(jù)現(xiàn)有的參數(shù)對(duì)相機(jī)進(jìn)行設(shè)置
     */
    private fun applyParams() {

        if (mCamera == null) {
            mCamera = Camera.open(params.cameraId)  //1
        }

        val cameraParams = mCamera!!.parameters

        //使用默認(rèn)參數(shù)
        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

        }

        //預(yù)覽相片的尺寸:相機(jī)傳過(guò)來(lái)的  注意是橫著的
        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) {
            //進(jìn)行90度旋轉(zhuǎn)后,需要進(jìn)行寬高轉(zhuǎn)換
            val temp = params.frameHeight
            params.frameHeight = params.frameWidth
            params.frameWidth = temp
        } else {
            //進(jìn)行90度旋轉(zhuǎn)后,需要進(jìn)行寬高轉(zhuǎn)換
            val temp = params.frameWidth
            params.frameWidth = params.frameHeight
            params.frameHeight = temp
        }


        //讓視頻根據(jù)控件大小進(jìn)行適配
        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) {
                //不要二次設(shè)置mPreviewBuffer 否則可能會(huì)有畫面延遲,原因還不知道
                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
    }


    /**
     * 對(duì)布局大小進(jìn)行重繪
     */
    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) {
        //屏幕旋轉(zhuǎn)的時(shí)候這個(gè)方法才會(huì)執(zhí)行
    }

    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) {
        //這個(gè)方法只會(huì)執(zhí)行一次
        this.surface = surface

        //在外面的onResume第一次調(diào)用resumeCamera()是打不開(kāi)相機(jī)的,需要在這里調(diào)用才能開(kāi)啟第一次相機(jī)預(yù)覽
        //因?yàn)閛nResume的時(shí)候還無(wú)法獲取到當(dāng)前控件的寬度
        resumeCamera()
    }

    /**
     * 通過(guò)對(duì)比得到與寬高比最接近的預(yù)覽尺寸(如果有相同尺寸,優(yōu)先選擇)
     *
     * @param isPortrait 是否豎屏
     * @param surfaceWidth 需要被進(jìn)行對(duì)比的原寬
     * @param surfaceHeight 需要被進(jìn)行對(duì)比的原高
     * @param preSizeList 需要對(duì)比的預(yù)覽尺寸列表
     * @return 得到與原寬高比例最接近的尺寸
     */
    private fun getCloselyPreSize(
        isPortrait: Boolean,
        surfaceWidth: Int,
        surfaceHeight: Int,
        preSizeList: List<Camera.Size>
    ): Camera.Size? {
        val reqTmpWidth: Int
        val reqTmpHeight: Int
        // 當(dāng)屏幕為垂直的時(shí)候需要把寬高值進(jìn)行調(diào)換,保證寬大于高
        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
    }


    /**
     * 預(yù)覽View中的坐標(biāo)映射到,原始圖片中。應(yīng)用場(chǎng)景舉例:裁剪框
     *
     * @param rect 預(yù)覽View中的坐標(biāo)
     */
    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里面進(jìn)行處理的圖片)中的坐標(biāo)到預(yù)覽View坐標(biāo)中的映射。應(yīng)用場(chǎng)景舉例:預(yù)覽頁(yè)面顯示人臉框。
     *
     * @param rectF 原始圖中的坐標(biāo)
     */
    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
    }

}

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 發(fā)現(xiàn) 關(guān)注 消息 iOS 第三方庫(kù)、插件、知名博客總結(jié) 作者大灰狼的小綿羊哥哥關(guān)注 2017.06.26 09:4...
    肇東周閱讀 15,397評(píng)論 4 61
  • 1. 需求與追求,先滿足誰(shuí)? 先舉兩個(gè)栗子,周杰倫畢業(yè)于淡江音樂(lè)中專鋼琴系,也曾在餐廳打工端盤子;李開(kāi)復(fù),在卡內(nèi)基...
    葡萄樹(shù)上的靜子閱讀 1,329評(píng)論 0 1
  • 前些天我在微博上看到一個(gè)講國(guó)內(nèi)爛俗景點(diǎn)的帖子,發(fā)現(xiàn)其中好幾個(gè)景點(diǎn)自己在多年前都曾去過(guò)。但回想起來(lái),記憶中卻并沒(méi)有廉...
    一閃一閃周大星閱讀 343評(píng)論 0 1
  • 正能量 總能自帶光芒 負(fù)能量 多是暗淡憂傷 多去接觸正能量 才能讓內(nèi)心敞亮 時(shí)刻遠(yuǎn)離負(fù)能量 才不會(huì)讓自我迷茫 好的...
    神于天圣于地閱讀 190評(píng)論 0 2
  • 雖然昨天看到老師發(fā)的期末考試成績(jī),感覺(jué)慈兒此次發(fā)揮不錯(cuò),真的沒(méi)想到能拿到三好生。所以,今天看到榮譽(yù)證書,真是很欣喜...
    莫潸1970閱讀 298評(píng)論 5 1

友情鏈接更多精彩內(nèi)容