Free to the community: Motion Detection...A red Highlight will appear around your movements, working out bugs on system beep if anyone has insight?

Justin

Member
Joined
May 1, 2025
Messages
15
Programming Experience
10+
I need help on the system beep when motion is detected...A puzzle if you can solve it...;)Ultimately I want the system beep to go off every 3 seconds when the red highlight surrounds your movement in the picturebox???. I can stay still and blink my eyes and this motion detection will target your eyes....Try it. I'll have great respect for anyone who can solve this here...
Motion Detection::
// REM: Created by: Justin Linwood Ross | Creation Date: 4/29/25 11:30 AM | Last Modified: 4/29/25 11:30 AM | Github: rythorian77

using System;
using System.Drawing;
using System.Windows.Forms;
using System.Media; // For playing system sounds
using AForge.Video; // For video processing
using AForge.Video.DirectShow; // For accessing video capture devices
using AForge.Vision.Motion; // For MotionDetector

// Ensure you have added references to:
// AForge.dll
// AForge.Video.dll
// AForge.Video.DirectShow.dll
// AForge.Vision.dll

namespace MotionDetectorApp // Replace with your application's namespace
{
    // This class assumes you have a Windows Form named Form1 with the following controls:
    // PictureBox named pbCameraFeed
    // Label named lblStatus
    // ComboBox named cboCameras
    // Button named btnStart
    // Button named btnStop
    public partial class Form1 : Form
    {
        #region Variable Declarations

        // Declare variables for video devices and video source
        private FilterInfoCollection videoDevices; // Collection of video input devices
        private VideoCaptureDevice videoSource; // The video capture device selected by the user
        private const double MinimumMotionScore = 0.005; // This score is compared against motionDetector.MotionLevel to determine if motion is detected

        // Declare a timer for periodic beeping when motion is detected
        private Timer BeepTimer = new Timer(); // Timer object to trigger periodic beeping
        // Changed beep interval to 3000 ms (3 seconds)
        private const int BeepIntervalMs = 3000; // Interval in milliseconds between each beep

        // Added constant for the difference threshold in motion detection
        // Adjust this value (0-255) - lower is more sensitive
        private const int DifferenceThreshold = 10; // Threshold for detecting differences between frames

        private MotionDetector motionDetector; // Motion detector object to process video frames

        #endregion

        // Constructor for the form
        public Form1()
        {
            InitializeComponent(); // This call is required for the Windows Form Designer.

            // Set the interval for the beep timer
            BeepTimer.Interval = BeepIntervalMs;
            // Add event handler for the beep timer tick event
            BeepTimer.Tick += BeepTimer_Tick;

            // Wire up Form event handlers (Handles clause in VB.NET is done here in C#)
            this.Load += Form1_Load;
            this.FormClosing += MainForm_FormClosing;
            this.Shown += Form1_Shown;

            // Wire up PictureBox event handler
            pbCameraFeed.Resize += PbCameraFeed_Resize;

            // Wire up Button event handlers
            btnStart.Click += BtnStart_Click;
            btnStop.Click += BtnStop_Click;
        }

        #region Form Event Handlers

        // Event handler for form load
        private void Form1_Load(object sender, EventArgs e)
        {
            try
            {
                // Get the collection of video input devices
                videoDevices = new FilterInfoCollection(FilterCategory.VideoInputDevice);

                // Check if any video devices are found
                if (videoDevices.Count == 0)
                {
                    // Show an error message if no video devices are found
                    MessageBox.Show("No video input devices found.", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
                    // Disable the start button since no devices are available
                    btnStart.Enabled = false;
                    // Update the status label to indicate no devices are found
                    UpdateStatusLabel("No video input devices found.", Color.Red);
                }
                else
                {
                    // If video devices are found, add their names to the combo box
                    foreach (FilterInfo device in videoDevices)
                    {
                        cboCameras.Items.Add(device.Name);
                    }

                    // Set the default selected camera to the first one in the list
                    cboCameras.SelectedIndex = 0;
                    // Enable the start button
                    btnStart.Enabled = true;
                    // Disable the stop button (since the camera is not started yet)
                    btnStop.Enabled = false;
                    // Enable the camera combo box for camera selection
                    cboCameras.Enabled = true;
                    // Update the status label to prompt the user to select a camera and start it
                    UpdateStatusLabel("Select camera and press Start.", SystemColors.ControlText);
                }

            }
            catch (Exception ex)
            {
                // Handle any exceptions that occur during video device enumeration
                MessageBox.Show("Error enumerating video devices: " + ex.Message, "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
                // Disable the start and stop buttons, and the camera combo box
                btnStart.Enabled = false;
                btnStop.Enabled = false;
                cboCameras.Enabled = false;
                // Update the status label to indicate an error during initialization
                UpdateStatusLabel("Error initializing.", Color.Red);
            }
        }

        // Event handler for form closing
        private void MainForm_FormClosing(object sender, FormClosingEventArgs e)
        {
            // Ensure the camera is stopped when the form is closing
            StopCamera();
        }

        // Event handler for form shown event
        private void Form1_Shown(object sender, EventArgs e)
        {
            // Set the size mode of the picture box to zoom when the form is shown
            pbCameraFeed.SizeMode = PictureBoxSizeMode.Zoom;
        }

        // Event handler for the picture box resize event
        private void PbCameraFeed_Resize(object sender, EventArgs e)
        {
            // Set the size mode of the picture box to zoom to fit the new size
            pbCameraFeed.SizeMode = PictureBoxSizeMode.Zoom;
        }
        #endregion

        #region Button Click Handlers
        // Event handler for the start button click
        private void BtnStart_Click(object sender, EventArgs e)
        {
            // Check if a camera has been selected
            if (cboCameras.SelectedItem == null)
            {
                // Show a warning message if no camera is selected
                MessageBox.Show("Please select a camera.", "Selection Error", MessageBoxButtons.OK, MessageBoxIcon.Warning);
                // Update the status label to indicate no camera is selected
                UpdateStatusLabel("Please select a camera.", Color.OrangeRed);
                // Return without starting the camera
                return;
            }

            try
            {
                // Initialize the video source with the selected camera's moniker string
                videoSource = new VideoCaptureDevice(videoDevices[cboCameras.SelectedIndex].MonikerString);
                // Add event handler for the new frame event
                videoSource.NewFrame += VideoSource_NewFrame;
                // Start the video source (camera)
                videoSource.Start();

                // Initialize the motion detector with the specified DifferenceThreshold
                // TwoFramesDifferenceDetector is used to compare the current frame with the previous one
                // MotionBorderHighlighting is used to highlight the motion detected area in the frame
                motionDetector = new MotionDetector(new TwoFramesDifferenceDetector(DifferenceThreshold), new MotionBorderHighlighting());

                // Disable the start button after starting the camera
                btnStart.Enabled = false;
                // Enable the stop button to allow stopping the camera
                btnStop.Enabled = true;
                // Disable the camera combo box while the camera is running
                cboCameras.Enabled = false;
                // Update the status label to indicate that motion detection has started
                UpdateStatusLabel("Processing Motion...", SystemColors.ControlText);
                // Stop the beep timer if it was running
                BeepTimer.Stop();
            }
            catch (Exception ex)
            {
                // Handle any exceptions that occur during initialization
                MessageBox.Show("Error starting camera: " + ex.Message, "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
                // Disable the start button
                btnStart.Enabled = false;
                // Disable the stop button
                btnStop.Enabled = false;
                // Enable the camera combo box for user to make a selection
                cboCameras.Enabled = true;
                // Update the status label to indicate an error during initialization
                UpdateStatusLabel("Error initializing camera.", Color.Red);
            }
        }

        // Event handler for the stop button click
        private void BtnStop_Click(object sender, EventArgs e)
        {
            // Call the StopCamera subroutine to stop the video source and reset UI elements
            StopCamera();
        }
        #endregion

        #region Video Source Event Handlers
        // Event handler for new frames captured by the video source
        private void VideoSource_NewFrame(object sender, NewFrameEventArgs eventArgs)
        {
            // Clone the current frame to avoid issues with the frame being used by the video source
            Bitmap currentFrame = (Bitmap)eventArgs.Frame.Clone();
            // Variable to store the motion score of the current frame
            // The motion score is a property of the motion detector after processing
            double motionScore = 0.0; // Initialize motionScore

            try
            {
                // Check if motion detector is initialized
                if (motionDetector != null)
                {
                    // Process the current frame to detect motion and get the motion level
                    // The ProcessFrame method returns a boolean indicating if motion was detected,
                    // and the motion level is stored in the MotionLevel property.
                    motionDetector.ProcessFrame(currentFrame);
                    // Get the motion level after processing the frame
                    motionScore = motionDetector.MotionLevel;

                    // Check if the motion score meets or exceeds the minimum motion score threshold
                    if (motionScore >= MinimumMotionScore)
                    {
                        // Motion detected: Update status and manage beeping
                        UpdateStatusLabel($"MOTION DETECTED! ({motionScore:P4})", Color.Red);
                        // If the beep timer is not enabled, start it.
                        // The beep will now be played by the timer's Tick event.
                        if (!BeepTimer.Enabled)
                        {
                            BeepTimer.Start();
                        }
                    }
                    else
                    {
                        // No significant motion detected: Update status to "Processing Motion..."
                        UpdateStatusLabel("Processing Motion...", SystemColors.ControlText);
                        // Stop the beep timer if motion stops
                        BeepTimer.Stop();
                    }
                }
                else
                {
                    // Handle the case where the motion detector is not initialized
                    UpdateStatusLabel("Motion detector not initialized.", SystemColors.ControlText);
                    // Stop the beep timer if the detector isn't ready
                    BeepTimer.Stop();
                }

                // Update the picture box to display the current frame
                // Ensure the UI update is performed on the main thread using Invoke
                // Use Action for a more modern approach
                this.Invoke((Action)(() =>
                {
                    // Dispose the previous image in the picture box if it exists
                    if (pbCameraFeed.Image != null)
                    {
                        pbCameraFeed.Image.Dispose();
                    }
                    // Set the picture box's image to the current frame
                    pbCameraFeed.Image = currentFrame;
                }));

            }
            catch (Exception ex)
            {
                // Handle any exceptions that occur during frame processing
                this.Invoke((Action)(() =>
                {
                    // Write the error message to the debug output
                    System.Diagnostics.Debug.WriteLine("Error in camera feed processing: " + ex.Message);
                    // Show an error message box to the user
                    MessageBox.Show("Error in camera feed processing: " + ex.Message, "Processing Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
                    // Stop the camera and reset UI elements in case of an error
                    StopCamera();
                }));
                // Dispose the current frame if an exception occurred
                if (currentFrame != null) currentFrame.Dispose();
            }
        }
        #endregion

        #region Timer Event Handlers
        // Event handler for the beep timer tick event
        private void BeepTimer_Tick(object sender, EventArgs e)
        {
            // Play the system beep sound
            SystemSounds.Beep.Play();
            // The timer remains enabled and will tick again after BeepIntervalMs if motion persists
        }
        #endregion

        #region Helper Subroutines
        // Subroutine to stop the camera and reset UI elements
        private void StopCamera()
        {
            // Check if video source is not null and is currently running
            if (videoSource != null && videoSource.IsRunning)
            {
                try
                {
                    // Remove the event handler for the new frame event
                    videoSource.NewFrame -= VideoSource_NewFrame;
                    // Signal the video source to stop capturing frames
                    videoSource.SignalToStop();
                    // Wait for the video source to stop
                    videoSource.WaitForStop();
                    // Set the video source to null after stopping
                    videoSource = null;

                    // Dispose the image in the picture box if it exists and set it to null
                    if (pbCameraFeed.Image != null)
                    {
                        pbCameraFeed.Image.Dispose();
                        pbCameraFeed.Image = null;
                    }

                    // Set motion detector to null after stopping the camera
                    motionDetector = null;

                    // Stop the beep timer
                    BeepTimer.Stop();
                    // Enable the start button
                    btnStart.Enabled = true;
                    // Disable the stop button
                    btnStop.Enabled = false;
                    // Enable the camera combo box
                    cboCameras.Enabled = true;
                    // Update the status label to indicate that the camera has been stopped
                    UpdateStatusLabel("Camera stopped.", SystemColors.ControlText);
                }
                catch (Exception ex)
                {
                    // Handle any exceptions that occur during stopping the camera
                    MessageBox.Show("Error stopping camera: " + ex.Message, "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
                    // Enable the start button in case of an error
                    btnStart.Enabled = true;
                    // Disable the stop button
                    btnStop.Enabled = false;
                    // Enable the camera combo box for user to make a selection
                    cboCameras.Enabled = true;
                    // Update the status label to indicate an error during stopping
                    UpdateStatusLabel("Error stopping camera.", Color.Red);
                }
            }
        }

        // Subroutine to update the status label text and color
        private void UpdateStatusLabel(string text, Color color)
        {
            // Ensure the UI update is performed on the main thread using Invoke
            this.Invoke((Action)(() =>
            {
                // Set the text of the status label
                lblStatus.Text = text;
                // Set the color of the status label text
                lblStatus.ForeColor = color;
            }));
        }
        #endregion

        // This method is required by the Windows Form Designer.
        private void InitializeComponent()
        {
            // Auto-generated code for designer components will go here.
            // You would typically create your form layout using the Visual Studio designer,
            // which generates this method.
            // For this code to compile and run, you need to have a Form1 with the specified controls.

            // Example of how controls might be declared if not using the designer:
            // this.pbCameraFeed = new System.Windows.Forms.PictureBox();
            // this.lblStatus = new System.Windows.Forms.Label();
            // this.cboCameras = new System.Windows.Forms.ComboBox();
            // this.btnStart = new System.Windows.Forms.Button();
            // this.btnStop = new System.Windows.Forms.Button();
            // this.SuspendLayout();
            // ... designer code ...
            // this.ResumeLayout(false);
            // this.PerformLayout();
        }

        // Declare the UI controls as members of the Form class
        // These would typically be generated by the designer, but are listed here for clarity.
        private System.Windows.Forms.PictureBox pbCameraFeed;
        private System.Windows.Forms.Label lblStatus;
        private System.Windows.Forms.ComboBox cboCameras;
        private System.Windows.Forms.Button btnStart;
        private System.Windows.Forms.Button btnStop;
    }
}
 
Last edited:
I decided to re-write the application above using different approaches. The results seem to be good, and it detects moving objects with green boxes. A label displays if motion is detected or not. This is free for anyone to use or improve:
Motion Detection: Detect moving objects:
// Created by: Justin Linwood Ross | 5/9/2025
using AForge.Imaging;
using AForge.Imaging.Filters;
using AForge.Video;
using AForge.Video.DirectShow;
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Imaging; // Needed for ImageFormat
using System.IO; // Needed for Directory and Path
using System.Media; // Needed for SystemSounds
using System.Windows.Forms;
using System.Threading; // Needed for Thread.Sleep (used in a safe context)

namespace Capture_Pro // Or your project namespace
{
    public partial class Form1 : Form // Partial class means definition is split
    {
        // Camera related variables
        private FilterInfoCollection videoDevices;
        private VideoCaptureDevice videoSource;

        // Motion detection variables
        private BlobCounter blobCounter;
        private Bitmap previousFrame; // To store the previous frame for comparison (grayscale)

        // Filters needed for manual motion detection
        private readonly Grayscale grayscaleFilter = Grayscale.CommonAlgorithms.BT709;
        private Difference differenceFilter; // Will be initialized with the previous frame
        // private readonly Threshold thresholdFilter = new Threshold(15); // Original fixed threshold
        private Threshold thresholdFilter; // Make threshold adjustable


        // Blob size filtering thresholds
        private const int MinBlobWidth = 20;  // Minimum width of a detected motion blob
        private const int MinBlobHeight = 20; // Minimum height of a detected motion blob
        private const int MaxBlobWidth = 500; // Maximum width of a detected motion blob
        private const int MaxBlobHeight = 500; // Maximum height of a detected motion blob


        // Pen for drawing bounding boxes
        private Pen greenPen = new Pen(Color.Green, 2); // 2 pixels thick green line

        // --- New Variables for Enhancements ---
        private bool _isMotionActive = false; // State flag for current motion detection
        private DateTime _lastAlertTime = DateTime.MinValue; // Timestamp of the last alert sound/action
        private DateTime _lastSaveTime = DateTime.MinValue; // Timestamp of the last saved event

        // Settings (Assume these are tied to UI controls by the user)
        private readonly int _motionThreshold = 15; // Initial value for threshold filter
        private TimeSpan _alertCooldown = TimeSpan.FromSeconds(5); // Minimum time between alerts
        private TimeSpan _saveCooldown = TimeSpan.FromSeconds(10); // Minimum time between saving snapshots
        private readonly string _saveDirectory = Path.Combine(Application.StartupPath, "MotionEvents"); // Default save directory
        private readonly bool _saveEventsEnabled = true; // Flag to enable/disable saving
        private readonly bool _alertSoundEnabled = true; // Flag to enable/disable sound alerts
        private readonly bool _alertLabelEnabled = true; // Flag to enable/disable status label updates

        // UI Controls (Placeholder names - replace with your actual control names)
        // public Label statusLabel; // Assume you have a Label control named statusLabel on your form
        // public TextBox saveDirectoryTextBox; // Assume you have a TextBox named saveDirectoryTextBox
        // public CheckBox saveEventsCheckBox; // Assume you have a CheckBox named saveEventsCheckBox
        // public CheckBox alertSoundCheckBox; // Assume you have a CheckBox named alertSoundCheckBox
        // public CheckBox alertLabelCheckBox; // Assume you have a CheckBox named alertLabelCheckBox
        // public NumericUpDown thresholdNumericUpDown; // Assume you have a NumericUpDown named thresholdNumericUpDown
        // public NumericUpDown alertCooldownNumericUpDown; // Assume you have a NumericUpDown named alertCooldownNumericUpDown
        // public NumericUpDown saveCooldownNumericUpDown; // Assume you have a NumericUpDown named saveCooldownNumericUpDown


        public Form1()
        {
            InitializeComponent(); // This is called by the designer and sets up controls

            // Add FormClosing event handler manually (good practice)
            // Check if the event is already hooked in the designer to avoid double hooking
            this.FormClosing += new FormClosingEventHandler(Form1_FormClosing);

            // Initialize blob counter to find motion regions
            blobCounter = new BlobCounter();

            // Initialize threshold filter (can be updated later from UI)
            thresholdFilter = new Threshold(_motionThreshold);

            // Ensure buttons are in the correct initial state
            startButton.Enabled = false; // Will be enabled if cameras are found by LoadCameraDevices
            stopButton.Enabled = false;

            // --- Initialize UI Elements with Default Settings (Connect to your actual controls) ---
            // Example: statusLabel.Text = "Initializing...";
            // Example: saveDirectoryTextBox.Text = _saveDirectory;
            // Example: saveEventsCheckBox.Checked = _saveEventsEnabled;
            // Example: alertSoundCheckBox.Checked = _alertSoundEnabled;
            // Example: alertLabelCheckBox.Checked = _alertLabelEnabled;
            // Example: thresholdNumericUpDown.Value = _motionThreshold;
            // Example: alertCooldownNumericUpDown.Value = (decimal)_alertCooldown.TotalSeconds;
            // Example: saveCooldownNumericUpDown.Value = (decimal)_saveCooldown.TotalSeconds;

            LoadCameraDevices(); // Try loading devices right after initializing controls
        }

        // Load available video input devices
        private void LoadCameraDevices()
        {
            try
            {
                // Update status label via Invoke as this might be called from form load
                // If LoadCameraDevices is only called from the constructor (UI thread), Invoke is not strictly needed here
                // but it's good practice if this method could be called from elsewhere.
                if (statusLabel != null && statusLabel.InvokeRequired)
                {
                    statusLabel.Invoke((MethodInvoker)delegate { statusLabel.Text = "Loading cameras..."; });
                }
                else if (statusLabel != null)
                {
                    statusLabel.Text = "Loading cameras...";
                }


                // Enumerate video devices
                videoDevices = new FilterInfoCollection(FilterCategory.VideoInputDevice);

                if (videoDevices.Count == 0)
                {
                    MessageBox.Show("No video sources found.", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
                    startButton.Enabled = false; // startButton remains disabled
                    if (statusLabel != null && statusLabel.InvokeRequired)
                    {
                        statusLabel.Invoke((MethodInvoker)delegate { statusLabel.Text = "No cameras found."; });
                    }
                    else if (statusLabel != null)
                    {
                        statusLabel.Text = "No cameras found.";
                    }
                }
                else
                {
                    // Add devices to the combo box
                    cameraComboBox.Items.Clear(); // Clear previous items
                    foreach (FilterInfo device in videoDevices)
                    {
                        cameraComboBox.Items.Add(device.Name); // Make sure you have a ComboBox named cameraComboBox
                    }

                    cameraComboBox.SelectedIndex = 0; // Select the first device by default
                    startButton.Enabled = true; // Enable start button if cameras are found

                    if (statusLabel != null && statusLabel.InvokeRequired)
                    {
                        statusLabel.Invoke((MethodInvoker)delegate { statusLabel.Text = $"Found {videoDevices.Count} camera(s). Ready."; });
                    }
                    else if (statusLabel != null)
                    {
                        statusLabel.Text = $"Found {videoDevices.Count} camera(s). Ready.";
                    }
                }
            }
            catch (Exception ex) // Catching general Exception to be safe
            {
                MessageBox.Show("Failed to load video sources: " + ex.Message, "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
                startButton.Enabled = false; // Ensure disabled on error
                if (statusLabel != null && statusLabel.InvokeRequired)
                {
                    statusLabel.Invoke((MethodInvoker)delegate { statusLabel.Text = "Error loading cameras."; });
                }
                else if (statusLabel != null)
                {
                    statusLabel.Text = "Error loading cameras.";
                }
            }
        }

        private void StartButton_Click(object sender, EventArgs e) // Make sure you have a Button named startButton
        {
            try // Add try-catch for debugging button handler
            {
                if (videoDevices == null || videoDevices.Count == 0 || cameraComboBox.SelectedIndex < 0)
                {
                    MessageBox.Show("Please select a camera.", "Warning", MessageBoxButtons.OK, MessageBoxIcon.Warning);
                    if (statusLabel != null) statusLabel.Text = "Select a camera.";
                    return;
                }

                // --- Load settings from UI controls (Connect to your actual controls) ---
                // Example: _saveEventsEnabled = saveEventsCheckBox.Checked;
                // Example: _alertSoundEnabled = alertSoundCheckBox.Checked;
                // Example: _alertLabelEnabled = alertLabelCheckBox.Checked;
                // Example: _saveDirectory = saveDirectoryTextBox.Text;
                // Example: _motionThreshold = (int)thresholdNumericUpDown.Value;
                // Example: _alertCooldown = TimeSpan.FromSeconds((double)alertCooldownNumericUpDown.Value);
                // Example: _saveCooldown = TimeSpan.FromSeconds((double)saveCooldownNumericUpDown.Value);

                // Update threshold filter with potentially new value
                if (thresholdFilter == null || thresholdFilter.ThresholdValue != _motionThreshold)
                {
                    thresholdFilter = new Threshold(_motionThreshold);
                }

                // Get the selected device
                videoSource = new VideoCaptureDevice(videoDevices[cameraComboBox.SelectedIndex].MonikerString);

                // Set the NewFrame event handler
                // Ensure you don't double-subscribe if Start is clicked multiple times without stopping
                videoSource.NewFrame -= new NewFrameEventHandler(VideoSource_NewFrame); // Unsubscribe first (safe even if not subscribed)
                videoSource.NewFrame += new NewFrameEventHandler(VideoSource_NewFrame); // Subscribe

                // Start the video source
                videoSource.Start();

                // Reset previous frame and filters on start
                if (previousFrame != null)
                {
                    previousFrame.Dispose();
                    previousFrame = null;
                }
                differenceFilter = null; // Ensure difference filter is recreated on first frame

                // Reset alert/save timestamps
                _lastAlertTime = DateTime.MinValue;
                _lastSaveTime = DateTime.MinValue;
                _isMotionActive = false;

                // Disable start and enable stop
                startButton.Enabled = false;
                stopButton.Enabled = true; // Make sure you have a Button named stopButton
                cameraComboBox.Enabled = false; // Disable camera selection while running

                // Disable settings controls (assume they exist)
                // Example: saveEventsCheckBox.Enabled = false;
                // ... disable other setting controls ...

                if (_alertLabelEnabled && statusLabel != null) statusLabel.Text = "Camera started. Detecting motion...";


            }
            catch (Exception ex)
            {
                // Handle the exception, e.g., show a message box
                MessageBox.Show("Error starting camera: " + ex.Message, "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);

                // Ensure buttons are in a stable state after an error
                startButton.Enabled = true;
                stopButton.Enabled = false;
                cameraComboBox.Enabled = true;

                // Re-enable settings controls (assume they exist)
                // Example: saveEventsCheckBox.Enabled = true;
                // ... re-enable other setting controls ...


                // Attempt to stop the camera if it partially started
                if (videoSource != null && videoSource.IsRunning)
                {
                    videoSource.SignalToStop();
                    // No need to WaitForStop here, just signal - WaitForStop is handled in StopCamera
                }
                videoSource = null; // Ensure videoSource is null after attempt
                if (_alertLabelEnabled && statusLabel != null) statusLabel.Text = "Error starting camera.";
            }
        }

        // New frame received event handler
        private void VideoSource_NewFrame(object sender, NewFrameEventArgs eventArgs)
        {
            // Critical: This runs on a separate thread! Exceptions here are dangerous.
            // Use a try-catch block inside this method.
            Bitmap currentFrame = null; // Declare outside try for finally block
            Bitmap frameToProcess = null; // Clone for processing
           // Bitmap frameToSave = null; // Optional: another clone if saving asynchronously

            try
            {
                // Get the current frame and CLONE it immediately for thread safety and local use.
                currentFrame = (Bitmap)eventArgs.Frame.Clone();
                frameToProcess = (Bitmap)currentFrame.Clone(); // Clone for processing pipeline (drawing will happen on this)


                // Process the frame for motion detection and drawing
                // This method will return true if motion was detected
                bool motionDetected = ProcessFrameForMotion(frameToProcess); // This modifies frameToProcess


                // --- Handle Motion Events and UI Updates via Invoke ---
                // Pass the processed frame and motion state to the UI thread
                if (videoPictureBox.InvokeRequired)
                {
                    videoPictureBox.Invoke((MethodInvoker)delegate
                    {
                        // Dispose the previously displayed image if any to prevent memory leaks
                        videoPictureBox.Image?.Dispose();
                        videoPictureBox.Image = frameToProcess; // Assign the processed frame with drawings
                                                                // frameToProcess is now owned by PictureBox. Do NOT dispose it here.

                        // Handle alerts and saving on the UI thread
                        HandleMotionEvents(motionDetected, currentFrame); // Pass the original (unmodified) frame for saving


                    });
                }
                else // This block is unlikely to be hit by the NewFrame event
                {
                    // Running on UI thread already
                    videoPictureBox.Image?.Dispose();
                    videoPictureBox.Image = frameToProcess; // Assign the processed frame with drawings
                                                            // frameToProcess is now owned by PictureBox. Do NOT dispose it here.

                    // Handle alerts and saving directly
                    HandleMotionEvents(motionDetected, currentFrame); // Pass the original (unmodified) frame for saving
                }

                // Dispose the original clone created at the start of NewFrame if it wasn't used elsewhere (like saving)
                // If SaveMotionSnapshot makes its *own* clone, then currentFrame can be disposed here.
                // If SaveMotionSnapshot uses the passed Bitmap directly, then we must NOT dispose it here.
                // Let's design SaveMotionSnapshot to take the Bitmap but make its own clone for saving.
                // So, it's safe to dispose currentFrame here after passing it to the UI thread delegate.
                // However, HandleMotionEvents needs access to the *original* current frame for saving.
                // A cleaner approach is to pass the *original* clone (`currentFrame`) to the invoked delegate,
                // let the delegate decide what to do with it (assign to PictureBox, save), and the delegate/PictureBox handles disposal.

                // Let's revise:
                // 1. Clone the original frame (`currentFrame = (Bitmap)eventArgs.Frame.Clone();`)
                // 2. Create a frame specifically for processing and drawing (`frameForDisplay = (Bitmap)currentFrame.Clone();`)
                // 3. Call ProcessFrameForMotion on `frameForDisplay`. It returns motion state.
                // 4. Invoke UI thread, passing `frameForDisplay` and `currentFrame`.
                // 5. Inside Invoke:
                //    - Dispose PictureBox.Image, assign `frameForDisplay`.
                //    - If motion detected, call `HandleMotionEvents(motionDetected, currentFrame)`.
                // 6. HandleMotionEvents uses `currentFrame` for saving (clones it there), updates UI, plays sound.
                // 7. The `currentFrame` passed to the delegate needs disposal *if* it's not used for display or saving.
                //    Since it *is* used for saving potentialy, let the HandleMotionEvents or the saving method handle its disposal or cloning logic.

                // Let's keep the original logic simple: Process on the clone, pass the processed clone to UI thread,
                // and if saving is needed, the UI thread delegate can clone the *processed* frame for saving.
                // This is simpler, though saving the pre-drawing frame might be preferred visually.
                // Sticking to the original variable names: `currentFrame` is the one passed to `ProcessFrameForMotion` and then to the UI.

            }
            catch (Exception ex)
            {
                // Log the exception or show a non-blocking message/indicator
                System.Diagnostics.Debug.WriteLine($"Error in NewFrame: {ex.Message}");
                // Consider invoking a method on the UI thread to show an error state or stop the camera
                videoPictureBox.Invoke((MethodInvoker)delegate
                {
                    // Optionally show an error message or stop camera
                    // MessageBox.Show("Frame processing error: " + ex.Message); // Use with caution
                    StopCamera(); // Attempt to stop camera safely
                });

                // Dispose the currentFrame if an exception occurred *before* it was assigned to PictureBox.Image
                // If an exception happens within ProcessFrameForMotion or the delegate, the delegate's cleanup handles it.
                // This disposal is primarily for exceptions *before* the Invoke or if Invoke fails.
                if (currentFrame != null)
                {
                    currentFrame.Dispose();
                    currentFrame = null;
                }
            }
            // No 'finally' needed to dispose currentFrame explicitly here in the thread method
            // as its lifecycle is tied to PictureBox.Image or StopCamera cleanup.
        }

        // Process the frame for motion detection and highlighting
        // Returns true if motion was detected above thresholds
        private bool ProcessFrameForMotion(Bitmap frame) // 'frame' here is a cloned color frame passed from NewFrame
        {
            // This method receives a CLONED bitmap, modifies it (draws), and does NOT dispose it.
            // It will dispose previousFrame and clone grayCurrentFrame for the next iteration.

            bool motionDetected = false; // Assume no motion initially

            // Convert the current frame to grayscale
            using (Bitmap grayCurrentFrame = grayscaleFilter.Apply(frame))
            {
                if (previousFrame != null)
                {
                    // Initialize difference filter with the previous frame if not already
                    if (differenceFilter == null)
                    {
                        differenceFilter = new Difference(previousFrame);
                    }
                    else
                    {
                        // Update the difference filter's background frame (the previous frame)
                        // This needs to be done *before* applying the filter to grayCurrentFrame
                        differenceFilter.OverlayImage = previousFrame;
                    }

                    // Apply difference filter to get the motion map
                    using (Bitmap motionMap = differenceFilter.Apply(grayCurrentFrame))
                    {
                        // Apply threshold filter to get a binary image (black/white motion map)
                        // Ensure thresholdFilter is initialized
                        if (thresholdFilter == null)
                        {
                            thresholdFilter = new Threshold(_motionThreshold); // Initialize if somehow null
                        }
                        else if (thresholdFilter.ThresholdValue != _motionThreshold)
                        {
                            // Update threshold value if setting changed
                            thresholdFilter.ThresholdValue = _motionThreshold;
                        }


                        using (Bitmap binaryMotionMap = thresholdFilter.Apply(motionMap))
                        {
                            // Use BlobCounter on the binary motion map to find ALL potential regions
                            blobCounter.ProcessImage(binaryMotionMap);

                            // Get ALL found rectangles
                            Rectangle[] allBlobsRects = blobCounter.GetObjectsRectangles();

                            // Manually filter blobs by size
                            List<Rectangle> filteredBlobs = new List<Rectangle>();
                            foreach (Rectangle blobRect in allBlobsRects)
                            {
                                if (blobRect.Width >= MinBlobWidth && blobRect.Height >= MinBlobHeight &&
                                     blobRect.Width <= MaxBlobWidth && blobRect.Height <= MaxBlobHeight)
                                {
                                    filteredBlobs.Add(blobRect);
                                }
                            }

                            // If motion is detected (filtered blobs found), draw rectangles on the original color frame
                            if (filteredBlobs.Count > 0)
                            {
                                motionDetected = true; // Motion detected!

                                // Create a Graphics object to draw on the color frame
                                using (Graphics g = Graphics.FromImage(frame)) // Drawing on the input 'frame'
                                {
                                    foreach (Rectangle blobRect in filteredBlobs)
                                    {
                                        // Draw the green rectangle around the blob
                                        g.DrawRectangle(greenPen, blobRect);
                                    }
                                }
                            }
                        } // using binaryMotionMap ensures disposal
                    } // using motionMap ensures disposal

                    // Dispose the previous grayscale frame as it's no longer needed for the next iteration
                    previousFrame.Dispose();
                }

                // Store the current grayscale frame as the previous frame for the next iteration
                // We clone it because grayCurrentFrame will be disposed by the 'using' statement
                // This cloned bitmap becomes the new 'previousFrame' for the next call
                previousFrame = (Bitmap)grayCurrentFrame.Clone();
            } // 'using' statement ensures grayCurrentFrame is disposed here
              // The input 'frame' (color) is NOT disposed here; it's passed to the UI update logic.

            return motionDetected; // Return the motion state
        }

        // --- New Method to Handle UI Updates, Alerts, and Saving on the UI Thread ---
        private void HandleMotionEvents(bool motionDetected, Bitmap originalFrame)
        {
            // This method is called from the NewFrame delegate and runs on the UI thread.
            // It receives the motion state and the original color frame (before drawing).
            // The originalFrame needs to be disposed if it's not used for saving.
            // If saving is enabled, we clone it for saving. Otherwise, dispose it.

            if (motionDetected)
            {
                if (_alertLabelEnabled && statusLabel != null)
                {
                    statusLabel.Text = "MOTION DETECTED!";
                }

                // Check for alert cooldown before playing sound
                if (_alertSoundEnabled && DateTime.Now - _lastAlertTime > _alertCooldown)
                {
                    // Play a system sound
                    SystemSounds.Asterisk.Play();
                    _lastAlertTime = DateTime.Now; // Update last alert time
                }

                // Check for save cooldown before saving
                if (_saveEventsEnabled && DateTime.Now - _lastSaveTime > _saveCooldown)
                {
                    // Save the frame (make a clone for safety if saving is slow)
                    try
                    {
                        // Ensure the save directory exists
                        if (!Directory.Exists(_saveDirectory))
                        {
                            Directory.CreateDirectory(_saveDirectory);
                        }

                        // Generate a timestamped filename
                        string timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss_fff");
                        string filename = $"motion_{timestamp}.jpg";
                        string fullPath = Path.Combine(_saveDirectory, filename);

                        // Save the frame. Use originalFrame as it doesn't have drawings yet.
                        // If you prefer saving the frame *with* the green boxes, use videoPictureBox.Image clone instead.
                        // Saving originalFrame is often better for post-analysis.
                        using (Bitmap frameToSave = (Bitmap)originalFrame.Clone())
                        {
                            frameToSave.Save(fullPath, ImageFormat.Jpeg);
                        }


                        _lastSaveTime = DateTime.Now; // Update last save time

                        System.Diagnostics.Debug.WriteLine($"Saved motion event: {fullPath}");

                        // Optionally update status label about saving (briefly)
                        if (_alertLabelEnabled && statusLabel != null)
                        {
                            statusLabel.Text = $"Saved motion event: {filename}";
                            // You might want a timer here to revert the status label back to "MOTION DETECTED!" after a moment
                        }
                    }
                    catch (Exception ex)
                    {
                        System.Diagnostics.Debug.WriteLine($"Error saving motion event: {ex.Message}");
                        // Optionally update status label about the save error
                        if (_alertLabelEnabled && statusLabel != null)
                        {
                            statusLabel.Text = "Error saving motion event!";
                        }
                    }
                }

                // Update the motion active state
                _isMotionActive = true;
            }
            else // No motion detected
            {
                if (_isMotionActive) // Only update status if motion was previously active
                {
                    if (_alertLabelEnabled && statusLabel != null)
                    {
                        statusLabel.Text = "No motion detected.";
                    }
                    _isMotionActive = false; // Reset motion active state
                }
                // If no motion and wasn't previously active, status remains "No motion detected." or similar
            }

            // Dispose the originalFrame clone passed to this method ONLY IF it wasn't used for saving.
            // Since SaveMotionSnapshot makes its own clone inside the using block, we can dispose originalFrame here.
            originalFrame?.Dispose();
        }


        private void StopButton_Click(object sender, EventArgs e) // Make sure you have a Button named stopButton
        {
            try // Add try-catch for debugging button handler
            {
                StopCamera();
            }
            catch (Exception ex)
            {
                MessageBox.Show("Error stopping camera: " + ex.Message, "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
                // Ensure buttons are in a stable state
                startButton.Enabled = true;
                stopButton.Enabled = false;
                cameraComboBox.Enabled = true;
                // Re-enable settings controls (assume they exist)
                // Example: saveEventsCheckBox.Enabled = true;
                // ... re-enable other setting controls ...
            }
            Application.Exit(); // Removed Application.Exit here, StopCamera handles cleanup on closing
        }

        // Stop the camera and clean up resources
        private void StopCamera()
        {
            // This method can be called from button click or FormClosing

            // Check if camera is running before trying to stop
            if (videoSource != null && videoSource.IsRunning)
            {
                // Unsubscribe from the event *before* signaling stop
                // This helps prevent NewFrame events from firing during shutdown
                videoSource.NewFrame -= new NewFrameEventHandler(VideoSource_NewFrame);

                videoSource.SignalToStop(); // Signal the thread to stop
                                            // Allow a small delay for the thread to potentially finish processing the last frame
                                            // while (videoSource.IsRunning)
                                            // {
                                            //     Application.DoEvents(); // Process messages to allow UI updates
                                            //     Thread.Sleep(50); // Wait a bit
                                            // }
                                            // AForge's WaitForStop is usually sufficient and safer than a manual loop with DoEvents.
                videoSource.WaitForStop(); // Wait for the thread to finish


                // Dispose the video source object
                videoSource = null;

            }

            // Clean up image resources held by the form/controls, regardless of if camera was running
            if (videoPictureBox.Image != null)
            {
                videoPictureBox.Image.Dispose();
                videoPictureBox.Image = null; // Important to set to null after disposing
            }
            if (previousFrame != null)
            {
                previousFrame.Dispose();
                previousFrame = null; // Important to set to null after disposing
            }

            // Dispose the difference filter if it was created
            if (differenceFilter != null)
            {
                // Difference filter doesn't implement IDisposable, setting to null for GC
                // Note: AForge filters often re-use internal images. If the filter held onto
                // the OverlayImage (previousFrame), disposing previousFrame handles that.
                differenceFilter = null;
            }

            // Dispose the threshold filter if it was created
            if (thresholdFilter != null)
            {
                // Threshold filter doesn't implement IDisposable, setting to null for GC
                thresholdFilter = null;
            }


            // Dispose the blob counter if it was created
            if (blobCounter != null)
            {
                // BlobCounter doesn't implement IDisposable, setting to null for GC
                blobCounter = null;
            }

            // Dispose the pen - managed GDI+ resource. Created once, dispose once when done.
            if (greenPen != null)
            {
                greenPen.Dispose();
                greenPen = null; // Important to set to null after disposing
            }

            // Reset motion state flag
            _isMotionActive = false;
            // Clear status label on stop
            if (statusLabel != null && statusLabel.InvokeRequired)
            {
                statusLabel.Invoke((MethodInvoker)delegate { statusLabel.Text = "Camera stopped. Ready."; });
            }
            else if (statusLabel != null)
            {
                statusLabel.Text = "Camera stopped. Ready.";
            }


            // Ensure buttons are in the correct state
            startButton.Enabled = true;
            stopButton.Enabled = false;
            cameraComboBox.Enabled = true; // Re-enable camera selection

            // Re-enable settings controls (assume they exist)
            // Example: saveEventsCheckBox.Enabled = true;
            // ... re-enable other setting controls ...
        }

        // Form closing event handler to ensure camera is stopped and resources cleaned up
        // This event fires when the form is closed (e.g., by clicking the X button)
        private void Form1_FormClosing(object sender, FormClosingEventArgs e)
        {
            // Call StopCamera to clean up AForge resources and the pen
            StopCamera();

            // Note: The base Dispose method in Form1.Designer.cs will handle
            // disposing of auto-generated components (like the PictureBox, Buttons, ComboBox).
        }

        // Removed the manual Dispose(bool disposing) override as it conflicts with designer file.
        // The necessary cleanup is handled in StopCamera and FormClosing.
    }
}
 
Last edited:
This is the final update to this application. I applied #Regions verse using Classes, however using Classes is generally your best option in coding if the application is extensive. New features, this application will take screenshots of any motion detected and save to the current user's desktop in a folder. These images are captured every 3 seconds. This application is for anyone to use or upgrade. Have a good day. :)
Motion Detection:
// Created by: Justin Linwood Ross | 5/9/2025 |GitHub: https://github.com/Rythorian77?tab=repositories
// For Other Tutorials Go To: https://www.youtube.com/@justinlinwoodrossakarythor63/videos
//
// This application, 'Capture_Pro', is designed for real-time motion detection using a webcam.
// It captures video frames, applies image processing filters to detect motion, and can trigger alerts,
// save snapshots, and log events when motion is detected.
//
// The application is well-engineered to handle memory and resource management effectively.
// The consistent use of 'using' statements for disposable objects created within method scopes,
// coupled with explicit disposal of long-lived class members and proper shutdown procedures for external resources like the camera,
// significantly minimizes the risk of memory leaks.
// This provides a solid foundation for a stable application from a resource management perspective.

using AForge.Imaging;             // For image processing filters like Grayscale, Difference, Threshold, BlobCounter.
using AForge.Imaging.Filters;     // Specific namespace for AForge image filters.
using AForge.Video;               // Base namespace for AForge video capture.
using AForge.Video.DirectShow;    // For capturing video from DirectShow compatible devices (webcams).
using System;                     // Provides fundamental classes and base types.
using System.Collections.Generic; // For List<T>.
using System.Drawing;             // For graphics objects like Bitmap, Graphics, Pen, Rectangle, Color.
using System.Drawing.Imaging;     // For ImageFormat (used for saving images).
using System.IO;                  // For file and directory operations (Path, Directory, File).
using System.Media;               // For playing system sounds.
using System.Windows.Forms;       // For Windows Forms UI elements (Form, PictureBox, Button, ComboBox, Label, MessageBox).
using System.Diagnostics;         // Added for System.Diagnostics.EventLog and Debug.

namespace Capture_Pro // This is the application's primary namespace. Consider renaming if 'Application\'s Name' is different.
{
    /// <summary>
    /// The main form for the Capture_Pro motion detection application.
    /// Handles camera initialization, frame processing, motion detection logic,
    /// and UI updates, ensuring robust resource management.
    /// </summary>
    public partial class Form1 : Form
    {
        #region Private Members - Variables

        #region Camera Related
        /// <summary>
        /// Collection of available video input devices (webcams) on the system.
        /// </summary>
        private FilterInfoCollection videoDevices;
        /// <summary>
        /// Represents the video capture device (webcam) currently in use.
        /// </summary>
        private VideoCaptureDevice videoSource;
        #endregion

        #region Motion Detection Related
        /// <summary>
        /// Used to count and extract information about detected blobs (connected components)
        /// in the motion map, representing areas of motion.
        /// </summary>
        private BlobCounter blobCounter;
        /// <summary>
        /// Stores the grayscale version of the previously processed frame.
        /// Used by the Difference filter to detect changes between consecutive frames.
        /// This object is disposed and re-assigned with each new frame.
        /// </summary>
        private Bitmap previousFrame;
        #endregion

        #region Filters
        /// <summary>
        /// A pre-initialized grayscale filter using the BT709 algorithm.
        /// This is a static instance as the filter itself is stateless and reusable.
        /// </summary>
        private readonly Grayscale grayscaleFilter = Grayscale.CommonAlgorithms.BT709;
        /// <summary>
        /// Filter used to calculate the absolute difference between the current frame
        /// and the previous frame, highlighting areas of change (motion).
        /// This object is re-initialized or its OverlayImage updated as previousFrame changes.
        /// </summary>
        private Difference differenceFilter;
        /// <summary>
        /// Filter used to convert the difference map into a binary image,
        /// where pixels above a certain threshold are white (motion) and others are black (no motion).
        /// The threshold value is configurable.
        /// </summary>
        private Threshold thresholdFilter;
        #endregion

        #region Drawing Pens
        /// <summary>
        /// Pen used for drawing rectangles around detected motion blobs on the video feed.
        /// Disposed explicitly on application shutdown or camera stop.
        /// </summary>
        private Pen greenPen = new Pen(Color.Green, 2);
        #endregion

        #region Automatic Enhancements & Settings
        /// <summary>
        /// Flag indicating whether active motion is currently being detected.
        /// </summary>
        private bool _isMotionActive = false;
        /// <summary>
        /// Timestamp of the last time an alert sound was played.
        /// Used to implement a cooldown period for alerts.
        /// </summary>
        private DateTime _lastAlertTime = DateTime.MinValue;
        /// <summary>
        /// Timestamp of the last time a motion snapshot was saved.
        /// Used to implement a cooldown period for saving images.
        /// </summary>
        private DateTime _lastSaveTime = DateTime.MinValue;
        /// <summary>
        /// Timestamp when the current continuous motion detection event started.
        /// Used for logging motion event durations.
        /// </summary>
        private DateTime _currentMotionStartTime = DateTime.MinValue;
        /// <summary>
        /// Timestamp of the very last frame where motion was detected.
        /// Used to determine when motion has ceased after a period of no detection.
        /// </summary>
        private DateTime _lastMotionDetectionTime = DateTime.MinValue;

        // Fixed Settings (No UI for adjustment in this version, can be made configurable later)
        /// <summary>
        /// The threshold value for the Threshold filter. Pixels with intensity difference
        /// above this value are considered motion. Lower values increase sensitivity.
        /// </summary>
        private readonly int _motionThreshold = 15;

        // Fixed Blob size filtering thresholds
        /// <summary>
        /// Minimum width (in pixels) a detected blob must have to be considered significant motion.
        /// </summary>
        private readonly int _minBlobWidth = 20;
        /// <summary>
        /// Minimum height (in pixels) a detected blob must have to be considered significant motion.
        /// </summary>
        private readonly int _minBlobHeight = 20;
        /// <summary>
        /// Maximum width (in pixels) a detected blob can have. Larger blobs might be noise or light changes.
        /// </summary>
        private readonly int _maxBlobWidth = 500;
        /// <summary>
        /// Maximum height (in pixels) a detected blob can have. Larger blobs might be noise or light changes.
        /// </summary>
        private readonly int _maxBlobHeight = 500;

        /// <summary>
        /// Cooldown duration after an alert sound is played before another can be played.
        /// Prevents continuous beeping during prolonged motion.
        /// </summary>
        private readonly TimeSpan _alertCooldown = TimeSpan.FromSeconds(5);
        /// <summary>
        /// Cooldown duration after an image is saved before another can be saved.
        /// Prevents an excessive number of images during prolonged motion.
        /// </summary>
        private readonly TimeSpan _saveCooldown = TimeSpan.FromSeconds(3);

        /// <summary>
        /// The directory path where detected motion images will be saved.
        /// Defaults to a 'Detected Images' folder on the user's Desktop.
        /// </summary>
        private readonly string _saveDirectory = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), "Detected Images");
        /// <summary>
        /// The file path for the application's motion event log.
        /// Located in the application's startup directory.
        /// </summary>
        private readonly string _logFilePath = Path.Combine(Application.StartupPath, "motion_log.txt");

        /// <summary>
        /// Flag to enable or disable saving motion event snapshots.
        /// </summary>
        private readonly bool _saveEventsEnabled = true;
        /// <summary>
        /// Flag to enable or disable playing an alert sound on motion detection.
        /// </summary>
        private readonly bool _alertSoundEnabled = true;
        /// <summary>
        /// Flag to enable or disable updating the status label on the UI for motion events.
        /// </summary>
        private readonly bool _alertLabelEnabled = true;
        /// <summary>
        /// Flag to enable or disable logging motion events to a file and EventLog.
        /// </summary>
        private readonly bool _logEventsEnabled = true;

        // --- Added for robust logging ---
        /// <summary>
        /// Counts consecutive failures when attempting to write to the motion log file.
        /// </summary>
        private int _consecutiveLogErrors = 0;
        /// <summary>
        /// The maximum number of consecutive log file write failures before considering
        /// file logging critically failed and potentially falling back to EventLog exclusively.
        /// </summary>
        private const int _maxConsecutiveLogErrors = 5;
        /// <summary>
        /// Flag to indicate if file logging has encountered a critical failure (e.g., permissions issues)
        /// beyond which it should not attempt to write to the file again.
        /// </summary>
        private bool _isFileLoggingCriticallyFailed = false;
        /// <summary>
        /// Timestamp of the last time a critical logging error message box was shown.
        /// Used to rate-limit message box pop-ups to avoid spamming the user.
        /// </summary>
        private DateTime _lastLogErrorMessageBoxTime = DateTime.MinValue;
        /// <summary>
        /// Cooldown duration for displaying a critical logging error message box.
        /// </summary>
        private readonly TimeSpan _logErrorMessageBoxCooldown = TimeSpan.FromMinutes(5);
        // --- End Added for robust logging ---

        #endregion

        #region Region of Interest (ROI) Variables
        /// <summary>
        /// Represents the selected Region of Interest (ROI) as a rectangle.
        /// If null, the entire frame is processed for motion.
        /// Initialized to cover the entire videoPictureBox by default.
        /// </summary>
        private Rectangle? _roiSelection = null;
        /// <summary>
        /// Pen used for drawing the Region of Interest rectangle on the video feed.
        /// Disposed explicitly on application shutdown or camera stop.
        /// </summary>
        private Pen _roiPen = new Pen(Color.Red, 2);
        #endregion

        #endregion

        #region Constructor
        /// <summary>
        /// Initializes a new instance of the Form1 class.
        /// Sets up UI components, initializes motion detection objects,
        /// and loads available camera devices.
        /// </summary>
        public Form1()
        {
            InitializeComponent(); // Initializes components defined in the designer file (e.g., buttons, picture boxes).

            // Set the _roiSelection to match the initial size of the videoPictureBox.
            // This makes the entire video feed the default ROI for motion detection.
            _roiSelection = new Rectangle(0, 0, videoPictureBox.Width, videoPictureBox.Height);

            // Subscribe to the FormClosing event to ensure resources are properly released
            // when the application is closed by the user.
            FormClosing += new FormClosingEventHandler(Form1_FormClosing);

            // Initialize the BlobCounter, which is used to find and analyze motion blobs.
            blobCounter = new BlobCounter();
            // Initialize the Threshold filter with the default motion threshold.
            thresholdFilter = new Threshold(_motionThreshold);

            // Initially disable the Start and Stop buttons until cameras are loaded.
            startButton.Enabled = false;
            stopButton.Enabled = false;

            LoadCameraDevices(); // Attempt to discover and list available camera devices.
        }
        #endregion

        #region Camera Management
        /// <summary>
        /// Discovers and populates the camera combo box with available video input devices.
        /// Handles cases where no cameras are found or an error occurs during discovery.
        /// </summary>
        private void LoadCameraDevices()
        {
            try
            {
                // Update status label on the UI, ensuring it's thread-safe via Invoke.
                if (statusLabel != null && statusLabel.InvokeRequired)
                {
                    statusLabel.Invoke((MethodInvoker)delegate { statusLabel.Text = "Loading cameras..."; });
                }
                else if (statusLabel != null)
                {
                    statusLabel.Text = "Loading cameras...";
                }

                // Create a collection of all video input devices found on the system.
                videoDevices = new FilterInfoCollection(FilterCategory.VideoInputDevice);

                if (videoDevices.Count == 0)
                {
                    // If no cameras are found, inform the user and disable the start button.
                    MessageBox.Show("No video sources found.", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
                    startButton.Enabled = false;
                    // Update status label.
                    if (statusLabel != null && statusLabel.InvokeRequired)
                    {
                        statusLabel.Invoke((MethodInvoker)delegate { statusLabel.Text = "No cameras found."; });
                    }
                    else if (statusLabel != null)
                    {
                        statusLabel.Text = "No cameras found.";
                    }
                    LogMotionEvent("WARNING: No video sources found."); // Log this significant event.
                }
                else
                {
                    // If cameras are found, clear any existing items and add each device's name to the combo box.
                    cameraComboBox.Items.Clear();
                    foreach (FilterInfo device in videoDevices)
                    {
                        cameraComboBox.Items.Add(device.Name);
                    }

                    // Select the first camera by default.
                    cameraComboBox.SelectedIndex = 0;
                    // Enable the Start button as a camera is available.
                    startButton.Enabled = true;

                    // Update status label with the number of cameras found.
                    if (statusLabel != null && statusLabel.InvokeRequired)
                    {
                        statusLabel.Invoke((MethodInvoker)delegate { statusLabel.Text = $"Found {videoDevices.Count} camera(s). Ready."; });
                    }
                    else if (statusLabel != null)
                    {
                        statusLabel.Text = $"Found {videoDevices.Count} camera(s). Ready.";
                    }
                    LogMotionEvent($"INFO: Found {videoDevices.Count} camera(s). Ready."); // Log successful camera detection.
                }
            }
            catch (Exception ex)
            {
                // Handle any exceptions during camera loading, display an error message,
                // and disable the start button to prevent further issues.
                MessageBox.Show("Failed to load video sources: " + ex.Message, "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
                startButton.Enabled = false;
                // Update status label.
                if (statusLabel != null && statusLabel.InvokeRequired)
                {
                    statusLabel.Invoke((MethodInvoker)delegate { statusLabel.Text = "Error loading cameras."; });
                }
                else if (statusLabel != null)
                {
                    statusLabel.Text = "Error loading cameras.";
                }
                LogMotionEvent($"ERROR: Failed to load video sources: {ex.Message}"); // Log the error for debugging.
            }
        }

        /// <summary>
        /// Stops the currently running video source and disposes of all associated resources
        /// to prevent memory leaks and ensure a clean shutdown. This method is designed
        /// to be robust against individual disposal failures.
        /// </summary>
        private void StopCamera()
        {
            // Attempt to stop the video source first.
            try
            {
                if (videoSource != null && videoSource.IsRunning)
                {
                    // Unsubscribe from the NewFrame event to prevent further processing
                    // after the camera has stopped.
                    videoSource.NewFrame -= new NewFrameEventHandler(VideoSource_NewFrame);
                    // Signal the video source to stop capturing frames.
                    videoSource.SignalToStop();
                    // Wait for the video source to completely stop. This is crucial for proper shutdown.
                    videoSource.WaitForStop();
                    // Nullify the videoSource reference to allow garbage collection.
                    videoSource = null;
                    LogMotionEvent("INFO: Video source signaled to stop and waited for stop.");
                }
            }
            catch (Exception ex)
            {
                LogMotionEvent($"ERROR: Exception while stopping video source: {ex.Message}");
                // Attempt to continue with resource disposal even if stopping the video source
                // failed partially, as other resources might still need to be released.
            }

            // Centralized disposal logic with individual try-catch blocks for robustness.
            // This ensures that if one resource fails to dispose, others can still be released.
            try
            {
                if (videoPictureBox.Image != null)
                {
                    // Dispose the image currently displayed in the PictureBox to release its memory.
                    videoPictureBox.Image.Dispose();
                    videoPictureBox.Image = null; // Clear the image reference.
                }
            }
            catch (Exception ex)
            {
                LogMotionEvent($"ERROR: Failed to dispose videoPictureBox.Image: {ex.Message}");
            }

            try
            {
                if (previousFrame != null)
                {
                    // Dispose the previous frame bitmap used for motion detection.
                    previousFrame.Dispose();
                    previousFrame = null; // Clear the reference.
                }
            }
            catch (Exception ex)
            {
                LogMotionEvent($"ERROR: Failed to dispose previousFrame: {ex.Message}");
            }

            // AForge filter objects like Difference and Threshold typically don't hold
            // unmanaged resources that require explicit Dispose(). They are usually
            // re-initialized or their properties updated. Nullifying them here
            // helps with garbage collection and ensures a clean state reset for restart.
            try
            {
                if (differenceFilter != null)
                {
                    differenceFilter = null;
                }
            }
            catch (Exception ex) // Catching just in case, though unlikely for nulling references
            {
                LogMotionEvent($"ERROR: Failed to reset differenceFilter: {ex.Message}");
            }

            try
            {
                if (thresholdFilter != null)
                {
                    thresholdFilter = null;
                }
            }
            catch (Exception ex)
            {
                LogMotionEvent($"ERROR: Failed to reset thresholdFilter: {ex.Message}");
            }

            try
            {
                if (blobCounter != null)
                {
                    // BlobCounter also typically doesn't require explicit dispose,
                    // but nulling helps for state reset.
                    blobCounter = null;
                }
            }
            catch (Exception ex)
            {
                LogMotionEvent($"ERROR: Failed to reset blobCounter: {ex.Message}");
            }

            // Dispose of GDI+ objects (Pens). These *do* hold unmanaged resources and must be disposed.
            try
            {
                if (greenPen != null)
                {
                    greenPen.Dispose();
                    greenPen = null; // Clear the reference.
                }
                // Re-initialize the pen for subsequent starts if needed, or ensure it's created in the constructor.
                // For consistency, if it's a class member, it's better to recreate it than to leave it null.
                greenPen = new Pen(Color.Green, 2); // Re-create for next use
            }
            catch (Exception ex)
            {
                LogMotionEvent($"ERROR: Failed to dispose or re-initialize greenPen: {ex.Message}");
            }

            try
            {
                if (_roiPen != null)
                {
                    _roiPen.Dispose();
                    _roiPen = null; // Clear the reference.
                }
                _roiPen = new Pen(Color.Red, 2); // Re-create for next use
            }
            catch (Exception ex)
            {
                LogMotionEvent($"ERROR: Failed to dispose or re-initialize _roiPen: {ex.Message}");
            }

            // Reset motion detection state flags.
            _isMotionActive = false;

            // Update UI status label.
            if (statusLabel != null && statusLabel.InvokeRequired)
            {
                statusLabel.Invoke((MethodInvoker)delegate { statusLabel.Text = "Camera stopped. Ready."; });
            }
            else if (statusLabel != null)
            {
                statusLabel.Text = "Camera stopped. Ready.";
            }

            // Update button and combo box states for restarting.
            startButton.Enabled = true;
            stopButton.Enabled = false;
            cameraComboBox.Enabled = true;

            LogMotionEvent("INFO: Camera stopped. Resources disposed and reset.");
        }
        #endregion

        #region Event Handlers
        /// <summary>
        /// Handles the click event for the 'Start' button.
        /// Initializes and starts the selected video capture device.
        /// Resets motion detection state variables.
        /// </summary>
        /// <param name="sender">The object that raised the event.</param>
        /// <param name="e">The event data.</param>
        private void StartButton_Click(object sender, EventArgs e)
        {
            try
            {
                // Validate that a camera is selected and available.
                if (videoDevices == null || videoDevices.Count == 0 || cameraComboBox.SelectedIndex < 0)
                {
                    MessageBox.Show("Please select a camera.", "Warning", MessageBoxButtons.OK, MessageBoxIcon.Warning);
                    if (statusLabel != null) statusLabel.Text = "Select a camera.";
                    LogMotionEvent("WARNING: Start button clicked with no camera selected or found.");
                    return; // Exit the method if no camera is selected.
                }

                // Re-initialize threshold filter if it's null or its value has changed
                // (though _motionThreshold is a readonly field, this check is good practice if it were configurable).
                if (thresholdFilter == null || thresholdFilter.ThresholdValue != _motionThreshold)
                {
                    thresholdFilter = new Threshold(_motionThreshold);
                }

                // Create a new VideoCaptureDevice instance using the moniker string of the selected device.
                videoSource = new VideoCaptureDevice(videoDevices[cameraComboBox.SelectedIndex].MonikerString);
                // Unsubscribe defensively to prevent multiple subscriptions if Start is clicked repeatedly
                // without a full application restart or proper StopCamera call.
                videoSource.NewFrame -= new NewFrameEventHandler(VideoSource_NewFrame);
                // Subscribe to the NewFrame event, which is triggered whenever a new video frame is available.
                videoSource.NewFrame += new NewFrameEventHandler(VideoSource_NewFrame);
                // Begin video capture.
                videoSource.Start();

                // Dispose of any previous frame stored from a prior run and clear the reference.
                if (previousFrame != null)
                {
                    previousFrame.Dispose();
                    previousFrame = null;
                }
                // Reset the difference filter as the previous frame has been reset.
                differenceFilter = null;

                // Reset all motion detection state variables to their initial values.
                _lastAlertTime = DateTime.MinValue;
                _lastSaveTime = DateTime.MinValue;
                _isMotionActive = false;
                _currentMotionStartTime = DateTime.MinValue;
                _lastMotionDetectionTime = DateTime.MinValue;

                // Update UI element states.
                startButton.Enabled = false;
                stopButton.Enabled = true;
                cameraComboBox.Enabled = false;

                // Update status label.
                if (_alertLabelEnabled && statusLabel != null) statusLabel.Text = "Camera started. Detecting motion...";
                LogMotionEvent("INFO: Application started. Camera activated.");
            }
            catch (Exception ex)
            {
                // Handle any errors during camera startup.
                MessageBox.Show("Error starting camera: " + ex.Message, "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
                // Re-enable start button and disable stop button on error.
                startButton.Enabled = true;
                stopButton.Enabled = false;
                cameraComboBox.Enabled = true;
                // Attempt to signal the video source to stop if it somehow started partially.
                if (videoSource != null && videoSource.IsRunning)
                {
                    videoSource.SignalToStop();
                }
                videoSource = null; // Ensure videoSource reference is cleared.
                if (_alertLabelEnabled && statusLabel != null) statusLabel.Text = "Error starting camera.";
                LogMotionEvent($"ERROR: Failed to start camera: {ex.Message}"); // Log the error.
            }
        }

        /// <summary>
        /// Event handler for when a new frame is received from the video source.
        /// This method is crucial for real-time processing and motion detection.
        /// It clones the frame, processes it for motion, updates the UI, and handles
        /// motion-triggered events (alerts, saving snapshots).
        /// </summary>
        /// <param name="sender">The video source that sent the frame.</param>
        /// <param name="eventArgs">Arguments containing the new frame.</param>
        private void VideoSource_NewFrame(object sender, NewFrameEventArgs eventArgs)
        {
            // 'currentFrame' will hold a clone of the original frame for display.
            Bitmap currentFrame = null;
            // 'frameToProcess' will hold a clone specifically for motion analysis,
            // allowing drawing on 'currentFrame' for display without affecting analysis.
            Bitmap frameToProcess = null;

            try
            {
                // Clone the incoming frame to work with it.
                // The original eventArgs.Frame is managed by AForge and should not be disposed here.
                currentFrame = (Bitmap)eventArgs.Frame.Clone();
                // Clone again for the motion processing pipeline.
                frameToProcess = (Bitmap)currentFrame.Clone();

                // Process the 'frameToProcess' for motion detection.
                // This method will draw rectangles on 'frameToProcess' if motion is found.
                bool motionDetected = ProcessFrameForMotion(frameToProcess);

                // Update the UI (videoPictureBox and statusLabel) on the UI thread.
                // This is critical because NewFrame events occur on a separate thread.
                if (videoPictureBox.InvokeRequired)
                {
                    videoPictureBox.Invoke((MethodInvoker)delegate
                    {
                        try
                        {
                            // Dispose of the previously displayed image in the PictureBox
                            // to prevent GDI+ memory leaks.
                            videoPictureBox.Image?.Dispose();
                            // Assign the processed frame (which might have motion rectangles) to the PictureBox.
                            videoPictureBox.Image = frameToProcess;
                            // Handle motion-related events (alerts, saving) using the original frame for saving.
                            HandleMotionEvents(motionDetected, currentFrame); // Pass the original clone for saving.
                        }
                        catch (Exception invokeEx)
                        {
                            // Log errors occurring within the Invoke delegate.
                            System.Diagnostics.Debug.WriteLine($"Error in VideoPictureBox Invoke delegate: {invokeEx.Message}");
                            LogMotionEvent($"ERROR: Exception in VideoPictureBox Invoke delegate: {invokeEx.Message}");
                            // Ensure disposal of bitmaps if an error occurs during UI update.
                            currentFrame?.Dispose();
                            frameToProcess?.Dispose();
                            // Attempt to stop the camera on a critical error that prevents UI updates.
                            StopCamera();
                        }
                    });
                }
                else // If not invoked (should generally not happen for NewFrame, but as fallback).
                {
                    videoPictureBox.Image?.Dispose();
                    videoPictureBox.Image = frameToProcess;
                    HandleMotionEvents(motionDetected, currentFrame);
                }
            }
            catch (Exception ex)
            {
                // Log errors occurring in the main NewFrame handler logic.
                System.Diagnostics.Debug.WriteLine($"Error in NewFrame (main thread logic): {ex.Message}");
                LogMotionEvent($"ERROR: Exception in NewFrame handler: {ex.Message}");
                // Ensure all created bitmaps are disposed in case of an error.
                currentFrame?.Dispose();
                currentFrame = null;
                frameToProcess?.Dispose();
                frameToProcess = null;
                // Attempt to stop the camera on critical error that prevents further frame processing.
                if (videoPictureBox.InvokeRequired)
                {
                    videoPictureBox.Invoke((MethodInvoker)delegate
                    {
                        StopCamera();
                    });
                }
                else
                {
                    StopCamera();
                }
            }
        }

        /// <summary>
        /// Handles the click event for the 'Stop' button.
        /// Initiates the camera stopping procedure and logs the event.
        /// </summary>
        /// <param name="sender">The object that raised the event.</param>
        /// <param name="e">The event data.</param>
        private void StopButton_Click(object sender, EventArgs e)
        {
            try
            {
                // Call the robust StopCamera method to halt video and release resources.
                StopCamera();
                LogMotionEvent("INFO: Application stopped. Camera deactivated.");
                // Close the form, which will also trigger the Form1_FormClosing event.
                this.Close();
            }
            catch (Exception ex)
            {
                // Handle any errors during the stopping process, inform the user,
                // and re-enable appropriate UI elements.
                MessageBox.Show("Error stopping camera: " + ex.Message, "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
                LogMotionEvent($"ERROR: Failed to stop camera: {ex.Message}");
                startButton.Enabled = true;
                stopButton.Enabled = false;
                cameraComboBox.Enabled = true;
                // If stopping failed critically, force application exit to prevent hanging resources.
                Application.Exit();
            }
        }

        /// <summary>
        /// Event handler for the form closing event.
        /// Ensures that camera resources are properly released before the application exits.
        /// </summary>
        /// <param name="sender">The object that raised the event.</param>
        /// <param name="e">The event data, indicating how the form is closing.</param>
        private void Form1_FormClosing(object sender, FormClosingEventArgs e)
        {
            // Call StopCamera to ensure all resources are released when the form closes.
            // StopCamera already has robust internal error handling for disposal.
            // Any critical failures in StopCamera will be logged internally.
            StopCamera();
            LogMotionEvent("INFO: Application is closing.");
        }
        #endregion

        #region Motion Processing Logic
        /// <summary>
        /// Processes a single video frame to detect motion.
        /// Applies grayscale, difference, and threshold filters, then uses BlobCounter
        /// to identify motion regions. Draws green rectangles around detected blobs.
        /// Handles Region of Interest (ROI) if defined.
        /// </summary>
        /// <param name="frame">The current frame (Bitmap) to be processed.
        /// This bitmap will be modified to draw motion rectangles.</param>
        /// <returns>True if motion is detected, false otherwise.</returns>
        private bool ProcessFrameForMotion(Bitmap frame)
        {
            bool motionDetected = false;

            try
            {
                // Apply grayscale filter to the current frame.
                // 'using' ensures the grayscale image is disposed after its scope.
                using (Bitmap grayCurrentFrame = grayscaleFilter.Apply(frame))
                {
                    // Proceed with motion detection only if a previous frame exists for comparison.
                    if (previousFrame != null)
                    {
                        // Initialize Difference filter if it's null, or update its OverlayImage.
                        // The OverlayImage is the 'background' or 'previous' frame for comparison.
                        if (differenceFilter == null)
                        {
                            differenceFilter = new Difference(previousFrame);
                        }
                        else
                        {
                            differenceFilter.OverlayImage = previousFrame;
                        }

                        // Apply the difference filter to get a 'motion map' highlighting changes.
                        using (Bitmap motionMap = differenceFilter.Apply(grayCurrentFrame))
                        {
                            // Initialize Threshold filter if null or update its value.
                            if (thresholdFilter == null)
                            {
                                thresholdFilter = new Threshold(_motionThreshold);
                            }
                            else if (thresholdFilter.ThresholdValue != _motionThreshold)
                            {
                                thresholdFilter.ThresholdValue = _motionThreshold; // Update threshold if it changed
                            }

                            // Apply the threshold filter to convert the motion map into a binary image.
                            // 'using' ensures the binaryMotionMap is disposed.
                            using (Bitmap binaryMotionMap = thresholdFilter.Apply(motionMap))
                            {
                                Bitmap imageToProcessForBlobs = binaryMotionMap; // Default to full binary motion map.
                                Rectangle currentRoi = Rectangle.Empty; // Initialize ROI for blob translation.

                                // If an ROI is defined, crop the binary motion map to the ROI.
                                if (_roiSelection.HasValue)
                                {
                                    currentRoi = _roiSelection.Value;

                                    // Ensure ROI is within image bounds before attempting to crop.
                                    if (currentRoi.X >= 0 && currentRoi.Y >= 0 &&
                                        currentRoi.Right <= binaryMotionMap.Width &&
                                        currentRoi.Bottom <= binaryMotionMap.Height)
                                    {
                                        // Create a Crop filter using the defined ROI.
                                        // AForge.NET's Crop filter is a struct, so it doesn't need Dispose().
                                        Crop cropFilter = new Crop(currentRoi);
                                        // Apply the crop filter and ensure the resulting bitmap is disposed after use.
                                        imageToProcessForBlobs = cropFilter.Apply(binaryMotionMap);
                                    }
                                    else
                                    {
                                        // Log a warning if ROI is out of bounds and fallback to processing the full frame.
                                        System.Diagnostics.Debug.WriteLine("Warning: ROI is out of image bounds. Processing full frame.");
                                        LogMotionEvent("WARNING: ROI is out of image bounds. Processing full frame.");
                                        imageToProcessForBlobs = binaryMotionMap; // Fallback to full frame.
                                        currentRoi = Rectangle.Empty; // Clear ROI to indicate full frame processing (no offset needed for blobs).
                                    }
                                }

                                // Process the (possibly cropped) binary motion map with the BlobCounter.
                                blobCounter.ProcessImage(imageToProcessForBlobs);

                                // Get the rectangles of all detected blobs.
                                Rectangle[] allBlobsRects = blobCounter.GetObjectsRectangles();

                                List<Rectangle> filteredBlobs = new List<Rectangle>();
                                // Iterate through detected blobs to filter by size (min/max width/height).
                                foreach (Rectangle blobRect in allBlobsRects)
                                {
                                    Rectangle translatedBlobRect = blobRect;
                                    // If an ROI was applied, translate the blob coordinates back to the original frame's coordinate system.
                                    if (currentRoi != Rectangle.Empty)
                                    {
                                        translatedBlobRect.X += currentRoi.X;
                                        translatedBlobRect.Y += currentRoi.Y;
                                    }

                                    // Apply size filtering to the blob.
                                    if (translatedBlobRect.Width >= _minBlobWidth && translatedBlobRect.Height >= _minBlobHeight &&
                                        translatedBlobRect.Width <= _maxBlobWidth && translatedBlobRect.Height <= _maxBlobHeight)
                                    {
                                        filteredBlobs.Add(translatedBlobRect);
                                    }
                                }

                                // Dispose cropped image if it was created (i.e., not equal to binaryMotionMap).
                                // This is important if a new bitmap was created by the Crop filter.
                                if (imageToProcessForBlobs != binaryMotionMap && imageToProcessForBlobs != null)
                                {
                                    imageToProcessForBlobs.Dispose();
                                    imageToProcessForBlobs = null;
                                }

                                // If any blobs passed the size filter, motion is detected.
                                if (filteredBlobs.Count > 0)
                                {
                                    motionDetected = true;

                                    // Draw green rectangles around the detected motion blobs on the original color frame.
                                    // 'using' ensures the Graphics object is disposed.
                                    using (Graphics g = Graphics.FromImage(frame))
                                    {
                                        foreach (Rectangle blobRect in filteredBlobs)
                                        {
                                            g.DrawRectangle(greenPen, blobRect);
                                        }
                                    }
                                }
                            } // binaryMotionMap is disposed here
                        } // motionMap is disposed here
                        previousFrame.Dispose(); // Dispose the old previous frame as it's about to be replaced.
                    }
                    // Set the current grayscale frame as the new previous frame for the next iteration.
                    // Clone it to prevent issues if grayCurrentFrame is disposed.
                    previousFrame = (Bitmap)grayCurrentFrame.Clone();
                } // grayCurrentFrame is disposed here

                // Draw ROI on the final frame (the original color frame passed in) if a region is defined.
                // This overlay helps the user visualize the active detection area.
                if (_roiSelection.HasValue)
                {
                    using (Graphics g = Graphics.FromImage(frame))
                    {
                        g.DrawRectangle(_roiPen, _roiSelection.Value);
                    }
                }
            }
            catch (Exception ex)
            {
                LogMotionEvent($"ERROR: Exception during motion processing (ProcessFrameForMotion): {ex.Message}");
                // Re-throw the exception so the NewFrame handler can catch it and potentially stop the camera,
                // preventing continuous errors.
                throw;
            }

            return motionDetected;
        }
        #endregion

        #region Motion Event Handling
        /// <summary>
        /// Handles actions to be taken when motion is detected or ceases.
        /// Triggers alerts, saves snapshots, and updates status based on cooldowns and settings.
        /// </summary>
        /// <param name="motionDetected">True if motion was detected in the current frame, false otherwise.</param>
        /// <param name="originalFrame">The original, unprocessed color frame, used for saving snapshots.</param>
        private void HandleMotionEvents(bool motionDetected, Bitmap originalFrame)
        {
            try
            {
                if (motionDetected)
                {
                    // If motion was not previously active, mark the start of a new motion event.
                    if (!_isMotionActive)
                    {
                        _currentMotionStartTime = DateTime.Now;
                        LogMotionEvent($"MOTION STARTED at {_currentMotionStartTime:yyyy-MM-dd HH:mm:ss.fff}");
                        if (_alertLabelEnabled && statusLabel != null)
                        {
                            statusLabel.Text = "MOTION DETECTED!";
                        }
                    }
                    _isMotionActive = true; // Set motion active flag.
                    _lastMotionDetectionTime = DateTime.Now; // Update last detection time.

                    // Play alert sound if enabled and the alert cooldown has passed.
                    if (_alertSoundEnabled && DateTime.Now - _lastAlertTime > _alertCooldown)
                    {
                        SystemSounds.Asterisk.Play();
                        _lastAlertTime = DateTime.Now;
                        LogMotionEvent("INFO: Alert sound played.");
                    }

                    // Save motion snapshot if enabled and the save cooldown has passed.
                    if (_saveEventsEnabled && DateTime.Now - _lastSaveTime > _saveCooldown)
                    {
                        try
                        {
                            // Create the save directory if it doesn't exist.
                            if (!Directory.Exists(_saveDirectory))
                            {
                                Directory.CreateDirectory(_saveDirectory);
                                LogMotionEvent($"INFO: Created save directory: {_saveDirectory}");
                            }

                            // Generate a unique filename with timestamp.
                            string timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss_fff");
                            string filename = $"motion_{timestamp}.jpg";
                            string fullPath = Path.Combine(_saveDirectory, filename);

                            // Clone the original frame before saving to avoid GDI+ issues if the
                            // original frame is simultaneously being used elsewhere (e.g., PictureBox).
                            using (Bitmap frameToSave = (Bitmap)originalFrame.Clone())
                            {
                                frameToSave.Save(fullPath, ImageFormat.Jpeg);
                            }

                            _lastSaveTime = DateTime.Now; // Update last save time.

                            LogMotionEvent($"SAVED snapshot: {filename}");

                            if (_alertLabelEnabled && statusLabel != null)
                            {
                                statusLabel.Text = $"Saved motion event: {filename}";
                            }
                        }
                        catch (Exception ex)
                        {
                            System.Diagnostics.Debug.WriteLine($"Error saving motion event: {ex.Message}");
                            LogMotionEvent($"ERROR: Failed to save snapshot: {ex.Message}");
                            if (_alertLabelEnabled && statusLabel != null)
                            {
                                statusLabel.Text = "Error saving motion event!";
                            }
                        }
                    }
                }
                else // No motion detected in this frame
                {
                    // If motion was previously active but hasn't been detected for a short grace period,
                    // consider motion as stopped. This prevents rapid toggling of "motion detected" status.
                    if (_isMotionActive && DateTime.Now - _lastMotionDetectionTime > TimeSpan.FromMilliseconds(500)) // A small grace period (e.g., 0.5 seconds)
                    {
                        _isMotionActive = false; // Mark motion as inactive.
                        LogMotionEvent($"MOTION STOPPED at {DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}. Duration: {(DateTime.Now - _currentMotionStartTime).TotalSeconds:F1}s");
                        if (_alertLabelEnabled && statusLabel != null)
                        {
                            statusLabel.Text = "No motion detected.";
                        }
                    }
                    else if (!_isMotionActive) // If motion was never active, just keep displaying no motion.
                    {
                        if (_alertLabelEnabled && statusLabel != null)
                        {
                            statusLabel.Text = "No motion detected.";
                        }
                    }
                }
            }
            catch (Exception ex)
            {
                LogMotionEvent($"ERROR: Exception in HandleMotionEvents: {ex.Message}");
            }
            finally
            {
                // Ensure the original frame bitmap passed into this handler is disposed
                // to release its memory, as it was cloned specifically for this processing cycle.
                originalFrame?.Dispose();
            }
        }
        #endregion

        #region Utility Methods
        /// <summary>
        /// Logs motion-related events to a file and, in case of critical file logging failures,
        /// falls back to the Windows Event Log. Implements error handling and rate-limiting
        /// for error message boxes to prevent spamming the user.
        /// </summary>
        /// <param name="message">The log message.</param>
        private void LogMotionEvent(string message)
        {
            // If logging is globally disabled or critically failed for file logging, do not attempt file logging.
            if (!_logEventsEnabled || _isFileLoggingCriticallyFailed)
            {
                // If file logging failed critically, but logging is still enabled,
                // we still want to log to the Event Log as a fallback.
                // This path handles cases where _isFileLoggingCriticallyFailed is true
                // and we are still attempting to log.
                if (_logEventsEnabled && _isFileLoggingCriticallyFailed)
                {
                    try
                    {
                        string eventSource = "CaptureProMotionDetection";
                        string eventLogName = "Application"; // Default Application log

                        // Check if the event source exists. Creating it requires administrative privileges
                        // the first time on a machine. For production, consider creating this during installation.
                        if (!EventLog.SourceExists(eventSource))
                        {
                            EventLog.CreateEventSource(eventSource, eventLogName);
                        }
                        // Log the message to the Windows Event Log as a Warning, indicating file logging issues.
                        EventLog.WriteEntry(eventSource,
                            $"FILE LOGGING CRITICALLY FAILED: {message}",
                            EventLogEntryType.Warning);
                    }
                    catch (Exception eventLogEx)
                    {
                        System.Diagnostics.Debug.WriteLine($"CRITICAL ERROR: Could not write to Event Log either: {eventLogEx.Message}");
                        // At this point, all logging attempts have failed. No further action, as it's an emergency.
                    }
                }
                return; // Exit if logging is disabled or critically failed for file.
            }

            string logEntry = $"{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}: {message}{Environment.NewLine}";

            try
            {
                string logDirectory = Path.GetDirectoryName(_logFilePath);
                // Ensure the log directory exists.
                if (!Directory.Exists(logDirectory))
                {
                    Directory.CreateDirectory(logDirectory);
                }

                // Use a lock to ensure thread-safe file writing, especially important
                // as NewFrame events occur on a separate thread and multiple log calls
                // could happen concurrently.
                lock (this) // Using 'this' (the Form instance) as the lock object.
                {
                    File.AppendAllText(_logFilePath, logEntry);
                }

                // Reset consecutive error count on successful log write.
                _consecutiveLogErrors = 0;
                // If logging was previously critically failed, and now it's successful, reset the flag.
                if (_isFileLoggingCriticallyFailed)
                {
                    _isFileLoggingCriticallyFailed = false;
                    System.Diagnostics.Debug.WriteLine("File logging recovered from critical failure.");
                }
            }
            catch (Exception ex)
            {
                _consecutiveLogErrors++; // Increment error counter.
                System.Diagnostics.Debug.WriteLine($"Error logging event to file (Attempt {_consecutiveLogErrors}): {ex.Message}");

                // Fallback to Windows Event Log for critical errors or repeated failures.
                try
                {
                    string eventSource = "CaptureProMotionDetection";
                    string eventLogName = "Application"; // Or a custom log like "CapturePro Logs"

                    // IMPORTANT: Creating an EventLog source (the first time it's run on a machine)
                    // often requires administrative privileges. If the application is not run with
                    // admin rights, this line might throw an UnauthorizedAccessException.
                    // For production, consider creating the event source during application installation.
                    if (!EventLog.SourceExists(eventSource))
                    {
                        EventLog.CreateEventSource(eventSource, eventLogName);
                    }

                    // Log to Event Log. Use Error type if _isFileLoggingCriticallyFailed.
                    EventLog.WriteEntry(eventSource,
                        $"FILE LOGGING ERROR (Attempt {_consecutiveLogErrors}): {message} - Details: {ex.Message}",
                        _consecutiveLogErrors >= _maxConsecutiveLogErrors ? EventLogEntryType.Error : EventLogEntryType.Warning);
                }
                catch (Exception eventLogEx)
                {
                    // If even EventLog fails, output to Debug console as a last resort.
                    System.Diagnostics.Debug.WriteLine($"CRITICAL ERROR: Could not write to Windows Event Log while handling file logging error: {eventLogEx.Message}");
                }

                // Check for critical logging failure threshold.
                if (_consecutiveLogErrors >= _maxConsecutiveLogErrors)
                {
                    _isFileLoggingCriticallyFailed = true; // Mark file logging as critically failed.

                    // Rate-limit the message box to avoid spamming the user.
                    if (DateTime.Now - _lastLogErrorMessageBoxTime > _logErrorMessageBoxCooldown)
                    {
                        MessageBox.Show($"CRITICAL ERROR: File logging to '{_logFilePath}' has failed multiple times. " +
                                        "Future events will be logged to the Windows Event Log only. " +
                                        $"Please check file permissions or disk space. Last error: {ex.Message}",
                                        "Critical Logging Failure", MessageBoxButtons.OK, MessageBoxIcon.Error);
                        _lastLogErrorMessageBoxTime = DateTime.Now;
                    }
                }
            }
        }
        #endregion
    }
}
 
Back
Top Bottom