AIFFEL(220125)_인물사진을 만들어 보자
피사체를 가깝게 찍을 때 배경이 흐려지는 효과는 보통 DSLR이나 미러리스 같은 전문적인 카메라와 렌즈에서 만들어 내기 쉬운 효과이다. 하지만 피사체를 충분히 가까운 거리에 두고 촬영한다면 핸드폰 카메라에서도 비슷한 효과를 낼 수 있다.
특히 2개 이상의 렌즈를 탑재한 최신 핸드폰에서는 '인물사진 모드(portrait mode)' 가 지원된다.
핸드폰 카메라의 인물사진 모드는 듀얼 카메라를 이용해 DSLR의 아웃포커싱 기능을 흉내낸다. 물론 인물사진의 아웃포커싱 구현은 DSLR의 아웃포커싱 구현과는 다르다.
- DSLR에서는 사진을 촬영할 때 피사계 심도(depth of field, DOF)를 얕게 하여 초점이 맞은 피사체를 제외한 배경을 흐리게 만든다.
- 핸드폰 인물사진 모드는 화각이 다른 두 렌즈를 사용한다. 일반(광각) 렌즈에서는 배경을 촬영하고 망원 렌즈에서는 인물을 촬영한 뒷배경을 흐리게 처리한 후 망원 렌즈의 인물과 적절하게 합성한다.
인물사진 모드에서 사용되는 용어
한국에서는 배경을 흐리게 하는 기술을 주로 '아웃포커싱'이라고 표현한다. 하지만 한국에서만 사용하는 용어이고 정확한 영어 표현은 얕은 피사계 심도(shallow depth of field) 또는 셸로우 포커스(shallow focus) 라고 한다.
또한 "보케(bokeh)"라는 일본어에서 유래된 표현 또한 많이 사용한다.
학습목표
- 딥러닝을 적용하여 핸드폰 인물 사진 모드를 따라 해 보자.
📖 셸로우 포커스 만들기_(1) 사진을 준비하자
하나의 카메라로 셸로우 포커스(shallow focus)를 만드는 방법
두 개의 렌즈가 맡은 역할을 하나의 렌즈에서 구현해야 한다. 이미지 세그멘테이션(image segmentation) 기술을 이용하면 하나의 이미지에서 배경과 사람을 분리할 수 있다. 분리된 배경을 블러(blur) 처리 후 사람 이미지와 다시 합하면 아웃포커싱 효과를 적용한 인물 사진을 얻을 수 있다.
따라서 다음과 같은 후가공으로 '인물사진 모드'인 사진을 만든다.
- 배경이 있는 셀카를 촬영한다. (배경과 사람의 거리가 약간 멀리 있으면 좋습니다.)
- 시맨틱 세그멘테이션(Semantic segmentation)으로 피사체(사람)와 배경을 분리한다.
- 블러링(blurring) 기술로 배경을 흐리게 한다.
- 피사체를 배경의 원래 위치에 합성한다.
사진을 준비하자
import os
import urllib
import cv2
import numpy as np
from pixellib.semantic import semantic_segmentation
from matplotlib import pyplot as plt
urllib
는 웹에서 데이터를 다운로드할 때 사용한다.
cv2
는 OpenCV 라이브러리로 이미지를 처리하기 위해 필요하다.
pixellib
는 시맨틱 세그멘테이션을 편하게 사용할 수 있는 라이브러리이다. 아직 시맨틱 세그멘테이션 모델을 직접 만들기에는 어려우니 이번에는 준비된 모델을 사용만 해본다.
준비한 이미지를 읽는다.
# 본인이 선택한 이미지의 경로에 맞게 바꿔 주세요.
img_path = os.getenv('HOME')+'/aiffel/human_segmentation/images/image.png'
img_orig = cv2.imread(img_path)
print(img_orig.shape)
plt.imshow(cv2.cvtColor(img_orig, cv2.COLOR_BGR2RGB))
plt.show()
# 실행결과
(450, 800, 3)
📖 셸로우 포커스 만들기_(2) 세그멘테이션으로 사람 분리하기
배경에만 렌즈 흐림 효과를 주기 위해서 그림과 같이 이미지에서 사람과 피사체를 분리해야 한다. 흔히 포토샵으로 '누끼 따기'라는 것이 바로 이런 작업을 말한다. 인물 사진에서 배경만 다르게 편집하려는 용도로 흔히 하는 작업이다.
세그멘테이션(Segmentation)이 뭔가요?
이미지에서 픽셀 단위로 관심 객체를 추출하는 방법을 이미지 세그멘테이션(image segmentation)이라고 한다. 이미지 세그멘테이션은 모든 픽셀에 라벨(label)을 할당하고 같은 라벨은 "공통적인 특징"을 가진다고 가정한다. 이때 공통 특징은 물리적 의미가 없을 수도 있다. 픽셀이 비슷하게 생겼다는 사실은 인식하지만, 우리가 아는 것처럼 실제 물체 단위로 인식하지 않을 수 있는 것이다. 물론 세그멘테이션에는 여러 가지 세부 태스크가 있으며, 태스크에 따라 다양한 기준으로 객체를 추출한다.
시맨틱 세그멘테이션(semantic segmentation)이란?
세그멘테이션 중에서도 특히 우리가 인식하는 세계처럼 물리적 의미 단위로 인식하는 세그멘테이션을 시맨틱 세그멘테이션 이라고 한다. 쉽게 설명하면 이미지에서 픽셀을 사람, 자동차, 비행기 등의 물리적 단위로 분류(classification)하는 방법이라고 이해하면 된다.
인스턴스 세그멘테이션(Instance segmentation)이란?
시맨틱 세그멘테이션은 '사람'이라는 추상적인 정보를 이미지에서 추출해 내는 방법이다. 그래서 사람이 누구인지 관계없이 같은 라벨로 표현이 된다.
더 나아가서 인스턴스 세그멘테이션은 사람 개개인별로 다른 라벨을 가지게 한다. 여러 사람이 한 이미지에 등장할 때 각 객체를 분할해서 인식하자는 것이 목표이다.
Q. 위 이미지의 MS COCO 데이터셋 라벨은 인스턴스 세그멘테이션이라고 부를 수 있을까
A. 부를 수 있다. 사람이라는 한 클래스 내에서도 각각의 개체가 구분되어 있기 때문이다.
Q. 인스턴스 세그멘테이션 데이터셋과 라벨이 존재하면 시맨틱 세그멘테이션을 학습시킬 수 있을까?
A. 인스턴스의 id 뿐만이 아닌 클래스 라벨이 존재해야 시맨틱 세그멘테이션으로 학습이 가능하다. 근본적으로는 다른 개념이지만 대부분의 데이터셋은 인스턴스 라벨이 있는 경우 클래스 라벨도 함께 존재한다.
📖 셸로우 포커스 만들기_(3) 시맨틱 세그멘테이션 다뤄보기
세그멘테이션 문제에는 FCN, SegNet, U-Net 등 많은 모델이 사용된다. 오늘은 그중에서 DeepLab이라는 세그멘테이션 모델을 이용해본다. DeepLab 알고리즘(DeepLab v3+)은 세그멘테이션 모델 중에서도 성능이 매우 좋아 최근까지도 많이 사용되고 있다.
Q. DeepLab에서 atrous convolution을 사용한 이유가 무엇인가?
A. receptive field를 넓게 사용하기 위해 사용합니다. (적은 파라미터로 필터가 더 넓은 영역을 보게 하기 위해)
Q. Depthwise separable convolution은 어디서 처음 사용한 것 일까? 왜 이 구조가 효율적일까?
A. Xception 에서 제안되다. 3x3 conv layer 의 receptive field를 1/9 수준의 파라미터로 구현할 수 있기 때문에 효율적이다.
DeepLab 모델을 준비한다. 아까 준비한 PixelLib
를 이용하면 편하게 사용할 수 있다.
먼저 PixelLib
에서 제공해 주는 모델을 다운로드한다.
# 저장할 파일 이름을 결정합니다
model_dir = os.getenv('HOME')+'/aiffel/human_segmentation/models'
model_file = os.path.join(model_dir, 'deeplabv3_xception_tf_dim_ordering_tf_kernels.h5')
# PixelLib가 제공하는 모델의 url입니다
model_url = 'https://github.com/ayoolaolafenwa/PixelLib/releases/download/1.1/deeplabv3_xception_tf_dim_ordering_tf_kernels.h5'
# 다운로드를 시작합니다
urllib.request.urlretrieve(model_url, model_file)
그다음 다운로드한 모델을 이용해 PixelLib로 우리가 사용할 세그멘테이션 모델을 생성한다.
model = semantic_segmentation()
model.load_pascalvoc_model(model_file)
이제 모델에 이미지를 입력해본다.
segvalues, output = model.segmentAsPascalvoc(img_path)
segmentAsPascalvoc
라는 함수 이름에서 세그멘테이션 모델에 대한 힌트를 얻을 수 있는다. PASCAL VOC 데이터로 학습된 모델을 이용한다는 의미이다. 모델마다 학습시킨 데이터에 따라 결괏값이 달라지므로 어떤 데이터를 학습시켰는지 확인하는 것은 매우 중요하다.
PASCAL VOC 데이터의 라벨 종류는 아래와 같다.
LABEL_NAMES = [
'background', 'aeroplane', 'bicycle', 'bird', 'boat', 'bottle', 'bus',
'car', 'cat', 'chair', 'cow', 'diningtable', 'dog', 'horse', 'motorbike',
'person', 'pottedplant', 'sheep', 'sofa', 'train', 'tv'
]
len(LABEL_NAMES)
# 실행결과
21
background를 제외하면 20개의 클래스가 있다. 따라서 20
의 의미는 tv
가 된다.
우리는 이 중 사람(person
) 라벨, 즉 15
를 가진 영역만 검출해 내고 싶다. 이제 사람만 어떻게 추출할 수 있을지 알아보자.
아까 모델에서 나온 출력값을 살짝 살펴본다.
plt.imshow(output)
plt.show()
segvalues
# 실행결과
{'class_ids': array([ 0, 9, 15]),
'masks': array([[False, False, False, ..., False, False, False],
[False, False, False, ..., False, False, False],
[False, False, False, ..., False, False, False],
...,
[False, False, False, ..., True, False, False],
[False, False, False, ..., False, False, False],
[False, False, False, ..., False, False, False]])}
for class_id in segvalues['class_ids']:
print(LABEL_NAMES[class_id])
# 실행결과
background
chair
person
output
에는 세그멘테이션이 된 결과가 각각 다른 색상으로 담겨 있다. segvalues
에는 class_ids
와 masks
가 있다. class_ids
를 통해 어떤 물체가 담겨 있는지 알 수 있다.
이제 물체마다 output
에 어떤 색상으로 나타나 있는지만 알아내면 된다.
# 아래 코드를 이해하지 않아도 좋습니다
# PixelLib에서 그대로 가져온 코드입니다
# 주목해야 할 것은 생상 코드 결과물이예요!
colormap = np.zeros((256, 3), dtype = int)
ind = np.arange(256, dtype=int)
for shift in reversed(range(8)):
for channel in range(3):
colormap[:, channel] |= ((ind >> channel) & 1) << shift
ind >>= 3
colormap[:20]
# 실행결과
array([[ 0, 0, 0],
[128, 0, 0],
[ 0, 128, 0],
[128, 128, 0],
[ 0, 0, 128],
[128, 0, 128],
[ 0, 128, 128],
[128, 128, 128],
[ 64, 0, 0],
[192, 0, 0],
[ 64, 128, 0],
[192, 128, 0],
[ 64, 0, 128],
[192, 0, 128],
[ 64, 128, 128],
[192, 128, 128],
[ 0, 64, 0],
[128, 64, 0],
[ 0, 192, 0],
[128, 192, 0]])
PixelLib
에 따르면 위와 같은 색을 사용했다고 한다. 그럼 사람을 나타내는 15번째 색상은 무엇일까?
colormap[15]
# 실행결과
array([192, 128, 128])
사람 외에도 다른 물체를 찾아내고 싶다면 colormap[class_id]
처럼 찾아낼 수 있다.
여기서 주의할 것은 output
이미지가 BGR 순서로 채널 배치가 되어 있다는 점이다. colormap
은 RGB 순서이다.
그러니 우리가 추출해야 하는 색상 값은 순서를 아래처럼 바꿔 줘야 한다.
seg_color = (128,128,192)
이제 seg_color
로만 이루어진 마스크를 만들어 보자.
# output의 픽셀 별로 색상이 seg_color와 같다면 1(True), 다르다면 0(False)이 됩니다
seg_map = np.all(output==seg_color, axis=-1)
print(seg_map.shape)
plt.imshow(seg_map, cmap='gray')
plt.show()
# 실행결과
(450, 800)
채널 가졌던 원본과는 다르게 채널 정보가 사라졌다.
이제 아래처럼 물체가 있는 위치는 1(True), 그 외에는 0(False)인 배열이 되었다.
[
[ 0 1 1 0 0 0 1 1 0 ],
[ 1 1 1 1 0 1 1 1 1 ],
[ 0 1 1 1 1 1 1 1 0 ],
[ 0 0 1 1 1 1 1 0 0 ],
[ 0 0 0 1 1 1 0 0 0 ]
]
원래 이미지와 겹쳐 보면 세그멘테이션이 얼마나 잘 되었는지 알아보기 쉽다.
img_show = img_orig.copy()
# True과 False인 값을 각각 255과 0으로 바꿔줍니다
img_mask = seg_map.astype(np.uint8) * 255
# 255와 0을 적당한 색상으로 바꿔봅니다
color_mask = cv2.applyColorMap(img_mask, cv2.COLORMAP_JET)
# 원본 이미지와 마스트를 적당히 합쳐봅니다
# 0.6과 0.4는 두 이미지를 섞는 비율입니다.
img_show = cv2.addWeighted(img_show, 0.6, color_mask, 0.4, 0.0)
plt.imshow(cv2.cvtColor(img_show, cv2.COLOR_BGR2RGB))
plt.show()
📖 셸로우 포커스 만들기_(4) 배경 흐리게 하기
이번에는 blur()
함수를 이용해 배경을 흐리게 만든다.
# (13,13)은 blurring kernel size를 뜻합니다
# 다양하게 바꿔보세요
img_orig_blur = cv2.blur(img_orig, (13,13))
plt.imshow(cv2.cvtColor(img_orig_blur, cv2.COLOR_BGR2RGB))
plt.show()
흐려진 이미지에서 세그멘테이션 마스크를 이용해서 배경만 추출한다.
img_mask_color = cv2.cvtColor(img_mask, cv2.COLOR_GRAY2BGR)
img_bg_mask = cv2.bitwise_not(img_mask_color)
img_bg_blur = cv2.bitwise_and(img_orig_blur, img_bg_mask)
plt.imshow(cv2.cvtColor(img_bg_blur, cv2.COLOR_BGR2RGB))
plt.show()
bitwise_not
함수를 이용하면 이미지가 반전된다. 원래 마스크에서는 배경이 0, 사람이 255였으니 bitwise_not
연산을 하고 나면 배경은 255, 사람은 0이 된다.
반전된 세그멘테이션 결과를 이용해서 이미지와 bitwise_and
연산을 수행하면 배경만 있는 영상을 얻을 수 있다. 0과 어떤 수를 bitwise_and
연산을 해도 0이 되기 때문에 사람이 0인 경우에는 사람이 있던 모든 픽셀이 0이 된다. 결국 사람이 사라지게 되는 것이다.
📖 셸로우 포커스 만들기_(5) 흐린 배경과 원본 영상 합성
이제 배경 영상과 사람 영상을 합치기만 하면 된다.
img_concat = np.where(img_mask_color==255, img_orig, img_bg_blur)
plt.imshow(cv2.cvtColor(img_concat, cv2.COLOR_BGR2RGB))
plt.show()
세그멘테이션 마스크가 255인 부분만 원본 이미지 값을 가지고 오고 아닌 영역은 블러된 이미지 값을 사용한다. np.where(조건, 참일때, 거짓일때)의 형식의 코드를 사용했다.
인물과 배경의 거리 차이가 있고 거리가 일정할수록 더 자연스러워 보인다.
세그멘테이션의 한계
Semantic segmentation의 부정확성이 여러 가지 문제를 발생시키는 주요 원인이다. 피사계심도를 이용한 보케(아웃포커싱) 효과는 말 그대로 심도를 표현하기 때문에 초점이 잡힌 거리를 광학적으로 아주 섬세하게 구별(segmentation) 하지만 이를 따라 한 semantic segmentation 모듈은 정확도가 1.00 이 되지 않는 한 완벽히 구현하기는 힘들다.
피사계 심도 이해하기
우선 피사계심도의 개념부터 명확히 이해해 보자.
3D Camera 활용하기
이미지를 2D 방식으로 받아들이는 한계를 넘어 3D로 인식하려는 시도는 꽤 오래전부터 계속되어 왔다. 기술도 이제는 상당한 수준에 이르렀다. 크게는 스테레오 비전, ToF 방식 등이 자주 사용된다. 하드웨어 이미지 센서를 통한 3D 이미징 기술이 많이 발전되었는데, 아래 참고 자료를 통해 최신 기술을 살펴봐도 좋다.
소프트웨어 기술 활용하기
하드웨어 개발과 생산에는 비용이 많이 들기 때문에 제한된 하드웨어를 이용하면서 소프트웨어로 그 한계를 극복하려는 노력도 많다. 구글의 struct2Depth가 대표적인 예일 것이다.
다른 기술과 융합해 보기
구글 pixel4에는 IR(적외선) 카메라가 달려있다. 물체의 온도를 측정하는 IR 카메라와 3D 이미지는 크게 관련이 없을 것 같지만, 이를 통해 보다 멋진 3d depth sensing이 가능하다고 한다.
uDepth: Real-time 3D Depth Sensing on the Pixel 4
Author And Source
이 문제에 관하여(AIFFEL(220125)_인물사진을 만들어 보자), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://velog.io/@mjk3136/AIFFEL220125인물사진을-만들어-보자저자 귀속: 원작자 정보가 원작자 URL에 포함되어 있으며 저작권은 원작자 소유입니다.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)