Bank check OCR with OpenCV and Python (Part II)

Today’s blog post is Part II in our two part series on OCR’ing bank check account and routing numbers using OpenCV, Python, and computer vision techniques.

Last week we learned how to extract MICR E-13B digits and symbols from input images. Today we are going to take this knowledge and use it to actually recognize each of the characters, thereby allowing us to OCR the actual bank check and routing number.

To learn how to OCR bank checks with Python and OpenCV, just keep reading.

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

Bank check OCR with OpenCV and Python

In Part I of this series we learned how to localize each of the fourteen MICR E-13B font characters used on bank checks.

Ten of these characters are digits, which form our actual account number and routing number. The remaining four characters are special symbols used by the bank to mark separations between routing numbers, account numbers, and any other information encoded on the check.

The image below displays all fourteen characters that we will be OCR’ing in this tutorial:

Figure 1: The fourteen MICR E-13B characters used in bank checks. We will write Python + OpenCV code to recognize each of these characters.

The list below displays the four symbols:

  • ⑆ Transit (delimit bank branch routing transit #)
  • ⑈ On-us (delimit customer account number)
  • ⑇ Amount (delimit transaction amount)
  • ⑉ Dash (delimit parts of numbers, such as routing or account)

Since OpenCV does not allow us to draw Unicode characters on images, we’ll use the following ASCII character mappings in our code to indicate the Transit, Amount, On-us, and Dash:

  • T = ⑆
  • U = ⑈
  • A = ⑇
  • D = ⑉

Now that we are able to actually localize the digits and symbols, we can apply template matching in a similar manner as we did in our credit card OCR post in order to perform OCR.

Reading account and routing numbers using OpenCV

In order to build our bank check OCR system, we’ll be reusing some of the code from last week. If you haven’t already read Part I of this series, take the time now to go back and read through it — the explanation of the extract_digitis_and_symbols  function is especially important and critical to localizing the bank check characters.

With that said, let’s go ahead and open a new file, name it , and insert the following code:

Lines 2-7 handle our standard imports. If you’re familiar with this blog, these imports should be nothing new. If you don’t have any of these packages on your system, you can perform the following to get them installed:

  1. Install OpenCV using the relevant instructions for your system (while ensuring you’re following any Python virtualenv commands).
  2. Activate your Python virtualenv and install packages:
    1. $ workon cv
    2. $ pip install numpy
    3. $ pip install skimage
    4. $ pip install imutils

Note: for any of the pip commands you may use the --upgrade  flag to update whether or not you already have the software installed.

Now that we’ve got our dependencies installed, let’s quickly review the function covered last week in Part I of this series:

This function has one goal — to find and localize digits and symbols based on contours. This is accomplished via iterating through the contours list, charCnts , and keeping track of the regions of interest and ROI locations ( rois  and locs ) in two lists that are returned at the end of the function.

On Line 29 we check to see if the bounding rectangle of the contour is at least as wide and tall as a digit. If it is, we extract and append the roi  (Lines 31 and 32) followed by appending the location of the ROI to locs  (Line 33). Otherwise, we take the following actions:

In the above code block, we have determined that a contour is part of a special symbol (such as Transit, Dash, etc.). In this case, we take the current contour and the next  two contours (using Python iterators which we discussed last week) on Line 41.

These parts  of a special symbol are looped over so that we can calculate the bounding box for extracting the  roi around all three contours (Lines 46-53). Then, as we did before, we extract the roi  and append it to rois  (Lines 56 and 57) followed by appending its location to locs  (Line 58).

Finally, we need to catch a StopIteration  exception to gracefully exit our function:

Once we have reached the end of the charCnts  list (and there are no further entries in the list), a next  call on charCnts  will result in a StopIteration  exception being throw. Catching this exception allows us to break  from our loop (Lines 62 and 63).

Finally, we return a 2-tuple containing rois  and corresponding locs .

That was a quick recap of the extract_digits_and_symbols  function — for a complete, detailed review, please refer to last week’s blog post.

Now it’s time to get to the new material. First, we’ll go through a couple code blocks that should also be a bit familiar:

Lines 69-74 handle our command line argument parsing. In this script, we’ll make use of both the input --image  and --reference  MICR E-13B font image.

Let’s initialize our special characters (since they can’t be represented with Unicode in OpenCV) as well as pre-process our reference image:

Lines 83 and 84 build a list of the character names including digits and special symbols.

Then, we load the --reference  image while converting to grayscale and resizing, followed by inverse thresholding (Lines 89-93).

Below you can see the output of pre-processing our reference image:

Figure 2: The MICR E-13B font for the digits 0-9 and four special symbols. We will be using this font along with template matching to OCR our bank check images.

Now we’re ready to find and sort contours in ref :

Reference image contours are computed on Lines 97 and 98 followed by updating the refCnts  depending on which OpenCV version we are running (Line 99).

We sort the refCnts  from left to right on Line 100.

At this point, we have our reference contours in an organized fashion. The next step is to extract the digits and symbols followed by building a dictionary of character ROIs:

We call the extract_digits_and_symbols  function on Lines 104 and 105 providing the ref  image and refCnts .

We then initialize a chars  dictionary on Line 106. We populate this dictionary in the loop spanning Lines 109-113. In the dictionary, the character name (key)  is associated with the roi  image (value).

Next, we’ll instantiate a kernel and load and extract the bottom 20% of the check image which contains the account number:

We’ll apply a rectangular kernel to perform some morphological operations (initialized on Line 117). We also initialize an output  list to contain the characters at the bottom of the check. We’ll print these characters to the terminal and also draw them on the check image later.

Lines 123-126 simply load the image , grab the dimensions, and extract the bottom 20% of the check image.

Note: This is not rotation invariant — if your check could possibly be rotated, appearing upside down or vertical, then you will need to add logic in to rotate it first. Applying a top-down perspective transform on the check (such as in our document scanner post) can help with task.

Below you can find our example check input image:

Figure 3: The example input bank check that we are going to OCR and extract the routing number and account number from (source).

Next, let’s convert the check to grayscale and apply a morphological transformation:

On Line 131 we convert the bottom of the check image to grayscale and on Line 132 we use the blackhat morphological operator to find dark regions against a light background. This operation makes use of our rectKernel .

The result reveal our account and routing numbers:

Figure 5: Applying black hat morphological operation reveals our bank account number and routing number from the rest of the check.


Now let’s compute the Scharr gradient in the x-direction:

Using our blackhat operator, we compute the Scharr gradient with the cv2.Sobel  function (Lines 136 and 137). We take the element-wise absolute value of gradX  on on Line 138.

Then we scale the gradX  to the range [0-255] on Lines 139-141:

Figure 6: Computing the Scharr gradient magnitude representation of the bank check image reveals vertical changes in the gradient.

Let’s see if we can close the gaps between the characters and binarize the image:

On Line 146, we utilize our kernel again while applying a closing operation. We follow this by performing a binary threshold on Lines 147 and 148.

The result of this operation can be seen below:

Figure 7: Thresholding our gradient magnitude representation reveals possible regions that contain the bank check account number and routing number.

When pre-processing a check image our morphological + thresholding operations will undoubtedly leave “false-positive” detection regions — we can apply a bit of extra processing to help remove these operations:

Line 152 simply clears the border by removing image border pixels; the result is subtle but will prove to be very helpful:

Figure 8: To help remove noise we can clear any connected components that lie on the border of the image.

As the image above displays, we have clearly found our three groupings of numbers on the check. But how did we go about actually extracting each of the individual groups? The following code block will show us how:

On Lines 156-158 we find our contours also take care of the pesky OpenCV version incompatibility.

Next, we initialize a list to contain our number group locations (Line 159).

Looping over the groupCnts , we determine the contour bounding box (Line 164), and check to see if the box parameters qualify as a grouping of characters — if they are, we append the ROI values to groupLocs  (Lines 168 and 169).

Using lambdas, we sort the digit locations from left to right (Line 172).

Our group regions are shown on this image:

Figure 9: Applying contour filtering allows us to find the (1) account number, (2) routing number, and (3) additional information groups on the bank check.

Next, let’s loop over the group locations:

In the loop, first, we initialize a groupOutput  list which will later be appended to the output  list (Line 177).

Subsequently, we extract the character grouping ROI from the image (Line 182) and threshold it (Lines 183 and 184).

For developmental and debugging purposes (Lines 186 and 187) we show the group to the screen and wait for a keypress before moving onward (feel free to remove this code from your script if you so wish).

We find and sort character contours within the group on Lines 191-195. The results of this step are shown in Figure 10.


Figure 10: By using the (x, y)-coordinates of the locations, we can extract each group from the thresholded image. Given the group, contour detection allows us to detect each individual character.

Now, let’s extract digits and symbols with our function and then loop over the rois :

On Line 198, we provide the group  and charCnts  to the extract_digits_and_symbols  function, which returns rois  and locs .

We loop over the rois , first initializing a template matching score list, followed by resizing the roi  to known dimensions.

We loop over the character names and perform template matching which compares the query image roi  to the possible character images (they are stored in the chars  dictionary and indexed by charName ) on Lines 212 and 213.

To extract a template matching score  for this operation, we use the cv2.minMaxLoc  function, and subsequently, we append it to scores  on Line 215.

The last step in this code block is to take the maximum score  from scores  and use it to find the character name — we append the result to groupOutput  (Line 220).

You can read more about this template matching-based approach to OCR in our previous blog post on Credit Card OCR.

Next, we’ll draw on the original image  append the groupOutput  result to a list named output .

Lines 224 and 225 handle drawing a red rectangle around the groups  and Lines 226-228 draw the group output characters (routing, checking, and check numbers) on the image.

Finally, we append the groupOutput  characters to an output  string (Line 231).

Our final step is to write the OCR text to our terminal and display the final output image:

We print the OCR results to the terminal, display the image to the screen, and wait until a key is pressed to exit on Lines 234-236.

Let’s see how our bank check OCR system performs in the next section.

Bank check OCR results

To apply our bank check OCR algorithm, make sure you use the “Downloads” section of this blog post to download the source code + example image.

From there, execute the following command:

The results of our hard work can be seen below:

Figure 11: Using OpenCV and Python, we have been able to correctly OCR our bank account number and routing number from an image of a check.

Improving our bank check OCR system

In this particular example, we were able to get away with using basic template matching as our character recognition algorithm.

However, template matching is not the most reliable method for character recognition, especially for real-world images that are likely to be much noisier and harder to segment.

In these cases, it would be best to train your own HOG + Linear SVM classifier or a Convolutional Neural Network. To accomplish this, you’ll want to create a dataset of check images and manually label and extract each digit in the image. I would recommend having 1,000-5,000 digits per character and then training your classifier.

From there, you’ll be able to enjoy much higher character classification accuracy — the biggest problem is simply creating/obtaining such a dataset.

Since checks by their very nature contain sensitive information, it’s often hard to find a dataset that is not only (1) representative of real-world bank check images but is also (2) cheap/easy to license.

Many of these datasets belong to the banks themselves, making it hard for computer vision researchers and developers to work with them.


In today’s blog post we learned how to apply back check OCR to images using OpenCV, Python, and template matching. In fact, this is the same method that we used for credit card OCR — the primary difference is that we had to take special care to extract each MICR E-13B symbol, especially when these symbols contain multiple contours.

However, while our template matching method worked correctly on this particular example image, real-world inputs are likely to be much more noisy, making it harder for us to extract the digits and symbols using simple contour techniques.

In these situations, it would be best to localize each of the digits and characters followed by applying machine learning to obtain higher digit classification accuracy. Methods such as Histogram of Oriented Gradients + Linear SVM and deep learning will obtain better digit and symbol recognition accuracy on real-world images that contain more noise.

If you are interested in learning more about HOG + Linear SVM along with deep learning, be sure to take a look at the PyImageSearch Gurus course.

And before you go, be sure to enter your email address in the form below to be notified when future blog posts are published!


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!

, , ,

15 Responses to Bank check OCR with OpenCV and Python (Part II)

  1. Dan July 31, 2017 at 11:23 am #

    Cool. Can this functionality be reproduced w R?

    • Adrian Rosebrock August 1, 2017 at 9:40 am #

      Hi Dan — I don’t have much experience with the R programming language so I’m not sure if this method could be easily reproduced with R. Check to see what types of OpenCV bindings R has (if any).

  2. Lucas Pereira July 31, 2017 at 5:55 pm #

    Could it be reasonable with CMC7 font too ?

    • Adrian Rosebrock August 1, 2017 at 9:36 am #

      The CMC7 font is a little more challenging due to the vertical lines, but yes, the same technique could be applied.

  3. Sshubham October 11, 2017 at 2:49 am #

    hey Adrian,

    Thanks for writing the post.

    I am working on the similar project, I also want to extract other feature from the check (Date, handwritten digits in the amount, name of the cheque holder)

    Can you let me know how to do that with the help of Open CV.


    • Adrian Rosebrock October 13, 2017 at 8:58 am #

      What you’re referring to is handwriting recognition. I would suggest using the Google Vision API as a first attempt and seeing how far that gets you. Creating your own OCR system for unconstrained handwriting recognition is quite challenging. I do not recommend implementing it from scratch.

      • Shubham joshi October 16, 2017 at 11:01 am #

        Thanks Adrian. 🙂

  4. Kleyson Rios November 25, 2017 at 3:19 pm #

    Hi Adrian,

    Thank you so much for the blog posts.

    I am starting now studying and understanding this new area.

    I would like to develop a program to extract text from receipts and recognize them.

    The problem that We can find different fonts types in those receipts. For this case should we use HOG and linear SVG ? What should I use to train the model ? Is there already some database that could be used ?

    Best Regards.

    • Adrian Rosebrock November 27, 2017 at 1:14 pm #

      HOG + Linear SVM would be a good start if your fonts are similar. If your fonts vary dramatically you will likely need a more advanced object detection method likely relying on deep learning. Take a look at the HOG + Linear SVM post I linked to earlier in this comment and from there take a look at the PyImageSearch Gurus course where I cover how to implement and train your own object detectors. I hope that helps!

  5. sk February 15, 2018 at 4:02 am #

    getting error list object has no attribute sort contours in line 103

  6. Vernika July 27, 2019 at 7:52 pm #


    1. Why you have used (17,7) kernel size?
    2. How did you decide values for minimum width and minimum height?
    3. How did you decide when to perform which morphological operation. specially blackhat operation?

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

      Hey Vernika — I provide my suggestions and best practices when defining those values inside the PyImageSearch Gurus course. I would suggest you start there.

  7. swathi October 23, 2019 at 5:54 am #

    Hai Andrian, I am using this model for indian bank checks to extract MICR code, but not getting the output. It is showing original image itself. can u pls suggest how can i work on it.

  8. Apoorv Srivastava November 21, 2019 at 7:20 am #

    I also need to know that if my MICR contains 4 groups instead of 3 then what should I do?

    • Adrian Rosebrock November 21, 2019 at 8:59 am #

      You could examine the contours and ensure there are four groups by checking the length of your filtered contours.

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