Skip to content

Commit bb8226e

Browse files
authored
(A1111) Img2Img inpaint (#147)
* Draw in paint screen * InPaint parameters form * Implement proper cropping * Fix inpaint overlay * InPaint prototype ready * Translations * Handle mode
1 parent 15df32c commit bb8226e

File tree

33 files changed

+1387
-83
lines changed

33 files changed

+1387
-83
lines changed

data/src/main/java/com/shifthackz/aisdv1/data/mappers/ImageToImagePayloadMappers.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,13 @@ import java.util.Date
1111
fun ImageToImagePayload.mapToRequest(): ImageToImageRequest = with(this) {
1212
ImageToImageRequest(
1313
initImages = listOf(base64Image),
14+
includeInitImages = true,
15+
mask = base64MaskImage.takeIf(String::isNotBlank),
16+
inPaintingMaskInvert = inPaintingMaskInvert,
17+
inPaintFullResPadding = inPaintFullResPadding,
18+
inPaintingFill = inPaintingFill,
19+
inPaintFullRes = inPaintFullRes,
20+
maskBlur = maskBlur,
1421
denoisingStrength = denoisingStrength,
1522
prompt = prompt,
1623
negativePrompt = negativePrompt,

dependencies.gradle

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ ext {
2828
cryptoVersion = '1.0.0'
2929
onnxruntimeVersion = '1.16.3'
3030
catppuccinVersion = '0.1.1'
31+
composeGesturesVersion = '3.1'
32+
composeEasyCropVersion = '0.1.1'
3133

3234
testJunitVersion = '4.13.2'
3335

@@ -92,6 +94,8 @@ ext {
9294
catppuccinLegacy : "com.github.ShiftHackZ.Catppuccin-Android-Library:palette-legacy:$catppuccinVersion",
9395
catppuccinCompose : "com.github.ShiftHackZ.Catppuccin-Android-Library:compose:$catppuccinVersion",
9496
catppuccinSplashscreen: "com.github.ShiftHackZ.Catppuccin-Android-Library:splashscreen:$catppuccinVersion",
97+
composeGestures : "com.github.SmartToolFactory:Compose-Extended-Gestures:$composeGesturesVersion",
98+
composeEasyCrop : "io.github.mr0xf00:easycrop:$composeEasyCropVersion",
9599
]
96100
log = [
97101
timber: "com.jakewharton.timber:timber:$timberVersion",

domain/src/main/java/com/shifthackz/aisdv1/domain/entity/ImageToImagePayload.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package com.shifthackz.aisdv1.domain.entity
22

33
data class ImageToImagePayload(
44
val base64Image: String,
5+
val base64MaskImage: String,
56
val denoisingStrength: Float,
67
val prompt: String,
78
val negativePrompt: String,
@@ -16,4 +17,9 @@ data class ImageToImagePayload(
1617
val sampler: String,
1718
val nsfw: Boolean,
1819
val batchCount: Int,
20+
val inPaintingMaskInvert: Int,
21+
val inPaintFullResPadding: Int,
22+
val inPaintingFill: Int,
23+
val inPaintFullRes: Boolean,
24+
val maskBlur: Int,
1925
)

network/src/main/java/com/shifthackz/aisdv1/network/request/ImageToImageRequest.kt

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,20 @@ import com.google.gson.annotations.SerializedName
55
data class ImageToImageRequest(
66
@SerializedName("init_images")
77
val initImages: List<String>,
8+
@SerializedName("include_init_images")
9+
val includeInitImages: Boolean,
10+
@SerializedName("mask")
11+
val mask: String?,
12+
@SerializedName("inpainting_mask_invert")
13+
val inPaintingMaskInvert: Int?,
14+
@SerializedName("inpaint_full_res_padding")
15+
val inPaintFullResPadding: Int?,
16+
@SerializedName("inpainting_fill")
17+
val inPaintingFill: Int?,
18+
@SerializedName("inpaint_full_res")
19+
val inPaintFullRes: Boolean?,
20+
@SerializedName("mask_blur")
21+
val maskBlur: Int?,
822
@SerializedName("denoising_strength")
923
val denoisingStrength: Float,
1024
@SerializedName("prompt")

presentation/build.gradle

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,5 +43,7 @@ dependencies {
4343
implementation ui.catppuccinCompose
4444
implementation ui.dayNightSwitch
4545
implementation ui.catppuccinSplashscreen
46+
implementation ui.composeGestures
47+
implementation ui.composeEasyCrop
4648
implementation "androidx.exifinterface:exifinterface:1.3.6"
4749
}

presentation/src/main/java/com/shifthackz/aisdv1/presentation/core/GenerationMviIntent.kt

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.shifthackz.aisdv1.presentation.core
22

3+
import android.graphics.Bitmap
34
import com.shifthackz.aisdv1.domain.entity.AiGenerationResult
45
import com.shifthackz.aisdv1.domain.entity.OpenAiModel
56
import com.shifthackz.aisdv1.domain.entity.OpenAiQuality
@@ -86,13 +87,17 @@ sealed interface GenerationMviIntent : MviIntent {
8687

8788
sealed interface ImageToImageIntent : GenerationMviIntent {
8889

90+
data object InPaint : ImageToImageIntent
91+
8992
data object FetchRandomPhoto : ImageToImageIntent
9093

9194
data object ClearImageInput : ImageToImageIntent
9295

9396
data class UpdateDenoisingStrength(val value: Float) : ImageToImageIntent
9497

95-
data class UpdateImage(val result: PickedResult) : ImageToImageIntent
98+
data class UpdateImage(val bitmap: Bitmap) : ImageToImageIntent
99+
100+
data class CropImage(val result: PickedResult) : ImageToImageIntent
96101

97102
enum class Pick : ImageToImageIntent {
98103
Camera, Gallery

presentation/src/main/java/com/shifthackz/aisdv1/presentation/di/UiUtilsModule.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import com.shifthackz.aisdv1.presentation.screen.debug.DebugMenuAccessor
77
import com.shifthackz.aisdv1.presentation.screen.gallery.detail.GalleryDetailBitmapExporter
88
import com.shifthackz.aisdv1.presentation.screen.gallery.detail.GalleryDetailSharing
99
import com.shifthackz.aisdv1.presentation.screen.gallery.list.GalleryExporter
10+
import com.shifthackz.aisdv1.presentation.screen.inpaint.InPaintStateProducer
1011
import org.koin.android.ext.koin.androidContext
1112
import org.koin.core.module.dsl.factoryOf
1213
import org.koin.core.module.dsl.singleOf
@@ -20,4 +21,5 @@ internal val uiUtilsModule = module {
2021
factoryOf(::GalleryDetailSharing)
2122
singleOf(::GenerationFormUpdateEvent)
2223
singleOf(::DebugMenuAccessor)
24+
singleOf(::InPaintStateProducer)
2325
}

presentation/src/main/java/com/shifthackz/aisdv1/presentation/di/ViewModelModule.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import com.shifthackz.aisdv1.presentation.screen.gallery.detail.GalleryDetailVie
1111
import com.shifthackz.aisdv1.presentation.screen.gallery.list.GalleryViewModel
1212
import com.shifthackz.aisdv1.presentation.screen.home.HomeNavigationViewModel
1313
import com.shifthackz.aisdv1.presentation.screen.img2img.ImageToImageViewModel
14+
import com.shifthackz.aisdv1.presentation.screen.inpaint.InPaintViewModel
1415
import com.shifthackz.aisdv1.presentation.screen.loader.ConfigurationLoaderViewModel
1516
import com.shifthackz.aisdv1.presentation.screen.settings.SettingsViewModel
1617
import com.shifthackz.aisdv1.presentation.screen.setup.ServerSetupLaunchSource
@@ -40,6 +41,7 @@ val viewModelModule = module {
4041
viewModelOf(::ExtrasViewModel)
4142
viewModelOf(::EmbeddingViewModel)
4243
viewModelOf(::EditTagViewModel)
44+
viewModelOf(::InPaintViewModel)
4345

4446
viewModel { parameters ->
4547
val launchSource = ServerSetupLaunchSource.fromKey(parameters.get())

presentation/src/main/java/com/shifthackz/aisdv1/presentation/modal/ModalRenderer.kt

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ import com.shifthackz.aisdv1.core.model.UiText
1616
import com.shifthackz.aisdv1.core.model.asUiText
1717
import com.shifthackz.aisdv1.presentation.R
1818
import com.shifthackz.aisdv1.presentation.core.GenerationMviIntent
19+
import com.shifthackz.aisdv1.presentation.core.ImageToImageIntent
20+
import com.shifthackz.aisdv1.presentation.modal.crop.CropImageModal
1921
import com.shifthackz.aisdv1.presentation.modal.embedding.EmbeddingScreen
2022
import com.shifthackz.aisdv1.presentation.modal.extras.ExtrasScreen
2123
import com.shifthackz.aisdv1.presentation.modal.history.InputHistoryScreen
@@ -24,6 +26,7 @@ import com.shifthackz.aisdv1.presentation.modal.tag.EditTagDialog
2426
import com.shifthackz.aisdv1.presentation.model.Modal
2527
import com.shifthackz.aisdv1.presentation.screen.gallery.detail.GalleryDetailIntent
2628
import com.shifthackz.aisdv1.presentation.screen.gallery.list.GalleryIntent
29+
import com.shifthackz.aisdv1.presentation.screen.inpaint.InPaintIntent
2730
import com.shifthackz.aisdv1.presentation.screen.settings.SettingsIntent
2831
import com.shifthackz.aisdv1.presentation.screen.setup.ServerSetupIntent
2932
import com.shifthackz.aisdv1.presentation.widget.dialog.DecisionInteractiveDialog
@@ -46,6 +49,7 @@ fun ModalRenderer(
4649
processIntent(GenerationMviIntent.SetModal(Modal.None))
4750
processIntent(GalleryIntent.DismissDialog)
4851
processIntent(GalleryDetailIntent.DismissDialog)
52+
processIntent(InPaintIntent.ScreenModal.Dismiss)
4953
}
5054
when (screenModal) {
5155
Modal.None -> Unit
@@ -223,5 +227,20 @@ fun ModalRenderer(
223227
onConfirmAction = { processIntent(ServerSetupIntent.LocalModel.DeleteConfirm(screenModal.model)) },
224228
onDismissRequest = dismiss,
225229
)
230+
231+
Modal.ClearInPaintConfirm -> DecisionInteractiveDialog(
232+
title = R.string.interaction_in_paint_clear_title.asUiText(),
233+
text = R.string.interaction_in_paint_clear_title.asUiText(),
234+
confirmActionResId = R.string.yes,
235+
dismissActionResId = R.string.no,
236+
onConfirmAction = { processIntent(InPaintIntent.Action.Clear) },
237+
onDismissRequest = dismiss,
238+
)
239+
240+
is Modal.Image.Crop -> CropImageModal(
241+
bitmap = screenModal.bitmap,
242+
onDismissRequest = dismiss,
243+
onResult = { processIntent(ImageToImageIntent.UpdateImage(it)) }
244+
)
226245
}
227246
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
package com.shifthackz.aisdv1.presentation.modal.crop
2+
3+
import android.graphics.Bitmap
4+
import androidx.compose.material3.MaterialTheme
5+
import androidx.compose.runtime.Composable
6+
import androidx.compose.runtime.LaunchedEffect
7+
import androidx.compose.ui.graphics.asAndroidBitmap
8+
import androidx.compose.ui.graphics.asImageBitmap
9+
import androidx.compose.ui.platform.LocalContext
10+
import com.mr0xf00.easycrop.AspectRatio
11+
import com.mr0xf00.easycrop.CropError
12+
import com.mr0xf00.easycrop.CropResult
13+
import com.mr0xf00.easycrop.CropperStyle
14+
import com.mr0xf00.easycrop.CropperStyleGuidelines
15+
import com.mr0xf00.easycrop.RectCropShape
16+
import com.mr0xf00.easycrop.crop
17+
import com.mr0xf00.easycrop.rememberImageCropper
18+
import com.mr0xf00.easycrop.ui.ImageCropperDialog
19+
import com.shifthackz.aisdv1.core.extensions.showToast
20+
21+
@Composable
22+
fun CropImageModal(
23+
bitmap: Bitmap,
24+
onResult: (Bitmap) -> Unit = {},
25+
onDismissRequest: () -> Unit = {},
26+
) {
27+
val imageCropper = rememberImageCropper()
28+
val state = imageCropper.cropState
29+
state?.let {
30+
LaunchedEffect(Unit) {
31+
it.region = when {
32+
it.region.height > it.region.width -> it.region.copy(
33+
bottom = it.region.width,
34+
)
35+
36+
it.region.width > it.region.height -> it.region.copy(
37+
right = it.region.height
38+
)
39+
40+
else -> it.region
41+
}
42+
it.aspectLock = true
43+
}
44+
ImageCropperDialog(
45+
state = it,
46+
style = CropperStyle(
47+
backgroundColor = MaterialTheme.colorScheme.background,
48+
overlay = MaterialTheme.colorScheme.surface,
49+
guidelines = CropperStyleGuidelines(),
50+
shapes = listOf(RectCropShape),
51+
aspects = listOf(AspectRatio(1, 1)),
52+
),
53+
)
54+
}
55+
val context = LocalContext.current
56+
LaunchedEffect(Unit) {
57+
when (val result = imageCropper.crop(bmp = bitmap.asImageBitmap())) {
58+
is CropResult.Success -> result.bitmap.asAndroidBitmap().let(onResult::invoke)
59+
60+
CropError.LoadingError -> {
61+
context.showToast("Loading error")
62+
onDismissRequest()
63+
}
64+
65+
CropError.SavingError -> {
66+
context.showToast("Saving error")
67+
onDismissRequest()
68+
}
69+
70+
CropResult.Cancelled -> onDismissRequest()
71+
}
72+
}
73+
}

0 commit comments

Comments
 (0)