Skip to content

Commit 198e908

Browse files
committed
Implement outpainting
1 parent 6cbc7c1 commit 198e908

File tree

6 files changed

+63
-6
lines changed

6 files changed

+63
-6
lines changed

ai-catalog/samples/imagen-editing/src/main/java/com/android/ai/samples/imagenediting/data/ImagenEditingDataSource.kt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,6 @@ class ImagenEditingDataSource @Inject constructor() {
4949
const val IMAGEN_MODEL_NAME = "imagen-4.0-ultra-generate-001"
5050
const val IMAGEN_EDITING_MODEL_NAME = "imagen-3.0-capability-001"
5151
const val DEFAULT_EDIT_STEPS = 50
52-
const val DEFAULT_STYLE_STRENGTH = 1
5352
}
5453

5554
@OptIn(PublicPreviewAPI::class)

ai-catalog/samples/imagen-editing/src/main/java/com/android/ai/samples/imagenediting/ui/ImagenEditingMaskEditor.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ fun ImagenEditingMaskEditor(sourceBitmap: Bitmap, onMaskFinalized: (Bitmap) -> U
109109
bitmap = sourceBitmap.asImageBitmap(),
110110
contentDescription = stringResource(R.string.editing_image_to_mask),
111111
modifier = Modifier.fillMaxSize(),
112-
contentScale = ContentScale.Crop,
112+
contentScale = ContentScale.Fit,
113113
)
114114
Canvas(modifier = Modifier.fillMaxSize()) {
115115
val canvasWidth = size.width

ai-catalog/samples/imagen-editing/src/main/java/com/android/ai/samples/imagenediting/ui/ImagenEditingScreen.kt

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ import com.android.ai.samples.imagenediting.R
6666
import com.android.ai.uicomponent.GenerateButton
6767
import com.android.ai.uicomponent.SampleDetailTopAppBar
6868
import com.android.ai.uicomponent.TextInput
69+
import com.google.firebase.ai.type.Dimensions
6970

7071
@OptIn(ExperimentalMaterial3Api::class)
7172
@Composable
@@ -80,6 +81,7 @@ fun ImagenEditingScreen(viewModel: ImagenEditingViewModel = hiltViewModel()) {
8081
bitmapForMasking = bitmapForMasking,
8182
onGenerateClick = viewModel::generateImage,
8283
onInpaintClick = { source, mask, prompt -> viewModel.inpaintImage(source, mask, prompt) },
84+
onOutpaintClick = { source, targetDimensions, prompt -> viewModel.outPaintImage(source, targetDimensions, prompt) },
8385
onImageMaskReady = { source, mask -> viewModel.onImageMaskReady(source, mask) },
8486
onCancelMasking = viewModel::onCancelMasking,
8587
modifier = Modifier.fillMaxSize(),
@@ -94,6 +96,7 @@ private fun ImagenEditingScreenContent(
9496
bitmapForMasking: Bitmap?,
9597
onGenerateClick: (String) -> Unit,
9698
onInpaintClick: (source: Bitmap, mask: Bitmap, prompt: String) -> Unit,
99+
onOutpaintClick: (source: Bitmap, targetDimensions: Dimensions, prompt: String) -> Unit,
97100
onImageMaskReady: (source: Bitmap, mask: Bitmap) -> Unit,
98101
onCancelMasking: () -> Unit,
99102
modifier: Modifier = Modifier,
@@ -132,6 +135,26 @@ private fun ImagenEditingScreenContent(
132135
.fillMaxSize(),
133136
contentAlignment = Alignment.Center,
134137
) {
138+
val keyboardController = LocalSoftwareKeyboardController.current
139+
if (uiState is ImagenEditingUIState.ImageGenerated) {
140+
val textFieldState = rememberTextFieldState()
141+
val originalWidth = uiState.bitmap.width
142+
val originalHeight = uiState.bitmap.height
143+
144+
// Don't exceed 4500x4500
145+
val targetWidth = (originalWidth * 2).coerceAtMost(4500)
146+
val targetHeight = (originalHeight * 2).coerceAtMost(4500)
147+
val targetDimensions = Dimensions(targetWidth, targetHeight)
148+
149+
TextField(
150+
textFieldState,
151+
ImageEditMode.OUTPAINT,
152+
isGenerating,
153+
onGenerateClick = { prompt -> onOutpaintClick(uiState.bitmap, targetDimensions, prompt) },
154+
keyboardController,
155+
placeholder = stringResource(R.string.describe_how_to_expand_image),
156+
)
157+
}
135158
Box(
136159
Modifier
137160
.padding(16.dp)
@@ -147,7 +170,6 @@ private fun ImagenEditingScreenContent(
147170
.background(ShaderBrush(imageShader)),
148171
contentAlignment = Alignment.Center,
149172
) {
150-
val keyboardController = LocalSoftwareKeyboardController.current
151173

152174
when (uiState) {
153175
is ImagenEditingUIState.Initial -> {
@@ -163,6 +185,7 @@ private fun ImagenEditingScreenContent(
163185

164186
TextField(
165187
textFieldState,
188+
ImageEditMode.GENERATE,
166189
isGenerating,
167190
onGenerateClick,
168191
keyboardController,
@@ -207,11 +230,12 @@ private fun ImagenEditingScreenContent(
207230
Image(
208231
bitmap = uiState.bitmap.asImageBitmap(),
209232
contentDescription = uiState.contentDescription,
210-
contentScale = ContentScale.Crop,
233+
contentScale = ContentScale.Fit,
211234
modifier = Modifier.fillMaxSize(),
212235
)
213236
TextField(
214237
textFieldState,
238+
ImageEditMode.GENERATE,
215239
isGenerating,
216240
onGenerateClick,
217241
keyboardController,
@@ -240,6 +264,7 @@ private fun ImagenEditingScreenContent(
240264

241265
TextField(
242266
textFieldState = textFieldState,
267+
imageEditMode = ImageEditMode.INPAINT,
243268
isGenerating = isGenerating,
244269
onGenerateClick = { prompt -> onInpaintClick(uiState.originalBitmap, uiState.maskBitmap, prompt) },
245270
keyboardController,
@@ -257,6 +282,7 @@ private fun ImagenEditingScreenContent(
257282
@Composable
258283
private fun BoxScope.TextField(
259284
textFieldState: TextFieldState,
285+
imageEditMode: ImageEditMode,
260286
isGenerating: Boolean,
261287
onGenerateClick: (String) -> Unit,
262288
keyboardController: SoftwareKeyboardController?,
@@ -268,7 +294,11 @@ private fun BoxScope.TextField(
268294
primaryButton = {
269295
GenerateButton(
270296
text = "",
271-
icon = painterResource(id = com.android.ai.uicomponent.R.drawable.ic_ai_img),
297+
icon = when (imageEditMode) {
298+
ImageEditMode.GENERATE -> painterResource(com.android.ai.uicomponent.R.drawable.ic_ai_send)
299+
ImageEditMode.INPAINT -> painterResource(com.android.ai.uicomponent.R.drawable.ic_ai_img)
300+
ImageEditMode.OUTPAINT -> painterResource(com.android.ai.uicomponent.R.drawable.ic_ai_bg)
301+
},
272302
modifier = Modifier
273303
.width(72.dp)
274304
.height(55.dp)
@@ -286,3 +316,10 @@ private fun BoxScope.TextField(
286316
.align(Alignment.BottomCenter),
287317
)
288318
}
319+
320+
enum class ImageEditMode {
321+
INPAINT,
322+
OUTPAINT,
323+
GENERATE,
324+
}
325+

ai-catalog/samples/imagen-editing/src/main/java/com/android/ai/samples/imagenediting/ui/ImagenEditingUIState.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ sealed interface ImagenEditingUIState {
2323
data object Loading : ImagenEditingUIState
2424
data class ImageGenerated(
2525
val bitmap: Bitmap,
26-
val dimensions: Dimensions,
26+
val dimensions: Dimensions = Dimensions(bitmap.width, bitmap.height),
2727
val contentDescription: String,
2828
) : ImagenEditingUIState
2929

ai-catalog/samples/imagen-editing/src/main/java/com/android/ai/samples/imagenediting/ui/ImagenEditingViewModel.kt

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import android.util.Log
2020
import androidx.lifecycle.ViewModel
2121
import androidx.lifecycle.viewModelScope
2222
import com.android.ai.samples.imagenediting.data.ImagenEditingDataSource
23+
import com.google.firebase.ai.type.Dimensions
2324
import dagger.hilt.android.lifecycle.HiltViewModel
2425
import javax.inject.Inject
2526
import kotlinx.coroutines.flow.MutableStateFlow
@@ -73,6 +74,25 @@ class ImagenEditingViewModel @Inject constructor(private val imagenDataSource: I
7374
}
7475
}
7576

77+
fun outPaintImage(sourceImage: Bitmap, targetDimensions: Dimensions?, prompt: String) {
78+
_uiState.value = ImagenEditingUIState.Loading
79+
viewModelScope.launch {
80+
try {
81+
val outpaintedImage = imagenDataSource.outpaintImage(
82+
sourceImage = sourceImage,
83+
targetDimensions = targetDimensions ?: Dimensions(sourceImage.width * 2, sourceImage.height * 2),
84+
prompt = prompt,
85+
)
86+
_uiState.value = ImagenEditingUIState.ImageGenerated(
87+
bitmap = outpaintedImage,
88+
contentDescription = "Outpainted image based on prompt: $prompt",
89+
)
90+
} catch (e: Exception) {
91+
_uiState.value = ImagenEditingUIState.Error(e.localizedMessage ?: "An unknown error occurred during outpainting")
92+
}
93+
}
94+
}
95+
7696
fun onImageMaskReady(originalBitmap: Bitmap, maskBitmap: Bitmap) {
7797
val originalContentDescription = (_uiState.value as? ImagenEditingUIState.ImageGenerated)?.contentDescription ?: "Edited image"
7898
_uiState.value = ImagenEditingUIState.ImageMasked(

ai-catalog/samples/imagen-editing/src/main/res/values/strings.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
<string name="undo_the_mask">Undo the mask</string>
3939
<string name="save_the_mask">Save the mask</string>
4040
<string name="describe_the_image_to_generate">describe the image to generate</string>
41+
<string name="describe_how_to_expand_image">describe the how to expand this image</string>
4142
<string name="generate_an_image_to_edit">Generate an image to edit</string>
4243
<string name="describe_the_image_to_in_paint">describe the image to in-paint</string>
4344
</resources>

0 commit comments

Comments
 (0)