Skip to content
Open
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 @@ -127,7 +127,7 @@ val sampleCatalog = listOf(
description = R.string.magic_selfie_sample_list_description,
route = "MagicSelfieScreen",
sampleEntryScreen = { MagicSelfieScreen() },
tags = listOf(SampleTags.IMAGEN, SampleTags.FIREBASE, SampleTags.ML_KIT),
tags = listOf(SampleTags.GEMINI_FLASH, SampleTags.FIREBASE),
needsFirebase = true,
keyArt = R.drawable.img_keyart_magic_selfie,
),
Expand Down
4 changes: 2 additions & 2 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@
<string name="imagen_sample_list_description">Generate images with Imagen, Google image generation model</string>
<string name="imagen_editing_sample_list_title">Image Editing with Imagen</string>
<string name="imagen_editing_sample_list_description">Generate images and edit only specific areas of a generated image with inpainting</string>
<string name="magic_selfie_sample_list_title">Magic Selfie with Imagen and ML Kit</string>
<string name="magic_selfie_sample_list_description">Change the background of your selfies with Imagen and the ML Kit Segmentation API</string>
<string name="magic_selfie_sample_list_title">Magic Selfie with Gemini</string>
<string name="magic_selfie_sample_list_description">Change the background of your selfies with the Gemini Flash model</string>
<string name="gemini_video_summarization_sample_list_title">Video Summarization with Gemini and Firebase</string>
<string name="gemini_video_summarization_sample_list_description">"Generate a summary of a video (from a cloud URL or Youtube) with Gemini API powered by Firebase"</string>
<string name="gemini_video_metadata_creation_sample_list_title">Video Metadata Creation with Gemini and Firebase</string>
Expand Down
1 change: 0 additions & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,6 @@ androidx-media3-ui = { module = "androidx.media3:media3-ui", version.ref = "medi
androidx-media3-ui-compose = { module = "androidx.media3:media3-ui-compose", version.ref = "media3"}
androidx-media3-transformer = { module = "androidx.media3:media3-transformer", version.ref = "media3" }
androidx-ui-tooling-preview-android = { group = "androidx.compose.ui", name = "ui-tooling-preview-android", version.ref = "uiToolingPreviewAndroid" }
mlkit-segmentation = { module = "com.google.android.gms:play-services-mlkit-subject-segmentation", version.ref = "mlkitSegmentation" }
ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview", version.ref = "uiToolingPreview" }
ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling", version.ref = "uiTooling" }
androidx-lifecycle-viewmodel-android = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-android", version.ref = "lifecycleViewmodelAndroid" }
Expand Down
25 changes: 10 additions & 15 deletions samples/magic-selfie/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,32 +4,27 @@ This sample is part of the [AI Sample Catalog](../../). To build and run this sa

## Description

This sample demonstrates how to create a "magic selfie" by replacing the background of a user's photo with a generated image. It uses the ML Kit Subject Segmentation API to isolate the user from their original background and the Imagen model to generate a new background from a text prompt.
This sample demonstrates how to create a "magic selfie" by replacing the background of a user's photo with a generated image. It uses the Nano Banana 2 (`gemini-3.1-flash-image-preview`) model to perform semantic image editing, transforming the background based on a text prompt while preserving the subject.

<div style="text-align: center;">
<img width="320" alt="Magic Selfie in action" src="magic_selfie.png" />
</div>

## How it works

The application uses two main components. First, the ML Kit Subject Segmentation API processes the user's selfie to create a bitmap containing only the foreground (the person). Second, the Firebase AI SDK (see [How to run](../../#how-to-run)) for Android interacts with the Imagen model to generate a new background image from a user-provided text prompt. Finally, the application combines the foreground bitmap with the newly generated background to create the final magic selfie. The core logic for this process is in the [`MagicSelfieViewModel.kt`](./src/main/java/com/android/ai/samples/magicselfie/ui/MagicSelfieViewModel.kt) and [`MagicSelfieRepository.kt`](./src/main/java/com/android/ai/samples/magicselfie/data/MagicSelfieRepository.kt) files.
The application uses the Firebase AI SDK (see [How to run](../../#how-to-run)) for Android to interact with the Nano Banana 2 model. Unlike older approaches that require manual subject segmentation and image compositing, Nano Banana 2 can process a multimodal prompt (an image plus text) to modify the scene directly. The application sends the user's selfie and a prompt describing the desired background, and the model generates a new version of the image with the background replaced. The core logic for this process is in the [`MagicSelfieViewModel.kt`](./src/main/java/com/android/ai/samples/magicselfie/ui/MagicSelfieViewModel.kt) and [`MagicSelfieRepository.kt`](./src/main/java/com/android/ai/samples/magicselfie/data/MagicSelfieRepository.kt) files.

Here is the key snippet of code that orchestrates the magic selfie creation from [`MagicSelfieViewModel.kt`](./src/main/java/com/android/ai/samples/magicselfie/ui/MagicSelfieViewModel.kt):
Here is the key snippet of code that calls the generative model from [`MagicSelfieRepository.kt`](./src/main/java/com/android/ai/samples/magicselfie/data/MagicSelfieRepository.kt):

```kotlin
fun createMagicSelfie(bitmap: Bitmap, prompt: String) {
viewModelScope.launch {
try {
_uiState.value = MagicSelfieUiState.RemovingBackground
val foregroundBitmap = magicSelfieRepository.generateForegroundBitmap(bitmap)
_uiState.value = MagicSelfieUiState.GeneratingBackground
val backgroundBitmap = magicSelfieRepository.generateBackground(prompt)
val resultBitmap = magicSelfieRepository.combineBitmaps(foregroundBitmap, backgroundBitmap)
_uiState.value = MagicSelfieUiState.Success(resultBitmap)
} catch (e: Exception) {
_uiState.value = MagicSelfieUiState.Error(e.message)
}
suspend fun generateMagicSelfie(bitmap: Bitmap, prompt: String): Bitmap {
val multimodalPrompt = content {
image(bitmap)
text("Change the background of this image to $prompt")
}
val response = generativeModel.generateContent(multimodalPrompt)
return response.candidates.firstOrNull()?.content?.parts?.firstNotNullOfOrNull { it.asImageOrNull() }
?: throw Exception("No image generated")
}
```

Expand Down
1 change: 0 additions & 1 deletion samples/magic-selfie/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,6 @@ dependencies {
implementation(libs.hilt.android)
implementation(libs.hilt.navigation.compose)
implementation(libs.androidx.runtime.livedata)
implementation(libs.mlkit.segmentation)
implementation(libs.ui.tooling.preview)
debugImplementation(libs.ui.tooling)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,87 +16,34 @@
package com.android.ai.samples.magicselfie.data

import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Paint
import com.google.firebase.Firebase
import com.google.firebase.ai.ai
import com.google.firebase.ai.type.GenerativeBackend
import com.google.firebase.ai.type.ImagenAspectRatio
import com.google.firebase.ai.type.ImagenGenerationConfig
import com.google.firebase.ai.type.ImagenImageFormat
import com.google.firebase.ai.type.PublicPreviewAPI
import com.google.mlkit.vision.common.InputImage
import com.google.mlkit.vision.segmentation.subject.SubjectSegmentation
import com.google.mlkit.vision.segmentation.subject.SubjectSegmenterOptions
import com.google.firebase.ai.type.ResponseModality
import com.google.firebase.ai.type.asImageOrNull
import com.google.firebase.ai.type.content
import com.google.firebase.ai.type.generationConfig
import javax.inject.Inject
import javax.inject.Singleton
import kotlin.coroutines.suspendCoroutine
import kotlin.math.roundToInt

@Singleton
class MagicSelfieRepository @Inject constructor() {
@OptIn(PublicPreviewAPI::class)
private val imagenModel = Firebase.ai(backend = GenerativeBackend.vertexAI()).imagenModel(
modelName = "imagen-4.0-generate-001",
generationConfig = ImagenGenerationConfig(
numberOfImages = 1,
aspectRatio = ImagenAspectRatio.PORTRAIT_3x4,
imageFormat = ImagenImageFormat.jpeg(compressionQuality = 75),
),
)

private val subjectSegmenter = SubjectSegmentation.getClient(
SubjectSegmenterOptions.Builder()
.enableForegroundBitmap()
.build(),
)

suspend fun generateForegroundBitmap(bitmap: Bitmap): Bitmap {
val image = InputImage.fromBitmap(bitmap, 0)
return suspendCoroutine { continuation ->
subjectSegmenter.process(image)
.addOnSuccessListener {
it.foregroundBitmap?.let { foregroundBitmap ->
continuation.resumeWith(Result.success(foregroundBitmap))
}
}
.addOnFailureListener {
continuation.resumeWith(Result.failure(it))
}
}
}

@OptIn(PublicPreviewAPI::class)
suspend fun generateBackground(prompt: String): Bitmap {
val imageResponse = imagenModel.generateImages(
prompt = prompt,
private val generativeModel by lazy {
Firebase.ai(backend = GenerativeBackend.googleAI()).generativeModel(
modelName = "gemini-3.1-flash-image-preview",
generationConfig = generationConfig {
responseModalities = listOf(ResponseModality.TEXT, ResponseModality.IMAGE)
}
)
val image = imageResponse.images.first()
return image.asBitmap()
}

fun combineBitmaps(foreground: Bitmap, background: Bitmap): Bitmap {
val height = background.height
val width = background.width

val resultBitmap = Bitmap.createBitmap(width, height, background.config!!)
val canvas = Canvas(resultBitmap)
val paint = Paint()
canvas.drawBitmap(background, 0f, 0f, paint)

var foregroundHeight = foreground.height
var foregroundWidth = foreground.width
val ratio = foregroundWidth.toFloat() / foregroundHeight.toFloat()

foregroundHeight = height
foregroundWidth = (foregroundHeight * ratio).roundToInt()

val scaledForeground = Bitmap.createScaledBitmap(foreground, foregroundWidth, foregroundHeight, false)

val left = (width - scaledForeground.width) / 2f
val top = (height - scaledForeground.height.toFloat())
canvas.drawBitmap(scaledForeground, left, top, paint)

return resultBitmap
suspend fun generateMagicSelfie(bitmap: Bitmap, prompt: String): Bitmap {
val multimodalPrompt = content {
image(bitmap)
text("Change the background of this image to $prompt")
}
val response = generativeModel.generateContent(multimodalPrompt)
return response.candidates.firstOrNull()?.content?.parts?.firstNotNullOfOrNull { it.asImageOrNull() }
?: throw Exception("No image generated")
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can we pass the error message?

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,6 @@ private fun MagicSelfieScreen(
text = "",
icon = painterResource(id = com.android.ai.uicomponent.R.drawable.ic_ai_bg),
enabled = textFieldState.text.isNotEmpty() &&
(uiState !is MagicSelfieUiState.RemovingBackground) &&
(uiState !is MagicSelfieUiState.GeneratingBackground),
) {
onGenerateClick(selfieBitmap, textFieldState.text.toString())
Expand All @@ -246,8 +245,7 @@ private fun MagicSelfieScreen(
SecondaryButton(
text = "",
icon = painterResource(id = com.android.ai.uicomponent.R.drawable.ic_ai_img),
enabled = (uiState !is MagicSelfieUiState.RemovingBackground) &&
(uiState !is MagicSelfieUiState.GeneratingBackground),
enabled = (uiState !is MagicSelfieUiState.GeneratingBackground),
onClick = onTakePictureClick,
)
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,8 @@ class MagicSelfieViewModel @Inject constructor(private val magicSelfieRepository
fun createMagicSelfie(bitmap: Bitmap, prompt: String) {
viewModelScope.launch {
try {
_uiState.value = MagicSelfieUiState.RemovingBackground
val foregroundBitmap = magicSelfieRepository.generateForegroundBitmap(bitmap)
_uiState.value = MagicSelfieUiState.GeneratingBackground
val backgroundBitmap = magicSelfieRepository.generateBackground(prompt)
val resultBitmap = magicSelfieRepository.combineBitmaps(foregroundBitmap, backgroundBitmap)
val resultBitmap = magicSelfieRepository.generateMagicSelfie(bitmap, prompt)
_uiState.value = MagicSelfieUiState.Success(resultBitmap)
} catch (e: Exception) {
_uiState.value = MagicSelfieUiState.Error(e.message)
Expand Down
2 changes: 1 addition & 1 deletion samples/magic-selfie/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="magic_selfie_title">Magic Selfie</string>
<string name="magic_selfie_subtitle">Change the background of you selfies with Imagen and the ML Kit Segmentation API</string>
<string name="magic_selfie_subtitle">Change the background of your selfies with the Gemini Flash model</string>
<string name="add_image">Add image</string>
<string name="unknown_error">Unknown error</string>
<string name="prompt_placeholder">A very scenic view of the grand canyon</string>
Expand Down
Loading