728x90
학습 내용
- OpenCV 라이브러리 실습
- 얼굴 검출 face detection 실습
- 얼굴 랜드마크 face landmark 실습
개요
- 이번 프로젝트는 얼굴을 인식하여 얼굴에 고양이 수염 스티커를 붙이는 프로젝트이다
- 이번 프로젝트를 통해 얼굴 검출기술, 얼굴 랜드마크 기술, OpenCV 이미지 다루는 방법 들을 익힐 수 있었다.
OpenCV 라이브러리
- 컴퓨터 비전 관련 프로그래밍을 쉽게 할 수 있도록 도와주는 라이브러리
- https://opencv-python.readthedocs.io/en/latest/doc/01.imageStart/imageStart.html
import cv2
# 이미지 읽기
cv2.imread('img.png', 0)
# 이미지 보기
cv2.imshow()
# 이미지 저장하기
cv2.imwrite()
이미지 읽기의 flag
cv2.IMREAD_COLOR : 이미지 파일을 Color로 읽어들입니다. 투명한 부분은 무시되며, Default값입니다. cv2.IMREAD_GRAYSCALE : 이미지를 Grayscale로 읽어 들입니다. 실제 이미지 처리시 중간단계로 많이 사용합니다. cv2.IMREAD_UNCHANGED : 이미지파일을 alpha channel까지 포함하여 읽어 들입니다.
- 3개의 flag대신에 1, 0, -1을 사용해도 됨
얼굴 검출 face detection
- dlib의 face detector
- HOG(Histogram of Oriented Gradients)와 SVM(Support Vector Machine)을 사용해서 얼굴을 찾음
- HOG : 이미지에서 색상의 변화량, 딥러닝이 나오기 이전에 다양하게 사용되던 방식
# dlib을 활용한 hog detector를 선언
detector_hog = dlib.get_frontal_face_detector() # 기본 얼굴 감지기를 반환
# dlib은 rgb 이미지를 입력으로 받기 때문에 cvtColor() 를 이용해서 opencv 의 bgr 이미지를 rgb로 변환
img_rgb = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB)
dlib_rects = detector_hog(img_rgb, 1) # (image, num of image pyramid)
얼굴 랜드마크 face landmark
- face landmark localization 기술 : 이목구비의 위치를 추론하는 것
- detection 의 결과물인 bounding box 로 잘라낸(crop) 얼굴 이미지를 이용
- Object keypoint estimation 알고리즘 : Face landmark와 같이 객체 내부의 점을 찾는 기술
- top-down : bounding box를 찾고 box 내부의 keypoint를 예측
- bottom-up : 이미지 전체의 keypoint를 먼저 찾고 point 관계를 이용해 군집화 해서 box 생성
Dlib landmark localization
- top-down 방식
- 잘라진 얼굴 이미지에서 아래 68개의 이목구비 위치를 찾음
이 때 점의 개수는 데이터셋과 논문마다 다름
예를 들면, AFLW 데이터셋은 21개를 사용하고 ibug 300w 데이터셋은 68개를 사용
Landmark 데이터셋 요약
- AFLW dataset
- Dlib은 ibug 300-W 데이터셋으로 학습한 pretrained model 을 제공
- 학습 알고리즘은 regression tree의 앙상블 모델을 사용
Dlib의 제공되는 모델을 사용하여 이번 프로젝트를 진행
- 먼저 공개되어 있는 weight file을 다운로드
- 해당 모델파일은 bz2 압축파일 형태로 제공되어 압축을 풀어준 후 사용
wget http://dlib.net/files/shape_predictor_68_face_landmarks.dat.bz2
bzip2 -d ./models/shape_predictor_68_face_landmarks.dat.bz2
모델 불러오기
model_path = 'aiffel/camera_sticker/models/shape_predictor_68_face_landmarks.dat'
landmark_predictor = dlib.shape_predictor(model_path)
landmark 실행 결과 확인
# 얼굴 영역 박스 마다 face landmark를 찾아내고 face landmark 좌표를 저장
list_landmarks = []
for dlib_rect in dlib_rects:
points = landmark_predictor(img_rgb, dlib_rect)
# 모든 landmark의 위치정보를 points 변수에 저장
list_points = list(map(lambda p: (p.x, p.y), points.parts()))
# 각각의 landmark 위치정보를 (x,y) 형태로 변환하여 list_points 리스트로 저장
list_landmarks.append(list_points)
# landmark 이미지 출력
for landmark in list_landmarks:
for point in landmark:
# cv2.circle: OpenCV의 원을 그리는 함수
cv2.circle(img_show, point, 2, (0, 255, 255), -1)
img_show_rgb = cv2.cvtColor(img_show, cv2.COLOR_BGR2RGB)
plt.imshow(img_show_rgb)
plt.show()
스티커 적용하기
- 33번과 30번의 위치를 기준으로 고양이 수염을 붙여주는 식으로 코드를 작성하였다.
- 여러가지 실험을 해봤는데 결과적으로 33번을 이용해서 자리를 지정하는 것이 가장 결과가 좋았다.
# dlib_rects : 얼굴 영역을 저장하고 있는 값
# list_landmarks : 68개의 랜드마크 값 저장(이목구비 위치(x,y))
for dlib_rect, landmark in zip(dlib_rects, list_landmarks):
print (landmark[33])
x = landmark[33][0]
y = landmark[33][1]
w = h = dlib_rect.width()
sticker_path = '이미지 경로'
img_sticker = cv2.imread(sticker_path) # 스티커 이미지를 불러옵니다 // cv2.imread(이미지 경로) → image객체 행렬을 반환
img_sticker = cv2.resize(img_sticker, (w,h)) # 스티커 이미지 조정 → w,h는 얼굴 영역의 가로를 차지하는 픽셀의 수(187) // cv2.resize(image객체 행렬, (가로 길이, 세로 길이))
refined_x = x - w // 2
refined_y = y - h // 2
sticker_area = img_show[refined_y:refined_y+img_sticker.shape[0], refined_x:refined_x+img_sticker.shape[1]]
img_show[refined_y:refined_y+img_sticker.shape[0], refined_x:refined_x+img_sticker.shape[1]] = \
np.where(img_sticker==255,sticker_area,img_sticker).astype(np.uint8)
plt.imshow(cv2.cvtColor(img_show, cv2.COLOR_BGR2RGB))
plt.show()
각도에 따른 회전
- 얼굴이 돌아가는 만큼 고양이 수염도 돌아가서 붙여주는 것이 좋을 것 같아서 이를 반영하여 코드를 개선했다.
- 2번과 14번의 위치를 기준으로 각도를 조정하면 알맞게 조정이 될 것 같아 이를 기준으로 회전을 시켰다
for dlib_rect, landmark in zip(dlib_rects, list_landmarks): # 얼굴 영역을 저장하고 있는 값과 68개의 랜드마크를 저장하고 있는 값으로 반복문 실행
print (landmark[33])
x = landmark[33][0]
y = landmark[33][1]
w = h = dlib_rect.width()
# 2와 14번 위치를 찾는다
dx = landmark[14][0] - landmark[2][0]
dy = landmark[14][1] - landmark[2][1]
angle = -np.degrees(np.arctan2(dy, dx))
# Load the sticker image
sticker_path = os.getenv('HOME') + '/aiffel/camera_sticker/images/cat-whiskers.png'
img_sticker = cv2.imread(sticker_path)
img_sticker = cv2.resize(img_sticker, (w, h))
# Rotate the sticker image
M = cv2.getRotationMatrix2D((w // 2, h // 2), angle, 1)
img_sticker_rotated = cv2.warpAffine(img_sticker, M, (w, h))
# Calculate the refined coordinates
refined_x = x - w // 2
refined_y = y - h // 2
# Place the sticker on the image
sticker_area = img_bgr[refined_y:refined_y + img_sticker_rotated.shape[0], refined_x:refined_x + img_sticker_rotated.shape[1]]
img_bgr[refined_y:refined_y + img_sticker_rotated.shape[0], refined_x:refined_x + img_sticker_rotated.shape[1]] = \
np.where(img_sticker_rotated == 255, sticker_area, img_sticker_rotated).astype(np.uint8)
plt.imshow(cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB))
plt.show()
- 돌리니까 이미지가 약간 손실되는 문제가 발생함
- 회전시킨뒤 정사각형의 형태로 다시 바꿔주어야 손실이 발생하지 않을 것으로 보인다
최종코드
def put_on_sticker(img_path):
# Load dlib's face detector and landmark predictor
detector = dlib.get_frontal_face_detector()
predictor_path = '/aiffel/aiffel/camera_sticker/models/shape_predictor_68_face_landmarks.dat'
predictor = dlib.shape_predictor(predictor_path)
img_bgr = cv2.imread(img_path)
# Detect faces in the image
dlib_rects = detector(img_bgr, 1)
# List to hold landmarks
list_landmarks = []
for dlib_rect in dlib_rects:
landmarks = predictor(img_bgr, dlib_rect)
list_landmarks.append([(p.x, p.y) for p in landmarks.parts()])
# 각도에 맞게 돌린 이미지를 다시 정사각형의 형태로 맞춰주는 함수
def get_largest_bounding_square(image, angle):
(h, w) = image.shape[:2]
center = (w // 2, h // 2)
# Calculate the size of the new bounding box
radians = np.deg2rad(angle)
cos = np.abs(np.cos(radians))
sin = np.abs(np.sin(radians))
new_w = int((h * sin) + (w * cos))
new_h = int((h * cos) + (w * sin))
# The new dimension must be the larger of the two to make the box square
dim = max(new_w, new_h)
# Adjust rotation matrix to take into account translation
M = cv2.getRotationMatrix2D(center, angle, 1)
M[0, 2] += (dim - w) / 2
M[1, 2] += (dim - h) / 2
# Perform the rotation
rotated = cv2.warpAffine(image, M, (dim, dim), flags=cv2.INTER_LINEAR, borderMode=cv2.BORDER_CONSTANT, borderValue=(255, 255, 255))
return rotated
# Process each detected face
for dlib_rect, landmark in zip(dlib_rects, list_landmarks):
print(landmark[33])
x = landmark[33][0]
y = landmark[33][1]
w = h = dlib_rect.width()
# 2와 14번 위치를 찾는다
dx = landmark[14][0] - landmark[2][0]
dy = landmark[14][1] - landmark[2][1]
angle = -np.degrees(np.arctan2(dy, dx))
# 스티커 이미지를 불러온다
sticker_path = os.getenv('HOME') + '/aiffel/camera_sticker/images/cat-whiskers.png'
img_sticker = cv2.imread(sticker_path)
img_sticker = cv2.resize(img_sticker, (w, h))
# 스티커 이미지를 돌리고
# 정사각형의 형태로 맞춰준다
img_sticker_rotated = get_largest_bounding_square(img_sticker, angle)
# Calculate the refined coordinates
refined_x = x - img_sticker_rotated.shape[1] // 2
refined_y = y - img_sticker_rotated.shape[0] // 2
# Ensure the coordinates are within image bounds
refined_x = max(0, min(refined_x, img_bgr.shape[1] - img_sticker_rotated.shape[1]))
refined_y = max(0, min(refined_y, img_bgr.shape[0] - img_sticker_rotated.shape[0]))
# Place the sticker on the image
sticker_area = img_bgr[refined_y:refined_y + img_sticker_rotated.shape[0], refined_x:refined_x + img_sticker_rotated.shape[1]]
img_bgr[refined_y:refined_y + img_sticker_rotated.shape[0], refined_x:refined_x + img_sticker_rotated.shape[1]] = \
np.where(img_sticker_rotated == 255, sticker_area, img_sticker_rotated).astype(np.uint8)
plt.imshow(cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB))
plt.show()
최종정리
- 손으로 얼굴을 많이 가리는 경우 : 인식안됨, 눈코입, 얼굴 윤곽이 보여야 인식이 되는 것으로 보임
- 화질, 밝기 : 어느정도 얼굴의 형체만 보이면 인식가능
- 회전정도 : 90도 이하의 회전정도는 인식이 가능한 것으로 보임
회고
- 얼굴인식 오픈소스를 활용해서 여러가지 이미지 처리를 해볼 수 있어서 재미있었다
- 90도까지 돌아간 얼굴, 정면에서 많이 벗어나서 얼굴 윤곽이 안잡히면 인식이 안되는 것을 보정할 수가 없어서 아쉽다
참고자료 / 최종 결과물
실습 과정 코드
실습 결과 확인 페이지
728x90
'AIFFLE > PROJECT' 카테고리의 다른 글
[CV] 인물사진 만들기 - 기본편 (OpenCV, semantic segmentation) (0) | 2024.06.08 |
---|---|
[kaggle] Bike Sharing Demand (자전거 수요 예측) (0) | 2024.05.28 |
[sklearn] 선형회귀와 경사하강법 직접 구현하기 (diabetes dataset) (0) | 2024.05.28 |
[kaggle] 포켓몬 분류하기 프로젝트 (Tree 모델들의 Feature importance를 확인해보자) (2) | 2024.05.28 |