Programming/Qt

[도서 실습] Qt5 and OpenCV4 Computer Vision – The GazerW Application (영상 녹화하기, 썸네일)

변화의 물결1 2024. 4. 21. 00:04

 

 

안녕하세요.

 

  이전 내용은 카메라에 접근하고 정보를 얻고 실제 영상을 재생방법에 대해 알아보았습니다. 이번에는 영상을 어떻게 녹화하는지 확인해 보겠습니다.

 

  단순한 방법으로는 카메라로부터 영상을 캡처하는 동안, 매 프레임을 압축해서 영상파일에 기록하는 것입니다. OpenCV에 포함된 videoio module의 VideoWriter Class에서 이런 간단한 기능을 제공합니다.


 

1. Utilities Class 파일 생성

 

  파일 저장 경로와 이름을 자동으로 생성될 수 있게 클래스 하나를 만듭니다. 실제 코드 부분만 공유하였고, 해더 파일이나 중복된 부분은 생략했습니다. 부족한 부분은 첨부한 파일을 확인하면 됩니다.

  

<utilities.h>

 

  - 함수선언을 보면 폴더 경로를 가져오고, 파일 이름 생성하고, 파일 이름에 확장자를 붙여 절대 경로를 문자열로 돌려줍니다.

 

class Utilities
{
     public:
        static QString getDataPath();
        static QString newSavedVideoName();
        static QString getSavedVideoPath(QString name, QString postfix);
};

  

 

<utilities.cpp>

 

  - 위의 함수선언한 것을 정의(구현)하는 소스를 확인해 보겠습니다.

  - 윈도우에서 지원하는 기본 동영상(Video) 폴더에 GazerW를 생성한 경로를 문자열로 리턴합니다.

    기본 경로는 C:\Users\<USERNAME>\Videos가 될 것입니다. 

 

 

QString Utilities::getDataPath()
{
    QString user_movie_path = QStandardPaths::standardLocations(QStandardPaths::MoviesLocation)[0];
    QDir movie_dir(user_movie_path);
    movie_dir.mkpath("GazerW");
    return movie_dir.absoluteFilePath("GazerW");
}

  

 

 - 파일이름을 매번 다르게 만들어주기 위해서 날짜와 시간을 이용합니다.

   여기서 확인해야 할 점이 있는데, 책 내용으로 하면 녹화와 이미지가 저장되지 않습니다. 확인 결과, 특수문자가 파일 이름으로 되어 있기 때문이었습니다. 그래서 특수문자를 최소화해서 파일 이름을 생성하면 됩니다.

 

 

QString Utilities::newSavedVideoName()
{
    QDateTime time = QDateTime::currentDateTime();
    return time.toString("yyyyMMdd_HHmmss");
    //return time.toString("yyyy-MM-dd+HH:mm:ss");
}

 

 

 - 파일이름과 확장자를 붙여서 최종적인 절대 경로를 리턴해줍니다. 

 

 

QString Utilities::getSavedVideoPath(QString name, QString postfix)
{
    return QString("%1/%2.%3").arg(Utilities::getDataPath(), name, postfix);
}

 

 

2. CaptureThread 파일 수정

 

  - 영상을 녹화하기 위해 필요한 변수, 함수 선언과 구현을 합니다.

 

 <capture_thread.h>

 

  - 상태를 구분할 수 있는 열거형 변수를 만들어줍니다.

 

public:
	enum VideoSavingStatus {
                        STARTING,
                        STARTED,
                        STOPPING,
                        STOPPED
};

 

 

  - private 영역에 영상 저장을 위한 변수를 추가합니다.   

 

 

        // video saving
        int frame_width, frame_height;
        VideoSavingStatus video_saving_status;
        QString saved_video_name;
        cv::VideoWriter *video_writer;

 

 

 - 구현을 위한 시그널과 함수를 정의합니다. 

 

public:
    // ...
    void setVideoSavingStatus(VideoSavingStatus status) {video_saving_status = status; };
    //...
signals:
    //...
    void videoSaved(QString name);
    //...
private:
    //...
    void startSavingVideo(cv::Mat &firstFrame);
    void stopSavingVideo();

  

 

<CaptureThread.cpp>

 

  - 저장을 위한 변수들을 스레드 생성 함수에서 초기화합니다. 

 

CaptureThread::CaptureThread(int camera, QMutex *lock):
        running(false), cameraID(camera), videoPath(""), data_lock(lock)
{
 ...
frame_width = frame_height = 0;
        video_saving_status = STOPPED;
        saved_video_name = "";
        video_writer = nullptr;
}

 

 

<capture_thread.cpp>

 

  - 녹화(record) 버튼을 누르면 첫 번째 프레임을 썸네일 이미지로 생성하고, motion-jpeg codec 형태로 해서 Video를 저장할 수 있는 객체를 생성합니다. 그리고 현재 상태를 STARTED로 바꿔줍니다.

  - 참고로, 'X', '2', '6', '4'로 지정하면 H.264/AVC 코텍을 선택할 수 있습니다. 상세 내용은 하단 참고 사이트를 참조하시면 됩니다. 

 

void CaptureThread::startSavingVideo(cv::Mat &firstFrame)
{
    saved_video_name = Utilities::newSavedVideoName();
    QString cover = Utilities::getSavedVideoPath(saved_video_name, "jpg");
    cv::imwrite(cover.toStdString(), firstFrame);

    video_writer = new cv::VideoWriter(
        Utilities::getSavedVideoPath(saved_video_name, "avi").toStdString(),
        cv::VideoWriter::fourcc('M','J','P','G'),
        fps? fps: 30,
        cv::Size(frame_width,frame_height));
    video_saving_status = STARTED;
}

 

 

   - 카메라가 Open 되면 영상 사이즈를 먼저 얻어옵니다.   

 

    frame_width = cap.get(cv::CAP_PROP_FRAME_WIDTH);
    frame_height = cap.get(cv::CAP_PROP_FRAME_HEIGHT);

 

 

  - run() 함수 내에 루프가 돌면서 현재 상태에 맞게 녹화 함수를 실행하게 합니다. 녹화가 시작되면 video_writer 객체에다가 매 프레임을 저장합니다.

 

...
while(running)
if(video_saving_status == STARTING) {
            startSavingVideo(tmp_frame);
        }
        if(video_saving_status == STARTED) {
            video_writer->write(tmp_frame);
        }
        if(video_saving_status == STOPPING) {
            stopSavingVideo();
        }

 

 

 - 녹화 멈춤을 누르면 상태를 정지로하고 각종 객체를 해제시키고 끝났다고 이벤트를 발생시킵니다. 

 

void CaptureThread::stopSavingVideo()
{
    video_saving_status = STOPPED;
    video_writer->release();
    delete video_writer;
    video_writer = nullptr;
    emit videoSaved(saved_video_name);
}

 

 

3. UI 화면 수정하기

 

  - 녹화하는 부분이 끝났기 때문에 이제 메인화면에 이벤트를 연결시켜 주는 작업을 합니다.

 

 <mainwindow.h>

 

  - 녹화에 필요한 slots과 썸네일 이미지를 나타낼 리스트 객체를 정의합니다. 

 

private slots:
    //...
    void recordingStartStop();
    void appendSavedVideo(QString name);
    //...
private:
    //...
    QStandardItemModel *list_model;

  

<mainwindow.cpp>

 

  - UI 초기화할 때 썸네일 리스트의 모드를 아이콘과 이름이 나올 수 있는 IconMode로 설정하고 정렬과 여백 등 설정을 한 후 메인 레이아웃에 추가합니다. 

 

MainWindow::initUI() {
	// list of saved videos
    saved_list = new QListView(this);
    saved_list->setViewMode(QListView::IconMode);
    saved_list->setResizeMode(QListView::Adjust);
    saved_list->setSpacing(5);
    saved_list->setWrapping(false);
    list_model = new QStandardItemModel(this);
    saved_list->setModel(list_model);
    main_layout->addWidget(saved_list, 13, 0, 4, 1);
}

 

 

  - Record 버튼의 이벤트를 구현합니다. 버튼 한 개로 start와 stop을 구현해 놓았습니다.

 

void MainWindow::recordingStartStop() {
    QString text = recordButton->text();
    if(text == "Record" && capturer != nullptr) {
        capturer->setVideoSavingStatus(CaptureThread::STARTING);
        recordButton->setText("Stop Recording");
    } else if(text == "Stop Recording" && capturer != nullptr) {
        capturer->setVideoSavingStatus(CaptureThread::STOPPING);
        recordButton->setText("Record");
    }
}

 

 

 - initUI() 함수에서 버튼 클릭 시그널과 구현 slot을 연결해 줍니다. 

 

connect(recordButton, SIGNAL(clicked(bool)), this, SLOT(recordingStartStop()));

 

 

  - 녹화가 완료되었을 때 처리되어야 할 내용을 구현합니다. 썸네일 리스트에 이미지를 넣을 수 있도록 모델을 만들어 추가합니다.

 

void MainWindow::appendSavedVideo(QString name)
{
    QString cover = Utilities::getSavedVideoPath(name, "jpg");
    QStandardItem *item = new QStandardItem();
    list_model->appendRow(item);
    QModelIndex index = list_model->indexFromItem(item);
    list_model->setData(index, QPixmap(cover).scaledToHeight(145), Qt::DecorationRole);
    list_model->setData(index, name, Qt::DisplayRole);
    saved_list->scrollTo(index);
}

 

 

  - 녹화가 완료되었다는 신호를 처리할 수 있도록 함수와 시그널을 연결시킵니다. 여기서 CaptureThread가 생성되었을 연결하고 객체가 없다면 disconnect에서 해제하는 작업을 해줍니다. 

 

if(capturer != nullptr) {
    	// if a thread is already running, stop it
        capturer->setRunning(false);
        ...
        disconnect(capturer, &CaptureThread::videoSaved, this, &MainWindow::appendSavedVideo);
    }
	...
  		connect(capturer, &CaptureThread::videoSaved, this, &MainWindow::appendSavedVideo);
}

 

 

4. 실행하기

 

  - GazerW 실행 녹화하기

 

 

   

 

- 동영상 폴더에 썸네일 이미지와 녹화된 영상이 있는 것을 확인할 수 있습니다.

 

 

 

 

 

감사합니다.

  

 

<참고 사이트>

1. [BOOK] Qt-5-and-OpenCV-4-Computer-Vision-Projects

2. OpenCV 4로 배우는 컴퓨터 비전과 머신 러닝 (fourcc 관련)

https://thebook.io/006939/ch04/01/04-02/ 

 

GazerW_day6.zip
0.01MB

 

반응형