2013年9月14日 星期六

EmguCV Image Process: Process Video Sequences part 2


EmguCV Image Process: Process Video Sequences

參考OpenCV 2 Computer Vision Application Programming Cookbook第十章

介紹內容如下:

Reading video sequences

Processing the video frames

Writing video sequences

Tracking feature points in video

Extracting the foreground objects in video

上一篇介紹了如何透過Capture這個類別來讀入視訊檔案

這篇來介紹如何對視訊的每一張frame做一些影像處理

也就是所謂的視訊處理

如果每一次做不同的視訊處理

都要重複寫Capture這個類別的實作方法有點煩

所以這裡要重新包裝Capture這個類別

變成一個方便以後可再利用的類別


基本概念就是這個類別要能夠開啟視訊檔案

然後可以設定要用什麼樣的方法去做影像處理

這裡用到一個C#語言中很方便的Func這個委派類別

影像處理一般都是input一張影像就output一張影像

假設先設定我們要對每一張影像要做的方法

例如這裡要做Canny的邊緣偵測

方法如下:

IImage Canny(IImage image)
{
    Image<Gray, byte> gray;
    if (image.NumberOfChannels == 3)
    {
        //Convert to gray
        gray = ((Image<Bgr, Byte>)image).Convert<Gray, Byte>();
    }
    else
    {
        gray = (Image<Gray, Byte>)image;
    }
    //Compute Canny edges
    gray = gray.Canny(100, 200);
    //Invert the image
    gray._ThresholdBinaryInv(new Gray(128), new Gray(255));

    return gray;
}
這裡的input值,與output值設定為image的基底介面

裡面的方法就是做Canny的邊緣偵測

所以其相對應的Func委派就是:Func<IImage, IImage>

不熟悉Func的人可到此學習

Func其實與Action這個委派是一樣的使用方式

只是Func有含回傳值,而Action是沒有回傳值的


那這邊開始包裝一下新的類別:VideoProcessor

public class VideoProcessor
{
    //the OpenCV video capture object
    private Capture capture;

    //a bool to determine if the process
    //callback will be called
    private bool callIt;
    //the callback function to be called
    //for the processing of each frame
    private Func<IImage, IImage> process;
    //Input display window name
    private string windowNameInput;
    //Output display window name
    private string windowNameOutput;
    //delay between each frame processing
    int delay;
    //number of processed frames
    long fnumber;
    //stop at this frame number
    long frameToStop;
    //to stop the processing
    bool stop;

    //Construct
    public VideoProcessor()
    {
        callIt = true;
        delay = 0;
        fnumber = 0;
        stop = false;
        frameToStop = -1;
    }
    ...
這個類別需要的一些欄位和建構子

這裡要設定callIt:是否要使用設定的方法來做影像處理

process:就是用來委派所需要的影像處理的方法

windowNameInput:設定原始畫面撥放的window的顯示名稱

windowNameOutput:設定處理畫面撥放的window的顯示名稱

delay:設定每張frame的延遲時間

fnumber:下一張要處理的frame number

frameToStop:設定處理到哪一張frame的時候停止

stop:是否要停止


接下來提供一些公開屬性和方法讓使用者設定

/// <summary>
/// set the callback function that will be called
/// for each frame
/// </summary>
/// <param name="process">the callback function</param>
public void SetFrameProcessor(Func<IImage, IImage> process)
{
    this.process = process;
}
/// <summary>
/// set the name of the video file
/// </summary>
/// <param name="filename">the name of the video file</param>
/// <returns>success or failure</returns>
public bool SetInput(string filename)
{
    this.fnumber = 0;
    //In case a resource was already
    //associated with the Capture instance
    if (this.capture != null)
        this.capture.Dispose();
    try
    {
        this.capture = new Capture(filename);
        return true;
    }
    catch (Exception)
    {
        return false;
    }
}
/// <summary>
/// to display the processed frames
/// </summary>
/// <param name="windowName">the name of the window</param>
public void DisplayInput(string windowName)
{
    this.windowNameInput = windowName;
    CvInvoke.cvNamedWindow(this.windowNameInput);
}
/// <summary>
/// to display the processed frames
/// </summary>
/// <param name="windowName">the name of the window</param>
public void DisplayOutput(string windowName)
{
    this.windowNameOutput = windowName;
    CvInvoke.cvNamedWindow(this.windowNameOutput);
}
/// <summary>
/// do not display the processed frames
/// </summary>
public void DontDisplay()
{
    CvInvoke.cvDestroyWindow(this.windowNameInput);
    CvInvoke.cvDestroyWindow(this.windowNameOutput);
}
/// <summary>
/// Stop the porcessing
/// </summary>
public void StopIt()
{
    this.stop = true;
}
/// <summary>
/// Is the process stopped?
/// </summary>
public bool IsStopped
{
    get { return this.stop; }
}
/// <summary>
/// Is a capture device opened?
/// </summary>
public bool IsOpened
{
    get { return this.capture != null; }
}
/// <summary>
/// set a delay between each frame
/// 0 means wait at each frame
/// negative means no delay
/// </summary>
/// <param name="delay">delay</param>
public void SetDelay(int delay)
{
    this.delay = delay;
}
/// <summary>
/// process callback to be called
/// </summary>
public void CallProcess()
{
    this.callIt = true;
}
/// <summary>
/// do not call process callback
/// </summary>
public void DontCallProcess()
{
    this.callIt = false;
}
/// <summary>
/// stop at the frame number
/// </summary>
/// <param name="frame">the frame number</param>
public void StopAtFrameNo(long frame)
{
    this.frameToStop = frame;
}
/// <summary>
/// return the frame number of the next frame
/// </summary>
/// <returns>the frame number of the next frame</returns>
public double GetFrameNumber()
{
    double frames = this.capture.GetCaptureProperty(Emgu.CV.CvEnum.CAP_PROP.CV_CAP_PROP_POS_FRAMES);
    return frames;
}
/// <summary>
/// return the frame rate of the video file
/// </summary>
/// <returns>the frame rate of the video file</returns>
public double GetFrameRate()
{
    double frameRate = this.capture.GetCaptureProperty(Emgu.CV.CvEnum.CAP_PROP.CV_CAP_PROP_FPS);
    return frameRate;
}
提供設定屬性和設定影像處理的方法

SetFrameProcessor(Func<IImage, IImage> process)就是傳入一個方法

此方法必須是只有一個引數,並且為IImage,回傳值也為IImage

然後SetInput(string filename)設定處理的視訊檔案的完整路徑

DisplayInput(string windowName)設定原始畫面window的顯示名稱

DisplayOutput(string windowName)設定處理畫面window的顯示名稱

和一些設定停止、delay時間、取得frame number、取得frame rate的方法


再來是運作的方法:

/// <summary>
/// to grab (and process) the frame of the sequence
/// </summary>
public void Run()
{
    //current frame
    IImage frame;
    //output frame
    IImage output;
    //if no catpure device has been set
    if (!this.IsOpened)
    {
        return;
    }

    this.stop = false;
    while (!IsStopped)
    { 
        //read next frame if any
        frame = this.capture.QueryFrame();
        if (frame == null)
        {
            break;
        }
        //display input frame
        if(!string.IsNullOrWhiteSpace(this.windowNameInput))
        {
            CvInvoke.cvShowImage(this.windowNameInput, frame.Ptr);
        }
        //calling the process function
        if (this.callIt)
        {
            //process the frame
            output = process(frame);
            //increment frame number
            this.fnumber++;
        }
        else
        {
            output = frame;
        }
        //display output frame
        if (!string.IsNullOrWhiteSpace(this.windowNameOutput))
        {
            CvInvoke.cvShowImage(this.windowNameOutput, output.Ptr);
        }
        //introduce a delay
        if (this.delay >= 0 && CvInvoke.cvWaitKey(delay) >= 0)
        {
            StopIt();
        }
        //check if we should stop
        if (this.frameToStop >= 0 && GetFrameNumber() == this.frameToStop)
        {
            StopIt();
        }

    }

}
這裡利用一個while迴圈去判斷是否被使用者停止

然後不斷的去取得最新一張frame

然後用一個window去呈現原始畫面

用另一個window去呈現處理的畫面

這裡可以發現Func<IImage, IImage> process

就直接被當作是一個方法來用output = process(frame);

他就會去執行使用者所設定的那個方法!

所以這裡是彈性的

使用者可以自由地設定他要處理的方法


使用的方式如下:

//Create instance
VideoProcessor processor = new VideoProcessor();
//Open the video file
processor.SetInput(@"video.mov");
//Declare a window to display the video
processor.DisplayInput("Current frame");
processor.DisplayOutput("Output frame");
//Play the video at the original frame rate
processor.SetDelay((int)(1000/processor.GetFrameRate()));
//Set the frame processor callback function
processor.SetFrameProcessor(Canny);
//Start the process
processor.Run();
先建立我們所包裝好的VideoProcessor這個類別

然後設定目標的視訊來源

設定播放原始畫面的window的顯示名稱

設定播放處理畫面的window的顯示名稱

設定延遲時間為1000/FPS (這樣才會符合原始視訊的fps)

設定剛剛所寫的Canny方法作為這個VideoProcessor的處理方法

然後run

執行結果如下:




執行時會跳出兩個新視窗

一個視窗Current frame撥放原始畫面

另一個視窗Output frame則撥放Canny邊緣偵測的畫面

透過這個VideoProcessor類別

可以輕鬆地更換你要執行的影像處理方法

完全不需要重複撰寫多餘的程式碼

只要利用SetFrameProcessor(Func<IImage, IImage> process)這個方法

就可以去更換

也來實作一個影像處理的方法來執行看看吧!!


下一篇介紹將此VideoProcessor類別加入寫檔的功能

沒有留言:

張貼留言