一、开发背景
由于在项目中需要集成多个人脸识别SDK,然后这些SDK有些有自己的Camera API(但又不是特别独立),有的SDK甚至都没有像样的Camera API。所以我就自己写了一个通用的,用来抓取视频流的Camera1的代码封装。
二、特色
快速适配各类相机(物理旋转、镜像、各种角度旋转等)
控件大小自动适配
人脸位置映射
使用缓冲机制获取视频流
代码简单(一个Class)、引入方面,CameraPreview的完整代码在第五点
三、基本使用,炒鸡简单
- 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
}
}