//Using statements omitted
///
/// 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 reference brightness thresholding images to compare video frames against.
///
public class CalibrationData
{
/*
* CONSTANTS
*/
///
/// the default pixel brightness threshold
///
public const int DEFAULT_THRESHOLD = 500;
///
/// the padding to add to the pixel brightness threshold calculation
///
public const int THRESHOLD_PADDING = 20;
///
/// the number of K vals needed to describe the camera lens
///
public const int NUM_K_VALS = 2;
/*
* CLASS PROPERTIES
*/
///
/// the camera device name
///
protected string cameraDeviceName;
///
/// the capture device object
///
protected CaptureDevice cameraDevice;
///
/// the calibration form object
///
protected CalibrationForm calibrationForm;
///
/// the video monitor on the calibration form
///
protected PictureBox videoMonitor;
///
/// the last captured frame from the video
/// for display to the video monitor
///
protected Bitmap displayFrame;
///
/// the full uncropped resolution of the video feed
///
protected WinDraw.Size fullResolution;
///
/// cropping rectangle
///
protected WinDraw.Rectangle cropRectangle;
//lens rectification
//K lens parameters
protected bool KValuesFound;
protected DoubleVector Kvector;
//normalization matrices
protected Matrix3 tNormLens;
protected Matrix3 tNormLensInv;
///
/// point correction matrix to adjust for cropping
///
protected Matrix3 tPointCorrection;
///
/// the laser dot tracker object
///
protected LaserDotTracking laserTracker;
///
/// the brightness threshold level, above
/// which pixels are considered to be a laser dot
///
protected int brightnessThreshold;
///
/// the number of lasers being tracked
///
protected int numLasers;
///
/// the list of laser dot objects
///
protected List lasers;
//user point manipulation data omitted...
///
/// constructor for a new video calibration object
///
/// the name of the capture device
/// the resolution of the video feed
/// the form object for displaying the video feed
/// the new capture device object
/// the calibration form object
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
*/
///
/// should be called every CalibrationForm.UPDATES_PER_SECOND
/// by the calibration form object
///
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());
}
}
}
///
/// draw laser positions and crop box onto frame
///
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;
}
}
///
/// event listener to handle a new video frame from the camera
///
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();
}
///
/// stops video feed processsing
///
public void stopAll()
{
if (cameraDevice != null)
{
cameraDevice.NewFrame -= new CameraEventHandler(ProcessFrame);
cameraDevice.Stop();
}
laserTracker.unload();
}
///
/// rectififies the identified laser dot positions
/// from video raster space to NDC application space
///
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
*/
///
/// crop window to corners of screen area
/// (overriden by planar calibration)
///
public virtual void AutoCrop()
{
//crop the brightnessThreshold 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);
}
///
/// gets / sets the current video stream cropping rectangle
///
public WinDraw.Rectangle CroppingRectangle
{
get { return cropRectangle; }
set { cropRectangle = value; updateCrop(); }
}
///
/// updates the cropped display
///
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);
}
///
/// sets the last captured frame to be the brightnessThreshold image
///
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);
}
}
///
/// 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
///
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
*/
///
/// updates the normalization matrix based on the video feed's width and height
///
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();
}
///
/// 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)
///
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));
}
///
/// 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)
///
public double distanceToLine(Vector2d lineStart, Vector2d lineDir,
double lengthSqr, Vector2d testPoint)
{
return Math.Sqrt(distanceSquaredToLine(lineStart, lineDir, lengthSqr, testPoint));
}
///
/// 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
///
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;
}
///
/// determines the error
///
/// the K vector that defines the lens distortion
/// the iteration count
/// the total number of iterations
/// the residual error
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;
}
///
/// determine appropriate K values to adjust for lens distortion
///
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;
}
}