Bubble sheet multiple choice scanner and test grader using OMR, Python and OpenCV

Figure 14: Recognizing bubble sheet exams using computer vision.

Over the past few months I’ve gotten quite the number of requests landing in my inbox to build a bubble sheet/Scantron-like test reader using computer vision and image processing techniques.

And while I’ve been having a lot of fun doing this series on machine learning and deep learning, I’d be lying if I said this little mini-project wasn’t a short, welcome break. One of my favorite parts of running the PyImageSearch blog is demonstrating how to build actual solutions to problems using computer vision.

In fact, what makes this project so special is that we are going to combine the techniques from many previous blog posts, including building a document scanner, contour sorting, and perspective transforms. Using the knowledge gained from these previous posts, we’ll be able to make quick work of this bubble sheet scanner and test grader.

You see, last Friday afternoon I quickly Photoshopped an example bubble test paper, printed out a few copies, and then set to work on coding up the actual implementation.

Overall, I am quite pleased with this implementation and I think you’ll absolutely be able to use this bubble sheet grader/OMR system as a starting point for your own projects.

To learn more about utilizing computer vision, image processing, and OpenCV to automatically grade bubble test sheets, keep reading.

Looking for the source code to this post?
Jump right to the downloads section.

Bubble sheet scanner and test grader using OMR, Python, and OpenCV

In the remainder of this blog post, I’ll discuss what exactly Optical Mark Recognition (OMR) is. I’ll then demonstrate how to implement a bubble sheet test scanner and grader using strictly computer vision and image processing techniques, along with the OpenCV library.

Once we have our OMR system implemented, I’ll provide sample results of our test grader on a few example exams, including ones that were filled out with nefarious intent.

Finally, I’ll discuss some of the shortcomings of this current bubble sheet scanner system and how we can improve it in future iterations.

What is Optical Mark Recognition (OMR)?

Optical Mark Recognition, or OMR for short, is the process of automatically analyzing human-marked documents and interpreting their results.

Arguably, the most famous, easily recognizable form of OMR are bubble sheet multiple choice tests, not unlike the ones you took in elementary school, middle school, or even high school.

If you’re unfamiliar with “bubble sheet tests” or the trademark/corporate name of “Scantron tests”, they are simply multiple-choice tests that you take as a student. Each question on the exam is a multiple choice — and you use a #2 pencil to mark the “bubble” that corresponds to the correct answer.

The most notable bubble sheet test you experienced (at least in the United States) were taking the SATs during high school, prior to filling out college admission applications.

believe that the SATs use the software provided by Scantron to perform OMR and grade student exams, but I could easily be wrong there. I only make note of this because Scantron is used in over 98% of all US school districts.

In short, what I’m trying to say is that there is a massive market for Optical Mark Recognition and the ability to grade and interpret human-marked forms and exams.

Implementing a bubble sheet scanner and grader using OMR, Python, and OpenCV

Now that we understand the basics of OMR, let’s build a computer vision system using Python and OpenCV that can read and grade bubble sheet tests.

Of course, I’ll be providing lots of visual example images along the way so you can understand exactly what techniques I’m applying and why I’m using them.

Below I have included an example filled in bubble sheet exam that I have put together for this project:

Figure 1: The example, filled in bubble sheet we are going to use when developing our test scanner software.

Figure 1: The example, filled in bubble sheet we are going to use when developing our test scanner software.

We’ll be using this as our example image as we work through the steps of building our test grader. Later in this lesson, you’ll also find additional sample exams.

I have also included a blank exam template as a .PSD (Photoshop) file so you can modify it as you see fit. You can use the “Downloads” section at the bottom of this post to download the code, example images, and template file.

The 7 steps to build a bubble sheet scanner and grader

The goal of this blog post is to build a bubble sheet scanner and test grader using Python and OpenCV.

To accomplish this, our implementation will need to satisfy the following 7 steps:

  • Step #1: Detect the exam in an image.
  • Step #2: Apply a perspective transform to extract the top-down, birds-eye-view of the exam.
  • Step #3: Extract the set of bubbles (i.e., the possible answer choices) from the perspective transformed exam.
  • Step #4: Sort the questions/bubbles into rows.
  • Step #5: Determine the marked (i.e., “bubbled in”) answer for each row.
  • Step #6: Lookup the correct answer in our answer key to determine if the user was correct in their choice.
  • Step #7: Repeat for all questions in the exam.

The next section of this tutorial will cover the actual implementation of our algorithm.

The bubble sheet scanner implementation with Python and OpenCV

To get started, open up a new file, name it test_grader.py , and let’s get to work:

On Lines 2-7 we import our required Python packages.

You should already have OpenCV and Numpy installed on your system, but you might not have the most recent version of imutils, my set of convenience functions to make performing basic image processing operations easier. To install imutils  (or upgrade to the latest version), just execute the following command:

Lines 10-12 parse our command line arguments. We only need a single switch here, --image , which is the path to the input bubble sheet test image that we are going to grade for correctness.

Line 17 then defines our ANSWER_KEY .

As the name of the variable suggests, the ANSWER_KEY  provides integer mappings of the question numbers to the index of the correct bubble.

In this case, a key of 0 indicates the first question, while a value of 1 signifies “B” as the correct answer (since “B” is the index 1 in the string “ABCDE”). As a second example, consider a key of 1 that maps to a value of 4 — this would indicate that the answer to the second question is “E”.

As a matter of convenience, I have written the entire answer key in plain english here:

  • Question #1: B
  • Question #2: E
  • Question #3: A
  • Question #4: D
  • Question #5: B

Next, let’s preprocess our input image:

On Line 21 we load our image from disk, followed by converting it to grayscale (Line 22), and blurring it to reduce high frequency noise (Line 23).

We then apply the Canny edge detector on Line 24 to find the edges/outlines of the exam.

Below I have included a screenshot of our exam after applying edge detection:

Figure 2: Applying edge detection to our exam neatly reveals the outlines of the paper.

Figure 2: Applying edge detection to our exam neatly reveals the outlines of the paper.

Notice how the edges of the document are clearly defined, with all four vertices of the exam being present in the image.

Obtaining this silhouette of the document is extremely important in our next step as we will use it as a marker to apply a perspective transform to the exam, obtaining a top-down, birds-eye-view of the document:

Now that we have the outline of our exam, we apply the cv2.findContours  function to find the lines that correspond to the exam itself.

We do this by sorting our contours by their area (from largest to smallest) on Line 37 (after making sure at least one contour was found on Line 34, of course). This implies that larger contours will be placed at the front of the list, while smaller contours will appear farther back in the list.

We make the assumption that our exam will be the main focal point of the image, and thus be larger than other objects in the image. This assumption allows us to “filter” our contours, simply by investigating their area and knowing that the contour that corresponds to the exam should be near the front of the list.

However, contour area and size is not enough — we should also check the number of vertices on the contour.

To do, this, we loop over each of our (sorted) contours on Line 40. For each of them, we approximate the contour, which in essence means we simplify the number of points in the contour, making it a “more basic” geometric shape. You can read more about contour approximation in this post on building a mobile document scanner.

On Line 47 we make a check to see if our approximated contour has four points, and if it does, we assume that we have found the exam.

Below I have included an example image that demonstrates the docCnt  variable being drawn on the original image:

Figure 3: An example of drawing the contour associated with the exam on our original image, indicating that we have successfully found the exam.

Figure 3: An example of drawing the contour associated with the exam on our original image, indicating that we have successfully found the exam.

Sure enough, this area corresponds to the outline of the exam.

Now that we have used contours to find the outline of the exam, we can apply a perspective transform to obtain a top-down, birds-eye-view of the document:

In this case, we’ll be using my implementation of the four_point_transform  function which:

  1. Orders the (x, y)-coordinates of our contours in a specific, reproducible manner.
  2. Applies a perspective transform to the region.

You can learn more about the perspective transform in this post as well as this updated one on coordinate ordering, but for the time being, simply understand that this function handles taking the “skewed” exam and transforms it, returning a top-down view of the document:

Figure 4: Obtaining a top-down, birds-eye view of both the original image along with the grayscale version.

Figure 4: Obtaining a top-down, birds-eye view of both the original image (left) along with the grayscale version (right).

Alright, so now we’re getting somewhere.

We found our exam in the original image.

We applied a perspective transform to obtain a 90 degree viewing angle of the document.

But how do we go about actually grading the document?

This step starts with binarization, or the process of thresholding/segmenting the foreground from the background of the image:

After applying Otsu’s thresholding method, our exam is now a binary image:

Figure 5: Using Otsu's thresholding allows us to segment the foreground from the background of the image.

Figure 5: Using Otsu’s thresholding allows us to segment the foreground from the background of the image.

Notice how the background of the image is black, while the foreground is white.

This binarization will allow us to once again apply contour extraction techniques to find each of the bubbles in the exam:

Lines 64-67 handle finding contours on our thresh  binary image, followed by initializing questionCnts , a list of contours that correspond to the questions/bubbles on the exam.

To determine which regions of the image are bubbles, we first loop over each of the individual contours (Line 70).

For each of these contours, we compute the bounding box (Line 73), which also allows us to compute the aspect ratio, or more simply, the ratio of the width to the height (Line 74).

In order for a contour area to be considered a bubble, the region should:

  1. Be sufficiently wide and tall (in this case, at least 20 pixels in both dimensions).
  2. Have an aspect ratio that is approximately equal to 1.

As long as these checks hold, we can update our questionCnts  list and mark the region as a bubble.

Below I have included a screenshot that has drawn the output of questionCnts  on our image:

Figure 6: Using contour filtering allows us to find all the question bubbles in our bubble sheet exam recognition software.

Figure 6: Using contour filtering allows us to find all the question bubbles in our bubble sheet exam recognition software.

Notice how only the question regions of the exam are highlighted and nothing else.

We can now move on to the “grading” portion of our OMR system:

First, we must sort our questionCnts  from top-to-bottom. This will ensure that rows of questions that are closer to the top of the exam will appear first in the sorted list.

We also initialize a bookkeeper variable to keep track of the number of correct  answers.

On Line 90 we start looping over our questions. Since each question has 5 possible answers, we’ll apply NumPy array slicing and contour sorting to to sort the current set of contours from left to right.

The reason this methodology works is because we have already sorted our contours from top-to-bottom. We know that the 5 bubbles for each question will appear sequentially in our list — but we do not know whether these bubbles will be sorted from left-to-right. The sort contour call on Line 94 takes care of this issue and ensures each row of contours are sorted into rows, from left-to-right.

To visualize this concept, I have included a screenshot below that depicts each row of questions as a separate color:

Figure 7: By sorting our contours from top-to-bottom, followed by left-to-right, we can extract each row of bubbles. Therefore, each row is equal to the bubbles for one question.

Figure 7: By sorting our contours from top-to-bottom, followed by left-to-right, we can extract each row of bubbles. Therefore, each row is equal to the bubbles for one question.

Given a row of bubbles, the next step is to determine which bubble is filled in.

We can accomplish this by using our thresh  image and counting the number of non-zero pixels (i.e., foreground pixels) in each bubble region:

Line 98 handles looping over each of the sorted bubbles in the row.

We then construct a mask for the current bubble on Line 101 and then count the number of non-zero pixels in the masked region (Lines 107 and 108). The more non-zero pixels we count, then the more foreground pixels there are, and therefore the bubble with the maximum non-zero count is the index of the bubble that the the test taker has bubbled in (Line 113 and 114).

Below I have included an example of creating and applying a mask to each bubble associated with a question:

Figure 8: An example of constructing a mask for each bubble in a row.

Figure 8: An example of constructing a mask for each bubble in a row.

Clearly, the bubble associated with “B” has the most thresholded pixels, and is therefore the bubble that the user has marked on their exam.

This next code block handles looking up the correct answer in the ANSWER_KEY , updating any relevant bookkeeper variables, and finally drawing the marked bubble on our image:

Based on whether the test taker was correct or incorrect yields which color is drawn on the exam. If the test taker is correct, we’ll highlight their answer in green. However, if the test taker made a mistake and marked an incorrect answer, we’ll let them know by highlighting the correct answer in red:

Figure 9: Drawing a "green" circle to mark "correct" or a "red" circle to mark "incorrect".

Figure 9: Drawing a “green” circle to mark “correct” or a “red” circle to mark “incorrect”.

Finally, our last code block handles scoring the exam and displaying the results to our screen:

Below you can see the output of our fully graded example image:

Figure 10: Finishing our OMR system for grading human-taken exams.

Figure 10: Finishing our OMR system for grading human-taken exams.

In this case, the reader obtained an 80% on the exam. The only question they missed was #4 where they incorrectly marked “C” as the correct answer (“D” was the correct choice).

Why not use circle detection?

After going through this tutorial, you might be wondering:

“Hey Adrian, an answer bubble is a circle. So why did you extract contours instead of applying Hough circles to find the circles in the image?”

Great question.

To start, tuning the parameters to Hough circles on an image-to-image basis can be a real pain. But that’s only a minor reason.

The real reason is:

User error.

How many times, whether purposely or not, have you filled in outside the lines on your bubble sheet? I’m not expert, but I’d have to guess that at least 1 in every 20 marks a test taker fills in is “slightly” outside the lines.

And guess what?

Hough circles don’t handle deformations in their outlines very well — your circle detection would totally fail in that case.

Because of this, I instead recommend using contours and contour properties to help you filter the bubbles and answers. The cv2.findContours  function doesn’t care if the bubble is “round”, “perfectly round”, or “oh my god, what the hell is that?”.

Instead, the cv2.findContours  function will return a set of blobs to you, which will be the foreground regions in your image. You can then take these regions process and filter them to find your questions (as we did in this tutorial), and go about your way.

Our bubble sheet test scanner and grader results

To see our bubble sheet test grader in action, be sure to download the source code and example images to this post using the “Downloads” section at the bottom of the tutorial.

We’ve already seen test_01.png  as our example earlier in this post, so let’s try test_02.png :

Here we can see that a particularly nefarious user took our exam. They were not happy with the test, writing “#yourtestsux” across the front of it along with an anarchy inspiring “#breakthesystem”. They also marked “A” for all answers.

Perhaps it comes as no surprise that the user scored a pitiful 20% on the exam, based entirely on luck:

Figure 11: By using contour filtering, we are able to ignore the regions of the exam that would have otherwise compromised its integrity.

Figure 11: By using contour filtering, we are able to ignore the regions of the exam that would have otherwise compromised its integrity.

Let’s try another image:

This time the reader did a little better, scoring a 60%:

Figure 12: Building a bubble sheet scanner and test grader using Python and OpenCV.

Figure 12: Building a bubble sheet scanner and test grader using Python and OpenCV.

In this particular example, the reader simply marked all answers along a diagonal:

Figure 13: Optical Mark Recognition for test scoring using Python and OpenCV.

Figure 13: Optical Mark Recognition for test scoring using Python and OpenCV.

Unfortunately for the test taker, this strategy didn’t pay off very well.

Let’s look at one final example:

Figure 14: Recognizing bubble sheet exams using computer vision.

Figure 14: Recognizing bubble sheet exams using computer vision.

This student clearly studied ahead of time, earning a perfect 100% on the exam.

Extending the OMR and test scanner

Admittedly, this past summer/early autumn has been one of the busiest periods of my life, so I needed to timebox the development of the OMR and test scanner software into a single, shortened afternoon last Friday.

While I was able to get the barebones of a working bubble sheet test scanner implemented, there are certainly a few areas that need improvement. The most obvious area for improvement is the logic to handle non-filled in bubbles.

In the current implementation, we (naively) assume that a reader has filled in one and only one bubble per question row.

However, since we determine if a particular bubble is “filled in” simply by counting the number of thresholded pixels in a row and then sorting in descending order, this can lead to two problems:

  1. What happens if a user does not bubble in an answer for a particular question?
  2. What if the user is nefarious and marks multiple bubbles as “correct” in the same row?

Luckily, detecting and handling of these issues isn’t terribly challenging, we just need to insert a bit of logic.

For issue #1, if a reader chooses not to bubble in an answer for a particular row, then we can place a minimum threshold on Line 108 where we compute cv2.countNonZero :

Figure 15: Detecting if a user has marked zero bubbles on the exam.

Figure 15: Detecting if a user has marked zero bubbles on the exam.

If this value is sufficiently large, then we can mark the bubble as “filled in”. Conversely, if total  is too small, then we can skip that particular bubble. If at the end of the row there are no bubbles with sufficiently large threshold counts, we can mark the question as “skipped” by the test taker.

A similar set of steps can be applied to issue #2, where a user marks multiple bubbles as correct for a single question:

Figure 16: Detecting if a user has marked multiple bubbles for a given question.

Figure 16: Detecting if a user has marked multiple bubbles for a given question.

Again, all we need to do is apply our thresholding and count step, this time keeping track if there are multiple bubbles that have a total  that exceeds some pre-defined value. If so, we can invalidate the question and mark the question as incorrect.


In this blog post, I demonstrated how to build a bubble sheet scanner and test grader using computer vision and image processing techniques.

Specifically, we implemented Optical Mark Recognition (OMR) methods that facilitated our ability of capturing human-marked documents and automatically analyzing the results.

Finally, I provided a Python and OpenCV implementation that you can use for building your own bubble sheet test grading systems.

If you have any questions, please feel free to leave a comment in the comments section!

But before you, be sure to enter your email address in the form below to be notified when future tutorials are published on the PyImageSearch blog!


If you would like to download the code and images used in this post, please enter your email address in the form below. Not only will you get a .zip of the code, I’ll also send you a FREE 17-page Resource Guide on Computer Vision, OpenCV, and Deep Learning. Inside you'll find my hand-picked tutorials, books, courses, and libraries to help you master CV and DL! Sound good? If so, enter your email address and I’ll send you the code immediately!

, , , ,

154 Responses to Bubble sheet multiple choice scanner and test grader using OMR, Python and OpenCV

  1. evandrix October 3, 2016 at 12:13 pm #

    what if the candidate marked one bubble, realised it’s wrong, crossed it out, and marked another? will this system still work?

    • Adrian Rosebrock October 4, 2016 at 7:00 am #

      When taking a “bubble sheet” exam like this you wouldn’t “cross out” your previous answer — you would erase it. The assumption is that you always use pencils for these types of exams.

  2. Jurriaan Schreuder October 3, 2016 at 2:25 pm #

    Made this a long time ago for android, when I used to give a cocktail party on every Friday the 13th. We always had a quiz, which took more than an hour to grade, so I made an app for it!

    Has long since been taken offline because I was banned from the Google Play Store.

    Was pure Java, no libraries. Could do max 40 questions, on 2 a4 papers (detected if first or second sheet)


    • Adrian Rosebrock October 4, 2016 at 6:58 am #

      Very nice, thanks for sharing Jurriaan!

    • Chacrit October 20, 2016 at 2:18 pm #

      Can I get you source code ?

    • Johnny March 10, 2017 at 11:13 am #

      You do you have to go to java code help me with. Thanhk

    • zikrie October 31, 2018 at 10:04 am #

      can i get the source code pls sirrrrrr

  3. hgeorge October 3, 2016 at 6:19 pm #

    Great article!

    One question though. ideally (assuming the input image was already a birds-eye view), won’t the loop in lines 26-49 be sufficient to detect the circle contours too?

    • Adrian Rosebrock October 4, 2016 at 6:55 am #

      If the image is already a birds-eye-view, then yes, you can use the same contours that were extracted previously — but again, you would have to make the assumption that you already have a birds-eye-view of the image.

  4. Madhup October 4, 2016 at 3:24 am #

    Hi Adrian,

    I am trying to run this code and am getting an error on running this code:

    from imutils.perspective import four_point_transform

    ImportError: No module named scipy.spatial

    I have installed imutils successfully and am not sure why I am getting this error. It would be great if you could help me here


    • Adrian Rosebrock October 4, 2016 at 6:48 am #

      Make sure you install NumPy and SciPy:

  5. King October 5, 2016 at 9:57 am #

    Wonderful Tut!
    I was wondering how to handle such OMR sheets.

    any idea or algorithm please?

    • Adrian Rosebrock October 6, 2016 at 6:53 am #

      I would suggest using more contour filtering. You can use contours to find each of the “boxes” in the sheet. Sort the contours from left-to-right and top-to-bottom. Then extract each of the boxes and process the bubbles in each box.

      • King October 6, 2016 at 10:07 am #

        What can I do to detect the four anchor points and transform the paper incase it rotates?

        • Adrian Rosebrock October 7, 2016 at 7:37 am #

          As long as you can detect the border of the paper, it doesn’t matter how the paper is oriented. The four_point_transform function will take care of the point ordering and transformation for you.

          • King October 8, 2016 at 10:16 am #

            I understand, but what If the paper is cropped being rotated without border of the paper?
            What technique shall I use to detect the four anchor points please?

          • Adrian Rosebrock October 11, 2016 at 1:09 pm #

            If you do not have the four corners of the paper (such as the corners being cropped out) then you cannot apply this perspective transform.

    • Johannes Brodwall November 15, 2016 at 5:25 pm #


      It looks like the marks on the right side of the paper are aligned with the target areas. You could threshold the image, findContours and filter contours in the leftmost 10% of the image to find the rows and sort them by y-position.

      Then you could look for contours in the rest of the area. The index of the closest alignment mark for y-direction gives row, the x position as percentage of the page width gives column.

      Once you have the column and row of each mark, you just need “normal code” to interpret which question and answer this represents.

      Watch out for smudges, though! 😉

    • silver January 18, 2017 at 11:04 am #

      you see this project:
      it’s free and opensource.

      you can design any form in the world.

  6. Edwin October 8, 2016 at 5:33 pm #

    Nice to see your implementation of this. I started a similar project earlier this year but I ended up putting it on parking for now.
    My main concern was the amount of work it goes into making one work right without errors and the demand didn’t seem to be there.
    Seems like scantron has a monopoly on this.
    What are your thoughts on that?

    • Adrian Rosebrock October 11, 2016 at 1:08 pm #

      There are a lot of companies in this space actually. I would suggest reading this thread on reddit to learn more about the companies involved and what they are doing for OMR.

  7. Linus October 14, 2016 at 12:35 pm #

    This is indeed a very cool post! Well explained 🙂

    • Adrian Rosebrock October 15, 2016 at 9:55 am #

      Thank you Linus, I’m glad you enjoyed it 🙂

  8. simon October 17, 2016 at 4:32 am #

    please, send me your code!

    • Adrian Rosebrock October 17, 2016 at 4:01 pm #

      You can download the code + example images to this post by using the “Downloads” form above.

  9. bhanu prakash December 13, 2016 at 10:58 am #

    Hi thank you very much..


    cnts = cv2.findContours(thresh.copy(), cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_SIMPLE)

    giving me only one counter as a result only one question is being identified.

    Could you pls help

    • Adrian Rosebrock December 14, 2016 at 8:30 am #

      I’m not sure what you mean by “giving me only one contour as a result”. Can you please elaborate?

  10. Ruhi December 27, 2016 at 5:15 am #

    Hi adrian,

    In my case, Image contain 4 reference rectangle which is base for image deskewing. Assume, Image contain some other information like text, circle and rectangle. Now, I want to write a script to straighten the image based on four rectangle.my resultant image should be straighten. So i can extract some information after deskewing it.How can be possible it? When i used for my perspective transformation, it only detects highest rectangle contour.
    my image is like https://i.stack.imgur.com/46rsL.png
    output image must be like https://i.stack.imgur.com/rqgsY.png

    • Adrian Rosebrock December 31, 2016 at 1:36 pm #

      So your question is on deskewing? I don’t have any tutorials on how to deskew an image, but I’ll certainly add it to my queue.

      • Ruhi January 7, 2017 at 12:19 am #

        I am waiting for that tutorial. i am not getting proper reference for deskewing of image in my scenario. In image there is barcode as well as that 4 small rectangle. i am not able to deskew it because of barcode in side that. As i am building commercial s/w, i can not provide real images here only. In side image, i have to extract person’s unique id and age, DOB which are in terms of optical mark. Once i scan form which is based on OMR i need to extract those information. Is there any approach which can help me to achieve this goals?
        I am very thankful to your guidance.

        • Adrian Rosebrock January 7, 2017 at 9:25 am #

          As I mentioned, I’ve added it to my queue. I’ll try to bump it up, but please keep in mind that I am very busy and cannot accommodate every single tutorial request in a timely manner. Thank you for your patience.

  11. silver January 18, 2017 at 10:39 am #

    Dear Adrian
    thank you
    i upgraded the code:
    – the code now capturing the image from laptop camera.
    – added dropdown to selected answer key.
    – added the date in the name of the result (result+date+.png).
    can i send the cod to you, and is this code opensource, free.
    best regards

    • Adrian Rosebrock January 18, 2017 at 12:18 pm #

      Hi Silver — feel free to send me the code or you can release it on your own GitHub page. If you don’t mind, I would appreciate a link back to the PyImageSearch site, but that’s not necessary if you don’t want to.

      • Iqra Naqvi December 24, 2017 at 11:20 am #

        AoA…My final Year project is Mobile OMR system for recognition of filled bubbles…but using Matlab will you please provide me Matlab code… 🙁

  12. silver January 21, 2017 at 12:15 pm #

    Hi adrian
    i made gui for this projects and add the program in sourceforge.net
    best regards

  13. Sanna Khan February 27, 2017 at 4:43 am #

    Hi adrian,

    I am facing below issues while making bounding rectangle on bubbles:
    1. In image, bubbles are somewhere near to rectangle where student can write manually their roll number because after thresolding bubbles get touched to rectangle. so, it can’t find circle.
    2. If bubble filled out of the boundary, again it can’t be detectable.
    3. False detection of circle because of similar height and width.

    Best Regards,

    • Adrian Rosebrock February 27, 2017 at 11:06 am #

      If you’re running into issues where the bubbles are touching other important parts of the image, applying an “opening” morphological operation to disconnect them.

      • Sanna Khan February 28, 2017 at 1:46 am #

        What about second and third issue? Is there any rough idea which can help me to sort out it?

        • Adrian Rosebrock February 28, 2017 at 6:55 am #

          It’s hard to say without seeing examples of what you’re working with. I’m not sure what you mean by if the bubble is filled in outside the circle it being impossible to detect — the code in this post actually helps prevent that by alleviating the need for Hough circles which can be hard to tune the parameters to. Again, I get the impression that you’re using Hough circles instead of following the techniques in this post.

  14. Usman March 10, 2017 at 12:38 pm #

    Dear sir I have install imutils but I am still facing “ImportError: No module named ‘imutils'” kingly guide me.

    • Adrian Rosebrock March 10, 2017 at 3:43 pm #

      You can install imutils using pip:

      $ pip install imutils

      If you are using a Python virtual environment, access it first and then install imutils via pip:

  15. Hoang Ngoc March 10, 2017 at 9:26 pm #

    How to convert py to android

  16. Nyx March 11, 2017 at 4:28 am #

    Can this also work with many items in the exam like 50 or 100?

    • Adrian Rosebrock March 13, 2017 at 12:19 pm #

      Yes. As long as you can detect and extract the rows of bubbles this approach can work.

  17. Dana March 11, 2017 at 12:00 pm #

    Adrian, do you have the android version of this application?

    • Adrian Rosebrock March 13, 2017 at 12:17 pm #

      You will need to port the Python code to Java if you would like to use it as an Android application.

  18. Nic March 13, 2017 at 1:08 am #

    Hello Adrian,

    Do you have a code for this in java? I am planning a project similar to this one, I am having problems especially since this program was created in python and using many plugin modules which is not available in java.

    I hope you can consider my request since this is related for my school work. Thank you

    • Adrian Rosebrock March 13, 2017 at 12:12 pm #

      Hey Nic — I only provide Python and OpenCV code on this blog. If you are doing this for a school project I would really suggest you struggle and fight your way through the Python to Java conversion. You will learn a lot more that way.

  19. Nic March 14, 2017 at 6:11 am #

    Hi again adrian, thanks for the reply on my previous comments.

    Can you provide a code that can allow this code to run directly on a python compiler rather than running the program on cmd. I would like to focus on python for developing a project same on this one, I’ve ask many experts and python was the first thing they recommended since it can create many projects and provides many support on many platforms unlike java.

    • Adrian Rosebrock March 15, 2017 at 8:57 am #

      Hey Nic — while I’m happy to help point readers like yourself in the write direction, I cannot write code for you. I would suggest you taking the time to learn and study the language. If you need help learning OpenCV and computer vision, take a look at Practical Python and OpenCV.

      • Nic March 24, 2017 at 5:22 am #

        Can this process of computation be possible in a mobile devices alone using openCV and python? If yes, In what way can it be done?

        • Adrian Rosebrock March 25, 2017 at 9:27 am #

          Most mobile devices won’t run native Python + OpenCV code. If you’re building for iOS, you would want to use Swift/Objective-C + OpenCV. For Android, Java + OpenCV.

  20. pawan March 18, 2017 at 2:14 am #

    hi Adrain

    could u please tell the code about how did u draw the output of questionCnts  on  image

    • Adrian Rosebrock March 21, 2017 at 7:37 am #

      I’m not sure what you mean Pawan. Can you please elaborate?

  21. Ckj April 7, 2017 at 3:29 pm #

    I’m capturing image through USB web camera and executing this program but that image not giving any answers and it’s shows multiple errors

    • Adrian Rosebrock April 8, 2017 at 12:41 pm #

      Without knowing what your errors are, it’s impossible to point you in the right direction.

      • jf pwork July 15, 2017 at 12:18 pm #

        In fact, try the same.

        1 – Take a picture with mobile phone => test_mobile.jpeg
        2 – python test_grader.py –image images/test_mobile.jpeg

        It seems that it comes from
        edged = cv2.Canny(blurred, 100, 200)

        3 – add instruction to check it: cv2.imshow(“edged”, edged)

        4 – In our program :

        # then we can assume we have found the paper

        => no ‘paper’ found …

        – – try different values for 2nd & 3rd arguments of cv2.Canny
        – – ( http://docs.opencv.org/trunk/da/d22/tutorial_py_canny.html )
        —-> same result, edged dont have paper contour well defined as your image.

        Must we have to convert jpeg to png ?

        when i show your edged image with test_01.png, we have a high quality of contour.

        Could you please explain how you get a so well defined contour ?

        Best regards

        • Adrian Rosebrock July 18, 2017 at 10:06 am #

          The best way to ensure you have a well defined contour is to ensure there is contrast between your paper and your background. For example, if you place a white piece of paper on a white countertop, there is going to be very little contrast and it will be hard for the edge detection algorithm to find the outlines of the paper. Place the paper on a background with higher contrast and the edges will easily be found.

  22. Jason May 1, 2017 at 5:54 am #

    Hi Adrian,

    I’m getting a really weird error and was hoping if you could provide some guidance.

    First of all, I’m using my own omr that is a bit different from yours.

    I did up to finding thresh and got a really nice clear picture.

    thresh = cv2.threshold(warp, 0, 255, cv2.THRESH_BINARY_INV | cv2.THRESH_OTSU)[1]

    However, when I try to find the bubbles, it fails to find them. The only contour it gives is the outermost boundary.

    On the other hand, if I do some additional steps on “warp” before finding thresh, I get almost all the bubbles. Here are the additional steps I performed on “warp”.

    (cnts, _) = cv2.findContours(warp.copy(),cv2.RETR_TREE,cv2.CHAIN_APPROX_SIMPLE)
    cv2.drawContours(warp, cnts, -1, (255,0,0), 3)

    I was wondering if you have any insights on this matter.
    Why is it not finding contours for the bubbles if I don’t do some steps on “warp”? The picture of “thresh” seems to have very clear boundaries of bubble.

    If this is confusing, I’d love to share with you my code and some images I generated to give you a more clear picture of what’s going on.

    • Adrian Rosebrock May 1, 2017 at 1:17 pm #

      The best way to diagnose this issue would be to see what your thresh image looks like. Also, is there any reason why you are using cv2.RETR_TREE? Usually for this application you would use cv2.RETR_EXTERNAL, but again, that is really dependent on what your thresh image looks like.

      • Jason May 1, 2017 at 10:34 pm #

        Thanks for the reply!

        Here is the link to my “thresh” image

        And for using RETR_EXTERNAL instead of RETR_TREE when processing warp, thank you for the suggestion. I was able to detect few more bubbles using RETR_EXTERNAL.

        I used RETR_TREE previously for cropping my omr image. (when I used RETR_EXTERNAL, I wasn’t able to crop the image properly for some reason) Here is the link to my original image for your information.

        Thank you so much in advance.

        • Adrian Rosebrock May 3, 2017 at 5:40 pm #

          I think I see the issue. The thresholded image contains white pixels along the borders. That is what is causing the contour detection to return strange results. Remove the white pixels along the boundaries and your script should work okay.

          • redget March 8, 2018 at 5:12 pm #

            just use adaptive threshold

        • Gautam June 11, 2019 at 6:26 am #

          Hi Jason and Adrian, could you share how you managed to overcome this problem? I have a similar thresh image, and am unable to remove the white borders. Any help would be greatly valued.

  23. debit May 4, 2017 at 3:28 am #

    if i have only four circles , then what should i modify to the given codes?help me with figures,

    • Adrian Rosebrock May 4, 2017 at 12:32 pm #

      You’ll want to change Line 90 so that the code only looks for 4 circles along the row instead of 5.

  24. Mehul Bhardwaj May 18, 2017 at 1:29 am #

    Hey Adrian,
    Thanks for a really good post. This has helped me a lot. I have doubt but.

    I can’t seem to figure out the use of this line

    cnts = cnts[0] if imutils.is_cv2() else cnts[1]

    I was able to detect the contour of my paper only after removing this line.

    What is imutils.is_cv2()?

    • Adrian Rosebrock May 18, 2017 at 11:48 am #

      The cv2.findContours return signature changed between OpenCV 2.4 and OpenCV 3. You can read more about this change (and the associated function call) here.

  25. Sandeep May 19, 2017 at 6:38 am #

    How to divide circles if options are not equally divide? cnts = contours.sort_contours(questionCnts[i:i + 5])[0]. In my case, somewhere, it is like 4 option or 3 option. How to resolve uneven division issue?

  26. rad May 23, 2017 at 3:20 pm #

    hello.. in need of your help.
    the system gives me a traceback error while installing imutils. why is this happening and how can i get over it.?

    • Adrian Rosebrock May 25, 2017 at 4:26 am #

      What is the error you are getting when installing imutils? Without knowing the error, neither myself nor any other PyImageSearch reader can help.

  27. Rafael June 28, 2017 at 3:20 pm #

    usage: test_grader.py [-h] -i IMAGE
    test_grader.py: error: argument -i/–image is required

    Can you help me with this error?

  28. Abhi July 6, 2017 at 9:23 am #

    please tell me how sorting is working??

    • Adrian Rosebrock July 7, 2017 at 9:54 am #

      Are you referring to contour sorting? If so, I cover contour sorting in detail in this blog post.

  29. Anchit July 10, 2017 at 5:27 am #

    Adrian, it is a great post for learners like me.
    I had this problem in detecting circle contours, for non filled circles it is detecting inside as well as outside edge of the circle. any method by which i can make it to detect only outside edges ??

    • Adrian Rosebrock July 11, 2017 at 6:35 am #

      I would suggest working with your pre-processing methods (edge detection and thresholding) to create a nicer edge map. Secondly, make sure you are passing the cv2.RETR_EXTERNAL into cv2.findContours.

      • Anchit July 11, 2017 at 10:34 am #

        Thanks!! it worked.

  30. Abhi July 10, 2017 at 11:17 am #

    Hey Adrian,
    Is there any specific blog or tutorial about porting python code to java?
    I am developing Application in android and struggling with few points.
    Please help me

    • Adrian Rosebrock July 11, 2017 at 6:32 am #

      Sorry, I am pretty far removed from the Java + OpenCV world so no recommendations come to mind.

  31. Tom July 15, 2017 at 11:42 am #

    Hi Adrian,
    I had to add “break” in line 125 and de-indent line 127 to get proper score and still draw the circles after the break. Otherwise (1) score was increased with every iteration after finding a filled bubble (resulting in final 280%) and (2) contours were drawn also repetitively. Maybe I made a mistake somewhere else in the code but the above fixed it.
    I’m glad you added a section about dealing with unexpected input (e.g. no bubbles filled or more than 1 filled), I wish more of your tutorials had such critical analysis.

    • Adrian Rosebrock July 18, 2017 at 10:07 am #

      Hi Tom — always make sure you use the “Downloads” section of a post when running the code. Don’t try to copy and paste it as that will likely introduce indentation errors/bugs.

  32. Pjay July 18, 2017 at 6:46 am #

    Hi Adrian,

    I’m trying to identify crosses in the check-boxes using similar approach, the problem I’m having is the mask becomes hollow box instead of the type we obtain here which is just the outline of the bubble (The reason for this is the bubble have alphabet inside and checkbox is completely blank). Thereafter, when I try to calculate total pixels (using bit-wise and logic between mask and thresh checkbox), the totals are somehow incorrect. As a result, the box that gets highlighted at the end is the one that is not crossed? Any suggestion how to modify the mask on plain boxes and check boxes without any background serialization?


    • Adrian Rosebrock July 18, 2017 at 9:43 am #

      Hi Pjay — do you have any example images of the crosses and check-boxes you are using? That might help me point you in the right direction.

  33. Chester August 4, 2017 at 12:36 pm #

    Hi. do you guys have links to tutorials or blogs that I can follow to develop the same bubble sheet scanner for android? I have this project and I really don’t know where to start. I would appreciate any suggestions.

    Thank you.

  34. Otis Olifant August 31, 2017 at 9:06 am #

    Good day,

    My name is Otis Olifant from an agency in South Africa called SPACE Grow Media. The company is hosting an event in Munich next year. There will be an exam during the event which will require a third party to assist us in marking the exam papers.

    I would like to ask if you will be able to assist me in marking the exam papers written at the event. If you are able to assist, may I please request that you reply to this mail with a formal high level price quotation for marking the papers.

    Please get back to me as soon as you can.

    Kind regards

    • Adrian Rosebrock September 1, 2017 at 9:43 am #

      Hi Otis — while I’m happy to help point you in the right direction, I cannot write code for you. I hope you understand. I would suggest you post your project on PyImageJobs, a computer vision and OpenCV jobs board I created to help connect employers with computer vision developers. Please take a look and consider posting your project — I know you would get a number of applicants.

  35. Daniel Moya September 2, 2017 at 11:41 pm #


    I want to know how can orientation be considered in the algorithm. In your examples the photos are well taken, but what if an user takes a photo upside down? would the algorithm still work but with wrong results? In some example sheets I have seen that 4 squares in the corners are used, but I don’t understand how could that determine the right orientation. Thank you in advance.

    Best regards

    • Adrian Rosebrock September 5, 2017 at 9:31 am #

      Hi Daniel — this particular example does not consider orientation. You can modify it to consider orientation by detecting markets on each of the four corners. One marker should be different than the rest. If you can detect that marker you can rotate it such that the orientation is the same for all input images.

  36. Arshey October 8, 2017 at 5:03 am #

    Hello Adrian,
    The code giving me error ”
    gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
    error: C:\projects\opencv-python\opencv\modules\imgproc\src\color.cpp:10638: error: (-215) scn == 3 || scn == 4 in function cv::cvtColor ”

    How to solve this?

    • Adrian Rosebrock October 9, 2017 at 12:27 pm #

      Double-check the path to your input image. It sounds like cv2.imshow is reading the path to an invalid image and returning “None”.

  37. Luisa Villaluna October 23, 2017 at 1:37 am #

    Hi, I have a question ! I took my exam yesterday and I’m quite concerned with my answers because I got to shade ¼ of each the circles only. I just tried to put mark inside the circles while I was answering. After I finished my exam the proctor didn’t anymore allow me to shade so some of the circles were left ¼ shaded only. Will the scantron be able to read them? ?

    If you don’t mind, could you send me your reply in my gmail account? (email removed)

    • Adrian Rosebrock October 23, 2017 at 6:12 am #

      The Scantron software is normally pretty good at recognizing partially filled in bubbles, but you should really take the time to fill in bubbles fully when you take an exam.

  38. Kapil December 5, 2017 at 5:32 am #

    Hello Adrian,

    Thanks a lot for such a nice article, I coded the same and found your blog very helpful.

    I have a major doubt, why some of major bubble sheet providers have a vertical strip of black markers. I searched it a lot, I just came to know that it will improve accuracy.

    but how the vertical strip of black markers is helpful to detect circles/elipses in the image.

    example of such a sheet is here

    Searching a lot, but could not find the reason of black strip of vertical markers on the right of the sheet.

    • Udayraj November 5, 2018 at 3:38 pm #

      I was looking for an answer to the same doubt for long. Strangely, found it in a OMR software guide! Quoting the snip:

      “In the old pattern, machine read sheets, were additional black marks placed at equal increments running throughout the length of the sheet on either side or on both sides. This strip of black marks is called a timeline which helps the OMR machine to identify the next row of bubbles”

      Ref link: https://www.addmengroup.com/Downloads/Addmen-OMR-Sheet-Design-Guide.pdf

  39. Fernando Vazquez March 3, 2018 at 3:34 pm #

    Hello Adrian I wanted to ask you how can you expand this to include more questions per page? Since in the expample it only showed 5 questions, I want to expand it to say 15 to 20 per page

    • Adrian Rosebrock March 7, 2018 at 9:43 am #

      Yes, it would still work.

  40. Sanjay Ghosh March 16, 2018 at 11:48 am #

    Is it possible to get output in .xls or .csv file?
    Can we scan multiple pages bubble sheets?
    Can we scan multiple students multi pages bubble sheets?

    • Adrian Rosebrock March 19, 2018 at 5:31 pm #

      Sure, you can absolutely save the output to a CSV file. A CSV file is just a comma separated file. If you’re new to CSV files I would encourage you to do your research on them. They are very easy to work with.

  41. pradeep chauhan April 16, 2018 at 11:37 am #

    Traceback (most recent call last):
    File “test_grader.py”, line 55, in
    paper = four_point_transform(image, docCnt.reshape(4, 2))
    AttributeError: ‘NoneType’ object has no attribute ‘reshape’

    • Adrian Rosebrock April 16, 2018 at 2:01 pm #

      It sounds like your document contour region was not properly found. This method assumes that the document has 4 vertices. Double-check the output of the contour approximation.

      • pradeep chauhan April 16, 2018 at 11:15 pm #

        The picture is perfect i do not why it is not working. The image i have given as input have 4 corners visible.

        • Adrian Rosebrock April 17, 2018 at 9:25 am #

          Okay, but what does OpenCV report? Does OpenCV report four corners after the contour approximation?

  42. Thai Nguyen May 7, 2018 at 6:04 am #

    I would like to ask if you have a test image when the examinee circles 2 answers for 1 question.
    Thank you.

    • Adrian Rosebrock May 9, 2018 at 10:05 am #

      I don’t have a test image for this but you can update the code to sort the bubbles based on how filled in they are and keep the two bubbles with the largest filled in values.

  43. Nhut Thai May 8, 2018 at 2:16 am #

    When my input is the original image of yours, which is without any bubble has been filled, it doesn’t work. How come I solve this problem? Please show me. Thank you.

    • Adrian Rosebrock May 9, 2018 at 9:48 am #

      Can you be more descriptive of what you mean by “it doesn’t work”? I’m happy to help but keep in mind that I cannot help if you do not provide more details on what specifically isn’t working.

  44. Bernard May 11, 2018 at 4:03 am #

    What if student leave some question blank? Any way I can be notified once empty answer is detected?

    • Adrian Rosebrock May 14, 2018 at 12:08 pm #

      I address this in the blog post. See the “Extending the OMR and test scanner” section.

      • Tevfik Yoruk November 21, 2018 at 2:55 pm #

        Hello Adrian, I not see your adress. Please shared me your adress. And I ask for your support in a matter.

  45. Jordan May 22, 2018 at 1:34 pm #

    Hi Adrian. Thanks so much for this tutorial – I’ve found it immensely helpful in learning OpenCV. A few questions:

    1) If I were to add additional questions to include a total of 20 questions and only have 4 answer options, where would I edit the code?

    2) Is it possible to include fill-in-the-blank questions on the same test paper and use a OCR-like solution for those? If so, how where would I edit the code to disregard these fill-in-the-blank questions? Any recommended OCR solutions that would work well with this?

    Again, thanks so much. Love the site & newsletter!

    • Adrian Rosebrock May 23, 2018 at 7:18 am #

      1. On Line 90 you would change the “5” to a “4”. Same goes for Line 94. This should be the main lines to edit.

      2. You may want to try Tesseract for OCR and then look at the Google Vision API.

      • Jordan May 29, 2018 at 2:30 pm #

        Thank you sir! Very much appreciated.

  46. Malik Razzaq May 26, 2018 at 2:05 pm #

    hi, can anyone can tell me this code is patent? can we use commercially or not with further development?

    i have some questions to about Indian software’s like OMR Home and admin OMR, they are selling their solutions but Scantron OMR scanners even scanning Techniques are patent by their respective owners. how they work to skip this part or they are just selling their software without any permission or they work with fair use? please help me in this regards


    • Adrian Rosebrock May 28, 2018 at 9:44 am #

      Are you asking about my code on this blog post? It’s MIT license. You can use it on your own projects but I would appreciate an attribution or a link back.

      • Malik Razzaq November 7, 2019 at 10:18 am #

        I was asking for the general, not for your code, if I code of any famous OMR software algorithm myself, by of its working function, the same function in my code that is an infringement of patent?

  47. Daniel June 17, 2018 at 3:53 pm #

    The predictions are the exact replica of the data with which it learned from.
    It doesn’t seem to matter what is “shown” to it.
    The answers are always the same.

    • Adrian Rosebrock June 19, 2018 at 8:51 am #

      I’m not sure what you mean by “data with which it learned from”. The algorithm in this post does not use any machine learning. Could you elaborate?

  48. XtianDR June 23, 2018 at 8:43 pm #

    Good morning. I proposed this project to our design project/thesis project. But our instructor asked why I do this system if there is a machine in store. Can I try this on Windows 10 OS? I want to make this project in our thesis that can answer 60 question and save the score via MySQL.

    • Adrian Rosebrock June 25, 2018 at 1:50 pm #

      Provided you have OpenCV + Python installed on your system this code will run on Windows.

  49. manas parida July 19, 2018 at 2:02 pm #

    hi, sir I have partially filled a wrong bubble then completely filled a correct bubble in my state staff selection commission exam.will it be counted as correct response??.plz sir tell me.

    • Adrian Rosebrock July 20, 2018 at 6:30 am #

      Hey Manas, I would recommend that you try the code and see. I’ve discussed extensions to handle partially filled in bubbles or no bubbles filled in either.

  50. Hannah July 24, 2018 at 10:15 am #

    After I execute the file, I get this error:

    usage: [-h] -i IMAGE
    : error: the following arguments are required: -i/–image

    How can I fix it please?

    • Adrian Rosebrock July 25, 2018 at 8:02 am #

      You need to provide the “–image” command line argument to the script. If you’re new to command line arguments you need to read this post first.

  51. abdurrahmankutlu August 9, 2018 at 3:26 am #

    This tutorial is awesome in every angle, Thanks man, very informative,

    • Adrian Rosebrock August 9, 2018 at 2:44 pm #

      Thank you for the kind words 🙂

  52. Lokesh Dangi October 5, 2018 at 4:26 am #

    Code for the last two problems (no bubble marked and multiple bubbles marked)

    • Adrian Rosebrock October 8, 2018 at 9:52 am #

      Thanks for sharing, Lokesh! 🙂

  53. Saurabh October 21, 2018 at 7:34 am #

    I am getting error at following line:

    ap.add_argument(“-i”, “–image”, required=True,
    help=”path to the input image”)

    I am replacing –image with image name for ex. “1.png” and “path to the input image” with my path where image is stored for ex. “C:\\Users\\SAURABH\\Desktop”.

  54. zikrie October 31, 2018 at 10:01 am #

    hello sir .. how to matching image on omr sheets paper ,,, but this project u was set the answer key in source code right .. pls give me the solution ,,, thanks

  55. Robert Farmer November 5, 2018 at 4:54 pm #

    Could you possibly help me utilize this code for a more traditional “scantron” style document? I’ve noticed a few issues with them already: If I blur the document almost nothing is left when examining it. I plan on making it to the end of the tutorial without changing too much but I may require assistance and I would be delighted if you would be able to help.

    • Adrian Rosebrock November 6, 2018 at 1:11 pm #

      Hey Robert — this blog post is really meant to be just an example of what you can accomplish with computer vision. It’s not meant to work with Scantron-style cards right out-of-the-box. I’m happy to provide this tutorial for free and if you have any specific questions related to the problems you’re having updating the code I’d be happy to provide suggestions.

  56. Constantin Baumgartner December 5, 2018 at 3:57 pm #

    Hi Adrian,

    I added the logic for skipped questions and multiple answers and finally got it to work. During the process I noticed that the code is EXTREMELY sensitive to input conditions (input being the picture of the test) such as:

    – If the paper isn’t perfectly flat, or close to it, the thresholding/masking produces weird shadows and results in additional contours that mess with the for-loop sequence
    – If I photograph the test with the flash on, the reflective properties of the graphite result in higher pixel values which in turn break the thresholding step and results in some of the filled in circles being interpreted as empty, especially the circles located directly under the lens of the camera because much of the light bounces directly back
    – If I photograph the test without flash, unless I have a light source that isn’t directly above (which was problematic because at the time I was working in an area that only had ceiling lighting), the resulting image has shadows from my hand/camera which again mess with the thresholding/masking

    I finally got it to work by taking a very good photo of the test. I was wondering if you encountered similar problems while writing the tutorial, and if there are any additional steps, aside from fine tuning the thresholding/masking, to deal with this kind of noise that generalize well to different images, i.e. with flash, without, wrinkled paper, etc.?

    For a commercial application I imagine the tests would be scanned and then these artifacts wouldn’t really be an issue, but I’m just curious what you think.

    • Adrian Rosebrock December 6, 2018 at 9:32 am #

      If you are trying to build a commercial grade application you would certainly need to apply more advanced algorithms. This tutorial is meant to be a proof of concept and show you what’s possible with a little bit of computer vision, OpenCV, and a few hours of hacking around. It’s meant to be educational, not necessarily a professional application.

  57. Lyko December 26, 2018 at 12:07 pm #

    Hi Adrian! just want to ask, will it work if i use squares instead of circles?

    • Adrian Rosebrock December 27, 2018 at 10:10 am #

      That is absolutely doable, just follow this guide, specifically the steps on contour approximation and detecting squares.

  58. coder February 6, 2019 at 10:06 am #

    Any idea how to use multiple columns and read it correctly?

  59. sanjog March 6, 2019 at 9:53 am #

    I have rectangular boxes inside of circles .
    How can i detect them?

    • Adrian Rosebrock March 8, 2019 at 5:36 am #

      Is that a type of bubblesheet/OMR system? Or is that just a general computer vision question?

  60. Jack March 17, 2019 at 3:13 am #

    if background is white, it not working?

  61. Fábio May 8, 2019 at 9:13 am #


    Is it possible to port this tutorial to use tensorflow?

    I need a mobile version of this.

    • Adrian Rosebrock May 8, 2019 at 12:47 pm #

      You don’t need TensorFlow for this project, just OpenCV.

  62. Gautam June 12, 2019 at 3:12 am #

    Hi Adrian, can you explain how we can use Hough Circle Transform in OpenCV to detect circles instead of the method you have used?

    • Adrian Rosebrock June 12, 2019 at 1:26 pm #

      You can follow this post on Hough circles and then replace the contour method with the Hough circles method.

  63. DD July 25, 2019 at 5:50 pm #


    I was wondering about your thoughts on this paper I found whilst looking for a OMR type solution: http://academicscience.co.in/admin/resources/project/paper/f201804301525069153.pdf

    It’s from 2018 in what seems like an academic journal of some sort. The pictures looks quite similar. Did you work on this together? As otherwise it’s very dubiously used.

    • Adrian Rosebrock August 7, 2019 at 1:15 pm #

      Ooof. That’s just pure plagiarism. Shame on the paper authors and journal publishers 🙁 Thank you for reporting it.

  64. ali moradi August 10, 2019 at 9:22 am #

    hi Adrian

    how are you ??

    how can i use oval instead of circle in omr sheet and detecting

    • Adrian Rosebrock August 16, 2019 at 5:54 am #

      Take a look at the scikit-image library — it includes a method for detecting ovals/ellipses.

  65. Joe Ciaramitaro November 7, 2019 at 12:04 pm #

    I’m really enjoying these tutorials! Thanks for all your hard work.

    I actually find it helpful that this one has some issues to deal with, as it provides an excellent opportunity to make changes to the code and learn more.

    Additionally, I find it helpful to read through all the comments posted on each tutorial and see the various questions people have asked. This helps me to consider things I may have not yet considered about the specific tutorial and also provides me with some laughs when I read the ridiculous things some people request (hey, you’ve done all this work in providing me with a great free tutorial, can you now also port this code to Java and make it production ready for my commercial endeavour? Thanks in advance!)

    • Adrian Rosebrock November 14, 2019 at 9:39 am #

      Thanks Joe, I really appreciate your comment. It definitely brought a smile to my face and made me feel appreciated. Thanks again!

  66. Bilal January 8, 2020 at 2:51 pm #

    Hi ardian,
    I want to create hybrid android and ios app which can scan bubble sheet by taking pictures but I’m not familiar with hybrid app development. I have searched out but not get the exact answer that which platform should I learn which support all of the functions that you use in this scanning process.

    can you suggest me a platform ? react-native, ionic, flutter , apache-cordova or any other.

    • Adrian Rosebrock January 16, 2020 at 10:44 am #

      Apache Cordova is the simplest method but requires you implement your OpenCV code as a REST API (i.e., the iOS app uploads the image to the server, OpenCV processes it, and returns the result). The downside is of course that it requires a network connection. Otherwise, you should look into React Native.

Before you leave a comment...

Hey, Adrian here, author of the PyImageSearch blog. I'd love to hear from you, but before you submit a comment, please follow these guidelines:

  1. If you have a question, read the comments first. You should also search this page (i.e., ctrl + f) for keywords related to your question. It's likely that I have already addressed your question in the comments.
  2. If you are copying and pasting code/terminal output, please don't. Reviewing another programmers’ code is a very time consuming and tedious task, and due to the volume of emails and contact requests I receive, I simply cannot do it.
  3. Be respectful of the space. I put a lot of my own personal time into creating these free weekly tutorials. On average, each tutorial takes me 15-20 hours to put together. I love offering these guides to you and I take pride in the content I create. Therefore, I will not approve comments that include large code blocks/terminal output as it destroys the formatting of the page. Kindly be respectful of this space.
  4. Be patient. I receive 200+ comments and emails per day. Due to spam, and my desire to personally answer as many questions as I can, I hand moderate all new comments (typically once per week). I try to answer as many questions as I can, but I'm only one person. Please don't be offended if I cannot get to your question
  5. Do you need priority support? Consider purchasing one of my books and courses. I place customer questions and emails in a separate, special priority queue and answer them first. If you are a customer of mine you will receive a guaranteed response from me. If there's any time left over, I focus on the community at large and attempt to answer as many of those questions as I possibly can.

Thank you for keeping these guidelines in mind before submitting your comment.

Leave a Reply