0

I have been battling to read frames from a video with the library Emgu CV and use a timer to play the frames in a picture box to create the impression of a video. This time round I used a rather sophisticated approach where I have an asynchronous method that Read Frames from the video and adds them to a List of Mat objects. I then subscribed the Form to the Application.Idle event handler which checks for me if the task object returned by the asynchronous method is not null and is completed. If those conditions are met then the timer is enabled which starts to loop through the Mat List and display them in the picture box at an interval of 33 miliseconds. However when I step through the debugger, the list of Mats is empty and it's count is 0. I have even done a Debug.WriteLine inside the asynchronous method to monitor what its doing but it's not adding Mat(which is basically an image container l) to my List. I have queried the frame count from the video using the library and the compiler returns 69 for the frame count which means the video has frames. Help me make this asynchronous method read frames into the list. Implementation details of the asynchronous method

private async Task ReadFrames(){
  if(video !=null){
   await Task.Run())=>{
     while(video.Grab()){
      //add the frames to the list
      frames.Add(video.QueryFrame());
      }
    });
  }
}

Inside my class I have defined a Task object, the list of frames, frame count and a timer object for playing the video.

public partial class Form1 :Form {
 //object to store the result of the asynchronous task
  private Task task = null;
  private Timer timer = new Timer();
  //integer to store the frame count
  private int frameCount = 0;
  //integer to keep track of the current frame 
  private int currentFrame = 0;
  //list to hold the frames in the video
  private List<Mat> frames = new List<Mat>();
  //inside the ctor, the timer is disabled by default
 public Form1(){
  timer.Interval = 33;
  timer.Enabled = false;
  timer.Elapsed += Timer_Elapsed;
 
  //subscribing to application idle event
  Application.Idle += Application_Idle;
 }
 //here I check if the task is completed and set to an instance 
   private void Application_Idle(object sender, EventArgs e){
    if(task !=null && task.IsCompleted) 
    { 
   //enable the timer to start looping through the frames
   timer.Enabled = true;
    }
  }
 }

Now the on elapse checks if the current frame index is less than the total and keeps changing at an interval of 33 milliseconds.

private void Timer_Elapsed(object? sender, ElapsedEventArgs e){
  if(currentFrame < frameCount){
    pictureBox1.Image = ToBitmap(frames[currentFrame];
    //Update the index
    currentFrame +=1;
    Task.Delay(33);
  }else{
    //disable the timer
     timer.Enabled = false:
  }
}

When I step through the debugger, it shows that frames has a count of 0, how can I fix this?

Son of Man
  • 1,213
  • 2
  • 7
  • 27

1 Answers1

1

This is just not a good way to do asynchronous capture. It is needlessly complicated, and likely error prone. If you intend to capture some fixed number of frames you could do something like this:

public Task<List<Mat>> CaptureFrames(int frameCount){

    List<Mat> CaptureLocal(){
        var result = new List<Mat>();
        for(int i = 0; i < frameCount && video.Read(out var image); i++){
            result.Add(image);
        }
        return result;
    }
    return Task.Run(CaptureLocal);
}

To do playback you could do something like

public async Task Playback(IEnumerable<Mat> frames, CancellationToken cancel){
    foreach(var frame in frames){
        cancel.ThrowIfCancellationRequested();
        pictureBox1.Image = ToBitmap(frame);
        await Task.Delay(33);
    }
}

But note that timers in windows, including Task.Delay are limited to ~16ms by default. If you need better accuracy you might need to use MultiMediaTimer or increase the timer frequency during playback.

If you want concurrent capture and playback you can use ConcurrentQueue so that you can safely add frames from a background thread, and display them from the UI thread. But this can become complicated, and will depend on the priorities, i.e. should you prioritize smooth frame rate? or that the latest image is displayed? Using DataFlow may make things simpler.

Also note that there are a bunch of additional things you may need to consider for smooth and efficient playback. But using proper techniques for asynchronous code should at least help a bit.

JonasH
  • 28,608
  • 2
  • 10
  • 23
  • Thanks for your answer, should I really hard code the frame rate per second of the video into the Task.Delay method or should I programmatically extract that property from the video using emgu cv? – Son of Man Aug 15 '23 at 14:44
  • @QingGuo You are completely right that you need to get that from the video. That is part of the "bunch of additional things to consider". I tried to simply show a better way to do what you are doing. Including everything you need to do for good video playback in the example would take to much time, and I'm frankly not sure I would do it correctly. – JonasH Aug 15 '23 at 14:49
  • @QingGuo I'm not sure why you would think that the playback does not run asynchronously, given that it is "async" and uses "await". Where exactly do you think it will block? – JonasH Aug 15 '23 at 14:51
  • Frame rate per second and the interval of the timer in Task.Delay are really confusing me. I am trying to understand the relationship because frame rate refers to the number of images to display per second while the interval of the timer is for updating the image. In other posts they are dividing 1000 against the frame rate per second of the video to compute the value to pass to Task.Delay(). Is there any particular reason as to why? – Son of Man Aug 15 '23 at 14:55
  • @QingGuo Task.Delay, Thread.Sleep etc uses milliseconds as the unit. So `1 / FrameRate` gives seconds per frame. Multiply by 1000 to get milliseconds per frame. – JonasH Aug 15 '23 at 14:59
  • Local video players such as Movies and TV or VLC play the video instantly without having to read all the frames, maybe they do some kind of frame buffer ahead of time and then display those that have been buffered as they keep disposing. Is it okay to do a frame.Dispose() after the delay in the playback method? – Son of Man Aug 15 '23 at 15:00
  • @QingGuo, yes, but again part of "bunch of additional things to consider". If you just want to play video you should likely just use a [video control](https://learn.microsoft.com/en-us/windows/win32/wmp/embedding-the-windows-media-player-control-in-a-c--solution) that takes a video file and does all the complicated bits for you. – JonasH Aug 15 '23 at 15:03
  • Thanks for the comment, I now understand why they do that. I will test your code in some few hours and update you. – Son of Man Aug 15 '23 at 15:03
  • The job of software engineering is not an easy feat as people tend to think. The real test is in writing code that is memory efficient and takes the least time to run. I used a Queue to store the frames, I expected the memory usage to go down while calling dequeue but I got fascinated. – Son of Man Aug 16 '23 at 03:39
  • When you said that my style was a bad way of querying frames I didn't understand but now I do. You meant enabling the timer every time the Application.Idle event is raised . This explains why video plays but has glitches – Son of Man Aug 18 '23 at 13:09
  • Is there a way I can unsubscribe to the Application.Idle event immediately task is resolved to an object and its been completed ? – Son of Man Aug 18 '23 at 13:10