Have you ever wondered how to grab pictures from a web cam into your .NET application. What if that web cam is remote and password protected. What if it is mobile web cam such as Rovio. What if you like to detect motion events and fire some logic to notify you of such motion.
Well, today I would like to walk you through on how to create such application.
Before we begin you need to download AForge if you like to have motion sensing capabilities - http://code.google.com/p/aforge/ . "AForge.NET is a C# framework designed for developers and researchers in the fields of Computer Vision and Artificial Intelligence - image processing, neural networks, genetic algorithms, machine learning, etc."
You would not need the whole package just several classes. I had to modify some of the original AForge classes to suit my needs, but you are welcome to use original provided you comply with licensing requirements.
Part 1 - Figuring out where web cams store their pictures.
In my case I had two types of web cams. TRENDnet Wireless Internet Camera Server TV-IP201W and Rovio which I mentioned earlier. By logging in to my web cam server I could see the following page. This web page is generated by a camera embedded web server. My guess that it is running some sort of windows embedded OS since it is rendering aspx pages, but leave this to the reader to figure out since it is irrelevant for this post.
This front page image is updated every time you request a page. So my guess was that camera stores temporary images internally and provides a link to those images. The next step is to figure out the exact uri of the image. This was easily done by opening page source and examining html code. Please see the screen shot below.
Yes, that weird number is a relative reference to server's folder. Convert it to full uri and you would get something like http://MyCameraServer.com/goform/capture?12378465237865336 .
This is our first type of cameras.
Now lets explore Rovio, which has a very simple to use set of APIs. There I found that I can just go to something like http://MyCameraServer:MyPort/Jpeg/CamImg0000.jpg , where MyCameraServer:Myport is an address where you set up your Rovio. That's it. Easy.
Part 2 - Connecting to remote server.
To connect to remote web server we would utilize classes WebRequest and WebResponce from System.Net namespace. It is very simple. First we need to create web request and provide URI, then we need to add credentials, then execute request, which returns response as instance of WebResponce class. Later we can access stream reader of web response and read our data.
Below is an example.
Dim request As WebRequest Dim response As WebResponse Dim reader As Stream Dim data(8191) As Byte Dim count As Integer Dim total As Integer request = WebRequest.Create(url) request.Credentials = New NetworkCredential("login name", "password") response = request.GetResponse() reader = response.GetResponseStream() total = 0 Dim myBitmap As Bitmap = New Bitmap(reader, True) While True count = reader.Read(data, 0, 8192) If count <= 0 Then Exit While End If total += 8192 End While reader.Close()
Part 3 - Automatic refresh
You probably would not want to refresh this picture every time. So here is what I did, there are many ways, I prefer the quickest one :). Set the timer and refresh the picture by timer. It works great, except now if you set it to refresh too often your UI will freeze. So the idea is to load pictures on a separate thread. And for that I have created this simple class ThreadingHelper.
Imports System.Threading Public Class ThreadingHelper Private Shared _currentForm As Form ''---------------------------------------------------------------------------- '' methods related to threading ''---------------------------------------------------------------------------- Public Delegate Function DelegateBegin(ByVal endCode As DelegateEnd) As DelegateEnd Public Delegate Sub DelegateEnd() Public Sub BeginCode(ByVal currentForm As Form, _ ByVal _beginCode As DelegateBegin, _ ByVal _endCode As DelegateEnd) _currentForm = currentForm Dim callbackTransfer As AsyncCallback = New AsyncCallback(AddressOf TransferContext) _beginCode.BeginInvoke(_endCode, callbackTransfer, Nothing) End Sub Public Sub TransferContext(ByVal asyncResult As System.IAsyncResult) Dim asyncResultBegin As Runtime.Remoting.Messaging.AsyncResult Dim delegateBegin As DelegateBegin Dim endCode As DelegateEnd asyncResultBegin = CType(asyncResult, Runtime.Remoting.Messaging.AsyncResult) delegateBegin = asyncResultBegin.AsyncDelegate endCode = delegateBegin.EndInvoke(asyncResult) Try If _currentForm.InvokeRequired Then _currentForm.Invoke(endCode) Else endCode() End If Catch ex As NullReferenceException ''if error occured on a different thread for the purpose of this streaming application we are just going to swallow it. '' write youe handler if you like Catch ex As ObjectDisposedException Catch ex As Exception End Try End Sub End Class
Here is how to use it.
myThreading = New ThreadingHelper()
myThreading.BeginCode(Me, AddressOf GetPicture, AddressOf UpdatePicture)
Part 4 - Rest of the code.
Imports System.Net
Imports System.IO
Imports MotionDetection
Public Class Form1
Private Const url1 As String = "http://myserver/goform/capture?1078432126434196"
Private Const url2 As String = "http://myserver/goform/capture?1073114869312048"
Private Const url3 As String = "http://myserver/goform/capture?1225387068534147"
Private Const url4 As String = "http://myserver/goform/capture?1225387347966623"
Private Const url5 As String = "http://myserver/goform/capture?1082672364580373"
Private Const url6 As String = "http://myserver/Jpeg/CamImg0000.jpg"
Private cameras(,) As String = { _
{"camera 1", url1}, _
{"camera 2", url2}, _
{"camera 3", url3}, _
{"camera 4", url4}, _
{"camera 5", url5}, _
{"rovio 1", url6} _
}
Private Shared _cameraIndex As Integer = 0
Private Shared _motionLevel As Double = 10.0
Private Shared _detectMotion As Boolean = False
Private Shared globalBitmap As Bitmap
Private Shared _motionDetector As MotionDetector3
Private myThreading As ThreadingHelper
Private Shared urlCurr As String = url1
Private Sub Form1_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
For i As Integer = 0 To (cameras.Length / 2) - 1
ComboBoxCameras.Items.Add(cameras(i, 0))
Next
ComboBoxCameras.SelectedIndex = 0
urlCurr = cameras(ComboBoxCameras.SelectedIndex, 1)
myThreading = New ThreadingHelper()
_motionDetector = New MotionDetector3()
_motionDetector.MotionLevelCalculation = True
Timer1.Start()
End Sub
Private Sub LoadFile(ByVal url As String)
Try
Dim request As WebRequest
Dim response As WebResponse
Dim reader As Stream
Dim data(8191) As Byte
Dim count As Integer
Dim total As Integer
request = WebRequest.Create(url)
request.Credentials = New NetworkCredential("login name", "password")
response = request.GetResponse()
reader = response.GetResponseStream()
total = 0
Dim myBitmap As Bitmap = New Bitmap(reader, True)
While True
count = reader.Read(data, 0, 8192)
If count <= 0 Then
Exit While
End If
total += 8192
End While
reader.Close()
If Not globalBitmap Is Nothing Then
globalBitmap.Dispose()
End If
''to prevent cross threading access to a shared memeber you would need to clone image since it would be stored on the local stack and you would need to update it
globalBitmap = myBitmap.Clone(New Rectangle(0, 0, myBitmap.Width, myBitmap.Height), Imaging.PixelFormat.Format32bppRgb)
If Not myBitmap Is Nothing Then
myBitmap.Dispose()
End If
response.Close()
Catch ex As Exception
'MessageBox.Show(ex.Message)
End Try
End Sub
Private Sub Timer1_Tick(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Timer1.Tick
Try
Timer1.Stop()
myThreading.BeginCode(Me, AddressOf GetPicture, AddressOf UpdatePicture)
Catch ex As Exception
MessageBox.Show(ex.Message)
Timer1.Start()
End Try
End Sub
Private Function GetPicture(ByVal endCode As ThreadingHelper.DelegateEnd) As ThreadingHelper.DelegateEnd
Try
LoadFile(urlCurr)
Catch ex As Exception
End Try
Return endCode
End Function
Private Sub UpdatePicture()
Try
If Not PictureBox1.Image Is Nothing Then
PictureBox1.Image.Dispose()
End If
Dim myBitmap As Bitmap
myBitmap = globalBitmap.Clone(New Rectangle(0, 0, globalBitmap.Width, globalBitmap.Height), Imaging.PixelFormat.Format32bppRgb)
If _detectMotion Then
_motionDetector.ProcessFrame(myBitmap)
If (_motionDetector.MotionLevel * 100 > _motionLevel) Then
_motionLevel += 10
TextBoxMotionLevel.Text = _motionLevel.ToString()
MessageBox.Show("Motion detected, here we can automatically send e-mail and deliver files over network somehow.")
_motionDetector.Reset()
End If
End If
PictureBox1.Image = myBitmap
Timer1.Start()
Catch ex As Exception
MessageBox.Show(ex.Message)
Timer1.Start()
End Try
End Sub
Private Sub Form1_FormClosing(ByVal sender As System.Object, ByVal e As System.Windows.Forms.FormClosingEventArgs) Handles MyBase.FormClosing
Timer1.Stop()
If Not PictureBox1.Image Is Nothing Then
PictureBox1.Image.Dispose()
End If
End Sub
Private Sub ComboBoxCameras_SelectedIndexChanged(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles ComboBoxCameras.SelectedIndexChanged
urlCurr = cameras(ComboBoxCameras.SelectedIndex, 1)
_cameraIndex = ComboBoxCameras.SelectedIndex
End Sub
Private Sub CheckBoxDetectMotion_CheckedChanged(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles CheckBoxDetectMotion.CheckedChanged
_detectMotion = CheckBoxDetectMotion.Checked
End Sub
Private Sub ButtonSetMotionLevel_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles ButtonSetMotionLevel.Click
Dim newLevel As Double
If (Double.TryParse(TextBoxMotionLevel.Text, newLevel)) Then
_motionLevel = newLevel
End If
End Sub
Private Sub ButtonResetDetector_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles ButtonResetDetector.Click
_motionDetector.Reset()
End Sub
End Class
Imports System.Net Imports System.IO Imports MotionDetection Public Class Form1 Private Const url1 As String = "http://myserver/goform/capture?1078432126434196" Private Const url2 As String = "http://myserver/goform/capture?1073114869312048" Private Const url3 As String = "http://myserver/goform/capture?1225387068534147" Private Const url4 As String = "http://myserver/goform/capture?1225387347966623" Private Const url5 As String = "http://myserver/goform/capture?1082672364580373" Private Const url6 As String = "http://myserver/Jpeg/CamImg0000.jpg" Private cameras(,) As String = { _ {"camera 1", url1}, _ {"camera 2", url2}, _ {"camera 3", url3}, _ {"camera 4", url4}, _ {"camera 5", url5}, _ {"rovio 1", url6} _ } Private Shared _cameraIndex As Integer = 0 Private Shared _motionLevel As Double = 10.0 Private Shared _detectMotion As Boolean = False Private Shared globalBitmap As Bitmap Private Shared _motionDetector As MotionDetector3 Private myThreading As ThreadingHelper Private Shared urlCurr As String = url1 Private Sub Form1_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load For i As Integer = 0 To (cameras.Length / 2) - 1 ComboBoxCameras.Items.Add(cameras(i, 0)) Next ComboBoxCameras.SelectedIndex = 0 urlCurr = cameras(ComboBoxCameras.SelectedIndex, 1) myThreading = New ThreadingHelper() _motionDetector = New MotionDetector3() _motionDetector.MotionLevelCalculation = True Timer1.Start() End Sub Private Sub LoadFile(ByVal url As String) Try Dim request As WebRequest Dim response As WebResponse Dim reader As Stream Dim data(8191) As Byte Dim count As Integer Dim total As Integer request = WebRequest.Create(url) request.Credentials = New NetworkCredential("login name", "password") response = request.GetResponse() reader = response.GetResponseStream() total = 0 Dim myBitmap As Bitmap = New Bitmap(reader, True) While True count = reader.Read(data, 0, 8192) If count <= 0 Then Exit While End If total += 8192 End While reader.Close() If Not globalBitmap Is Nothing Then globalBitmap.Dispose() End If ''to prevent cross threading access to a shared memeber you would need to clone image since it would be stored on the local stack and you would need to update it globalBitmap = myBitmap.Clone(New Rectangle(0, 0, myBitmap.Width, myBitmap.Height), Imaging.PixelFormat.Format32bppRgb) If Not myBitmap Is Nothing Then myBitmap.Dispose() End If response.Close() Catch ex As Exception 'MessageBox.Show(ex.Message) End Try End Sub Private Sub Timer1_Tick(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Timer1.Tick Try Timer1.Stop() myThreading.BeginCode(Me, AddressOf GetPicture, AddressOf UpdatePicture) Catch ex As Exception MessageBox.Show(ex.Message) Timer1.Start() End Try End Sub Private Function GetPicture(ByVal endCode As ThreadingHelper.DelegateEnd) As ThreadingHelper.DelegateEnd Try LoadFile(urlCurr) Catch ex As Exception End Try Return endCode End Function Private Sub UpdatePicture() Try If Not PictureBox1.Image Is Nothing Then PictureBox1.Image.Dispose() End If Dim myBitmap As Bitmap myBitmap = globalBitmap.Clone(New Rectangle(0, 0, globalBitmap.Width, globalBitmap.Height), Imaging.PixelFormat.Format32bppRgb) If _detectMotion Then _motionDetector.ProcessFrame(myBitmap) If (_motionDetector.MotionLevel * 100 > _motionLevel) Then _motionLevel += 10 TextBoxMotionLevel.Text = _motionLevel.ToString() MessageBox.Show("Motion detected, here we can automatically send e-mail and deliver files over network somehow.") _motionDetector.Reset() End If End If PictureBox1.Image = myBitmap Timer1.Start() Catch ex As Exception MessageBox.Show(ex.Message) Timer1.Start() End Try End Sub Private Sub Form1_FormClosing(ByVal sender As System.Object, ByVal e As System.Windows.Forms.FormClosingEventArgs) Handles MyBase.FormClosing Timer1.Stop() If Not PictureBox1.Image Is Nothing Then PictureBox1.Image.Dispose() End If End Sub Private Sub ComboBoxCameras_SelectedIndexChanged(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles ComboBoxCameras.SelectedIndexChanged urlCurr = cameras(ComboBoxCameras.SelectedIndex, 1) _cameraIndex = ComboBoxCameras.SelectedIndex End Sub Private Sub CheckBoxDetectMotion_CheckedChanged(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles CheckBoxDetectMotion.CheckedChanged _detectMotion = CheckBoxDetectMotion.Checked End Sub Private Sub ButtonSetMotionLevel_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles ButtonSetMotionLevel.Click Dim newLevel As Double If (Double.TryParse(TextBoxMotionLevel.Text, newLevel)) Then _motionLevel = newLevel End If End Sub Private Sub ButtonResetDetector_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles ButtonResetDetector.Click _motionDetector.Reset() End Sub End Class
And here is the result:
We could have almost streaming video :). The picture appears dark, because I have no control over the light switch in a remote facility. but hopefully you get the idea.
And finally with motion detector.
You could set level of the motion in % and then write your custom code and what to do about detected motion. For example you could send an e-mail with picture attached or using Rovio you could send a specific sound command and alert an "intruder" :).
In my next post I will provide a little more explanation on what modifications I made to AForge library.
This was a very nice ISolvable problem :)