Skip to content

Commit 9daac7d

Browse files
committed
Second gen face blur
1 parent 8233c19 commit 9daac7d

File tree

2 files changed

+74
-12
lines changed

2 files changed

+74
-12
lines changed

app/src/main/kotlin/com/darkrockstudios/app/securecamera/obfuscation/ObfuscatePhotoContent.kt

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@ import com.darkrockstudios.app.securecamera.camera.SecureImageManager
3434
import com.google.mlkit.vision.common.InputImage
3535
import com.google.mlkit.vision.face.Face
3636
import com.google.mlkit.vision.face.FaceDetection
37+
import com.google.mlkit.vision.face.FaceDetectorOptions
38+
import com.google.mlkit.vision.face.FaceDetectorOptions.LANDMARK_MODE_ALL
39+
import com.google.mlkit.vision.face.FaceDetectorOptions.PERFORMANCE_MODE_ACCURATE
3740
import kotlinx.coroutines.Dispatchers
3841
import kotlinx.coroutines.launch
3942
import net.engawapg.lib.zoomable.rememberZoomState
@@ -54,7 +57,14 @@ fun ObfuscatePhotoContent(
5457
var originalBitmap by remember { mutableStateOf<Bitmap?>(null) }
5558
var isLoading by remember { mutableStateOf(true) }
5659
var isFacesObscured by remember { mutableStateOf(false) }
57-
val detector = remember { FaceDetection.getClient() }
60+
val detector = remember {
61+
FaceDetection.getClient(
62+
FaceDetectorOptions.Builder()
63+
.setLandmarkMode(LANDMARK_MODE_ALL)
64+
.setPerformanceMode(PERFORMANCE_MODE_ACCURATE)
65+
.build()
66+
)
67+
}
5868

5969
var faces by remember { mutableStateOf<List<Face>>(emptyList()) }
6070

@@ -88,15 +98,12 @@ fun ObfuscatePhotoContent(
8898
fun obscureFaces() {
8999
originalBitmap?.let { bitmap ->
90100
if (faces.isNotEmpty() && !isFacesObscured) {
91-
// Create a mutable copy of the bitmap to modify
92101
val mutableBitmap = bitmap.copy(Bitmap.Config.ARGB_8888, true)
93102

94-
// Pixelate each detected face
95103
faces.forEach { face ->
96-
maskFace(mutableBitmap, face.boundingBox, context, MaskMode.PIXELATE, MaskMode.BLUR)
104+
maskFace(mutableBitmap, face, context, MaskMode.PIXELATE)
97105
}
98106

99-
// Update the displayed image
100107
imageBitmap = mutableBitmap.asImageBitmap()
101108
isFacesObscured = true
102109

app/src/main/kotlin/com/darkrockstudios/app/securecamera/obfuscation/ObfuscationTools.kt

Lines changed: 62 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import android.renderscript.Element
77
import android.renderscript.RenderScript
88
import android.renderscript.ScriptIntrinsicBlur
99
import androidx.core.graphics.scale
10+
import com.google.mlkit.vision.face.Face
11+
import com.google.mlkit.vision.face.FaceLandmark
1012
import java.security.SecureRandom
1113

1214
enum class MaskMode {
@@ -39,12 +41,13 @@ fun coerceRectToBitmap(rect: Rect, bitmap: Bitmap): Rect {
3941
return Rect(left, top, right, bottom)
4042
}
4143

42-
fun maskFace(bitmap: Bitmap, rect: Rect, context: Context, vararg modes: MaskMode) {
44+
fun maskFace(bitmap: Bitmap, face: Face, context: Context, vararg modes: MaskMode) {
45+
val rect = face.boundingBox
4346
val safeRect = coerceRectToBitmap(rect, bitmap)
4447
modes.forEach { mode ->
4548
when (mode) {
4649
MaskMode.BLACKOUT -> blackout(bitmap, safeRect)
47-
MaskMode.PIXELATE -> pixelate(bitmap, safeRect)
50+
MaskMode.PIXELATE -> pixelate(bitmap, safeRect, face)
4851
MaskMode.NOISE -> noise(bitmap, safeRect)
4952
MaskMode.BLUR -> blur(bitmap, safeRect, context)
5053
}
@@ -58,19 +61,18 @@ private fun blackout(bitmap: Bitmap, rect: Rect) {
5861
canvas.drawRect(safeRect, paint)
5962
}
6063

61-
private fun pixelate(bitmap: Bitmap, rect: Rect, targetBlockSize: Int = 8, addNoise: Boolean = true) {
64+
private fun pixelate(bitmap: Bitmap, rect: Rect, face: Face, targetBlockSize: Int = 8, addNoise: Boolean = true) {
6265
val faceBitmap = Bitmap.createBitmap(bitmap, rect.left, rect.top, rect.width(), rect.height())
6366

6467
val small = faceBitmap.scale(targetBlockSize, targetBlockSize, false)
65-
val pixelated = small.scale(rect.width(), rect.height(), false)
6668

6769
if (addNoise) {
6870
val random = SecureRandom.getInstanceStrong()
69-
val noiseCanvas = Canvas(pixelated)
71+
val noiseCanvas = Canvas(small)
7072
val paint = Paint()
7173

7274
// Replace random pixels entirely
73-
val noiseProbability = 0.33f
75+
val noiseProbability = 0.25f
7476
for (y in 0 until small.height) {
7577
for (x in 0 until small.width) {
7678
if (random.nextFloat() <= noiseProbability) {
@@ -85,11 +87,64 @@ private fun pixelate(bitmap: Bitmap, rect: Rect, targetBlockSize: Int = 8, addNo
8587
}
8688
}
8789

90+
val leftEye = face.allLandmarks.find { it.landmarkType == FaceLandmark.LEFT_EYE }
91+
val rightEye = face.allLandmarks.find { it.landmarkType == FaceLandmark.RIGHT_EYE }
92+
if (leftEye != null && rightEye != null) {
93+
val leftEyePosition = leftEye.position
94+
val rightEyePosition = rightEye.position
95+
96+
val leftEyeInFace = PointF(
97+
leftEyePosition.x - rect.left,
98+
leftEyePosition.y - rect.top
99+
)
100+
val rightEyeInFace = PointF(
101+
rightEyePosition.x - rect.left,
102+
rightEyePosition.y - rect.top
103+
)
104+
105+
val leftEyeInSmall = PointF(
106+
leftEyeInFace.x * small.width / faceBitmap.width,
107+
leftEyeInFace.y * small.height / faceBitmap.height
108+
)
109+
val rightEyeInSmall = PointF(
110+
rightEyeInFace.x * small.width / faceBitmap.width,
111+
rightEyeInFace.y * small.height / faceBitmap.height
112+
)
113+
114+
// Blackout the eyes
115+
val paint = Paint().apply { color = Color.BLACK }
116+
val noiseCanvas = Canvas(small)
117+
118+
if (leftEyeInSmall.x >= 0 && leftEyeInSmall.x < small.width &&
119+
leftEyeInSmall.y >= 0 && leftEyeInSmall.y < small.height &&
120+
rightEyeInSmall.x >= 0 && rightEyeInSmall.x < small.width &&
121+
rightEyeInSmall.y >= 0 && rightEyeInSmall.y < small.height
122+
) {
123+
noiseCanvas.drawLine(
124+
0f, leftEyeInSmall.y, 7f, rightEyeInSmall.y, paint,
125+
)
126+
} else {
127+
if (leftEyeInSmall.x >= 0 && leftEyeInSmall.x < small.width &&
128+
leftEyeInSmall.y >= 0 && leftEyeInSmall.y < small.height
129+
) {
130+
noiseCanvas.drawPoint(leftEyeInSmall.x, leftEyeInSmall.y, paint)
131+
}
132+
133+
if (rightEyeInSmall.x >= 0 && rightEyeInSmall.x < small.width &&
134+
rightEyeInSmall.y >= 0 && rightEyeInSmall.y < small.height
135+
) {
136+
noiseCanvas.drawPoint(rightEyeInSmall.x, rightEyeInSmall.y, paint)
137+
}
138+
}
139+
}
140+
141+
val pixelated = small.scale(rect.width(), rect.height(), false)
142+
88143
val canvas = Canvas(bitmap)
89144
canvas.drawBitmap(pixelated, rect.left.toFloat(), rect.top.toFloat(), null)
90145
}
91146

92-
private fun blur(bitmap: Bitmap, rect: Rect, context: Context, radius: Float = 25f, rounds: Int = 3) {
147+
private fun blur(bitmap: Bitmap, rect: Rect, context: Context, radius: Float = 25f, rounds: Int = 10) {
93148
val safeRect = coerceRectToBitmap(rect, bitmap)
94149
val faceBitmap = Bitmap.createBitmap(bitmap, safeRect.left, safeRect.top, safeRect.width(), safeRect.height())
95150

0 commit comments

Comments
 (0)