From 1d233d9b19adff49d0a97a3cfcf5c86e0e61d067 Mon Sep 17 00:00:00 2001 From: Mozart Louis Date: Mon, 6 Oct 2025 13:31:14 -0400 Subject: [PATCH 1/3] Implement Outpainted / Generative Expand to Imagen Editing Sample --- .../app/src/main/res/values/strings.xml | 2 +- .../data/ImagenEditingDataSource.kt | 28 +++++++++++++++++++ .../imagenediting/ui/ImagenEditingUIState.kt | 2 ++ 3 files changed, 31 insertions(+), 1 deletion(-) diff --git a/ai-catalog/app/src/main/res/values/strings.xml b/ai-catalog/app/src/main/res/values/strings.xml index ef6355ad..50caf875 100644 --- a/ai-catalog/app/src/main/res/values/strings.xml +++ b/ai-catalog/app/src/main/res/values/strings.xml @@ -15,7 +15,7 @@ Open sample Image generation with Imagen Generate images with Imagen, Google image generation model - Imagen Editing using Inpainting + Inpainting & Outpainting with Imagen Generate images and edit only specific areas of a generated image with Inpainting Magic Selfie with Imagen and ML Kit Change the background of your selfies with Imagen and the ML Kit Segmentation API diff --git a/ai-catalog/samples/imagen-editing/src/main/java/com/android/ai/samples/imagenediting/data/ImagenEditingDataSource.kt b/ai-catalog/samples/imagen-editing/src/main/java/com/android/ai/samples/imagenediting/data/ImagenEditingDataSource.kt index 90559012..70e13a36 100644 --- a/ai-catalog/samples/imagen-editing/src/main/java/com/android/ai/samples/imagenediting/data/ImagenEditingDataSource.kt +++ b/ai-catalog/samples/imagen-editing/src/main/java/com/android/ai/samples/imagenediting/data/ImagenEditingDataSource.kt @@ -18,14 +18,17 @@ package com.android.ai.samples.imagenediting.data import android.graphics.Bitmap import com.google.firebase.Firebase import com.google.firebase.ai.ai +import com.google.firebase.ai.type.Dimensions import com.google.firebase.ai.type.GenerativeBackend import com.google.firebase.ai.type.ImagenAspectRatio import com.google.firebase.ai.type.ImagenEditMode import com.google.firebase.ai.type.ImagenEditingConfig import com.google.firebase.ai.type.ImagenGenerationConfig import com.google.firebase.ai.type.ImagenImageFormat +import com.google.firebase.ai.type.ImagenMaskReference import com.google.firebase.ai.type.ImagenRawImage import com.google.firebase.ai.type.ImagenRawMask +import com.google.firebase.ai.type.ImagenStyleReference import com.google.firebase.ai.type.PublicPreviewAPI import com.google.firebase.ai.type.toImagenInlineImage import javax.inject.Inject @@ -120,4 +123,29 @@ class ImagenEditingDataSource @Inject constructor() { ) return imageResponse.images.first().asBitmap() } + + /** + * Outpaints an image to the target dimensions using the Firebase Imagen API. + * This function extends the original image by generating content around it + * based on the provided prompt and target dimensions. + * + * @param sourceImage The original bitmap image to be outpainted. + * @param targetDimensions The desired dimensions of the outpainted image. + * @param prompt An optional text prompt to guide the outpainting process. + * @return The outpainted bitmap image. + */ + @OptIn(PublicPreviewAPI::class) + suspend fun outpaintImage( + sourceImage: Bitmap, + targetDimensions: Dimensions, + prompt: String = "", + ): Bitmap { + val imageResponse = editingModel.outpaintImage( + image = sourceImage.toImagenInlineImage(), + newDimensions = targetDimensions, + prompt = prompt, + ) + + return imageResponse.images.first().asBitmap() + } } diff --git a/ai-catalog/samples/imagen-editing/src/main/java/com/android/ai/samples/imagenediting/ui/ImagenEditingUIState.kt b/ai-catalog/samples/imagen-editing/src/main/java/com/android/ai/samples/imagenediting/ui/ImagenEditingUIState.kt index 0a37a73b..e70a50a0 100644 --- a/ai-catalog/samples/imagen-editing/src/main/java/com/android/ai/samples/imagenediting/ui/ImagenEditingUIState.kt +++ b/ai-catalog/samples/imagen-editing/src/main/java/com/android/ai/samples/imagenediting/ui/ImagenEditingUIState.kt @@ -16,12 +16,14 @@ package com.android.ai.samples.imagenediting.ui import android.graphics.Bitmap +import com.google.firebase.ai.type.Dimensions sealed interface ImagenEditingUIState { data object Initial : ImagenEditingUIState data object Loading : ImagenEditingUIState data class ImageGenerated( val bitmap: Bitmap, + val dimensions: Dimensions, val contentDescription: String, ) : ImagenEditingUIState From 6cbc7c1dba0dbc9c008b1c942043f425ac1f471e Mon Sep 17 00:00:00 2001 From: madebymozart <5649571+madebymozart@users.noreply.github.com> Date: Mon, 6 Oct 2025 17:44:46 +0000 Subject: [PATCH 2/3] =?UTF-8?q?=F0=9F=A4=96=20Apply=20Spotless=20formattin?= =?UTF-8?q?g?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../samples/imagenediting/data/ImagenEditingDataSource.kt | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/ai-catalog/samples/imagen-editing/src/main/java/com/android/ai/samples/imagenediting/data/ImagenEditingDataSource.kt b/ai-catalog/samples/imagen-editing/src/main/java/com/android/ai/samples/imagenediting/data/ImagenEditingDataSource.kt index 70e13a36..cc31c204 100644 --- a/ai-catalog/samples/imagen-editing/src/main/java/com/android/ai/samples/imagenediting/data/ImagenEditingDataSource.kt +++ b/ai-catalog/samples/imagen-editing/src/main/java/com/android/ai/samples/imagenediting/data/ImagenEditingDataSource.kt @@ -25,10 +25,8 @@ import com.google.firebase.ai.type.ImagenEditMode import com.google.firebase.ai.type.ImagenEditingConfig import com.google.firebase.ai.type.ImagenGenerationConfig import com.google.firebase.ai.type.ImagenImageFormat -import com.google.firebase.ai.type.ImagenMaskReference import com.google.firebase.ai.type.ImagenRawImage import com.google.firebase.ai.type.ImagenRawMask -import com.google.firebase.ai.type.ImagenStyleReference import com.google.firebase.ai.type.PublicPreviewAPI import com.google.firebase.ai.type.toImagenInlineImage import javax.inject.Inject @@ -135,11 +133,7 @@ class ImagenEditingDataSource @Inject constructor() { * @return The outpainted bitmap image. */ @OptIn(PublicPreviewAPI::class) - suspend fun outpaintImage( - sourceImage: Bitmap, - targetDimensions: Dimensions, - prompt: String = "", - ): Bitmap { + suspend fun outpaintImage(sourceImage: Bitmap, targetDimensions: Dimensions, prompt: String = ""): Bitmap { val imageResponse = editingModel.outpaintImage( image = sourceImage.toImagenInlineImage(), newDimensions = targetDimensions, From 198e90845de478afa358982a4d9097df7af21706 Mon Sep 17 00:00:00 2001 From: Mozart Louis Date: Mon, 27 Oct 2025 23:32:24 -0400 Subject: [PATCH 3/3] Implement outpainting --- .../data/ImagenEditingDataSource.kt | 1 - .../ui/ImagenEditingMaskEditor.kt | 2 +- .../imagenediting/ui/ImagenEditingScreen.kt | 43 +++++++++++++++++-- .../imagenediting/ui/ImagenEditingUIState.kt | 2 +- .../ui/ImagenEditingViewModel.kt | 20 +++++++++ .../src/main/res/values/strings.xml | 1 + 6 files changed, 63 insertions(+), 6 deletions(-) diff --git a/ai-catalog/samples/imagen-editing/src/main/java/com/android/ai/samples/imagenediting/data/ImagenEditingDataSource.kt b/ai-catalog/samples/imagen-editing/src/main/java/com/android/ai/samples/imagenediting/data/ImagenEditingDataSource.kt index cc31c204..6f65882f 100644 --- a/ai-catalog/samples/imagen-editing/src/main/java/com/android/ai/samples/imagenediting/data/ImagenEditingDataSource.kt +++ b/ai-catalog/samples/imagen-editing/src/main/java/com/android/ai/samples/imagenediting/data/ImagenEditingDataSource.kt @@ -49,7 +49,6 @@ class ImagenEditingDataSource @Inject constructor() { const val IMAGEN_MODEL_NAME = "imagen-4.0-ultra-generate-001" const val IMAGEN_EDITING_MODEL_NAME = "imagen-3.0-capability-001" const val DEFAULT_EDIT_STEPS = 50 - const val DEFAULT_STYLE_STRENGTH = 1 } @OptIn(PublicPreviewAPI::class) diff --git a/ai-catalog/samples/imagen-editing/src/main/java/com/android/ai/samples/imagenediting/ui/ImagenEditingMaskEditor.kt b/ai-catalog/samples/imagen-editing/src/main/java/com/android/ai/samples/imagenediting/ui/ImagenEditingMaskEditor.kt index 6d66f321..a636eb3f 100644 --- a/ai-catalog/samples/imagen-editing/src/main/java/com/android/ai/samples/imagenediting/ui/ImagenEditingMaskEditor.kt +++ b/ai-catalog/samples/imagen-editing/src/main/java/com/android/ai/samples/imagenediting/ui/ImagenEditingMaskEditor.kt @@ -109,7 +109,7 @@ fun ImagenEditingMaskEditor(sourceBitmap: Bitmap, onMaskFinalized: (Bitmap) -> U bitmap = sourceBitmap.asImageBitmap(), contentDescription = stringResource(R.string.editing_image_to_mask), modifier = Modifier.fillMaxSize(), - contentScale = ContentScale.Crop, + contentScale = ContentScale.Fit, ) Canvas(modifier = Modifier.fillMaxSize()) { val canvasWidth = size.width diff --git a/ai-catalog/samples/imagen-editing/src/main/java/com/android/ai/samples/imagenediting/ui/ImagenEditingScreen.kt b/ai-catalog/samples/imagen-editing/src/main/java/com/android/ai/samples/imagenediting/ui/ImagenEditingScreen.kt index d76ff549..aa36dd23 100644 --- a/ai-catalog/samples/imagen-editing/src/main/java/com/android/ai/samples/imagenediting/ui/ImagenEditingScreen.kt +++ b/ai-catalog/samples/imagen-editing/src/main/java/com/android/ai/samples/imagenediting/ui/ImagenEditingScreen.kt @@ -66,6 +66,7 @@ import com.android.ai.samples.imagenediting.R import com.android.ai.uicomponent.GenerateButton import com.android.ai.uicomponent.SampleDetailTopAppBar import com.android.ai.uicomponent.TextInput +import com.google.firebase.ai.type.Dimensions @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -80,6 +81,7 @@ fun ImagenEditingScreen(viewModel: ImagenEditingViewModel = hiltViewModel()) { bitmapForMasking = bitmapForMasking, onGenerateClick = viewModel::generateImage, onInpaintClick = { source, mask, prompt -> viewModel.inpaintImage(source, mask, prompt) }, + onOutpaintClick = { source, targetDimensions, prompt -> viewModel.outPaintImage(source, targetDimensions, prompt) }, onImageMaskReady = { source, mask -> viewModel.onImageMaskReady(source, mask) }, onCancelMasking = viewModel::onCancelMasking, modifier = Modifier.fillMaxSize(), @@ -94,6 +96,7 @@ private fun ImagenEditingScreenContent( bitmapForMasking: Bitmap?, onGenerateClick: (String) -> Unit, onInpaintClick: (source: Bitmap, mask: Bitmap, prompt: String) -> Unit, + onOutpaintClick: (source: Bitmap, targetDimensions: Dimensions, prompt: String) -> Unit, onImageMaskReady: (source: Bitmap, mask: Bitmap) -> Unit, onCancelMasking: () -> Unit, modifier: Modifier = Modifier, @@ -132,6 +135,26 @@ private fun ImagenEditingScreenContent( .fillMaxSize(), contentAlignment = Alignment.Center, ) { + val keyboardController = LocalSoftwareKeyboardController.current + if (uiState is ImagenEditingUIState.ImageGenerated) { + val textFieldState = rememberTextFieldState() + val originalWidth = uiState.bitmap.width + val originalHeight = uiState.bitmap.height + + // Don't exceed 4500x4500 + val targetWidth = (originalWidth * 2).coerceAtMost(4500) + val targetHeight = (originalHeight * 2).coerceAtMost(4500) + val targetDimensions = Dimensions(targetWidth, targetHeight) + + TextField( + textFieldState, + ImageEditMode.OUTPAINT, + isGenerating, + onGenerateClick = { prompt -> onOutpaintClick(uiState.bitmap, targetDimensions, prompt) }, + keyboardController, + placeholder = stringResource(R.string.describe_how_to_expand_image), + ) + } Box( Modifier .padding(16.dp) @@ -147,7 +170,6 @@ private fun ImagenEditingScreenContent( .background(ShaderBrush(imageShader)), contentAlignment = Alignment.Center, ) { - val keyboardController = LocalSoftwareKeyboardController.current when (uiState) { is ImagenEditingUIState.Initial -> { @@ -163,6 +185,7 @@ private fun ImagenEditingScreenContent( TextField( textFieldState, + ImageEditMode.GENERATE, isGenerating, onGenerateClick, keyboardController, @@ -207,11 +230,12 @@ private fun ImagenEditingScreenContent( Image( bitmap = uiState.bitmap.asImageBitmap(), contentDescription = uiState.contentDescription, - contentScale = ContentScale.Crop, + contentScale = ContentScale.Fit, modifier = Modifier.fillMaxSize(), ) TextField( textFieldState, + ImageEditMode.GENERATE, isGenerating, onGenerateClick, keyboardController, @@ -240,6 +264,7 @@ private fun ImagenEditingScreenContent( TextField( textFieldState = textFieldState, + imageEditMode = ImageEditMode.INPAINT, isGenerating = isGenerating, onGenerateClick = { prompt -> onInpaintClick(uiState.originalBitmap, uiState.maskBitmap, prompt) }, keyboardController, @@ -257,6 +282,7 @@ private fun ImagenEditingScreenContent( @Composable private fun BoxScope.TextField( textFieldState: TextFieldState, + imageEditMode: ImageEditMode, isGenerating: Boolean, onGenerateClick: (String) -> Unit, keyboardController: SoftwareKeyboardController?, @@ -268,7 +294,11 @@ private fun BoxScope.TextField( primaryButton = { GenerateButton( text = "", - icon = painterResource(id = com.android.ai.uicomponent.R.drawable.ic_ai_img), + icon = when (imageEditMode) { + ImageEditMode.GENERATE -> painterResource(com.android.ai.uicomponent.R.drawable.ic_ai_send) + ImageEditMode.INPAINT -> painterResource(com.android.ai.uicomponent.R.drawable.ic_ai_img) + ImageEditMode.OUTPAINT -> painterResource(com.android.ai.uicomponent.R.drawable.ic_ai_bg) + }, modifier = Modifier .width(72.dp) .height(55.dp) @@ -286,3 +316,10 @@ private fun BoxScope.TextField( .align(Alignment.BottomCenter), ) } + +enum class ImageEditMode { + INPAINT, + OUTPAINT, + GENERATE, +} + diff --git a/ai-catalog/samples/imagen-editing/src/main/java/com/android/ai/samples/imagenediting/ui/ImagenEditingUIState.kt b/ai-catalog/samples/imagen-editing/src/main/java/com/android/ai/samples/imagenediting/ui/ImagenEditingUIState.kt index e70a50a0..0c5d6bb7 100644 --- a/ai-catalog/samples/imagen-editing/src/main/java/com/android/ai/samples/imagenediting/ui/ImagenEditingUIState.kt +++ b/ai-catalog/samples/imagen-editing/src/main/java/com/android/ai/samples/imagenediting/ui/ImagenEditingUIState.kt @@ -23,7 +23,7 @@ sealed interface ImagenEditingUIState { data object Loading : ImagenEditingUIState data class ImageGenerated( val bitmap: Bitmap, - val dimensions: Dimensions, + val dimensions: Dimensions = Dimensions(bitmap.width, bitmap.height), val contentDescription: String, ) : ImagenEditingUIState diff --git a/ai-catalog/samples/imagen-editing/src/main/java/com/android/ai/samples/imagenediting/ui/ImagenEditingViewModel.kt b/ai-catalog/samples/imagen-editing/src/main/java/com/android/ai/samples/imagenediting/ui/ImagenEditingViewModel.kt index ce6f8620..48296c92 100644 --- a/ai-catalog/samples/imagen-editing/src/main/java/com/android/ai/samples/imagenediting/ui/ImagenEditingViewModel.kt +++ b/ai-catalog/samples/imagen-editing/src/main/java/com/android/ai/samples/imagenediting/ui/ImagenEditingViewModel.kt @@ -20,6 +20,7 @@ import android.util.Log import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.android.ai.samples.imagenediting.data.ImagenEditingDataSource +import com.google.firebase.ai.type.Dimensions import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject import kotlinx.coroutines.flow.MutableStateFlow @@ -73,6 +74,25 @@ class ImagenEditingViewModel @Inject constructor(private val imagenDataSource: I } } + fun outPaintImage(sourceImage: Bitmap, targetDimensions: Dimensions?, prompt: String) { + _uiState.value = ImagenEditingUIState.Loading + viewModelScope.launch { + try { + val outpaintedImage = imagenDataSource.outpaintImage( + sourceImage = sourceImage, + targetDimensions = targetDimensions ?: Dimensions(sourceImage.width * 2, sourceImage.height * 2), + prompt = prompt, + ) + _uiState.value = ImagenEditingUIState.ImageGenerated( + bitmap = outpaintedImage, + contentDescription = "Outpainted image based on prompt: $prompt", + ) + } catch (e: Exception) { + _uiState.value = ImagenEditingUIState.Error(e.localizedMessage ?: "An unknown error occurred during outpainting") + } + } + } + fun onImageMaskReady(originalBitmap: Bitmap, maskBitmap: Bitmap) { val originalContentDescription = (_uiState.value as? ImagenEditingUIState.ImageGenerated)?.contentDescription ?: "Edited image" _uiState.value = ImagenEditingUIState.ImageMasked( diff --git a/ai-catalog/samples/imagen-editing/src/main/res/values/strings.xml b/ai-catalog/samples/imagen-editing/src/main/res/values/strings.xml index 4f7efff1..112d7335 100644 --- a/ai-catalog/samples/imagen-editing/src/main/res/values/strings.xml +++ b/ai-catalog/samples/imagen-editing/src/main/res/values/strings.xml @@ -38,6 +38,7 @@ Undo the mask Save the mask describe the image to generate + describe the how to expand this image Generate an image to edit describe the image to in-paint \ No newline at end of file