Skip to content

Commit 727f69c

Browse files
Migrate Magic Selfie to Nano Banana2 (#157)
1 parent fee369b commit 727f69c

File tree

10 files changed

+34
-100
lines changed

10 files changed

+34
-100
lines changed

app/src/main/java/com/android/ai/catalog/domain/SampleCatalog.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ val sampleCatalog = listOf(
127127
description = R.string.magic_selfie_sample_list_description,
128128
route = "MagicSelfieScreen",
129129
sampleEntryScreen = { MagicSelfieScreen() },
130-
tags = listOf(SampleTags.IMAGEN, SampleTags.FIREBASE, SampleTags.ML_KIT),
130+
tags = listOf(SampleTags.GEMINI_FLASH, SampleTags.FIREBASE),
131131
needsFirebase = true,
132132
keyArt = R.drawable.img_keyart_magic_selfie,
133133
),

app/src/main/res/values/strings.xml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@
1717
<string name="imagen_sample_list_description">Generate images with Imagen, Google image generation model</string>
1818
<string name="imagen_editing_sample_list_title">Image Editing with Imagen</string>
1919
<string name="imagen_editing_sample_list_description">Generate images and edit only specific areas of a generated image with inpainting</string>
20-
<string name="magic_selfie_sample_list_title">Magic Selfie with Imagen and ML Kit</string>
21-
<string name="magic_selfie_sample_list_description">Change the background of your selfies with Imagen and the ML Kit Segmentation API</string>
20+
<string name="magic_selfie_sample_list_title">Magic Selfie with Gemini</string>
21+
<string name="magic_selfie_sample_list_description">Change the background of your selfies with the Gemini Flash model</string>
2222
<string name="gemini_video_summarization_sample_list_title">Video Summarization with Gemini and Firebase</string>
2323
<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>
2424
<string name="gemini_video_metadata_creation_sample_list_title">Video Metadata Creation with Gemini and Firebase</string>

gradle/libs.versions.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,6 @@ androidx-media3-ui = { module = "androidx.media3:media3-ui", version.ref = "medi
8181
androidx-media3-ui-compose = { module = "androidx.media3:media3-ui-compose", version.ref = "media3"}
8282
androidx-media3-transformer = { module = "androidx.media3:media3-transformer", version.ref = "media3" }
8383
androidx-ui-tooling-preview-android = { group = "androidx.compose.ui", name = "ui-tooling-preview-android", version.ref = "uiToolingPreviewAndroid" }
84-
mlkit-segmentation = { module = "com.google.android.gms:play-services-mlkit-subject-segmentation", version.ref = "mlkitSegmentation" }
8584
ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview", version.ref = "uiToolingPreview" }
8685
ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling", version.ref = "uiTooling" }
8786
androidx-lifecycle-viewmodel-android = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-android", version.ref = "lifecycleViewmodelAndroid" }

samples/magic-selfie/README.md

Lines changed: 10 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,32 +4,27 @@ This sample is part of the [AI Sample Catalog](../../). To build and run this sa
44

55
## Description
66

7-
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.
7+
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.
88

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

1313
## How it works
1414

15-
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.
15+
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.
1616

17-
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):
17+
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):
1818

1919
```kotlin
20-
fun createMagicSelfie(bitmap: Bitmap, prompt: String) {
21-
viewModelScope.launch {
22-
try {
23-
_uiState.value = MagicSelfieUiState.RemovingBackground
24-
val foregroundBitmap = magicSelfieRepository.generateForegroundBitmap(bitmap)
25-
_uiState.value = MagicSelfieUiState.GeneratingBackground
26-
val backgroundBitmap = magicSelfieRepository.generateBackground(prompt)
27-
val resultBitmap = magicSelfieRepository.combineBitmaps(foregroundBitmap, backgroundBitmap)
28-
_uiState.value = MagicSelfieUiState.Success(resultBitmap)
29-
} catch (e: Exception) {
30-
_uiState.value = MagicSelfieUiState.Error(e.message)
31-
}
20+
suspend fun generateMagicSelfie(bitmap: Bitmap, prompt: String): Bitmap {
21+
val multimodalPrompt = content {
22+
image(bitmap)
23+
text("Change the background of this image to $prompt")
3224
}
25+
val response = generativeModel.generateContent(multimodalPrompt)
26+
return response.candidates.firstOrNull()?.content?.parts?.firstNotNullOfOrNull { it.asImageOrNull() }
27+
?: throw Exception("No image generated")
3328
}
3429
```
3530

samples/magic-selfie/build.gradle.kts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,6 @@ dependencies {
6969
implementation(libs.hilt.android)
7070
implementation(libs.hilt.navigation.compose)
7171
implementation(libs.androidx.runtime.livedata)
72-
implementation(libs.mlkit.segmentation)
7372
implementation(libs.ui.tooling.preview)
7473
debugImplementation(libs.ui.tooling)
7574

samples/magic-selfie/src/main/java/com/android/ai/samples/magicselfie/data/MagicSelfieRepository.kt

Lines changed: 18 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -16,87 +16,34 @@
1616
package com.android.ai.samples.magicselfie.data
1717

1818
import android.graphics.Bitmap
19-
import android.graphics.Canvas
20-
import android.graphics.Paint
2119
import com.google.firebase.Firebase
2220
import com.google.firebase.ai.ai
2321
import com.google.firebase.ai.type.GenerativeBackend
24-
import com.google.firebase.ai.type.ImagenAspectRatio
25-
import com.google.firebase.ai.type.ImagenGenerationConfig
26-
import com.google.firebase.ai.type.ImagenImageFormat
27-
import com.google.firebase.ai.type.PublicPreviewAPI
28-
import com.google.mlkit.vision.common.InputImage
29-
import com.google.mlkit.vision.segmentation.subject.SubjectSegmentation
30-
import com.google.mlkit.vision.segmentation.subject.SubjectSegmenterOptions
22+
import com.google.firebase.ai.type.ResponseModality
23+
import com.google.firebase.ai.type.asImageOrNull
24+
import com.google.firebase.ai.type.content
25+
import com.google.firebase.ai.type.generationConfig
3126
import javax.inject.Inject
3227
import javax.inject.Singleton
33-
import kotlin.coroutines.suspendCoroutine
34-
import kotlin.math.roundToInt
3528

3629
@Singleton
3730
class MagicSelfieRepository @Inject constructor() {
38-
@OptIn(PublicPreviewAPI::class)
39-
private val imagenModel = Firebase.ai(backend = GenerativeBackend.vertexAI()).imagenModel(
40-
modelName = "imagen-4.0-generate-001",
41-
generationConfig = ImagenGenerationConfig(
42-
numberOfImages = 1,
43-
aspectRatio = ImagenAspectRatio.PORTRAIT_3x4,
44-
imageFormat = ImagenImageFormat.jpeg(compressionQuality = 75),
45-
),
46-
)
47-
48-
private val subjectSegmenter = SubjectSegmentation.getClient(
49-
SubjectSegmenterOptions.Builder()
50-
.enableForegroundBitmap()
51-
.build(),
52-
)
53-
54-
suspend fun generateForegroundBitmap(bitmap: Bitmap): Bitmap {
55-
val image = InputImage.fromBitmap(bitmap, 0)
56-
return suspendCoroutine { continuation ->
57-
subjectSegmenter.process(image)
58-
.addOnSuccessListener {
59-
it.foregroundBitmap?.let { foregroundBitmap ->
60-
continuation.resumeWith(Result.success(foregroundBitmap))
61-
}
62-
}
63-
.addOnFailureListener {
64-
continuation.resumeWith(Result.failure(it))
65-
}
66-
}
67-
}
68-
69-
@OptIn(PublicPreviewAPI::class)
70-
suspend fun generateBackground(prompt: String): Bitmap {
71-
val imageResponse = imagenModel.generateImages(
72-
prompt = prompt,
31+
private val generativeModel by lazy {
32+
Firebase.ai(backend = GenerativeBackend.googleAI()).generativeModel(
33+
modelName = "gemini-3.1-flash-image-preview",
34+
generationConfig = generationConfig {
35+
responseModalities = listOf(ResponseModality.TEXT, ResponseModality.IMAGE)
36+
}
7337
)
74-
val image = imageResponse.images.first()
75-
return image.asBitmap()
7638
}
7739

78-
fun combineBitmaps(foreground: Bitmap, background: Bitmap): Bitmap {
79-
val height = background.height
80-
val width = background.width
81-
82-
val resultBitmap = Bitmap.createBitmap(width, height, background.config!!)
83-
val canvas = Canvas(resultBitmap)
84-
val paint = Paint()
85-
canvas.drawBitmap(background, 0f, 0f, paint)
86-
87-
var foregroundHeight = foreground.height
88-
var foregroundWidth = foreground.width
89-
val ratio = foregroundWidth.toFloat() / foregroundHeight.toFloat()
90-
91-
foregroundHeight = height
92-
foregroundWidth = (foregroundHeight * ratio).roundToInt()
93-
94-
val scaledForeground = Bitmap.createScaledBitmap(foreground, foregroundWidth, foregroundHeight, false)
95-
96-
val left = (width - scaledForeground.width) / 2f
97-
val top = (height - scaledForeground.height.toFloat())
98-
canvas.drawBitmap(scaledForeground, left, top, paint)
99-
100-
return resultBitmap
40+
suspend fun generateMagicSelfie(bitmap: Bitmap, prompt: String): Bitmap {
41+
val multimodalPrompt = content {
42+
image(bitmap)
43+
text("Change the background of this image to $prompt")
44+
}
45+
val response = generativeModel.generateContent(multimodalPrompt)
46+
return response.candidates.firstOrNull()?.content?.parts?.firstNotNullOfOrNull { it.asImageOrNull() }
47+
?: throw Exception("No image generated")
10148
}
10249
}

samples/magic-selfie/src/main/java/com/android/ai/samples/magicselfie/ui/MagicSelfieScreen.kt

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -235,7 +235,6 @@ private fun MagicSelfieScreen(
235235
text = "",
236236
icon = painterResource(id = com.android.ai.uicomponent.R.drawable.ic_ai_bg),
237237
enabled = textFieldState.text.isNotEmpty() &&
238-
(uiState !is MagicSelfieUiState.RemovingBackground) &&
239238
(uiState !is MagicSelfieUiState.GeneratingBackground),
240239
) {
241240
onGenerateClick(selfieBitmap, textFieldState.text.toString())
@@ -246,8 +245,7 @@ private fun MagicSelfieScreen(
246245
SecondaryButton(
247246
text = "",
248247
icon = painterResource(id = com.android.ai.uicomponent.R.drawable.ic_ai_img),
249-
enabled = (uiState !is MagicSelfieUiState.RemovingBackground) &&
250-
(uiState !is MagicSelfieUiState.GeneratingBackground),
248+
enabled = (uiState !is MagicSelfieUiState.GeneratingBackground),
251249
onClick = onTakePictureClick,
252250
)
253251
},

samples/magic-selfie/src/main/java/com/android/ai/samples/magicselfie/ui/MagicSelfieUiState.kt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ import android.graphics.Bitmap
1919

2020
sealed interface MagicSelfieUiState {
2121
data object Initial : MagicSelfieUiState
22-
data object RemovingBackground : MagicSelfieUiState
2322
data object GeneratingBackground : MagicSelfieUiState
2423
data class Success(val bitmap: Bitmap) : MagicSelfieUiState
2524
data class Error(val message: String?) : MagicSelfieUiState

samples/magic-selfie/src/main/java/com/android/ai/samples/magicselfie/ui/MagicSelfieViewModel.kt

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,8 @@ class MagicSelfieViewModel @Inject constructor(private val magicSelfieRepository
3434
fun createMagicSelfie(bitmap: Bitmap, prompt: String) {
3535
viewModelScope.launch {
3636
try {
37-
_uiState.value = MagicSelfieUiState.RemovingBackground
38-
val foregroundBitmap = magicSelfieRepository.generateForegroundBitmap(bitmap)
3937
_uiState.value = MagicSelfieUiState.GeneratingBackground
40-
val backgroundBitmap = magicSelfieRepository.generateBackground(prompt)
41-
val resultBitmap = magicSelfieRepository.combineBitmaps(foregroundBitmap, backgroundBitmap)
38+
val resultBitmap = magicSelfieRepository.generateMagicSelfie(bitmap, prompt)
4239
_uiState.value = MagicSelfieUiState.Success(resultBitmap)
4340
} catch (e: Exception) {
4441
_uiState.value = MagicSelfieUiState.Error(e.message)

samples/magic-selfie/src/main/res/values/strings.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<?xml version="1.0" encoding="utf-8"?>
22
<resources>
33
<string name="magic_selfie_title">Magic Selfie</string>
4-
<string name="magic_selfie_subtitle">Change the background of you selfies with Imagen and the ML Kit Segmentation API</string>
4+
<string name="magic_selfie_subtitle">Change the background of your selfies with the Gemini Flash model</string>
55
<string name="add_image">Add image</string>
66
<string name="unknown_error">Unknown error</string>
77
<string name="prompt_placeholder">A very scenic view of the grand canyon</string>

0 commit comments

Comments
 (0)