Click here to Skip to main content
15,892,809 members
Articles / Programming Languages / C#

C# - Optical Marks Recognition (OMR) Engine 1.0

Rate me:
Please Sign up or sign in to vote.
4.77/5 (34 votes)
9 Mar 2015CPOL6 min read 297.9K   25.4K   107  
An API that reads OMR sheets from camera taken/ scanner scanned images.
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.Drawing.Imaging;
using AForge;
using AForge.Imaging;
using AForge.Imaging.Filters;
using AForge.Math;
using AForge.Math.Geometry;
using System.IO;
using System.Xml;
using OMR;

namespace OMR
{
    public class OpticalReader
    {   
        TimeSpan ts = new TimeSpan();
        /// <summary>
        /// Extracts Images from smallSize CameraImage, No feed Back given during process
        /// </summary>
        /// <param name="SmallCameraImage"></param>
        /// <param name="OMRSpecsSheetAddress"></param>
        /// <returns>Formated, Right sized OMR Sheet</returns>
        public Bitmap ExtractOMRSheet(Bitmap SmallCameraImage, string OMRSpecsSheetAddress, XML.OMREnums.OMRSheet sheet)
        {
            Panel p = new Panel();
            TextBox t = new TextBox();
            t.Text = "";
            return ExtractOMRSheet(SmallCameraImage, 0, 0, OMRSpecsSheetAddress, ref p, ref t, false,sheet);
        }
        /// <summary>
        /// Extracts image and gives "In-Process" feebback on referenced panel and text box
        /// </summary>
        /// <param name="SmallCameraImage">any small image, as smaller as 3MP is recomended and enough clear. Bigger images take alot of time to be processed</param>
        /// <param name="OMRSpecsSheetAddress">XML sheet specification file</param>
        /// <param name="panel1"></param>
        /// <param name="textBox1"></param>
        /// <returns></returns>
        
        public Bitmap ExtractOMRSheet(Bitmap SmallCameraImage, string OMRSpecsSheetAddress,ref Panel panel1,ref TextBox textBox1, XML.OMREnums.OMRSheet sheet)
        {
            return ExtractOMRSheet(SmallCameraImage, 0, 0, OMRSpecsSheetAddress, ref panel1, ref textBox1, true,sheet);
        }
        private Bitmap ExtractOMRSheet(Bitmap basicImage, int fillint, int contint, string OMRSpecsSheetAddress, ref Panel panel1, ref TextBox textBox1, bool giveFB, XML.OMREnums.OMRSheet sheet)
        {
            ts = new TimeSpan(DateTime.Now.Ticks);
            System.Drawing.Image flattened = (System.Drawing.Image)flatten(basicImage, fillint, contint);
            if (giveFB)
            {
                showTimeStamp("Flatting Started", ref textBox1);
                panel1.BackgroundImage = flattened;
                showTimeStamp("Flattened",ref textBox1);
                panel1.Invalidate();
                Application.DoEvents();
                showTimeStamp("OMR Extraction Started",ref textBox1);
            }
            return ExtractPaperFromFlattened(new Bitmap(flattened), basicImage, 3, fillint, contint, OMRSpecsSheetAddress,ref textBox1, ref panel1, giveFB,sheet);
        }
        /// <summary>
        /// Detects, wrapps and crops out OMR sheet from flattened camera/scanner image.
        /// Flattened image is got by using method,  private Bitmap flatten(Bitmap bmp, int fillint, int contint);         
        /// </summary>
        /// <param name="bitmap">Bitmap image to process</param>
        /// <param name="basicImage">Backup image in case extraction fails</param>
        /// <param name="minBlobWidHei">Pre-configured variable, to be queried from XML reader</param>
        /// <param name="fillint">Pre-configured int, to be queried from XML reader</param>
        /// <param name="contint">Pre-configured int, to be queried from XML reader</param>
        /// <param name="OMRSheets">Sheets XML File Address</param>
        /// <param name="tb">Textbox to give in'process details on</param>
        /// <param name="panel1">Panel to draw in-process changes on.</param>
        /// <param name="giveFB">True when In- Process Feedback is required.</param>
        /// <param name="sheet">Type of sheet from OMREnums</param>
        /// <returns>Cropped OMR sheet (if detected) from camera/scanner image.</returns>
        private Bitmap ExtractPaperFromFlattened(Bitmap bitmap, Bitmap basicImage, int minBlobWidHei, int fillint, int contint, string OMRSheets, ref TextBox tb, ref Panel panel1, bool giveFB, XML.OMREnums.OMRSheet sheet)
        {

            // lock image, Bitmap itself takes much time to be processed
            BitmapData bitmapData = bitmap.LockBits(
                new Rectangle(0, 0, bitmap.Width, bitmap.Height),
                ImageLockMode.ReadWrite, bitmap.PixelFormat);

            // step 2 - locating objects
            BlobCounter blobCounter = new BlobCounter();

            blobCounter.FilterBlobs = true;
            blobCounter.MinHeight = minBlobWidHei;  // both these variables have to be given when calling the
            blobCounter.MinWidth = minBlobWidHei;   // method, the can also be queried from the XML reader using OMREnums

            blobCounter.ProcessImage(bitmapData);
            Blob[] blobs = blobCounter.GetObjectsInformation();
            bitmap.UnlockBits(bitmapData);

            Graphics g = Graphics.FromImage(bitmap);
            Pen yellowPen = new Pen(Color.Yellow, 2);   // create pen in case image extraction failes and we need to preview the
                                                        //blobs that were detected

            Rectangle[] rects = blobCounter.GetObjectsRectangles();
            Blob[] blobs2 = blobCounter.GetObjects(bitmap, false);

            //Detection of paper lies within the presence of crossmark printed on the corneres of printed sheet.
            // First, detect left edge.
            System.Drawing.Image compImg = System.Drawing.Image.FromFile("lc.jpg"); 
            // lc.jpg = Mirrored image sample as located on the corner of printed sheet
            UnmanagedImage compUMImg = UnmanagedImage.FromManagedImage((Bitmap)compImg);
            // this helps filtering out much smaller and much larger blobs depending upon the size of image.
            // can be queried from XML Reader
            double minbr = XML.OMRSheetReader.getProcessVariableD(OMRSheets, sheet, XML.OMREnums.OMRImageProcessVariables.MinBlobRatio);
            double maxbr = XML.OMRSheetReader.getProcessVariableD(OMRSheets, sheet, XML.OMREnums.OMRImageProcessVariables.MaxBlobRatio);
            
            List<IntPoint> quad = new List<IntPoint>(); // Store sheet corner locations (if anyone is detected )

            if (giveFB) showTimeStamp("Left edge detection started.", ref tb);
            try
            {
                foreach (Blob blob in blobs2)
                {
                    if (
                        ((double)blob.Area) / ((double)bitmap.Width * bitmap.Height) > minbr &&
                        ((double)blob.Area) / ((double)bitmap.Width * bitmap.Height) < maxbr &&
                            blob.Rectangle.X < (bitmap.Width) / 4) // filters oout very small or very larg blobs
                    {
                        if ((double)blob.Rectangle.Width / blob.Rectangle.Height < 1.4 &&
                            (double)blob.Rectangle.Width / blob.Rectangle.Height > .6) // filters out blobs having insanely wrong aspect ratio
                        {
                            compUMImg = UnmanagedImage.FromManagedImage(ImageUtilities.ResizeImage(compImg, blob.Rectangle.Width, blob.Rectangle.Height));
                            if (isSame(blob.Image, compUMImg))
                            {
                                g.DrawRectangle(yellowPen, blob.Rectangle);
                                quad.Add(new IntPoint((int)blob.CenterOfGravity.X, (int)blob.CenterOfGravity.Y));
                            }
                        }
                    }
                }
            }
            catch (ArgumentException) { MessageBox.Show("No Blobs"); }
            if (giveFB) showTimeStamp("Left edge detection finished.", ref tb);
            try
            { // Sort out the list in right sequence, UpperLeft,LowerLeft,LowerRight,upperRight
                if (quad[0].Y > quad[1].Y)
                {
                    IntPoint tp = quad[0];
                    quad[0] = quad[1];
                    quad[1] = tp;
                }
            }
            catch
            {
            }
            compImg = System.Drawing.Image.FromFile("rc.jpg");
            compUMImg = UnmanagedImage.FromManagedImage((Bitmap)compImg);
            if (giveFB) showTimeStamp("Right edge detection started.", ref tb); // jusst like left edge detection
            try
            {
                foreach (Blob blob in blobs2)
                {
                    if (
                        ((double)blob.Area) / ((double)bitmap.Width * bitmap.Height) > minbr &&
                        ((double)blob.Area) / ((double)bitmap.Width * bitmap.Height) < maxbr &&
                        blob.Rectangle.X > (bitmap.Width * 3) / 4)
                    {
                        if ((double)blob.Rectangle.Width / blob.Rectangle.Height < 1.4 &&
                        (double)blob.Rectangle.Width / blob.Rectangle.Height > .6)
                        {
                            compUMImg = UnmanagedImage.FromManagedImage(ImageUtilities.ResizeImage(compImg, blob.Rectangle.Width, blob.Rectangle.Height));
                            if (isSame(blob.Image, compUMImg))
                            {
                                g.DrawRectangle(yellowPen, blob.Rectangle);
                                quad.Add(new IntPoint((int)blob.CenterOfGravity.X, (int)blob.CenterOfGravity.Y));
                            }
                        }
                    }
                }
            }
            catch (ArgumentException) { MessageBox.Show("No Blobs"); }
            if (giveFB) showTimeStamp("Right edge detection finished.", ref tb);
            try
            {
                if (quad[2].Y < quad[3].Y)
                {
                    IntPoint tp = quad[2];
                    quad[2] = quad[3];
                    quad[3] = tp;
                }
            }
            catch
            {

            }

            yellowPen.Dispose();
            g.Dispose();
            //Again, filter out if wrong blobs pretended to our blobs.
            if (quad.Count == 4)
            {
                if (((double)quad[1].Y - (double)quad[0].Y) / ((double)quad[2].Y - (double)quad[3].Y) < .75 ||
                    ((double)quad[1].Y - (double)quad[0].Y) / ((double)quad[2].Y - (double)quad[3].Y) > 1.25)
                    quad.Clear(); // clear if, both edges have insanely wrong lengths
                else if (quad[0].X > bitmap.Width / 2 || quad[1].X > bitmap.Width / 2 || quad[2].X < bitmap.Width / 2 || quad[3].X < bitmap.Width / 2)
                    quad.Clear(); // clear if, sides appear to be "wrong sided"
            }
            if (quad.Count != 4)// sheet not detected, reccurrsive call.
            {
                if (contint <= 60)//try altering the contrast correction on both sides of numberline
                {
                    if (contint >= 0)
                    {
                        contint += 5;
                        contint *= -1;
                        return ExtractOMRSheet(basicImage, fillint, contint, OMRSheets, ref panel1, ref tb,giveFB,sheet);
                    }
                    else
                    {
                        contint *= -1;
                        contint += 10;
                        return ExtractOMRSheet(basicImage, fillint, contint, OMRSheets, ref panel1, ref tb, giveFB,sheet);
                    }
                }
                else // contrast correction yeilded no result
                {
                    MessageBox.Show("Extraction Failed.");
                    return basicImage;
                }
            }
            else // sheet found
            {
                IntPoint tp2 = quad[3];
                quad[3] = quad[1];
                quad[1] = tp2;
                //sort the edges for wrap operation
                QuadrilateralTransformation wrap = new QuadrilateralTransformation(quad);
                wrap.UseInterpolation = false; //perspective wrap only, no binary.
                Rectangle sr = XML.OMRSheetReader.GetSheetPropertyLocation(OMRSheets, sheet, XML.OMREnums.OMRProperty.SheetSize);
                wrap.AutomaticSizeCalculaton = false;
                wrap.NewWidth = sr.Width;
                wrap.NewHeight = sr.Height;
                wrap.Apply(basicImage).Save("LastImg.jpg", ImageFormat.Jpeg); // creat file backup for future use.
                return wrap.Apply(basicImage); // wrap
            }
        }
        private Bitmap flatten(Bitmap bmp, int fillint, int contint)
        {
            // step 1 - turn background to black
            ColorFiltering colorFilter = new ColorFiltering();

            colorFilter.Red = new IntRange(0, fillint);
            colorFilter.Green = new IntRange(0, fillint);
            colorFilter.Blue = new IntRange(0, fillint);
            colorFilter.FillOutsideRange = false;

            colorFilter.ApplyInPlace(bmp);
            AForge.Imaging.Filters.ContrastCorrection Contrast = new ContrastCorrection(contint);
            AForge.Imaging.Filters.Invert invert = new Invert();
            AForge.Imaging.Filters.ExtractChannel extract_channel = new ExtractChannel(0);
            AForge.Imaging.Filters.Threshold thresh_hold = new Threshold(44);
            bmp = invert.Apply(thresh_hold.Apply(extract_channel.Apply(Contrast.Apply(bmp))));

            return bmp;
        }
        private bool isSame(UnmanagedImage img1, UnmanagedImage img2)
        {
            int count = 0, tcount = img2.Width * img2.Height;
            for (int y = 0; y < img1.Height; y++)
                for (int x = 0; x < img1.Width; x++)
                {
                    Color c1 = img1.GetPixel(x, y), c2 = img2.GetPixel(x, y);
                    if ((c1.R + c1.G + c1.B) / 3 > (c2.R + c2.G + c2.B) / 3 - 10 &&
                        (c1.R + c1.G + c1.B) / 3 < (c2.R + c2.G + c2.B) / 3 + 10)
                        count++;
                }
            return (count * 100) / tcount >= 54;
        }
        private void showTimeStamp(string str,ref TextBox textBox1)
        {
            textBox1.AppendText(str + ": " + (new TimeSpan(DateTime.Now.Ticks) - ts).TotalSeconds + "\r\n");
            ts = new TimeSpan(DateTime.Now.Ticks);
        }

        public int getRegNumOfSheet(System.Drawing.Image image, XML.OMREnums.OMRSheet sheet, string OMRSheetFile,bool readInvalidRegNum)
        {
            Rectangle[] Blocks = new Rectangle[]
            {
                XML.OMRSheetReader.GetSheetPropertyLocation(OMRSheetFile, sheet, XML.OMREnums.OMRProperty.RegNumBlock),
            };

            List<Bitmap[]> bmps = new List<Bitmap[]>();
            bmps.Add(SliceOMarkBlock(image, Blocks[0], 3));

            int regNum = 0, multiplier = 100;
            foreach (Bitmap[] blk in bmps)
            {
                foreach (Bitmap line in blk)
                {
                    int num = rateSlice(line, 10) - 1;
                    if (num < 1 &&! readInvalidRegNum)
                        throw new Exception("Invalid Reg. No.");
                    else
                    {
                        if (num < 0) num = 0;
                        regNum += num * multiplier;
                        multiplier /= 10;
                    }
                }
            }

            return regNum;
        }
        /// <summary>
        /// Reads all the selected options on paper in one call
        /// </summary>
        /// <param name="image">Exctracted Sheet.</param>
        /// <param name="sheet">Type of printed sheet</param>
        /// <param name="OMRSheetFile">XML sheet address</param>
        /// <returns></returns>
        public List<List<int>> getScoreOfSheet(System.Drawing.Image image, XML.OMREnums.OMRSheet sheet,string OMRSheetFile)
        {
            List<List<int>> scores = new List<List<int>>();
            //number of blocks depend upon type of sheet selected
            Rectangle[] Blocks = new Rectangle[
                XML.OMRSheetReader.GetSheetPropertyInt(OMRSheetFile, sheet, XML.OMREnums.OMRProperty.NumOfBlocks)
                ];
            // Read block location from XML file for selected sheet
            for (int i = 1; i <= Blocks.Length; i++)
            {
                Blocks[i - 1] = XML.OMRSheetReader.GetSheetPropertyLocation(OMRSheetFile, sheet, (XML.OMREnums.OMRProperty)i);
            }

            // slice the blocks into lines inside them and record as bitmap
            List<Bitmap[]> bmps = new List<Bitmap[]>();
            for (int i = 0; i < 4; i++)
            {
                bmps.Add(SliceOMarkBlock(image, Blocks[i], 10));
                scores.Add(new List<int>());
            }

            int bn = 0;
            // read selected option of sliced line
            foreach (Bitmap[] blk in bmps)
            {
                foreach (Bitmap line in blk)
                {
                    scores[bn].Add(rateSlice(line, 5));
                }
                bn++;
            }

            return scores;
        }
        /// <summary>
        /// returns ant int representation of selected option
        /// </summary>
        /// <param name="slice">Sliced sinlge line in choices block</param>
        /// <param name="OMCount"></param>
        /// <returns></returns>
        private int rateSlice(Bitmap slice, int OMCount)
        {
            Rectangle[] cropRects = new Rectangle[OMCount];
            Bitmap[] marks = new Bitmap[OMCount];
            //sub-devide line into option (horizontal only)
            for (int i = 0; i < OMCount; i++)
            {
                cropRects[i] = new Rectangle(i * slice.Width / OMCount, 0, slice.Width / OMCount, slice.Height);
            }

            int crsr = 0;
            foreach (Rectangle cropRect in cropRects)
            {
                Bitmap target = new Bitmap(cropRect.Width, cropRect.Height);

                using (Graphics g = Graphics.FromImage(target))
                {
                    g.DrawImage(slice, new Rectangle(0, 0, target.Width, target.Height),
                                     cropRect,
                                     GraphicsUnit.Pixel);
                }
                marks[crsr] = target;
                crsr++;
            }
            List<long> fullInks = new List<long>();

            //get marking level
            foreach (Bitmap mark in marks)
            {
                fullInks.Add(InkDarkness(mark));
            }
            int indofMx = -1, indofMn = -1;
            long maxD = 0;

            //get maximum ink level
            for (int i = 0; i < OMCount; i++)
            {
                if (fullInks[i] > maxD)
                {
                    maxD = fullInks[i];
                    indofMx = i;
                }
            }
            bool parallelExist = false, spe = false, tpe = false, fpe = false;
            for (int i = 0; i < OMCount; i++)
            {
                if (i != indofMx)
                {
                    if ((double)fullInks[indofMx] / fullInks[i] <= 2) //both ink levels are nearly the same
                    {
                        if (tpe) fpe = true;
                        if (spe) tpe = true;
                        if (parallelExist) spe = true;
                        parallelExist = true;
                    }
                }
            }
            int negScore = parallelExist ? -1 : 0;
            negScore = spe ? -2 : negScore;
            negScore = tpe ? -3 : negScore;
            negScore = fpe ? -4 : negScore;

            if (!parallelExist)
                return indofMx + 1;
            //check if multiple options were selected
            bool atleastOneUnfilled = false;
            for (int i = 0; i < OMCount; i++)
            {
                if (i != indofMx)
                {
                    if ((double)fullInks[indofMx] / fullInks[i] >= 3)
                        atleastOneUnfilled = true;
                }
            }
            if (atleastOneUnfilled)
                return negScore;


            return 0;
        }
        private long InkDarkness(Bitmap OMark)
        {
            int darkestC = 255, lightestC = 0;

            UnmanagedImage mark = UnmanagedImage.FromManagedImage(OMark);
            for (int y = 0; y < OMark.Height; y++)
                for (int x = 0; x < OMark.Width; x++)
                {
                    Color c = mark.GetPixel(x, y);
                    if (((c.R + c.G + c.B) / 3) > lightestC)
                    {
                        lightestC = ((c.R + c.G + c.B) / 3);
                    }
                    if (((c.R + c.G + c.B) / 3) < darkestC)
                    {
                        darkestC = ((c.R + c.G + c.B) / 3);
                    }
                }
            int dc = 0;
            for (int y = 0; y < OMark.Height; y++)
                for (int x = 0; x < OMark.Width; x++)
                {
                    Color c = mark.GetPixel(x, y);

                    if (((c.R + c.G + c.B) / 3) < (lightestC + darkestC) / 2)
                    { dc += 255; }
                }
            return dc;
        }
        private Bitmap[] SliceOMarkBlock(System.Drawing.Image fullSheet, Rectangle slicer, int slices)
        {
            List<Rectangle> cropRects = new List<Rectangle>();
            Bitmap[] bmps = new Bitmap[slices];
            for (int i = 0; i < slices; i++)
            {
                cropRects.Add(new Rectangle(slicer.X, slicer.Y + (slicer.Height / slices) * i, slicer.Width, slicer.Height / slices));
            }
            Bitmap src = (Bitmap)fullSheet;
            int crsr = 0;
            foreach (Rectangle cropRect in cropRects)
            {
                Bitmap target = new Bitmap(cropRect.Width, cropRect.Height);

                using (Graphics g = Graphics.FromImage(target))
                {
                    g.DrawImage(src, new Rectangle(0, 0, target.Width, target.Height),
                                     cropRect,
                                     GraphicsUnit.Pixel);
                }
                bmps[crsr] = target;
                crsr++;
            }
            return bmps;
            throw new Exception("Couldn't slice");
        }

        
    }
}

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Engineer techCREATIONS
Pakistan Pakistan
Developer, Programmer, Beta Tester; technically, i'm none of these. I'm a mechanical engineer, programming is my passion, my hobby and my amateur non profit profession. I program when ii need and innovate whenever, wherever i want.

Learned:
C#

Mixed:
C#+Applied Mathematicss-Robotics+C++

Developed:
C# OMR Reader
Monopoly (Urdu language)
HybridAutomation Framework
SMS Bomber (Windows Mobile 6 Professional)
Hard disk watch tower
Farmville Super Clicker
Games Profile selector
Windows mobile salat reminder
Windows mobile SMS Pole Host
and alot of other small apps

Comments and Discussions