본문 바로가기
프로그래밍/OpenCV

[OpenCV] 카메라 & 동영상 파일 처리하기

by 별준 2022. 5. 3.

References

Contents

  • VideoCapture 클래스 (카메라 장치 또는 비디오 파일 사용하는 방법)
  • 카메라 입력 처리하는 방법
  • 동영상 파일 처리하는 방법
  • 동영상 파일 저장하기 (VideoWriter 클래스)

이번 포스팅에서는 컴퓨터에 연결되어 있는 카메라를 다루는 방법과 동영상 파일의 입출력 방법에 대해 살펴보도록 하겠습니다. 

 


VideoCapture 클래스

OpenCV에서는 VideoCapture라는 하나의 클래스를 이용하여 카메라 또는 동영상 파일로부터 영상의 프레임을 받아올 수 있습니다. 간략하게 나타낸 VideoCapture 클래스 정의는 다음과 같습니다. 참고로 이 클래스의 멤버 변수는 모두 protected로 선언되어 있어 사용자가 직접 접근할 수 없습니다.

class CV_EXPORTS_W VideoCapture
{
public:
    CV_WRAP VideoCapture();
    CV_WRAP explicit VideoCapture(const String& filename, int apiPreference = CAP_ANY);
    CV_WRAP explicit VideoCapture(int index, int apiPreference = CAP_ANY);
    virtual ~VideoCapture();
    
    CV_WRAP virtual bool open(const String& filename, int apiPreference = CAP_ANY);
    CV_WRAP virtual bool open(int index, int apiPreference = CAP_ANY);
    CV_WRAP virtual bool isOpened() const;
    CV_WRAP virtual void release();

    CV_WRAP virtual bool grab();
    CV_WRAP virtual bool retrieve(OutputArray image, int flag = 0);

    virtual VideoCapture& operator >> (CV_OUT Mat& image);
    virtual VideoCapture& operator >> (CV_OUT UMat& image);
    CV_WRAP virtual bool read(OutputArray image);
    
    CV_WRAP virtual bool set(int propId, double value);
    CV_WRAP virtual double get(int propId) const;
};

line 9-12는 동영상 파일 또는 카메라 장치를 열거나 닫는 작업과 관련된 멤버 함수이며, line 14-19의 함수들은 동영상 파일 또는 카메라 장치로부터 한 프레임을 받아 오는 기능의 멤버 함수입니다. line 21-22의 멤버함수들은 현재 열려 있는 동영상 파일 또는 카메라 장치로부터 정보를 가져오거나 설정하는 기능을 담당합니다.

 

 

동영상 파일 열기

먼저 VideoCapture 클래스를 사용하여 동영상 파일을 불러오는 기능에 대해 살펴보겠습니다. VideoCapture 클래스에서 동영상 파일을 불러오려면 처음 VideoCapture 객체를 생성할 때 생성자에 동영상 파일 이름을 지정하거나 또는 기본 생성자로 VideoCapture 객체를 생성한 후 VideoCapture::open() 멤버 함수를 호출해야 합니다. 이때 사용하는 생성자와 open() 멤버 함수는 다음과 같습니다.

/** @overload
@brief  Opens a video file or a capturing device or an IP video stream for video capturing with API Preference

@param filename it can be:
- name of video file (eg. `video.avi`)
- or image sequence (eg. `img_%02d.jpg`, which will read samples like `img_00.jpg, img_01.jpg, img_02.jpg, ...`)
- or URL of video stream (eg. `protocol://host:port/script_name?script_params|auth`)
- or GStreamer pipeline string in gst-launch tool format in case if GStreamer is used as backend
  Note that each video stream or IP camera feed has its own URL scheme. Please refer to the
  documentation of source stream to know the right URL.
@param apiPreference preferred Capture API backends to use. Can be used to enforce a specific reader
implementation if multiple are available: e.g. cv::CAP_FFMPEG or cv::CAP_IMAGES or cv::CAP_DSHOW.

@sa cv::VideoCaptureAPIs
*/
CV_WRAP explicit VideoCapture(const String& filename, int apiPreference = CAP_ANY);

/** @brief  Opens a video file or a capturing device or an IP video stream for video capturing.

@overload

Parameters are same as the constructor VideoCapture(const String& filename, int apiPreference = CAP_ANY)
@return `true` if the file has been successfully opened

The method first calls VideoCapture::release to close the already opened file or camera.
 */
CV_WRAP virtual bool open(const String& filename, int apiPreference = CAP_ANY);

filename 인자에는 말 그대로 *.avi, *mpg, *.mp4 등 확장자를 갖는 동영상 파일 이름을 전달합니다. 만약 현재 프로그램 실행 폴더가 아닌 다른 폴더에 파일이 있다면 절대 경로 또는 상대 경로를 추가하여 파일 이름을 지정합니다.

하나의 동영상 파일 대신 일련의 숫자로 구분되는 이름의 정지 영상 파일을 가지고 있고, 이 파일을 차례대로 불러오고 싶을 때도 VideoCapture 클래스를 사용할 수 있습니다. 예를 들어 img0001.jpg, img0002.jpg, img0003.jpg 등의 이름으로 구성된 다수의 정지 영상 파일이 있을 경우, filename 인자에 "img%04d.jpg"라고 입력하면 일련의 영상 파일을 순서대로 불러올 수 있습니다. 또한 "protocol://host:port/script_name?script_params|auth" 형태의 비디오 스트림 URL을 지정하여 인터넷 동영상을 사용할 수도 있습니다.

 

apiPreference 인자에는 동영상 파일을 불러오는 방법을 지정할 수 있습니다. apiPreference 인자에는 VideoCaptureAPIs 열거형 상수 중 하나를 지정합니다. 대부분의 경우 apiPreference 인자를 생략하거나 기본값인 CAP_ANY를 지정하며, 이 경우 시스템이 알아서 적절한 방법을 선택하여 사용합니다.

 

 

연결된 카메라 장치 사용

카메라 장치를 열 때에도 VideoCapture 생성자 혹은 VideoCapture::open() 함수를 사용하는데, 이때는 함수의 인자에 문자열이 아닌 정수 값을 전달합니다. 아래의 함수들은 카메라 장치를 열 때 사용됩니다.

/** @overload
@brief  Opens a camera for video capturing

@param index id of the video capturing device to open. To open default camera using default backend just pass 0.
(to backward compatibility usage of camera_id + domain_offset (CAP_*) is valid when apiPreference is CAP_ANY)
@param apiPreference preferred Capture API backends to use. Can be used to enforce a specific reader
implementation if multiple are available: e.g. cv::CAP_DSHOW or cv::CAP_MSMF or cv::CAP_V4L.

@sa cv::VideoCaptureAPIs
*/
CV_WRAP explicit VideoCapture(int index, int apiPreference = CAP_ANY);

/** @brief  Opens a camera for video capturing

@overload

Parameters are same as the constructor VideoCapture(int index, int apiPreference = CAP_ANY)
@return `true` if the camera has been successfully opened.

The method first calls VideoCapture::release to close the already opened file or camera.
*/
CV_WRAP virtual bool open(int index, int apiPreference = CAP_ANY);

카메라 장치를 사용하려고 할 때 전달되는 정수 값 index는 다음과 같은 형태로 구성됩니다.

index = camera_id + domain_offset_id

만약 컴퓨터에 한 대의 카메라만 연결되어 있다면 이 카메라의 camera_id 값은 0입니다. 두 대 이상의 카메라가 연결되어 있다면 각각의 카메라는 0과 같거나 0보다 큰 정수를 ID로 갖습니다. domain_offset_id는 카메라 장치를 사용하는 방식을 표현하는 정수 값이며 VideoCaptureAPIs 열거형 상수 중 하나를 지정합니다. 대부분의 경우에는 자동 선택을 의미하는 0(CAP_ANY)을 사용하기 때문에 index 값은 결국 camera_id와 같은 값으로 설정합니다. 즉, 컴퓨터에 연결된 기본 카메라를 사용하려면 index 값으로 0을 지정하고, 두 대의 카메라가 연결되어 있다면 0 또는 1을 지정합니다.

 

 

isOpened()와 release()

카메라 또는 동영상 파일 열기를 수행한 후에는 VideoCapture::isOpened() 함수를 사용하여 열기 작업이 성공적으로 수행되었는지 확인하는 것이 좋습니다.

/** @brief Returns true if video capturing has been initialized already.

If the previous call to VideoCapture constructor or VideoCapture::open() succeeded, the method returns
true.
 */
CV_WRAP virtual bool isOpened() const;

 

카메라 장치 또는 동영상 파일의 사용이 끝나면 VideoCapture::release() 함수를 호출하여 사용하던 리소스를 해제해야 합니다. 참고로 VideoCapture 소멸자에도 release() 함수와 마찬가지로 사용하고 있던 리소스를 해제하는 코드가 들어 있어서 VideoCapture 객체가 소멸할 때 자동으로 열려 있던 카메라 장치 또는 동영상 파일이 닫히게 됩니다.

/** @brief Closes video file or capturing device.

The method is automatically called by subsequent VideoCapture::open and by VideoCapture
destructor.

The C function also deallocates memory and clears \*capture pointer.
 */
CV_WRAP virtual void release();

 

프레임 받아오기

이번에는 카메라 또는 동영상 파일로부터 한 프레임의 정지 영상을 받아오는 방법에 대해 살펴보겠습니다. VideoCapture 클래스를 이용하여 카메라 또는 동영상 파일을 정상적으로 열었다면, 그 후에는 멤버 함수를 사용하여 프레임을 받아올 수 있습니다. 프레임을 받아 오기 위해서는 VideoCapture::operator>>() 연산자 오버로딩 또는 VideoCapture::read() 함수를 사용합니다.

/** @brief Stream operator to read the next video frame.
@sa read()
*/
virtual VideoCapture& operator >> (CV_OUT Mat& image);

/** @brief Grabs, decodes and returns the next video frame.

@param [out] image the video frame is returned here. If no frames has been grabbed the image will be empty.
@return `false` if no frames has been grabbed

The method/function combines VideoCapture::grab() and VideoCapture::retrieve() in one call. This is the
most convenient method for reading video files or capturing data from decode and returns the just
grabbed frame. If no frames has been grabbed (camera has been disconnected, or there are no more
frames in video file), the method returns false and the function returns empty image (with %cv::Mat, test it with Mat::empty()).

@note In @ref videoio_c "C API", functions cvRetrieveFrame() and cv.RetrieveFrame() return image stored inside the video
capturing structure. It is not allowed to modify or release the image! You can copy the frame using
cvCloneImage and then do whatever you want with the copy.
 */
CV_WRAP virtual bool read(OutputArray image);

>> 연산자 오버로딩과 read() 멤버 함수는 모두 카메라 또는 동영상 파일로부터 다음 프레임을 받아와서 Mat 클래스 포맷의 변수 image에 저장합니다. 사실 >> 연산자 오버로딩은 함수 내부에서 명시적으로 read() 함수를 호출하는 형태로 구현되어 있습니다.

 

예를 들어, 컴퓨터에 연결된 기본 카메라로부터 한 프레임의 정지 영상을 받아 오려면 다음과 같이 작성하면 됩니다.

#include <iostream>
#include "opencv2/opencv.hpp"

int main(int argc, char* argv[])
{
    cv::VideoCapture cap(0);
    cv::Mat frame1, frame2;

    cap >> frame1; // 1st frame
    cap.read(frame2); // 2nd frame
}
VideoCapture 클래스 멤버 함수 중에는 grab() 함수와 retrieve() 함수가 있습니다. grab() 함수는 카메라 장치에 다음 프레임을 획득하라는 명령을 내리는 함수이고, retrieve()는 획득한 프레임을 실제로 받아오는 함수입니다. 결국 read() 또는 operator>>() 연산자 함수는 grab()과 retrieve() 함수를 합쳐 놓은 것이라고 볼 수 있습니다.

만약 컴퓨터에 여러 대의 카메라를 연결하고 여러 카메라로부터 동시에 영상을 획득하고 싶다면 read() 함수를 쓰는 것보다 grab()과 retrieve() 함수를 따로 호출하여 사용하는 것이 좋습니다. 일반적으로 retrieve() 함수는 grab() 함수보다 속도가 느린 편입니다. 따라서 같은 시점의 사진을 획득할 때에는 여러 대의 카메라에 대해 차례대로 grab() 함수를 호출한 뒤, 다시 retrieve() 함수를 차례대로 호출하여 실제 프레임을 받아오는 것이 좋습니다. 동기화를 고려하는 상황이 아니라면 read() 함수 또는 >> 연산자를 사용하는 것이 편리합니다.

 

카메라 또는 동영상 파일의 정보 받아오기/설정하기

현재 열려 있는 카메라 장치 또는 동영상 파일로부터 여러 가지 정보를 받아 오기 위해서는 VideoCapture::set() 함수를 사용합니다.

/** @brief Returns the specified VideoCapture property

@param propId Property identifier from cv::VideoCaptureProperties (eg. cv::CAP_PROP_POS_MSEC, cv::CAP_PROP_POS_FRAMES, ...)
or one from @ref videoio_flags_others
@return Value for the specified property. Value 0 is returned when querying a property that is
not supported by the backend used by the VideoCapture instance.

@note Reading / writing properties involves many layers. Some unexpected result might happens
along this chain.
@code{.txt}
VideoCapture -> API Backend -> Operating System -> Device Driver -> Device Hardware
@endcode
The returned value might be different from what really used by the device or it could be encoded
using device dependent rules (eg. steps or percentage). Effective behaviour depends from device
driver and API Backend

*/
CV_WRAP virtual double get(int propId) const;

get() 함수는 인자로 지정한 속성 ID(propId)에 해당하는 속성값을 반환합니다. 이 인자로 지정할 수 있는 속성 ID는 VideoCaptureProperties 열거형 상수 중 하나를 지정할 수 있으며, 자주 사용되는 상수와 그 의미를 아래 표에 정리해두었습니다. 아래 나열된 속성 중에는 동영상 파일에서만 동작하는 속성도 있고, 카메라에 대해서만 사용할 수 있는 속성도 있습니다. 몇몇 속성은 실제 사용하는 카메라 하드웨어와 드라이버가 지원해야 동작하는 속성도 있습니다. 전체 속성은 link를 참조하시길 바랍니다.

VideoCaptureProperties 열거형 상수 설명
CAP_PROP_POS_MSEC 비디오 파일에서 현재 위치 (msec 단위)
CAP_PROP_POS_FRAMES 현재 프레임 위치 (0-based index)
CAP_PROP_POS_AVI_RATIO [0, 1] 구간으로 표현한 동영상 프레임의 상대적 위치 (0: start, 1: end)
CAP_PROP_FRAME_WIDTH 비디오 프레임의 가로 크기
CAP_PROP_FRAME_HEIGHT 비디오 프레임의 세로 크기
CAP_PROP_FPS 초당 프레임 수
CAP_PROP_FOURCC fourcc 코드 (코덱을 표현하는 정수 값)
CAP_PROP_FRAME_COUNT 비디오 파일의 전체 프레임 수
CAP_PROP_BRIGHTNESS (카메라에서 지원하는 경우) 밝기 조절
CAP_PROP_CONTRAST (카메라에서 지원하는 경우) 명암비 조절
CAP_PROP_SATURATION (카메라에서 지원하는 경우) 채도 조절
CAP_PROP_HUE (카메라에서 지원하는 경우) 색상 조절
CAP_PROP_GAIN (카메라에서 지원하는 경우) 감도 조절
CAP_PROP_EXPOSURE (카메라에서 지원하는 경우) 노출 조절
CAP_PROP_ZOOM (카메라에서 지원하는 경우) 줌 조절
CAP_PROP_FOCUS (카메라에서 지원하는 경우) 초점 조절

예를 들어, 기본 카메라를 열고 카메라의 기본 프레임 크기를 확인하려면 다음과 같이 코드를 작성할 수 있습니다.

cv::VideoCapture cap(0);

int w = cvRound(cap.get(cv::CAP_PROP_FRAME_WIDTH));
int h = cvRound(cap.get(cv::CAP_PROP_FRAME_HEIGHT));

 

get() 함수는 카메라 또는 동영상 파일 속성을 double 타입으로 반환합니다. 따라서 실제 코드에서 정수형 변수에 프레임 크기를 저장하려면 반올림하여 정수형으로 변환하는 것이 좋습니다. 위의 코드에서 사용된 cvRound() 함수는 OpenCV에서 제공하는 반올림 함수입니다.

 

get() 함수와 반대로 현재 열려 있는 카메라 또는 비디오 파일 재생과 관련된 속성 값을 설정할 때는 VideoCapture::set() 함수를 사용합니다.

/** @brief Sets a property in the VideoCapture.

@param propId Property identifier from cv::VideoCaptureProperties (eg. cv::CAP_PROP_POS_MSEC, cv::CAP_PROP_POS_FRAMES, ...)
or one from @ref videoio_flags_others
@param value Value of the property.
@return `true` if the property is supported by backend used by the VideoCapture instance.
@note Even if it returns `true` this doesn't ensure that the property
value has been accepted by the capture device. See note in VideoCapture::get()
 */
CV_WRAP virtual bool set(int propId, double value);

이 함수의 속성 ID에도 위에서 살펴본 VideoCaptureProperties 열거형 상수를 지정합니다. 만약 video.mp4 파일을 열어서 100번째 프레임으로 이동하려면 다음과 같이 작성할 수 있습니다.

cv::VideoCapture cap("video.mp4");
cap.set(cv::CAP_PROP_POS_FRAME, 100);

 


카메라 입력 처리하기

위에서 설명한 VideoCapture 클래스를 사용하여 컴퓨터에 연결된 카메라로부터 프레임을 받아 처리하는 예제 코드를 살펴보겠습니다.

VideoCapture 클래스를 이용하려면 우선 VideoCapture 클래스 객체를 생성합니다.

cv::VideoCapture cap(0);

일단 cap 변수를 선언하고 기본 카메라 장치를 사용하도록 코드를 작성했지만, 실제로 카메라 장치가 사용 가능한 상태로 열렸는지 확인하는 것이 좋습니다. 카메라 장치가 정상적으로 열렸는지 확인하는 것은 isOpened() 멤버 함수를 이용하면 됩니다. 만약 isOpened() 함수가 false를 반환하면 카메라 장치를 사용할 수 없는 상태이므로 예외 처리 코드를 추가하는 것이 좋습니다.

if (!cap.isOpened()) {
    std::cerr << "Camera open failed!\n";
    return -1;
}

 

카메라 장치를 사용할 수 있는 상태라면 이제 카메라 장치로부터 프레임을 받아올 수 있습니다. 하나의 프레임은 한 장의 정지 영상을 의미하기 때문에 Mat 클래스 객체에 저장할 수 있고, Mat 객체에 저장된 영상은 imshow() 함수를 사용하여 화면에 출력할 수 있습니다. 다음 코드는 카메라로부터 일정 시간 간격마다 프레임을 받아와서 화면에 출력합니다.

cv::Mat frame;
while (true) {
    cap >> frame;

    cv::imshow("frame", frame);
    cv::waitKey(10);
}

위 코드는 while 반복문을 무한으로 반복하면 매 프레임을 화면에 출력합니다. while문 내에서는 >> 연산자를 사용하여 카메라 장치로부터 프레임을 받아 와 frame 변수에 저장합니다. 그리고 imshow() 함수와 waitKey 함수를 이용하여 프레임을 화면에 출력합니다. waitKey() 함수의 인자에 0을 지정하면 사용자의 키 입력을 무한히 기다리기 때문에 카메라 혹은 동영상을 재생하는 경우에는 waitKey() 함수 인자에 보통 0보다 큰 정수를 입력해야 하며, 초당 프레임 수(FPS)를 고려하여 충분히 작은 정수를 입력해야 합니다. 여기서는 10을 전달하여 10ms 동안 기다린 후, 다음 프레임을 받아오게 됩니다.

 

그런데 위와 같이 작성하면 while 문을 빠져나올 수 없기 때문에 사용자가 종료시킬 수 없습니다. 따라서 동작 중 사용자가 키보드의 특정 키를 누를 때 종료할 수 있도록 코드를 추가하는 것이 좋습니다. 또한 혹시라도 카메라에서 정지 영상 프레임을 제대로 받아오지 못하는 경우에 대한 예외 처리도 추가하는 것이 좋습니다. 이러한 예외 코드를 추가한 코드는 다음과 같습니다.

cv::Mat frame;
while (true) {
    cap >> frame;
    if (frame.empty())
        break;

    cv::imshow("frame", frame);

    if (cv::waitKey(10) == 27) { // ESC Key
        break;
    }
}

위 소스 코드에서는 두 개의 if 조건문이 추가되었는데, 첫 번째 if문에서는 카메라로부터 받아온 frame 영상이 비어 있으면 while문을 빠져나가도록 작성했습니다. 두 번째 if문에서는 waitKey() 함수의 반환값을 확인하여, 만약 사용자의 키 입력이 있었고 해당 키 값이 27이면 while 루프를 빠져나오도록 설정했습니다. 여기서 27은 키보드에서 Esc 키에 해당하는 값입니다.

 

이렇게 프레임을 받아 왔다면 이후 각 프레임에 대한 다양한 영상 처리 기법을 적용할 수 있습니다. 예를 들어, 카메라의 매 프레임에 대해 영상의 반전을 수행하고, 그 결과를 화면에 같이 출력할 수 있습니다.

반전 처리를 수행하는 전체 소스 코드는 다음과 같습니다.

#include <iostream>
#include "opencv2/opencv.hpp"

int main(int argc, char* argv[])
{
    cv::VideoCapture cap(0);
    
    if (!cap.isOpened()) {
        std::cerr << "Camera open failed!\n";
        return -1;
    }

    std::cout << "Frame width: " << cvRound(cap.get(cv::CAP_PROP_FRAME_WIDTH)) << std::endl;
    std::cout << "Frame height: " << cvRound(cap.get(cv::CAP_PROP_FRAME_HEIGHT)) << std::endl;

    cv::Mat frame, inversed;
    while (true) {
        cap >> frame;
        if (frame.empty())
            break;

        inversed = ~frame;

        cv::imshow("frame", frame);
        cv::imshow("inversed", inversed);

        if (cv::waitKey(10) == 27) { // ESC Key
            break;
        }
    }

    cv::destroyAllWindows();
}

 


동영상 파일 처리하기

대부분의 동영상 파일은 고유의 코덱(codec)을 이용하여 압축된 형태로 저장됩니다. 코덱은 복잡한 알고리즘을 이용하여 대용량 동영상 데이터를 압축하거나, 반대로 압축을 해제하여 프레임을 받아 오는 기능을 제공합니다. 이렇게 압축된 영상 파일을 C/C++ 프로그램에서 불러오는 것이 결코 간단하지 않은데, OpenCV는 현재 널리 사용되는 MPEG-4, H.264 등 코덱을 제공하기 때문에 VideoCapture 클래스를 사용하면 쉽게 동영상 파일을 불러와서 사용할 수 있습니다. 동영상 파일을 다루는 방법은 위에서 살펴본 카메라 입력 처리하는 방법과 매우 유사합니다.

 

동영상 파일을 다룰 때에도 우선 VideoCapture 객체를 생성하고, open() 멤버 함수를 이용하여 동영상 파일을 여는 작업을 수행합니다. 또는 동영상 파일 이름을 인자로 갖는 생성자를 이용할 수도 있습니다.

cv::VideoCapture cap("stopwatch.avi");

 

동영상 파일은 각자 초당 프레임 수, 즉 FPS 값을 가지고 있습니다. 동영상 파일을 재생하는 프로그램을 만드는 경우 해당 동영상의 FPS를 고려하지 않으면 영상이 너무 빠르거나 느리게 재생되는 경우가 발생합니다. 따라서, 동영상을 적절한 속도로 재생하려면 동영상의 FPS 값을 참고해야 합니다. FPS 값은 아래 코드로 확인할 수 있습니다.

double fps = cap.get(cv::CAP_PROP_FPS);

동영상 파일의 FPS 값을 이용하면 매 프레임 사이의 시간 간격을 계산할 수 있습니다. 이 값은 다음과 같이 간단한 연산을 통해 계산됩니다.

int delay = cvRound(1000 / fps);

예를 들어, 초당 30프레임을 재생하는 동영상의 경우 delay 값은 33이며, 이는 매 프레임을 33ms 시간 간격으로 출력해야 함을 의미합니다. 여기서 구한 delay 값은 이후 동영상 프레임을 받아와서 화면에 출력하는 반복문 안에서 waitKey() 함수의 인자로도 활용합니다.

 

동영상 파일을 불러와서 처리하는 전체 소스 코드는 다음과 같습니다. 위에서 본 것과 동일하게, 받아온 영상 프레임에 반전 처리를 수행하고 화면에 출력합니다.

#include <iostream>
#include "opencv2/opencv.hpp"

int main(int argc, char* argv[])
{
    cv::VideoCapture cap("stopwatch.avi");
    
    if (!cap.isOpened()) {
        std::cerr << "Video open failed!\n";
        return -1;
    }

    std::cout << "Frame width: " << cvRound(cap.get(cv::CAP_PROP_FRAME_WIDTH)) << std::endl;
    std::cout << "Frame height: " << cvRound(cap.get(cv::CAP_PROP_FRAME_HEIGHT)) << std::endl;
    std::cout << "Frame count: " << cvRound(cap.get(cv::CAP_PROP_FRAME_COUNT)) << std::endl;

    double fps = cap.get(cv::CAP_PROP_FPS);
    std::cout << "FPS: " << fps << std::endl;

    int delay = cvRound(1000 / fps);

    cv::Mat frame, inversed;
    while (true) {
        cap >> frame;
        if (frame.empty())
            break;

        inversed = ~frame;

        cv::imshow("frame", frame);
        cv::imshow("inversed", inversed);

        if (cv::waitKey(delay) == 27) { // ESC Key
            break;
        }
    }

    cv::destroyAllWindows();
}

 


동영상 파일 저장하기

OpenCV에서는 일련의 프레임을 동영상 파일로 저장하는 기능도 제공합니다. OpenCV에서 동영상 파일을 생성하고 프레임을 저장하기 위해서는 VideoWriter 클래스를 사용합니다. 아래 클래스 정의는 VideoWriter의 정의의 일부를 가져온 것입니다.

class CV_EXPORTS_W VideoWriter
{
public:
    CV_WRAP VideoWriter();
    CV_WRAP VideoWriter(const String& filename, int fourcc, double fps,
                Size frameSize, bool isColor = true);
    virtual ~VideoWriter();

    CV_WRAP virtual bool open(const String& filename, int fourcc, double fps,
                      Size frameSize, bool isColor = true);
    CV_WRAP virtual bool isOpened() const;
    CV_WRAP virtual void release();

    virtual VideoWriter& operator << (const Mat& image);
    CV_WRAP virtual void write(InputArray image);

    CV_WRAP virtual bool set(int propId, double value);
    CV_WRAP virtual double get(int propId) const;
    
    CV_WRAP static int fourcc(char c1, char c2, char c3, char c4);
    // ... 생략
};

VideoWriter 클래스도 VideoCapture 클래스와 유사합니다만, fourcc()라는 멤버 함수가 추가되어 있는 것을 볼 수 있습니다. 이 함수는 fourcc 코드를 생성하는 정적 멤버 함수입니다.

 

새로운 동영상 파일을 만들려면 먼저 VideoWriter 클래스 객체를 생성해야 하는데, 기본 생성자를 이용하여 객체를 생성하려면 단순히 변수를 선언하기만 하면 됩니다.

cv::VideoWriter video;

객체를 생성하고 나면 open() 멤버 함수를 이용하여 저장할 동영상 파일을 쓰기 모드로 열어야 합니다. open() 함수의 원형은 다음과 같습니다.

CV_WRAP VideoWriter(const String& filename, int fourcc, double fps,
            Size frameSize, bool isColor = true);
CV_WRAP virtual bool open(const String& filename, int fourcc, double fps,
                  Size frameSize, bool isColor = true);

open() 함수의 두 번째 인자인 fourcc는 4-문자 코드(four character code)의 약자이며, 말 그대로 4개의 문자로 구성된 코드입니다. fourcc는 동영상 파일의 코덱, 압축 방식, 색상 혹은 픽셀 포맷 등을 정의하는 정수 값이며, 코덱을 표현하는 네 개의 문자를 묶어서 fourcc를 생성합니다. fourcc에 해당하는 정수 값은 VideoWriter::fourcc() 함수를 사용하여 생성할 수 있습니다.

/** @brief Concatenates 4 chars to a fourcc code

@return a fourcc code

This static method constructs the fourcc code of the codec to be used in the constructor
VideoWriter::VideoWriter or VideoWriter::open.
 */
CV_WRAP static int fourcc(char c1, char c2, char c3, char c4);

위 함수를 사용하여 생성할 수 있는 주요 fourcc 코드와 관련된 코덱 정보가 아래 표에 나열되어 있습니다. 더 많은 정보는 https://www.fourcc.org/codecs/를 참조하시길 바랍니다.

fourcc 코드 생성 방법 코덱
VideoWriter::fourcc('D', 'I', 'V', 'X') DivX MPEG-4
VideoWriter::fourcc('X', 'V', 'I', 'D') XVID MPEG-4
VideoWriter::fourcc('F', 'M', 'P', '4') FFMPEG MPEG4
VideoWriter::fourcc('W', 'M', 'V', '2') Windows Media Video 8
VideoWriter::fourcc('M', 'J', 'P', 'G') 모션 JPEG
VideoWriter::fourcc('Y', 'V', '1', '2') YUV 4:2:0 Planar(비압축)
VideoWriter::fourcc('X', '2', '6', '4') H.264/AVC
VideoWriter::fourcc('A', 'V', 'C', '1') Advanced Video

예를 들어, DivX MPEG-4 코덱을 이용하는 output.avi 동영상 파일을 생성하려면 다음과 같은 형태로 코드를 작성합니다.

cv::VideoWriter video;
int fourcc = VideoWriter::fourcc('D', 'I', 'V', 'X');
video.open("output.avi", fourcc, fps, cv::Size(w, h));

위 코드에서 fps는 FPS 값, w와 h는 프레임 크기를 나타냅니다. 또한, VideoWriter 클래스는 open() 멤버 함수와 같은 인자 구성을 갖는 생성자를 지원하므로 아래와 같이 간략하게 작성할 수도 있습니다.

cv::VideoWriter video("output.avi", fourcc, fps, cv::Size(w, h));

 

열려 있는 동영상 파일에 새로운 프레임을 추가하기 위해서는 << 연산자 오버로딩 또는 VideoWriter::write() 함수를 사용합니다. 이를 사용하여 프레임을 추가할 때, 새로 추가하는 image 프레임 크기는 동영상 파일을 생성할 때 지정했던 프레임 크기와 같아야 합니다. 또한 컬러로 설정된 동영상 파일에 grayscale 영상을 추가하면 정상적으로 저장되지 않으므로 주의해야 합니다.

 

프레임 저장이 완료되었으면 열려 있던 파일을 닫는 VideoWriter::release() 함수를 호출해야 합니다. 다만 VideoWriter 클래스의 소멸자에서 release()를 호출하기 때문에 객체가 소멸되면 자동으로 파일이 닫힙니다.

 

아래 코드는 VideoWriter 클래스를 이용하여 실제 동영상 파일을 생성합니다. 컴퓨터에 연결된 카메라로부터 프레임을 입력받고, 해당 프레임을 반전시킨 영상을 이용하여 동영상 파일을 생성합니다.

#include <iostream>
#include "opencv2/opencv.hpp"

int main(int argc, char* argv[])
{
    cv::VideoCapture cap(0);
    
    if (!cap.isOpened()) {
        std::cerr << "Camera open failed!\n";
        return -1;
    }

    std::cout << "Frame width: " << cvRound(cap.get(cv::CAP_PROP_FRAME_WIDTH)) << std::endl;
    std::cout << "Frame height: " << cvRound(cap.get(cv::CAP_PROP_FRAME_HEIGHT)) << std::endl;
    double fps = cap.get(cv::CAP_PROP_FPS);

    int fourcc = cv::VideoWriter::fourcc('D', 'I', 'V', 'X');
    int delay = cvRound(1000 / fps);

    cv::VideoWriter outputVideo("output.avi", fourcc, fps, cv::Size(w, h));
    if (outputVideo.isOpened()) {
        std::cerr << "File open failed!\n";
        return -1;
    }

    cv::Mat frame, inversed;
    while (true) {
        cap >> frame;
        if (frame.empty())
            break;

        inversed = ~frame;
        outputVideo << inversed;

        cv::imshow("frame", frame);
        cv::imshow("inversed", inversed);

        if (cv::waitKey(10) == 27) { // ESC Key
            break;
        }
    }

    cv::destroyAllWindows();
}

 

'프로그래밍 > OpenCV' 카테고리의 다른 글

[OpenCV] 이벤트 처리  (0) 2022.05.04
[OpenCV] 직선, 도형, 문자 그리기  (0) 2022.05.04
[OpenCV] Vec, Scalar, InputArray/OutputArray  (0) 2022.05.03
[OpenCV] Mat 클래스  (0) 2022.05.02
[OpenCV] 기본 자료형 클래스  (0) 2022.05.01

댓글