You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

CameraX预览模糊实现失败:捕获预览Bitmap为空问题求助

CameraX 实现模糊相机预览的正确方案

我正在使用CameraX构建相机预览,想要给预览添加模糊效果。尝试过捕获PreviewView的TextureView转Bitmap再用RenderScript模糊,但日志一直显示"Camera preview bitmap is null",参考的旧方法也不生效。


问题根源

  • PreviewView 内部实现不确定:CameraX的PreviewView默认使用SurfaceView而非TextureView,所以cameraPreview.getChildAt(0) as? TextureView大概率返回null,导致无法获取Bitmap。
  • 重复绑定相机:原代码中startCamera和cameraProviderFuture回调都执行了相机绑定逻辑,会导致冲突。
  • 帧捕获方式错误:依赖TextureView的bitmap属性获取预览帧不可靠,应该用CameraX的ImageAnalysis用例稳定获取每一帧。

修正方案

1. 用ImageAnalysis替代TextureView捕获帧

通过CameraX的ImageAnalysis用例直接获取相机帧,避免依赖PreviewView的内部视图结构。

2. 统一相机绑定逻辑

移除重复的绑定代码,确保权限通过后只绑定一次相机用例(Preview + ImageAnalysis)。

3. 正确处理YUV图像转Bitmap

CameraX返回的图像是YUV_420_888格式,需要转换为RGB Bitmap才能正常应用模糊效果。

4. 优化模糊性能与内存管理

缩小图片后再模糊提升性能,同时及时回收Bitmap、关闭ImageProxy、销毁RenderScript资源,避免内存泄漏。


修正后的完整代码

import android.Manifest
import android.content.Intent
import android.content.pm.PackageManager
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.ImageFormat
import android.graphics.Matrix
import android.graphics.Rect
import android.graphics.YuvImage
import android.os.Bundle
import android.util.Log
import android.view.View
import android.widget.ImageView
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.camera.core.CameraSelector
import androidx.camera.core.ImageAnalysis
import androidx.camera.core.ImageProxy
import androidx.camera.core.Preview
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.camera.view.PreviewView
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import android.renderscript.*
import java.io.ByteArrayOutputStream
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors

class WelcomeActivity : AppCompatActivity() {
    private lateinit var cameraPreview: PreviewView
    private lateinit var cameraBtn: View
    private lateinit var blurOverlayImageView: ImageView
    private lateinit var cameraProviderFuture: androidx.camera.core.impl.ListenableFuture<ProcessCameraProvider>
    private lateinit var cameraExecutor: ExecutorService
    private var blurLevel: Int = 12
    private val CAMERA_PERM_CODE = 101
    private val TAG = "CameraXBlur"

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_welcome)

        cameraPreview = findViewById(R.id.cameraPreview)
        cameraBtn = findViewById(R.id.cameraButton)
        blurOverlayImageView = findViewById(R.id.blurOverlay)
        cameraExecutor = Executors.newSingleThreadExecutor()

        if (allPermissionsGranted()) {
            startCamera()
        } else {
            ActivityCompat.requestPermissions(
                this, arrayOf(Manifest.permission.CAMERA), CAMERA_PERM_CODE
            )
        }

        cameraBtn.setOnClickListener {
            startActivity(Intent(this, CameraActivity::class.java))
        }
    }

    private fun startCamera() {
        cameraProviderFuture = ProcessCameraProvider.getInstance(this)

        cameraProviderFuture.addListener({
            val cameraProvider = cameraProviderFuture.get()

            // 配置预览用例
            val preview = Preview.Builder()
                .build()
                .also {
                    it.setSurfaceProvider(cameraPreview.surfaceProvider)
                }

            // 配置图像分析用例,获取预览帧
            val imageAnalysis = ImageAnalysis.Builder()
                .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
                .build()
                .also {
                    it.setAnalyzer(cameraExecutor) { imageProxy ->
                        processImageProxy(imageProxy)
                    }
                }

            val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA

            try {
                cameraProvider.unbindAll()
                cameraProvider.bindToLifecycle(
                    this, cameraSelector, preview, imageAnalysis
                )
            } catch (exc: Exception) {
                Log.e(TAG, "相机绑定失败", exc)
            }
        }, ContextCompat.getMainExecutor(this))

        initializeBlurAnimator()
    }

    private fun processImageProxy(imageProxy: ImageProxy) {
        val bitmap = imageProxy.toBitmap()
        bitmap?.let {
            val blurredBitmap = blurRenderScript(it, blurLevel)
            runOnUiThread {
                blurOverlayImageView.setImageBitmap(blurredBitmap)
            }
            it.recycle()
            blurredBitmap?.recycle()
        }
        imageProxy.close()
    }

    // 将ImageProxy转换为Bitmap并修正方向
    private fun ImageProxy.toBitmap(): Bitmap? {
        val image = this.image ?: return null
        val yuvImage = YuvImage(
            image.planes[0].buffer,
            ImageFormat.NV21,
            image.width,
            image.height,
            null
        )
        val outputStream = ByteArrayOutputStream()
        yuvImage.compressToJpeg(Rect(0, 0, image.width, image.height), 100, outputStream)
        val bitmap = BitmapFactory.decodeByteArray(outputStream.toByteArray(), 0, outputStream.size)

        val matrix = Matrix()
        matrix.postRotate(imageInfo.rotationDegrees.toFloat())
        val rotatedBitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true)
        bitmap.recycle()
        return rotatedBitmap
    }

    // RenderScript模糊处理
    private fun blurRenderScript(inputBitmap: Bitmap, radius: Int): Bitmap? {
        val maxRadius = 25
        val safeRadius = if (radius > maxRadius) maxRadius else radius

        // 缩小图片提升性能
        val scale = 0.2f
        val scaledWidth = (inputBitmap.width * scale).toInt()
        val scaledHeight = (inputBitmap.height * scale).toInt()
        val scaledBitmap = Bitmap.createScaledBitmap(inputBitmap, scaledWidth, scaledHeight, false)

        val outputBitmap = Bitmap.createBitmap(scaledBitmap)
        var renderScript: RenderScript? = null
        try {
            renderScript = RenderScript.create(this)
            val intrinsic = ScriptIntrinsicBlur.create(renderScript, Element.U8_4(renderScript))
            val tmpIn = Allocation.createFromBitmap(renderScript, scaledBitmap)
            val tmpOut = Allocation.createFromBitmap(renderScript, outputBitmap)

            intrinsic.setRadius(safeRadius.toFloat())
            intrinsic.setInput(tmpIn)
            intrinsic.forEach(tmpOut)
            tmpOut.copyTo(outputBitmap)

            return Bitmap.createScaledBitmap(outputBitmap, inputBitmap.width, inputBitmap.height, false)
        } catch (e: Exception) {
            Log.e(TAG, "模糊处理失败", e)
            return null
        } finally {
            scaledBitmap.recycle()
            outputBitmap.recycle()
            renderScript?.destroy()
        }
    }

    // 模糊程度动画
    private fun initializeBlurAnimator() {
        val animator = androidx.animation.ValueAnimator.ofInt(6, 12, 18, 24, 25)
        animator.repeatCount = androidx.animation.ValueAnimator.INFINITE
        animator.repeatMode = androidx.animation.ValueAnimator.RESTART
        animator.duration = 3000
        animator.addUpdateListener {
            blurLevel = it.animatedValue as Int
        }
        animator.start()
    }

    private fun allPermissionsGranted() = ActivityCompat.checkSelfPermission(
        this, Manifest.permission.CAMERA
    ) == PackageManager.PERMISSION_GRANTED

    override fun onRequestPermissionsResult(
        requestCode: Int, permissions: Array<String>, grantResults: IntArray
    ) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)
        if (requestCode == CAMERA_PERM_CODE) {
            if (allPermissionsGranted()) {
                startCamera()
            } else {
                Toast.makeText(this, "需要相机权限才能使用相机", Toast.LENGTH_SHORT).show()
            }
        }
    }

    override fun onDestroy() {
        super.onDestroy()
        cameraExecutor.shutdown()
    }
}

关键说明

  • ImageAnalysis 稳定性:直接从相机获取帧数据,不受PreviewView内部视图变化影响。
  • 图像格式转换:处理YUV转RGB的过程,同时修正图像旋转角度,保证预览方向正确。
  • 性能优化:先缩小图片再模糊,大幅降低计算量,模糊完成后再放大回原尺寸。
  • 内存安全:及时回收Bitmap资源、关闭ImageProxy、销毁RenderScript,避免内存泄漏。

内容的提问来源于stack exchange,提问作者Ka hei Tong

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.10 13:23:09