Look at me! Windows Image Acquisition
| |
Interfacing a Webcam that supports Windows Image Acquisition (WIA) using .Net |
|
Scott Hanselman
Difficulty: Intermediate
Time Required: 1-3 hours
Cost: $50-$100
|
I love Goodwill. If you're not familiar with Goodwill, it's a chain of non-profit stores that takes donations and then resells the items back to the public. I was at Goodwill recently (we stop by at least once a week) and noticed this little Logitech Webcam for US$3.99. Frankly, I thought it was a little overpriced, but I figured I'd go for it regardless.
One of the nice things about these older Logitech Webcams is that they don't require a driver download and they support Windows Image Acquisition (WIA) directly. WIA is an API included in Windows that aims to unify the acquisition of images from all kinds of devices, including scanners and cameras. This is a pretty low-level API and a bit of a hassle to us. However, after Windows XP SP1 the WIA Automation Layer was released with a simpler COM API meant for VB6 and ASP developers. You can download this layer from MSDN and other places as it is free for redistribution. The meat of the layer is the wiaaut.dll that should be copied into the system32 directory and registered via the command "regsvr32 wiaaut.dll."
Getting to WIA from .NET
The wiaaut.dll COM automation library can be added via "Add Reference" from with Visual Studio.NET 2005 and a .NET wrapper will be automatically generated. Only devices whose drivers support WIA will be available via this interface. Most name-brand cameras will work just fine, but some no-name brands won't appear. If your device appears in the Control Panel's "Scanners and Cameras" interface, then this technique, and this program, should be able to see it. My camera is shown in the figure below.
In our code, we can add a namespace qualifying statement like using WIA in C# or imports WIA in VB.NET to name it easier to access these newly imported classes and interfaces. To start, we'll need to get a hold of a DeviceID. WIA thinks about things in terms of Devices, Commands, and Formats. Devices have types like Camera, Scanner or Video. Commands are things like "Take Picture" and Formats are JPEG or BMP, etc.
We'll create an instance of a CommandDialogClass (from the newly imported WIA namespace) and ask it to show us a select dialog so that we might select from any kind of device. You can show only Video devices or only Scanners by changing the WiaDeviceType enumeration that's passed in. We'll only show this select dialog when the user clicks "Configure" in our application, or when the application has been started with invalid configuration data.
Visual C#
CommonDialogClass class1 = new CommonDialogClass();
Device d = class1.ShowSelectDevice(WiaDeviceType.UnspecifiedDeviceType, true,false);
if (d != null)
{
settings.DeviceID = d.DeviceID;
settings.Save();
}
Visual Basic
Dim class1 As CommonDialogClass = New CommonDialogClass
Dim d As Device = class1.ShowSelectDevice(WiaDeviceType.UnspecifiedDeviceType, true, false)
If (Not (d) Is Nothing) Then
settings.DeviceID = d.DeviceID
settings.Save
End If
This select dialog is, fortunately, supplied completely by Windows and returns a Device. Each Device has a DeviceID that we will save into our User-specific settings class. This class was generated automatically by new features in Visual Studio .NET 2005 that make managing settings fantastically easy. I right-clicked on the Project from within the Visual Studio Solution Explorer and selected "Settings." After I indicated the names and data types of the settings I needed to save, Visual Studio 2005 generated a class that exposed strongly-typed properties such as DeviceID. Settings can also be saved more easily in .NET 2.0. The DeviceID for my Webcam happened to be "{6BDD1FC6-810F-11D0-BEC7-08002BE2092F}\0003" and is stored in my user's Documents And Settings\Local Settings\Application Data\BlogWebcam directory.
Taking a Picture
Once we've stored away a DeviceID in the configuration file, we'll want to connect to our device and take a picture. We'll need to find the device via it's ID without showing the dialog, connect to it and hold on the Device instance.
Visual C#
DeviceManager manager = new DeviceManagerClass();
Device d = null;
foreach (DeviceInfo info in manager.DeviceInfos)
{
if (info.DeviceID == settings.DeviceID)
{
d = info.Connect();
break;
}
}
Visual Basic
Dim manager As DeviceManager = New DeviceManagerClass
Dim d As Device = Nothing
For Each info As DeviceInfo In manager.DeviceInfos
If (info.DeviceID = settings.DeviceID) Then
d = info.Connect
Exit For
End If
Next
Now we're back where we were before, with a Device instance in the variable d. We'll be connecting to the device each time our timer is started. Each device has a series of commands available to it, and these commands are well-known and identified by GUIDS both in the Registry and in the MSDN Help. We're interested in "Take Picture" which has the GUID string value "AF933CAC-ACAD-11D2-A093-00C04F72DC3C”, but the COM interface also exposes this value in the constant CommandID.wiaCommandTakePicture. When we get a hold of our Device we can spin through its available Commands until we find this one to determine if the device supports it, or we can just call it directly via Device.ExecuteCommand.
Visual C#
Item item = device.ExecuteCommand(CommandID.wiaCommandTakePicture);
Visual Basic
Dim item As Item = device.ExecuteCommand(CommandID.wiaCommandTakePicture)
Once we've called ExecuteCommand on our device an "Item" is returned. This isn't just any ordinary item, it's a WIA Item. Each WIA may include any number of image Formats. These formats are also well-known and appear in the registry. I look them up in the startup of the app just in case they change, rather than hard-coding them.
We spin through the available formats looking for JPEG. I could likely have hard-coded the JPEG GUID and avoided this quick spin, but I also wanted to illustrate how you can find your way around the WIA object model. Once we've found JPEG, we call item.Transfer and an ImageFile is returned that we can save to disk.
Visual C#
Item item = device.ExecuteCommand(CommandID.wiaCommandTakePicture);
foreach (string format in item.Formats)
{
if (format == jpegGuid)
{
WIA.ImageFile imagefile = item.Transfer(format) as WIA.ImageFile;
filename = GetFreeFileName();
if (string.IsNullOrEmpty(filename) == false)
{
imagefile.SaveFile(filename);
}
this.picLastImage.Load(filename);
return filename;
}
}
Visual Basic
Item item = device.Execute
Dim item As Item = device.ExecuteCommand(CommandID.wiaCommandTakePicture)
For Each format As String In item.Formats
If (format = jpegGuid) Then
Dim imagefile As WIA.ImageFile = CType(item.Transfer(format),WIA.ImageFile)
filename = GetFreeFileName
If (String.IsNullOrEmpty(filename) = false) Then
imagefile.SaveFile(filename)
End If
Me.picLastImage.Load(filename)
Return filename
End If
Next
Incidentally, I also load the saved image into a PictureBox on my WinForm and return it from this TakePicture() method.
Uploading the Picture to my WebLog
Now that I've taken a picture, what am I going to do to it? Well, why not upload it to a specific filename on my blog so folks can see me and my workspace; what could be more thrilling? In the past, FTP'ing a file would require a third-party library, but .NET 2.0 has extended the System.Net.WebRequest class with support for FTP.
Here we'll create an FtpWebRequest by passing an ftp:// URL to the WebRequest.Create method. For convenience I'll include the username and password in the URL like this: ftp://username:password@ftp.myurl.com/blog/webcam.jpg. Note that the URL includes the username, password, domain name and destination filename all in one string. We'll load our local file into a byte array and write it out (upload it) to the FtpWebRequest's underlying stream by retrieving it with GetRequestStream() and then Write().
Visual C#
FtpWebRequest request = (FtpWebRequest)WebRequest.Create(settings.FTPServerURL);
request.Method = WebRequestMethods.Ftp.UploadFile;
request.UseBinary = true;
FileInfo fileInfo = new FileInfo(filename);
byte[] fileContents = new byte[fileInfo.Length];
using (FileStream fr = fileInfo.OpenRead())
{
fr.Read(fileContents, 0, Convert.ToInt32(fileInfo.Length));
}
using (Stream writer = request.GetRequestStream())
{
writer.Write(fileContents, 0, fileContents.Length);
}
using (FtpWebResponse response = (FtpWebResponse)request.GetResponse())
{
}
Visual Basic
Dim request As FtpWebRequest = Nothing
request = CType(WebRequest.Create(settings.FTPServerURL),FtpWebRequest)
request.Method = WebRequestMethods.Ftp.UploadFile
request.UseBinary = true
Dim fileInfo As FileInfo = New FileInfo(filename)
Dim fileContents() As Byte = New Byte((fileInfo.Length) - 1) {}
Using fr As FileStream = fileInfo.OpenRead
fr.Read(fileContents, 0, Convert.ToInt32(fileInfo.Length))
End Using
Using writer As Stream = request.GetRequestStream
writer.Write(fileContents, 0, fileContents.Length)
End Using
Dim response As FtpWebResponse = CType(request.GetResponse,FtpWebResponse)
You may notice the use of the "using" statements in the code snippet above. Using "using" with classes that implement IDisposable will automatically cause Dispose() to be called when then using block exits. Some folks don't like the syntax and others believe that using it even if the underlying Dispose() doesn't do anything is syntactic sugar. Personally, I really like the syntax, and in this sample the using statement is closing the FileStream, the Request stream, and the FtpWebResponse.
Conclusion
There are things that could be extended, added, and improved on with this project. Here are some ideas to get you started:
- Interface with X10 or your doorbell to take a picture and display it on your Media Center PC.
- Stitch together hundreds of photos, perhaps of your baby, into time-lapse videos.
- Create a security system that detects motion by diff'ing photos and emails you with an alarm and the attached photo!
- Upload photos to Flickr or Smugmug with their APIs.
Have fun and have no fear when faced with the words: Some Assembly Required!