[Tip] 90초면 모든 촬영이 끝, AI 패션 모델 만들기 (2)👗
👇👇👇 이제 직접 만들어 볼까요?
출처: 무신사 스냅 (링크, 스냅 닉네임 zero_jean_), 스모어톡 의상 변경 예시 결과물
AI 패션 모델에 관한 전반적인 내용은 AI 패션 모델 만들기 1편 링크 를 확인해주세요

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

먼저 구도를 확보해야 합니다.
패션 사진은 단순히 촬영에 그치는 것이 아니라 사진에 스토리텔링을 담아야 합니다. (이걸 왜 패션 문외한인 제가 말씀드리는 지는 모르겠지만...)
저는 이미지 구도 확보를 목적으로 무신사 스냅(링크) 을 참고했습니다.
무신사 스냅은 트렌디한 패션 컨텐츠를 보여주는 무신사의 앱인앱(App-in-App) 형태 버티컬 SNS 입니다.

출처: 무신사 스냅 (김어몽 님, malko_bee 님)
아무래도 스트릿 패션과 관련한 구도를 잡기가 용이해 보입니다.
저는 무신사 스냅 구글 검색 후 나온 이미지 중에서 하나를 무작위로 선정했습니다.
바로 이 사진이죠.

출처: 무신사 스냅 (링크, 스냅 닉네임 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))
그러면 아래와 같은 자세 이미지가 추출됩니다.
자세 이미지는 말 그대로 자세만 있기 때문에 실제 사람 같은 가상 모델의 이미지를 만들어야 할 거에요.
해당 이미지로 이미지를 생성하는 방법으로 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 를 통해 텍스트 임베딩을 최적으로 조정하도록 했습니다.
굳이 사용 안 하셔도 되기는 합니다. 쉬운 코드는 추후에 수정해서 공유해드리겠습니다.
그렇게 일단 이미지 하나 완성했습니다.
다음으로 입고 있는 청자켓을 그대로 딴 마스킹 이미지가 필요합니다.
전체 이미지를 수정하는 것이 아니라 이미지 내 일부를 수정해야 하기 때문에, 수정이 필요한 영역을 지정하는 마스킹 이미지가 필요합니다.
필요한 영역의 마스킹 이미지를 생성하는 방법으로 자연어 기반 마스킹이 가능한 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' 을 입력했을 때 자켓의 이미지만 분할 되는 모습을 확인하실 수 있습니다.

그리고 마스킹 이미지를 만들어야 합니다.
mask = segmented_frame_masks[0][0].cpu().numpy()
image_mask_pil = Image.fromarray(mask)
(중요) 참고로 마스킹 이미지의 의미는 하얀색 부분이 수정하는 영역, 검은색 부분이 고정되는 영역입니다.
그렇게 마스킹 이미지도 확보했습니다.
마지막으로 입고 있는 청자켓을 다른 자켓 이미지로 수정합니다.
저는 가상의 옷이 아니라 실제 가지고 있는 옷을 그대로 반영하는 것을 원하기 때문에, 기존의 이미지 입력 방식으로는 한계가 있습니다. 학습을 하자니 시간과 필요한 이미지가 많이 필요한 것이 문제입니다.
그래서 있는 옷 이미지를 그대로 집어 넣어주고자 이미지 방식의 프롬프팅 효율성을 극대화한 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 에 해당하고요.

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

패딩 이미지가 꽤나 잘 반영된 이미지가 나오는 것을 확인할 수 있습니다.
사실 새로운 모델 이미지를 사용하지 않아도 됩니다.
첫 단계를 생략하고 진행하면 아래와 같은 이미지가 만들어집니다.
실제 이미지에서도 패딩 이미지가 잘 반영된 모습을 확인하실 수 있습니다.
다만 이미지 생성 모델을 통해 나온 이미지가 아닐 경우 이미지 내 일부를 수정하는 인페인팅 작업 과정에서 부자연스럽게 결과 이미지가 나올 수 있다는 점은 기억해 주셔야 할 것 같습니다. 현재 기준 어도비 파이어플라이의 제너레이티브필을 사용한 결과를 생각해보시면 쉬울 것 같습니다.
이번에는 사실 많은 코드를 공유해드리지는 못했습니다.
작성된 코드가 매우 길기 때문에 과정을 안내해드리고자 한 글이 다소 어지러워질 수 있다고 판단했기 때문입니다.
다만 궁금하시면 최대한 많이 질문 남겨주세요. 글 수정을 통해 반영하도록 하겠습니다.
그리고 이 방법이 사실 최선은 아니고,
구글에서 발표한 TryOnDiffusion (링크) 이 현재 기준으로 최고의 방법인데 안타깝게도 공개된 코드가 하나도 없습니다.
추석 연휴가 끝나기 전 팀이 필요한 업무를 다 마치면 한 번 스크래치를 만들어볼까 합니다. 스모어톡이 타겟하는 시장은 아니라서 시간 날 때 혼자 해보려고 합니다.
이미지 AI 모델이 궁금하시다면 언제든 연락주세요!
이정민 (스모어톡, tonylee@smoretalk.io)