Skip to content

Commit

Permalink
Committed
Browse files Browse the repository at this point in the history
  • Loading branch information
Gautya18 committed May 15, 2020
2 parents 826393e + af7c8f0 commit 5115a9a
Show file tree
Hide file tree
Showing 2 changed files with 105 additions and 4 deletions.
101 changes: 99 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,101 @@
# BubbleGrader

BubbleGrader is a tool to analyse and detect the bubbles filled in OMR answer sheets and accordingly grade the answer sheet.
This tool uses opencv library from python.
> BubbleGrader is a tool to analyse and detect the bubbles filled in OMR answer sheets and accordingly grade the answer sheet.
## What is OMR?
OMR stands for **Optical Mark Recognition** is the process of automatically analyzing human-marked documents and interpreting their results.

## Tools and libraries used
- opencv
- numpy
- imutils

To install all these dependencies, use `python -m pip install requirements.txt`.
**Requirements.txt** is provided with this repository.

## Steps
1. Detect the exam paper from the image
2. Apply a perspective transform to extract the top-down, birds eye view of the exam sheet.
3. Extract the set of bubbles from the perspective transformed exam sheet.
4. Sort the bubbles from the exam sheet into rows. 1 row corresponding to 1 question.
4. Determine the marked bubbles from each row.
5. Cross-check the marked answer with the answer key.
6. Repeat this process for all rows.


## Implementation
The line 16 from the file *test_grader.py* defines the answer key for our exam.
```python
ANSWER_KEY = { 0:1, 1:4, 2:0, 3:3, 4:1 }
```
It is basically a dictionary.
Right from line 18 to 25 we preprocess the image.
```python
if args["crop"] == 1:
image = imutils.resize(cv2.imread(args["image"]), width=700)
else:
image = cv2.imread(args["image"])
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
blurred = cv2.GaussianBlur(gray, (5,5), 0)
edged = cv2.Canny(blurred, 75, 200)
```
By preprocessing, I mean, resizing, blurring, converting the image to grayscale and edge detection.
Each of these processes help reduce the complexity of the further operations to be done on the image.
>Images of the preprocessing done.
The next step is to identify the exam sheet present in the image. Here, we use contours to find the exam sheet.
```python
cnts = cv2.findContours(edged.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
cnts = imutils.grab_contours(cnts)
```
After this, the cnts variable stores a list of contours. Now, in order to identify the exam paper, (which will be a rectangle) we loop through the contours to find a contour having **4** corner points. This contour will correspond to the exam sheet. The code to achieve this is
```python
for cnt in cnts:
perimeter = cv2.arcLength(cnt, True)
approx = cv2.approxPolyDP(cnt, 0.02*perimeter, True) #approximating the contour to 2% of the found contour.

if len(approx) == 4:
docCnt = approx
break
```
The function `cv2.approxPolyDP` approximates the contour ``cnt`` to 2% of the original contour. For more info about this function, visit [here](https://docs.opencv.org/2.4/modules/imgproc/doc/structural_analysis_and_shape_descriptors.html).
Variable `docCnt` now has the contour of the exam paper.
> Image of exam contour detected.
Now, we apply perspective transform on the image to get a birds eye view.
```python
warped = four_point_transform(gray, docCnt.reshape((4, 2)))
```
After this, we threshold the image using Binary / Otsu threshold
> Thresholded image

Binarization again allows us to find contours from the image. This time, the contours will be the bubbles from the image. We append all such contours in a list called `questionContours`
```python
for c in cnts:
(x, y, w, h) = cv2.boundingRect(c)
aspect_ratio = w / float(h)

if w >= 20 and h >= 20 and aspect_ratio >= 0.9 and aspect_ratio <= 1.1:
questionContours.append(c)
```
Here, we calculate the bounding box around each contour and check whether it is actually a bubble by checking aspect ratio and width-height. After validations, we append the contour to `questionContours`.
```python
for (q, i) in enumerate(np.arange(0, len(question_cnts), 5)):
cnts = contours.sort_contours(question_cnts[i: i+5])[0]
bubbled = None

for (j, c) in enumerate(cnts):
mask = np.zeros(thresh.shape, dtype='uint8')
cv2.drawContours(mask, [c], -1, 255, -1)

mask = cv2.bitwise_and(thresh, thresh, mask=mask)
total = cv2.countNonZero(mask)

if bubbled is None or bubbled[0] < total:
bubbled = (total, j)

```
This arranges the contours in rows, each row corresponding to each question and identifies the filled bubble. `total = cv2.countNonZero(mask)` counts number of non zero pixels in the contour. After this, we compare the filled bubble index to that of the correct answer in our `ANSWER_KEY` and print the corresponding result and draw the appropriate contour on the image.

> Final image.
8 changes: 6 additions & 2 deletions test_grader.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,19 @@

# create argument parser
ap = argparse.ArgumentParser()
ap.add_argument('-i', '--image', help="path to the image")
ap.add_argument('-i', '--image', required=True, help="path to the image")
ap.add_argument('-c', '--crop', type=int, help="whether to crop image 1 if yes 0 if no")
args = vars(ap.parse_args())

#define the answer key
#answer key is a dictionary with zero based indexing fot the question and answers
ANSWER_KEY = { 0:1, 1:4, 2:0, 3:3, 4:1 }

#preprocess the image
image = cv2.imread(args["image"])
if args["crop"] == 1:
image = imutils.resize(cv2.imread(args["image"]), width=700)
else:
image = cv2.imread(args["image"])
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
blurred = cv2.GaussianBlur(gray, (5,5), 0)
edged = cv2.Canny(blurred, 75, 200)
Expand Down

0 comments on commit 5115a9a

Please sign in to comment.