Laser Pointer Tracking

LaserPointerTracking.zip

The C# code below defines several data structures thar make up the core of a laser pointer tracking application I developed for the .NET framework. The video stream is handled by the DirectShow library and the (not included) code to create and render the video stream graph is straight forward. I developed this application to be used with my thesis. You can read more about it if you are curious how it applies.

Feel free to explore the files on the page, or you can download the original C# code in a zip file. The code provided is incomplete. Its purpose is to give a general idea of the code's architecture as well as how the application works.

LaserDotTracking - The LaserDotTracking class handles processing of video frames to identify laser dot positions. It works with the calibration classes to control what pixels are identified as laser dots as well as the PixelList and PixelClump classes to make these points meaningful.

(show)
/// LaserDotTracking class...

public class LaserDotTracking
{
    /*
    * CONSTANTS
    */
    /// <summary>
    /// modifier for the red channel's contribution to brightness
    /// </summary>
    public const ushort RED_MODIFIER = 4;
    /// <summary>
    /// modifier for the green channel's contribution to brightness
    /// </summary>
    public const ushort GREEN_MODIFIER = 2;
    /// <summary>
    /// modifier for the blue channel's contribution to brightness
    /// </summary>
    public const ushort BLUE_MODIFIER = 2;

    //red * 4 + green * 2 + blue * 2
    public const int MAX_POSSIBLE_BRIGHTNESS = 255 * (RED_MODIFIER +
                                                    GREEN_MODIFIER +
                                                    BLUE_MODIFIER);
    
    /*
    *  CLASS PROPERTIES
    */
    /// <summary>
    /// the object to store video calibration data
    /// </summary>
    private CalibrationData calibrationData;

    /// <summary>
    /// the raster size of the video feed
    /// </summary>
    private Size sourceSize;

    /// <summary>
    /// the number of lasers identified in the last frame
    /// </summary>
    private int numLasersOnScreen;

    /// <summary>
    /// the list of laser dots being tracked
    /// </summary>
    private List<LaserDot> lasers;

    /// <summary>
    /// bright pixel clump list
    /// </summary>
    private PixelList brightPixelList;

    /// <summary>
    /// the video frame used for thresholding
    /// </summary>
    private UnsafeBitmap uThresholdFrame;

    /*
    *  CONSTRUCTOR
    */
    /// <summary>
    /// constructor for the tracking object
    /// </summary>
    /// <param name="newData">the calibration object data</param>
    /// <param name="newLasers">the array of laser dots</param>
    public LaserDotTracking(CalibrationData newData, List<LaserDot> newLasers)
    {
        calibrationData = newData;
        lasers = newLasers;
    }

    /// <summary>
    /// unloads the laser dot tracker object
    /// </summary>
    public void unload()
    {
        uThresholdFrame.Dispose();
        lasers.Clear();
        lasers = null;
        brightPixelList = null;
        calibrationData = null;
    }

    //encapsulators omitted...

    /*
    *  FRAME PROCESSING
    */
    /// <summary>
    /// process a new frame of the input video
    /// </summary>
    public void ProcessFrame(Bitmap tmpImage1)
    {
        //Get width and height of source video
        if (brightPixelList == null ||
            sourceSize.Width != tmpImage1.Width ||
            sourceSize.Height != tmpImage1.Height)
        {
            sourceSize.Width = tmpImage1.Width;
            sourceSize.Height = tmpImage1.Height;

            brightPixelList = new PixelList(this, calibrationData,
                                    sourceSize.Width, sourceSize.Height);
        }

        //clear pixel list and prepare to process new frame
        brightPixelList.beginFrame();

        //copy image content to bitmap in unmanaged memory 
        UnsafeBitmap uBitmap = new UnsafeBitmap(tmpImage1);

        //lock memory
        uBitmap.LockBitmap();

        if (!uThresholdFrame.Locked)
            this.uThresholdFrame.LockBitmap();

        //integer variables avoid byte overflow
        int dRed; int dGreen; int dBlue;
        //for each scan line
        for (int y = 0; y < sourceSize.Height; y++)
        {
            //for each pixel in the scan line
            for (int x = 0; x < sourceSize.Width; x++)
            {
                PixelData thresholdPixel = this.uThresholdFrame.GetPixel(x, y);
                PixelData newPixel = uBitmap.GetPixel(x, y);

                //subtract brightnessThreshold pixel color from current pixel color
                dRed = (newPixel.red - thresholdPixel.red);
                dGreen = (newPixel.green - thresholdPixel.green);
                dBlue = (newPixel.blue - thresholdPixel.blue);

                //enforce zero minimum for adjusted color
                if (dRed < 0)
                    dRed = 0;
                if (dGreen < 0)
                    dGreen = 0;
                if (dBlue < 0)
                    dBlue = 0;

                //pixel brightness function
                int brightness = (RED_MODIFIER * dRed) +
                                (GREEN_MODIFIER * dGreen) +
                                (BLUE_MODIFIER * dBlue);

                //if pixel is bright enough to be considered
		// a laser dot add it to the pixel list
                if (brightness > calibrationData.Threshold)
                    brightPixelList.addPixel(new Point(x, y), (ushort)(brightness));
            }
        }

        //identify laser pixel clumps
        brightPixelList.finishFrame();

        //unlock bitmap
        uBitmap.UnlockBitmap();

        //dispose frame
        uBitmap.Bitmap.Dispose();
        tmpImage1.Dispose();

        //process clumps and match them with laser dots from the previous frame
        foreach(LaserDot laser in lasers)
            if(laser.onScreen)
                laser.setClumpDistances( ref brightPixelList.PixelClumps );
        
        // iteratively approach a best solution
        // for laser dot to pixel clump matching
        Boolean changeMade = true;
        int iteration = 0;
        while (changeMade && iteration < 10)
        {
            changeMade = false;
            foreach (LaserDot laser in lasers)
            {
                if (laser.claimBestClump())
                    changeMade = true;
            }
            iteration++;
        }

        //update position from best pixel clump
        foreach (LaserDot laser in lasers)
            laser.updateFromClaim();

        //check for new lasers that were not visible in the previous frame
        foreach(LaserDot laser in lasers)
            laser.checkForNewLaser( ref brightPixelList.PixelClumps );

        //total number of lasers dots in the current frame
        numLasersOnScreen = 0;
        for (int i = 0; i < lasers.Count; i++)
        {
            if (lasers[i].onScreen)
                numLasersOnScreen++;
        }
    }
}

CalibrationData - This class stores the information regarding the calibration of a video capture source for tracking laser pointer dots or "laser dots". This class performs the following functions:

  • Crops the video stream to help optimize tracking
  • Corrects tracked positions to account for lens distortion
  • Stores a reference image to compare video frames against for brightness thresholding.
(show)
//CalibrationData class...

public class CalibrationData
{
    /*
    * CONSTANTS
    */
    /// <summary>
    /// the default pixel brightness threshold
    /// </summary>
    public const int DEFAULT_THRESHOLD = 500;
    /// <summary>
    /// the padding to add to the pixel brightness threshold calculation
    /// </summary>
    public const int THRESHOLD_PADDING = 20;
    /// <summary>
    /// the number of K vals needed to describe the camera lens
    /// </summary>
    public const int NUM_K_VALS = 2;

    /*
    * CLASS PROPERTIES
    */
    /// <summary>
    /// the camera device name
    /// </summary>
    protected string cameraDeviceName;
    /// <summary>
    /// the capture device object
    /// </summary>
    protected CaptureDevice cameraDevice;

    /// <summary>
    /// the calibration form object
    /// </summary>
    protected CalibrationForm calibrationForm;
    /// <summary>
    /// the video monitor on the calibration form
    /// </summary>
    protected PictureBox videoMonitor;
    /// <summary>
    /// the last captured frame from the video
    /// for display to the video monitor
    /// </summary>
    protected Bitmap displayFrame;

    /// <summary>
    /// the full uncropped resolution of the video feed
    /// </summary>
    protected WinDraw.Size fullResolution;
    
    /// <summary>
    /// cropping rectangle
    /// </summary>
    protected WinDraw.Rectangle cropRectangle;

    //lens rectification
    //K lens parameters
    protected bool KValuesFound;
    protected DoubleVector Kvector;

    //normalization matrices
    protected Matrix3 tNormLens;
    protected Matrix3 tNormLensInv;

    /// <summary>
    /// point correction matrix to adjust for cropping
    /// </summary>
    protected Matrix3 tPointCorrection;

    /// <summary>
    /// the laser dot tracker object
    /// </summary>
    protected LaserDotTracking laserTracker;
    
    /// <summary>
    /// the brightness threshold level, above
    /// which pixels are considered to be a laser dot
    /// </summary>
    protected int brightnessThreshold;

    /// <summary>
    /// the number of lasers being tracked
    /// </summary>
    protected int numLasers;
    /// <summary>
    /// the list of laser dot objects
    /// </summary>
    protected List<LaserDot> lasers;

    //user point manipulation data omitted...

    /// <summary>
    /// constructor for a new video calibration object
    /// </summary>
    /// <param name="newCaptureDeviceName">the name of the capture device</param>
    /// <param name="videoResolution">the resolution of the video feed</param>
    /// <param name="newMonitor">the form object for displaying the video feed</param>
    /// <param name="newCaptureDevice">the new capture device object</param>
    /// <param name="newParentForm">the calibration form object</param>
    public CalibrationData(string newCaptureDeviceName,
        WinDraw.Size videoResolution, PictureBox newMonitor,
        CaptureDevice newCaptureDevice, CalibrationForm newCalibrationForm)
    {
        //video feed
        calibrationForm = newCalibrationForm;
        videoMonitor = newMonitor;

        fullResolution = videoResolution;

        cameraDeviceName = newCaptureDeviceName;
        cameraDevice = newCaptureDevice;
        if (cameraDevice != null)
            cameraDevice.NewFrame += new CameraEventHandler(ProcessFrame);

        //lens correction
        KValuesFound = false;
        Kvector = new DoubleVector(NUM_K_VALS);
        updateTnorm();

        //default threshold
        brightnessThreshold = DEFAULT_THRESHOLD;

        //no cropping
        cropRectangle = new WinDraw.Rectangle(0, 0,
                        fullResolution.Width, fullResolution.Height);
        tPointCorrection = Matrix3.Identity;

        //laser dots
        this.NumLasers = DEFAULT_NUM_LASERS;

        //create laserTracker
        laserTracker = new LaserDotTracking(this, lasers);
    }

    //encapsulators omitted

    /*
    * VIDEO PROCESSING
    */
    /// <summary>
    /// should be called every CalibrationForm.UPDATES_PER_SECOND
    /// by the calibration form object
    /// </summary>
    public virtual void update()
    {
        //update display
        if (displayFrame != null && videoMonitor != null)
        {
            try
            {
                if (videoMonitor.Image != this.displayFrame)
                {
                    if (videoMonitor.Image != null)
                        videoMonitor.Image.Dispose();

                    drawOnDisplayFrame();
                    videoMonitor.Image = this.displayFrame;
                }
            }
            catch (InvalidOperationException exc)
            {
                Log(exc.Tostring());
            }
            catch (ArgumentException exc)
            {
                Log(exc.Tostring());
            }
        }
    }

    /// <summary>
    /// draw laser positions and crop box onto frame
    /// </summary>
    private void drawOnDisplayFrame()
    {
        try
        {
            Graphics dc = Graphics.FromImage(displayFrame);

            //draw cropping rectangle
            dc.DrawRectangle(new Pen(Color.White), cropRectangle);

            //add circle to image where laser positions were located
            if (laserTracker.NumVisibleLasers > 0)
            {
                Pen p = new Pen(Color.Red, 1);
                foreach (LaserDot laser in lasers)
                    if (laser.onScreen)
                        dc.DrawEllipse(p, (float)laser.NextPosition.X - 5,
                            (float)laser.NextPosition.Y - 5, 10, 10);
            }
            dc.Dispose();
        }
        catch (OutOfMemoryException OoM)
        {
            Log(OoM.Tostring());
            GC.Collect(); //force garbage collection
        }
        catch (AccessViolationException exc)
        {
            Log(exc.Tostring());
            return;
        }
    }

    /// <summary>
    /// event listener to handle a new video frame from the camera
    /// </summary>
    protected virtual void ProcessFrame(object sender, CameraEventArgs e)
    {
        if (videoMonitor.Image != displayFrame)
            displayFrame.Dispose();

        //frame for display to the video monitor
        displayFrame = (Bitmap)e.Bitmap.Clone();

        //process frame to identify new laser dot positions
        laserTracker.ProcessFrame((Bitmap)e.Bitmap.Clone(cropRectangle,
                                e.Bitmap.PixelFormat));
        
        //rectify positions from video space to NDC application space
        rectifyLaserPositions();
    }

    /// <summary>
    /// stops video feed processsing
    /// </summary>
    public void stopAll()
    {
        if (cameraDevice != null)
        {
            cameraDevice.NewFrame -= new CameraEventHandler(ProcessFrame);
            cameraDevice.Stop();
        }
        laserTracker.unload();
    }

    /// <summary>
    /// rectififies the identified laser dot positions
    /// from video raster space to NDC application space
    /// </summary>
    public virtual void rectifyLaserPositions()
    {
        foreach(LaserDot laser in lasers)
        {
            if (laser.onScreen)
            {
                //lens correction...
                if (KValuesFound)
                    laser.AdjustedPosition = tNormLensInv.transform(
                                    radialDistort(
                                    tNormLens.transform(
                                    laser.NextPosition), 
                                    Kvector));
                else
                    laser.AdjustedPosition = laser.NextPosition;
            }
        }
    }

    /*
    * AUTO CALLIBRATION
    */
    public virtual void AutoUpdateCorners()
    {
        //overriden by planar calibration
    }
    
    /*
    * CROPPING
    */
    /// <summary>
    /// crop window to corners of screen area
    /// (overriden by planar calibration)
    /// </summary>
    public virtual void AutoCrop()
    {
        //crop the thresholding image
       if (laserTracker.ThresholdImage != null)
            laserTracker.ThresholdImage = laserTracker.ThresholdImage.Clone(
            cropRectangle, laserTracker.ThresholdImage.PixelFormat); 

        //update callibration form
        if (calibrationForm != null)
            calibrationForm.updateCrop(cropRectangle.Top,
                        fullResolution.Width - cropRectangle.Right + 1,
                        fullResolution.Height - cropRectangle.Bottom + 1,
                        cropRectangle.Left);
    }

    /// <summary>
    /// gets / sets the current video stream cropping rectangle
    /// </summary>
    public WinDraw.Rectangle CroppingRectangle
    {
        get { return cropRectangle; }
        set { cropRectangle = value; updateCrop(); }
    }

    /// <summary>
    /// updates the cropped display
    /// </summary>
    public void updateCrop()
    {
        //videoMonitor.Location = new Point(cropRectangle.X, cropRectangle.Y);
        if (laserTracker != null)
            laserTracker.Reset();
        //update the matrix which corrects video positions to adjust for cropping
        tPointCorrection = Matrix3.Identity;
        tPointCorrection.translate(cropRectangle.X, cropRectangle.Y);
    }

    /// <summary>
    /// sets the last captured frame to be the brightnessThreshold image
    /// </summary>
    public void updateThresholdImage()
    {
        if (this.displayFrame != null)
        {
            //save threshold image for testing purposes
            displayFrame.Save("./" + CalibrationData.THRESHOLD_IMAGE_NAME,
                            WinImage.ImageFormat.Jpeg);
            //cropped brightnessThreshold image
            laserTracker.ThresholdImage = displayFrame.Clone(cropRectangle,
                            displayFrame.PixelFormat);
        }
    }

    /// <summary>
    /// uses the brightnessThreshold frame
    ///(an image of the screen at 50% brightness)
    /// and the max brightness frame 
    /// (an image with the screen at 100% brightness)
    /// to derive a reasonable brightness brightnessThreshold,
    /// above which pixels are considered to
    /// be a laser pointer dot
    /// </summary>
    public void updateThresholdLimit()
    {
        if (this.laserTracker == null || t
                his.laserTracker.ThresholdImage == null)
            return;

        UnsafeBitmap thresholdBitmap = 
				new UnsafeBitmap((Bitmap)laserTracker.ThresholdImage);
        UnsafeBitmap maxBrightBitmap = new UnsafeBitmap((Bitmap)displayFrame.Clone(
                    this.cropRectangle, this.laserTracker.ThresholdImage.PixelFormat));

        maxBrightBitmap.Bitmap.Save("./" + CalibrationData.MAX_THRESHOLD_IMAGE_NAME,
                                    WinImage.ImageFormat.Jpeg);
        
        thresholdBitmap.LockBitmap();
        maxBrightBitmap.LockBitmap();

        if (thresholdBitmap.Bitmap.Width != maxBrightBitmap.Bitmap.Width ||
            thresholdBitmap.Bitmap.Height != maxBrightBitmap.Bitmap.Height)
        {
            throw new Exception("raster image size mismatch");
        }

        // find the maximum difference between the max
        // brightness image and the thresholding image
        int maxBrightness = 0;
        
        for (int i = 0; i < maxBrightBitmap.Bitmap.Height; i++)
        {
            for (int j = 0; j < maxBrightBitmap.Bitmap.Width; j++)
            {
                PixelData brightPixel = maxBrightBitmap.GetPixel(j, i);
                PixelData thresPixel = thresholdBitmap.GetPixel(j, i);

                int ired = brightPixel.red - thresPixel.red;
                int igreen = brightPixel.green - thresPixel.green;
                int iblue = brightPixel.blue - thresPixel.blue;

                if(ired < 0)
                    thresPixel.red = 0;
                else
                    thresPixel.red = (byte)ired;

                if(igreen < 0)
                    thresPixel.green = 0;
                else
                    thresPixel.green = (byte)igreen;

                if(iblue < 0)
                    thresPixel.blue = 0;
                else
                    thresPixel.blue = (byte)iblue;

                int curThreshold = (thresPixel.red * LaserDotTracking.RED_MODIFIER) +
                           (thresPixel.green * LaserDotTracking.GREEN_MODIFIER) +
                           (thresPixel.blue * LaserDotTracking.BLUE_MODIFIER);

                if (curThreshold > maxBrightness)
                    maxBrightness = curThreshold;
            }
        }

        //set the brightnessThreshold padding based
        //on the maximum brightness difference
        this.Threshold = maxBrightness + THRESHOLD_PADDING;
        calibrationForm.setThreshold(this.Threshold);
    }

    //user grid point manipulation omitted...

    /*
    * LENS RECTIFICATION
    */
    /// <summary>
    /// updates the normalization matrix based on the video feed's width and height
    /// </summary>
    private void updateTnorm()
    {
        //normalization matrix places origin at the center of the video
        tNormLensInv = Matrix3.Identity;

        //translation
        tNormLensInv[0, 2] = (float)fullResolution.Width / 2;
        tNormLensInv[1, 2] = (float)fullResolution.Height / 2;

        //scaling
        tNormLensInv[0, 0] = (float)(fullResolution.Width + fullResolution.Height);
        tNormLensInv[1, 1] = (float)(fullResolution.Width + fullResolution.Height);

        tNormLensInv[2, 2] = 1.0f;
        tNormLens = tNormLensInv.returnInverse();
    }
    
    /// <summary>
    /// get the square of the distance from a test point to a line defined
    /// by a point on the line and a direction and the line length squared (for performance)
    /// </summary>
    public double distanceSquaredToLine(Vector2d lineStart, Vector2d lineDir,
                            double lengthSqr, Vector2d testPoint)
    {
        // vector from line's start to test point
        Vector2d w = testPoint - lineStart;

        // find perpendicular projection of hit point onto line segment
        double r = (w * lineDir) / lengthSqr;

        // closest point is at parameter r distance from start
        return Vector2d.DistanceSquared(testPoint, lineStart + (r * lineDir));
    }

    /// <summary>
    /// get the distance from a test point to a line defined
    /// by a point and a line direction and the line length squared (for performance)
    /// </summary>
    public double distanceToLine(Vector2d lineStart, Vector2d lineDir,
                        double lengthSqr, Vector2d testPoint)
    {
        return Math.Sqrt(distanceSquaredToLine(lineStart, lineDir, lengthSqr, testPoint));
    }

    /// <summary>
    /// get the radially distorted point position from a current point and a K matrix
    /// this method assumes the point has been normalized to a central origin
    /// </summary>
    public Vector2d radialDistort(Vector2d curP, DoubleVector Kvals)
    {
        double r = curP.Length();
        double Lr = 1;

        for (int i = 0; i < Kvals.Length; i++)
            Lr += Math.Pow(r, i + 1) * Kvals[i];

        return Lr * curP;
    }

    /// <summary>
    /// determines the error 
    /// </summary>
    /// <param name="x">the K vector that defines the lens distortion</param>
    /// <param name="count">the iteration count</param>
    /// <param name="maxIter">the total number of iterations</param>
    /// <returns>the residual error</returns>
    private double determineError(DoubleVector Kvals, int count, int maxIter)
    {
        double sum = 0;

        Vector2d correctLine = Vector2d.Zero;
        Vector2d lineStart = Vector2d.Zero;
        Vector2d lineEnd = Vector2d.Zero;

        double lengthSqr = 0;

        //lines defined by grid rows
        for (int i = 0; i < numGridRows; i++)
        {
            //rectified start and end points of grid row
            lineStart = radialDistort(gridPoints[i, 0], Kvals);
            lineEnd = radialDistort(gridPoints[i, numGridCols - 1], Kvals);

            //corrected line length and vector
            correctLine = lineEnd - lineStart;
            lengthSqr = correctLine.LengthSquared();

            //use all points in row except far left and right
            for (int j = 1; j < numGridCols-1; j++)
                //distance from staright line to test point
                sum += distanceSquaredToLine(lineStart, correctLine, lengthSqr,
                                    radialDistort(gridPoints[i, j], Kvals));
        }
        //lines defined by grid columns
        for (int j = 0; j < numGridCols; j++)
        {
            //rectified start and end points of grid column
            lineStart = radialDistort(gridPoints[0, j], Kvals);
            lineEnd = radialDistort(gridPoints[numGridRows - 1, j], Kvals);

            //corrected line length and vector
            correctLine = lineEnd - lineStart;
            lengthSqr = correctLine.LengthSquared();

            //all points in column except top and bottom
            for (int i = 1; i < numGridRows - 1; i++)
                //distance from staright line to test point
                sum += distanceSquaredToLine(lineStart, correctLine, lengthSqr,
                                    radialDistort(gridPoints[i, j], Kvals));
        }
        return sum;
    }

    /// <summary>
    /// determine appropriate K values to adjust for lens distortion
    /// </summary>
    public void LensDistortionCorrection()
    {
        gridPoints = new Vector2d[numGridRows, numGridCols];

        for (int i = 0; i < numGridRows; i++)
            for (int j = 0; j < numGridCols; j++)
                gridPoints[i, j] = tNormLens * new Vector2d(gridCircles[i, j].Location);

        Kvector = new DoubleVector(NUM_K_VALS);

        //test error when fixing barrel distortion
        FloatMatrix.setAllValues(ref Kvector, 0.1);
        double barrelDistortionError = determineError(Kvector, 0, 200);

        //test error when fixing pin cushion distortion
        FloatMatrix.setAllValues(ref Kvector, -0.1);
        double pinCushionDistortionError = determineError(Kvector, 0, 200);

        //use more accurate model
        if(barrelDistortionError < pinCushionDistortionError)
            FloatMatrix.setAllValues(ref Kvector, 0.1);

        double minimizedError = 0;
        string outmsg = "";

        //two methods for iteratively solving for lens distortion parameters
        //SimpleIterative uses a method of my own design, LMFsolve uses the Levenberg-Marquardt method
        Kvector = FloatMatrix.SimpleIterative(Kvector, new LFMResiduals(determineError),
                                    1E-12, ref outmsg, ref minimizedError, 50, 10);
        //Kvector = FloatMatrix.LMFsolve(Kvector, new LFMResiduals(determineError), ref outmsg,
                            //ref numIterations, ref minimizedError, 50, 1e-12, 1e-12, 10);
        Log(outmsg);

        int numPoints = numGridRows * numGridCols;
        double initialError = determineError(new DoubleVector(Kvector.Length), 0, 1);
        double residualError = determineError(this.Kvector, 0, 1);
        
        Log("initial error: " + initialError + "  residual error: " + residualError);

        KValuesFound = true;
    }
}

PlaneCalibration - The plane callibration class handles the transformation that map points from the raster space of the video feed to NDC coordinates. It accomplishes this transformation by analying an image from the video feed to identify and store the corner locations of the screen in the image. Then a homography is found that maps these points to NDC corners.

(show)
//PlaneCalibration class...

public class PlaneCalibration : CallibrationData
{
     /*
     * CONSTANTS
     */
    public const int GREEN_CORNER_THRESHOLD = 10;
    public const int BOUNDS_PADDING = 10;

     /*
     * CLASS PROPERTIES
     */
    private bool HomFound;
    private Matrix3 HomMatrix;

    private List<PictureBox> hCircles;

    private Vector2 cornerTL;
    private Vector2 cornerTR;
    private Vector2 cornerBL;
    private Vector2 cornerBR;

    private WinDraw.Rectangle bounds;


     /*
     * CONSTRUCTOR
     */
    /// <summary>
    /// constructor for a new plane callibration object
    /// </summary>
    /// <param name="newCaptureDeviceName">the name of the capture device</param>
    /// <param name="videoResolution">the resolution of the video feed</param>
    /// <param name="newMonitor">the form object for displaying the video feed</param>
    /// <param name="newCaptureDevice">the new capture device object</param>
    /// <param name="newParentForm">the calibration form object</param>
    public PlaneCalibration(String newCaptureDeviceName,
        WinDraw.Size videoResolution, PictureBox newMonitor,
        CaptureDevice newCaptureDevice, CalibrationForm newCalibrationForm)
        : base( newName, videoResolution, newMonitor, newDevice, newParentForm)
    {
        HomFound = false;
        HomMatrix = Matrix3.Identity;
        hCircles = new List<PictureBox>();
    }

    //manual point manipualtion omitted

     /*
     * AUTO CALIBRATION
     */
    /// <summary>
    /// Automatically identifies the corners of the screen
    /// in the video feed image (using the most recent frame).
    /// The application displays an image with green circles in the four
    /// corners of the screen so that only a few pixels need to be considered.
    /// </summary>
    public override void AutoUpdateCorners()
    {
        //verify non null values
        if (displayFrame == null || videoMonitor == null)
            throw new Exception("null component, cannot proceed with auto calibration");

        //get most recent frame as a bitmap in unmanaged memory
        Bitmap cornerRefBitmap = (Bitmap)displayFrame.Clone();
        UnsafeBitmap cornerRef = new UnsafeBitmap(cornerRefBitmap);
        cornerRef.LockBitmap();

        //brightest pixels
        List<Vector2d> brightPoints = new List<Vector2d>();

        // identify all points bright enough to
        // be considered part of the screen image
        for (int y = 0; y < cornerRef.Bitmap.Height; y++)
        {
            for (int x = 0; x < cornerRef.Bitmap.Width; x++)
            {
                //for each pixel in image checkt to see if pixel is bright enough
                //to be a corner area pixel
                if (cornerRef.GetPixel(x, y).green > GREEN_CORNER_THRESHOLD)
                    brightPoints.Add(new Vector2d(x, y));
            }
        }

        if (brightPoints.Count < 4)
            throw new Exception("Not enough points found.");

        //find the hull of the bright pixels
        double minX = brightPoints[0].X;
        double maxX = brightPoints[0].X;
        double minY = brightPoints[0].Y;
        double maxY = brightPoints[0].Y;

        for (int i = 1; i < brightPoints.Count; i++)
        {
            if (brightPoints[i].X < minX)
                minX = brightPoints[i].X;
            if (brightPoints[i].Y < minY)
                minY = brightPoints[i].Y;
            if (brightPoints[i].X > maxX)
                maxX = brightPoints[i].X;
            if (brightPoints[i].Y > maxY)
                maxY = brightPoints[i].Y;
        }

        //boudning box of the corner points
        bounds = new System.Drawing.Rectangle((int)minX, (int)minY,
						(int)(maxX - minX), (int)(maxY - minY));

        //points on the corners of the bounding box
        Vector2d boundsTL = new Vector2d(minX, minY);
        Vector2d boundsTR = new Vector2d(maxX, minY);
        Vector2d boundsBL = new Vector2d(minX, maxY);
        Vector2d boundsBR = new Vector2d(maxX, maxY);

        //direction of the bounding box edges
        Vector2d topDir = boundsTR - boundsTL;
        Vector2d bottomDir = boundsBR - boundsBL;
        Vector2d leftDir = boundsTL - boundsBL;
        Vector2d rightDir = boundsTR - boundsBR;

        //screen corners
        Vector2d TL = Vector2d.Zero;
        Vector2d TR = Vector2d.Zero;
        Vector2d BL = Vector2d.Zero;
        Vector2d BR = Vector2d.Zero;

        //edge lengths
        double topLength = Vector2d.DistanceSquared(boundsTL, boundsTR);
        double bottomLength = Vector2d.DistanceSquared(boundsBL, boundsBR);
        double rightLength = Vector2d.DistanceSquared(boundsTR, boundsBR);
        double leftLength = Vector2d.DistanceSquared(boundsTL, boundsBL);

        double curDist = 0;
        double TLMinDist = 0;
        double TRMinDist = 0;
        double BLMinDist = 0;
        double BRMinDist = 0;

        // find the positions that minimizes the distance between
        // two edges of the bounding box to find the screen corner
        for (int i = 0; i < brightPoints.Count; i++)
        {
            //top and left
            curDist = distanceToLine(boundsTL, topDir, topLength, brightPoints[i])
			+ distanceToLine(boundsBL, leftDir, leftLength, brightPoints[i]);
            if (curDist < TLMinDist || i == 0)
            {
                TL = brightPoints[i];
                TLMinDist = curDist;
            }

            //top and right
            curDist = distanceToLine(boundsTL, topDir, topLength, brightPoints[i])
			+ distanceToLine(boundsBR, rightDir, rightLength, brightPoints[i]);
            if (curDist < TRMinDist || i == 0)
            {
                TR = brightPoints[i];
                TRMinDist = curDist;
            }

            //bottom and left
            curDist = distanceToLine(boundsBL, bottomDir, bottomLength, brightPoints[i])
			+ distanceToLine(boundsBL, leftDir, leftLength, brightPoints[i]);
            if (curDist < BLMinDist || i == 0)
            {
                BL = brightPoints[i];
                BLMinDist = curDist;
            }

            //bottom and right
            curDist = distanceToLine(boundsBL, bottomDir, bottomLength, brightPoints[i])
			+ distanceToLine(boundsBR, rightDir, rightLength, brightPoints[i]);
            if (curDist < BRMinDist || i == 0)
            {
                BR = brightPoints[i];
                BRMinDist = curDist;
            }
        }

        //convert to XNA vectors
        cornerTL = TL.ToXNAVector();
        cornerTR = TR.ToXNAVector();
        cornerBL = BL.ToXNAVector();
        cornerBR = BR.ToXNAVector();

        //output image with identified corners circled for testing purposes
        cornerRef.UnlockBitmap();
        Graphics dc = Graphics.FromImage(cornerRefBitmap);
        Pen p = new Pen(Color.Red, 1);
        dc.DrawEllipse(p, (float)cornerTL.X - 2, (float)cornerTL.Y - 2, 4, 4);
        dc.DrawEllipse(p, (float)cornerTR.X - 2, (float)cornerTR.Y - 2, 4, 4);
        dc.DrawEllipse(p, (float)cornerBL.X - 2, (float)cornerBL.Y - 2, 4, 4);
        dc.DrawEllipse(p, (float)cornerBR.X - 2, (float)cornerBR.Y - 2, 4, 4);
        dc.Dispose();

        cornerRefBitmap.Save(curGame.ApplicationDirectory +
			CallibrationData.CORNER_REF_IMAGE, WinImage.ImageFormat.Jpeg);

        //update corner point positions (hCircles list)
        //in video monitor on calibration form
        updateCorners();
    }

    /// <summary>
    /// crop video feed to the corners of the screen image area
    /// </summary>
    public override void AutoCrop()
    {
        base.autoCrop();

        if (bounds != null && bounds.Width > 0 && bounds.Height > 0)
        {
            //add padding to bounding box
            bounds.X -= BOUNDS_PADDING;
            bounds.Y -= BOUNDS_PADDING;
            float twicePadding = BOUNDS_PADDING * 2;
            bounds.Width += twicePadding;
            bounds.Height += twicePadding;

            if (bounds.X < 0)
                bounds.X = 0;
            if (bounds.Y < 0)
                bounds.Y = 0;
            if (bounds.Right > fullResolution.Width - 1)
                bounds.Width = fullResolution.Width - bounds.X - 1;
            if (bounds.Bottom > fullResolution.Height - 1)
                bounds.Height = fullResolution.Height - bounds.Y - 1;

            //update crop...
            this.cropRectangle = bounds;
        }
    }

     /*
     * HOMOGRAPHIC MATRIX CALCULATION
     */
    /// <summary>
    /// put point positions into a list in the correct
    /// order for homography calculation(TL, TR, BL, BR)
    /// </summary>
    private void getCornerPoints()
    {
        if (hCircles.Count >= 4)
        {
            List<Vector2d> screenCorners = new List<Vector2d>();
            for (int i = 0; i < 4; i++)
            {
                //correct for video scaling and cropping
                screenCorners.Add(new Vector2d(hCircles[i].Location));
                if (mKfound)
                    screenCorners[i] = tNormLensInv.transform(radialDistort(
					tNormLens.transform(screenCorners[i]), Kvector));
            }

            //identify each of 4 corners as the top left, top right, bottom left, and bottom right positions
            double minX = fullResolution.Width;
            double minX2 = fullResolution.Width;
            double minY = fullResolution.Height;
            double minY2 = fullResolution.Height;
            double minXIndex = 0;
            double minX2Index = 0;
            double minYIndex = 0;
            double minY2Index = 0;

            //find smallest two X coordinates
            for (int i = 0; i < 4; i++)
            {
                if (screenCorners[i].X < minX)
                {
                    minX2 = minX;
                    minX2Index = minXIndex;

                    minX = screenCorners[i].X;
                    minXIndex = i;
                }
                else if (screenCorners[i].X < minX2)
                {
                    minX2 = screenCorners[i].X;
                    minX2Index = i;
                }
            }

            //find smallest two Y coordinates
            for (int i = 0; i < 4; i++)
            {
                if (screenCorners[i].Y < minY)
                {
                    minY2 = minY;
                    minY2Index = minYIndex;

                    minY = screenCorners[i].Y;
                    minYIndex = i;
                }
                else if (screenCorners[i].Y < minY2)
                {
                    minY2 = screenCorners[i].Y;
                    minY2Index = i;
                }
            }

            //find identified screen corners
            Vector2d TL = Vector2d.Zero;
            Vector2d TR = Vector2d.Zero;
            Vector2d BL = Vector2d.Zero;
            Vector2d BR = Vector2d.Zero;
            for (int i = 0; i < 4; i++)
            {
                if ((i == minXIndex || i == minX2Index) && 
					i == minYIndex || i == minY2Index))
                    TL = screenCorners[i];
                else if ((!(i == minXIndex || i == minX2Index)) &&
					(i == minYIndex || i == minY2Index))
                    TR = screenCorners[i];
                else if ((i == minXIndex || i == minX2Index) &&
					!(i == minYIndex || i == minY2Index))
                    BL = screenCorners[i];
                else
                    BR = screenCorners[i];
            }

            screenCorners[0] = TL;
            screenCorners[1] = TR;
            screenCorners[2] = BL;
            screenCorners[3] = BR;
            return screenCorners;
        }
        return null;
    }

    /// <summary>
    /// updates homographic matrix based on corner circle positions
    /// </summary>
    private void updateHomography()
    {
        HomFound = false;
        if (hCircles != null && hCircles.Count >= 4)
        {
            List<Vector2d> screenCorners = getCornerPoints();

            //normalized device coordinate corners
            NDCPoints = new List<Vector2d>();
            NDCPoints.Add(new Vector2d(-1, -1)); NDCPoints.Add(new Vector2d(1, -1));
            NDCPoints.Add(new Vector2d(-1, 1)); NDCPoints.Add(new Vector2d(1, 1));

            //find mapping from raster corners in video space to NDC corners
            HomMatrix = Matrix3.Homography(screenCorners, NDCPoints);

            HomFound = true;
        }
    }
    
     /*
     * LASER POSITION RECTIFICATION
     */
    /// <summary>
    /// rectify point positions from video space to NDC application space
    /// </summary>
    public override void rectifyLaserPositions()
    {
        base.rectifyLaserPositions();
        if (HomFound)
        {
            foreach (LaserDot laser in lasers)
                if (laser.onScreen)
                    laser.AdjustedPosition = HomMatrix.transform(laser.AdjustedPosition);
        }
    }
}

PixelList - This class manages a list of pixel coordinates that have an intensity bright enough to be a laser dot. Once a frame has finished processing by the tracker class the identified coordinates are grouped together into PixelClumps.

(show)
//PixelList class

public class PixelList
{
     /*
     *  CONSTANTS
     */
    /// <summary>
    /// the minimum number of pixels in a clump to be considered part of a laser dot
    /// </summary>
    public const int NUM_PIXELS_NEEDED_FOR_LASER = 3;

     /*
     * CLASS PROPERTIES
     */
    /// <summary>
    /// the list of bright pixel clumps
    /// </summary>
    public List<PixelClump> PixelClumps;
    /// <summary>
    /// the maximum number of clumps
    /// </summary>
    protected int maxNumClumps;

    //possible pixel attributes
    /// <summary>
    /// the list of bright pixel positions identified by the tracker
    /// </summary>
    protected List<Point> brightPixels;
    /// <summary>
    /// the raster size of the video stream
    /// </summary>
    protected Size sourceSize;
    /// <summary>
    /// the matrix of values flags indicating which pixels
    /// have been identified as part of a pixel clump
    /// </summary>
    protected Boolean[,] identified;
    /// <summary>
    /// the matrix of laser dot weights (brightness) for each pixel
    /// </summary>
    protected ushort[,] pixelWeights;
    /// <summary>
    /// the video calibration data object
    /// </summary>
    protected CalibrationData calibrationData;
    /// <summary>
    /// the laser dot tracking object
    /// </summary>
    protected LaserDotTracking curTracker;

     /*
     *  CONSTRUCTOR
     */
    /// <summary>
    /// constructor for a new pixel list manager object
    /// </summary>
    /// <param name="newTracker">the laser dot tracking object</param>
    /// <param name="newData">the calibration data object</param>
    /// <param name="sourceWidth">the raster width of the video feed</param>
    /// <param name="sourceHeight">the raster height of the video feed</param>
    public PixelList(LaserDotTracking newTracker, CalibrationData newData,
							int sourceWidth, int sourceHeight)
    {
        curTracker = newTracker;
        calibrationData = newData;

        maxNumClumps = calibrationData.NumLasers * 2;

        sourceSize = new Size(sourceWidth, height);
        pixelWeights = new ushort[sourceSize.sourceHeight, sourceSize.Height];
        identified = new Boolean[sourceSize.Width, sourceSize.Height];
        brightPixels = new List<Point>(150);

        PixelClumps = new List<PixelClump>();
    }

    /// <summary>
    /// called when a new frame begins processing
    /// </summary>
    public void beginFrame()
    {
        brightPixels.Clear();

        for (int i = 0; i < sourceSize.Height; i++)
            for (int j = 0; j < sourceSize.Width; j++)
            {
                //set all pixels to no laser weight
                pixelWeights[j, i] = 0;
                identified[j, i] = false;
            }
    }

    /// <summary>
    /// adds a new entry to the list of pixels which are intense
    /// enough to be considered a laser pointer
    /// </summary>
    /// <param name="pos">the pixel's position</param>
    /// <param name="weight">the pixel's weight (brightness)</param>
    public void addPixel(Point pos, ushort weight)
    {
        pixelWeights[pos.X, pos.Y] = weight;
        brightPixels.Add(pos);
    }
    
     /*
     *  PROCESS FINISHED FRAME
     */
    /// <summary>
    /// called when a frame has finished processing
    /// by the laser dot tracking object. Processes
    /// the identified bright pixels to group them
    /// into pixel clumps
    /// </summary>
    public virtual void finishFrame()
    {
        PixelClumps.Clear();

        for (int i = 0; i < brightPixels.Count; i++)
        {
            //if the current bright pixel has not already been identified as part of another pixel clump...
            if (!identified[brightPixels[i].X, brightPixels[i].Y])
            {
                //create a new pixel clump
                PixelClump curClump = new PixelClump(false);
                
                //gets all the pixels that make up this clump
                recursiveGetPixelClump(brightPixels[i].X, brightPixels[i].Y,ref curClump);

                if (curClump.NumPixels == 0)
                {
                    curClump.AveragePosition.X = 0;
                    curClump.AveragePosition.Y = 0;
                    curClump.AverageWeight = 0;
                }
                else
                {
                    //find the average position of this clump
                    curClump.AveragePosition.X = curClump.PositionSum.X
								/ curClump.NumPixels;
                    curClump.AveragePosition.Y = curClump.PositionSum.Y
								/ curClump.NumPixels;
                    curClump.AveragePosition += calibrationData.CropOffset;
                    curClump.AverageWeight = curClump.TotalWeight / curClump.NumPixels;
                }

                //if the clump is large enough add it to the list of pixel clumps that represent laser dots
                if (curClump.NumPixels >= PixelList.NUM_PIXELS_NEEDED_FOR_LASER)
                    PixelClumps.Add(curClump);

                //limit the number of pixel clumps
                //to prevent a stack overflow or OoM
                if(PixelClumps.Count > maxNumClumps)
                    break;
            }
        }
    }

    /// <summary>
    /// recursively identifies all pixels in this
    /// bright clump by flood filling algorithm
    /// </summary>
    /// <param name="X">the current X location</param>
    /// <param name="Y">the current Y location</param>
    /// <param name="clump">the current clump of pixels</param>
    private void recursiveGetPixelClump(int X, int Y, ref PixelClump clump)
    {
        if (X >= 0 && X < sourceSize.Width &&
            Y >= 0 && Y < sourceSize.Height &&
            !identified[X, Y] &&
            pixelWeights[X, Y] > 0)
        {
            //if in bounds and not yet a part of a pixel clump and pixel weight is large enough
            identified[X, Y] = true;

            clump.TotalWeight += pixelWeights[X, Y];
            clump.PositionSum.X += X;
            clump.PositionSum.Y += Y;
            clump.NumPixels++;

            // limit number of pixels which can make up a clump
            // to prevent a stack overflow
            if (clump.NumPixels > 400)
                return; 

            //add neighbors
            recursiveGetPixelClump(X + 1, Y - 1, ref clump); //right, top
            recursiveGetPixelClump(X + 1, Y, ref clump); //right
            recursiveGetPixelClump(X + 1, Y + 1, ref clump);  //right, botom

            recursiveGetPixelClump(X, Y - 1, ref clump); //top
            recursiveGetPixelClump(X, Y + 1, ref clump); //bottom

            recursiveGetPixelClump(X - 1, Y - 1, ref clump); //left, top
            recursiveGetPixelClump(X - 1, Y, ref clump); //left
            recursiveGetPixelClump(X - 1, Y + 1, ref clump); //left, botom
        }
    }
}

PixelClump - A simple struct that stores information describing a clump of bright pixels. This clump is then matched with a laser dot being tracked from the previous frame or is identified as a new laser dot.

(show)
//PixelClump struct...

public struct PixelClump
{
         /*
         *  STRUCT PROPERTIES
         */
	/// <summary>
	/// clump position from average pixel position
	/// </summary>
	public Vector2d AveragePosition;
	/// <summary>
	/// the average pixel position in this clump
	/// </summary>
	public Point PositionSum;
	/// <summary>
	/// the sum of all pixel weights
	/// </summary>
	public int TotalWeight;
	/// <summary>
	/// the average pixel weight
	/// </summary>
	public int AverageWeight;
	/// <summary>
	/// the number of pixels in this clump
	/// </summary>
	public int NumPixels;
	/// <summary>
	/// indicates if this clump has been claimed by a laser dot
	/// </summary>
	public Boolean Claimed;
	/// <summary>
	/// the laser dot that claims this laser dot
	/// </summary>
	public LaserDot LaserDot;
	/// <summary>
	/// the distance of this laser dot from the expected position
	/// </summary>
	public double DistanceFromExpected;

         /*
         *  CONSTRUCTOR
         */
	/// <summary>
	/// the constructor for a new pixel clump
	/// </summary>
	public PixelClump(bool newClaimed)
	{
		PositionSum = new Point(0, 0);
		TotalWeight = 0;
		NumPixels = 0;
		AveragePosition = Vector2d.Zero;
		AverageWeight = 0;

		Claimed = newClaimed;
		LaserDot = null;
		DistanceFromExpected = 0;
	}

         /*
         *  CLAIM / UNCLAIM
         */
	/// <summary>
	/// claims the laser dot as belonging to the specified laser dot
	/// </summary>
	/// <param name="dot">the laser dot claiming this pixel clump</param>
	/// <param name="distance">the distance from the expected laser dot position to this clump's position</param>
	public void claim(LaserDot dot, double distance)
	{
		Claimed = true;
		LaserDot = dot;
		DistanceFromExpected = distance;
	}
	
	/// <summary>
	/// sets this clump to be unclaimed by any laser dots
	/// </summary>
	public void unclaim()
	{
		LaserDot.unclaim();
		Claimed = false;
		LaserDot = null;
		DistanceFromExpected = 0;
	}
}