12

What I've tried so far:


Convert every frame into bitmap, blur it with library and put it into ImageView which is in front of camera preview. Obviously was too slow - something like 1 fps.


Then I started to use RenderScript which blurs every frame and result of processing should be placed in TextureView which is cover camera preview.

Essential peaces of code of that approach:

BlurFilter

ScriptIntrinsicBlur.create(rs, Element.RGBA_8888(rs)).apply {
    setRadius(BLUR_RADIUS)
}
private val yuvToRgb = ScriptIntrinsicYuvToRGB.create(rs, Element.RGBA_8888(rs))
private var surface: SurfaceTexture? = null

private fun setupSurface() {
    if (surface != null) {
        aBlurOut?.surface = Surface(surface)
    }
}

fun reset(width: Int, height: Int) {
    aBlurOut?.destroy()

    this.width = width
    this.height = height

    val tbConvIn = Type.Builder(rs, Element.U8(rs))
            .setX(width)
            .setY(height)
            .setYuvFormat(android.graphics.ImageFormat.NV21)
    aConvIn = Allocation.createTyped(rs, tbConvIn.create(), Allocation.USAGE_SCRIPT)

    val tbConvOut = Type.Builder(rs, Element.RGBA_8888(rs))
            .setX(width)
            .setY(height)
    aConvOut = Allocation.createTyped(rs, tbConvOut.create(), Allocation.USAGE_SCRIPT)

    val tbBlurOut = Type.Builder(rs, Element.RGBA_8888(rs))
            .setX(width)
            .setY(height)
    aBlurOut = Allocation.createTyped(rs, tbBlurOut.create(),
            Allocation.USAGE_SCRIPT or Allocation.USAGE_IO_OUTPUT)

    setupSurface()
}

fun execute(yuv: ByteArray) {
    if (surface != null) {
        //YUV -> RGB
        aConvIn!!.copyFrom(yuv)
        yuvToRgb.setInput(aConvIn)
        yuvToRgb.forEach(aConvOut)
        //RGB -> BLURED RGB
        blurRc.setInput(aConvOut)
        blurRc.forEach(aBlurOut)
        aBlurOut!!.ioSend()
    }
}

MainActivity

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

    initQrScanner()
}

override fun onStart() {
    super.onStart()
    fotoapparat.start()
}

override fun onStop() {
    fotoapparat.stop()
    super.onStop()
}

private fun initQrScanner() {
    val filter = BlurFilter(RenderScript.create(this))
    tvWholeOverlay.surfaceTextureListener = filter

    fotoapparat = Fotoapparat
            .with(this)
            .into(cvQrScanner)
            .frameProcessor({
                if (it.size.width != filter.width || it.size.height != filter.height) {
                    filter.reset(it.size.width, it.size.height)
                }
                filter.execute(it.image)
            })
            .build()
}

activity_main.xml

<android.support.constraint.ConstraintLayout 
xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.blur.andrey.blurtest.MainActivity">

    <io.fotoapparat.view.CameraView
        android:id="@+id/cvQrScanner"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

    <TextureView
        android:id="@+id/tvWholeOverlay"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</android.support.constraint.ConstraintLayout>

And unfortunately it is still to slow - 3-4 FPS. Also blurring overlay is rotated, but it's another problem.


I've created test project on Github where you can quickly reproduce problem and check how it is possible to optimise. Looking forward for your ideas.


UPD I was able to improve performance with scaling down input date before blurring. I pushed those changes to test repo. Now I have really good (15-20 FPS) performance even on low end devices, but with low res (HD for instance), and not good enough on FHD and UHD ((8-12 FPS).

Divers
  • 9,531
  • 7
  • 45
  • 88
  • Have you tried profiling the app? – Xiao Oct 23 '17 at 23:34
  • You mean profiler of Android Studio? – Divers Oct 24 '17 at 07:17
  • Is there a particular reason why you are dealing with a yuv ```byte[]``` with ```ImageFormat.NV_21``` and not consuming the preview frame directly from the camera? Camera 2 preview frames can be consumed directly into a yuv Allocation with ```USAGE_IO_INPUT``` and ```ImageFormat.YUV_420_888```. You would have to switch to native rs, but it should be fine since you have minApi 19. If that was ok, then I think I have a solution. – lydia_schiff Oct 26 '17 at 20:22
  • @lydia_schiff reason is that I don't wan't to have work with camera hell directly, when there is so good library which solves 99.9% of problems for you. Plus there is another problem with camera2 - https://stackoverflow.com/questions/43687624/preview-callback-in-camera2-is-significantly-slower-than-in-camera1. But I think I can modify source code of `fotoapparat` and expose camera2 if it works well, so please provide your solution – Divers Oct 26 '17 at 21:15

2 Answers2

11

I think the best approach you can take is to replace CameraView with a TextureView and use it as a camera preview. You can easily find examples how to do it here https://developer.android.com/reference/android/view/TextureView.html, and here https://github.com/dalinaum/TextureViewDemo/blob/master/src/kr/gdg/android/textureview/CameraActivity.java.

Next step is to use a blurring shader on your TextureView. You can find an example here: https://github.com/nekocode/blur-using-textureview/blob/master/app/src/main/java/cn/nekocode/blurringview/sample/BlurFilter.java

You can use this shader or find some more nice-looking one.

In case if you want a ready to use solution, you can take a look at this library: https://github.com/krazykira/VidEffects

It allows you to apply shaders to a Video View, so again, you can use a blurring shader to achieve a desired effect.

Example for this library: https://stackoverflow.com/a/38125734/3286059

EDIT

So I forked a great library from Nekocode and added a working blur filter. Please see my latest commit. https://github.com/Dimezis/CameraFilter

As I said, TextureView + GL shaders. To enable blur shader, click on menu in top right corner and select the corresponding option.

If you need faster/better/simpler blur shader, you can search for one on https://www.shadertoy.com

On some devices you might camera preview flipped, but that's another problem to solve.

Dimezis
  • 1,541
  • 11
  • 24
  • Thanks for your answer. I saw all those links before. Did you try anything of it? Because first one is about blurring static image, second about blurring video stream. – Divers Oct 23 '17 at 10:07
  • @Divers I did try the first one, particularly this example https://github.com/nekocode/blur-using-textureview/blob/master/app/src/main/java/cn/nekocode/blurringview/sample/BlurFilter.java. It actually blurs not a static image, but a View Hierarchy that is constantly updated (on scroll, for example). And it works perfectly fine, and for sure would work with camera stream, as in first TextureView example. The reason it works fast enough, is that you don't allocate new bitmaps each time, also blurring is done in a background thread with OpenGL shader, which is pretty fast itself. – Dimezis Oct 23 '17 at 10:38
  • @Divers speaking about the second library (VideoEffects) it's possible to make it render a camera preview as well - https://github.com/google/grafika/blob/master/src/com/android/grafika/CameraCaptureActivity.java – Dimezis Oct 23 '17 at 10:45
  • Can you maybe create PR to my github repo with sample, in order to make it more specific to current case? And btw, if you will look into sample which I've provided, I don't allocate bitmap each time, all done with RenderScript. – Divers Oct 23 '17 at 10:52
  • @Divers ok, yes, in your case it does not allocate bitmaps, but it still wastes a lot of time on YUV -> RGB conversion, values copying and blurring itself. Shader approach would be much faster. Sorry, but PR is too much, I don't have that much time. TextureView camera preview is pretty easy to implement, then you'll just need to integrate GL shader processing based on this example https://github.com/nekocode/blur-using-textureview/blob/master/app/src/main/java/cn/nekocode/blurringview/sample/BlurringTextureView.java – Dimezis Oct 23 '17 at 12:47
  • @Divers I can tell you for sure, that `RenderScript` performance won't be enough for blurring this big image in 60fps. I'm using similar approach in https://github.com/Dimezis/BlurView my library, but I'm downscaling input image by 8 times before processing. In your case you can't do it really efficiently, as you receive just a ByteArray as an input – Dimezis Oct 23 '17 at 12:55
  • Of course I can and I did it already, which gives me good performance on devices with resolution HD, but for some reason not good enough on devices with FHD and bigger res. Anyway thank you for googling for me. – Divers Oct 23 '17 at 12:59
  • @Divers Well that's natural, as FHD preview has much more pixels to process – Dimezis Oct 23 '17 at 13:01
  • As per my measurements problem not in processing, but in drawing already processed result. Processing of 1 frame takes ~10ms which more then enough. – Divers Oct 23 '17 at 13:02
  • @Divers I can't agree with that, I checked out the project, logged `fun execute(yuv: ByteArray)` execution, and it takes more than 200-300ms on Nexus 5X – Dimezis Oct 23 '17 at 13:15
  • Please pull my latest changes. I just pushed it. I was talking about case when I downscale input before applying blur. I will update my question as well. – Divers Oct 23 '17 at 13:28
  • @Divers now it's much better, but still takes 18-35ms for me. Plus some expenses on drawing time, thread switching and `camera.addCallbackBuffer` call inside of this library and we get what we see. – Dimezis Oct 23 '17 at 13:54
  • @Divers I edited my answer and provided a ready to use solution https://github.com/Dimezis/CameraFilter, please check this out – Dimezis Oct 24 '17 at 09:30
  • Thank you very much! But is it fast for you? Even with this small blurring radius (I need stronger blur) on some devices it is very laggy. It feels that in order to get good performance downscaling must be done anyway, which means that second `SurfaceView` must be involved (not sure about that) – Divers Oct 24 '17 at 10:08
  • @Divers I tested it only on Nexus 5X, Pixel and emulators, works great for me. No visible fps drop compared to original preview. But yeah, these are pretty high end devices. If you are sure that this shader slows down rendering, you can search for `fast blur` shader implementations, and integrate it to this test app, taking into account uniforms names. – Dimezis Oct 24 '17 at 10:36
  • I tried on Nexus 6p and htc a9, especially super slow when I did radios = 50. Thank you, I will play with that and hopefully will find good solution. – Divers Oct 24 '17 at 10:44
  • @Divers Does radius=50 mean that the blur radius is 50 pixels? That seems like an enormous amount of work to do per pixel. ScriptIntrinsicBlur only goes up to 25, and the performance degrades noticeably as the radius increases. – lydia_schiff Nov 02 '17 at 21:30
  • 1
    @lydia_schiff it does. That's an unbearable task for mobile GPU, as it would require extreme amount of texture fetches. What can be done instead - is downscaling the texture, and leveraging linear interpolation: http://rastergrid.com/blog/2010/09/efficient-gaussian-blur-with-linear-sampling/ – Dimezis Nov 02 '17 at 21:49
2

I have achieved nice live camera blur effect with RenderScript and previewing the output result to the ImageView.

  1. First I created a BlurBuilder class which uses renderscript

     public class BlurBuilder {
     private static final float BITMAP_SCALE = 4f;
     private static final float BLUR_RADIUS = 25f; // 0 - 25
    
     public static Bitmap blur(Context context, Bitmap image) {
         int width = Math.round(image.getWidth() * BITMAP_SCALE);
         int height = Math.round(image.getHeight() * BITMAP_SCALE);
         Bitmap inputBitmap = Bitmap.createScaledBitmap(image, width, height, false);
         Bitmap outputBitmap = Bitmap.createBitmap(inputBitmap);
         RenderScript rs = RenderScript.create(context);
         ScriptIntrinsicBlur intrinsicBlur = ScriptIntrinsicBlur.create(rs, Element.U8_4(rs));
         Allocation tmpIn = Allocation.createFromBitmap(rs, inputBitmap);
         Allocation tmpOut = Allocation.createFromBitmap(rs, outputBitmap);
         intrinsicBlur.setRadius(BLUR_RADIUS);
         intrinsicBlur.setInput(tmpIn);
         intrinsicBlur.forEach(tmpOut);
         tmpOut.copyTo(outputBitmap);
         return outputBitmap;
     }}
    
  2. I used TextureView to preview the camera preview

  3. Add a ImageView over the TextureView

  4. Add a setSurfaceTextureListener and I added a little tweak

@Override
    public void onSurfaceTextureUpdated(SurfaceTexture surface) {
        Bitmap bm = BlurBuilder.blur(getApplicationContext(),textureView.getBitmap());
        imageView.setImageBitmap(bm);
    }

PS: I used Opengles in TextureView.

My Result: https://vimeo.com/517761912 Device Name: Samsung j7 prime OS Version: Android 8.1 Camera: 13mp 30fps (Device capacity)

sudayn
  • 1,169
  • 11
  • 14
  • 1
    Mind to show a video how it looks like? Settings bitmap on every frame looks terribly slow tbh – Divers Feb 27 '21 at 07:39
  • @Divers I have edited the answer and provided the link. Not that great as iOS UIVisualEffect View – sudayn Feb 28 '21 at 13:05