Vlad Provalionok
Committed by GitHub

Improved VirtualBackgroundVideoProcessor and VirtualBackgroundTransformer (#731)

* Made blur radius dynamically changeable

Added lazy initialization of Segmenter

Removed blurring in shader if radius less or equals 0

Configured in sample Target aspect ratio for ImageAnalysis

* make blurRadius a constructor variable

Also keeps the original API to avoid any changes.

* Remove unneeded updateBlurRadius method

* Fix compile

* changeset

* fix compile

---------

Co-authored-by: davidliu <davidliu@deviange.net>
  1 +---
  2 +"client-sdk-android": patch
  3 +---
  4 +
  5 +Make blurRadius in the VirtualBackgroundTransformer variable to allow for dynamically changing the value.
@@ -51,6 +51,13 @@ class MainActivity : AppCompatActivity() { @@ -51,6 +51,13 @@ class MainActivity : AppCompatActivity() {
51 track?.addRenderer(renderer) 51 track?.addRenderer(renderer)
52 } 52 }
53 53
  54 + findViewById<Button>(R.id.buttonIncreaseBlur).setOnClickListener {
  55 + viewModel.increaseBlur()
  56 + }
  57 + findViewById<Button>(R.id.buttonDecreaseBlur).setOnClickListener {
  58 + viewModel.decreaseBlur()
  59 + }
  60 +
54 requestNeededPermissions { 61 requestNeededPermissions {
55 viewModel.startCapture() 62 viewModel.startCapture()
56 } 63 }
@@ -22,6 +22,8 @@ import androidx.annotation.OptIn @@ -22,6 +22,8 @@ import androidx.annotation.OptIn
22 import androidx.appcompat.content.res.AppCompatResources 22 import androidx.appcompat.content.res.AppCompatResources
23 import androidx.camera.camera2.interop.ExperimentalCamera2Interop 23 import androidx.camera.camera2.interop.ExperimentalCamera2Interop
24 import androidx.camera.core.ImageAnalysis 24 import androidx.camera.core.ImageAnalysis
  25 +import androidx.camera.core.resolutionselector.AspectRatioStrategy
  26 +import androidx.camera.core.resolutionselector.ResolutionSelector
25 import androidx.lifecycle.AndroidViewModel 27 import androidx.lifecycle.AndroidViewModel
26 import androidx.lifecycle.MutableLiveData 28 import androidx.lifecycle.MutableLiveData
27 import androidx.lifecycle.ProcessLifecycleOwner 29 import androidx.lifecycle.ProcessLifecycleOwner
@@ -52,15 +54,23 @@ class MainViewModel(application: Application) : AndroidViewModel(application) { @@ -52,15 +54,23 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
52 eglBase = eglBase, 54 eglBase = eglBase,
53 ), 55 ),
54 ) 56 )
55 -  
56 - val processor = VirtualBackgroundVideoProcessor(eglBase, Dispatchers.IO).apply { 57 + private var blur = 16f
  58 + private val processor = VirtualBackgroundVideoProcessor(eglBase, Dispatchers.IO, initialBlurRadius = blur).apply {
57 val drawable = AppCompatResources.getDrawable(application, R.drawable.background) as BitmapDrawable 59 val drawable = AppCompatResources.getDrawable(application, R.drawable.background) as BitmapDrawable
58 backgroundImage = drawable.bitmap 60 backgroundImage = drawable.bitmap
59 } 61 }
60 62
61 private var cameraProvider: CameraCapturerUtils.CameraProvider? = null 63 private var cameraProvider: CameraCapturerUtils.CameraProvider? = null
62 64
63 - private var imageAnalysis = ImageAnalysis.Builder().build() 65 + private var imageAnalysis = ImageAnalysis.Builder()
  66 + .setResolutionSelector(
  67 + ResolutionSelector.Builder()
  68 + // LocalVideoTrack has default aspect ratio 16:9 VideoPreset169.H720
  69 + // ImageAnalysis of CameraX has default aspect ratio 4:3
  70 + .setAspectRatioStrategy(AspectRatioStrategy.RATIO_16_9_FALLBACK_AUTO_STRATEGY)
  71 + .build(),
  72 + )
  73 + .build()
64 .apply { setAnalyzer(Dispatchers.IO.asExecutor(), processor.imageAnalyzer) } 74 .apply { setAnalyzer(Dispatchers.IO.asExecutor(), processor.imageAnalyzer) }
65 75
66 init { 76 init {
@@ -99,4 +109,14 @@ class MainViewModel(application: Application) : AndroidViewModel(application) { @@ -99,4 +109,14 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
99 processor.enabled = newState 109 processor.enabled = newState
100 return newState 110 return newState
101 } 111 }
  112 +
  113 + fun decreaseBlur() {
  114 + blur -= 5
  115 + processor.updateBlurRadius(blur)
  116 + }
  117 +
  118 + fun increaseBlur() {
  119 + blur += 5
  120 + processor.updateBlurRadius(blur)
  121 + }
102 } 122 }
@@ -17,4 +17,21 @@ @@ -17,4 +17,21 @@
17 android:layout_margin="10dp" 17 android:layout_margin="10dp"
18 android:text="Disable" /> 18 android:text="Disable" />
19 19
  20 + <Button
  21 + android:id="@+id/buttonIncreaseBlur"
  22 + android:layout_width="wrap_content"
  23 + android:layout_height="wrap_content"
  24 + android:layout_gravity="end"
  25 + android:layout_margin="10dp"
  26 + android:text="Blur more" />
  27 +
  28 + <Button
  29 + android:id="@+id/buttonDecreaseBlur"
  30 + android:layout_width="wrap_content"
  31 + android:layout_gravity="end"
  32 + android:layout_height="wrap_content"
  33 + android:layout_marginTop="64dp"
  34 + android:layout_marginEnd="10dp"
  35 + android:text="Blur less" />
  36 +
20 </FrameLayout> 37 </FrameLayout>
@@ -37,8 +37,8 @@ import java.nio.ByteBuffer @@ -37,8 +37,8 @@ import java.nio.ByteBuffer
37 * Blurs the background of the camera video stream. 37 * Blurs the background of the camera video stream.
38 */ 38 */
39 class VirtualBackgroundTransformer( 39 class VirtualBackgroundTransformer(
40 - val blurRadius: Float = 16f,  
41 - val downSampleFactor: Int = 2, 40 + var blurRadius: Float = 16f,
  41 + var downSampleFactor: Int = 2,
42 ) : RendererCommon.GlDrawer { 42 ) : RendererCommon.GlDrawer {
43 43
44 data class MaskHolder(val width: Int, val height: Int, val buffer: ByteBuffer) 44 data class MaskHolder(val width: Int, val height: Int, val buffer: ByteBuffer)
@@ -54,7 +54,7 @@ class VirtualBackgroundTransformer( @@ -54,7 +54,7 @@ class VirtualBackgroundTransformer(
54 54
55 private lateinit var downSampler: ResamplerShader 55 private lateinit var downSampler: ResamplerShader
56 56
57 - var backgroundImageStateLock = Any() 57 + private var backgroundImageStateLock = Any()
58 var backgroundImage: Bitmap? = null 58 var backgroundImage: Bitmap? = null
59 set(value) { 59 set(value) {
60 if (value == field) { 60 if (value == field) {
@@ -66,7 +66,7 @@ class VirtualBackgroundTransformer( @@ -66,7 +66,7 @@ class VirtualBackgroundTransformer(
66 backgroundImageNeedsUploading = true 66 backgroundImageNeedsUploading = true
67 } 67 }
68 } 68 }
69 - var backgroundImageNeedsUploading = false 69 + private var backgroundImageNeedsUploading = false
70 70
71 // For double buffering the final mask 71 // For double buffering the final mask
72 private var readMaskIndex = 0 // Index for renderFrame to read from 72 private var readMaskIndex = 0 // Index for renderFrame to read from
@@ -250,6 +250,7 @@ class VirtualBackgroundTransformer( @@ -250,6 +250,7 @@ class VirtualBackgroundTransformer(
250 } 250 }
251 251
252 override fun release() { 252 override fun release() {
  253 + if (!initialized) return
253 compositeShader.release() 254 compositeShader.release()
254 blurShader.release() 255 blurShader.release()
255 boxBlurShader.release() 256 boxBlurShader.release()
@@ -49,17 +49,27 @@ import java.util.concurrent.Semaphore @@ -49,17 +49,27 @@ import java.util.concurrent.Semaphore
49 * By default, blurs the background of the video stream. 49 * By default, blurs the background of the video stream.
50 * Setting [backgroundImage] will use the provided image instead. 50 * Setting [backgroundImage] will use the provided image instead.
51 */ 51 */
52 -class VirtualBackgroundVideoProcessor(private val eglBase: EglBase, dispatcher: CoroutineDispatcher = Dispatchers.Default) : NoDropVideoProcessor() { 52 +class VirtualBackgroundVideoProcessor(
  53 + private val eglBase: EglBase,
  54 + dispatcher: CoroutineDispatcher = Dispatchers.Default,
  55 + initialBlurRadius: Float = 16f,
  56 +) : NoDropVideoProcessor() {
53 57
54 private var targetSink: VideoSink? = null 58 private var targetSink: VideoSink? = null
55 - private val segmenter: Segmenter 59 + private val segmenter: Segmenter by lazy {
  60 + val options =
  61 + SelfieSegmenterOptions.Builder()
  62 + .setDetectorMode(SelfieSegmenterOptions.STREAM_MODE)
  63 + .build()
  64 + Segmentation.getClient(options)
  65 + }
56 66
57 private var lastRotation = 0 67 private var lastRotation = 0
58 private var lastWidth = 0 68 private var lastWidth = 0
59 private var lastHeight = 0 69 private var lastHeight = 0
60 private val surfaceTextureHelper = SurfaceTextureHelper.create("BitmapToYUV", eglBase.eglBaseContext) 70 private val surfaceTextureHelper = SurfaceTextureHelper.create("BitmapToYUV", eglBase.eglBaseContext)
61 private val surface = Surface(surfaceTextureHelper.surfaceTexture) 71 private val surface = Surface(surfaceTextureHelper.surfaceTexture)
62 - private val backgroundTransformer = VirtualBackgroundTransformer() 72 + private val backgroundTransformer = VirtualBackgroundTransformer(blurRadius = initialBlurRadius)
63 private val eglRenderer = EglRenderer(VirtualBackgroundVideoProcessor::class.java.simpleName) 73 private val eglRenderer = EglRenderer(VirtualBackgroundVideoProcessor::class.java.simpleName)
64 .apply { 74 .apply {
65 init(eglBase.eglBaseContext, EglBase.CONFIG_PLAIN, backgroundTransformer) 75 init(eglBase.eglBaseContext, EglBase.CONFIG_PLAIN, backgroundTransformer)
@@ -88,12 +98,6 @@ class VirtualBackgroundVideoProcessor(private val eglBase: EglBase, dispatcher: @@ -88,12 +98,6 @@ class VirtualBackgroundVideoProcessor(private val eglBase: EglBase, dispatcher:
88 private var backgroundImageNeedsUpdating = false 98 private var backgroundImageNeedsUpdating = false
89 99
90 init { 100 init {
91 - val options =  
92 - SelfieSegmenterOptions.Builder()  
93 - .setDetectorMode(SelfieSegmenterOptions.STREAM_MODE)  
94 - .build()  
95 - segmenter = Segmentation.getClient(options)  
96 -  
97 // Funnel processing into a single flow that won't buffer, 101 // Funnel processing into a single flow that won't buffer,
98 // since processing may be slower than video capture. 102 // since processing may be slower than video capture.
99 scope.launch { 103 scope.launch {
@@ -167,7 +171,11 @@ class VirtualBackgroundVideoProcessor(private val eglBase: EglBase, dispatcher: @@ -167,7 +171,11 @@ class VirtualBackgroundVideoProcessor(private val eglBase: EglBase, dispatcher:
167 } 171 }
168 } 172 }
169 173
170 - fun processFrame(frame: VideoFrame) { 174 + override fun setSink(sink: VideoSink?) {
  175 + targetSink = sink
  176 + }
  177 +
  178 + private fun processFrame(frame: VideoFrame) {
171 if (lastRotation != frame.rotation) { 179 if (lastRotation != frame.rotation) {
172 lastRotation = frame.rotation 180 lastRotation = frame.rotation
173 backgroundImageNeedsUpdating = true 181 backgroundImageNeedsUpdating = true
@@ -227,8 +235,8 @@ class VirtualBackgroundVideoProcessor(private val eglBase: EglBase, dispatcher: @@ -227,8 +235,8 @@ class VirtualBackgroundVideoProcessor(private val eglBase: EglBase, dispatcher:
227 } 235 }
228 } 236 }
229 237
230 - override fun setSink(sink: VideoSink?) {  
231 - targetSink = sink 238 + fun updateBlurRadius(blurRadius: Float) {
  239 + backgroundTransformer.blurRadius = blurRadius
232 } 240 }
233 241
234 fun dispose() { 242 fun dispose() {
@@ -34,6 +34,11 @@ uniform float u_radius; @@ -34,6 +34,11 @@ uniform float u_radius;
34 out vec4 fragColor; 34 out vec4 fragColor;
35 35
36 void main() { 36 void main() {
  37 + if (u_radius <= 0.0) {
  38 + fragColor = texture(u_texture, texCoords);
  39 + return;
  40 + }
  41 +
37 float sigma = u_radius; 42 float sigma = u_radius;
38 float twoSigmaSq = 2.0 * sigma * sigma; 43 float twoSigmaSq = 2.0 * sigma * sigma;
39 float totalWeight = 0.0; 44 float totalWeight = 0.0;