Skip to content
Draft
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
fcb312a
Initial mediapipe segmentation
Jan 12, 2025
772a10f
Add something that currently just blurs the whole image
Jan 12, 2025
5b582fa
Working portrait mode
Jan 12, 2025
d18a13f
Use a faster blurring algorithm & refactor to apply mask at blur time
Jan 13, 2025
cdc4085
Add coroutines lib
Jan 20, 2025
7cf8cc4
Run blurring in a coroutine
Jan 20, 2025
059cfab
Fix applying the mask when blurring
Jan 20, 2025
011f2d2
Add comment for mask blurring
Jan 20, 2025
50f0324
Add portrait mode setting
Jan 26, 2025
6c58f8e
Disable portrait mode when setting off
Jan 26, 2025
d26e1e2
Clean up the SegmenterHelper to remove stream specific code
Jan 26, 2025
780c7c9
Remove image segmenter listener
Jan 26, 2025
9e92755
Ensure rotation is correct on save
Jan 26, 2025
a1d598f
unused file
Jan 26, 2025
289799b
Fix linting errors
Jan 26, 2025
54c6cd3
Merge branch 'main' into portrait-mediapipe
Jan 26, 2025
2d4ac18
Fix build errors
Jan 26, 2025
a75f673
Use proper library management
Jan 27, 2025
98ac693
Add some proguard rules
Jan 27, 2025
196dc66
Remove more dead code
Jan 27, 2025
8391197
Use compatable coroutines version
Jan 27, 2025
a2e551f
Use actual latest mediapipe task version
Jan 27, 2025
59b8191
Another proguard rule
Jan 27, 2025
ca845fa
Use actual latest mediapipe task version
Feb 2, 2025
494e349
Remove ImageSegmenterHelper.kt
Feb 3, 2025
f7e4d8f
Remove specific portrait setting with aim of using photo modes instead
Feb 12, 2025
3b580aa
Add portrait photo mode and use in photo mode plugin
Feb 12, 2025
72d86c0
Remove imports
Feb 12, 2025
d7c3630
Move to a mode for toggling portrait
Feb 12, 2025
35179f4
Move back to CPU to make mediapipe work
Feb 12, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,9 @@ dependencies {

// ML Kit for Barcode/QR scanning
implementation(libs.barcode.scanning)

//Mediapipe for segmentation (portrait)
implementation("com.google.mediapipe:tasks-vision:latest.release")
}


Expand Down
Binary file added app/src/main/assets/selfie_segmenter.tflite
Binary file not shown.
27 changes: 24 additions & 3 deletions app/src/main/java/co/stonephone/stonecamera/StoneCameraApp.kt
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,35 @@ import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.FlipCameraAndroid
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight
Expand All @@ -30,6 +49,7 @@ import co.stonephone.stonecamera.plugins.AspectRatioPlugin
import co.stonephone.stonecamera.plugins.FlashPlugin
import co.stonephone.stonecamera.plugins.FocusBasePlugin
import co.stonephone.stonecamera.plugins.PinchToZoomPlugin
import co.stonephone.stonecamera.plugins.PortraitModePlugin
import co.stonephone.stonecamera.plugins.QRScannerPlugin
import co.stonephone.stonecamera.plugins.SettingLocation
import co.stonephone.stonecamera.plugins.ShutterFlashPlugin
Expand All @@ -47,6 +67,7 @@ val shootModes = arrayOf("Photo", "Video")
// Order here is important, they are loaded and initialised in the order they are listed
// ZoomBar depends on ZoomBase, etc.
val PLUGINS = listOf(
PortraitModePlugin(),
QRScannerPlugin(),
ZoomBasePlugin(),
ZoomBarPlugin(),
Expand Down
189 changes: 189 additions & 0 deletions app/src/main/java/co/stonephone/stonecamera/plugins/Portrait.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
package co.stonephone.stonecamera.plugins

import android.content.ContentResolver
import android.content.ContentValues
import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.Canvas
import android.graphics.Color
import android.net.Uri
import android.os.Environment
import android.os.SystemClock
import android.provider.MediaStore
import android.renderscript.Allocation
import android.renderscript.Element
import android.renderscript.RenderScript
import android.renderscript.ScriptIntrinsicBlur
import androidx.camera.core.ImageCapture
import co.stonephone.stonecamera.MyApplication
import co.stonephone.stonecamera.StoneCameraViewModel
import co.stonephone.stonecamera.utils.ImageSegmenterHelper
import com.google.mediapipe.framework.image.BitmapImageBuilder
import com.google.mediapipe.framework.image.ByteBufferExtractor
import com.google.mediapipe.tasks.vision.core.RunningMode
import com.google.mediapipe.tasks.vision.imagesegmenter.ImageSegmenterResult
import java.io.File
import java.io.FileOutputStream
import java.io.OutputStream
import java.nio.ByteBuffer


class PortraitModePlugin : IPlugin, ImageSegmenterHelper.SegmenterListener {
override val id: String = "portraitModePlugin"
override val name: String = "Portrait Mode"

private lateinit var imageSegmenterHelper: ImageSegmenterHelper

// Note the various models available here: /home/izaak/Downloads/selfie_segmenter.tflite.

override fun initialize(viewModel: StoneCameraViewModel) {
imageSegmenterHelper = ImageSegmenterHelper(
context = MyApplication.getAppContext(),
runningMode = RunningMode.IMAGE,
currentModel = ImageSegmenterHelper.MODEL_SELFIE_SEGMENTER,
currentDelegate = ImageSegmenterHelper.DELEGATE_CPU,
imageSegmenterListener = this
);

imageSegmenterHelper.setupImageSegmenter();
}

//TODO fix image rotation being incorrect after processing
override fun onImageSaved(
stoneCameraViewModel: StoneCameraViewModel,
outputFileResults: ImageCapture.OutputFileResults
) {
val contentResolver: ContentResolver = MyApplication.getAppContext().contentResolver
val imageUri = outputFileResults.savedUri ?: return

// Open the input stream of the original image
val inputStream = contentResolver.openInputStream(imageUri)
val bitmap: Bitmap = BitmapFactory.decodeStream(inputStream)

val segmentationResults: ImageSegmenterResult = imageSegmenterHelper.segmentImageFile(BitmapImageBuilder(bitmap).build()) ?: return

val byteBuffer: ByteBuffer = ByteBufferExtractor.extract(segmentationResults.categoryMask().get())

val blurred = applyBlurBasedOnMask(MyApplication.getAppContext(), imageUri, byteBuffer) ?: return
blurred.saveImage(MyApplication.getAppContext())
inputStream?.close()
}

fun applyBlurBasedOnMask(context: Context, imageUri: Uri, byteBuffer: ByteBuffer): Bitmap? {
// Step 1: Load the image from URI
val bitmap = loadBitmapFromUri(context, imageUri) ?: return null

// Step 2: Extract the mask from the ByteBuffer (assuming it's a binary mask with the same dimensions as the image)
val width = bitmap.width
val height = bitmap.height
val maskPixels = IntArray(width * height)

// Convert ByteBuffer to pixel values (assuming it's a grayscale mask)
byteBuffer.rewind() // Reset ByteBuffer position
for (i in 0 until width * height) {
val pixelValue = byteBuffer.get().toInt() // Get a single byte from the buffer
maskPixels[i] = if (pixelValue != 0) Color.WHITE else Color.BLACK
}

// Step 3: Apply a blur using RenderScript (or RenderEffect if on a newer Android version)
val rs = RenderScript.create(context)
val inputAllocation = Allocation.createFromBitmap(rs, bitmap)
val outputAllocation = Allocation.createTyped(rs, inputAllocation.type)

val blurScript = ScriptIntrinsicBlur.create(rs, Element.U8_4(rs))
blurScript.setRadius(25f) // Adjust blur radius

blurScript.setInput(inputAllocation)
blurScript.forEach(outputAllocation)

val blurredBitmap = Bitmap.createBitmap(bitmap.width, bitmap.height, Bitmap.Config.ARGB_8888)
outputAllocation.copyTo(blurredBitmap)

// Step 4: Apply the mask: only blur pixels where the mask is non-zero
val finalBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
val canvas = Canvas(finalBitmap)
canvas.drawBitmap(bitmap, 0f, 0f, null) // Draw the original bitmap on the canvas

// Apply blur only on mask areas
for (y in 0 until height) {
for (x in 0 until width) {
if (maskPixels[y * width + x] == Color.WHITE) {
finalBitmap.setPixel(x, y, blurredBitmap.getPixel(x, y))
}
}
}

rs.destroy() // Clean up RenderScript resources
return finalBitmap
}

fun loadBitmapFromUri(context: Context, uri: Uri): Bitmap? {
try {
val inputStream = context.contentResolver.openInputStream(uri)
return BitmapFactory.decodeStream(inputStream)
} catch (e: Exception) {
e.printStackTrace()
return null
}
}

fun Bitmap.saveImage(context: Context): Uri? {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Long term will just overwrite the saved image, but for now i wanna compare

if (android.os.Build.VERSION.SDK_INT >= 29) {
val values = ContentValues()
values.put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg")
values.put(MediaStore.Images.Media.DATE_ADDED, System.currentTimeMillis() / 1000)
values.put(MediaStore.Images.Media.DATE_TAKEN, System.currentTimeMillis())
values.put(MediaStore.Images.Media.RELATIVE_PATH, "Pictures/test_pictures")
values.put(MediaStore.Images.Media.IS_PENDING, true)
values.put(MediaStore.Images.Media.DISPLAY_NAME, "img_${SystemClock.uptimeMillis()}")

val uri: Uri? =
context.contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values)
if (uri != null) {
saveImageToStream(this, context.contentResolver.openOutputStream(uri))
values.put(MediaStore.Images.Media.IS_PENDING, false)
context.contentResolver.update(uri, values, null, null)
return uri
}
} else {
val directory =
File(context.getExternalFilesDir(Environment.DIRECTORY_PICTURES).toString() + "_test_pictures")
if (!directory.exists()) {
directory.mkdirs()
}
val fileName = "img_${SystemClock.uptimeMillis()}"+ ".jpeg"
val file = File(directory, fileName)
saveImageToStream(this, FileOutputStream(file))
if (file.absolutePath != null) {
val values = ContentValues()
values.put(MediaStore.Images.Media.DATA, file.absolutePath)
// .DATA is deprecated in API 29
context.contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values)
return Uri.fromFile(file)
}
}
return null
}

fun saveImageToStream(bitmap: Bitmap, outputStream: OutputStream?) {
if (outputStream != null) {
try {
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, outputStream)
outputStream.close()
} catch (e: Exception) {
e.printStackTrace()
}
}
}

override val settings: List<PluginSetting> = emptyList() // No settings for portrait yet

override fun onError(error: String, errorCode: Int) {
TODO("Not yet implemented")
}

override fun onResults(resultBundle: ImageSegmenterHelper.ResultBundle) {
TODO("Not yet implemented")
}
}
Loading
Loading