본문 바로가기

카테고리 없음

DRF + ML 5일차

구글 OAuth 기능 추가 및 이미지 처리 연결

 

1. 구글 OAuth추가

백엔드만으로 OAuth 기능을 dj_rest_auth와 django_allauth를 이용하여 토큰을 발급하는 view를 만들어 Oauth를 적용하기로 하였다.

 

# views.py
from dj_rest_auth.registration.views import SocialLoginView
from allauth.socialaccount.providers.oauth2.client import OAuth2Client
from allauth.socialaccount.providers.google import views as google_view

class GoogleLogin(SocialLoginView):
    adapter_class = google_view.GoogleOAuth2Adapter
    callback_url = GOOGLE_CALLBACK_URI
    client_class = OAuth2Client

위 view로 code와 access_token을 body로 실어 요청을 보내자, access token과 refresh token이 아닌 key가 반환되었다.

이번 프로젝트에서는 이를 직접 토큰을 발행하는 코드를 작성하여 해결하였다.

class GoogleTokenView(APIView):


    def post(self, request):

        access_token = request.data.get("access_token")

        if access_token is None:
            return Response(
                {"message": "토큰이 없습니다."}, status=status.HTTP_401_UNAUTHORIZED
            )
        headers = {"Authorization": f"Bearer {access_token}"}
        user_info_request = requests.get(
            "https://www.googleapis.com/oauth2/v2/userinfo", headers=headers
        )
        user_data = user_info_request.json()
        email = user_data.get("email")
        if email is None:
            return Response(
                {"message": "유저정보에 접근할 수 없습니다."}, status=status.HTTP_401_UNAUTHORIZED
            )
        username = email.split("@")[0] + "_g"
        password = "0000"
        try:
            user = User.objects.get(email=email)
        except:
            # 회원 등록 후 다시 조회
            
        if user.logintype != "GOOGLE":
            return Response(
                {"message": "소셜 로그인 이메일이 아닙니다."},
                status=status.HTTP_400_BAD_REQUEST,
            )
        refresh = RefreshToken.for_user(user)
        refresh["email"] = user.email
        refresh["username"] = user.username
        refresh["logintype"] = user.logintype
        return Response(
            {
                "refresh": str(refresh),
                "access": str(refresh.access_token),
            },
            status=status.HTTP_200_OK,
        )

 

 

이 문제는 settings.py가 잘 설정되지 않아 기본설정대로 동작한 것이 원인이다. 공식문서를 참조하자 다음과 같이 설정하면 해결된다고 한다.

REST_FRAMEWORK = {
    ...
    'DEFAULT_AUTHENTICATION_CLASSES': (
        ...
        'dj_rest_auth.jwt_auth.JWTCookieAuthentication',
    )
    ...
}

REST_AUTH = {
    ...
    'USE_JWT': True,  # enable JWT authentication in dj-rest-auth
    'JWT_AUTH_COOKIE': 'my-app-auth',  # Declare what you want the cookie key to be called
    'JWT_AUTH_REFRESH_COOKIE': 'my-refresh-token',
}

 

2.  이미지 처리 모델

 

먼저 Dlib을 통해 안면을 locate한다.

detector = dlib.get_frontal_face_detector()
    img = cv2.imread(input_pic_url)
    dets = detector(img)

안면이 여러개 검출될 경우 랜덤하게 한개를 선택한다.

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

이후 안면의 위/아래/왼쪽/오른쪽 의 여백 공간의 크기를 저장한다.

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_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:
                # 스티커 부착 시도 : 스티커 이미지가 원본 이미지를 넘어갈 시 오류 발생
            except:
                # 실패 시 타겟을 변경해 시도
                target = target_list.pop(target_list.index(max(target_list)))
                continue
            break
            
            cv2.imwrite(save_uri, img)

 

이때, 수정할 이미지파일의 열고 닫음은 cv2.imread, cv2.imwrite 메소드를 이용한다. cv2는 이미지 파일을 numpy.ndarray 형식으로 불러온다. 

그러나 Django에서는 이미지 파일을 models.ImageField에 FieldFile에 InMemoryUploadFile로 불러온다.

이러한 형식의 차이를 어떻게 극복하여 두 부분을 연결할지 고민하였다.

 

시도 1. 열린 파일을 직접 타입 변환하기

# img가 InMemoryUploadFile입니다
file_bytes = numpy.asarray(bytearray(img.file.read()), dtype=numpy.uint8)
print(type(file_bytes))
img = cv2.imdecode(file_bytes, cv2.IMREAD_COLOR)
 print(type(img)

numpy의 asarray와 cv2의 imdecode를 이용해 django의 이미지 필드에 저장된 파일을 cv2에서 다룰 수 있게 타입을 변환할 수 있다.

그러나 역으로 변환하는 방법은 따로 찾지 못했다.

 

해결: 파일 저장후 경로로 주고 받기

1. ImageField는 저장된 파일 경로를 저장한다.

2. cv2는 저장된 파일을 열 수 있고, 파일을 저장할 수 있다.

위 두가지 사실을 이용하여, 이미지 처리 모듈과 DRF 모듈이 서로 파일 경로를 주고 받아 열고 닫게 하는 것으로 해결할 수 있었다.

# views.py
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)
# image_process.py
def picture_generator(input_pic_url):
    global h, w
    detector = dlib.get_frontal_face_detector()
    img = cv2.imread(input_pic_url)