Imagen으로 이미지 생성

Imagen은 이미지 생성 모델입니다. 사용자 프로필의 맞춤 아바타를 생성하거나 맞춤설정된 시각적 애셋을 기존 화면 흐름에 통합하여 사용자 참여도를 높이는 데 사용할 수 있습니다.

Imagen 모델에 Firebase AI Logic SDK 를 사용하여 Android 앱에서 액세스할 수 있습니다. Imagen 모델은 두 가지 Firebase AI Logic API 제공업체인 Gemini Developer API (대부분의 개발자에게 권장됨)와 Vertex AI를 사용하여 사용할 수 있습니다.

Gemini Developer API에 액세스하기 위한 Firebase AI Logic 통합 아키텍처를 보여주는 다이어그램 Android 앱은 Firebase Android SDK를 사용하여 Firebase에 연결합니다. 그러면 Firebase가 클라우드 내에서 Gemini Pro 및 Flash에 액세스하는 Gemini Developer API와 상호작용합니다.
그림 1. Firebase AI Logic을 사용하여 Imagen 모델에 액세스합니다.

프롬프트 실험

이상적인 프롬프트를 만들려면 여러 번 시도해야 하는 경우가 많습니다. 프롬프트 디자인 및 프로토타입 제작을 위한 IDE인 Google AI Studio에서 이미지 프롬프트를 실험해 볼 수 있습니다. 프롬프트를 개선하는 방법에 관한 도움말은 프롬프트 및 이미지 속성 가이드를 검토하세요.

선사 시대 숲에서 파란색 배낭을 멘 티라노사우루스의 생성된 이미지를 4개 표시하는 Google AI Studio 인터페이스의 스크린샷
그림 2. Google AI Studio를 사용하면 이미지 생성 프롬프트를 미세 조정할 수 있습니다.

Firebase 프로젝트 설정 및 앱 연결

Firebase 문서의 단계에 따라 Android 프로젝트에 Firebase를 추가합니다.

Gradle 종속 항목 추가

build.gradle 파일에 다음 종속 항목을 추가합니다.

dependencies {
  // Import the BoM for the Firebase platform
  implementation(platform("com.google.firebase:firebase-bom:34.12.0"))

  // Add the dependency for the Firebase AI Logic library. When using the BoM,
  // you don't specify versions in Firebase library dependencies
  implementation("com.google.firebase:firebase-ai")
}

이미지 생성

Android 앱에서 이미지를 생성하려면 먼저 선택적 구성으로 ImagenModel을 인스턴스화합니다.

generationConfig 매개변수를 사용하여 부정적 프롬프트, 이미지 수, 출력 이미지 가로세로 비율, 이미지 형식을 정의하고 워터마크를 추가할 수 있습니다. safetySettings 매개변수를 사용하여 안전 및 인물 필터를 구성할 수 있습니다.

Kotlin

val config = ImagenGenerationConfig(
    numberOfImages = 2,
    aspectRatio = ImagenAspectRatio.LANDSCAPE_16x9,
    imageFormat = ImagenImageFormat.jpeg(compressionQuality = 100),
    addWatermark = false,
)

// Initialize the Gemini Developer API backend service
// For Vertex AI use Firebase.ai(backend = GenerativeBackend.vertexAI())
val model = Firebase.ai(backend = GenerativeBackend.googleAI()).imagenModel(
    modelName = "imagen-4.0-generate-001",
    generationConfig = config,
    safetySettings = ImagenSafetySettings(
        safetyFilterLevel = ImagenSafetyFilterLevel.BLOCK_LOW_AND_ABOVE,
        personFilterLevel = ImagenPersonFilterLevel.BLOCK_ALL
    ),
)

자바

ImagenGenerationConfig config = new ImagenGenerationConfig.Builder()
        .setNumberOfImages(2)
        .setAspectRatio(ImagenAspectRatio.LANDSCAPE_16x9)
        .setImageFormat(ImagenImageFormat.jpeg(100))
        .setAddWatermark(false)
        .build();

// For Vertex AI use Firebase.ai(backend = GenerativeBackend.vertexAI())
ImagenModelFutures model = ImagenModelFutures.from(
        FirebaseAI.getInstance(GenerativeBackend.googleAI()).imagenModel(
                "imagen-4.0-generate-001",
                config,
                new ImagenSafetySettings(
                        ImagenSafetyFilterLevel.BLOCK_LOW_AND_ABOVE,
                        ImagenPersonFilterLevel.BLOCK_ALL))
);

ImagenModel이 인스턴스화되면 generateImages를 호출하여 이미지를 생성할 수 있습니다.

Kotlin

val imageResponse = model.generateImages(
    prompt = "A hyper realistic picture of a t-rex with a blue bagpack in a prehistoric forest",
)
val image = imageResponse.images.first()
val bitmapImage = image.asBitmap()

자바

ListenableFuture<ImagenGenerationResponse<ImagenInlineImage>> futureResponse =
        model.generateImages(
                "A hyper realistic picture of a t-rex with a blue bagpack in a prehistoric forest");

try {
    ImagenGenerationResponse<ImagenInlineImage> imageResponse = futureResponse.get();
    List<ImagenInlineImage> images = null;
    if (imageResponse != null) {
        images = imageResponse.getImages();
    }
    if (images != null && !images.isEmpty()) {
        ImagenInlineImage image = images.get(0);
        Bitmap bitmapImage = image.asBitmap();
        // Use bitmapImage
    }
} catch (ExecutionException | InterruptedException e) {
    e.printStackTrace();
}

Imagen으로 이미지 수정

Firebase AI Logic SDK는 Imagen 모델을 통해 고급 이미지 수정 기능을 제공하므로 다음 작업을 할 수 있습니다.

  • 마스크를 기반으로 이미지 수정 : 객체 삽입 또는 삭제, 이미지 콘텐츠를 원래 경계 너머로 확장, 배경 변경과 같은 작업이 포함됩니다.
  • 이미지 맞춤설정 : 특정 스타일 (패턴, 질감 또는 아티스트 스타일)을 적용하거나, 다양한 피사체 (예: 제품, 사람 또는 동물)에 집중하거나, 다양한 컨트롤 (예: 손으로 그린 스케치, 캐니 윤곽선 이미지 또는 얼굴 메시)을 준수하여 이미지를 맞춤설정합니다.

모델 초기화

Imagen 편집 기능을 사용하려면 이미지 수정을 지원하는 Imagen 모델(예: imagen-3.0-capability-001)을 지정합니다.

val imagenModel = Firebase.ai(backend = GenerativeBackend.vertexAI())
    .imagenModel("imagen-3.0-capability-001")

마스크 기반 수정

Imagen의 마스크 기반 수정을 사용하면 모델이 조작할 특정 영역을 정의하여 이미지를 수정할 수 있습니다. 이 기능을 사용하면 마스크 만들기 및 적용, 객체 삽입 또는 삭제, 이미지 콘텐츠를 원래 경계 너머로 확장하는 등 다양한 작업을 할 수 있습니다.

마스크 만들기

객체 삽입 또는 삭제와 같은 마스크 기반 수정을 실행하려면 모델에서 수정해야 하는 영역인 마스크 를 정의해야 합니다.

마스크를 만들려면 ImagenBackgroundMask() 또는 ImagenSemanticMask()를 사용하여 모델에서 마스크를 자동 생성하도록 할 수 있습니다. 클래스 ID를 전달합니다.

마스크 비트맵을 생성하고 이를 ImagenRawMask로 변환하여 화면에 마스크를 직접 그릴 수도 있습니다. detectDragGesturesCanvas를 사용하면 다음과 같이 앱에서 Jetpack Compose로 마스크 그리기 사용자 인터페이스를 구현할 수 있습니다.

//import androidx.compose.ui.graphics.Color as ComposeColor

@Composable
fun ImagenEditingMaskEditor(
    sourceBitmap: Bitmap,
    onMaskFinalized: (Bitmap) -> Unit,
) {

    val paths = remember { mutableStateListOf<Path>() }
    var currentPath by remember { mutableStateOf<Path?>(null) }
    var scale by remember { mutableFloatStateOf(1f) }
    var offsetX by remember { mutableFloatStateOf(0f) }
    var offsetY by remember { mutableFloatStateOf(0f) }

    Column(
        modifier = Modifier.fillMaxSize(),
    ) {
        Box(
            modifier = Modifier
                .fillMaxWidth()
                .pointerInput(Unit) {
                    detectDragGestures(
                        onDragStart = { startOffset ->
                            val transformedStart = Offset(
                                (startOffset.x - offsetX) / scale,
                                (startOffset.y - offsetY) / scale,
                            )
                            currentPath = Path().apply { moveTo(transformedStart.x, transformedStart.y) }
                        },
                        onDrag = { change, _ ->
                            currentPath?.let {
                                val transformedChange = Offset(
                                    (change.position.x - offsetX) / scale,
                                    (change.position.y - offsetY) / scale,
                                )
                                it.lineTo(transformedChange.x, transformedChange.y)
                                currentPath = Path().apply { addPath(it) }
                            }
                            change.consume()
                        },
                        onDragEnd = {
                            currentPath?.let { paths.add(it) }
                            currentPath = null
                        },
                    )
                },
        ) {
            Image(
                bitmap = sourceBitmap.asImageBitmap(),
                contentDescription = null,
                modifier = Modifier.fillMaxSize(),
                contentScale = ContentScale.Fit,
            )
            Canvas(modifier = Modifier.fillMaxSize()) {
                val canvasWidth = size.width
                val canvasHeight = size.height
                val bitmapWidth = sourceBitmap.width.toFloat()
                val bitmapHeight = sourceBitmap.height.toFloat()
                scale = min(canvasWidth / bitmapWidth, canvasHeight / bitmapHeight)
                offsetX = (canvasWidth - bitmapWidth * scale) / 2
                offsetY = (canvasHeight - bitmapHeight * scale) / 2
                withTransform(
                    {
                        translate(left = offsetX, top = offsetY)
                        scale(scale, scale, pivot = Offset.Zero)
                    },
                ) {
                    val strokeWidth = 70f / scale
                    val stroke = Stroke(width = strokeWidth, cap = StrokeCap.Round, join = StrokeJoin.Round)
                    val pathColor = ComposeColor.White.copy(alpha = 0.5f)
                    paths.forEach { path ->
                        drawPath(path = path, color = pathColor, style = stroke)
                    }
                    currentPath?.let { path ->
                        drawPath(path = path, color = pathColor, style = stroke)
                    }
                }
            }
        }
        Button(
            onClick = {
                val maskBitmap = createMaskBitmap(sourceBitmap, paths)
                onMaskFinalized(maskBitmap)
            },
        ) {
            Text("Save mask")
        }
    }
}

그런 다음 캔버스에 경로를 그려 마스크 비트맵을 만들 수 있습니다.

// import android.graphics.Color as AndroidColor
// import android.graphics.Paint

private fun createMaskBitmap(
    sourceBitmap: Bitmap,
    paths: SnapshotStateList<Path>,
): Bitmap {
    val maskBitmap = Bitmap.createBitmap(sourceBitmap.width, sourceBitmap.height, Bitmap.Config.ARGB_8888)
    val canvas = android.graphics.Canvas(maskBitmap)
    val paint = Paint().apply {
        color = AndroidColor.RED
        strokeWidth = 70f
        style = Paint.Style.STROKE
        strokeCap = Paint.Cap.ROUND
        strokeJoin = Paint.Join.ROUND
        isAntiAlias = true
    }
    paths.forEach { path -> canvas.drawPath(path.asAndroidPath(), paint) }

    return maskBitmap
}

마스크가 소스 이미지와 동일한 크기인지 확인합니다. 자세한 내용은 Imagen AI 카탈로그 샘플을 참고하세요.

객체 삽입

기존 이미지에 새 객체 또는 콘텐츠를 삽입할 수 있습니다. 이를 인페인팅 이라고도 합니다. 모델은 지정된 마스크 영역에 새 콘텐츠를 생성하고 삽입합니다.

이를 위해 editImage() 함수를 사용합니다. 삽입할 콘텐츠를 설명하는 원본 이미지, 마스크, 텍스트 프롬프트 를 제공해야 합니다. 또한 ImagenEditingConfig 객체를 전달하여 editMode 속성이 ImagenEditMode.INPAINT_INSERTION으로 설정되어 있는지 확인합니다.

suspend fun insertFlowersIntoImage(
    model: ImagenModel,
    originalImage: Bitmap,
    mask: ImagenMaskReference
): ImagenGenerationResponse<ImagenInlineImage> {
    val prompt = "a vase of flowers"

    // Pass the original image, a mask, the prompt, and an editing configuration.
    val editedImage = model.editImage(
        referenceImages = listOf(
            ImagenRawImage(originalImage.toImagenInlineImage()),
            mask,
        ),
        prompt = prompt,
        // Define the editing configuration for inpainting and insertion.
        config = ImagenEditingConfig(ImagenEditMode.INPAINT_INSERTION)
    )
    return editedImage
}

객체 삭제

인페인팅을 사용하면 이미지에서 원치 않는 객체를 삭제할 수 있습니다. 이렇게 하려면 editImage 함수를 사용합니다. 삭제할 객체를 강조표시하는 원본 이미지와 마스크를 제공해야 합니다. 선택적으로 객체를 설명하는 텍스트 프롬프트를 포함할 수 있습니다. 이는 모델이 정확하게 식별하는 데 도움이 될 수 있습니다. 또한 ImagenEditingConfig 내에서 editModeImagenEditMode.INPAINT_REMOVAL로 설정해야 합니다.

suspend fun removeBallFromImage(
    model: ImagenModel,
    originalImage: Bitmap,
    mask: ImagenMaskReference
): ImagenGenerationResponse<ImagenInlineImage> {

    // Optional: provide the prompt describing the content to be removed.
    val prompt = "a ball"

    // Pass the original image, a mask, the prompt, and an editing configuration.
    val editedImage = model.editImage(
        referenceImages = listOf(
            ImagenRawImage(originalImage.toImagenInlineImage()),
            mask
        ),
        prompt = prompt,
        // Define the editing configuration for inpainting and removal.
        config = ImagenEditingConfig(ImagenEditMode.INPAINT_REMOVAL)
    )

    return editedImage
}

이미지 콘텐츠 확장

`outpaintImage()` 함수를 사용하여 이미지를 원래 경계 너머로 확장할 수 있습니다. 이를 아웃페인팅이라고 합니다. 이 함수에는 원본 이미지와 확장된 이미지의 필요한 Dimensions가 필요합니다. 선택적으로 확장에 관한 설명 프롬프트를 포함하고 새로 생성된 이미지 내에서 원본 이미지의 새로 생성된 이미지 내에서 원본 이미지의 ImagenImagePlacement를 지정할 수 있습니다.

suspend fun expandImage(originalImage: Bitmap, imagenModel: ImagenModel): ImagenGenerationResponse<ImagenInlineImage> {

    // Optionally describe what should appear in the expanded area.
    val prompt = "a sprawling sandy beach next to the ocean"

    val editedImage = imagenModel.outpaintImage(
        originalImage.toImagenInlineImage(),
        Dimensions(1024, 1024),
        prompt = prompt,
        newPosition = ImagenImagePlacement.LEFT_CENTER
    )


    return editedImage
}

배경 바꾸기

전경 피사체를 유지하면서 이미지의 배경을 바꿀 수 있습니다. 이렇게 하려면 editImage 함수를 사용합니다. 원본 이미지, ImagenBackgroundMask 객체 (새 배경의 텍스트 프롬프트 포함), editMode 속성이 ImagenEditMode.INPAINT_INSERTION으로 설정된 ImagenEditingConfig를 전달합니다.

suspend fun replaceBackground(model: ImagenModel, originalImage: Bitmap): ImagenGenerationResponse<ImagenInlineImage> {
    // Provide the prompt describing the new background.
    val prompt = "space background"

    // Pass the original image, a mask, the prompt, and an editing configuration.
    val editedImage = model.editImage(
        referenceImages = listOf(
            ImagenRawImage(originalImage.toImagenInlineImage()),
            ImagenBackgroundMask(),
        ),
        prompt = prompt,
        config = ImagenEditingConfig(ImagenEditMode.INPAINT_INSERTION)
    )

    return editedImage
}

맞춤설정

Imagen의 맞춤설정 기능 을 사용하여 피사체, 컨트롤 또는 스타일을 지정하는 참고 이미지를 기반으로 이미지를 생성하거나 수정할 수 있습니다. 이는 모델을 안내하기 위해 하나 이상의 참고 이미지와 함께 텍스트 프롬프트를 제공하여 달성됩니다.

피사체를 기반으로 맞춤설정

참고 이미지 (예: 제품, 사람 또는 동물)에서 특정 피사체의 새 이미지를 생성할 수 있습니다. 텍스트 프롬프트와 피사체의 참고 이미지를 하나 이상 제공하기만 하면 됩니다. 예를 들어 반려동물의 사진을 업로드하고 완전히 다른 환경에서 새 이미지를 생성할 수 있습니다.

이렇게 하려면 ImagenSubjectReference를 사용하여 피사체 참고를 정의한 다음 프롬프트와 함께 editImage에 전달합니다. 또한 editSteps 수를 지정하는 ImagenEditingConfig를 포함합니다. 일반적으로 editSteps 값이 높을수록 더 나은 품질의 결과가 생성됩니다.

suspend fun customizeCatImage(model: ImagenModel, referenceCatImage: Bitmap): ImagenGenerationResponse<ImagenInlineImage> {

    // Define the subject reference using the reference image.
    val subjectReference = ImagenSubjectReference(
        image = referenceCatImage.toImagenInlineImage(),
        referenceId = 1,
        description = "cat",
        subjectType = ImagenSubjectReferenceType.ANIMAL
    )

    // Provide a prompt that describes the final image.
    // The "[1]" links the prompt to the subject reference with ID 1.
    val prompt = "A cat[1] flying through outer space"

    // Use the editImage API to perform the subject customization.
    val editedImage = model.editImage(
        referenceImages = listOf(subjectReference),
        prompt = prompt,
        config = ImagenEditingConfig(
            editSteps = 50 // Number of editing steps, a higher value can improve quality
        )
    )

    return editedImage
}

컨트롤을 기반으로 맞춤설정

이 기법은 손으로 그린 스케치('낙서'), 캐니 윤곽선 이미지 또는 얼굴 메시와 같은 컨트롤 참고 이미지를 기반으로 새 이미지를 생성합니다. 모델은 컨트롤 이미지를 새 이미지의 레이아웃 및 구성을 위한 구조적 가이드로 사용하며 텍스트 프롬프트는 색상 및 질감과 같은 세부정보를 제공합니다.

ImagenControlReference로 컨트롤 참고를 정의하고 프롬프트 및 editSteps 수가 포함된 ImagenEditingConfig와 함께 editImage에 제공합니다 (값이 높을수록 품질이 향상될 수 있음).

suspend fun customizeCatImageByControl(model: ImagenModel, referenceImage: Bitmap): ImagenGenerationResponse<ImagenInlineImage> {

    // Define the subject reference using the reference image.
    val controlReference = ImagenControlReference(
        image = referenceImage.toImagenInlineImage(),
        referenceId = 1,
        type = ImagenControlType.SCRIBBLE,
    )

    val prompt = "A cat flying through outer space arranged like the scribble map[1]"

    val editedImage = model.editImage(
        referenceImages = listOf(controlReference),
        prompt = prompt,
        config = ImagenEditingConfig(
            editSteps = 50
        ),
    )

    return editedImage
}

스타일을 기반으로 맞춤설정

참고 이미지의 특정 스타일(예: 패턴, 질감 또는 디자인)과 일치하도록 이미지를 생성하거나 수정할 수 있습니다. 모델은 참고 이미지를 사용하여 필요한 미적 감각을 파악하고 이를 텍스트 프롬프트에 설명된 새 이미지에 적용합니다. 예를 들어 유명한 그림의 이미지를 제공하여 해당 그림의 스타일로 고양이 이미지를 생성할 수 있습니다.

ImagenStyleReference로 스타일 참고를 정의하고 프롬프트 및 editSteps 수가 포함된 ImagenEditingConfig와 함께 editImage에 제공합니다 (값이 높을수록 품질이 향상될 수 있음).

suspend fun customizeImageByStyle(model: ImagenModel, referenceVanGoghImage: Bitmap): ImagenGenerationResponse<ImagenInlineImage> {

    // Define the style reference using the reference image.
    val styleReference = ImagenStyleReference(
        image = referenceVanGoghImage.toImagenInlineImage(),
        referenceId = 1,
        description = "Van Gogh style"
    )

    // Provide a prompt that describes the final image.
    // The "1" links the prompt to the style reference with ID 1.
    val prompt = "A cat flying through outer space, in the Van Gogh style[1]"

    // Use the editImage API to perform the style customization.
    val editedImage = model.editImage(
        referenceImages = listOf(styleReference),
        prompt = prompt,
        config = ImagenEditingConfig(
            editSteps = 50 // Number of editing steps, a higher value can improve quality
        ),
    )

    return editedImage
}

다음 단계