Wednesday, February 24, 2010

Using a Webcam with DirectShowNET and F#

In this post I'm going to show a small F# example of using DirectShowNET to access a webcam and manipulate the image data.

DirectShow


DirectShow is a huge Windows API for video playback and capture. Among many things this API allows flexible access to data captured by video input devices such as a webcam.

DirectShowNET


The DirectShow is COM based API. According to the documentation, it's meant to be used from C++. Luckily there's DirectShowNET which is a very nice library that exposes the DirectShow interfaces to .NET languages .

Accessing the webcam


What I wanted to do for this example is to have access to the data of the image being captured at a given time. The DirectShow API provides the concept of a Sample Grabber which not only gives you access to the captured data, but also allows its modification .

The following example shows the use of a couple of functions defined below:


let device = findCaptureDevice

let mediaControl,filterGraph = createVideoCaptureWithSampleGrabber
device
nullGrabber
None



let form = new Form(Size = new Size(300,300), Visible = true,Text = "Webcam input")


let videoWindow = configureVideoWindow (form.Handle) 300 300 filterGraph

form.Closing.Add (fun _ ->
mediaControl.StopWhenReady()
Marshal.ReleaseComObject(videoWindow) |> ignore
Marshal.ReleaseComObject(mediaControl) |> ignore
Marshal.ReleaseComObject(filterGraph) |> ignore)


mediaControl.Run() |> ignore

Application.Run(form)


Running this program will show the following window:



Notice that we called the createVideoCaptureWithSampleGrabber function using the nullGrabber parameter. This parameter specifies a sample grabber that does nothing with the current frame. Here's the definition:


let nullGrabber =
fun (width,height) ->
{ new ISampleGrabberCB with
member this.SampleCB(sampleTime:double , pSample:IMediaSample )= 0
member this.BufferCB(sampleTime:double , pBuffer:System.IntPtr , bufferLen:int) = 0
}


We can change this sample grabber to something more interesting:


let incrementGrabber =
fun (width,height) ->
{ new ISampleGrabberCB with
member this.SampleCB(sampleTime:double , pSample:IMediaSample )= 0
member this.BufferCB(sampleTime:double , pBuffer:System.IntPtr , bufferLen:int) =
for i = 0 to (bufferLen - 1) do
let c = Marshal.ReadByte(pBuffer,i)
Marshal.WriteByte(pBuffer,i ,if c > byte(150) then byte(255) else c+byte(100))
0 }



We can change the program to use:


let mediaControl,filterGraph = createVideoCaptureWithSampleGrabber
device
incrementGrabber
None


Running the program we can see:



This new sample grabber increments each pixel value by 100 or leaves the maximum value to prevent overflow. As presented here the pixel information is provided as an unmanaged pointer.


Using DirectShowNet



DirectShow also has the concept of filters which are software components that are assembled together to mix various inputs and outputs. For this examples we're going to used only a couple of interfaces. The code for this post is based on the DxLogo sample provided with DirectShowNET.

First we define the module containing these functions:


module LangExplrExperiments.DirectShowCapture

open DirectShowLib
open System.Runtime.InteropServices
open System.Runtime.InteropServices.ComTypes


let private checkResult hresult = DsError.ThrowExceptionForHR( hresult )


The checkResult function is an utility function to check for the HRESULT of some of the calls to COM interfaces. Since DirectShowNET is a thin wrapper for these interfaces we need to use this function to check for errors.

The findCaptureDevice returns the first capture devices detected by DirectShowNET .


let findCaptureDevice =
let devices = DsDevice.GetDevicesOfCat(FilterCategory.VideoInputDevice)
let source : obj ref = ref null
devices.[0].Mon.BindToObject(null,null,ref typeof<IBaseFilter>.GUID,source)
devices.[0]


The following functions define the capture filter and sample grabber. The createVideoCaptureWithSampleGrabber function is the entry point. We can specify a file name as the final parameter to save the captured data to a video file.


let private ConfigureSampleGrabber( sampGrabber:ISampleGrabber,callbackobject:ISampleGrabberCB) =
let media = new AMMediaType()

media.majorType <- MediaType.Video
media.subType <- MediaSubType.RGB24
media.formatType <- FormatType.VideoInfo

sampGrabber.SetMediaType( media ) |> checkResult
DsUtils.FreeAMMediaType(media);

sampGrabber.SetCallback( callbackobject, 1 ) |> checkResult


let getCaptureResolution(capGraph:ICaptureGraphBuilder2 , capFilter:IBaseFilter) =
let o : obj ref = ref null
let media : AMMediaType ref = ref null
let videoControl = capFilter :?> IAMVideoControl

capGraph.FindInterface(new DsGuid( PinCategory.Capture),
new DsGuid( MediaType.Video),
capFilter,
typeof<IAMStreamConfig>.GUID,
o ) |> checkResult

let videoStreamConfig = o.Value :?> IAMStreamConfig;

videoStreamConfig.GetFormat(media) |> checkResult

let v = new VideoInfoHeader()
Marshal.PtrToStructure( media.Value.formatPtr, v )
DsUtils.FreeAMMediaType(media.Value)

v.BmiHeader.Width,v.BmiHeader.Height

let createCaptureFilter (captureDevice:DsDevice)
(sampleGrabberCBCreator: int*int -> ISampleGrabberCB) =
let captureGraphBuilder = box(new CaptureGraphBuilder2()) :?> ICaptureGraphBuilder2
let sampGrabber = box(new SampleGrabber()) :?> ISampleGrabber;
let filterGraph = box(new FilterGraph()) :?> IFilterGraph2
let capFilter: IBaseFilter ref = ref null

captureGraphBuilder.SetFiltergraph(filterGraph) |> checkResult

filterGraph.AddSourceFilterForMoniker(
captureDevice.Mon,
null,
captureDevice.Name,
capFilter) |> checkResult


let resolution = getCaptureResolution(captureGraphBuilder,capFilter.Value)
ConfigureSampleGrabber(sampGrabber, sampleGrabberCBCreator(resolution) )

filterGraph.AddFilter(box(sampGrabber) :?> IBaseFilter , "FSGrabberFilter") |> checkResult


captureGraphBuilder,filterGraph,sampGrabber,capFilter.Value

let getMediaControl (captureGraphBuilder :ICaptureGraphBuilder2)
(sampGrabber: ISampleGrabber)
(capFilter:IBaseFilter)
(filterGraph:IFilterGraph2)
(fileNameOpt:string option)=
let muxFilter: IBaseFilter ref = ref null
let fileWriterFilter : IFileSinkFilter ref = ref null
try
match fileNameOpt with
| Some filename ->
captureGraphBuilder.SetOutputFileName(
MediaSubType.Avi,
filename,
muxFilter,
fileWriterFilter) |> checkResult

| None -> ()

captureGraphBuilder.RenderStream(
new DsGuid( PinCategory.Capture),
new DsGuid( MediaType.Video),
capFilter,
sampGrabber :?> IBaseFilter,
muxFilter.Value) |> checkResult

finally
if fileWriterFilter.Value <> null then
Marshal.ReleaseComObject(fileWriterFilter.Value) |> ignore
if muxFilter.Value <> null then
Marshal.ReleaseComObject(muxFilter.Value) |> ignore

filterGraph :?> IMediaControl


let createVideoCaptureWithSampleGrabber (device:DsDevice)
(sampleGrabberCBCreator: int*int -> ISampleGrabberCB)
(outputFileName: string option) =
let capGraphBuilder,filterGraph,sampGrabber,capFilter = createCaptureFilter device sampleGrabberCBCreator

let mediaControl = getMediaControl capGraphBuilder sampGrabber capFilter filterGraph outputFileName

Marshal.ReleaseComObject capGraphBuilder |> ignore
Marshal.ReleaseComObject capFilter |> ignore
Marshal.ReleaseComObject sampGrabber |> ignore

mediaControl,filterGraph



Also for the creation of the video window the following function is provided:

let configureVideoWindow windowHandle width height (filterGraph:IFilterGraph2) =
let videoWindow = filterGraph :?> IVideoWindow

videoWindow.put_Owner(windowHandle) |> checkResult
videoWindow.put_WindowStyle(WindowStyle.Child ||| WindowStyle.ClipChildren) |> checkResult
videoWindow.SetWindowPosition(0,0,width,height) |> checkResult
videoWindow.put_Visible(OABool.True)|> checkResult

videoWindow


For future posts I'm going to try some experiments with the pixel data provided by the sample grabber.