공부/AIFFEL

Going Deeper(CV)_DJ 13 : 내가 만든 카메라앱, 무엇이 문제일까?

dong_dong_2 2021. 4. 26. 17:30

1. 들어가며
    - 스티커앱! 3번째 과제(Exploration)에서 다뤘던 기억이 있다.
    - 노드에서는 내 사진(얼굴 인식이 잘 안되면 노드에 주어진 사진)을 가지고 머리에 왕관을 씌워보고, 과제에서는 연예인 사진에 고양이 수염을 붙이는 것을 했었다.
    - 이 때 카메라앱을 통해 얼굴인식, 랜드마크 탐색, 이미지 처리를 통한 보정 등의 가장 기본적인 개념을 아주 간단히 살펴봤다.
    - 하지만 상품으로 출시하기에는 완성도가 높지 않았다. 오늘은 그 당시 완성도가 높지 않은 이유를 살펴보고, 완성도 높은 앱을 만들기 위해 고려해야할 점을 정리해 보는 시간을 갖는다.
2. 스티커앱의 원리
    - 스티커앱의 원리에 익숙하지 않거나, 기억이 흐려졌을 수 있으니 다시 한 번 되짚어 보자.
    - 이미지에서 얼굴을 찾고 눈, 코, 입의 랜드마크를 검출한다.
    - 랜드마크를 기준으로 스티커를 합성하면 원하는 위치에 스티커가 따라다니게 된다.
    - dlib 패키지에서 얼굴 랜드마크는 68개의 점으로 이루어져 있었다.
    - 그럼 이미지 경계 밖에 스티커를 합성해야할 때 주의해야 할 점은 무엇이 있을까?
       1) numpy array에서 설정된 범위 밖의 index에 접근하면 에러가 난다.
       2) 즉, crop할 때 bbox의 slicing 값에 주의해야 한다.
3. 동영상이란
    - 저번에는 사진에 스티커앱을 적용했었다면, 오늘은 동영상에 적용할 것이다.
    - 동영상이 무엇인가?
       1) 이미 유튜브 등을 통해서 동영상에 많이 익숙해져있다.
       2) 동영상은 움직이는 영상이란 뜻이다.
       3) 대부분 사람들은 영상이라는 단어와 동영상을 많이 혼용해서 사용한다.
       4) 하지만 영어 변역을 보면 영상 -> image, 동영상 -> video로 구분되는 것을 알 수 있다.
       5) 동영상의 뜻에서 움직인다는 표현을 다시 생각해볼 필요가 있다.
       6) 동영상은 여러 개의 이미지(image frame)가 일정한 시간 간격으로 이어진 이미지의 시퀀스(image sequence)이다.
       7) 따라서 매 프레임의 이미지마다 스티커를 적용해서 다시 동일한 간격으로 이어 붙이면 스티커가 적용된 동영상을 만들 수 있다.
    - 동영상에서의 용어 정리
       1) Frame : 동영상 특정 시간대의 이미지 1장
       2) FPS : frame per second의 약자로 초당 프레임 수
    - 게임을 즐기는 사람이라면 FPS라는 표현을 많이 들어 봤을 것이다. (First-Person Shooter가 아니다)
    - FPS(First-Person Shooter)류의 게임을 할 때 최소 60fps(frame per second)를 만족하도록 장비를 구성했던 것 같다.
    - 이 때 60fps는 초당 60개의 이미지(frame)가 표시된다는 의미이다.
    - 참고로 인간은 최소 15fps의 동영상을 동영상으로 인식한다고 합다. 애니메이션이 15fps부터 시작한다고 한다.
    - 하지만 30fps부터는 자연스러운 동영상으로 느끼기 때문에 대부분의 동영상들이 30fps로 제작되고, 게임 등 빠른 순간을 잡아야하는 영역에서는 60fps, 144fps등을 사용한다고 한다.
    - 이론적으로는 사람의 눈으로 60fps 이상부터는 체감하기 힘들다고 한다.
4. 동영상 처리 방법
    - 노드에서는 1. 들어가며 부분에서 노드 진행에 필요한 파일을 다운로드 받았었다. 코드 실습에 관한 부분이라 적지 않았다.
    - 이번 스텝의 시작은 이 다운로드 받은 파일의 구성을 살펴보고 moviepy라는 패키지를 install하는 것으로 시작한다.
    - 그리고 코드 위주의 실습이 진행됐다. (동영상 확인하기)
       1) moviepy패키지의 editor에서 VideoFileClip과 ipython_display를 로딩했다.
          (1) VideoFileClip은 비디오 파일을 읽어올 때 사용되는 클래스이다.
          (2) ipython_display()는 동영상을 주피터 노트북에 랜더링할 수 있게 도와주는 함수이다.
       2) 비디오 파일의 크기가 HD(1280x720)이여서 랩탑에서 보기 쉽도록 크기를 줄였다.
       3) 이 비디오 클립을 ipython_display() 함수를 사용하면 쉽게 출력할 수 있다.
       4) loop와 autoplay는 각각 반복 재생과 자동 재생 옵션이다.
       5) 해당 코드는 아래의 사진과 같다.


    - moviepy를 이용하면 쉽게 비디오를 읽어오고 화면에 출력할 수 있다.
    - 실시간으로 동영상을 처리하면서 모든 프레임을 관찰하는 것은 쉬울까? 어렵다!
    - 동영상을 처리하기 위해서는
       1) 동영상을 읽고
       2) 프레임별 이미지를 numpy 형태로 추출하고
       3) 얼굴 검출 같은 이미지 처리를 수행하고
       4) 다시 동영상으로 조합
    - 위 4가지의 과정이 필요하다.
    - moviepy는 동영상을 읽는 것은 쉽지만 numpy 데이터로 변환하기 위한 과정을 거쳐야 하고,
    - 프레임 이미지에 검출 모델을 적용하더라도, 결과 동영상을 확인하기 위해서는 동영상 파일을 저장했다가 다시 읽어야 하기 때문이다.
    - 이런 문제로 인해 동영상을 다룰 때는 주피터 노트북이 아닌 코드 에디터와 터미널을 이용해서 작업을 한다고 한다.
    - 그래서 우리도 에디터와 터미널을 사용해서 실습을 했다.
    - 그리고 py파일 하나를 실행시키고 코드를 하나하나 살펴봤다. 해당 코드는 아래의 사진과 같다.


       1) 5행에서 원하는 동영상을 읽는다. 오디오 정보는 포함되지 않으며 비디오의 기술자(descriptor) 역할을 한다.
       2) 8행에서 동영상이 갖고 있는 정보를 vc의 get() 함수로 읽을 수 있다. FRAME_COUNT은 비디오 전체 프레임 개수를 의미한다.
       3) 13행에서 vc객체에서 read()함수로 img를 읽는다. ret는 read() 함수에서 이미지가 반환되면 True, 반대면 False를 받는다.
       4) 15행에서 ret값이 False이면 프로그램을 종료시킨다.
       5) 19행에서 이미지를 출력한다.
       6) 20행에서 waitkKey() 함수 파라미터에 wait time 값을 적절히 넣으면 루프를 돌면서 이미지가 연속적으로 화면에 출력된다.
          (1) 이미지를 연속으로 재생시키면 동영상을 볼 수 있게 된다.
          (2) 이 때 wait time 값은 ms 단위이다.
          (3) 보통 30fps를 갖는 동영상이 많으니 여기에 1대신 33을 입력하면 비슷한 동영상이 출력된다.
       7) 21행 ~ 22행은 key 값이 27을 받으면 멈춘다.
          (1) waitKey() 함수는 키보드가 입력될 때 키보드 값을 반환한다.
          (2) key == 27은 27번 키 값을 가지는 키보드 버튼이 입력될 때를 의미한다. 참고로 27번 키 값을 가지는 키보드 버튼은 ESC이다.
5. 이미지 시퀀스에 스티커앱 적용하기
    - 이제 동영상이 시간 순서대로 나열된 이미지의 조합이란 것을 알았다.
    - 그리고 OpenCV를 통해 개별 프레임에 하나씩 접근하는 법도 경험했다.
    - 이제 본격적으로 동영상에 스티커앱을 적용해 보자.
    - 이번 실습에서는 다운로드 받은 파일 중 addsticker라는 파일에 대해서 알아봤다.
       1) 정지된 이미지를 위해 스티커앱을 만든다면 스티커가 머리 위로 이미지 경계를 벗어나는 상황은 크게 고려하지 않아도 될 지 모른다.
       2) 하지만 동영상은 다양한 환경이 나올 수 있기 때문에 이 경우도 고려해 주어야 한다.
       3) 이 코드에서는 refined_y가 0보다 작을 때 img_sticker[-refined_y:]만 표시하게 했다.
       4) 그리고 이전 코드처럼 비디오 기술자 vc에서 img를 읽는다.
       5) 이 img를 img2sticker_orig()에 입력한다. 그럼 스티커가 적용된 이미지가 출력된다. imshow 함수로 img_result를 화면에 렌더링 한다.
       6) img2sticker_orig() 함수의 시간을 측정하기 위해 함수 앞뒤로 시간 측정 코드도 추가했다.
       7) OpenCV에서는 getTickCount()와 getTickFrequency()를 사용해서 시간을 측정한다.
       8) 초 단위로 나오기 때문에 보통 이미지 한 장을 처리할 때 1000을 곱해 ms단위로 프로그램 속도를 관찰한다.
       9) 보통 비디오는 30fps이기 때문에 프레임당 ms 처리 단위를 가지기 때문이다.
6. 동영상 저장하기
    - 꼭 필요한 부분은 아니지만 OpenCV로 처리한 동영상을 저장하는 방법이 궁금할까봐 이번 스텝을 준비했다고 한다.
    - 영상처리 개발 업무를 담당하면 리더와 동료에게 결과를 공유할 일이 빈번히 일어난다. 동영상을 공유하는 것이 큰 도움이 되기 때문에 알아두면 좋다고 한다.
    - 해당 스텝은 savevideo라는 파일에 대해 다뤘다.
       1) VideoWriter로 동영상 핸들러를 정의한다. 저장할 파일 이름, 코덱정보, fps, 동영상 크기를 파라미터로 입력한다.
       2) fourcc는 four character code의 약자로, 코덱의 이름을 명기하는 데 사용한다.
       3) 컴퓨터 os 등에 따라 지원되는 코덱이 다르므로 avc1, mp4v, mpeg, x264 등에서 적절히 선택해서 사용하면 된다.
       4) 해당 노드를 진행하는 노트북(AIFFEL에서 제공해준)은 우분투 os를 사용하고 있어서 mp4v를 사용했다.
       5) 재생이 잘 되기 위해서는 코덱을 적절히 고르는 것이 중요하다. 입력된 동영상과 같은 코덱이면 조금 수월하다.
       6) 아래의 이미지는 입력 동영상의 fourcc를 알아내는 방법이다.


       7) 프레임 수를 얻었던 방법처럼 여기서도 get()을 사용한다. 이 때 얻을 수 있는 값은 정수형이기 때문에 비트연산을 이용해서 char 형태로 변경한다.
       8) 해당 코드를 사용해서 이번에 사용하는 video2라는 파일은 avc1을 사용하고 있는 것을 확인할 수 있었다.
       9) 최종적으로, 원본 동영상에 스티커를 붙여 합성한 영상을 다시 저장한다.
7. 동영상에서 문제점 찾기
    - 동영상을 돌려보니 몇 가지 문제점이 보였다. 아래에서 하나씩 확인해보자.
    - 속도가 너무 느리다.
       1) 앞서 실행한 코드는 개별 프레임을 처리하는 시간을 측정해서 터미널에 출력한다.
       2) 프로그램을 실행시킬 때 속도는 프레임 한 장을 처리하는 데 105ms가 나왔다.
       3) 이렇게 처리 속도가 느리면 실제 사용자가 보게 되는 동영상 결과가 자연스럽지 않다.
       4) 이 프로그램은 약 10fps의 속도를 가지기 때문에 이전 스텝에서 addsticker를 실행했을 땐 동영상이라기보다 이미지가 한 장씩 넘어가는 느낌이 들었다.
       5) 프레임 처리 시간을 33ms 이하로 줄일 수 있다면 30fps를 만족할 것이다.
       6) 프로그램 내 병목이 있는 함수를 파악하고 시간이 오래 걸리는 함수를 개선해야 한다.
    - 스티커가 안정적이지 못하고 바들 떨린다.
       1) 동영상을 관찰해보면 거의 비슷한 구간임에도 스티커 합성이 자연스럽지 못하다.
       2) 어떤 것은 스티커 크기가 작고, 어떤 것은 스티커 크기가 크다. (비슷한 구도임에도)
       3) 왜 이런 현상이 벌어질까?
          (1) 얼굴 검출기의 바운딩 박스 크기가 매 프레임마다 변경되기 때문
          (2) 바운딩 박스의 x, y 위치를 측정할 때 에러가 존재 (가우시안 노이즈 형태)
       4) 첫 번째 이유가 발생하는 이유는 알고리즘 문제로 볼 수 있고, 두 번째 이유가 발생하는 이유는 학습 데이터의 라벨 정확도 문제로 볼 수 있다.
    - 고개를 돌리는 등 구도가 바뀌면 스티커가 자연스럽지 못하다.
       1) 머리를 좌우로 돌리는 경우나 고개를 갸우뚱 하는 경우 스티커도 맞춰서 모습이 변해야 하지만 그렇지 않다.
       2) 왜 이럴까? 바운딩 박스가 얼굴의 각도를 반영하지 못하기 때문이다.
       3) 특히 3차원 공간을 2차원으로 투영하기 때문에 고개를 좌우로 돌리면서 갸우뚱 하는 경우는 원근(perspective)변환을 적용해야 한다.
       4) 이 변환을 하기 위해서는 3차원에서 yaw, pitch, roll 각도를 알고 있어야 한다.
    - 얼굴을 잘 못 찾는 것 같다.
       1) 카메라에서 조금만 멀어져도 얼굴을 찾기 힘들어한다.
       2) 얼굴 검출기의 성능이 낮기 때문이다.
       3) HOG 알고리즘을 기반으로 학습된 박스는 일정 크기 이상의 박스를 내놓는다.
       4) 얼굴 크기가 작을수록(멀리 있을수록) 학습된 박스 크기와 다르게 나올 가능성이 커진다.
    - 그럼 이 문제를 하나하나 해결해보자.
8. 실행시간 분석하기
    - 속도가 느린 문제를 확인하기 위해 img2sticker_orig() 함수 내부의 알고리즘 속도를 분석해본다. 이 함수는 크게 5단계로 나눠진다.
       1) 전처리 (Perprocess)
       2) 얼굴 검출 (Detection)
       3) 랜드마크 검출 (Landmark)
       4) 좌표 변환 (Coord)
       5) 스티커 합성 (Sticker)
    - 이 다섯 단계의 각각 시간을 측정해본다.
    - 이번에는 addsticker_timecheck라는 파일을 실습했다.
    - 이 파일을 실행해 보면 실행 시간 대부분이 d, detection에서 소모되는 것을 알 수 있다.
    - 약 96% 정도의 시간이 검출 단계에서 소모된다. 검출 시간을 줄이는 것을 가장 큰 목표로 해야 한다.
    - 그런데 검출기는 왜 느린걸까?
9. 얼굴 검출기 이해하기 - 속도, 안정성
    - dlib 얼굴 검출기(face detector)는 HOG(histogram of oriented gradient) 기반 알고리즘 이다.
    - dlib은 HOG 특징 공간에서 슬라이딩 윈도우(sliding window) 기법을 통해 얼굴을 검출한다.
    - HOG와 슬라이딩 윈도우를 오랜만에 접해서 가물가물하다.
       1) 슬라이딩 윈도우와 분류기(classifier)의 관계는?
          (1) 모든 픽셀 위치에 대해 동일한 크기의 바운딩 박스와 reference feature 간의 이진 분류기 (binary classifier)
       2) 이미지 피라미드(image pyramid)를 사용하는 이유는?
          (1) 바운딩 박스에 들어오지 못하는 크기나 작은 객체를 찾기 위해
       3) 이미지 슬라이딩(image sliding) 방법의 속도를 예상해 보자. 더 빠르게 만들 수 있는 방법은 무엇이 있을까?
          (1) 이미지 위치에 독립적으로 시행 가능한 알고리즘이기 때문에 병렬처리가 가능할 듯 하다.
    - HOG 특성 맵(feature map)에서 입력 이미지 크기(HD : 1280x720)만큼 슬라이딩 윈도우를 수행하기 때문에 프로그램은 O(n^3)의 시간을 가진다. 당연히 느려질 수 밖에 없다.
       1) O(1280 * 720 * (bbox size) * 피라미드 개수) = O(n^3)
    - 속도 개선 방법 중 가장 간단한 방법은 사용되는 이미지 크기를 줄이거나 피라미드 수를 줄이는 방법이다.
    - 이번 실습은 addsticker_modified 파일을 실습했다.
    - 해당 코드를 사용하면 이론적으로는 가로, 세로 크기의 절반씩 전체 1 / 4 계산량이 감소하기 때문에 소모 시간도 25% 정도 되여야 한다.
    - 실제로는 29% 정도로 감소한 것을 확인했다.
    - landmark등 기타 단계 시간이 3ms 정도 되기 때문에 검출기는 약 27% 정도로 시간 소모를 줄였다고 생각할 수 있다.
    - 예외 처리 같은 단계가 검출기에 포함되기 때문에 2% 정도의 손실은 존재할 수 있다.
    - 이젠 33ms 이내 시간으로 실행할 수 있기 때문에 동영상처럼 재생된다.
10. 신호와 신호처리
    - 떨리는 스티커는 어떻게 안정화를 시킬까?
    - 일단 신호에 대해서 알아보자.
    - 신호(Signal)와 노이즈(Noise)
       1) 시간과 공간에 따라 변화하는 물리량을 나타내는 함수를 신호(signal)이라고 한다.
       2) 주로 시간과 관련된 물리량을 나타낼 때 많이 사용하고 있다.
       3) 대표적으로 우리가 매일 사용하는 220V 교류 전원이 있다. 우리나라는 초당 60번으로 진동하는 특징(220v 60hz)을 가지고 있다.
       4) 시간 축으로 그래프를 그리면 아래의 그림과 같이 나타난다.


       5) 위 이미지는 마치 사인(sin) 함수와 같은 형태로 파형을 이루고 있다. 하지만 실제 신호를 측정해 보면 깔끔한 형태의 파형이 잘 나오지 않는다. 마치 아래의 이미지 처럼.


       6) 신호에 노이즈(noise)가 섞이는 이유는 크게 두 가지이다.
          (1) 신호를 출력하는 모델의 노이즈
          (2) 신호를 측정할 때 생기는 노이즈
       7) 예를 들어 사람이 뛰는 장면을 측정한다고 상상해보자.
       8) 이 사람은 평균 20km/h의 속도로 달리고 있다.
       9) 이 때 생기는 노이즈를 생각해보면
          (1) 뛰는 사람이 힘들어서 19km/h로 뛰다가 다시 21km/h로 뛰는 경우
          (2) 측정 카메라가 사람 다리를 측정할 때, 머리를 측정할 때 속도가 달라지는 경우
      10) 이론적인 값을 얻기 힘들다는 것을 쉽게 알 수 있다. 
      11) 스티커앱도 마찬가지이다. 얼굴 검출기는 매 프레임 얼굴의 위치를 측정한다.
      12) 하지만 측정 오차로 인해 매번 같은 위치의 얼굴을 찾아내기 힘들다는 한계가 있다. 어떻게 해결해야 할까?
    - 신호처리(Signal Processing)
       1) 신호가 아닌 노이즈는 같은 시간 동안 신호의 크기가 급격하게 변화한다.
       2) 이런 특징을 주파수가 높다(high frequency)라고 표현한다.
       3) 그럼 주파수가 높은 성분만 제거하고 주파수가 낮은 신호만 통과하게 하면 되지 않을까?
       4) 이렇게 원본 신호를 우리가 원하는 신호 형태로 만드는 방법을 신호처리(signal processing)이라고 한다.
       5) 저역 주파수만 통과하게 만드는 기술은 신호처리 분야에서 널리 사용되어 왔다. LPF(low pass filter)라고 부르는 기술이다.
          (1) LPF를 구현하기 위해 어떤 방법을 사용할 수 있을까? -> 이전 5개 측정값의 평균을 이용한다.
          (2) 평균필터와 이동 평균 필터에 대해 말해보자. -> 평균 필터는 이전 측정값 전체의 평균이고, 이동 평균 필터는 최근 k개의 평균과 측정값에 대한 가중치를 부여한다.
          (3) LPF의 단점은 무엇인가? -> 이전값에 큰 영향을 받기 때문에 delay가 생긴다. 현재의 실제 값과 차이가 반드시 존재한다.
11. 칼만 필터
    - 칼만 필터(Kalman filter)는 시스템 모델과 측정 모델을 만들고 데이터 입력을 바탕으로 각 모델을 예측하는 알고리즘을 의미한다.
    - 예측된 모델을 바탕으로 현재의 실제 값을 추정할 수 있고 다음 시점의 모델 출력을 예측할 수 있다.
    - 이 때 시스템 모델과 측정 모델은 모두 선형(linear)이고 가우시안(gaussian) 분포를 따루는 경우를 가정한다.
       1) 칼만 필터에 사용하는 행렬들의 의미를 살펴보자.
          (1) A : 시스템 모델
          (2) Q : 시스템 오차 (노이즈)
          (3) H : 측정 모델
          (4) R : 측정 오차 (노이즈)
       2) 칼만 필터는 크게 두 단계로 나눌 수 있다.
          (1) 예측 단계
          (2) 추정 단계
       3) 측정 값이 사용되는 단계는 추정 단계이다.
       4) 칼만 이득(Kalman gain)의 의미는 무엇인가?
          (1) 측정값과 추정값 중 추정값에 영향을 미치는 가중치를 의미한다.
    - 칼만 필터 단계를 정리하면 아래 플로우 차트(flow chart)로 나타낼 수 있다.


    - 코드에서 얼굴 검출(detection)과 얼굴 랜드마크(landmark)는 프레임마다 가우시안 오차를 갖는 특정 시스템이라고 가정할 수 있다.
    - 라벨링 할 때 사람도 매번 같은 위치를 찍을 수 없으며 자연 상태의 측정값은 대체로 정규분포를 따르기 때문이다.
    - 얼굴 랜드마크에 칼만 필터를 적용하면 안정적인 스티커 결과를 얻을 수 있을 것으로 예상된다.
12. 칼만 필터 적용하기
    - 얼굴 랜드마크에 칼만 필터를 사용하기 위해 이미지 좌표에서 객체의 위치와 속도에 대한 모델을 만들어야 한다.
    - 이번 단계에서는 간단한 칼만 필터를 직접 만들어 봤다.
    - 코드로 실습했다.