Skip to content

Commit 9dffec2

Browse files
davidmotsonDavid Motsonashvili
andauthored
Add imagen editing cases to the Firebase AI quickstart (#2702)
* add imagen editing cases to the quickstart * add outpainting example * fixes for comments, plus adding subject references and style customization * Add atachment list fo imagen3, also specify that image editing is vertex only * typo * Added options to inpainting and outpainting, also added downscaling to attached images to speed up generation time * format * checkpoint * change radio buttons to dropdown, and fix minor typos * add wiring * make the extra image for style transfer more obvious, and fix all the UI issues that this caused * adding label to style transfer and formating all files * fixes for comments * rename list * fix inpainting description * centralize all image genration code in ImagenViewModel.kt * clean up imports --------- Co-authored-by: David Motsonashvili <[email protected]>
1 parent f8225b2 commit 9dffec2

File tree

10 files changed

+428
-64
lines changed

10 files changed

+428
-64
lines changed

firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/FirebaseAISamples.kt

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,17 @@ package com.google.firebase.quickstart.ai
22

33
import com.google.firebase.ai.type.FunctionDeclaration
44
import com.google.firebase.ai.type.GenerativeBackend
5+
import com.google.firebase.ai.type.PublicPreviewAPI
56
import com.google.firebase.ai.type.ResponseModality
67
import com.google.firebase.ai.type.Schema
78
import com.google.firebase.ai.type.Tool
89
import com.google.firebase.ai.type.content
910
import com.google.firebase.ai.type.generationConfig
11+
import com.google.firebase.quickstart.ai.feature.media.imagen.EditingMode
1012
import com.google.firebase.quickstart.ai.ui.navigation.Category
1113
import com.google.firebase.quickstart.ai.ui.navigation.Sample
1214

15+
@OptIn(PublicPreviewAPI::class)
1316
val FIREBASE_AI_SAMPLES = listOf(
1417
Sample(
1518
title = "Travel tips",
@@ -131,7 +134,61 @@ val FIREBASE_AI_SAMPLES = listOf(
131134
text(
132135
"A photo of a modern building with water in the background"
133136
)
134-
}
137+
},
138+
allowEmptyPrompt = false,
139+
editingMode = EditingMode.GENERATE,
140+
),
141+
Sample(
142+
title = "Imagen 3 - Inpainting (Vertex AI)",
143+
description = "Replace part of an image using Imagen 3",
144+
modelName = "imagen-3.0-capability-001",
145+
backend = GenerativeBackend.vertexAI(),
146+
navRoute = "imagen",
147+
categories = listOf(Category.IMAGE),
148+
initialPrompt = content { text("A sunny beach") },
149+
includeAttach = true,
150+
allowEmptyPrompt = true,
151+
selectionOptions = listOf("Mask", "Background", "Foreground"),
152+
editingMode = EditingMode.INPAINTING,
153+
),
154+
Sample(
155+
title = "Imagen 3 - Outpainting (Vertex AI)",
156+
description = "Expand an image by drawing in more background",
157+
modelName = "imagen-3.0-capability-001",
158+
backend = GenerativeBackend.vertexAI(),
159+
navRoute = "imagen",
160+
categories = listOf(Category.IMAGE),
161+
initialPrompt = content { text("") },
162+
includeAttach = true,
163+
allowEmptyPrompt = true,
164+
selectionOptions = listOf("Image Alignment", "Center", "Top", "Bottom", "Left", "Right"),
165+
editingMode = EditingMode.OUTPAINTING,
166+
),
167+
Sample(
168+
title = "Imagen 3 - Subject Reference (Vertex AI)",
169+
description = "Generate an image using a referenced subject (must be an animal)",
170+
modelName = "imagen-3.0-capability-001",
171+
backend = GenerativeBackend.vertexAI(),
172+
navRoute = "imagen",
173+
categories = listOf(Category.IMAGE),
174+
initialPrompt = content { text("<subject> flying through space") },
175+
includeAttach = true,
176+
allowEmptyPrompt = false,
177+
editingMode = EditingMode.SUBJECT_REFERENCE,
178+
),
179+
Sample(
180+
title = "Imagen 3 - Style Transfer (Vertex AI)",
181+
description = "Change the art style of a cat picture using a reference",
182+
modelName = "imagen-3.0-capability-001",
183+
backend = GenerativeBackend.vertexAI(),
184+
navRoute = "imagen",
185+
categories = listOf(Category.IMAGE),
186+
initialPrompt = content { text("A picture of a cat") },
187+
includeAttach = true,
188+
allowEmptyPrompt = true,
189+
additionalImage = MainActivity.catImage,
190+
imageLabels = listOf("Style Target", "Style Source"),
191+
editingMode = EditingMode.STYLE_TRANSFER
135192
),
136193
Sample(
137194
title = "Gemini 2.0 Flash - image generation",

firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/MainActivity.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package com.google.firebase.quickstart.ai
22

3+
import android.graphics.Bitmap
4+
import android.graphics.BitmapFactory
35
import android.os.Bundle
46
import androidx.activity.ComponentActivity
57
import androidx.activity.compose.setContent
@@ -22,6 +24,7 @@ import androidx.navigation.NavDestination
2224
import androidx.navigation.compose.NavHost
2325
import androidx.navigation.compose.composable
2426
import androidx.navigation.compose.rememberNavController
27+
import com.google.firebase.ai.type.toImagenInlineImage
2528
import com.google.firebase.quickstart.ai.feature.live.StreamRealtimeRoute
2629
import com.google.firebase.quickstart.ai.feature.live.StreamRealtimeScreen
2730
import com.google.firebase.quickstart.ai.feature.media.imagen.ImagenRoute
@@ -36,6 +39,7 @@ class MainActivity : ComponentActivity() {
3639
override fun onCreate(savedInstanceState: Bundle?) {
3740
super.onCreate(savedInstanceState)
3841
enableEdgeToEdge()
42+
catImage = BitmapFactory.decodeResource(applicationContext.resources, R.drawable.cat)
3943
setContent {
4044
val navController = rememberNavController()
4145

@@ -110,4 +114,7 @@ class MainActivity : ComponentActivity() {
110114
})
111115
}
112116
}
117+
companion object{
118+
lateinit var catImage: Bitmap
119+
}
113120
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package com.google.firebase.quickstart.ai.feature.media.imagen
2+
3+
enum class EditingMode {
4+
GENERATE,
5+
INPAINTING,
6+
OUTPAINTING,
7+
SUBJECT_REFERENCE,
8+
STYLE_TRANSFER,
9+
}

firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/media/imagen/ImagenScreen.kt

Lines changed: 151 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,54 @@
11
package com.google.firebase.quickstart.ai.feature.media.imagen
22

3+
import android.net.Uri
4+
import android.provider.OpenableColumns
5+
import android.text.format.Formatter
6+
import androidx.activity.compose.rememberLauncherForActivityResult
7+
import androidx.activity.result.contract.ActivityResultContracts
38
import androidx.compose.foundation.Image
9+
import androidx.compose.foundation.clickable
10+
import androidx.compose.foundation.layout.Arrangement
411
import androidx.compose.foundation.layout.Box
512
import androidx.compose.foundation.layout.Column
13+
import androidx.compose.foundation.layout.Row
614
import androidx.compose.foundation.layout.fillMaxWidth
15+
import androidx.compose.foundation.layout.height
716
import androidx.compose.foundation.layout.padding
817
import androidx.compose.foundation.lazy.grid.GridCells
9-
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
18+
import androidx.compose.foundation.lazy.grid.LazyHorizontalGrid
1019
import androidx.compose.foundation.lazy.grid.items
20+
import androidx.compose.foundation.rememberScrollState
21+
import androidx.compose.foundation.verticalScroll
1122
import androidx.compose.material3.Card
1223
import androidx.compose.material3.CardDefaults
1324
import androidx.compose.material3.CircularProgressIndicator
25+
import androidx.compose.material3.DropdownMenu
26+
import androidx.compose.material3.DropdownMenuItem
1427
import androidx.compose.material3.ElevatedCard
1528
import androidx.compose.material3.MaterialTheme
1629
import androidx.compose.material3.OutlinedTextField
1730
import androidx.compose.material3.Text
1831
import androidx.compose.material3.TextButton
1932
import androidx.compose.runtime.Composable
2033
import androidx.compose.runtime.getValue
34+
import androidx.compose.runtime.mutableIntStateOf
2135
import androidx.compose.runtime.mutableStateOf
36+
import androidx.compose.runtime.remember
37+
import androidx.compose.runtime.rememberCoroutineScope
2238
import androidx.compose.runtime.saveable.rememberSaveable
2339
import androidx.compose.runtime.setValue
2440
import androidx.compose.ui.Alignment
2541
import androidx.compose.ui.Modifier
2642
import androidx.compose.ui.graphics.asImageBitmap
43+
import androidx.compose.ui.platform.LocalContext
44+
import androidx.compose.ui.res.painterResource
2745
import androidx.compose.ui.unit.dp
2846
import androidx.lifecycle.compose.collectAsStateWithLifecycle
2947
import androidx.lifecycle.viewmodel.compose.viewModel
48+
import com.google.firebase.quickstart.ai.R
49+
import com.google.firebase.quickstart.ai.feature.text.Attachment
50+
import com.google.firebase.quickstart.ai.feature.text.AttachmentsList
51+
import kotlinx.coroutines.launch
3052
import kotlinx.serialization.Serializable
3153

3254
@Serializable
@@ -40,9 +62,35 @@ fun ImagenScreen(
4062
val errorMessage by imagenViewModel.errorMessage.collectAsStateWithLifecycle()
4163
val isLoading by imagenViewModel.isLoading.collectAsStateWithLifecycle()
4264
val generatedImages by imagenViewModel.generatedBitmaps.collectAsStateWithLifecycle()
65+
val attachedImage by imagenViewModel.attachedImage.collectAsStateWithLifecycle()
66+
val context = LocalContext.current
67+
val contentResolver = context.contentResolver
68+
val scope = rememberCoroutineScope()
69+
val openDocument = rememberLauncherForActivityResult(ActivityResultContracts.OpenDocument()) { optionalUri: Uri? ->
70+
optionalUri?.let { uri ->
71+
var fileName: String? = null
72+
// Fetch file name and size
73+
contentResolver.query(uri, null, null, null, null)?.use { cursor ->
74+
val nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
75+
val sizeIndex = cursor.getColumnIndex(OpenableColumns.SIZE)
76+
cursor.moveToFirst()
77+
val humanReadableSize = Formatter.formatShortFileSize(
78+
context, cursor.getLong(sizeIndex)
79+
)
80+
fileName = "${cursor.getString(nameIndex)} ($humanReadableSize)"
81+
}
82+
83+
contentResolver.openInputStream(uri)?.use { stream ->
84+
val bytes = stream.readBytes()
85+
scope.launch {
86+
imagenViewModel.attachImage(bytes)
87+
}
88+
}
89+
}
90+
}
4391

4492
Column(
45-
modifier = Modifier
93+
modifier = Modifier.verticalScroll(rememberScrollState())
4694
) {
4795
ElevatedCard(
4896
modifier = Modifier
@@ -59,18 +107,49 @@ fun ImagenScreen(
59107
.padding(16.dp)
60108
.fillMaxWidth()
61109
)
62-
TextButton(
63-
onClick = {
64-
if (imagenPrompt.isNotBlank()) {
65-
imagenViewModel.generateImages(imagenPrompt)
66-
}
67-
},
68-
modifier = Modifier
69-
.padding(end = 16.dp, bottom = 16.dp)
70-
.align(Alignment.End)
71-
) {
72-
Text("Generate")
110+
if (imagenViewModel.selectionOptions.isNotEmpty()) {
111+
DropDownMenu(imagenViewModel.selectionOptions) { imagenViewModel.selectOption(it) }
73112
}
113+
val attachmentsList = buildList {
114+
if (imagenViewModel.additionalImage != null) {
115+
add(
116+
Attachment(
117+
imagenViewModel.imageLabels.getOrElse(0) { "" },
118+
imagenViewModel.additionalImage
119+
)
120+
)
121+
}
122+
if (attachedImage != null) {
123+
add(Attachment(imagenViewModel.imageLabels.getOrElse(1) { "" }, attachedImage))
124+
}
125+
}
126+
127+
if (imagenViewModel.includeAttach && attachmentsList.isNotEmpty()) {
128+
AttachmentsList(attachmentsList)
129+
}
130+
Row() {
131+
if (imagenViewModel.includeAttach) {
132+
TextButton(
133+
onClick = {
134+
openDocument.launch(arrayOf("image/*"))
135+
},
136+
modifier = Modifier
137+
.padding(end = 16.dp, bottom = 16.dp)
138+
) { Text("Attach") }
139+
}
140+
TextButton(
141+
onClick = {
142+
if (imagenViewModel.allowEmptyPrompt || imagenPrompt.isNotBlank()) {
143+
imagenViewModel.generateImages(imagenPrompt)
144+
}
145+
},
146+
modifier = Modifier
147+
.padding(end = 16.dp, bottom = 16.dp)
148+
) {
149+
Text("Generate")
150+
}
151+
}
152+
74153
}
75154

76155
if (isLoading) {
@@ -100,9 +179,11 @@ fun ImagenScreen(
100179
)
101180
}
102181
}
103-
LazyVerticalGrid(
104-
columns = GridCells.Fixed(2),
105-
modifier = Modifier.padding(16.dp)
182+
LazyHorizontalGrid(
183+
rows = GridCells.Fixed(2),
184+
modifier = Modifier
185+
.padding(16.dp)
186+
.height(500.dp)
106187
) {
107188
items(generatedImages) { image ->
108189
Card(
@@ -120,3 +201,57 @@ fun ImagenScreen(
120201
}
121202
}
122203
}
204+
205+
@Composable
206+
fun DropDownMenu(items: List<String>, onClick: (String) -> Unit) {
207+
208+
val isDropDownExpanded = remember {
209+
mutableStateOf(false)
210+
}
211+
212+
val itemPosition = remember {
213+
mutableIntStateOf(0)
214+
}
215+
216+
217+
Column(
218+
horizontalAlignment = Alignment.Start,
219+
verticalArrangement = Arrangement.Top,
220+
modifier = Modifier.padding(horizontal = 10.dp)
221+
) {
222+
223+
Box {
224+
Row(
225+
horizontalArrangement = Arrangement.Start,
226+
verticalAlignment = Alignment.Top,
227+
modifier = Modifier.clickable {
228+
isDropDownExpanded.value = true
229+
}
230+
) {
231+
Text(text = items[itemPosition.intValue])
232+
Image(
233+
painter = painterResource(id = R.drawable.round_arrow_drop_down_24),
234+
contentDescription = "Dropdown Icon"
235+
)
236+
}
237+
DropdownMenu(
238+
expanded = isDropDownExpanded.value,
239+
onDismissRequest = {
240+
isDropDownExpanded.value = false
241+
}) {
242+
items.forEachIndexed { index, item ->
243+
DropdownMenuItem(
244+
text = {
245+
Text(text = item)
246+
},
247+
onClick = {
248+
isDropDownExpanded.value = false
249+
itemPosition.intValue = index
250+
onClick(item)
251+
})
252+
}
253+
}
254+
}
255+
256+
}
257+
}

0 commit comments

Comments
 (0)