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

Android Camera预览:SurfaceView与ImageView显示差异及帧残留Artifact问题

Camera预览Artifact问题排查与解决

问题现象

基于AOSP及SOM提供商源码开发时,使用SurfaceView搭配Camera类预览清晰,但通过onPreviewFrame回调将预览数据显示在ImageView中时,出现持续的线条类Artifact,疑似前一帧残留:

  • 图像可划分为5个均等垂直区域;高分辨率下,Artifact仅出现在每个区域的第一个垂直象限,位置固定、间距均等,但并非每帧都出现所有Artifact,水平方向看似随机分布;
  • 分辨率减半后,Artifact覆盖每个区域的整个水平范围,但5区域划分仍清晰可见。

Camera预览Artifact示例

相关代码片段

import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.ImageFormat;
import android.graphics.Rect;
import android.graphics.YuvImage;
import android.hardware.Camera;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;

import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;

import com.example.testapp.R;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Timer;
import java.util.TimerTask;

public class CameraTestFragment extends Fragment implements Camera.PreviewCallback, SurfaceHolder.Callback {

    private View root;

    private SurfaceView surfacePreview;

    private ImageView testPreview;

    private Camera camera;

    private boolean isActive;

    @Override
    public View onCreateView(@NonNull LayoutInflater inflater,
                             ViewGroup container, Bundle savedInstanceState) {

        root = inflater.inflate(R.layout.fragment_camera_test, container, false);

        surfacePreview = root.findViewById(R.id.surface_preview_capture);
        testPreview = root.findViewById(R.id.testPreview);

        surfacePreview.getHolder().addCallback(this);

        updateVideoPreview(surfacePreview.getHolder());

        return root;
    }

    @Override
    public void onDestroyView() {
        super.onDestroyView();

        if(camera != null){
            isActive = false;
            camera.setPreviewCallback(null);
            camera.stopPreview();
            camera.release();
            camera = null;
        }
    }

    @Override
    public void onPreviewFrame(byte[] data, Camera camera)
    {
        if(isActive) {
            Camera.Size  previewSize = camera.getParameters().getPreviewSize();
            YuvImage yuvImage = new YuvImage(data, ImageFormat.NV21, previewSize.width, previewSize.height, null);
            ByteArrayOutputStream out = new ByteArrayOutputStream();
            yuvImage.compressToJpeg(new Rect(0,0,previewSize.width,previewSize.height),100,out);
            byte[] jpegData = out.toByteArray();
            Bitmap bitmap = BitmapFactory.decodeByteArray(jpegData, 0, jpegData.length);
            testPreview.setImageBitmap(bitmap);
        }
    }


    @Override
    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
        Log.d("VideoPreviewHandler",  "surfaceChanged");
        updateVideoPreview(holder);
    }

    @Override
    public void surfaceCreated(SurfaceHolder holder) {

    }

    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {
        Log.d("VideoPreviewHandler",  "surfaceDestroyed");

    }

    public void updateVideoPreview(SurfaceHolder holder) {

        Log.d("VideoPreviewHandler",  "surfaceDestroyed");

        camera = Camera.open();

        if (camera != null) {

            try {

                Camera.Parameters cp = camera.getParameters();
                cp.setPreviewSize(640, 360);
                cp.setPreviewFormat(ImageFormat.NV21);
                cp.setPreviewFpsRange(60, 60);
                try {
                    camera.setParameters(cp);
                } catch (RuntimeException e) {
                    Log.d("RuntimeException", e.getMessage());
                }

                camera.setPreviewDisplay(holder);
                camera.setPreviewCallback(this);
                camera.startPreview();

                isActive = true;
            } catch (IOException e) {
                e.printStackTrace();
            }
        } else {
            new Timer().schedule(new TimerTask() {
                @Override
                public void run() {
                    updateVideoPreview(surfacePreview.getHolder());
                }
            }, 1000);
        }
    }

}

疑问解答

1. SurfaceView与ImageView预览差异原因

SurfaceView预览走硬件加速的直接渲染路径,数据从Camera HAL直接输出到Surface的GPU缓冲区,无用户空间内存拷贝和额外编解码操作;而onPreviewFrame回调是将原始YUV数据拷贝到用户空间,再经过YUV转JPEG、JPEG转Bitmap的软件编解码流程,若数据处理不及时、缓冲区复用异常或格式转换对齐错误,就会出现Artifact。

2. AOSP中两种流程的差异

  • SurfaceView预览流程:Camera HAL → Camera Service → SurfaceFlinger → 硬件显示控制器。数据在GPU/硬件层面流转,使用连续硬件缓冲区,由系统负责帧同步和缓冲区管理,无帧残留问题。
  • onPreviewFrame回调流程:Camera HAL → Camera Service → 应用进程用户空间(byte[] data)→ 应用内YUV转JPEG、Bitmap解码 → ImageView绘制。数据需多次内存拷贝,且应用层需自行处理帧同步和缓冲区复用,处理不当易出现数据覆盖、残留。

3. Artifact的成因与解决方法

成因

从固定区域、分辨率相关性来看,核心原因是YUV数据行对齐/stride不匹配:

  • Camera输出的NV21格式数据中,实际行字节数(stride)可能因硬件要求(如16字节对齐)大于设定的预览宽度,但代码中直接用previewSize.width创建YuvImage,导致每行数据读取不完整,下一行数据错位覆盖,形成固定位置线条。
  • 分辨率变化时,stride对齐规则不变,因此区域划分模式保留,仅覆盖范围随分辨率调整。
    另外,onPreviewFrame的byte[] data是复用缓冲区,若当前帧未处理完Camera已写入下一帧,会导致数据污染出现前帧残留。

解决方法

  1. 使用正确的stride值:通过数据总长度反推实际stride,避免直接用预览宽度:
    @Override
    public void onPreviewFrame(byte[] data, Camera camera)
    {
        if(isActive) {
            Camera.Size previewSize = camera.getParameters().getPreviewSize();
            int actualWidth = previewSize.width;
            int actualHeight = previewSize.height;
            // 反推Y分量的stride
            int yStride = (data.length - actualWidth * actualHeight / 2) / actualHeight;
            // 传入正确的stride数组(Y和UV分量的stride)
            YuvImage yuvImage = new YuvImage(data, ImageFormat.NV21, actualWidth, actualHeight, new int[]{yStride, yStride/2});
            ByteArrayOutputStream out = new ByteArrayOutputStream();
            yuvImage.compressToJpeg(new Rect(0,0,actualWidth,actualHeight),100,out);
            byte[] jpegData = out.toByteArray();
            Bitmap bitmap = BitmapFactory.decodeByteArray(jpegData, 0, jpegData.length);
            testPreview.setImageBitmap(bitmap);
        }
    }
    
  2. 避免缓冲区复用污染:改用setPreviewCallbackWithBuffer自行管理缓冲区:
    // 初始化时申请缓冲区
    Camera.Size previewSize = camera.getParameters().getPreviewSize();
    int bufferSize = previewSize.width * previewSize.height * 3 / 2;
    byte[] buffer = new byte[bufferSize];
    camera.addCallbackBuffer(buffer);
    camera.setPreviewCallbackWithBuffer(this);
    
    // onPreviewFrame处理完后重新添加缓冲区
    @Override
    public void onPreviewFrame(byte[] data, Camera camera) {
        if(isActive) {
            // 数据处理逻辑...
            camera.addCallbackBuffer(data);
        }
    }
    
  3. 优化性能:将YUV转Bitmap操作放到子线程,避免主线程阻塞导致帧处理不及时,减少缓冲区被覆盖概率。

内容的提问来源于stack exchange,提问作者Sefa Teber

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.25 15:45:55