파이썬 예제 (얼굴, 눈 인식)

대학교 졸업 후 DVR (Digital Video Recorder) 을 프로그래밍할 기회가 있었습니다.

DVR은 쉽게 다수의 CCTV 카메라 영상을 녹화해 저장하는 장치입니다.

제가 만든 제품이 부산 사직야구장에 설치되었으며, 현재는 시간이 오래되어 아마 다른 제품이 설치되어 있을 것입니다.

당시, 녹화용량을 줄이기 위해 동영상의 정지 영상을 한 프레임씩 가져와 이전 프레임과 비교해 픽셀의 변화가 있는 경우 (움직임 감지) 저장하는 방식으로 구현했던 기억이 납니다.

요즘은 영상처리기술이 얼마나 진보했나 싶어 자료를 찾아보다 OpenCV 를 이용해 쉽게 얼굴, 눈, 전신, 상체, 하체 등을 인식하는 방법이 있어 소개합니다.

이 프로그램은 Python + OpenCV + PyQt5를 이용해 제작되었습니다.

프로그램 실행파일의 경로에 아래에 소개된 Haar Cascades.xml 파일이 같이 위치해야 합니다.

Pyinstaller로 제작된 실행파일 링크

(PyQt5와 OpenCV 모듈에 필한 dll, lib 가 모두 single exe로 포함되어 용량(약 80MB)이 큽니다) 

좋아하는 배우인 이선균씨 얼굴을 핸드폰에 띄우고 노트북 웹캠으로 얼굴, 눈을 인식해 봤습니다.

[실행 화면]



OpenCV 를 이용해 만들었으므로, 컴퓨터 비전에 대한 지식이 없어도 무방합니다.

아래 동영상은 포토샵의 '레나' 이미지를 인식시켜 본 결과입니다.


 

OpenCV 란?

  • 실시간 컴퓨터 비전을 목적으로 인텔에서 C++로 만든 크로스 플랫폼 라이브러리
  • 파이썬에서도 python -m pip install opencv-python 으로 설치 후 사용 가능
[VS OpenCV 설치 모습]

소스코드

2개의 파일 (main.py video.py) 로 구성되어 있습니다.

main.py 소스코드

import sys
from PyQt5.QtWidgets import *
from video import *

QApplication.setAttribute(Qt.AA_EnableHighDpiScaling, True)

class CWidget(QWidget):

    def __init__(self):
        super().__init__()    
        size = QSize(600,500)
        self.initUI(size)
        self.video = video(self, QSize(self.frm.width(), self.frm.height()))

    def initUI(self, size):
        vbox = QVBoxLayout()        
        # cam on, off button
        self.btn = QPushButton('start cam', self)
        self.btn.setCheckable(True)
        self.btn.clicked.connect(self.onoffCam)
        vbox.addWidget(self.btn)

        # kind of detection
        txt = ['full body', 'upper body', 'lower body', 'face', 'eye', 'eye glass', 'smile']       
        self.grp = QButtonGroup(self)
        self.grp = QButtonGroup(self)
        for i in range(len(txt)):
            btn = QCheckBox(txt[i], self)
            self.grp.addButton(btn, i)
            vbox.addWidget(btn)   
        vbox.addStretch(1)
        self.grp.setExclusive(False)
        self.grp.buttonClicked[int].connect(self.detectOption)
        self.bDetect = [False for i in range(len(txt))]
                
        # video area
        self.frm = QLabel(self)     
        self.frm.setFrameShape(QFrame.Panel)
        
        hbox = QHBoxLayout()
        hbox.addLayout(vbox)       
        hbox.addWidget(self.frm, 1)        
        self.setLayout(hbox)
       
        self.setFixedSize(size)
        self.move(100,100)
        self.setWindowTitle('OpenCV + PyQt5')
        self.show()

    def onoffCam(self, e):
        if self.btn.isChecked():
            self.btn.setText('stop cam')
            self.video.startCam()
        else:
            self.btn.setText('start cam')
            self.video.stopCam()            

    def detectOption(self, id):
        if self.grp.button(id).isChecked():
            self.bDetect[id] = True
        else:
            self.bDetect[id] = False
        #print(self.bDetect)
        self.video.setOption(self.bDetect)

    def recvImage(self, img):        
        self.frm.setPixmap(QPixmap.fromImage(img))

    def closeEvent(self, e):
        self.video.stopCam()  


if __name__ == '__main__':
    app = QApplication(sys.argv)
    w = CWidget()
    sys.exit(app.exec_())

PyQt5 를 이용해 윈도우 창을 띄우고 체크박스, 버튼 등 컨트롤을 생성하는 역할을 담당하는 CWidget 클래스로 구성되어 있습니다.

실행시 아래 윈도우 창을 띄웁니다.

[main.py 기본 화면 구성]

9번 라인 생성자 __init__() 함수의 video class 는 차후 소개할 video.py 파일에 정의된 클래스의 객체 입니다.

카메라를 연결하고, 영상을 읽어 한 프레임씩 짤라 분석하는 핵심역할을 담당합니다.

15번 라인 생성자함수에서 호출되는 initUI() 함수는 Haar Cascades (하르 영상 객체 검출)를 위해 체크 박스 생성후 감지할 수 있는 검출기 종류를 체크박스로 선택할 수 있도록 구성합니다.

또한 QPushButton 을 이용해 웹캠의 연결 On, Off 를 제어합니다.

Haar Cascades 알고리즘에 대한 내용은 인터넷을 통해 한 번 찾아 보시기 바랍니다.

OpenCV 홈페이지에서 미리 학습된 Haar 검출기 (*.xml파일)를 다운로드 가능합니다.


[openCV 홈페이지 다운화면]

이후 다운 받은 *.zip 파일을 풀면 아래 *.xml 파일이 포함되어 있습니다.

[haar cascades XML 파일들]

이 파일들은 아래에 소개할 video.py 파일에서 불러들인 후 이미지의 특징을 검출하는데 쓰일 검출기 파일 입니다.

main.py 코드를 요약하자면, 윈도우 창을 만들어내는 역할을 담당할 뿐 영상처리와 관계된 코드는 전혀 없습니다.


video.py 소스코드

import cv2
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from threading import Thread
import time

class video(QObject):

    sendImage = pyqtSignal(QImage)

    def __init__(self, widget, size):
        super().__init__()
        self.widget = widget
        self.size = size
        self.sendImage.connect(self.widget.recvImage)        
                
        files = ['haarcascade_fullbody.xml',
                'haarcascade_upperbody.xml',
                'haarcascade_lowerbody.xml',
                'haarcascade_frontalface_default.xml',
                'haarcascade_eye.xml',
                'haarcascade_eye_tree_eyeglasses.xml',
                'haarcascade_smile.xml']
     
        self.filters = []
        for i in range(len(files)):
            filter = cv2.CascadeClassifier(files[i])
            self.filters.append(filter)     

        self.option = [False for i in range(len(files))]
        self.color = [QColor(255,0,0), QColor(255,128,0), QColor(255,255,0), QColor(0,255,0), QColor(0,0,255), QColor(0,0,128), QColor(128,0,128)]        
            
    def setOption(self, option):
        self.option = option        

    def startCam(self):
        try:
            self.cap = cv2.VideoCapture(0, cv2.CAP_DSHOW)
        except Exception as e:
            print('Cam Error : ', e)
        else:
            self.bThread = True
            self.thread = Thread(target=self.threadFunc)
            self.thread.start()

    def stopCam(self):        
        self.bThread = False
        bopen = False
        try:
            bopen = self.cap.isOpened()
        except Exception as e:
            print('Error cam not opened')
        else:
            self.cap.release()

    def threadFunc(self):
        while self.bThread:
            ok, frame = self.cap.read()
            if ok:
                # detect image                
                gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
                for i in range(len(self.filters)):
                    if self.option[i]:
                        detects = self.filters[i].detectMultiScale(gray, 1.1, 5)
                        for (x, y, w, h) in detects:
                            r = self.color[i].red()
                            g = self.color[i].green()
                            b = self.color[i].blue()
                            cv2.rectangle(frame, (x,y),(x+w,y+h), (b,g,r), 2)              

                # create image
                rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
                h, w, ch = rgb.shape
                bytesPerLine = ch * w
                img = QImage(rgb.data, w, h, bytesPerLine, QImage.Format_RGB888)
                resizedImg = img.scaled(self.size.width(), self.size.height(), Qt.KeepAspectRatio)
                self.sendImage.emit(resizedImg)
            else:
                print('cam read errror')

            time.sleep(0.01)

        print('thread finished')

코드는 video 라는 하나의 class로 구성되어 있습니다.

1번 라인에서 openCV 모듈(cv2)을 불러옵니다.

9번라인 sendImage는 카메라 영상을 메인 윈도우로 보내 출력하는 사용자 정의 시그널입니다. 15번 라인을 보면 이 시그널이 main.py 의 recvImage() 슬롯 함수와 연결되는 코드를 확인 할 수 있습니다.

즉 사용자 정의 신호를 송출(emit) 하면 main.py의 recvImage() 함수가 호출됩니다.

17번 라인 files 리스트는 앞서 설명한 haar cascades 검출기 XML 파일의 경로 입니다.

다양한 검출기가 있지만 7가지 정도만 선정해 리스트에 저장해 둡니다.

실행파일과 같은 위치에 복사해야 하며, 경로가 맞지 않다면 아래와 같은 오류가 발생합니다.

[haar 검출기파일 경로 오류]

25~28번 라인은 검출기 files 리스트에서 파일 경로를 불러와 openCV 분류기를 생성하는 코드입니다.

생성된 분류기 필터는 self.filters 리스트에 추가합니다.

30번 라인 self.option은 윈도우에 생성된 체크박스의 체크 여부를 받아와 저장하는 bool 타입 리스트 입니다. 체크된 분류 (예를 들면 얼굴, 눈 ) 만 검출하기 위한 용도입니다.

36번 라인 startCam() 함수는 윈도우 'cam start' 버튼을 눌렀을때 호출되며, 38번 라인에서 웹캠을 연결합니다.

웹캠이 없을수도 있으므로 try 구문을 통해 처리하며, 웹캠이 연결되면 thread를 생성합니다.

46번 라인 stopCam() 함수는 윈도우 'cam stop' 토글 버튼을 누르면 호출되며, thread를 중지하고 웹캠의 연결을 종료합니다.

56번 라인 threadFunc()는 쓰레드가 호출하는 대상함수이며, 아래와 같이 동작합니다.

[검출 순서]
1. self.cap.read() 함수로 카메라 프레임 얻기 (ok: 성공여부, frame: 정지영상이미지)

2. cv2.cvtColor() 함수로 이미지를 흑백화

3. haar 검출기 수 만큼 반복, 만약 해당 필터가 윈도우에 선택(체크박스)되어 있다면

4. 흑백이미지에서 해당 검출기 대상이 있는지 검사, 있다면 detects 리스트 저장

5. 만약 얼굴을 찾는다면 찾은 얼굴(detects 리스트)이 여러개 일 수 있으므로, 다시 반복

6. 검출된 영역을 cv2.rectange() 함수로 원본 컬러 이미지에 사각형 표시

요약하자면 동영상의 한 프레임을 얻어, 흑백으로 변환하고 검출기 필터를 이용해 대상 찾기 후 사각형으로 찾은 원본 이미지에 표시

아직 끝이 아닙니다.

openCV를 통해 검출된 이미지를 PyQt에서 사용가능한 QImage형태로 변환하는 작업이 72~76 라인에서 이루어집니다.

openCV는 BGR을 사용하는데 Qt는 RGB를 쓰므로 변환해 QImage를 생성하고, 윈도우의 QLabel 크기에 맞도록 리사이즈해 윈도우에서 출력하도록 합니다.


이상으로 코드 분석을 마칩니다.

댓글

  1. sleep을 준 이유 관련해서 설명 조금 여쭈어도 될까요

    답글삭제
    답글
    1. 아래 두가지 이유로 넣어두었습니다.

      1. 테스트 도중 CPU 점유율 (70~90%) 이 너무 높아 팬소리가 시끄러워서...^^

      2. 쓰레드 내부에서 한 프레임씩 캡쳐해 메인위젯으로 이미지를 보내는 방식인데 0.01초 쉬면 이론상 1초에 100프레임 (30 fps면 충분) 보내게 되는데 이것도 성능이 제대로 다 나오지 않아 마음의 평화를 위해 슬립을 넣어두었습니다.

      결론적으로 없어도 별 상관 없습니다.

      삭제
  2. 파이썬 완전 쌩초보입니다. XML 파일을 실행 파일과 같은 위치에 복사하라고 했는데 그 위치가 main.py 파일이 있는 디렉토리가 맞나요?

    답글삭제
  3. 안녕하세요. 올리신 튜톨 잘 봤습니다. MP3 Pyqt부분을 따라해 보았는데 궁금한 질문이 이쪽과 맞아 질문 드립니다.

    제가 영상컨덴츠쪽으로 일하고 있는데 프로그래밍을 메인으로 하는 일이 아니다보니 개발 방향에 있어 어떤 언어가 원하는 개발에 맞는지 잘 판단이 안서네요.

    개발하고 싶은 게 촬영된 얼굴영상(음성포함)에서 실시간으로 음성을 텍스트화, 얼굴트래킹(표정), 감정을 유추할 수 있는 음원정보(덩어리 형태로 음높이나 템포)를 뽑아다 디지털 캐릭터 얼굴에다 입히고 싶은데 원본영상 재생 위치를 조절할때 같이 실시간으로 지연없이 캐릭터가 따라움직이게 하고 싶은데...
    ~~개발 언어를 Python로 하는게 나을지 C++이 나을지 잘 모르겠네요. 아니면 하나를 메인으로 하고 둘을 다 써야하는지.~~ 필요한 모든 기술을 개발하려는 건 아니고 완성되어 있는 부분별 기술들 API나 모듈을 끌어다 조합해 쓰려고 합니다.

    Python이 속도면에서 C++보다 느릴것 같아 시간지연이 원본영상과 결과물 사이에 왠지 발생할 것 같은데 C++도 파이썬만큼 음성,영상인식,텍스트 변환, AI이용 같은 그런 분야로 확장성이나 공부할 수 있는 자료풀이 넓은지 알고싶네요. Mp3 튜톨 따라하면서 QtMultimedia나 Qaudio 같은 클래스에 음원정보를 얻을 수 있는 쓸만한 함수가 있나 싶어 봤는데 많이 부족한 것 같고.. Qt쪽으로 파고드는 건 원하는 개발 주제랑 아닌 것 같아서 질문 드립니다.

    답글삭제
    답글
    1. 게시물과 관련없는 내용은 답변드리지 않음을 양해바랍니다.

      참고로 얼굴, 감정 인지, 자연언어처리 분야는 박사과정에서 다루는 쉽지않은 주제입니다.

      만약 구현에 촛점을 맞춘다면 MS의 Azure, Google GCP, Amazon의 클라우드 플랫폼에서 이미 제공하고 있습니다.

      삭제
    2. 네 답변 감사합니다. 답을 따로 찾아봐야겠네요. 범위가 광범위하고 비전문가 접근할 수 있는 서비스가 있는지, 제품마다 어떤 언어가 지원되는지 좀 더 알아봐야겠습니다.

      삭제

댓글 쓰기

이 블로그의 인기 게시물

Qt Designer 설치하기

C++ 예제 (소켓 서버, 이미지, 파일전송)