페이지

2010-02-06

OpenCV 2.0 에서 웹캠 영상 캡처하기

OpenCV (Open Source Computer Vision)Computer Vision 을 위한 라이브러리이다. Computer Vision은 간단히 말해서 이미지로부터 정보를 뽑아내는 것이다. (Computer Graphic을 모델로부터 이미지를 만드는 것으로 본다면, Computer Graphic과 Computer Vision은 서로 상대적인 관계라고 할 수도 있다.)

2009년 말에 릴리즈 된 OpenCV 2.0 에서는 C++ API가 추가되었는데, 웹캠에서 영상을 캡처하는 부분에는 VideoCapture 라는 클래스가 추가되었다. 이것을 이용해 간단하게 웹캠의 영상을 화면에 표시해주는 프로그램을 만들었다.

먼저, VideoCapture class를 사용하기 위해 "opencv/cv.h"와 "opencv/highgui.h"를 include 해야 한다.

#include "opencv/cv.h"
#include "opencv/highgui.h"

영상을 화면에 보여주기 위해 윈도우가 하나 필요한데, wxFrame을 상속받아 MainFrame을 만들었다. (앞에 wx가 붙은 클래스는 모두 wxWidgets의 클래스이다.)

class MainFrame: public wxFrame
{
public:
MainFrame(wxWindow* parent);
virtual ~MainFrame();
protected:
VideoCapture m_cam;  // VideoCapture 객체
Mat m_original;           // 캡처한 이미지를 담을 곳
ImageView* m_camView;    // 이미지를 보여주는 윈도우
wxTimer m_timer;        // 주기적으로 이미지를 가져와 보여주기 위해 타이머를 사용한다.
}

생성자에서 다음과 같이 VideoCapture를 설정한다.

MainFrame::MainFrame(wxWindow* parent)
: wxFrame(parent, wxID_ANY, wxEmptyString), m_timer(this, ID_TIMER)
{
m_timer.Start(1000 / 20); // 20 fps

m_cam.open(0);
m_cam.set(CV_CAP_PROP_FRAME_WIDTH, 640);
m_cam.set(CV_CAP_PROP_FRAME_HEIGHT, 480);
}

VideoCapture::open(int) 함수는 디바이스를 open 하는데, 파라미터는 웹캠 번호이다. (TODO: 시스템에 존재하는 웹캠 리스트를 가져오는 방법은?) 혹은 open(const string&) 안에 string 을 넘겨 동영상을 불러올 수도 있다고 한다.

VideoCapture::set(int propId, double value) 함수는 파라미터를 설정하는 함수인데, 다음과 같은 파라미터가 있다. (cvSetCaptureProperty() 함수 참고)
  • CV_CAP_PROP_POS_MSEC - Film current position in milliseconds or video capture timestamp
  • CV_CAP_PROP_POS_FRAMES - 0-based index of the frame to be decoded/captured next
  • CV_CAP_PROP_POS_AVI_RATIO - Relative position of the video file (0 - start of the film, 1 - end of the film)
  • CV_CAP_PROP_FRAME_WIDTH - Width of the frames in the video stream
  • CV_CAP_PROP_FRAME_HEIGHT - Height of the frames in the video stream
  • CV_CAP_PROP_FPS - Frame rate
  • CV_CAP_PROP_FOURCC - 4-character code of codec
  • CV_CAP_PROP_BRIGHTNESS - Brightness of the image (only for cameras)
  • CV_CAP_PROP_CONTRAST - Contrast of the image (only for cameras)
  • CV_CAP_PROP_SATURATION - Saturation of the image (only for cameras)
  • CV_CAP_PROP_HUE - Hue of the image (only for cameras)

m_timer 는 wxTimer 객체인데, 주기적으로 이벤트를 발생시키는 역할을 한다. 파라미터는 이벤트 주기이고, millisecond 단위이다. m_timer가 주기적으로 이벤트를 발생시키면, OnUpdateCamera() 함수가 호출되도록 했다. 이 함수 안에서 m_cam (VideoCapture 객체) 로부터 이미지를 가져오도록 구현했다.

BEGIN_EVENT_TABLE(MainFrame, wxFrame)
EVT_TIMER(ID_TIMER, MainFrame::OnUpdateCamera)
END_EVENT_TABLE()

void MainFrame::OnUpdateCamera(wxTimerEvent& event)
{
Mat temp1, temp2;
m_cam >> temp1;
flip(temp1, temp2, 1);

cvtColor(temp2, m_original, CV_BGR2RGB);

m_camView->SetImage(m_original);
Refresh(false); }

m_cam 으로부터 영상을 캡처하기 위해서는 frame을 grab() 하고 retrieve() 해야하는데, >> 연산자가 이 두 역할을 한번에 해준다. 위와 같이 사용하면 temp1 안에 영상이 들어오게 된다.

다음에 영상이 거울처럼 보이게 하려고 flip(..) 함수를 호출했고, m_cam 에서 가져온 영상이 BGR 모델이기 때문에 이것을 RGB로 변경하기 위해 cvtColor(..) 함수를 호출했다.

이제 영상은 m_original 에 담겼다. 이것을 ImageView 객체인 m_camView 에 넣어주는데, ImageView는 다음과 같이 만들어져 있다.

class ImageView: public wxWindow
{
public:
ImageView(wxWindow* parent);
virtual ~ImageView();
void SetImage(Mat& mat);
protected:
wxImage* m_image;
};
void ImageView::SetImage(Mat& mat)
{
if (mat.channels() == 3) {
memcpy(m_image->GetData(), mat.data, (mat.rows * mat.cols * mat.channels()));
}
else if (mat.channels() == 1) {
int size = (mat.rows * mat.cols);
unsigned char* data = m_image->GetData();
for (int i = 0; i < size; ++i) {
data[i * 3] = data[i * 3 + 1] = data[i * 3 + 2] = mat.data[i];
}
}
}

Mat 는 OpenCV에서 이미지 나타내는 클래스이다. (C API에서는 IplImage 를 사용하는데, C++ API에서는 모두 Mat 를 사용한다.) 이것을 wxWidgets에서 사용하는 wxImage로 변환하는데, 컬러 수가 같으면 단순히 데이터를 복사하고, Mat의 컬러 수가 1개라면 wxImage에는 흑백 이미지로 복사하도록 했다.

이제 화면에 wxImage의 내용을 표시하면, 웹캠의 영상을 화면으로 볼 수 있다. 이 부분은 VideoCapture와 관련이 없으므로 생략~

(첨부된 소스코드에서는 이보다 더 많은 작업을 하기 때문에, 문서에 있는 코드와는 약간 차이가 있다. 하지만 VideoCapture를 이용해 웹캠의 영상을 가져오는 부분은 동일하다.)

2010-02-05

TagLib을 이용해 mp3안의 메타 데이터 (ID3) 가져오기

mp3 안에는 노래 제목, 앨범, 가수, 트랙 번호 등의 정보가 들어갈 수 있는데, 가장 많이 이용되는 방식이 ID3 라는 표준이다. 이 정보를 뽑아낼 일이 있어서 TagLib 이라고 하는 C++로 만들어진 라이브러리를 사용해봤다.

(참고) ID3 사이트의 Implementations에는 C/C++ 라이브러리가 세 개 있는데, 이중에 ID3Lib 은 한글 이름의 파일을 읽어들이는데 문제가 있었다. TagLib은 한글 파일 이름도 잘 읽고, ID3 뿐만 아니라 다른 메타 데이터 포맷도 지원하고, API도 이해하기 쉽게 되어 있었다. 하지만 한가지 문제가 있었는데, wxWidgets과 함께 사용했을 때 약 20byte 정도의 메모리 누수가 있었다. (Visual C++ 2008 Express Edition으로 컴파일 시.) 큰 문제가 아니라고 생각해서 일단은 그냥 사용하기로 했지만, 약간 찜찜하긴 하다. ㅡㅡ;;

먼저 TagLib 을 다운받아 빌드해서 사용할 준비를 해야한다. 바이너리 배포도 있는데, 아무래도 직접 빌드하는 것이 마음이 놓인다. 이 과정은 기록 생략...

먼저 필요한 헤더 파일을 include.
#include "taglib/fileref.h"
#include "taglib/tag.h"

TagLib은 'TagLib' 이라는 namespace를 사용한다. using namespace TagLib; 해주든가, 아니면 매번 TagLib:: 을 붙여주어야 한다. 일단 매번 붙이기로.

mp3 안의 정보를 가져와 저장하기 위해 AudioFileInfo 라는 클래스를 만들었다.
class AudioFileInfo
{
public:
    AudioFileInfo(const wxString& file);
protected:
    wxString ToWxString(const TagLib::String& str);
    bool ExtractPicture(const wxString& filename);
protected:
    wxString m_title;
    wxString m_artist;
    wxString m_album;
    wxString m_comment;
    wxString m_genre;
    unsigned int m_year;
    unsigned int m_track;
    int m_bitrate;
    int m_sampleRate;
    int m_channels;
    int m_length;
    wxImage m_picture;
};

AudioFileInfo 의 생성자(constructor)에서 TagLib을 이용해 정보를 가져온 후, 각 멤버 변수에 저장하도록 했다. 하지만 먼저 멤버 변수 초기화부터...
AudioFileInfo::AudioFileInfo(const wxString& filename)
{
    m_year = 0;
    m_track = 0;
    m_bitrate = 0;
    m_sampleRate = 0;
    m_channels = 0;
    m_length = 0;
    

파일 타입에 대한 고민 없이 쉽게 정보를 알아오는 방법은, TagLib::FileRef 객체를 이용하는 것이다.
    TagLib::FileRef f(filename.fn_str());
    if (!f.isNull())
    {
        if (f.tag()) {
            m_title = ToWxString(f.tag()->title());
            m_artist = ToWxString(f.tag()->artist());
            m_album = ToWxString(f.tag()->album());
            m_comment = ToWxString(f.tag()->comment());
            m_genre = ToWxString(f.tag()->genre());
            m_year = f.tag()->year();
            m_track = f.tag()->track();
        }
f.tag()는 FileLib::Tag 객체인데, 여러 타입의 태그에 대한 추상 클래스이고, 실제 클래스는 TagLib::ID3v2::Tag 라든가 TagLib::MP4::Tag 같은 클래스이다. FileLib::Tag는 이 구체 태그들의 속성 중 공통적인 것만 가져올 수 있는 함수들을 제공한다. 위와같이 title, artist, album.. 등이 그것이다.

wxString의 fn_str()은 스트링을 OS의 file name encoding에 맞도록 const char* 형식으로 돌려주는 함수이다.

ToWxString(...) 함수는 TagLib::String 객체를 wxString 객체로 변환하기 위해 만든 함수이고, 내용은 다음과 같다.
wxString AudioFileInfo::ToWxString(const TagLib::String& str)
{
if (str == TagLib::String::null)
return wxEmptyString;
return wxString(str.toCString(true), wxConvUTF8);
}

TagLib::String의 toCString(bool) 함수는 스트링을 const char* 형식으로 변환해주는데, 함수 인자로 true 가 넘어가면 UTF-8로 인코딩을, false가 넘어가면 ISO8859_1 로 변환을 해준다. 한글이 제대로 나오게 하기 위해서는 위와같이 UTF-8 로 인코딩을 하게 한 후, wxString을 생성할 때 wxConvUTF8 로 다시 변환을 해주도록 하면 된다.

TagLib::FileRef 에서는 위와 같은 정보 뿐만 아니라 Audio File에 대한 속성도 가져올 수 있다.
        if (f.audioProperties()) {
            TagLib::AudioProperties *p = f.audioProperties();
            m_bitrate = p->bitrate();
            m_sampleRate = p->sampleRate();
            m_channels = p->channels();
            m_length = p->length();
        }

ID3에는 AttachedPicture가 추가될 수 있는데, 이 정보를 가져오기 위해서는 TagLib::ID3v2::Tag 클래스의 객체를 사용해야 한다. f.tag() 를 TagLib::ID3v2::Tag로 type casting해서 사용할 수도 있지만, f.tag() 가 다른 종류의 Tag일 수도 있으므로, 새로운 TagLib::ID3v2::Tag를 직접 생성하기로 했다. ExtractPicture() 함수가 그 역할을 한다.

bool AudioFileInfo::ExtractPicture(const wxString& filename)
{
    // if file is 'mp3', try ID3v2::Tag to extract picture information.
    if (filename.EndsWith(wxT(".mp3")))
    {
확장자가 mp3인 경우에만 처리하도록 했다.

TagLib::MPEG::File 객체를 만들면 곧바로 TagLib::ID3v2::Tag 객체를 가져올 수 있다.
        TagLib::MPEG::File mf(filename.fn_str());
        TagLib::ID3v2::Tag* v2 = mf.ID3v2Tag();
        if (v2) {

Tag 안에는 Frame이 들어있는데, 이 중에서 id가 'APIC' 인 것이 AttachedPicture 정보를 담고 있는 Frame이다. frameList(..) 함수를 이용해 'APIC' Frame만 가져올 수 있다.
            // 'APIC' is frame if of AttachedPictureFrame.
            // See http://id3.org/id3v2.3.0#head-70a65d30522ef0d37642224c2a40517ae35b7155
            const TagLib::ID3v2::FrameList& list = v2->frameList("APIC");
            if (!list.isEmpty()) {
                // extract just one picture
                TagLib::ID3v2::AttachedPictureFrame* pic = (TagLib::ID3v2::AttachedPictureFrame*)(*list.begin());

TagLib::ID3v2::AttachedPictureFrame 객체를 가져왔으면, 그로부터 Picture의 mime type과 실제 data를 얻을 수 있다. wxWidget이 'image/jpg'와 'image/bmp' mime type을 인식하지 못해서, 각각 'image/jpeg', 'image/x-bmp'로 보정하는 코드를 넣었다.
                wxString mime = ToWxString(pic->mimeType());
                TagLib::ByteVector data = pic->picture();
                if (data.size() > 0) {
                    // mime type correction: wxWidgets doesn't recognize 'image/jpg' and 'image/bmp'.
                    if (mime == wxT("image/jpg")) mime = wxT("image/jpeg");
                    if (mime == wxT("image/bmp")) mime = wxT("image/x-bmp");
                    m_picture = wxImage(wxMemoryInputStream(data.data(), data.size()), mime);
                    return true;
                }
            }
        }
    }
    return false;
}


이렇게 만들어진 AudioFileInfo 클래스는 다음과 같이 사용한다.

wxString filename(wxT("test.mp3"));
AudioFileInfo info(filename);

wxMessageBox(info.GetTitle());
wxMessageBox(info.GetArtist());
...

if (info.GetPicture().IsOk()) {
    // 사진을 보여주는 코드
}