JavaScript Error

You currently have JavaScript disabled on your web browser.

This website uses JavaScript, and This web page needs JavaScript activated to work correctly.

Please active JavaScript on your web browser and then refresh this web page.



WebCam Monitor



webcam

In this blog post I will share with you the steps that I took to create myself a WebCam Monitor program using Visual Studio 2022 and C# .NET 6.0 (although I believe that this same code might work OK with some older versions of .NET as well).

The reason why I developed this particular WebCam Monitor program, is because I want to take time-lapse snapshot images of a flower pot plant that I have sitting next to a bay window, so that I can see its leaves move over a period of time over the course of a couple of days (see below).

The WebCam that I am using for this project is a USB Logitech Webcam, that is connected to a Desktop PC that is running Windows 10 operating system.


Page Contents:

 


New Project

To begin, I performed the following steps on my development computer to create a new blank C# Windows Forms project:

  1. I open up Visual Studio 2022, and select Create a new project in the opening screen:
    example1

  2. In the Create a new project screen, I set the template filters to C# > Windows > Desktop, and then select the Windows Forms App project template before clicking on the Next button:
    example2

  3. In the Configure your new project screen, I set the Project name, Location, and Solution name, before clicking on the Next button:
    example3

  4. In the Additional information screen, I select .NET 6.0 (Long-term support) option from the Framework drop down, and then click on the Create button:
    example4



 


Custom User Settings

Once the Create a new project wizard finishes creating the initial project and loads it into Visual Studio's editor, I right mouse click on the project name from Solution Explorer and then select Add and Class... from the popup context menu to create and add a new Class file to the project. I named this new class file settingsData.cs:

example5

The settingsData.cs data class will be used to load and save any program user settings for this application (i.e. window size, location, last used form settings, etc...) into an external XML file. I include this so that the last used form values can automatically be reapplied and set the next time the user re-uses this application, so that the user does not have to manually reset the form values every time they re-use the application (added value by providing users convenience).

The following is the complete source code of settingsData.cs:

 
// Copyright 2024 T&J Divisions, LLC	
// Designed and developed by Tim Tolbert
// All Rights Reserved
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using System.Xml;
using System.Xml.Serialization;

namespace WebCamMonitor
{
    [Serializable]
    [XmlRoot("settings")]
    public class SettingsData
    {
        [XmlIgnore]
        public string filePath = Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location) + "\\settings.xml";

        [XmlElement]
        public int left { get; set; }

        [XmlElement]
        public int height { get; set; }

        [XmlElement]
        public int top { get; set; }

        [XmlElement]
        public int width { get; set; }
        
        [XmlElement]
        public int saveInterval { get; set; }

        [XmlElement]
        public int intervalType { get; set; }

        [XmlElement]
        public string savePath { get; set; }

        public void initFrom(SettingsData? copy)
        {
            // set default values
            this.left = 100;
            this.height = 450;
            this.top = 100;
            this.width = 850;
            this.saveInterval = 1;
            this.intervalType = 0;
            this.savePath = "C:\\temp\\webcam\\";

            // init from copy, if present
            if (copy != null)
            {
                this.left = copy.left;
                this.height = copy.height;
                this.top = copy.top;
                this.width = copy.width;
                this.saveInterval = copy.saveInterval;
                this.intervalType = copy.intervalType;
                this.savePath = copy.savePath;
            }
        }

        public SettingsData()
        {
            // init with default values
            initFrom(null);
        }

        public void initFrom(object copy)
        {
            try
            {
                // try and init object as SettingsData
                initFrom((SettingsData)copy);
            }
            catch
            {
                // if error then just init with default values
                initFrom(null);
            }
        }

        public bool initFromXMLFile()
        {
            return initFromXMLFile(this.filePath);
        }
        
        // init this data object from an xml file
        public bool initFromXMLFile(string? filename)
        {
            // init return value
            bool functionResult = false;

            // check if null
            if(filename == null)
            {
                filename = this.filePath;
            }

            // try and load from file
            try
            {
                if (File.Exists(filename))
                {
                    XmlSerializer mySerializer = new XmlSerializer(this.GetType());
                    FileStream fs = new FileStream(filename, FileMode.Open);
                    object copy = (object)mySerializer.Deserialize(fs);
                    functionResult = true;
                    fs.Close();
                    this.initFrom(copy);
                }
                else
                {
                    functionResult = false;
                }
            }
            catch
            {
                functionResult = false;
            }
            return functionResult;
        }

        public void saveToXMLFile()
        {
            saveToXMLFile(this.filePath, true);
        }

        // serialize and save this data class object to an XML file
        public void saveToXMLFile(string filename, bool overwrite)
        {
            // pre-check
            if (filename.Trim() == "")
            {
                throw new Exception("No filename was specified! Please provide a valid file name first!");
            }
            if (File.Exists(filename) == true)
            {
                if (overwrite == true)
                {
                    File.Delete(filename);
                }
                else
                {
                    throw new Exception("File already exists! Set the overwrite flag to true to overwrite it!");
                }
            }
            // save to file
            XmlSerializer mySerializer = new XmlSerializer(this.GetType());
            TextWriter writer = new StreamWriter(filename);
            mySerializer.Serialize(writer, this);
            writer.Close();
        }

    }
}


 


WebCam Object

I then right mouse click on the project name again, and add another new Class file to the project. I name this new class file TCamDevice.cs. This class will be used to represent the main WebCam object itself.

The following is the complete source code for TCamDevice.cs:

 
// Copyright 2024 T&J Divisions, LLC	
// Designed and developed by Tim Tolbert
// All Rights Reserved
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;

namespace WebCamMonitor
{
    public class TCamDevice
    {
        private const short WM_CAP = 0x400;
        private const int WM_CAP_DRIVER_CONNECT = 0x40a;
        private const int WM_CAP_DRIVER_DISCONNECT = 0x40b;
        private const int WM_CAP_EDIT_COPY = 0x41e;
        private const int WM_CAP_SET_PREVIEW = 0x432;
        private const int WM_CAP_SET_OVERLAY = 0x433;
        private const int WM_CAP_SET_PREVIEWRATE = 0x434;
        private const int WM_CAP_SET_SCALE = 0x435;
        private const int WM_CAP_START = 0x400;
        private const int WM_CAP_SEQUENCE = WM_CAP_START + 62;
        private const int WM_CAP_FILE_SAVEAS = WM_CAP_START + 23;
        private const int WM_CAP_FILE_SAVEDIB = WM_CAP_START + 25;
        private const int WM_CAP_GRAB_FRAME = WM_CAP_START + 60;
        private const int WS_CHILD = 0x40000000;
        private const int WS_VISIBLE = 0x10000000;
		
        [DllImport("avicap32.dll")]
        protected static extern int capCreateCaptureWindowA([MarshalAs(UnmanagedType.VBByRefStr)] ref string lpszWindowName, int dwStyle, int x, int y, int nWidth, int nHeight, int hWndParent, int nID);

        [DllImport("user32", EntryPoint = "SendMessageA")]
        protected static extern int SendMessage(int hwnd, int wMsg, int wParam, [MarshalAs(UnmanagedType.AsAny)] object lParam);

        [DllImport("user32")]
        protected static extern int SetWindowPos(int hwnd, int hWndInsertAfter, int x, int y, int cx, int cy, int wFlags);

        [DllImport("user32")]
        protected static extern bool DestroyWindow(int hwnd);

        int deviceHandle;
        int index;
		
        public TCamDevice(int index)
        {
            this.index = index;
        }

        private string _name;
        public string Name
        {
            get { return _name; }
            set { _name = value; }
        }

        private string _version;
        public string Version
        {
            get { return _version; }
            set { _version = value; }
        }

        public override string ToString()
        {
            return this.Name;
        }
        
        // To Initialize the device
        public void Init(int windowHeight, int windowWidth, int handle)
        {
            string deviceIndex = Convert.ToString(this.index);
            deviceHandle = capCreateCaptureWindowA(ref deviceIndex, WS_VISIBLE | WS_CHILD, 0, 0, windowWidth, windowHeight, handle, 0);

            if (SendMessage(deviceHandle, WM_CAP_DRIVER_CONNECT, this.index, 0) > 0)
            {
                SendMessage(deviceHandle, WM_CAP_SET_SCALE, -1, 0);
                SendMessage(deviceHandle, WM_CAP_SET_PREVIEWRATE, 0x42, 0);
                SendMessage(deviceHandle, WM_CAP_SET_PREVIEW, -1, 0);

                SetWindowPos(deviceHandle, 1, 0, 0, windowWidth, windowHeight, 6);
            }
        }

        // Shows the webcam preview in the control
        public void ShowWindow(global::System.Windows.Forms.Control windowsControl)
        {
            Init(windowsControl.Height, windowsControl.Width, windowsControl.Handle.ToInt32());
        }

        // Stop the webcam and destroy the handle
        public void Stop()
        {
            SendMessage(deviceHandle, WM_CAP_DRIVER_DISCONNECT, this.index, 0);
            DestroyWindow(deviceHandle);
        }

        public void StartRecording()
        {
            SendMessage(deviceHandle, WM_CAP_SEQUENCE, 0, 0);
        }
		
        public void StopRecording(string targetAVIFilePath)
        {
            SendMessage(deviceHandle, WM_CAP_FILE_SAVEAS, 0, targetAVIFilePath);
        }
		
        public Image CaptureImage(int imgWidth = 500, int imgHeight = 500)
        {
            Bitmap bmp = new Bitmap(imgWidth, imgHeight);
            Image img = null;
            Clipboard.Clear();
            SendMessage(deviceHandle, WM_CAP_EDIT_COPY, 0, 0);
            IDataObject data = Clipboard.GetDataObject();
            if (data.GetDataPresent(bmp.GetType()))
            {
                img = (Image)data.GetData(bmp.GetType());
            }
            return img;
        }
    }
}


 


Front-End UI

For developing the Front-End user interface, I load Form1 into the form editor and conduct the following activities:



example6


 


Back-End Source Code

To complete the program, I edited Form1.cs source code to instantiate an instance of the WebCam object (TCamDevice), and then I coded each of the form controls callback methods, including user settings during form load and closing, with the code below. I also added a few helper functions for reseting the countdown timer, and for saving the snapshot to file.

The following is the complete source code for Form1.cs:

 
// Copyright 2024 T&J Divisions, LLC	
// Designed and developed by Tim Tolbert
// All Rights Reserved
namespace WebCamMonitor
{
    public partial class Form1 : Form
    {
        private TCamDevice webcam1;
        private SettingsData mySettings;
        private int totalSnapshots;
        private DateTime endTime;

        public Form1()
        {
            InitializeComponent();
        }

        private void Form1_Load(object sender, EventArgs e)
        {
            // update form title to show program name and app version
            this.Text = Application.ProductName + " ~ Version " + Application.ProductVersion;

            // load user settings from XML file
            mySettings = new SettingsData();
            mySettings.initFromXMLFile();

            // ensure form will be positioned on screen
            if (mySettings.left < 0)
            {
                mySettings.left = 0;
            }
            if (mySettings.top < 0)
            {
                mySettings.top = 0;
            }
            if (mySettings.width < 100)
            {
                mySettings.width = 100;
            }
            if (mySettings.height < 100)
            {
                mySettings.height = 100;
            }

            // set form position
            this.Left = mySettings.left;
            this.Top = mySettings.top;

            // set form size
            this.Width = mySettings.width;
            this.Height = mySettings.height;

            // init form controls with last used values
            saveInterval.Value = mySettings.saveInterval;
            intervalType.SelectedIndex = mySettings.intervalType;
            timer1.Interval = mySettings.saveInterval;
            textBoxSavePath.Text = mySettings.savePath;

            // init webcam
            totalSnapshots = 0;
            webcam1 = new TCamDevice(0);
            webcam1.ShowWindow(pictureBox1);

            // init form controls
            buttonRecord.BackColor = Color.IndianRed;
            buttonRecord.ForeColor = Color.White;
            labelCountdown.Text = "";

        }

        private void Form1_FormClosing(object sender, FormClosingEventArgs e)
        {
            // stop the webcam
            webcam1.Stop();
            webcam1 = null;

            // make sure timer is stopped
            if (timer1.Enabled == true)
            {
                timer1.Enabled = false;
            }

            // update user settings
            mySettings.left = this.Left;
            mySettings.top = this.Top;
            mySettings.width = this.Width;
            mySettings.height = this.Height;
            mySettings.saveInterval = (int)saveInterval.Value;
            mySettings.intervalType = intervalType.SelectedIndex;
            mySettings.savePath = textBoxSavePath.Text;

            // save user settings to XML file
            mySettings.saveToXMLFile();
        }

        private void buttonRecord_Click(object sender, EventArgs e)
        {
            // toggle the record state
            if (buttonRecord.Text == "Start Recording")
            {
                // update form controls
                buttonRecord.Text = "Stop Recording";
                buttonRecord.BackColor = Color.Orange;
                buttonRecord.ForeColor = Color.Red;
                labelCountdown.Text = "";

                // disable form controls
                buttonBrowse.Enabled = false;
                saveInterval.Enabled = false;
                intervalType.Enabled = false;
                textBoxSavePath.Enabled = false;

                // set countdown
                resetCountDown();

                // reset snapshot count
                totalSnapshots = 0;

                // begin timer
                timer1.Enabled = true;
            }
            else
            {
                // stop timer
                timer1.Enabled = false;

                // enable form controls
                buttonBrowse.Enabled = true;
                saveInterval.Enabled = true;
                intervalType.Enabled = true;
                textBoxSavePath.Enabled = true;

                // update form controls
                buttonRecord.Text = "Start Recording";
                buttonRecord.BackColor = Color.IndianRed;
                buttonRecord.ForeColor = Color.White;
                labelCountdown.Text = "";
            }
			
            // take snapshot now
            saveSnapshot();

            // update snapshot counter
            totalSnapshots++;
            toolStripStatusLabel1.Text = "Total Snapshots: " + Convert.ToString(totalSnapshots);

        }

        private void buttonBrowse_Click(object sender, EventArgs e)
        {
            // browse for a new root path to save snapshot images into
            folderBrowserDialog1.SelectedPath = textBoxSavePath.Text;
            if (folderBrowserDialog1.ShowDialog() == DialogResult.OK)
            {
                textBoxSavePath.Text = folderBrowserDialog1.SelectedPath;
                // ensure folder path ends with a backslash
                if (textBoxSavePath.Text.Trim() == "")
                {
                    textBoxSavePath.Text = "C:\\temp\\webcam\\";
                }
                else if (textBoxSavePath.Text.EndsWith("\\") == false)
                {
                    textBoxSavePath.Text = textBoxSavePath.Text + "\\";
                }
            }
        }

        private void resetCountDown()
        {
            // calculate total number of seconds to count down
            int totalSeconds = 1;
            if(intervalType.SelectedIndex == 0)
            {
                // seconds
                totalSeconds = (int)saveInterval.Value;
            }
            else if (intervalType.SelectedIndex == 1)
            {
                // minutes
                totalSeconds = (int)saveInterval.Value * 60;
            }
            else if (intervalType.SelectedIndex == 2)
            {
                // hours
                totalSeconds = (int)saveInterval.Value * 60 * 60;
            }

            // set the end time to be totalSeconds from now
            endTime = DateTime.Now.AddSeconds(totalSeconds);
        }

        private void saveSnapshot()
        {
            // This function simply saves the image as a PNG file, however
            // it can easily be adjusted to save as different image types.

            // get webcam image using default 500x500 image size
            Image img = webcam1.CaptureImage();
            if (img != null)
            {
                // Save the image as temp image into the root path. This
                // temp image can be displayed on a website or something.
                string tempPath = textBoxSavePath.Text + "webcam.png";
                img.Save(tempPath);

                // Also save the image into archive subfolder within the root path.

                // build archive subfolder path
                string tempPath2 = textBoxSavePath.Text + "archive\\" + 
                    DateTime.Now.ToString("yyyy") + "\\" + 
                    DateTime.Now.ToString("MM") + "\\" + 
                    DateTime.Now.ToString("dd") + "\\";

                // if archive subfolder path does not exist then create it
                if (System.IO.Directory.Exists(tempPath2) == false)
                {
                    System.IO.Directory.CreateDirectory(tempPath2);
                }

                // build complete archive save path
                tempPath2 = tempPath2 + DateTime.Now.ToString("HH-mm-ss-ff") + ".png";

                // save at complete archive save path
                img.Save(tempPath2, System.Drawing.Imaging.ImageFormat.Png);
            }
        }

        private void timer1_Tick(object sender, EventArgs e)
        {
            // display countdown
            TimeSpan tempTS = endTime.Subtract(DateTime.Now);
            labelCountdown.Text = "Countdown Time Remaining : " + tempTS.ToString();

            // check if countdown is up
            if (tempTS.TotalSeconds <= 0)
            {
                // take snapshot
                saveSnapshot();

                // update snapshot counter
                totalSnapshots++;
                toolStripStatusLabel1.Text = "Total Snapshots: " + Convert.ToString(totalSnapshots);

                // reset countdown
                resetCountDown();
            }
        }
    }
}


 


Compile and Run

When the above code is compiled and executed, the program resembles something similar to the following:


example8


example9


example10


example11


example12


 


Results: Plant Leaves Moving

After spinning the bay window plant 180 degrees, I ran this program continuously for a few days and captured the plants leaves move at 30 minute time lapse intervals. I combined the generated snapshot images to produce the following animated results (please pardon the cats outside the window, they just saying hi) :





Final Thoughts

This WebCam Monitor program is not just limited to Logitech WebCam's, as I have tested that it works well with various other USB WebCam types and Built-In WebCam's as well.

This basic WebCam Monitor program can also be modified and enhanced to support multiple WebCam's at once. If you run this program now as is and you have multiple WebCam's attached to your computer, then you might be prompted to select which WebCam you want to use this program with:

example7

This WebCam Monitor program can also be modified and enhanced to provide and offer other features as well, such as Motion Detection, Microphone, Alert Notifications, People/Face Recognition, etc...

Thank you for reading, I hope you found this blog post (tutorial) educational and helpful.


(0) pollYesResult
(0) pollNoResult



 
     About   |   Contact Us   |   Privacy   |   Terms & Conditions   |   © 2024 - T&J Divisions, LLC, All Rights Reserved