简体   繁体   English

Android - 使用 Renderscript 和 Camera2 API 处理相机数据

[英]Android - Processing of Camera Data using Renderscript & Camera2 API

in the following I want to show pieces of my custom camera app code.在下面,我想展示我的自定义相机应用程序代码的片段。 My goal is to apply filters on the incoming video frames and output them.我的目标是对传入的视频帧应用过滤器并输出它们。 For that, I use Renderscript and Camera2.为此,我使用 Renderscript 和 Camera2。

Here is my MainActivity.java (because it is a bit long I deleted the methods dealing with getting the camera permissions):这是我的 MainActivity.java (因为它有点长,我删除了处理获取相机权限的方法):

public class MainActivity extends AppCompatActivity {

    private static final String TAG = "MainActivity";
    private static final int REQUEST_CAMERA_PERMISSION_RESULT = 0;

    private TextureView mTextureView;
    private Button mButton;
    private CameraDevice mCameraDevice;
    private String mCameraId;
    private HandlerThread mBackgroundHandlerThread;
    private Handler mBackgroundHandler;
    private Size mPreviewSize;
    private CaptureRequest.Builder mCaptureRequestBuilder;
    private RsSurfaceRenderer mRenderer;
    private Surface mPreviewSurface;
    private Surface mProcessingNormalSurface;
    private RsCameraPreviewRenderer cameraPreviewRenderer;
    private RenderScript rs;
    private List<Surface> mSurfaces;

    private Toast rendererNameToast;
    private String rendererName;

    private int currentRendererIndex = 0;

    private static List<Class<? extends RsRenderer>> rendererTypes;

    static {
        rendererTypes = new ArrayList<>();
        rendererTypes.add(DefaultRsRenderer.class);
        rendererTypes.add(GreyscaleRsRenderer.class);
        rendererTypes.add(SharpenRenderer.class);
        rendererTypes.add(BlurRsRenderer.class);
        rendererTypes.add(ColorFrameRenderer.class);
        rendererTypes.add(HueRotationRenderer.class);
        rendererTypes.add(TrailsRenderer.class);
        rendererTypes.add(AcidRenderer.class);
    }



    private static final SparseIntArray ORIENTATIONS =
            new SparseIntArray();

    static {
        ORIENTATIONS.append(Surface.ROTATION_0, 0);
        ORIENTATIONS.append(Surface.ROTATION_90, 90);
        ORIENTATIONS.append(Surface.ROTATION_180, 180);
        ORIENTATIONS.append(Surface.ROTATION_270, 270);
    }

    private TextureView.SurfaceTextureListener mSurfaceTextureListener = new TextureView.SurfaceTextureListener() {
        @Override
        public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture, int width, int height) {

            setupCamera(width, height);
            connectCamera();


        }

        @Override
        public void onSurfaceTextureSizeChanged(SurfaceTexture surfaceTexture, int width, int height) {

        }

        @Override
        public boolean onSurfaceTextureDestroyed(SurfaceTexture surfaceTexture) {
            return false;
        }

        @Override
        public void onSurfaceTextureUpdated(SurfaceTexture surfaceTexture) {

        }
    };

    private CameraDevice.StateCallback mCameraDeviceStateCallback = new CameraDevice.StateCallback() {
        @Override
        public void onOpened(@NonNull CameraDevice cameraDevice) {
            mCameraDevice = cameraDevice;
            startPreview();
        }
        @Override
        public void onDisconnected(@NonNull CameraDevice cameraDevice) {
            cameraDevice.close();
            mCameraDevice = null;
        }
        @Override
        public void onError(@NonNull CameraDevice cameraDevice, int i) {
            cameraDevice.close();
            mCameraDevice = null;
        }
    };

    private CameraCaptureSession.StateCallback mCameraCaptureSessionStateCallback = new CameraCaptureSession.StateCallback() {
        @Override
        public void onConfigured(@NonNull CameraCaptureSession cameraCaptureSession) {
            try {
                cameraCaptureSession.setRepeatingRequest(mCaptureRequestBuilder.build(), null, mBackgroundHandler);
            } catch (CameraAccessException e) {
                e.printStackTrace();
            }
        }

        @Override
        public void onConfigureFailed(@NonNull CameraCaptureSession cameraCaptureSession) {

            Toast.makeText(getApplicationContext(), "Unable to setup camera preview", Toast.LENGTH_SHORT).show();
        }
    };


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);

        mTextureView = (TextureView) findViewById(R.id.preview);
        mButton= (Button) findViewById(R.id.next_button);

        mButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                cycleRendererType();
                updateRsRenderer();
                if (rendererNameToast != null) {
                    rendererNameToast.cancel();
                }
                rendererNameToast =
                        Toast.makeText(MainActivity.this, rendererName, Toast.LENGTH_LONG);
                rendererNameToast.show();
            }
        });

        rs = RenderScript.create(this);
        warmUpInBackground(rs);
    }

    private void cycleRendererType() {
        currentRendererIndex++;
        if (currentRendererIndex == rendererTypes.size()) {
            currentRendererIndex = 0;
        }
    }

    private void updateRsRenderer() {
        try {
            RsRenderer renderer = rendererTypes.get(currentRendererIndex).newInstance();
            rendererName = renderer.getName();
            cameraPreviewRenderer.setRsRenderer(renderer);


        } catch (InstantiationException | IllegalAccessException e) {
            throw new RuntimeException(
                    "Unable to create renderer for index " + currentRendererIndex +
                            ", make sure it has a no-arg constructor please.", e);
        }
    }

    @Override
    protected void onResume() {
        super.onResume();
        startBackgroundThread();

        if(mTextureView.isAvailable()){
            setupCamera(mTextureView.getWidth(), mTextureView.getHeight());
            connectCamera();

        }
        else{
            mTextureView.setSurfaceTextureListener(mSurfaceTextureListener);
        }
    }

    @Override
    public void onWindowFocusChanged(boolean hasFocus){
        super.onWindowFocusChanged(hasFocus);

        View decorView = getWindow().getDecorView();
        if(hasFocus){
            decorView.setSystemUiVisibility(
                    View.SYSTEM_UI_FLAG_LAYOUT_STABLE
                            | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
                            | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
                            | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
                            | View.SYSTEM_UI_FLAG_FULLSCREEN
                            | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
            );
        }
    }

    private void connectCamera(){
        CameraManager cameraManager = (CameraManager) getSystemService(Context.CAMERA_SERVICE);
        try{
            if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.M){
                if(ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) ==
                        PackageManager.PERMISSION_GRANTED){
                    cameraManager.openCamera(mCameraId, mCameraDeviceStateCallback, mBackgroundHandler);
                }
                else{
                    if(shouldShowRequestPermissionRationale(Manifest.permission.CAMERA)){
                        Toast.makeText(this, "Video app required access to camera", Toast.LENGTH_SHORT).show();

                    }
                    requestPermissions(new String[] {Manifest.permission.CAMERA}, REQUEST_CAMERA_PERMISSION_RESULT);
                }
            }
            else{
                cameraManager.openCamera(mCameraId, mCameraDeviceStateCallback, mBackgroundHandler);
            }

        } catch (CameraAccessException e) {
            e.printStackTrace();
        }
    }

    @Override
    protected void onPause() {
        closeCamera();
        stopBackgroundThread();
        super.onPause();
    }

    private void closeCamera(){
        if(mCameraDevice != null){
            mCameraDevice.close();
            mCameraDevice = null;
        }
    }

    private void startPreview(){

        SurfaceTexture surfaceTexture = mTextureView.getSurfaceTexture();
        surfaceTexture.setDefaultBufferSize(mPreviewSize.getWidth(), mPreviewSize.getHeight());

        mPreviewSurface = new Surface(surfaceTexture);


        if (mRenderer == null) {
            mRenderer = createNewRendererForCurrentType(mPreviewSize);
        }
        if (mPreviewSurface == null)
            return;

        /*
        * leads us to rgbOutAlloc.setSurface(outputSurface) whereas outputSurface = mPreviewSurface
        *
        * setSurface(Surface):
        * Associate a Surface with this Allocation. This operation is only valid for Allocations with USAGE_IO_OUTPUT.
        *
        * rgbOutAlloc is an RGBA_8888 allocation that can act as a Surface producer.
        * */
        mRenderer.setOutputSurface(mPreviewSurface);

        /*
        * leads us to yuvInAlloc.getSurface()
        *
        * getSurface():
        * Returns the handle to a raw buffer that is being managed by the screen compositor.
        * This operation is only valid for Allocations with USAGE_IO_INPUT.
        *
        * HERE:
        * Get the Surface that the camera will push frames to. This is the Surface from our yuv
        * input allocation. It will recieve a callback when a frame is available from the camera.
        * */
        mProcessingNormalSurface = mRenderer.getInputSurface();



        List<Surface> cameraOutputSurfaces = new ArrayList<>();
        cameraOutputSurfaces.add(mProcessingNormalSurface);


        mSurfaces = cameraOutputSurfaces;

        try {
            mCaptureRequestBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
            mCaptureRequestBuilder.addTarget(mProcessingNormalSurface);

            mCameraDevice.createCaptureSession(
                    mSurfaces,
                    mCameraCaptureSessionStateCallback,
                    mBackgroundHandler);
        } catch (CameraAccessException e) {
            e.printStackTrace();
        }

    }

    private void setupCamera(int width, int height){
        CameraManager cameraManager = (CameraManager) getSystemService(Context.CAMERA_SERVICE);

        try{
            for(String cameraId: cameraManager.getCameraIdList()){
                CameraCharacteristics cameraCharacteristics =
                        cameraManager.getCameraCharacteristics(cameraId);

                if(cameraCharacteristics.get(CameraCharacteristics.LENS_FACING)==
                        CameraCharacteristics.LENS_FACING_FRONT){
                    continue;
                }

                StreamConfigurationMap map =
                        cameraCharacteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);

                int deviceOrientation = getWindowManager().getDefaultDisplay().getRotation();
                int totalRotation = sensorToDeviceRotation(cameraCharacteristics, deviceOrientation);
                boolean swapRotation = totalRotation == 90 || totalRotation == 270;
                int rotatedWidth = width;
                int rotatedHeigth = height;

                if(swapRotation){
                    rotatedWidth = height;
                    rotatedHeigth = width;
                }

                mPreviewSize = chooseOptimalSize(map.getOutputSizes(SurfaceTexture.class), rotatedWidth, rotatedHeigth);
                //mTextureView.setRotation(90);


                mCameraId = cameraId;
                return;
            }
        } catch (CameraAccessException e) {
            e.printStackTrace();
        }
    }

    private RsSurfaceRenderer createNewRendererForCurrentType(Size outputSize) {
        if (cameraPreviewRenderer == null) {
            cameraPreviewRenderer =
                    new RsCameraPreviewRenderer(rs, outputSize.getWidth(), outputSize.getHeight());
        }
        updateRsRenderer();
        return cameraPreviewRenderer;
    }

    private void startBackgroundThread(){
        mBackgroundHandlerThread = new HandlerThread("Camera2VideoImage");
        mBackgroundHandlerThread.start();
        mBackgroundHandler = new Handler(mBackgroundHandlerThread.getLooper());
    }

    private void stopBackgroundThread(){
        mBackgroundHandlerThread.quitSafely();
        try {
            mBackgroundHandlerThread.join();
            mBackgroundHandlerThread = null;
            mBackgroundHandler = null;
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    private static int sensorToDeviceRotation(CameraCharacteristics c, int deviceOrientation){
        int sensorOrientation = c.get(CameraCharacteristics.SENSOR_ORIENTATION);

        // get device orientation in degrees
        deviceOrientation = ORIENTATIONS.get(deviceOrientation);

        // calculate desired JPEG orientation relative to camera orientation to make
        // the image upright relative to the device orientation
        return (sensorOrientation + deviceOrientation + 360) % 360;

    }

    static class CompareSizesByArea implements Comparator<Size>{
        @Override
        public int compare(Size lhs, Size rhs) {
            // we cast here to ensure the multiplications won't
            // overflow
            return Long.signum((long) lhs.getWidth() * lhs.getHeight() /
                    (long) rhs.getWidth() * rhs.getHeight());
        }
    }

    private static Size chooseOptimalSize(Size[] choices, int width, int height){
        // Collect the supported resolutions that are the least as big as the preview
        // Surface
        List<Size> bigEnough = new ArrayList<Size>();
        for(Size option: choices){
            if(option.getHeight() == option.getWidth() * height/width && option.getWidth() >= width && option.getHeight()>=height){
                bigEnough.add(option);
            }
        }

        // pick the smallest of those, assuming we found any
        if(bigEnough.size() > 0){
            return Collections.min(bigEnough, new CompareSizesByArea());
        }
        else{
            Log.e(TAG, "Couldn't find any suitable preview size");
            return choices[0];
        }
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        if(requestCode == REQUEST_CAMERA_PERMISSION_RESULT){
            if(grantResults[0] != PackageManager.PERMISSION_GRANTED){
                Toast.makeText(
                        getApplicationContext(),
                        "Application will not run without camera service",
                        Toast.LENGTH_SHORT).show();
            }
        }
    }

    /**
     * These are custom kernels that are AoT compiled on the very first launch so we want to make
     * sure that happens outside of a render loop and also not in the UI thread.
     */
    public static void warmUpInBackground(RenderScript rs) {
        new Thread(() -> {
            Log.i(TAG, "RS warmup start...");
            long start = System.currentTimeMillis();
            try {
                ScriptC_color_frame color_frame = new ScriptC_color_frame(rs);
                ScriptC_set_alpha set_alpha = new ScriptC_set_alpha(rs);
                ScriptC_to_grey to_grey = new ScriptC_to_grey(rs);
            } catch (Exception e) {
                e.printStackTrace();
            }
            Log.i(TAG, "RS warmup end, " + (System.currentTimeMillis() - start) + " ms");
        }).start();
    }
}

Now, the following class contains all the methods used to create the Renderscript Allocations.现在,以下类包含用于创建 Renderscript 分配的所有方法。 It also has methods for binding the allocations to the two surfaces we have seen in the startPreview() method of the MainActivity.java class above.它还具有将分配绑定到我们在上面 MainActivity.java 类的 startPreview() 方法中看到的两个表面的方法。 It also starts a render thread which processes incoming frames to yuvInAlloc .它还启动一个渲染线程,该线程将传入的帧处理到yuvInAlloc

public class RsCameraPreviewRenderer
        implements RsSurfaceRenderer, Allocation.OnBufferAvailableListener, Runnable {

    private static final String TAG = "RsCameraPreviewRenderer";

    private final RenderScript rs;
    private final Allocation yuvInAlloc;
    private final Allocation rgbInAlloc;
    private final Allocation rgbOutAlloc;
    private final ScriptIntrinsicYuvToRGB yuvToRGBScript;

    @Nullable
    private final HandlerThread renderThread;

    // all guarded by "this"
    private Handler renderHandler;
    private RsRenderer rsRenderer;

    private int nFramesAvailable;
    private boolean outputSurfaceIsSet;

    /**
     * @param rs
     * @param x
     * @param y
     */
    public RsCameraPreviewRenderer(RenderScript rs, int x, int y) {
        this(rs, new DefaultRsRenderer(), x, y);
    }

    /**
     * @param rs
     * @param rsRenderer
     * @param x
     * @param y
     */
    public RsCameraPreviewRenderer(RenderScript rs, RsRenderer rsRenderer, int x, int y) {
        this(rs, rsRenderer, x, y, null);
    }

    /**
     * @param rs
     * @param rsRenderer
     * @param x
     * @param y
     * @param renderHandler
     */
    public RsCameraPreviewRenderer(RenderScript rs,
                                   RsRenderer rsRenderer,
                                   int x,
                                   int y,
                                   Handler renderHandler) {
        this.rs = rs;
        this.rsRenderer = rsRenderer;

        if (renderHandler == null) {
            this.renderThread = new HandlerThread(TAG);
            this.renderThread.start();
            this.renderHandler = new Handler(renderThread.getLooper());
        } else {
            this.renderThread = null;
            this.renderHandler = renderHandler;
        }

        Log.i(TAG,
                "Setting up RsCameraPreviewRenderer with " + rsRenderer.getName() + " (" + x + "," +
                        y + ")");

        /*
        * Create an YUV allocation that can act as a Surface consumer. This lets us call
        * Allocation#getSurface(), set a Allocation.OnBufferAvailableListener
        * callback to be notified when a frame is ready, and call Allocation#ioReceive() to
        * latch a frame and access its yuv pixel data.
        *
        * The yuvFormat should be the value ImageFormat#YUV_420_888, ImageFormat#NV21 or maybe
        * ImageFormat#YV12.
        *
        * @param rs        RenderScript context
        * @param x         width in pixels
        * @param y         height in pixels
        * @param yuvFormat yuv pixel format
        * @return a YUV Allocation with USAGE_IO_INPUT
        * */
        yuvInAlloc = RsUtil.createYuvIoInputAlloc(rs, x, y, ImageFormat.YUV_420_888);
        yuvInAlloc.setOnBufferAvailableListener(this);


        /**
         * Create a sized RGBA_8888 Allocation to use with scripts.
         *
         * @param rs RenderScript context
         * @param x  width in pixels
         * @param y  height in pixels
         * @return an RGBA_8888 Allocation
         */
        rgbInAlloc = RsUtil.createRgbAlloc(rs, x, y);

        /**
         * Create an RGBA_8888 allocation that can act as a Surface producer. This lets us call
         * Allocation#setSurface(Surface) and call Allocation#ioSend(). If
         * you wanted to read the data from this Allocation, do so before calling ioSend(), because
         * after, the data is undefined.
         *
         * @param rs rs context
         * @param x  width in pixels
         * @param y  height in pixels
         * @return an RGBA_8888 Allocation with USAGE_IO_INPUT
         */
        rgbOutAlloc = RsUtil.createRgbIoOutputAlloc(rs, x, y);

        yuvToRGBScript = ScriptIntrinsicYuvToRGB.create(rs, Element.RGBA_8888(rs));

        yuvToRGBScript.setInput(yuvInAlloc);
    }

    @Override
    @AnyThread
    public synchronized void setRsRenderer(RsRenderer rsRenderer) {
        if (isRunning()) {
            this.rsRenderer = rsRenderer;
        }
    }


    /**
     * Check if this renderer is still running or has been shutdown.
     *
     * @return true if we're running, else false
     */
    @Override
    @AnyThread
    public synchronized boolean isRunning() {
        if (renderHandler == null) {
            Log.w(TAG, "renderer was already shut down");
            return false;
        }
        return true;
    }

    /**
     * Set the output surface to consume the stream of edited camera frames. This is probably
     * from a SurfaceView or TextureView. Please make sure it's valid.
     *
     * @param outputSurface a valid surface to consume a stream of edited frames from the camera
     */
    @AnyThread
    @Override
    public synchronized void setOutputSurface(Surface outputSurface) {
        if (isRunning()) {
            if (!outputSurface.isValid()) {
                throw new IllegalArgumentException("output was invalid");
            }
            rgbOutAlloc.setSurface(outputSurface);
            outputSurfaceIsSet = true;
            Log.d(TAG, "output surface was set");
        }
    }

    /**
     * Get the Surface that the camera will push frames to. This is the Surface from our yuv
     * input allocation. It will recieve a callback when a frame is available from the camera.
     *
     * @return a surface that consumes yuv frames from the camera preview, or null renderer is
     * shutdown
     */
    @AnyThread
    @Override
    public synchronized Surface getInputSurface() {
        return isRunning() ? yuvInAlloc.getSurface() : null;
    }

    /**
     * Callback for when the camera has a new frame. We want to handle this on the render thread
     * specific thread, so we'll increment nFramesAvailable and post a render request.
     */
    @Override
    public synchronized void onBufferAvailable(Allocation a) {
        if (isRunning()) {
            if (!outputSurfaceIsSet) {
                Log.e(TAG, "We are getting frames from the camera but we never set the view " +
                        "surface to render to");
                return;
            }
            nFramesAvailable++;
            renderHandler.post(this);
        }
    }

    /**
     * Render a frame on the render thread. Everything is async except for ioSend() will block
     * until the rendering completes. If we wanted to time it, make sure to log the time after
     * that call.
     */
    @WorkerThread
    @Override
    public void run() {
        RsRenderer renderer;
        int nFrames;
        synchronized (this) {
            if (!isRunning()) {
                return;
            }
            renderer = rsRenderer;
            nFrames = nFramesAvailable;
            nFramesAvailable = 0;

            renderHandler.removeCallbacks(this);
        }

        for (int i = 0; i < nFrames; i++) {

            /*
            * Receive the latest input into the Allocation.
            * This operation is only valid if USAGE_IO_INPUT is set on the Allocation.
            * */
            yuvInAlloc.ioReceive();
        }

        yuvToRGBScript.forEach(rgbInAlloc);

        /*
        * Render an edit to an input Allocation and write it to an output allocation. This must
        * always overwrite the out Allocation. This is called once for a Bitmap, and once per frame
        * for stream rendering.
        * */
        renderer.renderFrame(rs, rgbInAlloc, rgbOutAlloc);

        /*
        * Send a buffer to the output stream. The contents of the Allocation will be undefined after this
        * operation. This operation is only valid if USAGE_IO_OUTPUT is set on the Allocation.
        * */
        rgbOutAlloc.ioSend();
    }


    /**
     * Shut down the renderer when you're finished.
     */
    @Override
    @AnyThread
    public void shutdown() {
        synchronized (this) {
            if (!isRunning()) {
                Log.d(TAG, "requesting shutdown...");
                renderHandler.removeCallbacks(this);
                renderHandler.postAtFrontOfQueue(() -> {
                    Log.i(TAG, "shutting down");
                    synchronized (this) {
                        yuvInAlloc.destroy();
                        rgbInAlloc.destroy();
                        rgbOutAlloc.destroy();
                        yuvToRGBScript.destroy();
                        if (renderThread != null) {
                            renderThread.quitSafely();
                        }
                    }
                });
                renderHandler = null;
            }
        }
    }
}

I scrambled most of the code from the following projects:我从以下项目中打乱了大部分代码:

My problem is that the output is shown as in the following GIF:我的问题是输出显示在以下 GIF 中: 在此处输入图片说明 It is rotated somehow.它以某种方式旋转。 Not how I expected it.不是我预期的那样。 So, why is this happening?那么,为什么会发生这种情况? I could not find an answer.我找不到答案。 Can someone help ?有人可以帮忙吗?

This is because YUV format only output in the default orientation.这是因为 YUV 格式仅以默认方向输出。 The sensorToDeviceRotation() you have only rotates for JPEG outputs.您拥有的sensorToDeviceRotation()仅针对 JPEG 输出进行旋转。 You need to handle the YUV rotation by yourself, either create a function in your Activity to rotate the bitmap or use other script to rotate in RenderScript.您需要自己处理YUV旋转,要么在您的Activity中创建一个函数来旋转位图,要么在RenderScript中使用其他脚本进行旋转。 The later is recommenced as it is faster.后者是重新开始,因为它更快。

I found a sample to use rotate script here RenderScript我在这里找到了一个使用旋转脚本的示例RenderScript

I did not test this, but it should do the work if you research on it a bit more.我没有对此进行测试,但是如果您对其进行更多研究,它应该可以完成工作。 Hope this helps.希望这可以帮助。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM