Skip to content

Commit da4655b

Browse files
Merge pull request #5484 from nextcloud/improving-background-blur-1
Improving background blur
2 parents 64b0186 + f70818f commit da4655b

3 files changed

Lines changed: 89 additions & 126 deletions

File tree

app/src/main/java/com/nextcloud/talk/activities/CallActivity.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
/*
22
* Nextcloud Talk - Android Client
33
*
4+
* SPDX-FileCopyrightText: 2025 Julius Linus <juliuslinus1@gmail.com>
45
* SPDX-FileCopyrightText: 2023 Marcel Hibbe <dev@mhibbe.de>
56
* SPDX-FileCopyrightText: 2022 Tim Krüger <t@timkrueger.me>
67
* SPDX-FileCopyrightText: 2017-2018 Mario Danic <mario@lovelyhq.com>
@@ -504,7 +505,7 @@ class CallActivity : CallBaseActivity() {
504505
val isOn = state == BackgroundBlurOn
505506

506507
val processor = if (isOn) {
507-
BackgroundBlurFrameProcessor(context, frontFacing)
508+
BackgroundBlurFrameProcessor(context)
508509
} else {
509510
null
510511
}

app/src/main/java/com/nextcloud/talk/camera/BackgroundBlurFrameProcessor.kt

Lines changed: 78 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,10 @@ import org.webrtc.JavaI420Buffer
2222
import org.webrtc.VideoFrame
2323
import org.webrtc.VideoProcessor
2424
import org.webrtc.VideoSink
25+
import org.webrtc.YuvHelper
2526
import java.nio.ByteBuffer
2627

27-
class BackgroundBlurFrameProcessor(val context: Context, val isFrontFacing: Boolean) :
28+
class BackgroundBlurFrameProcessor(val context: Context) :
2829
VideoProcessor,
2930
ImageSegmenterHelper.SegmenterListener {
3031

@@ -92,9 +93,13 @@ class BackgroundBlurFrameProcessor(val context: Context, val isFrontFacing: Bool
9293

9394
val finalFrame = blurredMat.toVideoFrame(resultBundle.inferenceTime)
9495

96+
if (finalFrame == null) {
97+
Log.e(TAG, "Frame was null")
98+
}
99+
95100
sink?.onFrame(finalFrame)
96101

97-
finalFrame.release()
102+
finalFrame?.release()
98103
} finally {
99104
frameMat.release()
100105
blurredMat.release()
@@ -132,6 +137,7 @@ class BackgroundBlurFrameProcessor(val context: Context, val isFrontFacing: Bool
132137

133138
try {
134139
// weirdly rotation is 270 degree in portrait and 180 degree in landscape, no idea why
140+
// regardless if this behavior is device dependant, this calculation should correct the orientation
135141
val angle = ROT_360 - videoFrame.rotation.toDouble()
136142
rotationMat = Imgproc.getRotationMatrix2D(
137143
center,
@@ -166,107 +172,90 @@ class BackgroundBlurFrameProcessor(val context: Context, val isFrontFacing: Bool
166172
this.sink = sink
167173
}
168174

169-
private fun Mat.toVideoFrame(time: Long): VideoFrame {
170-
val i420Mat = Mat()
171-
Imgproc.cvtColor(this, i420Mat, Imgproc.COLOR_RGBA2YUV_I420)
172-
173-
// Get the raw bytes from the new I420 Mat
174-
val i420ByteArray = ByteArray((i420Mat.total() * i420Mat.elemSize()).toInt())
175-
i420Mat.get(0, 0, i420ByteArray)
176-
177-
val width = this.width()
178-
val height = this.height()
179-
180-
val yPlaneSize = width * height
181-
val uvPlaneSize = (width / 2) * (height / 2)
182-
183-
val yBuffer = ByteBuffer.allocateDirect(yPlaneSize)
184-
val uBuffer = ByteBuffer.allocateDirect(uvPlaneSize)
185-
val vBuffer = ByteBuffer.allocateDirect(uvPlaneSize)
186-
187-
yBuffer.put(i420ByteArray, 0, yPlaneSize)
188-
uBuffer.put(i420ByteArray, yPlaneSize, uvPlaneSize)
189-
vBuffer.put(i420ByteArray, yPlaneSize + uvPlaneSize, uvPlaneSize)
190-
191-
yBuffer.rewind()
192-
uBuffer.rewind()
193-
vBuffer.rewind()
194-
195-
// Create the I420Buffer using the separate planes
196-
val finalFrameBuffer = JavaI420Buffer.wrap(
197-
width,
198-
height,
199-
yBuffer,
200-
width,
201-
uBuffer,
202-
width / 2,
203-
vBuffer,
204-
width / 2,
205-
null
206-
)
175+
private fun Mat.toVideoFrame(time: Long): VideoFrame? =
176+
runCatching {
177+
val i420Mat = Mat()
178+
Imgproc.cvtColor(this, i420Mat, Imgproc.COLOR_RGBA2YUV_I420)
207179

208-
i420Mat.release()
209-
210-
return VideoFrame(finalFrameBuffer, 0, time)
211-
}
180+
// Get the raw bytes from the new I420 Mat to i420ByteArray
181+
val i420ByteArray = ByteArray((i420Mat.total() * i420Mat.elemSize()).toInt())
182+
i420Mat.get(0, 0, i420ByteArray)
212183

213-
private fun VideoFrame.I420Buffer.toMat(): Mat? =
214-
kotlin.runCatching {
215-
val i420Buffer = this
184+
val width = this.width()
185+
val height = this.height()
216186

217-
val width = i420Buffer.width
218-
val height = i420Buffer.height
219187
val yPlaneSize = width * height
188+
val uvPlaneSize = (width / 2) * (height / 2)
189+
190+
val yBuffer = ByteBuffer.allocateDirect(yPlaneSize)
191+
val uBuffer = ByteBuffer.allocateDirect(uvPlaneSize)
192+
val vBuffer = ByteBuffer.allocateDirect(uvPlaneSize)
193+
194+
yBuffer.put(i420ByteArray, 0, yPlaneSize)
195+
uBuffer.put(i420ByteArray, yPlaneSize, uvPlaneSize)
196+
vBuffer.put(i420ByteArray, yPlaneSize + uvPlaneSize, uvPlaneSize)
197+
198+
yBuffer.rewind()
199+
uBuffer.rewind()
200+
vBuffer.rewind()
201+
202+
// Create the I420Buffer using the separate planes
203+
val finalFrameBuffer = JavaI420Buffer.wrap(
204+
width,
205+
height,
206+
yBuffer,
207+
width,
208+
uBuffer,
209+
width / 2,
210+
vBuffer,
211+
width / 2,
212+
null
213+
)
220214

221-
val nv21Height = (height * NV21_HEIGHT_MULTI).toInt()
222-
val nv21Width = width
223-
val nv21Size = nv21Height * nv21Width
224-
val nv21Data = ByteArray(nv21Size)
225-
226-
val dataY = i420Buffer.dataY
227-
val dataU = i420Buffer.dataU
228-
val dataV = i420Buffer.dataV
215+
i420Mat.release()
229216

230-
val strideY = i420Buffer.strideY // Likely equal to the width, but not always, depending on mem alignment
231-
val strideU = i420Buffer.strideU // U and V have identical dimens and strides
232-
val strideV = i420Buffer.strideV
217+
return VideoFrame(finalFrameBuffer, 0, time)
218+
}.getOrElse { throwable ->
219+
Log.e(TAG, "Error in Mat.toVideoFrame $throwable")
233220

234-
if (strideY == width) {
235-
// Fast path: contiguous data
236-
dataY.get(nv21Data, 0, yPlaneSize)
237-
} else {
238-
// Slow path: row-by-row copy
239-
for (row in 0 until height) {
240-
dataY.position(row * strideY)
241-
dataY.get(nv21Data, row * width, width)
242-
}
243-
}
221+
null
222+
}
244223

245-
val vuPlaneOffset = width * height
246-
for (row in 0 until height / 2) {
247-
for (col in 0 until width / 2) {
248-
// Get U and V values from their respective planes using row/col/stride
249-
val v = dataV[row * strideV + col]
250-
val u = dataU[row * strideU + col]
251-
252-
// Put them into the NV21 buffer (V, then U)
253-
val nv21Index = vuPlaneOffset + (row * width) + (col * 2)
254-
nv21Data[nv21Index] = v
255-
nv21Data[nv21Index + 1] = u
256-
}
257-
}
224+
private fun VideoFrame.I420Buffer.toMat(): Mat? =
225+
runCatching {
226+
val chromaWidth = (width + 1) / 2
227+
val chromaHeight = (height + 1) / 2
228+
val minSize = width * height + chromaWidth * chromaHeight * 2
229+
230+
val nv12ByteBuffer = ByteBuffer.allocateDirect(minSize)
231+
YuvHelper.I420ToNV12(
232+
this.dataY,
233+
this.strideY,
234+
this.dataU,
235+
this.strideU,
236+
this.dataV,
237+
this.strideV,
238+
nv12ByteBuffer,
239+
width,
240+
height
241+
)
258242

259243
val mat = Mat(
260-
nv21Height,
261-
nv21Width,
244+
(height * NV21_HEIGHT_MULTI).toInt(),
245+
width,
262246
CvType.CV_8UC1 // 8 bit unsigned 1 channel
263247
)
264248

265-
mat.put(0, 0, nv21Data)
266-
Imgproc.cvtColor(mat, mat, Imgproc.COLOR_YUV2RGBA_NV21)
249+
mat.put(0, 0, nv12ByteBuffer.array())
267250

268-
i420Buffer.release()
251+
Imgproc.cvtColor(mat, mat, Imgproc.COLOR_YUV2RGBA_NV12)
252+
253+
this.release()
269254

270255
mat
271-
}.getOrNull()
256+
}.getOrElse { throwable ->
257+
Log.e(TAG, "Error in VideoFrame.I420Buffer.toMat $throwable")
258+
259+
null
260+
}
272261
}

app/src/main/java/com/nextcloud/talk/camera/ImageSegmenterHelper.kt

Lines changed: 9 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,7 @@ import org.opencv.core.Mat
2424
import org.opencv.core.Scalar
2525
import java.nio.ByteBuffer
2626

27-
class ImageSegmenterHelper(
28-
var currentDelegate: Int = DELEGATE_CPU,
29-
var runningMode: RunningMode = RunningMode.LIVE_STREAM,
30-
val context: Context,
31-
var imageSegmenterListener: SegmenterListener? = null
32-
) {
27+
class ImageSegmenterHelper(val context: Context, var imageSegmenterListener: SegmenterListener? = null) {
3328

3429
private var imageSegmenter: ImageSegmenter? = null
3530

@@ -53,39 +48,26 @@ class ImageSegmenterHelper(
5348
* @throws IllegalStateException
5449
*/
5550
fun setupImageSegmenter() {
56-
val baseOptionsBuilder = BaseOptions.builder()
57-
when (currentDelegate) {
58-
DELEGATE_CPU -> {
59-
baseOptionsBuilder.setDelegate(Delegate.CPU)
60-
}
61-
62-
DELEGATE_GPU -> {
63-
baseOptionsBuilder.setDelegate(Delegate.GPU)
64-
}
51+
val baseOptionsBuilder = BaseOptions.builder().apply {
52+
setDelegate(Delegate.CPU)
53+
setModelAssetPath(MODEL_SELFIE_SEGMENTER_PATH)
6554
}
6655

67-
baseOptionsBuilder.setModelAssetPath(MODEL_SELFIE_SEGMENTER_PATH)
68-
6956
if (imageSegmenterListener == null) {
7057
throw IllegalStateException("ImageSegmenterListener must be set.")
7158
}
7259

7360
runCatching {
7461
val baseOptions = baseOptionsBuilder.build()
7562
val optionsBuilder = ImageSegmenter.ImageSegmenterOptions.builder()
76-
.setRunningMode(runningMode)
63+
.setRunningMode(RunningMode.LIVE_STREAM)
7764
.setBaseOptions(baseOptions)
7865
.setOutputCategoryMask(true)
7966
.setOutputConfidenceMasks(false)
67+
.setResultListener(this::returnSegmentationResult)
68+
.setErrorListener(this::returnSegmentationHelperError)
8069

81-
if (runningMode == RunningMode.LIVE_STREAM) {
82-
optionsBuilder
83-
.setResultListener(this::returnSegmentationResult)
84-
.setErrorListener(this::returnSegmentationHelperError)
85-
}
86-
87-
val options = optionsBuilder.build()
88-
imageSegmenter = ImageSegmenter.createFromOptions(context, options)
70+
imageSegmenter = ImageSegmenter.createFromOptions(context, optionsBuilder.build())
8971
}.getOrElse { e ->
9072
when (e) {
9173
is IllegalStateException -> {
@@ -114,12 +96,6 @@ class ImageSegmenterHelper(
11496
* @throws IllegalArgumentException
11597
*/
11698
fun segmentLiveStreamFrame(bitmap: Bitmap, videoFrameTimeStamp: Long) {
117-
if (runningMode != RunningMode.LIVE_STREAM) {
118-
throw IllegalArgumentException(
119-
"Attempting to call segmentLiveStreamFrame while not using RunningMode.LIVE_STREAM"
120-
)
121-
}
122-
12399
val mpImage = BitmapImageBuilder(bitmap).build()
124100

125101
imageSegmenter?.segmentAsync(mpImage, videoFrameTimeStamp)
@@ -146,6 +122,7 @@ class ImageSegmenterHelper(
146122
mat.put(0, 0, data)
147123

148124
Core.bitwise_not(mat, mat)
125+
149126
Core.multiply(mat, Scalar(RGB_MAX), mat)
150127

151128
imageSegmenterListener?.onResults(
@@ -167,14 +144,10 @@ class ImageSegmenterHelper(
167144
data class ResultBundle(val mask: Mat, val inferenceTime: Long)
168145

169146
companion object {
170-
const val DELEGATE_CPU = 0
171-
const val DELEGATE_GPU = 1 // DO NOT USE THIS
172147
const val OTHER_ERROR = 0
173148
const val GPU_ERROR = 1
174-
175149
const val MODEL_SELFIE_SEGMENTER_PATH = "selfie_segmenter.tflite"
176150
const val RGB_MAX = 255.0
177-
178151
private const val TAG = "ImageSegmenterHelper"
179152
}
180153

0 commit comments

Comments
 (0)