Introduction
The dnnScanFree project (an Open Source project to enable the creation of free and inexpensive testing programs) was faced with the challenge of creating a method to indicate the student taking the test. Pre-printing the test forms with the student number will allow the grading program to grade the test and automatically update the student's record. This will provide significant time savings.
Naturally, bar codes were considered, and an example code was available on CodeProject. However, if the scan is not straight the bar codes will not be read correctly. There is a CodeProject article on de-skewing an image; however, it is resource intensive.
The dnnScanFree project decided to use a simple method: a number would be converted to a string of boxes printed on the page. The boxes would then be read using the same technology that is being used to grade the tests.
The program
Start the program, and select BinaryCode from the menu, then Create.
A popup box will allow you to enter a seven digit number.
The number will appear on the screen along with its binary representation and a block in the upper right-hand corner that will be used to detect the starting point when the scan of the image is read.
Selecting File, then Print, will allow you to print out the image and scan it
After scanning the image, it can be read by opening the image using File, then Open. Red boxes will be drawn around each recognition zone (based on the detected starting point). A popup box will indicate the number that is recognized.
Dealing with skew
It is rare for an image to scan or print completely straight. The following shows how the same image is detected when the image is skewed to the right:
If the image is skewed to the left, the code that detects the starting point realizes there is an error because it counts the black pixels in a 20 pixel square. If the count does not match, it assumes a left skew and subtracts pixels on the X axis.
int intPixels = CountBlackPixelsInRectangle(m_Bitmap, StartingPoint.X,
StartingPoint.Y, 20, 20);
if (intPixels < 120)
{
DrawARedRectangle(m_Bitmap, StartingPoint);
StartingPoint.X = StartingPoint.X - 25;
MessageBox.Show(String.Format("Scan is skewed. Only {0} pixels found " +
"at the starting point. StartingPoint will be reset to {1}",
intPixels.ToString(), StartingPoint.X.ToString()));
}
The image is still readable with the skew.
Fixing the skew
While the boxes can be read with a skew on the left hand side of the page, the skew gets worse on the right-hand side of the page. It was necessary to implement a method to de-skew the image. The CodeProject article on de-skewing an image did not work because its algorithm was confused by the image. It also does not work with all image types, and is significantly slower.
It did, however, provide a very useful RotateImage
method. This was very helpful because a Bitmap
normally can only be rotated in 90 degree increments using an enumeration. This project required a more granular rotation.
The process to de-skew the image is simple. FirstStartingPoint
and SecondStartingPoint
are detected, and those values along with the Bitmap
image are passed to the StraightenImage
method which straightens the image.
Point FirstStartingPoint = FindStartingPoint1(m_Bitmap, BoxSize);
if (FirstStartingPoint.IsEmpty)
{
MessageBox.Show("Scan is unreadable");
return;
}
Point SecondStartingPoint = FindStartingPoint2(m_Bitmap,
FirstStartingPoint, 2158, BoxSize);
if (SecondStartingPoint.IsEmpty)
{
MessageBox.Show("Scan is unreadable");
return;
}
m_Bitmap = StraightenImage(m_Bitmap, FirstStartingPoint, SecondStartingPoint, BoxSize);
The code for the StraightenImage
method is:
private Bitmap StraightenImage(Bitmap b, Point FirstStartingPoint,
Point SecondStartingPoint, int BoxSize)
{
int intCompletelyBlackSquare = (BoxSize * 85);
int intTmpPixels = CountBlackPixelsInRectangle(b, SecondStartingPoint, BoxSize, BoxSize);
if (intTmpPixels < (intCompletelyBlackSquare - (BoxSize * 2)))
{
int intPixelsOff = intCompletelyBlackSquare - intTmpPixels;
Point tmpStartingPoint = new Point(SecondStartingPoint.X,
SecondStartingPoint.Y + 10);
int intTmpPixels2 = CountBlackPixelsInRectangle(b, tmpStartingPoint,
BoxSize, BoxSize);
float fOfsetPercentage;
int intPixelSkew = intTmpPixels - intTmpPixels2;
if (intPixelSkew > 0)
{
fOfsetPercentage = ((float)intPixelsOff / (float)intCompletelyBlackSquare);
}
else
{
fOfsetPercentage = ((float)intPixelsOff /
(float)intCompletelyBlackSquare) * (float)-1;
}
b = RotateImage(b, fOfsetPercentage);
}
return b;
}
Reading the numbers
Creating the boxes is a straightforward process. A number is entered into the popup box and converted to a string of 0's and 1's.
private void createToolStripMenuItem_Click(object sender, EventArgs e)
{
m_Bitmap = new Bitmap(800, 600);
StartingPoint = new Point(10, 30);
DrawARectangle(m_Bitmap, StartingPoint, true);
string strNumber = "5234567";
strNumber = Microsoft.VisualBasic.Interaction.InputBox("Enter a seven digit number",
"Enter Number", strNumber, 100, 100);
string strBinaryNumber = ConvertToBinaryNumber(strNumber);
int intWidth = 50;
for (int i = 0; i <= 27; i++)
{
intWidth = intWidth + 10;
StartingPoint = new Point(intWidth, 50);
DrawARectangle(m_Bitmap, StartingPoint, (strBinaryNumber.Substring(i, 1) == "1"));
}
StartingPoint = new Point(50, 100);
DrawText(m_Bitmap, StartingPoint, String.Format("Number: {0}", strNumber));
}
The GetBinaryNumber
method is called by the ConvertToBinaryNumber
method to create the binary string.
private string GetBinaryNumber(string p)
{
string strBinaryNumber = "";
switch (p)
{
case "0":
strBinaryNumber = "0000";
break;
case "1":
strBinaryNumber = "0001";
break;
case "2":
strBinaryNumber = "0010";
break;
case "3":
strBinaryNumber = "0011";
break;
case "4":
strBinaryNumber = "0100";
break;
case "5":
strBinaryNumber = "0101";
break;
case "6":
strBinaryNumber = "0110";
break;
case "7":
strBinaryNumber = "0111";
break;
case "8":
strBinaryNumber = "1000";
break;
case "9":
strBinaryNumber = "1001";
break;
}
return strBinaryNumber;
}
When the image is read, the process is reversed.
string[] Answers = ReadBlocks(StartingPoint);
string strNumber = ConvertAnswersToBinary(Answers);
MessageBox.Show(strNumber);
Why not just use bar codes?
Barcodes are a great technology, however, they were designed to be read with a special bar code reader. They were also designed to be printed clearly, not read from possibly reprinted, faxed, and scanned images. While they may be readable under these conditions, it is an attempt to make a technology work under conditions it was not designed for.
This project presents an alternative technology that is designed to be read under adverse conditions.