안녕하세요.
이번에 챕터에서 진행되는 내용은 실시간으로 얼굴을 인식해서 마스크를 씌우는 프로그램을 만드는 것으로 진행됩니다. 프로그램의 기본 형틀은 앞에서 만든 GazerW를 기반으로 해서 레코딩과 모션 감지 부분을 제거하고 얼굴 인식하는 기능을 넣어서 작동하게 하는 것입니다.
1. 프로그램 틀 만들기
1) 폴더 내 수정
- GazerW 마지막 버전의 프로젝트 폴더를 복사해서 Facetious로 변경합니다. 이 글에서는 첫 번째 하는 것이라. Facetious_day1로 변경하였습니다.
- GazerW.pro 파일을 FacetiousW.pro로 변경합니다. 책 내용에서는 여러 가지를 내용을 수정해주어야 하지만, 폴더와 pro 파일에서는 현재 Windows 버전의 Qt에서는 크게 진행할 내용이 없습니다.
2) 파일 내 소스 수정
- 이제 FacetiousW.pro 파일을 Qt Creator로 열어서 capture_thread.h 파일 수정을 시작합니다. 수정이라기보다는 틀을 만들기 위해서 불필요한 부분을 삭제한다는 말이 맞을 듯합니다.
<capture_thread.h>
- 사용하지 않는 변수와 함수 선언을 삭제합니다.
// FPS calculating
bool fps_calculating;
int fps;
// video saving
// int frame_width, frame_height; // notice: we keep this line
VideoSavingStatus video_saving_status;
QString saved_video_name;
cv::VideoWriter *video_writer;
// motion analysis
bool motion_detecting_status;
bool motion_detected;
cv::Ptr<cv::BackgroundSubtractorMOG2> segmentor;
// 선언된 함수도 삭제
void startCalcFPS() {...};
void setVideoSavingStatus(VideoSavingStatus status) {...};
void setMotionDetectingStatus(bool status) {...};
// ...
signals:
// ...
void fpsChanged(int fps);
void videoSaved(QString name);
private:
void calculateFPS(cv::VideoCapture &cap);
void startSavingVideo(cv::Mat &firstFrame);
void stopSavingVideo();
void motionDetect(cv::Mat &frame);
// 자료형도 삭제
enum VideoSavingStatus
<capture_thread.cpp>
- 사용하지 않는 변수와 함수 정의를 삭제합니다.
- 생성자 내의 코드들을 삭제합니다.
CaptureThread::CaptureThread(int camera, QMutex *lock):
running(false), cameraID(camera), videoPath(""), data_lock(lock) {
}
CaptureThread::CaptureThread(QString videoPath, QMutex *lock):
running(false), cameraID(-1), videoPath(videoPath), data_lock(lock) {
}
- capture_thread.h 파일에서 삭제한 함수 구현부를 삭제해 줍니다.
- run() 함수 안에서는 video saving, motion detecting, and FPS calculating 부분을 삭제합니다. (컴파일 에러 나는 부분들)
<mainwindow.h>
- 위와 동일하게 video saving, motion detecting, and FPS calculating 선언한 부분을 삭제합니다.
void calculateFPS();
void updateFPS(int);
void recordingStartStop();
void appendSavedVideo(QString name);
void updateMonitorStatus(int status);
private:
// ...
QAction *calcFPSAction;
// ...
QCheckBox *monitorCheckBox;
QPushButton *recordButton;
// 재사용할 수 있는 부분은 삭제하지 않고 변경합니다.
void appendSavedVideo(QString name); 는 appendSavePhoto 로 변경합니다
QPushButton *recordButton; 은 QPushButton *shutterButton 로 변경합니다.
// 2개의 함수 선언을 추가합니다.
private slots:
void appendSavedPhoto(QString name);
void populateSavedList();
<mainwindow.cpp>
- mainwindow.h 파일에서 삭제한 함수들의 코드도 삭제합니다.
void calculateFPS();
void updateFPS(int);
void recordingStartStop();
void appendSavedVideo(QString name);
void updateMonitorStatus(int status);
- initUI() 함수에서 #ifdef GAZER_USE_QT_CAMERA #endif 사이에 선언된 부분에서 아래 내용만 남기고 삭제합니다. 그리고 #ifdef GAZER_USE_QT_CAMERA #endif 전처리 연산자도 삭제합니다.
//Main area
QGridLayout *main_layout = new QGridLayout();
imageScene = new QGraphicsScene(this);
imageView = new QGraphicsView(imageScene);
main_layout->addWidget(imageView, 0, 0, 12, 1);
- 해더 파일에서 변경한 record버튼을 다른 용도로 변경합니다.
shutterButton = new QPushButton(this);
shutterButton->setText("Take a Photo");
tools_layout->addWidget(shutterButton, 0, 0, Qt::AlignHCenter);
// 상태바의 내용도 변경합니다.
mainStatusLabel->setText("Facetious is Ready");
- createActions()에서 calcFPSAction action과 생성에 필요한 시그널(signal slot connection) 등을 삭제합니다.
- openCamera() 함수에서도 FPS calculating, the video saving과 연결된 signal slot connecti과 disconnect을 삭제합니다.
- 그리고 새로운 함수 2개를 추가합니다.
void MainWindow::populateSavedList()
{
// TODO
}
void MainWindow::appendSavedPhoto(QString name)
{
// TODO
}
<utilies.h, utilies.cpp>
- h 파일과 cpp파일에 있는 함수 이름을 먼저 변경하고 저장 경로도 변경합니다.
public:
static QString getDataPath();
static QString newPhotoName();
static QString getPhotoPath(QString name, QString postfix);
// GazerW로 되어 있는 경로들을 변경해줍니다.
QString Utilities::getDataPath()
{
QString user_pictures_path = QStandardPaths::standardLocations(QStandardPaths::PicturesLocation)[0];
QDir pictures_dir(user_pictures_path);
pictures_dir.mkpath("Facetious");
return pictures_dir.absoluteFilePath("Facetious");
}
- 코드상 이상 없다면 빌드를 시킵니다. 만약, 오타나 빨간색으로 나타나는 부분이 있다면 클릭해서 디버깅합니다. 첨부된 소스와 비교해 봅니다.
3) 프로그램 틀 잡은 소스 실행
- 오류가 없다면 다음과 같이 실행되는 것을 확인할 수 있습니다.
- 이 틀을 기반으로 프로그램 소스 코드를 추가하는 것으로 진행합니다.
2. 사진 촬영 기능 추가
- 초기화 작업한 소스코드에 사진을 찍는 소스코드를 추가해 보겠습니다.
<capture_thread.h>
- 사진 찍기에 필요한 함수와 시그널을 추가합니다.
- taking_photo 변수는 스레드 함수에서 사진 찍는다는 상태 값을 가지는 변수입니다.
- 함수의 설명은 아래 구현에서 간단하게 흐름을 설명하겠습니다.
public:
// ...
void takePhoto() {taking_photo = true; }
// ...
signals:
// ...
void photoTaken(QString name);
private:
void takePhoto(cv::Mat &frame);
private:
// ...
// take photos
bool taking_photo;
<capture_thread.cpp>
- 해더 파일에 선언했던 함수를 정의합니다. 그러기 전에 잠깐 흐름을 알아보겠습니다.
- "Take a Photo" 버튼을 누르면 mainwindow::takePhoto() 함수가 실행되면서 사진 찍는 명령어 들어왔다는 상태 변수를 true로 만들고 영상 재생 스레드(run())가 돌면서 taking_photo변수 상태를 확인한 후 현재 상태 프레임을 capture_thread에 있는 takePhoto() 함수 인자로 전달합니다.
takePhoto() 함수가 실행되면서 사진이 저장되고, photoTaken 시그널이 발생하여 appendSavedPhoto() 함수가 호출되어 이미지 리스트에 찍은 사진이 추가가 됩니다.
- 설명이 조금 헷갈리는데 "Take a Photo" 버튼을 기준으로 코드를 따라가면 이해하실 수 있습니다.
void CaptureThread::takePhoto(cv::Mat &frame)
{
QString photo_name = Utilities::newPhotoName();
QString photo_path = Utilities::getPhotoPath(photo_name, "jpg");
cv::imwrite(photo_path.toStdString(), frame);
emit photoTaken(photo_name);
taking_photo = false;
}
- run() 함수의 while(running) 내에 추가합니다.
if(taking_photo) {
takePhoto(tmp_frame);
}
cvtColor(tmp_frame, tmp_frame, cv::COLOR_BGR2RGB);
data_lock->lock();
...
- CaptureThread::CaptureThread (...) 생성자에 변수를 초기화해 줍니다.
taking_photo = false;
<mainwindow.h>
- 시그널과 연결할 함수를 선언해 줍니다.
private slots:
// ...
void takePhoto();
<mainwindow.cpp>
- slot 함수를 구현합니다. capture_thread에서 사진 찍는다는 상태 변숫값을 true로 만들어주는 함수를 호출합니다.
void MainWindow::takePhoto()
{
if(capturer != nullptr) {
capturer->takePhoto();
}
}
- initUI()에 connect와 disconnect를 구현해 줍니다.
if(capturer != nullptr) {
// if a thread is already running, stop it
capturer->setRunning(false);
disconnect(capturer, &CaptureThread::frameCaptured, this, &MainWindow::updateFrame);
disconnect(capturer, &CaptureThread::photoTaken, this, &MainWindow::appendSavedPhoto);
connect(capturer, &CaptureThread::finished, capturer, &CaptureThread::deleteLater);
}
int camID = 0;
capturer = new CaptureThread(camID, data_lock);
connect(capturer, &CaptureThread::frameCaptured, this, &MainWindow::updateFrame);
connect(capturer, &CaptureThread::photoTaken, this, &MainWindow::appendSavedPhoto);
...
- appendSavedPhoto() 함수는 찍은 사진을 이미지 리스트에 추가해 주는 기능을 합니다.
void MainWindow::appendSavedPhoto(QString name)
{
QString photo_path = Utilities::getPhotoPath(name, "jpg");
QStandardItem *item = new QStandardItem();
list_model->appendRow(item);
QModelIndex index = list_model->indexFromItem(item);
list_model->setData(index, QPixmap(photo_path).scaledToHeight(145), Qt::DecorationRole);
list_model->setData(index, name, Qt::DisplayRole);
saved_list->scrollTo(index);
}
- 추가적으로 생성자에서 capturer(nullptr)를 넣어주지 않으면 crashed 발생하면서 종료되는 현상이 생기므로 생성자에 추가해주어야 합니다.
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent) , fileMenu(nullptr), capturer(nullptr)
3. 실행 결과
- "Take a Photo" 버튼을 누르면 아래에 찍은 사진을 리스트에 나타나는 것을 확인할 수 있습니다. 이 프로그램 틀을 기반으로 실시간 들어오는 영상에 수염 등 여러 가지 마스크를 씌우는 작업으로 진행될 예정입니다.
- 참고로 여기에 나오는 사진은 https://generated.photos 사이트에서 만들어낸 가상의 얼굴입니다.
감사합니다.
<참고 사이트>
1. [BOOK] Qt-5-and-OpenCV-4-Computer-Vision-Projects