Click here to Skip to main content
15,860,972 members
Articles / Desktop Programming / Windows Forms

Versatile WebCam C# library

Rate me:
Please Sign up or sign in to vote.
4.96/5 (137 votes)
23 Feb 2015CPOL9 min read 1.1M   84.9K   444   207
Easy to use C# WebCam library that's not plagued with weird DLL references or PInvoke calls

Index

Update on 2015-06-06

Thanks to Static Flux for his extensive pull request. I'll try to follow up with more changes. If you have code changes to share, just submit your own pull request on GitHub.

Update on 2015-02-23

Thanks to bntr and Axel I think that resolution problem is finally resolved. Take a look at this discussion for more info. To enable easier collaboration on future improvements to the library I've setup Github repository, click here for C# Web Cam repo. If you do improve the library please feel free to submit pull requests so that others can benefit from your work.

Rest of the article is unmodified - leaving it in original form for now.

Introduction

It was 2005 and time to start coding a project for Imagine Cup when I first stumbled upon the need to capture images from a webcam. Luckily, I was in a pretty good team and the teammate who took this task upon himself (Filip Popović) wrapped up a Visual Studio project after a couple days of scouring the web and reading a bunch of articles (including CodeProject ones of course ;).

Since then, whenever I needed to capture an image from a webcam I would go back to that old code from 2005. And even though the code served me well, I wasn’t pleased with it. The problem was in the foundation of the solution – Filip utilized PInvoke and used the clipboard for copying an image into a Bitmap that could be manipulated within C#. Not that I blame him or anything – me being unable to improve his solution over the years was the bigger problem. Every search I tried ended up in finding something similar (this is the only solution I liked); and the more I read about the subject the more I realized that I missed certain knowledge (C++ and DirectShow) in order to develop something better.

That’s the first reason I’m writing this article – if you are good when it comes to C++ and DirectShow, please read on and see if you can help. The second reason is that I also wanted to make this article a merging point for all fellow C# developers with the same problem – I see that StackOverflow is overflowing with questions on this subject and most solutions out there are either really (really, really) badly written or way too conceptual.

So, don’t worry that this article will be simple call for help ;). The solution I am sharing as a foundation is pretty well performing and robust, and I am sure that when you finish reading this article, you’ll finally have a solid library on which you can depend on when it comes to easily capturing images from a WebCam in C#.

Disclaimer

The current library I’m using is completely extracted from Touchless SDK (if you have couple seconds definitely check it out – Touchless is one of those cool projects that simply invite you to start playing with them). Meaning that I’m not claiming any authorship; all I did was take out the parts that are not WebCam related in order to promote easier reuse. If this article does spark improvements, I will make sure that they are integrated back into the Touchless SDK as a way of saying thanks to Michwass and his crew for the terrific job they’ve done.

So, how do we capture an image from a WebCam?

Once you download the source code that is attached to the article you should have the following three projects:

  • Demo – simple Windows Forms project that demonstrates how a WebCam is used. It references WebCamWrapper which in turn references WebCamLib.
  • WebCamLib – this is where the magic is happening – it is a C++ project with just two files (WebCamLib.h and WebCamLib.cpp) that queries a WebCam using DirectShow and returns results.
  • WebCamWrapper – a C# wrapper on top of the C++ project that enables easy integration into the .NET world.

For a starting point I recommend a code view of Demo\MainForm.cs. This form implements most of the operations you can think of when it comes to WebCam access. First is the iteration through the WebCams hooked up to the computer:

C#
private void MainForm_Load(object sender, EventArgs e)
{
    if (!DesignMode)
    {
        comboBoxCameras.Items.Clear();
        foreach (Camera cam in CameraService.AvailableCameras)
            comboBoxCameras.Items.Add(cam);

        if (comboBoxCameras.Items.Count > 0)
            comboBoxCameras.SelectedIndex = 0;
    }
}

The CameraService class you see in the code is contained in the WebCamWrapper project and is the main wrapper over the main class CameraMethods that is the only class implemented in the C++ WebCamLib project. CameraService exposes AvailableCameras as a list of Camera classes that contain the logic for a certain WebCam. Once the user makes a choice of camera, you’ll obviously want to start the capture:

C#
private CameraFrameSource _frameSource;
private static Bitmap _latestFrame;

private void btnStart_Click(object sender, EventArgs e)
{
    if (_frameSource != null && _frameSource.Camera == comboBoxCameras.SelectedItem)
        return;

    thrashOldCamera();
    startCapturing();
}

_frameSource is the variable in which we’ll save the currently selected Camera. Touchless developers decided not to tie their capture source exclusively to WebCam (good choice obviously) so they made a generic IFrameSource interface that CameraFrameSource implements… and that’s how this class ended up as a container instead of the Camera class directly. The rest of the code is pretty self-explanatory – if we select the same frame source, we’ll just exit; if not we will thrash the old camera and start a new one. Onto the startCapturing method:

C#
private void startCapturing()
{
    try
    {
        Camera c = (Camera)comboBoxCameras.SelectedItem;
        setFrameSource(new CameraFrameSource(c));
        _frameSource.Camera.CaptureWidth = 320;
        _frameSource.Camera.CaptureHeight = 240;
        _frameSource.Camera.Fps = 20;
        _frameSource.NewFrame += OnImageCaptured;

        pictureBoxDisplay.Paint += new PaintEventHandler(drawLatestImage);
        _frameSource.StartFrameCapture();
    }
    catch (Exception ex)
    {
        comboBoxCameras.Text = "Select A Camera";
        MessageBox.Show(ex.Message);
    }
}

private void setFrameSource(CameraFrameSource cameraFrameSource)
{
    if (_frameSource == cameraFrameSource)
        return;

    _frameSource = cameraFrameSource;
}

private void drawLatestImage(object sender, PaintEventArgs e)
{
    if (_latestFrame != null)
    {
        e.Graphics.DrawImage(_latestFrame, 0, 0, _latestFrame.Width, _latestFrame.Height);
    }
}

public void OnImageCaptured(Touchless.Vision.Contracts.IFrameSource frameSource, 
                            Touchless.Vision.Contracts.Frame frame, double fps)
{
    _latestFrame = frame.Image;
    pictureBoxDisplay.Invalidate();
}

We start off by fetching the selected Camera from the ComboBox which we then use to create and set the CameraFrameSource. Lines after that influence the capture parameters (be sure to remember these three lines as we will be getting back to them later) and after that we have a subscription to two events.

The first event, NewFrame, is raised whenever WebCamLib captures an image from the WebCam. As you can see, we save that image into a local variable _latestFrame and from there you can do any additional image processing you like. The second event is just a fancy (and more efficient) way of saying pictureBoxDisplay.Image = frame.Image. For some reason, setting the Image property on a PictureBox too often causes flicker and we obviously do not want that – instead we resort to invalidating the PictureBox and then handling its paint event to draw the current image from the WebCam.

Now that all that is implemented, we just StartFrameCapture and enjoy the view from our WebCam. Try it out – press F5 and then click the ‘Start’ button once the Form loads up.

Rent is too damn high

When you grow tired of watching yourself, simply close the form. Once you are back in Visual Studio, check out the thrashOldCamera method (that is utilized from the Form_Closing and btnStop_Click methods also):

C#
private void thrashOldCamera()
{
    if (_frameSource != null)
    {
        _frameSource.NewFrame -= OnImageCaptured;
        _frameSource.Camera.Dispose();
        setFrameSource(null);
        pictureBoxDisplay.Paint -= new PaintEventHandler(drawLatestImage);
    }
}

Well, nothing too fancy – we unsubscribe from the two mentioned events, set the _frameSource variable to null, and call Dispose on Camera so that the C++ WebCamLib can perform cleanup operations.

Believe it or not – that’s it. There is nothing more critical to explain or implement in order to use images from your WebCam in C#. The extra code that exists in MainForm.cs is just there for saving the current image:

C#
private void btnSave_Click(object sender, EventArgs e)
{
    if (_frameSource == null)
        return;

    Bitmap current = (Bitmap)_latestFrame.Clone();
    using (SaveFileDialog sfd = new SaveFileDialog())
    {
        sfd.Filter = "*.bmp|*.bmp";
        if (sfd.ShowDialog() == DialogResult.OK)
        {
            current.Save(sfd.FileName);
        }
    }

    current.Dispose();
}

And bringing up the configuration dialog:

C#
private void btnConfig_Click(object sender, EventArgs e)
{
    // snap camera
    if (_frameSource != null)
        _frameSource.Camera.ShowPropertiesDialog();
}

Configuration Dialog

Problem(s)

As you can see from the code – the implementation is pretty easy and clean (unlike some other approaches that use WIA, obscure DLLs, clipboard, or hard disk for saving images, etc.), meaning that there are not many problems. Actually, currently there is only one problem I can identify. You remember these three lines?

C#
_frameSource.Camera.CaptureWidth = 320;
_frameSource.Camera.CaptureHeight = 240;
_frameSource.Camera.Fps = 20;

Well, it turns out that they are not working as advertised. First, let’s talk about FPS. If we dive into the Camera class (line 254) here is what we will see (the method that gets called after an image is captured from the webcam):

C#
private void ImageCaptured(Bitmap bitmap)
{
    DateTime dtCap = DateTime.Now;

    // Always save the bitmap
    lock (_bitmapLock)
    {
        _bitmap = bitmap;
    }

    // FPS affects the callbacks only
    if (_fpslimit != -1)
    {
        if (_dtLastCap != DateTime.MinValue)
        {
            double milliseconds = ((dtCap.Ticks - _dtLastCap.Ticks) / TimeSpan.TicksPerMillisecond) * 1.15;
            if (milliseconds + _timeBehind >= _timeBetweenFrames)
            {
                _timeBehind = (milliseconds - _timeBetweenFrames);
                if (_timeBehind < 0.0)
                {
                    _timeBehind = 0.0;
                }
            }
            else
            {
                _timeBehind = 0.0;
                return; // ignore the frame
            }
        }
    }

    if (OnImageCaptured != null)
    {
        var fps = (int)(1 / dtCap.Subtract(_dtLastCap).TotalSeconds);
        OnImageCaptured.Invoke(this, new CameraEventArgs(bitmap, fps));
    }

    _dtLastCap = dtCap;
}

Even if you just glanced at the method, you probably saw that most of it is dedicated to calculating the time between frames and ditching the frame if it came too soon. Which is not too bad, I guess – controlling the frame rate on C# level rather than on hardware level will probably not kill you.

But what about finding out the other two lines, which influence the size of the captured image, also not working (line 235 in Camera.cs)?

C#
private void CaptureCallbackProc(int dataSize, byte[] data)
{
    // Do the magic to create a bitmap
    int stride = _width * 3;
    GCHandle handle = GCHandle.Alloc(data, GCHandleType.Pinned);
    var scan0 = (int)handle.AddrOfPinnedObject();
    scan0 += (_height - 1) * stride;
    var b = new Bitmap(_width, _height, -stride, PixelFormat.Format24bppRgb, (IntPtr)scan0);
    b.RotateFlip(_rotateFlip);
    // Copy the image using the Thumbnail function to also resize if needed
    var copyBitmap = (Bitmap)b.GetThumbnailImage(_width, _height, null, IntPtr.Zero);
    //var copyBitmap = (Bitmap)b.Clone();

    // Now you can free the handle
    handle.Free();

    ImageCaptured(copyBitmap);
}

As you can see, the image size is actually faked. Majority of cameras I’ve tested out will tend to return images in the 640x480 size. Which is fine in most cases – if you need a smaller image, b.GetThumbnailImage will allow you to easily resize it. However, if you wish a higher resolution image, you are stuck and that’s not a good thing.

So, anyone from the C++ world is more than welcome to help with this. The following links I’ve read gave me the impression that all that’s needed to be done is somehow invoke the Video Format window in C++, the same way we are now invoking the Video Source Configuration window (for setting Brightness, Contracts, etc):

Video Format Dialog

  • Setting up Webcam properties with DirectShow forum post
  • EasyWebCam project – for most part it is bad, but it does have a Video Format window. I decompiled WebCam_Capture.dll from the project only to find out that everything is implemented using PInvoke - meaning that it’s useless for our approach. So, if somebody can bring up that same window using C++ and DirectShow – please help out by extending the existing CameraMethods class.

Conclusion

I hope that you leave this article with that warm and fuzzy feeling of happiness knowing that you can now easily communicate with your WebCam even though you are just a simple C# developer.

In case you have anything to add, be my guest and go wild - I’ll be watching the comments section. Any calls for help (even this simple approach is not simple enough for you), suggestions (you did the same thing using a different approach), criticism (you did the same thing using a different approach and you don’t like this one), or offers for help… are all welcome.

History

  • 2015-06-06 - Merged Static Flux's extensive pull request
  • 2013-01-01 - Added a link to the source modified by Jake Dreww[^]. Still searching for a working solution when it comes to resolution settings. Some leads:
  • 2010-10-07 - Initial version of the article.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Chief Technology Officer
United States United States
If you liked this article, consider reading other articles by me. For republishing article on other websites, please contact me by leaving a comment.

Comments and Discussions

 
Buggetting exception Pin
pearl_srivastava19-Dec-11 18:17
pearl_srivastava19-Dec-11 18:17 
BugCompilation errors Pin
Josh Reuben19-Dec-11 5:50
Josh Reuben19-Dec-11 5:50 
BugMore Webcams - "Graph Builder is null" Pin
Johannes@Austria16-Dec-11 22:57
Johannes@Austria16-Dec-11 22:57 
GeneralRe: More Webcams - "Graph Builder is null" Pin
bobble1498831-Mar-12 13:48
bobble1498831-Mar-12 13:48 
QuestionHow to capture image with an external webcam on webcam C# library Pin
Member 407735117-Nov-11 11:56
Member 407735117-Nov-11 11:56 
QuestionSend video over LAN Pin
homedb3-Nov-11 22:55
homedb3-Nov-11 22:55 
QuestionWorked all day to implement the resolution setting code - it does nothing Pin
sklett11-Sep-11 16:18
sklett11-Sep-11 16:18 
AnswerI finally got the resolution setting to work PinPopular
Dan C.17-Sep-11 19:23
Dan C.17-Sep-11 19:23 
I have been spinning my wheels on this exact same issue. After a few hours I finally cracked it! Here is what I got so far...

In CameraMethods::StartCamera() call this just before the call to RenderStream(). Instead of using the camera index in the SetCaptureFormat() function as in the original code to find the appropriate IBaseFilter I used the IBaseFilter that was already found in the StartCamera function directly. I think the other method worked on a copy of the camera filter and not the one actually added to the graph and so the size changes were lost.

C++
SetCaptureSize(g_pIBaseFilterCam, *width, *height);


Add this function to set the size. My version is a bit different in that it cycles through the supported sizes and picks the largest that fits the desired width. I also FindInterface on the PIN_CATEGORY_CAPTURE. However, the original SetCaptureFormat() may also work.

C++
int CameraMethods::SetCaptureSize(IBaseFilter *pCap, int desiredWidth, int desiredHeight)
{
	IAMStreamConfig *pConfig = NULL;
	HRESULT hr = g_pCaptureGraphBuilder->FindInterface(
		&PIN_CATEGORY_CAPTURE,
		&MEDIATYPE_Video,
		pCap, // Pointer to the capture filter
		IID_IAMStreamConfig,
		(void**)&pConfig);
	if(FAILED(hr))
	{
		return -1;
	}

	LONG width=0;
	LONG height=0;
	int iMax=0;

	int iCount = 0, iSize = 0;
	hr = pConfig->GetNumberOfCapabilities(&iCount, &iSize);
	if(FAILED(hr))
	{
		return -1;
	}
	AM_MEDIA_TYPE *pmt;
	VIDEO_STREAM_CONFIG_CAPS scc;

	// Check the size to make sure we pass in the correct structure.
	if (iSize == sizeof(VIDEO_STREAM_CONFIG_CAPS))
	{
		// Use the video capabilities structure.
		// Find the largest video that fits in the desired width
		for (int iFormat = 0; iFormat < iCount; iFormat++)
		{
			hr = pConfig->GetStreamCaps(iFormat, &pmt, (BYTE*)&scc);
			if (SUCCEEDED(hr))
			{				
				if (desiredWidth == 0 && width < scc.MaxOutputSize.cx)
				{
					width = scc.MaxOutputSize.cx;
					height = scc.MaxOutputSize.cy;
					iMax = iFormat;
				}
				else if (desiredWidth > 0 && width < desiredWidth && scc.MaxOutputSize.cx <= desiredWidth)
				{
					width = scc.MaxOutputSize.cx;
					height = scc.MaxOutputSize.cy;
					iMax = iFormat;
				}
				DeleteMediaType(pmt);
				pmt=NULL;
			}
			if(FAILED(hr))
			{
				return -1;
			}
		}

		// Get the resulting video capability
		hr = pConfig->GetStreamCaps(iMax, &pmt, (BYTE*)&scc);
		if (SUCCEEDED(hr))
		{
			width = scc.MaxOutputSize.cx;
			height = scc.MaxOutputSize.cy;
		}
		if(FAILED(hr))
		{
			return -1;
		}

		// default capture format
		if (pConfig) pConfig->GetFormat(&pmt);
		if (pmt->formattype == FORMAT_VideoInfo) {
			VIDEOINFOHEADER *pvi = (VIDEOINFOHEADER *)pmt->pbFormat;
			pvi->bmiHeader.biWidth = width;
			pvi->bmiHeader.biHeight = height;
			hr = pConfig->SetFormat(pmt);
			if(FAILED(hr))
			{
				return -1;
			}
		}
		DeleteMediaType(pmt); 
		pConfig->Release();
		pConfig=NULL;
	}

	return 0;
}

}


Then, in the startCapturing() method, set the size:

C++
_frameSource.Camera.CaptureWidth = 640;
_frameSource.Camera.CaptureHeight = 480;


I am still working on this so need to check that all references are released, etc., but at least got past that hurdle.

Dan
GeneralRe: I finally got the resolution setting to work Pin
Vaccano13-Jan-14 17:15
Vaccano13-Jan-14 17:15 
QuestionBug in Demo Pin
ewyll6-Sep-11 3:46
ewyll6-Sep-11 3:46 
Questioncan I retrieve the array of byte from 1 frame? Pin
Member 795584924-Aug-11 9:57
Member 795584924-Aug-11 9:57 
QuestionFind video device on xp, but no windows 7. Pin
Member 811368128-Jul-11 9:13
Member 811368128-Jul-11 9:13 
GeneralMy vote of 5 Pin
Filip D'haene5-Jul-11 7:15
Filip D'haene5-Jul-11 7:15 
GeneralException when trying run demo on Win7 64bit Pin
sellenoff26-May-11 14:05
sellenoff26-May-11 14:05 
GeneralMessage Closed Pin
9-Jun-11 2:09
AbbeytekMD9-Jun-11 2:09 
GeneralRe: Exception when trying run demo on Win7 64bit Pin
vineetmehta20-Jul-11 17:22
vineetmehta20-Jul-11 17:22 
GeneralChanging Image Size Pin
abbid Siddiqui25-May-11 0:57
abbid Siddiqui25-May-11 0:57 
GeneralVisual Studio Pin
Carlesmk4-May-11 2:34
Carlesmk4-May-11 2:34 
GeneralRe: Visual Studio Pin
lepipele23-May-11 11:15
lepipele23-May-11 11:15 
GeneralWork with Microsoft Lifecams Pin
hosseinsinohe9-Apr-11 8:31
hosseinsinohe9-Apr-11 8:31 
GeneralRe: Work with Microsoft Lifecams Pin
sklett11-Sep-11 12:53
sklett11-Sep-11 12:53 
GeneralMy vote of 3 Pin
ekimred15-Mar-11 1:50
ekimred15-Mar-11 1:50 
GeneralMy vote of 5 Pin
zarkodolas10-Feb-11 9:26
zarkodolas10-Feb-11 9:26 
GeneralSvaka cast! Pin
zarkodolas10-Feb-11 9:25
zarkodolas10-Feb-11 9:25 
GeneralExcellent article Pin
Alex Zheng1-Feb-11 10:08
Alex Zheng1-Feb-11 10:08 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.