Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import com.android.ai.samples.genai_image_description.GenAIImageDescriptionScree
import com.android.ai.samples.genai_summarization.GenAISummarizationScreen
import com.android.ai.samples.genai_writing_assistance.GenAIWritingAssistanceScreen
import com.android.ai.samples.imagen.ui.ImagenScreen
import com.android.ai.samples.magicselfie.MagicSelfieScreen
import com.android.ai.samples.magicselfie.ui.MagicSelfieScreen

val sampleCatalog = listOf<SampleCatalogItem>(
SampleCatalogItem(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,11 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.ai.samples.magicselfie
package com.android.ai.samples.magicselfie.data

import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Paint
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.google.firebase.Firebase
import com.google.firebase.ai.ai
import com.google.firebase.ai.type.GenerativeBackend
Expand All @@ -33,19 +29,13 @@ import com.google.mlkit.vision.common.InputImage
import com.google.mlkit.vision.segmentation.subject.SubjectSegmentation
import com.google.mlkit.vision.segmentation.subject.SubjectSegmenterOptions
import javax.inject.Inject
import javax.inject.Singleton
import kotlin.coroutines.suspendCoroutine
import kotlin.math.roundToInt
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch

@OptIn(PublicPreviewAPI::class)
class MagicSelfieViewModel @Inject constructor() : ViewModel() {

private val _foregroundBitmap = MutableStateFlow<Bitmap?>(null)
val foregroundBitmap: MutableStateFlow<Bitmap?> = _foregroundBitmap

private val _progress = MutableLiveData<String?>(null)
val progress: LiveData<String?> = _progress

@Singleton
class MagicSelfieRepository @Inject constructor() {
@OptIn(PublicPreviewAPI::class)
private val imagenModel = Firebase.ai(backend = GenerativeBackend.vertexAI()).imagenModel(
modelName = "imagen-4.0-generate-preview-06-06",
generationConfig = ImagenGenerationConfig(
Expand All @@ -61,36 +51,28 @@ class MagicSelfieViewModel @Inject constructor() : ViewModel() {
.build(),
)

fun createMagicSelfie(bitmap: Bitmap, prompt: String) {
suspend fun generateForegroundBitmap(bitmap: Bitmap): Bitmap {
val image = InputImage.fromBitmap(bitmap, 0)

_progress.value = "Removing selfie background..."

subjectSegmenter.process(image)
.addOnSuccessListener {
it.foregroundBitmap?.let {
_foregroundBitmap.value = it
generateBackground(prompt)
return suspendCoroutine { continuation ->
subjectSegmenter.process(image)
.addOnSuccessListener {
it.foregroundBitmap?.let { foregroundBitmap ->
continuation.resumeWith(Result.success(foregroundBitmap))
}
}
.addOnFailureListener {
continuation.resumeWith(Result.failure(it))
}
}.addOnFailureListener {
_progress.postValue("Something went wrong :(")
}
}
Comment thread
lethargicpanda marked this conversation as resolved.
}

private fun generateBackground(prompt: String) {
_progress.value = "Generating new background..."

viewModelScope.launch {
val imageResponse = imagenModel.generateImages(
prompt = prompt,
)
val image = imageResponse.images.first()

val bitmapImage = image.asBitmap()

_foregroundBitmap.value = combineBitmaps(_foregroundBitmap.value!!, bitmapImage)
_progress.postValue(null)
}
@OptIn(PublicPreviewAPI::class)
suspend fun generateBackground(prompt: String): Bitmap {
val imageResponse = imagenModel.generateImages(
prompt = prompt,
)
val image = imageResponse.images.first()
return image.asBitmap()
}

fun combineBitmaps(foreground: Bitmap, background: Bitmap): Bitmap {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
* Copyright 2025 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.ai.samples.magicselfie.ui

import android.graphics.Bitmap
import android.graphics.Matrix
import android.media.ExifInterface
import java.io.File

fun rotateImageIfRequired(imageFile: File, bitmap: Bitmap): Bitmap {
val ei = ExifInterface(imageFile.absolutePath)
val orientation = ei.getAttributeInt(
ExifInterface.TAG_ORIENTATION,
ExifInterface.ORIENTATION_NORMAL,
)

return when (orientation) {
ExifInterface.ORIENTATION_ROTATE_90 -> rotateImage(bitmap, 90f)
ExifInterface.ORIENTATION_ROTATE_180 -> rotateImage(bitmap, 180f)
ExifInterface.ORIENTATION_ROTATE_270 -> rotateImage(bitmap, 270f)
ExifInterface.ORIENTATION_FLIP_HORIZONTAL -> flipImage(bitmap, true, false)
ExifInterface.ORIENTATION_FLIP_VERTICAL -> flipImage(bitmap, false, true)
ExifInterface.ORIENTATION_TRANSPOSE -> flipImage(rotateImage(bitmap, 90f), true, false)
ExifInterface.ORIENTATION_TRANSVERSE -> flipImage(rotateImage(bitmap, 270f), true, false)
else -> bitmap
}
}

fun rotateImage(bitmap: Bitmap, degrees: Float): Bitmap {
val matrix = Matrix()
matrix.postRotate(degrees)
return Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true)
}

fun flipImage(bitmap: Bitmap, horizontal: Boolean, vertical: Boolean): Bitmap {
val matrix = Matrix()
val scaleX = if (horizontal) -1f else 1f
val scaleY = if (vertical) -1f else 1f
matrix.setScale(scaleX, scaleY)
return Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true)
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,12 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.ai.samples.magicselfie
package com.android.ai.samples.magicselfie.ui

import android.annotation.SuppressLint
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.Matrix
import android.media.ExifInterface
import android.net.Uri
import android.provider.MediaStore
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.ActivityResult
Expand All @@ -35,13 +31,13 @@ import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.CameraAlt
import androidx.compose.material.icons.filled.Code
import androidx.compose.material.icons.filled.SmartToy
import androidx.compose.material3.Button
import androidx.compose.material3.Card
Expand All @@ -58,7 +54,6 @@ import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
Expand All @@ -69,16 +64,17 @@ import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.core.content.FileProvider
import androidx.hilt.navigation.compose.hiltViewModel
import com.android.ai.samples.magicselfie.R
import java.io.File

@OptIn(ExperimentalMaterial3Api::class)
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
@Composable
fun MagicSelfieScreen(viewModel: MagicSelfieViewModel = hiltViewModel()) {
val context = LocalContext.current
val uiState by viewModel.uiState.collectAsState()

val topAppBarState = rememberTopAppBarState()
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(topAppBarState)
Expand All @@ -95,8 +91,6 @@ fun MagicSelfieScreen(viewModel: MagicSelfieViewModel = hiltViewModel()) {
cameraIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)

var selfieBitmap by remember { mutableStateOf<Bitmap?>(null) }
val progress by viewModel.progress.observeAsState(null)
val generatedBitmap by viewModel.foregroundBitmap.collectAsState()
var editTextValue by remember { mutableStateOf("A very scenic view from the edge of the grand canyon") }

val resultLauncher =
Expand Down Expand Up @@ -132,6 +126,7 @@ fun MagicSelfieScreen(viewModel: MagicSelfieViewModel = hiltViewModel()) {
Modifier
.padding(12.dp)
.padding(innerPadding)
.imePadding()
.verticalScroll(rememberScrollState()),
) {
Card(
Expand All @@ -141,12 +136,12 @@ fun MagicSelfieScreen(viewModel: MagicSelfieViewModel = hiltViewModel()) {
height = 450.dp,
),
) {

if (generatedBitmap != null) {
if (uiState is MagicSelfieUiState.Success) {
val successState = uiState as MagicSelfieUiState.Success
Image(
bitmap = generatedBitmap!!.asImageBitmap(),
bitmap = successState.bitmap.asImageBitmap(),
contentDescription = "Picture",
contentScale = ContentScale.Fit,
contentScale = ContentScale.Crop,
modifier = Modifier.fillMaxSize(),
)
} else if (selfieBitmap != null) {
Expand Down Expand Up @@ -183,74 +178,43 @@ fun MagicSelfieScreen(viewModel: MagicSelfieViewModel = hiltViewModel()) {
viewModel.createMagicSelfie(selfieBitmap!!, editTextValue)
}
},
enabled = progress == null,
enabled = (uiState !is MagicSelfieUiState.RemovingBackground) &&
Comment thread
lethargicpanda marked this conversation as resolved.
(uiState !is MagicSelfieUiState.GeneratingBackground),
) {
Comment thread
lethargicpanda marked this conversation as resolved.
Icon(Icons.Default.SmartToy, contentDescription = "Robot")
Text(modifier = Modifier.padding(start = 8.dp), text = "Generate")
}

if (progress != null) {
if (uiState is MagicSelfieUiState.RemovingBackground) {
Spacer(
modifier = Modifier
.height(30.dp)
.padding(12.dp),
)
Text(
text = progress!!,
text = stringResource(R.string.removing_background),
)
} else if (uiState is MagicSelfieUiState.GeneratingBackground) {
Spacer(
modifier = Modifier
.height(30.dp)
.padding(12.dp),
)
Text(
text = stringResource(R.string.generating_new_background),
)
} else if (uiState is MagicSelfieUiState.Error) {
val errorState = uiState as MagicSelfieUiState.Error
Spacer(
modifier = Modifier
.height(30.dp)
.padding(12.dp),
)
Text(
text = errorState.message ?: stringResource(R.string.unknown_error),
color = MaterialTheme.colorScheme.error,
)
}
}
}
}

fun rotateImageIfRequired(imageFile: File, bitmap: Bitmap): Bitmap {
val ei = ExifInterface(imageFile.absolutePath)
val orientation = ei.getAttributeInt(
ExifInterface.TAG_ORIENTATION,
ExifInterface.ORIENTATION_NORMAL,
)

return when (orientation) {
ExifInterface.ORIENTATION_ROTATE_90 -> rotateImage(bitmap, 90f)
ExifInterface.ORIENTATION_ROTATE_180 -> rotateImage(bitmap, 180f)
ExifInterface.ORIENTATION_ROTATE_270 -> rotateImage(bitmap, 270f)
ExifInterface.ORIENTATION_FLIP_HORIZONTAL -> flipImage(bitmap, true, false)
ExifInterface.ORIENTATION_FLIP_VERTICAL -> flipImage(bitmap, false, true)
ExifInterface.ORIENTATION_TRANSPOSE -> flipImage(rotateImage(bitmap, 90f), true, false)
ExifInterface.ORIENTATION_TRANSVERSE -> flipImage(rotateImage(bitmap, 270f), true, false)
else -> bitmap
}
}

fun rotateImage(bitmap: Bitmap, degrees: Float): Bitmap {
val matrix = Matrix()
matrix.postRotate(degrees)
return Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true)
}

fun flipImage(bitmap: Bitmap, horizontal: Boolean, vertical: Boolean): Bitmap {
val matrix = Matrix()
val scaleX = if (horizontal) -1f else 1f
val scaleY = if (vertical) -1f else 1f
matrix.setScale(scaleX, scaleY)
return Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true)
}

@Composable
fun SeeCodeButton(context: Context) {
val githubLink = "https://github.com/android/ai-samples/tree/main/ai-catalog/samples/magic-selfie"
Button(
onClick = {
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(githubLink))
context.startActivity(intent)
},
modifier = Modifier.padding(end = 8.dp),
) {
Icon(Icons.Filled.Code, contentDescription = "See code")
Text(
modifier = Modifier.padding(start = 8.dp),
fontSize = 12.sp,
text = stringResource(R.string.see_code),
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* Copyright 2025 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.ai.samples.magicselfie.ui

import android.graphics.Bitmap

sealed interface MagicSelfieUiState {
data object Initial : MagicSelfieUiState
data object RemovingBackground : MagicSelfieUiState
data object GeneratingBackground : MagicSelfieUiState
data class Success(val bitmap: Bitmap) : MagicSelfieUiState
data class Error(val message: String?) : MagicSelfieUiState
}
Loading