NO.2 CameraX
程序员文章站
2023-12-25 20:27:51
...
零蚀
[camerax 官方文档(*)](这是一个高能网站,很多相关的google教程都在这)
配置 ->动态权限 ->预览->文件->YUV_420_888转bitmap
step 1 (依赖)
导入最新的camerax 版本
// Use the most recent version of CameraX, currently that is alpha06.
ext {
amerax_version = '1.0.0-alpha06'
}
implementation "androidx.camera:camera-core:${camerax_version}"
implementation "androidx.camera:camera-camera2:${camerax_version}"
还需要jdk1.8的支持
// android 下
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
camera显示控件TextureView
<TextureView
android:id="@+id/view_finder"
android:layout_width="640px"
android:layout_height="640px"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
step 2(权限)
权限(动态权限)
<uses-permission android:name="android.permission.CAMERA" />
// 获取权限
if(ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA)
!=PERMISSION_GRANTED){
ActivityCompat.requestPermissions(this, arrayOf(
Manifest.permission.WRITE_EXTERNAL_STORAGE,
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.CAMERA
),10001)
}else{
LogUtil.d("已经申请了权限")
normalInit()
}
// 权限返回值
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if(requestCode==10001){
permissions.forEachIndexed { index, it ->
if(grantResults[index]==0) {
LogUtil.d("权限: $it 申请成功")
}else{
LogUtil.d("权限: $it 申请失败")
}
}
// 初始化摄像头等操作
normalInit()
}
}
step 3(最简demo)
添加监听
object CameraBaseUtil{
.....
fun initCameraListener(view:TextureView){
view.addOnLayoutChangeListener { v,
left, top, right, bottom,
oldLeft, oldTop, oldRight, oldBottom ->
LogUtil.d("camera的监听发生改变")
}
}
....
构建camera,启动,变化,结束的demo
object CameraWorkUtil {
private var preview:Preview?=null
enum class CameraStatus {
START,
UPDATE,
FINISH
}
fun initCameraListener(view: TextureView) {
view.addOnLayoutChangeListener { v,
left, top, right, bottom,
oldLeft, oldTop, oldRight, oldBottom ->
//action(this,view,CameraStatus.UPDATE)
LogUtil.d("camera的监听发生改变")
}
}
fun action(owner: LifecycleOwner, view: TextureView, status: CameraStatus) {
when (status) {
CameraStatus.START -> startCamera(owner, view)
CameraStatus.UPDATE -> upDataCamera()
CameraStatus.FINISH -> finishCamera()
}
}
private fun finishCamera() {
if (preview!=null) {
CameraX.unbind(preview)
}
}
private fun upDataCamera() {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
private fun startCamera(owner: LifecycleOwner, viewFinder: TextureView) {
// 创建配置对象,供给TextureView使用
val previewConfig = PreviewConfig.Builder().apply {
setTargetResolution(Size(640, 480))
}.build()
// 构建使用场景
preview = Preview(previewConfig)
// 每当发生变化都会使得,重新计算&布局
preview.setOnPreviewOutputUpdateListener {
val parent = viewFinder.parent as ViewGroup
parent.removeView(viewFinder)
parent.addView(viewFinder, 0)
viewFinder.surfaceTexture = it.surfaceTexture
//upDataCamera()
}
CameraX.bindToLifecycle(owner,preview)
}
}
step 4(生成文件)
获取图像的文件操作
private fun startCamera(owner: LifecycleOwner, viewFinder: TextureView) {
val previewConfig = PreviewConfig.Builder().apply {
setTargetResolution(Size(640,640))
}.build()
val preview=Preview(previewConfig)
// 每当发生变化都会使得,重新计算&布局
preview.setOnPreviewOutputUpdateListener {
val parent = viewFinder.parent as ViewGroup
parent.removeView(viewFinder)
parent.addView(viewFinder, 0)
viewFinder.surfaceTexture = it.surfaceTexture
//upDataCamera()
}
// 捕获图像(它自己氛围低质和高质两种图像,差异在于获取的时间)
// 这里不在设置分辨率,而是相位定量
val imageCaptureConfig = ImageCaptureConfig.Builder()
.apply {
setCaptureMode(ImageCapture.CaptureMode.MIN_LATENCY)
}.build()
// 将图像捕捉的场景依附于点击监听上。
imageCapture = ImageCapture(imageCaptureConfig)
CameraX.bindToLifecycle(owner, preview , imageCapture)
}
fun getPictureConfig(file:File,listener:OnCameraCaptureInter){
imageCapture?.takePicture(file, Executors.newCachedThreadPool(),
object :ImageCapture.OnImageSavedListener{
override fun onImageSaved(file: File) {
listener.success(file)
}
override fun onError(imageCaptureError: ImageCapture.ImageCaptureError, message: String, cause: Throwable?) {
listener.error(message)
}
})
}
ivCaptureButton.setOnClickListener {
// 内存地址都是固定的,不能创建文件夹
val file=File(Environment.getExternalStorageDirectory().absolutePath+
File.separator+
Environment.DIRECTORY_PICTURES,
"${System.currentTimeMillis()}.jpg")
LogUtil.e(file.absolutePath)
CameraBaseUtil.getPictureConfig(file,object:OnCameraCaptureInter{
override fun success(file: File) {
toast("the file's path is ${file.path}")
}
override fun error(message: String) {
toast(message)
}
})
}
step 5(YUV_420_888)
获取图像像素矩阵,并转为bitmap(划重点,官网没有)
首先我们要知道camerax默认的iamgeFormat是YUV_420_888,而一般我们转为图片的格式只有NV21和YUY2所以采用YuvImage来转图片的byteArray是行不通的。(我们从camera直接拿到的byteArray是不能进行直接的Bitmap的,可以了解一下YUV),所以为了转换计算方便需要用到NDK来处理这些byte转换问题
具体转化方法可以看**[???? Android的YUV_420_888图片转换Bitmap时的rowStride问题]**
通过图像代理类,获取到位图
object BitmapUtils {
fun imageToBitmap(image: ImageProxy): Bitmap? {
val yBuffer = image.planes[0].buffer
val uBuffer = image.planes[1].buffer
val uStride = image.planes[1].pixelStride
val vBuffer = image.planes[2].buffer
val vStride = image.planes[2].pixelStride
val buffer = ByteArray(image.width * image.height * 3 / 2)
val rowStride = image.planes[0].rowStride
val padding = rowStride - image.width
var pos = 0
if (padding == 0) {
pos = yBuffer.remaining()
yBuffer.get(buffer, 0, pos)
} else {
var yBufferPos = 0
for (row in 0 until image.height) {
yBuffer.position(yBufferPos)
yBuffer.get(buffer, pos, image.width)
yBufferPos += rowStride
pos += image.width
}
}
var i = 0
val uRemaining = uBuffer.remaining()
while (i < uRemaining) {
buffer[pos++] = uBuffer[i]
i += uStride
if (padding == 0) continue
val rowLen = i % rowStride
if (rowLen >= image.width) {
i += padding
}
}
i = 0
val vRemaining = vBuffer.remaining()
while (i < vRemaining) {
buffer[pos++] = vBuffer[i]
i += vStride
if (padding == 0) continue
val rowLen = i % rowStride
if (rowLen >= image.width) {
i += padding
}
}
return ColorConvertUtil.yuv420pToBitmap(buffer, image.width, image.height)
}
}
设置analyze,并设置获取图像的数据频率,
class CameraPicAnalyzer(pixel:PixelCameraInter): ImageAnalysis.Analyzer{
// 图像的平面(像素矩阵)缓冲
private fun ByteBuffer.toByteArray(): ByteArray {
rewind() // Rewind the buffer to zero
val data = ByteArray(remaining())
get(data) // Copy the buffer into a byte array
return data // Return the byte array
}
private var lastAnalyzedTimestamp:Long=0
private val listener =pixel
override fun analyze(image: ImageProxy?, rotationDegrees: Int) {
val currentTimestamp = System.currentTimeMillis()
//计算平均频率不超过每秒一次
if (currentTimestamp - lastAnalyzedTimestamp >=
TimeUnit.SECONDS.toMillis(1)) {
if(image!=null){
val bitmap = BitmapUtils.imageToBitmap(image)
listener.data(bitmap!!)
}
lastAnalyzedTimestamp = currentTimestamp
}
}
}
启动摄像头时添加获取数据的场景
private fun startCamera(owner: LifecycleOwner, viewFinder: TextureView) {
val previewConfig = PreviewConfig.Builder().apply {
// 宽高比只有16:9和4:3
setTargetAspectRatio(AspectRatio.RATIO_4_3)
//设置分辨率
setTargetResolution(Size(1280,1280))
}.build()
val preview=Preview(previewConfig)
// 每当发生变化都会使得,重新计算&布局
preview.setOnPreviewOutputUpdateListener {
val parent = viewFinder.parent as ViewGroup
parent.removeView(viewFinder)
parent.addView(viewFinder, 0)
viewFinder.surfaceTexture = it.surfaceTexture
//upDataCamera()
}
// 捕获图像(它自己氛围低质和高质两种图像,差异在于获取的时间)
// 这里不在设置分辨率,而是相位定量
val imageCaptureConfig = ImageCaptureConfig.Builder()
.apply {
setCaptureMode(ImageCapture.CaptureMode.MIN_LATENCY)
}.build()
// 将图像捕捉的场景依附于点击监听上。
imageCapture = ImageCapture(imageCaptureConfig)
// 图像的数据设置场景
val analyzerConfig = ImageAnalysisConfig.Builder().apply {
setImageReaderMode(ImageAnalysis.ImageReaderMode.ACQUIRE_LATEST_IMAGE)
}.build()
val analyzerUseCase = ImageAnalysis(analyzerConfig).apply {
setAnalyzer(executors, analyze)
}
CameraX.bindToLifecycle(owner, preview , imageCapture, analyzerUseCase)
}
调用方法
fun getPictureConfig():Bitmap{
val matrix = Matrix()
matrix.postRotate(90f)
return Bitmap.createBitmap(data, 0, 0, data!!.width, data!!.height,matrix, true)
}
//布局中调用
ivCapture.setOnClickListener {
image.setImageBitmap(cameraFactory.getPictureConfig())
}
这样我们就能获取到他的bitmap并且在这转化中就可以对这bitmap进行人像操作等,进行诸多设计。
由于只支持4:3和16:9所以无法获取原生的正方形图。