WIL 딥러닝 프로젝트를 마치며
2023.05.29 월 < 일요일에 써야 했지만, 프로젝트를 제출하니 월요일 06시였습니다!(세상에, 놀라워라!)
여러 문제가 있었지만, 일단 프로젝트를 마치고 제출하였습니다.
회고나 KPT는 오늘 TIL로 작성할 생각이니, 일단 그 부분은 넘어가겠습니다.
그래서 이번 프로젝트에서는 무엇을 느꼈나?
강의를 통해 배운 것 만으로는 부족하다는 것을 느꼈습니다.
이번 프로젝트를 제외하고 이전 프로젝트에서 제가 부족하다 느꼈던 것은, 강의를 이해하지 못해 프로젝트에서 기능을 구현하거나, 역할을 맡을 때, 어려움이 있었습니다.
그랬기에, 공부하였고 바로 직전 프로젝트에서 이를 극복하여, 한 사람의 몫을 하였습니다.
이번 프로젝트에서는 어땠을까요?
비록 부족한 부분이 있었지만, 딥러닝 모델을 사용하고, 파이썬을 이용하여 무리없이 마주친 문제들을 해결했습니다.
하지만, 그것만으로는 부족했습니다.
가장 먼저 그것을 느낀 부분은 다른 작업 환경에서 만든 딥러닝 코드를 프로젝트 폴더에 이식시킬 때 였습니다.
대비는 첫 날부터 어느정도 했었죠? 이전 기수들이 어떻게 drf에 합쳤는지 코드를 통해 예상할 수 있었습니다, 그랬기에 딥러닝 코드를 짜면서도, drf와 합치기 전에 최대한 함수 형태로 만들고 다른 파일에 두어 import하기 좋은 형태로 만들었습니다.
막상 drf에 이 코드를 합치려고 하니 문제가 발생했습니다.
게시글을 생성할 때, 원본 이미지(origin_img)를 받고 그것을 input값으로 받아 같은 app에 만든 딥러닝 코드가 모인 파일에서 처리 후 아웃 풋으로 고양이 스티커가 합성된 이미지를 돌려주고 그 이미지를 Article 모델의 합성된 이미지(img)필드에 넣어주고 시리얼라이저를 이용하여, 게시글을 조회했을 때, 프론트에서 출력을 조절하여 필요한 것만 보여주자 생각했습니다.
이것 방법은 와이어프레임에서 계획했던 프론트적 부분을 보여주지 못했기에, 사용되지 못했습니다.

와이어프레임에서 계획했던 게시글 작성 페이지입니다.
생성 전, 생성 후 이미지를 작성 페이지에서 이미 보여주고 있습니다.
이미지 파일 선택을 통해 원본이미지를 등록하고, 생성버튼을 통해 생성 전 이미지와 생성 후 이미지를 출력해 줍니다.

실제 만들어진 페이지 입니다, 파일 선택 시에는 이미지가 출력되지 않습니다, 고양이 생성 버튼을 눌려야지, 원본이미지와 합성된 이미지를 보여줍니다.
이것을 구현하기 위해 새로운 app이 필요했습니다. openapi를 이용해 챗gpt와 연동하고 디스크립션의 내용을 받아 미리 만든 역할에 맞춰 gpt가 고양이가 화난 이유를 출력해줍니다. 이 또한 image와 같이 게시글 작성 페이지에서 버튼을 통해 출력받기 때문에 이미 새로운 app이 만들어져있었습니다.
그랬기에, 딥러닝 코드 대부분이 저 app 내부에 위치하여 돌아가는 형태로 합쳐졌습니다.
ai_process app
기능적인 부분은 cat.py에서 돌아갑니다. 고양이를 붙이고, 사이즈를 조절하는 기능이 이루어집니다.
#ai_process/cat.py
import cv2
import dlib
import numpy as np
import random
import os
h, w = 0, 0
# a, d 에서 사용
def width_control(pt1, pt2, control_y):
if abs(pt2[0] - pt1[0]) < w * 0.4:
width_increase = int((pt2[0] - pt1[0]) * 0.3)
down_width_increase = pt1[0]
up_width_increase = pt2[0]
down_width_increase -= width_increase
up_width_increase += width_increase
half_y = control_y
pt1 = [down_width_increase, pt1[1]]
pt2 = [up_width_increase, half_y]
return pt1, pt2
else:
return pt1, pt2
# b, c에서 사용
def height_control(pt1, pt2, control_x):
if abs(pt2[1] - pt1[1]) < h * 0.4:
height_increase = int(abs(pt2[1] - pt1[1]) * 0.2)
down_height_increase = pt1[1]
up_height_increase = pt2[1]
down_height_increase -= height_increase
up_height_increase += height_increase
half_x = control_x
pt1 = [pt1[0], down_height_increase]
pt2 = [half_x, up_height_increase]
return pt1, pt2
else:
return pt1, pt2
def random_control(random_images):
random_image = random.choice(random_images)
sticker_img_path = random_image["img"]
return sticker_img_path
def select_target(target, a, b, c, d, x1, x2, y1, y2, center_x, center_y):
if target == a:
# 랜덤 이미지 딕셔너리
random_images = [
{"num": 0, "img": "static/imgs/up_cat.png"},
{"num": 1, "img": "static/imgs/top_cat1.png"},
]
sticker_img_path = random_control(random_images)
# 스티커 이미지 로드
sticker_img = cv2.imread(sticker_img_path, cv2.IMREAD_UNCHANGED)
control_y = a // 2
pt1 = x1, y1
pt2 = x2, 0
pt1, pt2 = width_control(pt1, pt2, control_y)
elif target == b:
# 랜덤 이미지 딕셔너리
random_images = [
{"num": 0, "img": "static/imgs/left_cat1.png"},
{"num": 1, "img": "static/imgs/left_cat2.png"},
]
sticker_img_path = random_control(random_images)
# 스티커 이미지 로드
sticker_img = cv2.imread(sticker_img_path, cv2.IMREAD_UNCHANGED)
control_x = b // 2
pt1 = x1, y1
pt2 = 0, y2
pt1, pt2 = height_control(pt1, pt2, control_x)
elif target == c:
# 랜덤 이미지 딕셔너리
random_images = [
{"num": 0, "img": "static/imgs/right_cat.png"},
{"num": 1, "img": "static/imgs/right_cat1.png"},
]
sticker_img_path = random_control(random_images)
# 스티커 이미지 로드
sticker_img = cv2.imread(sticker_img_path, cv2.IMREAD_UNCHANGED)
control_x = center_x + c // 2
pt1 = x2, y1
pt2 = w, y2
pt1, pt2 = height_control(pt1, pt2, control_x)
elif target == d:
# 랜덤 이미지 딕셔너리
random_images = [
{"num": 0, "img": "static/imgs/under_cat.png"},
{"num": 1, "img": "static/imgs/under_cat1.png"},
{"num": 2, "img": "static/imgs/under_cat2.png"},
]
sticker_img_path = random_control(random_images)
# 스티커 이미지 로드
sticker_img = cv2.imread(sticker_img_path, cv2.IMREAD_UNCHANGED)
control_y = center_y + d // 2
pt1 = x1, y2
pt2 = x2, h
pt1, pt2 = width_control(pt1, pt2, control_y)
return pt1, pt2, sticker_img, target
def picture_generator(input_pic_url):
global h, w
detector = dlib.get_frontal_face_detector()
img = cv2.imread(input_pic_url)
dets = detector(img)
# 얼굴이 1개 이상 감지된 경우에만 스티커 적용
if len(dets) >= 1:
# 얼굴 선택 랜덤
face_index = random.randrange(len(dets))
det = dets[face_index]
x1 = det.left()
y1 = det.top()
x2 = det.right()
y2 = det.bottom()
h, w, c = img.shape
center_x = (x2 + x1) // 2
center_y = (y2 + y1) // 2
a = center_y
b = center_x
c = w - b
d = h - a
# target = None
target_list = [a, b, c, d]
target = target_list.pop(target_list.index(max(target_list)))
# target = search_target
while True:
pt1, pt2, sticker_img, target = select_target(
target, a, b, c, d, x1, x2, y1, y2, center_x, center_y
)
# 스티커 이미지 크기 변경
sticker_width = int(abs(pt2[0] - pt1[0]))
sticker_height = int(abs(pt2[1] - pt1[1]))
sticker_resized = cv2.resize(
sticker_img, dsize=(sticker_width, sticker_height)
)
# 알파 채널 값(투명도) 계산
alpha = sticker_resized[:, :, 3] / 255.0
alpha = np.expand_dims(alpha, axis=2)
overlay_rgb = sticker_resized[:, :, :3]
try:
img[
min(pt1[1], pt2[1]) : max(pt1[1], pt2[1]),
min(pt1[0], pt2[0]) : max(pt1[0], pt2[0]),
] = (
alpha * overlay_rgb
+ (1.0 - alpha)
* img[
min(pt1[1], pt2[1]) : max(pt1[1], pt2[1]),
min(pt1[0], pt2[0]) : max(pt1[0], pt2[0]),
]
)[
:, :, :3
]
except:
target = target_list.pop(target_list.index(max(target_list)))
# pt1, pt2, sticker_img, target = select_target(
# target, a, b, c, d, x1, x2, y1, y2, center_x, center_y
# )
continue
break
else:
print("얼굴이 탐지되지 않았다.")
# 출력
save_uri = input_pic_url.replace("input", "change")
save_dir = "/".join(save_uri.split("/")[:-1])
if not os.path.exists(save_dir):
os.mkdir(save_dir)
cv2.imwrite(save_uri, img)
return save_uri
models.py에서는 이미지를 받고 출력하는 필드를 생성하였습니다.
from django.db import models
from user.models import User
class Picture(models.Model):
"""Picture 모델
Picgen view가 발생할때마다 입력사진과 변환사진을 저장합니다.
Attributes:
input_pic (Image): 입력된 사진
change_pic (Image): AI가 변환한 사진
"""
input_pic = models.ImageField(upload_to="%Y/%m/input/")
change_pic = models.ImageField(upload_to="%Y/%m/change/", null=True)
author = models.ForeignKey(
User, on_delete=models.CASCADE, related_name="picture_set"
)
def delete(self):
"""Picture.delete Picture모델 및 하위 이미지 삭제
Picture모델이 삭제될 때, 이미지필드의 경로에 해당하는 이미지들도 media 폴더에서 삭제됩니다.
"""
self.change_pic.delete(save=False)
self.input_pic.delete(save=False)
super(Picture, self).delete()
여기서 작성된 Picture모델은 Article 모델에서 related_name을 사용하여, 참조되어 사용할 수 있습니다.
class PicgenView(APIView):
"""PicgenView
post 요청시 입력된 사진으로 변환된 사진을 생성하여 반환합니다.
매 요청마다 두 사진을 Picture모델에 저장합니다.
Attributes:
permission (permissions): IsAuthenticated 로그인한 사용자만 접속을 허용합니다.
"""
permission_classes = [permissions.IsAuthenticated]
def post(self, request, *args, **kwargs):
"""PicgenView.post
post요청 시 입력받은 사진으로 변환된 사진을 생성하여 반환합니다.
"""
Picture.objects.filter(article=None, author=request.user).delete()
serializer = PictureSerializer(data=request.data)
if serializer.is_valid():
orm = serializer.save(author=request.user)
change_pic = picture_generator("media/" + orm.__dict__["input_pic"])
orm.change_pic = change_pic.replace("media/", "")
orm.save()
new_serializer = PictureSerializer(instance=orm)
return Response(new_serializer.data, status=status.HTTP_201_CREATED)
else:
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
원본 이미지를 넣고 합성된 이미지를 버튼을 눌러 확인 할 수 있지만, 문제는 게시글이 작성되었을 때, 원본 이미지 하나, 합성된 이미지 하나가 media경로에 저장되는 것이 아니라, 게시글 작성완료 버튼을 누르기 전 원본 이미지만 계속 바꿔 합성된 이미지를 볼 수 있기 때문에 게시글 하나를 작성하는데 사용되는 이미지 db에 부담을 주게 됩니다.
그렇기에 필터를 통해 작성자이면서, 아직 article에 저장이 안된 이미지들 삭제하여 db에 저장되는 것을 막아줍니다.
단, 이때 media경로에는 모델에서 정의한 방식에 따라 이미지가 저장됩니다, 이 부분은 일정한 규칙을 두어, 주기적인 삭제를 통해 과부화를 막아야 합니다.
class PictureSerializer(serializers.ModelSerializer):
"""PictureSerializer
Picgen view 에서 반환을 위한 시리얼라이저입니다.
"""
class Meta:
model = Picture
fields = "__all__"
extra_kwargs = {"author": {"read_only": True}}
시리얼라이저 부분에서는 extra_kwargs를 통해 읽기 전용으로 만들어 줍니다.

image부분은 pictures 부분이며, No Image 부분이 각각 input_pic과 change_pic이 됩니다, 읽기 전용이기에 등장하는 사진에는 다른 조작이 불가능하며, pictures에 새로운 이미지를 넣고 고양이 생성버튼을 눌러 새로운 사진을 등록, 출력할 수 있게 만들어졌습니다.
change_pic = picture_generator("media/" + orm.__dict__["input_pic"])
orm.change_pic = change_pic.replace("media/", "")
orm.save()
new_serializer = PictureSerializer(instance=orm)
change_pic 변수에는 고양이 스티커를 합성하는 picture_generator 함수를 사용하며 인자로는 __dict__를 사용하여 orm객체가 가진 input_pic에 접근하여 그것을 인자로 사용할 수 있게 합니다.
[gpt가 해주는 쉬운 설명]
orm.__dict__["input_pic"]는 orm 객체의 "input_pic"이라는 속성에 접근하여 해당 속성의 값을 반환합니다. 이를 통해 input_pic 필드에 저장된 이미지 파일의 경로를 가져올 수 있습니다.
orm.change_pic = change_pic.replace("media/", "")은 앞서 생성된 변환 이미지의 경로에 있는 "media/"를 제거한 경로를 orm.change_pic에 저장합니다. 이렇게 하면 change_pic 필드에는 변환 이미지의 상대 경로가 저장됩니다.
orm.save(): 변경된 orm 객체를 데이터베이스에 저장합니다. 이를 통해 change_pic 필드의 값이 업데이트되고, 데이터베이스에 해당 정보가 반영됩니다.
(단, article에 반영되지 않는다면 삭제처리됩니다.)
new_serializer = PictureSerializer(instance=orm)를 사용한 이유는 orm에 요청자의 정보와 input_pic이 직렬화 되었고 이후 picture_generator를 거치며, change_pic을 생성합니다.
이때 모델에서 change_pic에 null=True 값을 부여하여 에러를 방지하고, 새롭게 추가된 change_pic과 인자들을 직렬화하기위해 새롭게 시리얼라이저를 해주는 것입니다.
이후 성공 시 유저정보와 input_pic, change_pic을 response 시켜줍니다.
이렇게 구현 전 생각했던 방식과 너무나 다른 방법으로 코드를 작성하였고 기능을 구현시켰습니다.
저는 계획과 달라진, 그리고 모르는 구조가 많은 이 코드에 기여하지 못했습니다. 팀원분들이 하나 둘 합쳐나가는 과정을 보며, 읽어보고 이해하기위해 바빴습니다.
막히는 부분에서 토론하고, 각자 의견을 제시하며, 바로 이해하는 모습을 보니 많이 부러웠습니다.
이외에도 콘다를 사용하면서 발생했던, 배포부분과 크게 기여하지 못했던 프론트부분, 딥러닝부분을 제외한 백엔드부분 등 정말 처음 맡았던 부분만 해결하는 일인분만 하는 그런 모습을 보여주었습니다.
많이 공부하고 많은 것을 익히며, 할 줄 아는것이 많아지니, 접근조차 못했던 영역에서도 기여하여, 일인분 이상의 역할을 하고싶어집니다.
그런 점에서 또 부족한 부분을 느끼고 또 나아가야 겠다는 다짐을 하게됩니다.
이것으로 WIL을 마치며, 오늘 TIL을 통해 KPT를 작성해보도록 합시다.