안녕하세요.
이전 내용은 카메라에 접근하고 정보를 얻고 실제 영상을 재생방법에 대해 알아보았습니다. 이번에는 영상을 어떻게 녹화하는지 확인해 보겠습니다.
단순한 방법으로는 카메라로부터 영상을 캡처하는 동안, 매 프레임을 압축해서 영상파일에 기록하는 것입니다. 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/