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..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 @@ -18,6 +18,7 @@ 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 @@ -48,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) @@ -120,4 +120,25 @@ 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/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 0a37a73b..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 @@ -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 = 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