secile / UsbCamera

C# source code for using usb camera and web camera in WinForms/WPF. With only single CSharp source code. No external library required.
MIT License
179 stars 55 forks source link

Can screen tearing be avoided #2

Closed genejo closed 4 years ago

genejo commented 4 years ago

Do you happen to know if anything can be done to avoid screen tearing while capturing at higher resolutions?

For instance, if I capture at 2592 x 1944 (~5 MP), I get this kind of artifacts:

capture_tearing

Seemingly there's not much that can be done within the class (method GetBitmapMainMain()). This is how the local data buffer is copied from the unmanaged DirectShow buffer - already teared. Any settings for DirectShow maybe, or is there not much hope?

secile commented 4 years ago

I do not have USB camera that supports such large resolution, so I can not test your problem.

Judgeing from the image you posted, new image is overlapped on old image. As you mentioned, buffer copied from DirectShow is already teared, I think.

To get image from DirectShow, I use 'SampleGrabber'. And to get image from SampleGrabber, there are 2 ways. I don't know if it works or not, could you try 2nd way?

I'll post sample code later.

secile commented 4 years ago

Add the code below in UsbCamera class.

private class SampleGrabberCallback : DirectShow.ISampleGrabberCB
{
    private byte[] Buffer;
    private object BufferLock = new object();

    public Bitmap GetBitmap(int width, int height, int stride)
    {
        var result = new Bitmap(width, height, System.Drawing.Imaging.PixelFormat.Format24bppRgb);
        if (Buffer == null) return result;

        var bmp_data = result.LockBits(new Rectangle(Point.Empty, result.Size), System.Drawing.Imaging.ImageLockMode.WriteOnly, System.Drawing.Imaging.PixelFormat.Format24bppRgb);
        lock (BufferLock)
        {
            // copy from last row.
            for (int y = 0; y < height; y++)
            {
                var src_idx = Buffer.Length - (stride * (y + 1));
                var dst = new IntPtr(bmp_data.Scan0.ToInt32() + (stride * y));
                Marshal.Copy(Buffer, src_idx, dst, stride);
            }
        }
        result.UnlockBits(bmp_data);

        return result;
    }

    // called when each sample completed.
    public int BufferCB(double SampleTime, IntPtr pBuffer, int BufferLen)
    {
        if (Buffer == null || Buffer.Length != BufferLen)
        {
            Buffer = new byte[BufferLen];
        }

        lock (BufferLock)
        {
            Marshal.Copy(pBuffer, Buffer, 0, BufferLen);
        }
        return 0;
    }

    // never called.
    public int SampleCB(double SampleTime, DirectShow.IMediaSample pSample)
    {
        throw new NotImplementedException();
    }
}

private Func<Bitmap> GetBitmapForSampleGrabberCallback(DirectShow.ISampleGrabber i_grabber, int width, int height, int stride)
{
    var sampler = new SampleGrabberCallback();
    i_grabber.SetCallback(sampler, 1); // WhichMethodToCallback = BufferCB
    return () => sampler.GetBitmap(width, height, stride);
}

And replace the line below. (around line 161)

//GetBitmap = () => GetBitmapMain(i_grabber, width, height, stride);
GetBitmap = GetBitmapForSampleGrabberCallback(i_grabber, width, height, stride);

That is all. Could you let me know this works well or not.

genejo commented 4 years ago

Hey, thank you for the reply! Apologies for writing only now - turns out I was subscribed to notifications, but the e-mail notification checkbox was unchecked 🙄.

Thank you for sharing the other callback for the sample grabber! It does seem to work well (no tearing).

As a minor thing, it crashes after a couple of seconds of running at line

var result = new Bitmap(width, height, System.Drawing.Imaging.PixelFormat.Format24bppRgb);

with

System.ArgumentException: Parameter is not valid.

even though width and height seem appropriate, but it doesn't matter. I'm happy with the rgb24 byte frame anyway.

This is really a neat self-contained 1 file library, @secile !

secile commented 4 years ago

Thank you for your reply. I'm glad that using callback works fine.

1st way use sample grabber's GetCurrentBuffer function, and according to the page below, the function can be cause screen tearing problem.

https://docs.microsoft.com/en-us/windows/win32/directshow/isamplegrabber-getcurrentbuffer

Do not call this method while the filter graph is running. While the filter graph is running, the Sample Grabber filter overwrites the contents of the buffer whenever it receives a new sample.

So, using callbacks seems to be the right way. I will fix future release.

One thing worry about is you reported that app crashed after a couple of seconds of running. On my PC, running for 3 hours did not cause any problems. Is this problem still happening?

genejo commented 4 years ago

It only happens for 3840 x 2160 and 2592 x 1944. The version using the grabber's function doesn't crash... but aside from tearing it's also a bit more dragging.

With the callback, after the first exception is thrown all future bitmap allocation attempts throw the same exception. I'm planning to investigate at some point and come back if things become clear.