Imagen으로 이미지 생성

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

Firebase AI Logic SDK를 사용하여 Android 앱에서 Imagen 모델에 액세스할 수 있습니다. 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.4.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.ai(backend = GenerativeBackend.googleAI()).imagenModel(
       "imagen-4.0-generate-001",
       config,
       ImagenSafetySettings.builder()
          .setSafetyFilterLevel(ImagenSafetyFilterLevel.BLOCK_LOW_AND_ABOVE)
          .setPersonFilterLevel(ImagenPersonFilterLevel.BLOCK_ALL)
          .build())
);

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()

자바

CompletableFuture<GenerateContentResponse> futureResponse =
    model.generateContent(
        Content.newBuilder()
            .addParts(
                Part.newBuilder()
                    .setText("A hyper realistic picture of a t-rex with a blue bagpack in a prehistoric forest")
                    .build())
            .build());

try {
  GenerateContentResponse imageResponse = futureResponse.get();
  List<GeneratedImage> images =
      imageResponse
          .getCandidates(0)
          .getContent()
          .getParts(0)
          .getInlineData()
          .getImagesList();

  if (!images.isEmpty()) {
    GeneratedImage image = images.get(0);
    Bitmap bitmapImage = image.asBitmap();
    // Use bitmapImage
  }
} catch (ExecutionException | InterruptedException e) {
  e.printStackTrace();
}

Imagen으로 이미지 수정하기

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

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

모델 초기화

Imagen 수정 기능을 사용하려면 이미지 수정을 지원하는 Imagen 모델(예: imgen-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 = createMask(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 = createBitmap(sourceBitmap.width, sourceBitmap.height)
    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(
        sources = listOf(
            ImagenRawImage(originalImage),
            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(
        sources = listOf(
            ImagenRawImage(originalImage),
            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 = model.outpaintImage(
        ImagenRawImage(originalImage),
        Dimension(width, height),
        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(
        sources = listOf(
            ImagenRawImage(originalImage),
            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,
        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(
        references = listOf(subjectReference),
        prompt = prompt,
        config = ImagenEditingConfig(
            editSteps = 50 // Number of editing steps, a higher value can improve quality
        )
    )

    return editedImage
}

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

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

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

suspend fun customizeCatImageByControl(model: ImagenModel, referenceCatImage: Bitmap): ImagenGenerationResponse<ImagenInlineImage> {
 
   // Define the subject reference using the reference image.
    val controlReference = ImagenControlReference(
        image = referenceImage,
        referenceID = 1,
        controlType = CONTROL_TYPE_SCRIBBLE
    )

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

    val editedImage = model.editImage(
        references = 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,
        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(
        references = listOf(styleReference),
        prompt = prompt,
        config = ImagenEditingConfig(
            editSteps = 50 // Number of editing steps, a higher value can improve quality
        )
    )

    return editedImage 
}

다음 단계