0

I am currently working with an opensource piece of Optical Mark Recognition software that uses Ghostscript and AForge for graphics processing. The software is basically designed to read when a student has marked a bubble on a test form, e.g., by using a premade template to analyze areas of the image for "blobs." It then outputs all answers to a text file.

Here is the sheet I am using to be analyzed: Sheet.

The following code is the main analyzer of the sheet:

public OmrPageOutput ApplyTemplate(OmrTemplate template, ScannedImage image)
    {
        // Image ready for scan
        if (!image.IsReadyForScan)
        {
            if (!image.IsScannable)
                image.Analyze();
            image.PrepareProcessing();
        }

        // Page output
        OmrPageOutput retVal = new OmrPageOutput()
        {
            Id = image.TemplateName + DateTime.Now.ToString("yyyyMMddHHmmss"), 
            TemplateId = image.TemplateName,
            Parameters = image.Parameters, 
            StartTime = DateTime.Now,
            Template = template
        };

        // Save directory for output images 
        string saveDirectory = String.Empty;
        var parmStr = new StringBuilder();
        if (this.SaveIntermediaryImages)
        {
            if(image.Parameters != null)
                foreach (var pv in image.Parameters)
                    parmStr.AppendFormat("{0}.", pv);
            retVal.RefImages = new List<string>()
            {
                String.Format("{0}-{1}-init.bmp", retVal.Id, parmStr),
                String.Format("{0}-{1}-tx.bmp", retVal.Id, parmStr),
                String.Format("{0}-{1}-fields.bmp", retVal.Id, parmStr),
                String.Format("{0}-{1}-gs.bmp", retVal.Id, parmStr),
                String.Format("{0}-{1}-bw.bmp", retVal.Id, parmStr),
                String.Format("{0}-{1}-inv.bmp", retVal.Id, parmStr)
            };

            saveDirectory = Path.Combine(Path.GetDirectoryName(Assembly.GetEntryAssembly().Location), "imgproc");
            Console.WriteLine(Path.GetDirectoryName(Assembly.GetEntryAssembly().Location).ToString());

            if (!Directory.Exists(saveDirectory))
                Directory.CreateDirectory(saveDirectory);
            image.Image.Save(Path.Combine(saveDirectory, string.Format("{0}-{1}-init.bmp", DateTime.Now.ToString("yyyyMMddHHmmss"), parmStr)));

        }

        // First, we want to get the image from the scanned image and translate it to the original
        // position in the template
        Bitmap bmp = null;
        try
        {
            bmp = new Bitmap((int)template.BottomRight.X, (int)template.BottomRight.Y, System.Drawing.Imaging.PixelFormat.Format24bppRgb);

            // Scale
            float width = template.TopRight.X - template.TopLeft.X,
                height = template.BottomLeft.Y - template.TopLeft.Y;


            // Translate to original
            using (Graphics g = Graphics.FromImage(bmp))
            {
                ResizeBicubic bc = new ResizeBicubic((int)width, (int)height);
                g.DrawImage(bc.Apply((Bitmap)image.Image), template.TopLeft.X, template.TopLeft.Y);
            }

            if (this.SaveIntermediaryImages)
                bmp.Save(Path.Combine(saveDirectory, string.Format("{0}-{1}-tx.bmp", DateTime.Now.ToString("yyyyMMddHHmmss"), parmStr)));


            // Now try to do hit from the template
            if (this.SaveIntermediaryImages)
            {
                using (var tbmp = bmp.Clone() as Bitmap)
                {
                    using (Graphics g = Graphics.FromImage(tbmp))
                    {
                        foreach (var field in template.Fields)
                        {
                            g.DrawRectangle(Pens.Black, field.TopLeft.X, field.TopLeft.Y, field.TopRight.X - field.TopLeft.X, field.BottomLeft.Y - field.TopLeft.Y);
                            g.DrawString(field.Id, SystemFonts.CaptionFont, Brushes.Black, field.TopLeft);
                        }
                    }

                    tbmp.Save(Path.Combine(saveDirectory, string.Format("{0}-{1}-fields.bmp", DateTime.Now.ToString("yyyyMMddHHmmss"), parmStr)));
                }
            }


            // Now convert to Grayscale
            GrayscaleY grayFilter = new GrayscaleY();
            var gray = grayFilter.Apply(bmp);
            bmp.Dispose();
            bmp = gray;

            if (this.SaveIntermediaryImages)
                bmp.Save(Path.Combine(saveDirectory, string.Format("{0}-{1}-gs.bmp", DateTime.Now.ToString("yyyyMMddHHmmss"), parmStr)));

            // Prepare answers
            Dictionary<OmrQuestionField, OmrOutputData> hitFields = new Dictionary<OmrQuestionField, OmrOutputData>();
            BarcodeReader barScan = new BarcodeReader();
            barScan.Options.UseCode39ExtendedMode = true;
            barScan.Options.UseCode39RelaxedExtendedMode = true;
            barScan.Options.TryHarder = true;
            barScan.TryInverted = true;
            barScan.Options.PureBarcode = false;
            barScan.AutoRotate = true;

            foreach (var itm in template.Fields.Where(o => o is OmrBarcodeField))
            {
                PointF position = itm.TopLeft;
                SizeF size = new SizeF(itm.TopRight.X - itm.TopLeft.X, itm.BottomLeft.Y - itm.TopLeft.Y);
                using (var areaOfInterest = new Crop(new Rectangle((int)position.X, (int)position.Y, (int)size.Width, (int)size.Height)).Apply(bmp))
                {
                    // Scan the barcode
                    var result = barScan.Decode(areaOfInterest);


                    if (result != null)
                        hitFields.Add(itm, new OmrBarcodeData()
                        {
                            BarcodeData = result.Text,
                            Format = result.BarcodeFormat,
                            Id = itm.Id,
                            TopLeft = new PointF(result.ResultPoints[0].X + position.X, result.ResultPoints[0].Y + position.Y),
                            BottomRight = new PointF(result.ResultPoints[1].X + position.X, result.ResultPoints[0].Y + position.Y + 10)
                        });
                }

            }

            // Now binarize
            Threshold binaryThreshold = new Threshold(template.ScanThreshold);
            binaryThreshold.ApplyInPlace(bmp);

            if (this.SaveIntermediaryImages)
                bmp.Save(Path.Combine(saveDirectory, string.Format("{0}-{1}-bw.bmp", DateTime.Now.ToString("yyyyMMddHHmmss"), parmStr)));

            // Set return parameters
            String tAnalyzeFile = Path.Combine(Path.GetTempPath(), Path.GetTempFileName());
            bmp.Save(tAnalyzeFile, System.Drawing.Imaging.ImageFormat.Jpeg);
            retVal.AnalyzedImage = tAnalyzeFile;
            retVal.BottomRight = new PointF(bmp.Width, bmp.Height);

            // Now Invert 
            Invert invertFiter = new Invert();
            invertFiter.ApplyInPlace(bmp);

            if (this.SaveIntermediaryImages)
                bmp.Save(Path.Combine(saveDirectory, string.Format("{0}-{1}-inv.bmp", DateTime.Now.ToString("yyyyMMddHHmmss"), parmStr)));


            // Crop out areas of interest
            List<KeyValuePair<OmrQuestionField, Bitmap>> areasOfInterest = new List<KeyValuePair<OmrQuestionField, Bitmap>>();
            foreach (var itm in template.Fields.Where(o => o is OmrBubbleField))
            {
                PointF position = itm.TopLeft;
                SizeF size = new SizeF(itm.TopRight.X - itm.TopLeft.X, itm.BottomLeft.Y - itm.TopLeft.Y);
                areasOfInterest.Add(new KeyValuePair<OmrQuestionField, Bitmap>(
                    itm,
                    new Crop(new Rectangle((int)position.X, (int)position.Y, (int)size.Width, (int)size.Height)).Apply(bmp))
                    );
            }

            // Queue analysis
            WaitThreadPool wtp = new WaitThreadPool();
            Object syncLock = new object();

            foreach (var itm in areasOfInterest)
            {
                wtp.QueueUserWorkItem(img =>
                {
                    var parm = (KeyValuePair<OmrQuestionField, Bitmap>)itm;

                    try
                    {
                        var areaOfInterest = parm.Value;
                        var field = parm.Key;

                        BlobCounter blobCounter = new BlobCounter();
                        blobCounter.FilterBlobs = true;

                        // Check for circles
                        blobCounter.ProcessImage(areaOfInterest);
                        Blob[] blobs = blobCounter.GetObjectsInformation();
                        var blob = blobs.FirstOrDefault(o => o.Area == blobs.Max(b => b.Area));
                        if (blob != null)
                        {
                            //var area = new AForge.Imaging.ImageStatistics(blob).PixelsCountWithoutBlack;
                            if (blob.Area < 30) 
                                return;
                            var bubbleField = field as OmrBubbleField;
                            lock (syncLock)
                                hitFields.Add(field, new OmrBubbleData()
                                {
                                    Id = field.Id,
                                    Key = bubbleField.Question,
                                    Value = bubbleField.Value,
                                    TopLeft = new PointF(blob.Rectangle.X + field.TopLeft.X, blob.Rectangle.Y + field.TopLeft.Y),
                                    BottomRight = new PointF(blob.Rectangle.X + blob.Rectangle.Width + field.TopLeft.X, blob.Rectangle.Y + blob.Rectangle.Height + field.TopLeft.Y),
                                    BlobArea = blob.Area
                                });
                        }
                    }
                    catch (Exception e) {
                        Trace.TraceError(e.ToString());
                    }
                    finally
                    {
                        parm.Value.Dispose();
                    }
                }, itm);
            }

            wtp.WaitOne();

            // Organize the response 
            foreach(var res in hitFields)
            {
                if (String.IsNullOrEmpty(res.Key.AnswerRowGroup))
                {
                    this.AddAnswerToOutputCollection(retVal, res);
                }
                else
                {
                    // Rows of data
                    OmrRowData rowGroup = retVal.Details.Find(o => o.Id == res.Key.AnswerRowGroup) as OmrRowData;
                    if(rowGroup == null)
                    {
                        rowGroup = new OmrRowData()
                        {
                            Id = res.Key.AnswerRowGroup
                        };
                        retVal.Details.Add(rowGroup);
                    }

                    this.AddAnswerToOutputCollection(rowGroup, res);
                }
            }
            // Remove temporary images
            //foreach (var f in retVal.RefImages)
            //    File.Delete(Path.Combine(saveDirectory, f));

            hitFieldsCopy = hitFields;

            // Outcome is success
            retVal.Outcome = OmrScanOutcome.Success;
        }
        catch(Exception e)
        {
            retVal.Outcome = OmrScanOutcome.Failure;
            retVal.ErrorMessage = e.Message;
            Trace.TraceError(e.ToString());
        }
        finally
        {
            retVal.StopTime = DateTime.Now;
            bmp.Dispose();
        }

        //Based off values from hitFieldsCopy
        //This bit of code doesn't seem to work properly -- created by Simon Ward
        List<OmrQuestionField> questionNumber = hitFieldsCopy.Keys.ToList();
        List<OmrOutputData> questionAnswer = hitFieldsCopy.Values.ToList();
        Array questionsNumberArray = questionNumber.ToArray();
        Array questionAnswerArray = questionAnswer.ToArray();

        string answerString = "";
        for(int i = 0; i < questionsNumberArray.Length; i++)
        {
            answerString += ("Question #" + questionsNumberArray.GetValue(i).ToString() + "   ");
            answerString += (questionAnswerArray.GetValue(i).ToString() + "\n");
        }

        //Console.WriteLine(answerString);
        return retVal;
    }

    /// <summary>
    /// Add an answer to an output collection
    /// </summary>
    /// <param name="retVal"></param>
    /// <param name="answerPair"></param>
    private void AddAnswerToOutputCollection(OmrOutputDataCollection collection, KeyValuePair<OmrQuestionField, OmrOutputData> answerPair )
    {
        // Now add answer
        if (answerPair.Key is OmrBubbleField)
        {
            var bubbleField = answerPair.Key as OmrBubbleField;
            switch (bubbleField.Behavior)
            {
                case BubbleBehaviorType.One:
                    if (!collection.AlreadyAnswered(answerPair.Value))
                        collection.Details.Add(answerPair.Value);
                    break;
                case BubbleBehaviorType.Multi:
                    collection.Details.Add(answerPair.Value);
                    break;
                case BubbleBehaviorType.Count:
                    var aggregate = collection.Details.Find(o => o.Id == bubbleField.Question) as OmrAggregateDataOutput;
                    if (aggregate == null)
                    {
                        aggregate = new OmrAggregateDataOutput() { Id = bubbleField.Question };
                        aggregate.Function = AggregationFunction.Count;
                        collection.Details.Add(aggregate);
                    }
                    if (collection is OmrRowData)
                        aggregate.RowId = collection.Id;
                    // Add this answer to the aggregate
                    aggregate.Details.Add(answerPair.Value);
                    break;
            }
        }
        else if (!collection.AlreadyAnswered(answerPair.Value))
        {
            collection.Details.Add(answerPair.Value);
        }
    }


}

Now, when I was using a previous sheet (a classic Parscore test sheet), everything ran just beautifully -- no errors at all. Since I have switched, however, the code has not been functioning properly -- and I cannot figure out why for the life of me.

Here is the example that I am struggling with: I have the following sheet filled with marks Sheet. When I run the code, the following output occurs:

ID Number: 951619984
Question #1 - B
Question #2 - C
Question #3 - D
Question #4 - E
Question #5 - A
Question #6 - B
Question #7 - C
Question #8 - D
Question #9 - E
Question #10 - A
Question #11 - B
Question #12 - C
Question #13 - D
Question #14 - E
Question #15 - A
Question #16 - B
Question #17 - C
Question #18 - D
Question #19 - E
Question #20 - A
Question #21 - B
Question #22 - C
Question #23 - D
Question #24 - E
Question #25 - A
Question #26 - A
Question #27 - B
Question #28 - D
Question #29 - D
Question #30 - E
Question #31 - A
Question #32 - B
Question #33 - C
Question #34 - D
Question #35 - E
Question #36 - A
Question #37 - B
Question #38 - C
Question #39 - D
Question #40 - E
Question #41 - A
Question #42 - B
Question #43 - C
Question #44 - D
Question #45 - E
Question #46 - A
Question #47 - B
Question #48 - C
Question #49 - D
Question #50 - E
Question #51 - D
Question #52 - C
Question #53 - B
Question #54 - A
Question #55 - E
Question #56 - A
Question #57 - C
Question #58 - B
Question #59 - A
Question #60 - E
Question #61 - D
Question #62 - C
Question #63 - B
Question #64 - A
Question #65 - E
Question #66 - D
Question #67 - C
Question #68 - B
Question #69 - A
Question #70 - E
Question #71 - D
Question #72 - C
Question #73 - B
Question #74 - A
Question #75 - E
Question #76 - E
Question #77 - D
Question #78 - C
Question #79 - B
Question #80 - A
Question #81 - E
Question #82 - D
Question #83 - C
Question #84 - B
Question #85 - A
Question #86 - E
Question #87 - D
Question #88 - C
Question #89 - B
Question #90 - A
Question #91 - E
Question #92 - D
Question #93 - C
Question #94 - B
Question #95 - A
Question #96 - E
Question #97 - D
Question #98 - C
Question #99 - B
Question #100 - A
Question #101 - C
Question #102 - D
Question #103 - E
Question #104 - A
Question #105 - B
Question #106 - D
Question #107 - E
Question #108 - A
Question #109 - B
Question #110 - C
Question #111 - D
Question #112 - A
Question #113 - B
Question #114 - C
Question #115 - D
Question #116 - E
Question #117 - E
Question #118 - E
Question #119 - E
Question #120 - A
Question #121 - B
Question #122 - C
Question #123 - D
Question #124 - E
Question #125 - A
Question #126 - B
Question #127 - B
Question #128 - C
Question #129 - D
Question #130 - D
Question #131 - D
Question #132 - D
Question #133 - E
Question #134 - D
Question #135 - C
Question #136 - B
Question #137 - A
Question #138 - B
Question #139 - C
Question #140 - A
Question #141 - B
Question #142 - C
Question #143 - D
Question #144 - E
Question #145 - E
Question #146 - A
Question #147 - B
Question #148 - C
Question #149 - D
Question #150 - E

As you can see, the answers seem to be just one line off or so. I have measured everything pixel to pixel, and all of the templates and the like are set perfectly.

So, my question is: is it something to do with mark recognition in Ghostscript or AForge? Are the answer bubbles too dark, and so the scanner is misreading unfilled bubbles as filled? Or is it some other issue? I apologize, as I am still very much a student of programming. Any answers are much appreciated. Thanks you!

Simon Ward
  • 73
  • 7
  • I'm afraid I cannot see how Ghostscript is being used here. I doubt that your problem is anything to do with it though. Ghostscript certainly won't be doing any 'mark recognition' ! – KenS Dec 05 '19 at 23:34
  • Thanks @KenS As I said, I'm still learning a lot. It must be an AForge issue then (which makes more sense), so I changed the title. Thanks for helping at least to pinpoint the issue better! – Simon Ward Dec 05 '19 at 23:49

0 Answers0