뒤로
이정민
이정민 ·

[Tip] 90초면 모든 촬영이 끝, AI 패션 모델 만들기 (2)👗

👇👇👇 이제 직접 만들어 볼까요?

작업 예시 _1 (1).png출처: 무신사 스냅 (링크, 스냅 닉네임 zero_jean_), 스모어톡 의상 변경 예시 결과물

AI 패션 모델에 관한 전반적인 내용은 AI 패션 모델 만들기 1편 링크 를 확인해주세요

모델 의상 변경 예시.png

그럼 이제 만들어 봐야죠?

어도비 코리아의 블로그 글 [입문자들을 위한 패션 사진 촬영법](링크) 을 참고한 저의 작업 플로우는 아래와 같습니다. 여기에 세부적인 작업 과정이 추가가 될 것입니다.

작업 구조도.png

먼저 구도를 확보해야 합니다.

패션 사진은 단순히 촬영에 그치는 것이 아니라 사진에 스토리텔링을 담아야 합니다. (이걸 왜 패션 문외한인 제가 말씀드리는 지는 모르겠지만...)

저는 이미지 구도 확보를 목적으로 무신사 스냅(링크) 을 참고했습니다.

무신사 스냅은 트렌디한 패션 컨텐츠를 보여주는 무신사의 앱인앱(App-in-App) 형태 버티컬 SNS 입니다.

image.png

출처: 무신사 스냅 (김어몽 님, malko_bee 님)

아무래도 스트릿 패션과 관련한 구도를 잡기가 용이해 보입니다.

저는 무신사 스냅 구글 검색 후 나온 이미지 중에서 하나를 무작위로 선정했습니다.

바로 이 사진이죠.

모델 사진 _기본 (1).png

출처: 무신사 스냅 (링크, 스냅 닉네임 zero_jean_)

감각적인 이 사진을 활용해서 하는 작업은 먼저 자세를 추출합니다. (전체 코드는 아닙니다)

from controlnet_aux import OpenposeDetector

#set the detector
pose_detector = OpenposeDetector.from_pretrained("lllyasviel/Annotators").to("cuda") 

image_path = "/content/drive/MyDrive/{file_name}" #chcck file_name
image = load_image(image_path)

#detect posture
pose_image = pose_detector(image, detect_resolution=512, image_resolution=1024, include_face=True, include_hand=False)
pose_image = np.array(pose_image)[:, :, ::-1]
pose_image = Image.fromarray(np.uint8(pose_image))

그러면 아래와 같은 자세 이미지가 추출됩니다.

작업 구도 추출 _1 (1).png자세 이미지는 말 그대로 자세만 있기 때문에 실제 사람 같은 가상 모델의 이미지를 만들어야 할 거에요.

해당 이미지로 이미지를 생성하는 방법으로 Stable Diffusion 을 구성하는 모듈 중 U-Net 에 지속적으로 윤곽값을 설정해주는 ControlNet 을 사용합니다.

아래 코드 중 image=pose_image 가 바로 윤곽값을 설정해주는 문장입니다. (전체 코드는 아닙니다)

# Generate an image with ControlNet
prompt = "photo of beautiful girl, with pastel hair, blue jeans, in golden ratio, model pose, dslr, 8k, 4k, ultrarealistic, realistic, photorealistic, natural skin, textured skin"
negative_prompt = "wrong, deformed hands, mutated limbs, blurry, bad quality, nude, text"
seed = 2024

compel_base = Compel(tokenizer=[pipe.tokenizer, pipe.tokenizer_2] , text_encoder=[pipe.text_encoder, pipe.text_encoder_2], returned_embeddings_type=ReturnedEmbeddingsType.PENULTIMATE_HIDDEN_STATES_NON_NORMALIZED, requires_pooled=[False, True])
compel_refiner = Compel(tokenizer=refiner.tokenizer_2 , text_encoder=refiner.text_encoder_2, returned_embeddings_type=ReturnedEmbeddingsType.PENULTIMATE_HIDDEN_STATES_NON_NORMALIZED, requires_pooled=True)

high_noise_frac = 0.85

conditioning, pooled = compel_base(prompt)
conditioning_neg, pooled_neg = compel_base(negative_prompt) if negative_prompt is not None else (None, None)
generator = torch.Generator(device="cuda").manual_seed(seed)

latents = pipe(prompt_embeds=conditioning,
               pooled_prompt_embeds=pooled,
               negative_prompt_embeds=conditioning_neg,
               negative_pooled_prompt_embeds=pooled_neg,
               image=pose_image,
               num_inference_steps=40,
               adapter_conditioning_scale=0.9,
               adapter_conditioning_factor=1.1,
               guidance_scale=8,
               denoising_end=high_noise_frac,
               generator=generator,
               output_type="latent",
               cross_attention_kwargs={"scale": 1.}
               ).images

conditioning, pooled = compel_refiner(prompt)
conditioning_neg, pooled_neg = compel_refiner(negative_prompt) if negative_prompt is not None else (None, None)
generator = torch.Generator(device="cuda").manual_seed(seed)

images = refiner(
    prompt_embeds=conditioning,
    pooled_prompt_embeds=pooled,
    negative_prompt_embeds=conditioning_neg,
    negative_pooled_prompt_embeds=pooled_neg,
    guidance_scale=7.5,
    denoising_start=high_noise_frac,
    image=latents,
    generator=generator,).images

image = images[0]

저는 compel library 를 통해 텍스트 임베딩을 최적으로 조정하도록 했습니다.

굳이 사용 안 하셔도 되기는 합니다. 쉬운 코드는 추후에 수정해서 공유해드리겠습니다.

작업 구도 추출 _3 (1).png그렇게 일단 이미지 하나 완성했습니다.

다음으로 입고 있는 청자켓을 그대로 딴 마스킹 이미지가 필요합니다.

전체 이미지를 수정하는 것이 아니라 이미지 내 일부를 수정해야 하기 때문에, 수정이 필요한 영역을 지정하는 마스킹 이미지가 필요합니다.

필요한 영역의 마스킹 이미지를 생성하는 방법으로 자연어 기반 마스킹이 가능한 Grounded SAM 을 사용합니다. Grounding DINO 를 통한 자연어 기반 객체 인식, SAM 을 통한 객체 이미지 분할을 연결한 구조라고 이해하시면 될 것 같습니다.

세 가지 함수를 이용합니다. (전체 코드는 아닙니다)

# detect object using grounding DINO
def detect(image, text_prompt, model, box_threshold = 0.3, text_threshold = 0.25):
  boxes, logits, phrases = predict(
      model=model,
      image=image,
      caption=text_prompt,
      box_threshold=box_threshold,
      text_threshold=text_threshold
  )

  annotated_frame = annotate(image_source=image_source, boxes=boxes, logits=logits, phrases=phrases)
  annotated_frame = annotated_frame[...,::-1] # BGR to RGB
  return annotated_frame, boxes

# segment object with SAM
def segment(image, sam_model, boxes):
  sam_model.set_image(image)
  H, W, _ = image.shape
  boxes_xyxy = box_ops.box_cxcywh_to_xyxy(boxes) * torch.Tensor([W, H, W, H])

  transformed_boxes = sam_model.transform.apply_boxes_torch(boxes_xyxy.to(device), image.shape[:2])
  masks, _, _ = sam_model.predict_torch(
      point_coords = None,
      point_labels = None,
      boxes = transformed_boxes,
      multimask_output = False,
      )
  return masks.cpu()

# Make masking image based on the result of SAM
def draw_mask(mask, image, random_color=True):
    if random_color:
        color = np.concatenate([np.random.random(3), np.array([0.8])], axis=0)
    else:
        color = np.array([30/255, 144/255, 255/255, 0.6])
    h, w = mask.shape[-2:]
    mask_image = mask.reshape(h, w, 1) * color.reshape(1, 1, -1)

    annotated_frame_pil = Image.fromarray(image).convert("RGBA")
    mask_image_pil = Image.fromarray((mask_image.cpu().numpy() * 255).astype(np.uint8)).convert("RGBA")

    return np.array(Image.alpha_composite(annotated_frame_pil, mask_image_pil))

그러면 아래와 같이 'jacket' 을 입력했을 때 자켓의 이미지만 분할 되는 모습을 확인하실 수 있습니다.

작업 구도 마스킹 2 (1).png

그리고 마스킹 이미지를 만들어야 합니다.

mask = segmented_frame_masks[0][0].cpu().numpy()
image_mask_pil = Image.fromarray(mask)

(중요) 참고로 마스킹 이미지의 의미는 하얀색 부분이 수정하는 영역, 검은색 부분이 고정되는 영역입니다.

작업 구도 마스킹 3 (1).png그렇게 마스킹 이미지도 확보했습니다.

마지막으로 입고 있는 청자켓을 다른 자켓 이미지로 수정합니다.

저는 가상의 옷이 아니라 실제 가지고 있는 옷을 그대로 반영하는 것을 원하기 때문에, 기존의 이미지 입력 방식으로는 한계가 있습니다. 학습을 하자니 시간과 필요한 이미지가 많이 필요한 것이 문제입니다.

그래서 있는 옷 이미지를 그대로 집어 넣어주고자 이미지 방식의 프롬프팅 효율성을 극대화한 IP Adapter 를 사용합니다. (전체 코드는 아닙니다)

image_encoder_path = "/content/h94/sdxl_models/image_encoder"
ip_ckpt = "/content/h94/sdxl_models/ip-adapter_sdxl.bin"

ip_model2 = IPAdapterXL(pipe2, image_encoder_path, ip_ckpt, device="cuda")

# save the width and height
width, height = image_source_pil.size

# resize for inpainting
image_source_pil = image_source_pil.resize((1024, 1024))
image_mask_pil = image_mask_pil.resize((1024, 1024))

# Upload an image of clothes
new_clothes_image = Image.open("/content/drive/MyDrive/{image_file}")
new_clothes_image.resize((1024, 1024))

# Generate an image with IP Adapter
final_images = ip_model2.generate(pil_image=new_clothes_image, prompt="best quality", negative_prompt=negative_prompt, image=image_source_pil, mask_image=image_mask_pil, num_inference_steps=60, strength=0.95)
final_image = final_images[0].resize((width, height))

아래 이미지는 순서대로 코드 내 image_source_pil, image_mask_pil, final_image 입니다.

패딩 이미지는 코드 내 new_clothes_image 에 해당하고요.

작업 결과 (1).png

그래서 나온 이미지를 다시 크게 살펴보면,

작업 결과 2 (1).png

패딩 이미지가 꽤나 잘 반영된 이미지가 나오는 것을 확인할 수 있습니다.

사실 새로운 모델 이미지를 사용하지 않아도 됩니다.

첫 단계를 생략하고 진행하면 아래와 같은 이미지가 만들어집니다.

작업 결과 3 (1).png실제 이미지에서도 패딩 이미지가 잘 반영된 모습을 확인하실 수 있습니다.

다만 이미지 생성 모델을 통해 나온 이미지가 아닐 경우 이미지 내 일부를 수정하는 인페인팅 작업 과정에서 부자연스럽게 결과 이미지가 나올 수 있다는 점은 기억해 주셔야 할 것 같습니다. 현재 기준 어도비 파이어플라이의 제너레이티브필을 사용한 결과를 생각해보시면 쉬울 것 같습니다.

이번에는 사실 많은 코드를 공유해드리지는 못했습니다.

작성된 코드가 매우 길기 때문에 과정을 안내해드리고자 한 글이 다소 어지러워질 수 있다고 판단했기 때문입니다.

다만 궁금하시면 최대한 많이 질문 남겨주세요. 글 수정을 통해 반영하도록 하겠습니다.

그리고 이 방법이 사실 최선은 아니고,

구글에서 발표한 TryOnDiffusion (링크) 이 현재 기준으로 최고의 방법인데 안타깝게도 공개된 코드가 하나도 없습니다.

추석 연휴가 끝나기 전 팀이 필요한 업무를 다 마치면 한 번 스크래치를 만들어볼까 합니다. 스모어톡이 타겟하는 시장은 아니라서 시간 날 때 혼자 해보려고 합니다.

이미지 AI 모델이 궁금하시다면 언제든 연락주세요!
이정민 (스모어톡, tonylee@smoretalk.io)

플라멜 Flamel v0.1

프롬프트 엔지니어링이 필요 없는 가장 쉬운 검색형 이미지 생성 서비스

18

댓글

로그인 후 댓글을 남길 수 있습니다.

조완기
조완기

잘 보았습니다 ^^ 코드 매우 궁금하네요 ㅎㅎ 작업하신 것을 보니 미드저니에서도 마키로 영역 잡고 인페인팅하여 리믹스 합성할수 있을까 궁금해졌어요. 그런데 기존의 옷 영역 밖으로는 새 옷 패턴이 나가질 못해서 결과물과 입력한 옷이 다른 상품 같아요. 이를테면 패딩에 목 부분 감싸는 파트가 결과물에서는 안 보여서요. 혹시 같은 방식으로 하의 변화라거나, 상하의 영역 모두 지정해서 여성 원피스 입힌다거나 등도 가능할까요?

이정민
이정민

감사합니다 :) 말씀하신대로 최고 좋은 방법은 아니었어요. TryOnDiffusion 정도는 되어야 사용이 가능할 것으로 보여집니다. (물론 해당 논문도 제가 보기에는 한계점이 있어 보이기는 합니다. 최고 좋은 논문이지만요) 제가 제안한 방식에서 문제가 발생하는 이유는 다음과 같습니다. 1) 아무래도 마지막 옷을 반영하는 IP-Adapter 가 image encoder 와 관련해서 가장 반영을 잘하게 한 것은 맞으나, 옷에 특정해서 한 것은 아닌 것이 1차적인 문제이고요. 2) 마스킹 영역이 너무 타이트하게 된 것도 문제이기는 합니다. 이 부분을 보다 공백을 포함해서 늘리면 일부 자연스러워질 수는 있습니다. 대신 이 경우 인간 골격에 대한 반영이 되어야 팔 위치에 정확히 인페인팅이 될 것으로 보여집니다. 마지막으로 모두 가능하기는 합니다. 해보았을 때 워킹하더라고요. 다만 정확하게 의상이 반영되는 것은 아니고 정말 비슷하게 된다라는 점이라고 미리 양해를 구하겠습니다!

장영운
장영운

옷의 핏을 정확하게 재현할 수 없어서 옷을 팔기 위한 사진에는 쓸 수 없겠다는 생각이 들지만, 그 외에는 유용할 것 같아요.

이정민
이정민

감사합니다 :) 위 대댓글과 같은 말씀이지만 정확한 방식은 아니었어요. TryOnDiffusion과 같은 의상 전용 모델이 아니기 때문에 실제 모델 사진에는 아직 한계가 있고, 대신 패션 사진이 아닌 기타 이미지 편집용으로는 충분히 가능한 방식으로 보여집니다.