OCR with OpenCV, Tesseract, and Python [Practitioner Edition 1.0]

5,134 980 54MB

English Pages 324 Year 2021

Report DMCA / Copyright

DOWNLOAD FILE

Polecaj historie

OCR with OpenCV, Tesseract, and Python [Practitioner Edition 1.0]

Citation preview

OCR with OpenCV, Tesseract, and Python OCR Practitioner Bundle, 1st Edition (version 1.0)

Adrian Rosebrock, PhD Abhishek Thanki Sayak Paul Jon Haase

py i mages ear ch

The contents of this book, unless otherwise indicated, are Copyright ©2020 Adrian Rosebrock, PyImageSearch.com. All rights reserved. Books like this are made possible by the time invested by the authors. If you received this book and did not purchase it, please consider making future books possible by buying a copy at https://www.pyimagesearch.com/ books-and-courses/ today.

© 2020 PyImageSearch

Contents Contents

iii

Companion Website

xiii

Acronyms

xv

1 Introduction

1

1.1

Book Organization . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

2

1.2

What You Will Learn . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

2

1.3

Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

3

2 Training an OCR Model with Keras and TensorFlow

5

2.1

Chapter Learning Objectives . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

5

2.2

OCR with Keras and TensorFlow . . . . . . . . . . . . . . . . . . . . . . . . . . .

6

2.2.1

Our Digits and Letters Dataset . . . . . . . . . . . . . . . . . . . . . . .

6

2.2.2

Project Structure . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

7

2.2.3

Implementing Our Dataset Loader Functions . . . . . . . . . . . . . . .

8

2.2.4

Implementing Our Training Script . . . . . . . . . . . . . . . . . . . . . . 11

2.2.5

Training Our Keras and TensorFlow OCR Model . . . . . . . . . . . . . . 19

2.3

Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21

3 Handwriting Recognition with Keras and TensorFlow

23

3.1

Chapter Learning Objectives . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24

3.2

OCR’ing Handwriting with Keras and TensorFlow . . . . . . . . . . . . . . . . . . 24 3.2.1

What Is Handwriting Recognition? . . . . . . . . . . . . . . . . . . . . . 25

3.2.2

Project Structure . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26

3.2.3

Implementing Our Handwriting Recognition Script . . . . . . . . . . . . . 26

3.2.4

Handwriting Recognition Results . . . . . . . . . . . . . . . . . . . . . . 32

3.2.5

Limitations, Drawbacks, and Next Steps . . . . . . . . . . . . . . . . . . 35 iii

iv

CONTENTS

3.3

Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 36

4 Using Machine Learning to Denoise Images for Better OCR Accuracy

37

4.1

Chapter Learning Objectives . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 38

4.2

Image Denoising with Machine Learning . . . . . . . . . . . . . . . . . . . . . . 38 4.2.1

Our Noisy Document Dataset . . . . . . . . . . . . . . . . . . . . . . . . 39

4.2.2

The Denoising Document Algorithm . . . . . . . . . . . . . . . . . . . . 40

4.2.3

Project Structure . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 42

4.2.4

Implementing Our Configuration File . . . . . . . . . . . . . . . . . . . . 44

4.2.5

Creating Our Blur and Threshold Helper Function . . . . . . . . . . . . . 45

4.2.6

Implementing the Feature Extraction Script . . . . . . . . . . . . . . . . 47

4.2.7

Running the Feature Extraction Script . . . . . . . . . . . . . . . . . . . 51

4.2.8

Implementing Our Denoising Training Script . . . . . . . . . . . . . . . . 52

4.2.9

Training Our Document Denoising Model . . . . . . . . . . . . . . . . . . 54

4.2.10 Creating the Document Denoiser Script . . . . . . . . . . . . . . . . . . 54 4.2.11 Running our Document Denoiser . . . . . . . . . . . . . . . . . . . . . . 57 4.3

Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 59

5 Making OCR "Easy" with EasyOCR

61

5.1

Chapter Learning Objectives . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 62

5.2

Getting Started with EasyOCR . . . . . . . . . . . . . . . . . . . . . . . . . . . . 62

5.3

5.4

5.2.1

What Is EasyOCR? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 62

5.2.2

Installing EasyOCR . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 63

Using EasyOCR for Optical Character Recognition . . . . . . . . . . . . . . . . . 64 5.3.1

Project Structure . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 64

5.3.2

Implementing our EasyOCR Script . . . . . . . . . . . . . . . . . . . . . 65

5.3.3

EasyOCR Results . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 67

Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 70

6 Image/Document Alignment and Registration

71

6.1

Chapter Learning Objectives . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 72

6.2

Document Alignment and Registration with OpenCV . . . . . . . . . . . . . . . . 72 6.2.1

What Is Document Alignment and Registration? . . . . . . . . . . . . . . 72

6.2.2

How Can OpenCV Be Used for Document Alignment? . . . . . . . . . . 74

6.2.3

Project Structure . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 76

6.2.4

Aligning Documents with Keypoint Matching . . . . . . . . . . . . . . . . 77

6.2.5

Implementing Our Document Alignment Script . . . . . . . . . . . . . . . 81

CONTENTS

6.2.6 6.3

v

Image and Document Alignment Results . . . . . . . . . . . . . . . . . . 83

Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 86

7 OCR’ing a Document, Form, or Invoice 7.1

7.2

87

Automatically Registering and OCR’ing a Form . . . . . . . . . . . . . . . . . . . 88 7.1.1

Why Use OCR on Forms, Invoices, and Documents? . . . . . . . . . . . 88

7.1.2

Steps to Implementing a Document OCR Pipeline with OpenCV and Tesseract . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 88

7.1.3

Project Structure . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 91

7.1.4

Implementing Our Document OCR Script with OpenCV and Tesseract . 91

7.1.5

OCR Results Using OpenCV and Tesseract . . . . . . . . . . . . . . . . 99

Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 103

8 Sudoku Solver and OCR

105

8.1

Chapter Learning Objectives . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 106

8.2

OpenCV Sudoku Solver and OCR . . . . . . . . . . . . . . . . . . . . . . . . . . 106 8.2.1

How to Solve Sudoku Puzzles with OpenCV and OCR . . . . . . . . . . 107

8.2.2

Configuring Your Development Environment to Solve Sudoku Puzzles with OpenCV and OCR . . . . . . . . . . . . . . . . . . . . . . . . . . . 108

8.2.3

Project Structure . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 108

8.2.4

SudokuNet: A Digit OCR Model Implemented in Keras and TensorFlow . 109

8.2.5

Implementing with Keras and TensorFlow . . . . . . . . . . . . . . . . . 111

8.2.6

Training Our Sudoku Digit Recognizer with Keras and TensorFlow . . . . 114

8.2.7

Finding the Sudoku Puzzle Board in an Image with OpenCV . . . . . . . 115

8.2.8

Extracting Digits from a Sudoku Puzzle with OpenCV . . . . . . . . . . . 121

8.2.9

Implementing Our OpenCV Sudoku Puzzle Solver . . . . . . . . . . . . 123

8.2.10 OpenCV Sudoku Puzzle Solver OCR Results . . . . . . . . . . . . . . . 128 8.2.11 Credits . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 130 8.3

Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 130

9 Automatically OCR’ing Receipts and Scans

131

9.1

Chapter Learning Objectives . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 132

9.2

OCR’ing Receipts with OpenCV and Tesseract . . . . . . . . . . . . . . . . . . . 132

9.3

9.2.1

Project Structure . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 132

9.2.2

Implementing Our Receipt Scanner . . . . . . . . . . . . . . . . . . . . . 132

9.2.3

Receipt Scanner and OCR Results . . . . . . . . . . . . . . . . . . . . . 140

Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 142

vi

CONTENTS

10 OCR’ing Business Cards

143

10.1 Chapter Learning Objectives . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 143 10.2 Business Card OCR . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 144 10.2.1 Project Structure . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 144 10.2.2 Implementing Business Card OCR . . . . . . . . . . . . . . . . . . . . . 144 10.2.3 Business Card OCR Results . . . . . . . . . . . . . . . . . . . . . . . . . 149 10.3 Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 152 11 OCR’ing License Plates with ANPR/ALPR

153

11.1 Chapter Learning Objectives . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 154 11.2 ALPR/ANPR and OCR . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 154 11.2.1 What Is Automatic License Plate Recognition? . . . . . . . . . . . . . . 155 11.2.2 Project Structure . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 156 11.2.3 Implementing ALPR/ANPR with OpenCV and OCR . . . . . . . . . . . . 157 11.2.4 Debugging Our Computer Vision Pipeline . . . . . . . . . . . . . . . . . 158 11.2.5 Locating Potential License Plate Candidates . . . . . . . . . . . . . . . . 159 11.2.6 Pruning License Plate Candidates . . . . . . . . . . . . . . . . . . . . . 164 11.2.7 Defining Our Tesseract ANPR Options . . . . . . . . . . . . . . . . . . . 167 11.2.8 The Heart of the ANPR Implementation . . . . . . . . . . . . . . . . . . 168 11.2.9 Creating Our ANPR and OCR Driver Script . . . . . . . . . . . . . . . . 169 11.2.10 Automatic License Plate Recognition Results . . . . . . . . . . . . . . . 172 11.2.11 Limitations and Drawbacks . . . . . . . . . . . . . . . . . . . . . . . . . 174 11.3 Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 174 12 Multi-Column Table OCR

177

12.1 Chapter Learning Objectives . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 178 12.2 OCR’ing Multi-Column Data . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 178 12.2.1 Our Multi-Column OCR Algorithm . . . . . . . . . . . . . . . . . . . . . . 178 12.2.2 Project Structure . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 180 12.2.3 Installing Required Packages . . . . . . . . . . . . . . . . . . . . . . . . 181 12.2.4 Implementing Multi-Column OCR . . . . . . . . . . . . . . . . . . . . . . 182 12.2.5 Multi-Column OCR Results . . . . . . . . . . . . . . . . . . . . . . . . . 192 12.3 Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 195 13 Blur Detection in Text and Documents

197

13.1 Chapter Learning Objectives . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 197 13.2 Detecting Blurry Text and Documents . . . . . . . . . . . . . . . . . . . . . . . . 198

CONTENTS

vii

13.2.1 What Is Blur Detection and Why Do We Want to Detect Blur? . . . . . . 198 13.2.2 What Is the Fast Fourier Transform? . . . . . . . . . . . . . . . . . . . . 200 13.3 Implementing Our Text Blur Detector

. . . . . . . . . . . . . . . . . . . . . . . . 201

13.3.1 Project Structure . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 201 13.3.2 Creating the Blur Detector Helper Function . . . . . . . . . . . . . . . . 202 13.3.3 Detecting Text and Document Blur . . . . . . . . . . . . . . . . . . . . . 204 13.3.4 Blurry Text and Document Detection Results . . . . . . . . . . . . . . . . 206 13.4 Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 207 14 OCR’ing Video Streams

209

14.1 Chapter Learning Objectives . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 209 14.2 OCR’ing Real-Time Video Streams . . . . . . . . . . . . . . . . . . . . . . . . . 210 14.2.1 Project Structure . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 210 14.2.2 Implementing Our Video Writer Utility . . . . . . . . . . . . . . . . . . . . 211 14.2.3 Implementing Our Real-Time Video OCR Script . . . . . . . . . . . . . . 214 14.2.4 Real-Time Video OCR Results . . . . . . . . . . . . . . . . . . . . . . . 222 14.3 Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 223 15 Improving Text Detection Speed with OpenCV and GPUs

225

15.1 Chapter Learning Objectives . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 225 15.2 Using Your GPU for OCR with OpenCV . . . . . . . . . . . . . . . . . . . . . . . 226 15.2.1 Project Structure . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 226 15.2.2 Implementing Our OCR GPU Benchmark Script . . . . . . . . . . . . . . 226 15.2.3 Speed Test: OCR With and Without GPU . . . . . . . . . . . . . . . . . 229 15.2.4 OCR on GPU for Real-Time Video Streams . . . . . . . . . . . . . . . . 230 15.2.5 GPU and OCR Results . . . . . . . . . . . . . . . . . . . . . . . . . . . . 235 15.3 Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 235 16 Text Detection and OCR with Amazon Rekognition API

237

16.1 Chapter Learning Objectives . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 238 16.2 Amazon Rekognition API for OCR . . . . . . . . . . . . . . . . . . . . . . . . . . 238 16.2.1 Obtaining Your AWS Rekognition Keys . . . . . . . . . . . . . . . . . . . 239 16.2.2 Installing Amazon’s Python Package . . . . . . . . . . . . . . . . . . . . 239 16.2.3 Project Structure . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 239 16.2.4 Creating Our Configuration File . . . . . . . . . . . . . . . . . . . . . . . 240 16.2.5 Implementing the Amazon Rekognition OCR Script . . . . . . . . . . . . 240 16.2.6 Amazon Rekognition OCR Results . . . . . . . . . . . . . . . . . . . . . 244

viii

CONTENTS

16.3 Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 246 17 Text Detection and OCR with Microsoft Cognitive Services

249

17.1 Chapter Learning Objectives . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 249 17.2 Microsoft Cognitive Services for OCR . . . . . . . . . . . . . . . . . . . . . . . . 250 17.2.1 Obtaining Your Microsoft Cognitive Services Keys . . . . . . . . . . . . . 250 17.2.2 Project Structure . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 250 17.2.3 Creating Our Configuration File . . . . . . . . . . . . . . . . . . . . . . . 251 17.2.4 Implementing the Microsoft Cognitive Services OCR Script . . . . . . . 251 17.2.5 Microsoft Cognitive Services OCR Results . . . . . . . . . . . . . . . . . 255 17.3 Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 259 18 Text Detection and OCR with Google Cloud Vision API

261

18.1 Chapter Learning Objectives . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 261 18.2 Google Cloud Vision API for OCR . . . . . . . . . . . . . . . . . . . . . . . . . . 262 18.2.1 Obtaining Your Google Cloud Vision API Keys . . . . . . . . . . . . . . . 262 18.2.1.1 Prerequisite . . . . . . . . . . . . . . . . . . . . . . . . . . . . 262 18.2.1.2 Steps to Enable Google Cloud Vision API and Download Credentials . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 262 18.2.2 Configuring Your Development Environment for the Google Cloud Vision API . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 262 18.2.3 Project Structure . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 263 18.2.4 Implementing the Google Cloud Vision API Script . . . . . . . . . . . . . 263 18.2.5 Google Cloud Vision API OCR Results . . . . . . . . . . . . . . . . . . . 267 18.3 Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 269 19 Training a Custom Tesseract Model

271

19.1 Chapter Learning Objectives . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 272 19.2 Your Tesseract Training Development Environment . . . . . . . . . . . . . . . . . 272 19.2.1 Step #1: Installing Required Packages . . . . . . . . . . . . . . . . . . . 273 19.2.1.1 Ubuntu . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 273 19.2.1.2 macOS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 273 19.2.2 Step #2: Building and Installing Tesseract Training Tools . . . . . . . . . 274 19.2.2.1 Ubuntu . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 274 19.2.2.2 macOS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 275 19.2.3 Step #3: Cloning tesstrain and Installing Requirements . . . . . . . . 276 19.3 Training Your Custom Tesseract Model . . . . . . . . . . . . . . . . . . . . . . . 277

CONTENTS

ix

19.3.1 Project Structure . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 277 19.3.2 Our Tesseract Training Dataset . . . . . . . . . . . . . . . . . . . . . . . 278 19.3.3 Creating Your Tesseract Training Dataset . . . . . . . . . . . . . . . . . . 279 19.3.4 Step #1: Setup the Base Model . . . . . . . . . . . . . . . . . . . . . . . 280 19.3.5 Step #2: Set Your TESSDATA_PREFIX . . . . . . . . . . . . . . . . . . . 281 19.3.6 Step #3: Setup Your Training Data . . . . . . . . . . . . . . . . . . . . . 282 19.3.7 Step #4: Fine-Tune the Tesseract Model . . . . . . . . . . . . . . . . . . 283 19.3.8 Training Tips and Suggestions . . . . . . . . . . . . . . . . . . . . . . . . 286 19.4 Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 287 20 OCR’ing Text with Your Custom Tesseract Model

289

20.1 Chapter Learning Objectives . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 289 20.2 OCR with Your Custom Tesseract Model . . . . . . . . . . . . . . . . . . . . . . 289 20.2.1 Project Structure . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 290 20.2.2 Implementing Your Tesseract OCR Script . . . . . . . . . . . . . . . . . 290 20.2.3 Custom Tesseract OCR Results . . . . . . . . . . . . . . . . . . . . . . . 292 20.3 Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 296 21 Conclusions

297

21.1 Where to Now? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 299 21.2 Thank You . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 300 Bibliography

301

To my first computer science teacher, Marla Wood. Thank you for everything you’ve done for me when I was a high school kid. Rest assured, wherever you are, you’re a big part and a big reason for the person I am today.

Companion Website Thank you for picking up a copy of OCR with OpenCV, Tesseract, and Python! To accompany this book, I have created a companion website which includes: • Up-to-date installation instructions on how to configure your environment for OCR development • Instructions on how to use the pre-configured Oracle VirtualBox Virtual Machine on your system • Supplementary materials that we could not fit inside this book, including how to use pre-configured environments • Frequently Asked Questions (FAQs) and their suggested fixes and solutions • Access to the PyImageSearch Community Forums for discussion about OCR and other computer vision and deep learning topics (Expert Bundle only) You can find the companion website here: http://pyimg.co/ocrcw

xiii

Acronyms ALPR: automatic license plate recognition Amazon S3: Amazon Simple Storage Service ANPR: automatic number plate recognition API: application programming interface ASCII: American Standard Code for Information Interchange ATR: automatic target recognition AWS: Amazon Web Services BGR: Blue Green Red cd: change directory CPU: central processing unit CRNN: convolutional recurrent neural network CSV: comma-separated values CTC: connectionist temporal classification d: dimensional DFT: discrete Fourier transform DL4CV: Deep Learning for Computer Vision with Python dnn: deep neural network EAST: efficient and accurate scene text detector FFT: fast Fourier transform FPS: frames per second GIF: graphics interchange format xv

xvi

GCP: Google Cloud Platform GFTT: good features to track GIMP: GNU image manipulation program GPU: graphics processing unit gt: ground-truth GUI: graphical user interface HAC: hierarchical agglomerative clustering HDF5: Hierarchical Data Format version 5 HOG: histogram of oriented gradient HP: Hewlett-Packard I/O: input/output IDE: integrated development environment JSON: JavaScript Object Notation LSTM: long short-term memory maxAR: maximum aspect ratio MCS: Microsoft Cognitive Services minAR: minimum aspect ratio mph: miles per hour NLP: natural language processing NMS: non-maximum suppression OCR: optical character recognition OpenCV: Open Source Computer Vision Library ORB: oriented FAST and rotated BRIEF PIL: Python Imaging Library PNG: portable network graphics PSM: page segmentation method PSM: page segmentation mode

Acronyms

Acronyms

R-CNN: regions-convolutional neural network RANSAC: random sample consensus REST: representational state transfer RFR: random forest regressor RGB: Red Green Blue RMSE: root-mean-square error RNN: recurrent neural network ROI: region of interest SDK: software development kit segROI: segment ROI SGD: stochastic gradient descent SIFT: scale-invariant feature transform sym-link: symbolic link SPECT: single-photon emission computerized tomography SSD: single-shot detectors SVM: support vector machine UMBC: University of Maryland, Baltimore County URL: Uniform Resource Locator VM: virtual machine YOLO: you only look once

xvii

Chapter 1

Introduction Welcome to the “OCR Practitioner Bundle” of OCR with OpenCV, Tesseract, and Python! Take a second to congratulate yourself on completing the “OCR Starter” Bundle — you now have a strong foundation to learn more advanced optical character recognition (OCR). While the “OCR with OpenCV, Tesseract, and Python: Intro to OCR” book focused primarily on image processing and computer vision techniques, with only a hint of deep learning, this volume flips the relationship — here, we’ll be focusing primarily on deep learning-based OCR techniques. Computer vision and image processing algorithms will be used, but mainly to facilitate deep learning. Additionally, this volume starts to introduce solutions to real-world OCR problems, including: • Automatic license/number plate recognition • OCR’ing and scanning receipts • Image/document alignment and registration • Building software to OCR forms, invoices, and other documents. • Automatically denoising images with machine learning (to improve OCR accuracy) Just like the first OCR book, “OCR with OpenCV, Tesseract, and Python: Intro to OCR,” I place a strong emphasis on learning by doing. It’s not enough to follow this text — you need to open your terminal/code editor and execute the code to learn. Merely reading a book or watching a tutorial is a form of passive learning. You only involve two of your senses: eyes and ears. Instead, what you need to master this material is active learning. Use your eyes and ears as you follow along, but then take the next step to involve tactile sensations — use your keyboard to write code and run sample scripts. 1

2

Chapter 1. Introduction

Then use your highly intelligent mind (you wouldn’t be reading this book if you weren’t smart) to critically analyze the results. What would happen if you tweak this parameter over here? What if you adjusted the learning rate of your neural network? Or what happens if you apply the same technique to a different dataset? The more senses and experiences you can involve in your learning, the easier it will be for you to master OCR.

1.1

Book Organization

The “Intro to OCR” Bundle was organized linearly, taking a stair-step approach to learning OCR. You would learn a new OCR technique, apply it to a set of sample images, and then learn how to extend that approach in the next chapter to improve accuracy and learn a new skill. This volume is organized slightly differently. You’ll still learn linearly; however, I’ve organized the chapters based on case studies that build on each other. Doing so allows you to jump to a set of chapters that you’re specifically interested in — ideally, these chapters will help you complete your work project, perform novel research, or submit your final graduation project. I still recommend that you read this book from start to finish (as that will ensure you maximize value), but at the same time, don’t feel constrained to read it linearly. This book is meant to be your playbook to solve real-world optical character recognition problems.

1.2

What You Will Learn

As stated in the previous section, the “OCR Practitioner” Bundle takes a “project-based” approach to learn OCR. You will improve your OCR skills by working on actual real-world projects, including: • Training a machine learning model to denoise documents, thereby improving OCR results • Performing automatic image/document alignment and registration • Building a project that uses image alignment and registration to OCR a structured document, such as a form, invoice, etc. • Solving Sudoku puzzles with OCR • Scanning and OCR’ing receipts • Building an automatic license/number plate recognition (ALPR/ANPR) system

1.3. Summary

3

• OCR’ing real-time video streams • Performing handwriting recognition • Leveraging GPUs to improve text detection and OCR speed • Training and fine-tuning Tesseract models on your custom datasets By the end of this book, you will have the ability to approach real-world OCR projects confidently.

1.3

Summary

As you found out in the “Intro to OCR” Bundle, performing optical character recognition isn’t as easy as installing a library via pip and then letting the OCR engine do the magic with you. If it were that easy, this series of books wouldn’t exist! Instead, you found out that OCR is more challenging. You need to understand the various modes and options inside of Tesseract. Furthermore, you need to know when and where to use each setting. To improve OCR accuracy further, you sometimes need to leverage computer vision and image processing libraries such as OpenCV. The OpenCV library can help you pre-process your images, clean them up, and improve your OCR accuracy. I’ll end this chapter by asking you to keep in mind that this volume is meant to be your playbook for approaching OCR problems. If you have a particular OCR project you’re trying to solve, you can jump immediately to that chapter; however, I strongly encourage you to read this book from start-to-finish still. The field of OCR, just like computer vision as a whole, is like a toolbox. It’s not enough to fill your toolbox with the right tools — you still need to know when and where to pull out your hammer, saw, screwdriver, etc. Throughout the rest of this volume, you’ll gain practical experience and see firsthand when to use each tool. Let’s get started.

Chapter 2

Training an OCR Model with Keras and TensorFlow In this chapter, you will learn how to train an optical character recognition (OCR) model using Keras and TensorFlow. Then, in the next chapter, you’ll learn how to take our trained model and then apply it to the task of handwriting recognition. The goal of these two chapters is to obtain a deeper understanding of how deep learning is applied to the classification of handwriting, and more specifically, our goal is to: • Become familiar with some well-known, readily available handwriting datasets for both digits and letters • Understand how to train deep learning models to recognize handwritten digits and letters • Gain experience in applying our custom-trained model to some real-world sample data • Understand some of the challenges with noisy, real-world data and how we might want to augment our handwriting datasets to improve our model and results We’ll start with the fundamentals of using well-known handwriting datasets and training a ResNet deep learning model on these data. Let’s get started now!

2.1

Chapter Learning Objectives

In this chapter, you will: • Review two separate character datasets, one for the characters 0–9 and then a second dataset for the characters A–Z 5

6

Chapter 2. Training an OCR Model with Keras and TensorFlow

• Combine both these datasets into a single dataset that we’ll use for handwriting recognition • Train ResNet to recognize each of these characters using Keras and TensorFlow

2.2

OCR with Keras and TensorFlow

In the first part of this chapter, we’ll discuss the steps required to implement and train a custom OCR model with Keras and TensorFlow. We’ll then examine the handwriting datasets that we’ll use to train our model. From there, we’ll implement a couple of helper/utility functions that will aid us in loading our handwriting datasets from disk and then pre-processing them. Given these helper functions, we’ll be able to create our custom OCR training script with Keras and TensorFlow. After training, we’ll review the results of our OCR work.

2.2.1

Our Digits and Letters Dataset

To train our custom Keras and TensorFlow model, we’ll be utilizing two datasets: i. The standard MNIST 0–9 dataset by LeCun et al. [1] (which is included in most popular Python deep learning libraries) ii. The Kaggle A–Z dataset [2] by Sachin Patel, based on the NIST Special Database 19 [3] The standard MNIST dataset is built into popular deep learning frameworks, including Keras, TensorFlow, PyTorch, etc. A sample of the MNIST 0–9 dataset can be seen in Figure 2.1 (left). The MNIST dataset will allow us to recognize the digits 0–9. Each of these digits is contained in a 28 x 28 grayscale image. But what about the letters A–Z ? The standard MNIST dataset doesn’t include examples of the characters A–Z — how will we recognize them? The answer is to use the National Institute of Standards and Technology (NIST) Special Database 19, which includes A–Z characters. This dataset covers 62 American Standard Code for Information Interchange (ASCII) hexadecimal characters corresponding to the digits 0–9, capital letters A–Z, and lowercase letters a–z. To make the dataset easier to use, Kaggle user Sachin Patel has released a simple comma-separated values (CSV) file: http://pyimg.co/2psdb [2]. This dataset takes the capital

2.2. OCR with Keras and TensorFlow

7

letters A–Z from NIST Special Database 19 and rescales them from 28 x 28 grayscale pixels to the same format as our MNIST data.

Figure 2.1. We are using two separate datasets to train our Keras/TensorFlow OCR model. On the left, we have the standard MNIST 0–9 dataset. On the right, we have the Kaggle A–Z dataset from Sachin Patel [2], which is based on the NIST Special Database 19 [3]

For this project, we will be using just the Kaggle A–Z dataset, which will make our pre-processing a breeze. A sample of it can be seen in Figure 2.1, (right). We’ll be implementing methods and utilities that will allow us to: i. Load both the datasets for MNIST 0–9 digits and Kaggle A–Z letters from disk ii. Combine these datasets into a single, unified character dataset iii. Handle class label skew/imbalance from having a different number of samples per character iv. Successfully train a Keras and TensorFlow model on the combined dataset v. Plot the results of the training and visualize the output of the validation data To accomplish these tasks, let’s move on to reviewing our directory structure for the project.

2.2.2

Project Structure

Let’s get started reviewing our project directory structure:

8

Chapter 2. Training an OCR Model with Keras and TensorFlow

|-| | | | | | | |-|-|-|-|--

pyimagesearch |-- __init__.py |-- az_dataset | |-- __init__.py | |-- helpers.py |-- models | |-- __init__.py | |-- resnet.py a_z_handwritten_data.zip a_z_handwritten_data.csv handwriting.model plot.png train_ocr_model.py

Inside our project directory, you’ll find: • pyimagesearch module: includes the submodules az_dataset for input/output (I/O) helper files and models for implementing the ResNet deep learning architecture • a_z_handwritten_data.csv: contains the Kaggle A–Z dataset (you’ll need to extract a_z_handwritten_data.zip to obtain this file) • handwriting.model: where the deep learning ResNet model is saved • plot.png: plots the results of the most recent run of training of ResNet • train_ocr_model.py: the main driver file for training our ResNet model and displaying the results Now that we have the lay of the land, let’s dig into the I/O helper functions, we will use to load our digits and letters.

2.2.3

Implementing Our Dataset Loader Functions

To train our custom Keras and TensorFlow OCR model, we first need to implement two helper utilities that will allow us to load both the Kaggle A–Z datasets and the MNIST 0–9 digits from disk. These I/O helper functions are appropriately named: • load_az_dataset: for the Kaggle A–Z letters • load_mnist_dataset: for the MNIST 0–9 digits They can be found in the helpers.py file of az_dataset submodule of pyimagesearch.

2.2. OCR with Keras and TensorFlow

9

Let’s go ahead and examine this helpers.py file. We will begin with our import statements and then dig into our two helper functions: load_az_dataset and load_mnist_dataset:

1 2 3

# import the necessary packages from tensorflow.keras.datasets import mnist import numpy as np

Line 2 imports the MNIST dataset, mnist, which is now one of the standard datasets that conveniently comes with Keras in tensorflow.keras.datasets. Next, let’s dive into load_az_dataset, the helper function to load the Kaggle A–Z letter data:

5 6 7 8

def load_az_dataset(datasetPath): # initialize the list of data and labels data = [] labels = []

9 10 11 12 13 14 15

# loop over the rows of the A-Z handwritten digit dataset for row in open(datasetPath): # parse the label and image from the row row = row.split(",") label = int(row[0]) image = np.array([int(x) for x in row[1:]], dtype="uint8")

16 17 18 19 20

# images are represented as single channel (grayscale) images # that are 28x28=784 pixels -- we need to take this flattened # 784-d list of numbers and reshape them into a 28x28 matrix image = image.reshape((28, 28))

21 22 23 24

# update the list of data and labels data.append(image) labels.append(label)

Our function load_az_dataset takes a single argument datasetPath, which is the location of the Kaggle A–Z CSV file (Line 5). Then, we initialize our arrays to store the data and labels (Lines 7 and 8). Each row in Sachin Patel’s CSV file contains 785 columns — one column for the class label (i.e., “A–Z ”) plus 784 columns corresponding to the 28 x 28 grayscale pixels. Let’s parse it. Beginning on Line 11, we will loop over each row of our CSV file and parse out the label and the associated image. Line 14 parses the label, which will be the integer label associated with a letter A–Z. For example, the letter “A” has a label corresponding to the integer “0,” and the letter “Z” has an integer label value of “25.”

10

Chapter 2. Training an OCR Model with Keras and TensorFlow

Next, Line 15 parses our image and casts it as a NumPy array of unsigned 8-bit integers, which correspond to each pixel’s grayscale values from [0, 255]. We reshape our image (Line 20) from a flat 784-d array to one that is 28 x 28, corresponding to the dimensions of each of our images. We will then append each image and label to our data and label arrays, respectively (Lines 23 and 24). To finish up this function, we will convert the data and labels to NumPy arrays and return the image data and labels:

26 27 28

# convert the data and labels to NumPy arrays data = np.array(data, dtype="float32") labels = np.array(labels, dtype="int")

29 30 31

# return a 2-tuple of the A-Z data and labels return (data, labels)

Presently, our image data and labels are just Python lists, so we are going to typecast them as NumPy arrays of float32 and int, respectively (Lines 27 and 28). Great job implementing our first function! Our next I/O helper function, load_mnist_dataset, is considerably simpler.

33 34 35 36 37 38 39

def load_mnist_dataset(): # load the MNIST dataset and stack the training data and testing # data together (we'll create our own training and testing splits # later in the project) ((trainData, trainLabels), (testData, testLabels)) = mnist.load_data() data = np.vstack([trainData, testData]) labels = np.hstack([trainLabels, testLabels])

40 41 42

# return a 2-tuple of the MNIST data and labels return (data, labels)

Line 37 loads our MNIST 0–9 digit data using Keras’s helper function, mnist.load_data. Notice that we don’t have to specify a datasetPath as we did for the Kaggle data because Keras, conveniently, has this dataset built-in. Keras’s mnist.load_data comes with a default split for training data, training labels, test data, and test labels. For now, we are just going to combine our training and test data for MNIST using np.vstack for our image data (Line 38) and np.hstack for our labels (Line 39).

2.2. OCR with Keras and TensorFlow

11

Later, in train_ocr_model.py, we will be combining our MNIST 0–9 digit data with our Kaggle A–Z letters. At that point, we will create our custom split of test and training data. Finally, Line 42 returns the image data and associated labels to the calling function. Congratulations! You have now completed the I/O helper functions to load both the digit and letter samples for OCR and deep learning. Next, we will examine our main driver file used for training and viewing the results.

2.2.4

Implementing Our Training Script

In this section, we will train our OCR model using Keras, TensorFlow, and a PyImageSearch implementation of the popular and successful deep learning architecture, ResNet [4]. To get started, locate our primary driver file, train_ocr_model.py, found in the main project directory. This file contains a reference to a file resnet.py, located in the models/ subdirectory under the pyimagesearch module. Although we will not be doing a detailed walk-through of resnet.py in this chapter, you can get a feel for the ResNet architecture with my blog post on Fine-Tuning ResNet with Keras and Deep Learning (http://pyimg.co/apodn [5]). Please see my book, Deep Learning for Computer Vision with Python (http://pyimg.co/dl4cv [6]), for more advanced details. Let’s take a moment to review train_ocr_model.py. Afterward, we will come back and break it down, step by step. First, we’ll review the packages that we will import:

1 2 3

# set the matplotlib backend so figures can be saved in the background import matplotlib matplotlib.use("Agg")

4 5 6 7 8 9 10 11 12 13 14 15 16 17 18

# import the necessary packages from pyimagesearch.models import ResNet from pyimagesearch.az_dataset import load_mnist_dataset from pyimagesearch.az_dataset import load_az_dataset from tensorflow.keras.preprocessing.image import ImageDataGenerator from tensorflow.keras.optimizers import SGD from sklearn.preprocessing import LabelBinarizer from sklearn.model_selection import train_test_split from sklearn.metrics import classification_report from imutils import build_montages import matplotlib.pyplot as plt import numpy as np import argparse import cv2

12

Chapter 2. Training an OCR Model with Keras and TensorFlow

This is a long list of import statements, but don’t worry. It means we have many packages that have already been written to make our lives much easier. Starting on Line 2, we will import matplotlib and set up the back end of it by writing the results to a file using matplotlib.use("Agg") (Line 3). We then have some imports from our custom pyimagesearch module for our deep learning architecture and our I/O helper functions that we just reviewed: • We import ResNet from our pyimagesearch.model, which contains our custom implementation of the popular ResNet deep learning architecture (Line 6). • Next, we import our I/O helper functions load_mnist_data (Line 7) and load_az_dataset (Line 8) from pyimagesearch.az_dataset. We have a couple of imports from the Keras module of TensorFlow, which greatly simplify our data augmentation and training: • Line 9 imports ImageDataGenerator to help us efficiently augment our dataset. • We then import SGD, the stochastic gradient descent (SGD) optimization algorithm (Line 10). Following on, we import three helper functions from scikit-learn to help us label our data, split our testing and training datasets, and print out a classification report to show us our results: • To convert our labels from integers to a vector in what is called one-hot encoding, we import LabelBinarizer (Line 11). • To help us easily split out our testing and training datasets, we import train_test_split from scikit-learn (Line 12). • From the metrics submodule, we import classification_report to print out a nicely formatted classification report (Line 13). From imutils, we import build_montages to help us build a montage from a list of images (Line 14). For more information on creating montages, please refer to my Montages with OpenCV tutorial (http://pyimg.co/vquhs [7]). We will round out our notable imports with matplotlib (Line 18) and OpenCV (Line 21). Now, let’s review our three command line arguments:

20 21

# construct the argument parser and parse the arguments ap = argparse.ArgumentParser()

2.2. OCR with Keras and TensorFlow

22 23 24 25 26 27 28

13

ap.add_argument("-a", "--az", required=True, help="path to A-Z dataset") ap.add_argument("-m", "--model", type=str, required=True, help="path to output trained handwriting recognition model") ap.add_argument("-p", "--plot", type=str, default="plot.png", help="path to output training history file") args = vars(ap.parse_args())

We have three arguments to review: • --az: The path to the Kaggle A–Z dataset • --model: The path to output the trained handwriting recognition model • --plot: The path to output the training history file So far, we have our imports, convenience function, and command line args ready to go. We have several steps remaining to set up the training for ResNet, compile it, and train it. Now, we will set up the training parameters for ResNet and load our digit and letter data using the helper functions that we already reviewed:

30 31 32 33 34

# initialize the number of epochs to train for, initial learning rate, # and batch size EPOCHS = 50 INIT_LR = 1e-1 BS = 128

35 36 37 38 39

# load the A-Z and MNIST datasets, respectively print("[INFO] loading datasets...") (azData, azLabels) = load_az_dataset(args["az"]) (digitsData, digitsLabels) = load_mnist_dataset()

Lines 32–34 initialize the parameters for the training of our ResNet model. Then, we load the data and labels for the Kaggle A–Z and MNIST 0–9 digits data, respectively (Lines 38 and 39), using the I/O helper functions that we reviewed at the beginning of the post. Next, we are going to perform several steps to prepare our data and labels to be compatible with our ResNet deep learning model in Keras and TensorFlow:

41 42 43 44

# the MNIST dataset occupies the labels 0-9, so let's add 10 to every # A-Z label to ensure the A-Z characters are not incorrectly labeled # as digits azLabels += 10

14

Chapter 2. Training an OCR Model with Keras and TensorFlow

45 46 47 48

# stack the A-Z data and labels with the MNIST digits data and labels data = np.vstack([azData, digitsData]) labels = np.hstack([azLabels, digitsLabels])

49 50 51 52 53 54

# each image in the A-Z and MNIST digts datasets are 28x28 pixels; # however, the architecture we're using is designed for 32x32 images, # so we need to resize them to 32x32 data = [cv2.resize(image, (32, 32)) for image in data] data = np.array(data, dtype="float32")

55 56 57 58 59

# add a channel dimension to every image in the dataset and scale the # pixel intensities of the images from [0, 255] down to [0, 1] data = np.expand_dims(data, axis=-1) data /= 255.0

As we combine our letters and numbers into a single character dataset, we want to remove any ambiguity where there is overlap in the labels so that each label in the combined character set is unique. Currently, our labels for A–Z go from [0, 25], corresponding to each letter of the alphabet. The labels for our digits go from 0–9, so there is overlap — which would be problematic if we were to combine them directly. No problem! There is a straightforward fix. We will add ten to all of our A–Z labels, so they all have integer label values greater than our digit label values (Line 44). We have a unified labeling schema for digits 0–9 and letters A–Z without any overlap in the labels’ values. Line 47 combines our datasets for our digits and letters into a single character dataset using np.vstack. Likewise, Line 51 unifies our corresponding labels for our digits and letters using np.hstack. Our ResNet architecture requires the images to have input dimensions of 32 x 32, but our input images currently have a size of 28 x 28. We resize each of the images using cv2.resize (Line 53). We have two final steps to prepare our data for use with ResNet. On Line 58, we will add an extra “channel” dimension to every image in the dataset to make it compatible with the ResNet model in Keras/TensorFlow. Finally, we will scale our pixel intensities from a range of [0, 255] down to [0.0, 1.0] (Line 59). Our next step is to prepare the labels for ResNet, weight the labels to account for the skew in the number of times each class (character) is represented in the data, and partition the data into test and training splits:

61 62

# convert the labels from integers to vectors le = LabelBinarizer()

2.2. OCR with Keras and TensorFlow

63 64

15

labels = le.fit_transform(labels) counts = labels.sum(axis=0)

65 66 67 68

# account for skew in the labeled data classTotals = labels.sum(axis=0) classWeight = {}

69 70 71 72

# loop over all classes and calculate the class weight for i in range(0, len(classTotals)): classWeight[i] = classTotals.max() / classTotals[i]

73 74 75 76 77

# partition the data into training and testing splits using 80% of # the data for training and the remaining 20% for testing (trainX, testX, trainY, testY) = train_test_split(data, labels, test_size=0.20, stratify=labels, random_state=42)

We instantiate a LabelBinarizer (Line 62), and then we convert the labels from integers to a vector of binaries with one-hot encoding (Line 63) using le.fit_transform. Lines 67–72 weight each class, based on the frequency of occurrence of each character. Next, we will use the scikit-learn train_test_split utility (Lines 76 and 77) to partition the data into 80% training and 20% testing. From there, we’ll augment our data using an image generator from Keras:

79 80 81 82 83 84 85 86 87

# construct the image generator for data augmentation aug = ImageDataGenerator( rotation_range=10, zoom_range=0.05, width_shift_range=0.1, height_shift_range=0.1, shear_range=0.15, horizontal_flip=False, fill_mode="nearest")

We can improve our ResNet classifier’s results by augmenting the input data for training using an ImageDataGenerator. Lines 80–87 include various rotations, scaling the size, horizontal translations, vertical translations, and tilts in the images. For more details on data augmentation, see both my tutorial on Keras ImageDataGenerator and Data Augmentation (http://pyimg.co/pedyk [8]) as well as my book, Deep Learning for Computer Vision with Python (http://pyimg.co/dl4cv [6]). Now we are ready to initialize and compile the ResNet network:

89 90

# initialize and compile our deep neural network print("[INFO] compiling model...")

16

91 92 93 94 95

Chapter 2. Training an OCR Model with Keras and TensorFlow

opt = SGD(lr=INIT_LR, decay=INIT_LR / EPOCHS) model = ResNet.build(32, 32, 1, len(le.classes_), (3, 3, 3), (64, 64, 128, 256), reg=0.0005) model.compile(loss="categorical_crossentropy", optimizer=opt, metrics=["accuracy"])

Using the SGD optimizer and a standard learning rate decay schedule, we build our ResNet architecture (Lines 91–93). Each character/digit is represented as a 32 × 32 pixel grayscale image as is evident by the first three parameters to ResNet’s build method. Lines 94 and 95 compile our model with “categorical_crossentropy” loss and our established SGD optimizer. Please beware that if you are working with a 2-class only dataset (we are not), you would need to use the "binary_crossentropy" loss function. Next, we will train the network, define label names, and evaluate the performance of the network:

97 98 99 100 101 102 103 104 105

# train the network print("[INFO] training network...") H = model.fit( aug.flow(trainX, trainY, batch_size=BS), validation_data=(testX, testY), steps_per_epoch=len(trainX) // BS, epochs=EPOCHS, class_weight=classWeight, verbose=1)

106 107 108 109 110

# define the list of label names labelNames = "0123456789" labelNames += "ABCDEFGHIJKLMNOPQRSTUVWXYZ" labelNames = [l for l in labelNames]

111 112 113 114 115 116

# evaluate the network print("[INFO] evaluating network...") predictions = model.predict(testX, batch_size=BS) print(classification_report(testY.argmax(axis=1), predictions.argmax(axis=1), target_names=labelNames))

We train our model using the model.fit method (Lines 99–105). The parameters are as follows: • aug.flow: establishes in-line data augmentation • validation_data: test input images (testX) and test labels (testY) • steps_per_epoch: how many batches are run per each pass of the full training data • epochs: the number of complete passes through the entire dataset during training

2.2. OCR with Keras and TensorFlow

17

• class_weight: weights due to the imbalance of data samples for various classes (e.g., digits and letters) in the training data • verbose: shows a progress bar during the training Next, we establish labels for each character. Lines 108–110 concatenates all of our digits and letters and form an array where each member of the array is a single digit or number. To evaluate our model, we make predictions on the test set and print our classification report. We’ll see the report very soon in the next section! Lines 115 and 116 print out the results using the convenient scikit-learn classification_report utility. We will save the model to disk, plot the results of the training history, and save the training history: 118 119 120

# save the model to disk print("[INFO] serializing network...") model.save(args["model"], save_format="h5")

121 122 123 124 125 126 127 128 129 130 131 132

# construct a plot that plots and saves the training history N = np.arange(0, EPOCHS) plt.style.use("ggplot") plt.figure() plt.plot(N, H.history["loss"], label="train_loss") plt.plot(N, H.history["val_loss"], label="val_loss") plt.title("Training Loss and Accuracy") plt.xlabel("Epoch #") plt.ylabel("Loss/Accuracy") plt.legend(loc="lower left") plt.savefig(args["plot"])

As we have finished our training, we need to save the model comprised of the architecture and final weights. We will save our model to disk, as a Hierarchical Data Format version 5 (HDF5) file, which is specified by the save_format (Line 120). Next, we use matplotlib to generate a line plot for the training loss and validation set loss along with titles, labels for the axes, and a legend. The data for the training and validation losses come from the history of H, which are the results of model.fit from above with one point for every epoch (Lines 123–131). The plot of the training loss curves is saved to plot.png (Line 132). Finally, let’s code our visualization procedure, so we can see whether our model is working or not: 134

# initialize our list of output images

18

135

Chapter 2. Training an OCR Model with Keras and TensorFlow

images = []

136 137 138 139 140 141 142

# randomly select a few testing characters for i in np.random.choice(np.arange(0, len(testY)), size=(49,)): # classify the character probs = model.predict(testX[np.newaxis, i]) prediction = probs.argmax(axis=1) label = labelNames[prediction[0]]

143 144 145 146 147

# extract the image from the test data and initialize the text # label color as green (correct) image = (testX[i] * 255).astype("uint8") color = (0, 255, 0)

148 149 150 151

# otherwise, the class label prediction is incorrect if prediction[0] != np.argmax(testY[i]): color = (0, 0, 255)

152 153 154 155 156 157 158 159

# merge the channels into one image, resize the image from 32x32 # to 96x96 so we can better see it and then draw the predicted # label on the image image = cv2.merge([image] * 3) image = cv2.resize(image, (96, 96), interpolation=cv2.INTER_LINEAR) cv2.putText(image, label, (5, 20), cv2.FONT_HERSHEY_SIMPLEX, 0.75, color, 2)

160 161 162

# add the image to our list of output images images.append(image)

Line 135 initializes our array of test images. Starting on Line 138, we randomly select 49 characters (to form a 7 × 7 grid) and proceed to: • Classify the character using our ResNet-based model (Lines 140–142) • Grab the individual character image from our test data (Line 146) • Set an annotation text color as green (correct) or red (incorrect) via Lines 147–151 • Create an RGB representation of our single channel image and resize it for inclusion in our visualization montage (Lines 156 and 157) • Annotate the colored text label (Lines 158 and 159) • Add the image to our output images array (Line 162) We’re almost done. Only one code block left to go:

164 165 166

# construct the montage for the images montage = build_montages(images, (96, 96), (7, 7))[0]

2.2. OCR with Keras and TensorFlow

167 168 169

19

# show the output montage cv2.imshow("OCR Results", montage) cv2.waitKey(0)

To close out, we assemble each annotated character image into an OpenCV montage visualization grid (http://pyimg.co/vquhs [7]), displaying the result until a key is pressed (Lines 165–169). Congratulations! We learned a lot along the way! Next, we’ll see the results of our hard work.

2.2.5

Training Our Keras and TensorFlow OCR Model

Before we run our training script, take a second to recall from the last section that our script: i. Loads MNIST 0–9 digits and Kaggle A–Z letters ii. Trains a ResNet model on the dataset iii. Produces a visualization so that we can ensure it is working properly In this section, we’ll execute our OCR model training and visualization script. Let’s do that now:

$ python train_ocr_model.py --az a_z_handwritten_data.csv --model handwriting.model [INFO] loading datasets... [INFO] compiling model... [INFO] training network... Epoch 1/50 2765/2765 [==============================] - 102s 37ms/step - loss: 0.9246 - accuracy: 0.8258 ,→ - val_loss: 0.4715 - val_accuracy: 0.9381 Epoch 2/50 2765/2765 [==============================] - 96s 35ms/step - loss: 0.4708 - accuracy: 0.9374 ,→ val_loss: 0.4146 - val_accuracy: 0.9521 Epoch 3/50 2765/2765 [==============================] - 95s 34ms/step - loss: 0.4349 - accuracy: 0.9454 ,→ val_loss: 0.3986 - val_accuracy: 0.9543 ... Epoch 48/50 2765/2765 [==============================] - 94s 34ms/step - loss: 0.3501 - accuracy: 0.9609 ,→ val_loss: 0.3442 - val_accuracy: 0.9623 Epoch 49/50 2765/2765 [==============================] - 94s 34ms/step - loss: 0.3496 - accuracy: 0.9606 ,→ val_loss: 0.3444 - val_accuracy: 0.9624 Epoch 50/50 2765/2765 [==============================] - 94s 34ms/step - loss: 0.3494 - accuracy: 0.9608 ,→ val_loss: 0.3468 - val_accuracy: 0.9616 [INFO] evaluating network... precision recall f1-score support 0 1 2

0.48 0.98 0.90

0.44 0.97 0.94

0.46 0.97 0.92

1381 1575 1398

20

Chapter 2. Training an OCR Model with Keras and TensorFlow

3 4 5 6 7 8 9 A B C D E F G H I J K L M N O P Q R S T U V W X Y Z

0.97 0.92 0.80 0.96 0.95 0.97 0.98 0.99 0.97 0.99 0.96 0.99 0.99 0.96 0.96 0.98 0.98 0.98 0.97 0.99 0.99 0.93 1.00 0.97 0.98 0.98 0.99 0.98 0.98 0.99 0.99 0.98 0.94

0.99 0.96 0.89 0.97 0.98 0.97 0.98 0.99 0.98 0.99 0.96 0.99 0.95 0.94 0.96 0.96 0.95 0.96 0.98 0.99 0.99 0.94 0.99 0.96 0.99 0.97 0.99 0.99 0.98 0.98 0.98 0.94 0.92

0.98 0.94 0.85 0.96 0.97 0.97 0.98 0.99 0.98 0.99 0.96 0.99 0.97 0.95 0.96 0.97 0.97 0.97 0.98 0.99 0.99 0.93 0.99 0.96 0.98 0.98 0.99 0.99 0.98 0.98 0.98 0.96 0.93

1428 1365 1263 1375 1459 1365 1392 2774 1734 4682 2027 2288 232 1152 1444 224 1699 1121 2317 2467 3802 11565 3868 1162 2313 9684 4499 5802 836 2157 1254 2172 1215

accuracy macro avg weighted avg

0.95 0.96

0.95 0.96

0.96 0.95 0.96

88491 88491 88491

[INFO] serializing network...

Remark 2.1. When loaded into memory, the entire A–Z and 0–9 character datasets consume over 4GB of RAM. If you are using the VM included with your purchase of this book, then you will need to adjust the VM settings to include more RAM (4GB is the default amount). Setting the default RAM allocation to 8GB on the VM is more than sufficient. As you can see, our Keras/TensorFlow OCR model is obtaining ≈ 96% accuracy on the testing set. The training history plot can be seen in Figure 2.2 (left). As evidenced by the plot, there are few overfitting signs, implying that our Keras and TensorFlow model performs well at our basic OCR task. We can see the prediction output from a sample of our testing set in Figure 2.2 (right). As our results show, our handwriting recognition model is performing quite well! And finally, if you check your current working directory, you should find a new file named handwriting.model:

2.3. Summary

21

$ ls *.model handwriting.model

Figure 2.2. Left: A plot of our training history. Our model shows little signs of overfitting, implying that our OCR model is performing well. Right: A sample output of character classifications made on the testing set by our trained OCR model. Note that we have correctly classified each character.

This file is our serialized Keras and TensorFlow OCR model — we’ll be using it in Chapter 3 on handwriting recognition.

2.3

Summary

In this chapter, you learned how to train a custom OCR model using Keras and TensorFlow. Our model was trained to recognize alphanumeric characters, including the digits 0–9 and the letters A–Z. Overall, our Keras and TensorFlow OCR model obtained ≈ 96% accuracy on our testing set. In the next chapter, you’ll learn how to take our trained Keras/TensorFlow OCR model and use it for handwriting recognition on custom input images.

Chapter 3

Handwriting Recognition with Keras and TensorFlow In this chapter, you will learn how to perform optical character recognition (OCR) handwriting recognition using OpenCV, Keras, and TensorFlow. As you’ll see further below, handwriting recognition tends to be significantly harder than traditional OCR that uses specific fonts/characters. This concept is so challenging because there are nearly infinite variations of handwriting styles, unlike computer fonts. Every one of us has a personal style that is specific and unique. My wife, for example, has amazing penmanship. Her handwriting is not only legible, but it’s stylized in a way that you would think a professional calligrapher wrote it (Figure 3.1, left). Me, on the other hand, my handwriting looks like someone crossed a doctor with a deranged squirrel (Figure 3.1, right).

Figure 3.1. My wife has beautiful penmanship (left), while my handwriting looks like a fourth grader (right).

23

24

Chapter 3. Handwriting Recognition with Keras and TensorFlow

It’s barely legible. I’m often asked by those who read my handwriting at least 2–3 clarifying what a specific word or phrase is. And on more than one occasion, I’ve had to admit that I couldn’t read them either. Talk about embarrassing! Truly, it’s a wonder they ever let me out of grade school. These handwriting style variations pose a problem for OCR engines, typically trained on computer fonts, not handwriting fonts. And worse, handwriting recognition is further complicated because letters can “connect” and “touch” each other, making it incredibly challenging for OCR algorithms to separate them, ultimately leading to incorrect OCR results. Handwriting recognition is arguably the “holy grail” of OCR. We’re not there yet, but with the help of deep learning, we’re making tremendous strides. This chapter will serve as an introduction to handwriting recognition. You’ll see examples of where handwriting recognition has performed well and other instances where it has failed to correctly OCR a handwritten character. I genuinely think you’ll find value in reading the rest of this handwriting recognition guide.

3.1

Chapter Learning Objectives

In this chapter, you will: • Discover what handwriting recognition is • Learn how to take our handwriting recognition model (trained in the previous chapter) and use it to make predictions • Create a Python script that loads the input model and a sample image, pre-process is it, and then performs handwriting recognition

3.2

OCR’ing Handwriting with Keras and TensorFlow

In the first part of this chapter, we’ll discuss handwriting recognition and how it is different from “traditional” OCR. I’ll then provide a brief review of the process for training our recognition model using Keras and TensorFlow — we’ll be using this trained model to OCR handwriting in this chapter. We’ll review our project structure and then implement a Python script to perform handwriting recognition with OpenCV, Keras, and TensorFlow.

3.2. OCR’ing Handwriting with Keras and TensorFlow

25

To wrap up this OCR section, we’ll discuss our handwriting recognition results, including what worked and what didn’t.

3.2.1

What Is Handwriting Recognition?

Traditional OCR algorithms and techniques assume we’re working with a fixed font of some sort. In the early 1900s, that could have been the font used by microfilms. In the 1970s, specialized fonts were explicitly developed for OCR algorithms, thereby making them more accurate. By the 2000s, we could use the fonts pre-installed on our computers to automatically generate training data and use these fonts to train our OCR models. Each of these fonts had something in common: i. They were engineered in some manner. ii. There was a predictable and assumed space between each character (thereby making segmentation easier). iii. The styles of the fonts were more conducive to OCR. Essentially, engineered/computer-generated fonts make OCR far easier. Figure 3.2 shows how computer text on the right is more uniform and segmented than handwritten text which is on the left.

Figure 3.2. Left: Handwritten text. Right: Typed text.

Handwriting recognition is an entirely different beast, though. Consider the extreme amounts of variation and how characters often overlap. Everyone has a unique writing style. Characters can be elongated, swooped, slanted, stylized, crunched, connected, tiny, gigantic, etc. (and come in any of these combinations).

26

Chapter 3. Handwriting Recognition with Keras and TensorFlow

Digitizing handwriting recognition is exceptionally challenging and is still far from solved — but deep learning helps us improve our handwriting recognition accuracy.

3.2.2

Project Structure

Let’s get started by reviewing our project directory structure:

|-| | | |-|--

images |-- hello_world.png |-- umbc_address.png |-- umbc_zipcode.png handwriting.model ocr_handwriting.py

The project directory contains: • handwriting.model: The custom OCR ResNet model we trained in the previous chapter • images/ subdirectory: Contains three portable network graphic (PNG) test files for us to OCR with our Python driver script • ocr_handwriting.py: The main Python script that we will use to OCR our handwriting samples Now that we have a handle on the project structure let’s dive into our driver script.

3.2.3

Implementing Our Handwriting Recognition Script

Let’s open ocr_handwriting.py and review it, starting with the imports and command line arguments:

1 2 3 4 5 6 7

# import the necessary packages from tensorflow.keras.models import load_model from imutils.contours import sort_contours import numpy as np import argparse import imutils import cv2

8 9 10 11 12 13

# construct the argument parser and parse the arguments ap = argparse.ArgumentParser() ap.add_argument("-i", "--image", required=True, help="path to input image") ap.add_argument("-m", "--model", type=str, required=True,

3.2. OCR’ing Handwriting with Keras and TensorFlow

14 15

27

help="path to trained handwriting recognition model") args = vars(ap.parse_args())

Line 2 imports the load_model utility, which allows us to easily load the OCR model that we developed in the previous chapter. Using my imutils package, we then import sort_contours (Line 3) and imutils (Line 6) to facilitate operations with contours and resizing images. Our command line arguments include --image, our input image, and --model, the path to our trained handwriting recognition model. Next, we will load our custom handwriting OCR model that we trained earlier in this book:

17 18 19

# load the handwriting OCR model print("[INFO] loading handwriting OCR model...") model = load_model(args["model"])

The load_model utility from Keras and TensorFlow makes it super simple to load our serialized handwriting recognition model (Line 19). Recall that our OCR model uses the ResNet deep learning architecture to classify each character corresponding to a digit 0–9 or a letter A–Z. Since we’ve loaded our model from disk, let’s grab our image, pre-process it, and find character contours:

21 22 23 24 25

# load the input image from disk, convert it to grayscale, and blur # it to reduce noise image = cv2.imread(args["image"]) gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) blurred = cv2.GaussianBlur(gray, (5, 5), 0)

26 27 28 29 30 31 32 33

# perform edge detection, find contours in the edge map, and sort the # resulting contours from left-to-right edged = cv2.Canny(blurred, 30, 150) cnts = cv2.findContours(edged.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) cnts = imutils.grab_contours(cnts) cnts = sort_contours(cnts, method="left-to-right")[0]

34 35 36 37

# initialize the list of contour bounding boxes and associated # characters that we'll be OCR'ing chars = []

After loading the image (Line 23), we convert it to grayscale (Line 24), and then apply Gaussian blurring to reduce noise (Line 25).

28

Chapter 3. Handwriting Recognition with Keras and TensorFlow

From there, we detect the edges of our blurred image using cv2.Canny (Line 29). To locate the contours for each character, we apply contour detection (Lines 30 and 31). To conveniently sort the contours from "left-to-right" (Line 33), we use my sort_contours method. Line 37 initializes the chars list, which will soon hold each character image and associated bounding box. In Figure 3.3, we can see the sample results from our image pre-processing steps.

Figure 3.3. Steps of our handwriting OCR pipeline. We have our original color image (upper-left), our grayscale image (upper-right), our blurred image with reduced noise (lower-left), and our edge-detection map (lower-right).

Our next step will involve a large contour processing loop. Let’s break that down in more detail so that it is easier to get through:

39 40 41 42

# loop over the contours for c in cnts: # compute the bounding box of the contour (x, y, w, h) = cv2.boundingRect(c)

3.2. OCR’ing Handwriting with Keras and TensorFlow

29

43 44 45 46 47 48 49 50 51 52 53

# filter out bounding boxes, ensuring they are neither too small # nor too large if (w >= 5 and w = 15 and h tH: thresh = imutils.resize(thresh, width=32)

59 60 61 62

# otherwise, resize along the height else: thresh = imutils.resize(thresh, height=32)

Beginning on Line 40, we loop over each contour and perform a series of four steps. In Step #1, we select appropriately sized contours and extract them by: • Computing the bounding box of the contour (Line 42) • Making sure the bounding boxes are of reasonable size, filtering out those that are either too large or too small (Line 46) • For each bounding box meeting our size criteria, we extract the region of interest (roi) associated with the character (Line 50) Next, we move on to Step #2 of our algorithm — cleaning up the images using Otsu’s thresholding technique, with the ultimate goal of segmenting the characters such that they appear as white on a black background (Lines 51 and 52). We then arrive at Step #3, where the ultimate goal is to resize every character to 32 x 32 pixels while maintaining the aspect ratio. Depending on whether the width is greater than the height or the height is greater than the width, we resize the thresholded character ROI accordingly (Lines 57–62). But wait! Before we can continue our loop that began on Line 40, we need to pad these ROIs and add them to the chars list:

64 65

# re-grab the image dimensions (now that its been resized) # and then determine how much we need to pad the width and

30

66 67 68 69

Chapter 3. Handwriting Recognition with Keras and TensorFlow

# height such that our image will be 32x32 (tH, tW) = thresh.shape dX = int(max(0, 32 - tW) / 2.0) dY = int(max(0, 32 - tH) / 2.0)

70 71 72 73 74 75

# pad the image and force 32x32 dimensions padded = cv2.copyMakeBorder(thresh, top=dY, bottom=dY, left=dX, right=dX, borderType=cv2.BORDER_CONSTANT, value=(0, 0, 0)) padded = cv2.resize(padded, (32, 32))

76 77 78 79 80

# prepare the padded image for classification via our # handwriting OCR model padded = padded.astype("float32") / 255.0 padded = np.expand_dims(padded, axis=-1)

81 82 83

# update our list of characters that will be OCR'd chars.append((padded, (x, y, w, h)))

Now that we have padded the ROIs and added them to the chars list, we can finish resizing and padding by computing the necessary delta padding in the x and y directions. (Lines 67–69) , followed by applying the padding to create the padded image (Lines 72–74). Due to integer rounding on Lines 68 and 69, we then explicitly resize the image to 32 x 32 pixels on Line 75 to round out the pre-processing phase. Ultimately, these pre-processing steps ensure that each image is 32 x 32 while the aspect ratio is correctly maintained. Next comes Step #4, where we prepare each padded ROI for classification as a character: • Scale pixel intensities to the range [0, 1] and add a batch dimension (Lines 79 and 80). • Package both the padded character and bounding box as a 2-tuple, and add it to our chars list (Line 83). With our extracted and prepared set of character ROIs completed, we can perform handwriting recognition:

85 86 87

# extract the bounding box locations and padded characters boxes = [b[1] for b in chars] chars = np.array([c[0] for c in chars], dtype="float32")

88 89 90

# OCR the characters using our handwriting recognition model preds = model.predict(chars)

91 92 93

# define the list of label names labelNames = "0123456789"

3.2. OCR’ing Handwriting with Keras and TensorFlow

94 95

31

labelNames += "ABCDEFGHIJKLMNOPQRSTUVWXYZ" labelNames = [l for l in labelNames]

Lines 86 and 87 extract the original bounding boxes with associated chars in NumPy array format. To perform handwriting recognition OCR on our set of pre-processed characters, we classify the entire batch with the model.predict method (Line 90). This results in a list of predictions, preds. As we learned from our previous chapter, we then concatenate our labels for our digits and letters into a single list of labelNames (Lines 93–95). We’re almost done! It’s time to see the fruits of our labor. To see if our handwriting recognition results meet our expectations, let’s visualize and display them:

97 98 99 100 101 102 103

# loop over the predictions and bounding box locations together for (pred, (x, y, w, h)) in zip(preds, boxes): # find the index of the label with the largest corresponding # probability, then extract the probability and label i = np.argmax(pred) prob = pred[i] label = labelNames[i]

104 105 106 107 108 109

# draw the prediction on the image print("[INFO] {} - {:.2f}%".format(label, prob * 100)) cv2.rectangle(image, (x, y), (x + w, y + h), (0, 255, 0), 2) cv2.putText(image, label, (x - 10, y - 10), cv2.FONT_HERSHEY_SIMPLEX, 1.2, (0, 255, 0), 2)

110 111 112 113

# show the image cv2.imshow("Image", image) cv2.waitKey(0)

Wrapping up, we loop over each prediction and corresponding bounding box (Line 98). Inside the loop, we grab the highest probability prediction resulting in the particular character’s label (Lines 101–103). To visualize the results, we annotate each character with the bounding box and label text and display the result (Lines 107–113). To cycle to the next character, press any key. Congratulations! You have completed the main Python driver file to perform OCR on input images. Let’s take a look at our results in the next section.

32

Chapter 3. Handwriting Recognition with Keras and TensorFlow

3.2.4

Handwriting Recognition Results

Let’s put our handwriting recognition model to work! Open a terminal and execute the following command:

$ python ocr_handwriting.py --model handwriting.model --image ,→ images/hello_world.png [INFO] loading handwriting OCR model... [INFO] H - 92.48% [INFO] W - 54.50% [INFO] E - 94.93% [INFO] L - 97.58% [INFO] 2 - 65.73% [INFO] L - 96.56% [INFO] R - 97.31% [INFO] 0 - 37.92% [INFO] L - 97.13% [INFO] D - 97.83%

In Figure 3.4, we are attempting to OCR the handwritten text “Hello World.” Our handwriting recognition model performed well here, but made two mistakes: i. First, it confused the letter “O” with the digit “0” (zero) — that’s an understandable mistake. ii. Second, and a bit more concerning, the handwriting recognition model confused the “O” in “World” with a “2.” That’s a combination of a pre-processing issue along with not enough real-world training data for the “2” and “W” characters. This next example contains the handwritten name and ZIP code of my alma mater, University of Maryland, Baltimore County (UMBC):

$ python ocr_handwriting.py --model handwriting.model --image ,→ images/umbc_zipcode.png [INFO] loading handwriting OCR model... [INFO] U - 34.76% [INFO] 2 - 97.88% [INFO] M - 75.04% [INFO] 7 - 51.22% [INFO] B - 98.63% [INFO] 2 - 99.35% [INFO] C - 63.28% [INFO] 5 - 66.17% [INFO] 0 - 66.34%

3.2. OCR’ing Handwriting with Keras and TensorFlow

33

Figure 3.4. True to form for programmers, we start with a “HELLO WORLD” example to examine our deep learning OCR model results. But as you can see, the results aren’t quite perfect!

As Figure 3.5 shows, our handwriting recognition algorithm performed almost perfectly here. We can correctly OCR every handwritten character in the “UMBC.” However, the ZIP code is incorrectly OCR’d — our model confuses the “1” digit with a “7.” If we were to apply de-skewing to our character data, we could improve our results. Let’s inspect one final example. This image contains the full address of UMBC: $ python ocr_handwriting.py --model handwriting.model --image ,→ images/umbc_address.png [INFO] loading handwriting OCR model... [INFO] B - 97.71% [INFO] 1 - 95.41% [INFO] 0 - 89.55% [INFO] A - 87.94% [INFO] L - 96.30% [INFO] 0 - 71.02% [INFO] 7 - 42.04% [INFO] 2 - 27.84% [INFO] 0 - 67.76% [INFO] Q - 28.67%

34

[INFO] [INFO] [INFO] [INFO] [INFO] [INFO] [INFO] [INFO] [INFO] [INFO] [INFO] [INFO] [INFO] [INFO] [INFO] [INFO] [INFO] [INFO] [INFO] [INFO]

Chapter 3. Handwriting Recognition with Keras and TensorFlow

Q H Z R L E L 7 M U D P 2 C I 1 R 2 S O

-

39.30% 86.53% 61.18% 87.26% 91.07% 98.18% 84.20% 74.81% 74.32% 68.94% 92.87% 57.57% 99.66% 35.15% 67.39% 90.56% 65.40% 99.60% 42.27% 43.73%

Figure 3.5. For this example, we use an envelope with a name and ZIP code. Not bad — nearly perfect results. The digit “1” was confused for the digit “7.” If we were to apply some computer vision pre-processing, we might be able to improve our results.

3.2. OCR’ing Handwriting with Keras and TensorFlow

Figure 3.6 shows that our handwriting recognition model struggled. As you can see, there are multiple mistakes in the words “Hilltop,” “Baltimore,” and the ZIP code.

Figure 3.6. A sample two-line address written on an envelope. We can see there are still multiple mistakes, and thus there are limitations to our OCR model.

Given that our handwriting recognition model performed so well during training and testing, shouldn’t we expect it to perform well on our custom images as well? To answer that question, let’s move on to the next section.

3.2.5

Limitations, Drawbacks, and Next Steps

While our handwriting recognition model obtained 96% accuracy on our testing set, our handwriting recognition accuracy on our custom images is slightly less than that. One of the most significant issues is that we used variants of the MNIST (digits) and NIST (alphabet characters) datasets to train our handwriting recognition model. While interesting to study, these datasets don’t necessarily translate to real-world projects because the images have already been pre-processed and cleaned for us — real-world characters aren’t that “clean.” Additionally, our handwriting recognition method requires characters to be individually segmented. That may be possible for some characters, but many of us (especially cursive

35

36

Chapter 3. Handwriting Recognition with Keras and TensorFlow

writers) connect characters when writing quickly. This confuses our model into thinking a group of characters is a single character, which ultimately leads to incorrect results. Finally, our model architecture is a bit too simplistic. While our handwriting recognition model performed well on the training and testing set, the architecture — combined with the training dataset itself — is not robust enough to generalize as an “off-the-shelf” handwriting recognition model. To improve our handwriting recognition accuracy, we should: • Improve our image pre-processing techniques • Attempt to remove slant and slope from the handwritten words/characters • Utilize connectionist temporal classification (CTC) loss • Implement a novel convolutional recurrent neural network (CRNN) architecture that combines convolutional layers with recurrent layers, ultimately yielding higher handwriting recognition accuracy.

3.3

Summary

In this chapter, you learned how to perform OCR handwriting recognition using Keras, TensorFlow, and OpenCV. Our handwriting recognition system utilized basic computer vision and image processing algorithms (edge detection, contours, and contour filtering) to segment characters from an input image. From there, we passed each character through our trained handwriting recognition model to recognize each character. Our handwriting recognition model performed well, but there were some cases where the results could have been improved (ideally, with more training data that is representative of the handwriting we want to recognize) — the higher quality the training data, the more accurate we can make our handwriting recognition model! Secondly, our handwriting recognition pipeline did not handle the case where characters may be connected, causing multiple connected characters to be treated as a single character, thus confusing our OCR model. Dealing with connected handwritten characters is still an open area of research in the computer vision and OCR field; however, deep learning models, specifically LSTMs and recurrent layers, have shown significant promise in improving handwriting recognition accuracy.

Chapter 4

Using Machine Learning to Denoise Images for Better OCR Accuracy One of the most challenging aspects of applying optical character recognition (OCR) isn’t the OCR itself. Instead, it’s the process of pre-processing, denoising, and cleaning up images such that they can be OCR’d. When working with documents generated by a computer, screenshots, or essentially any piece of text that has never touched a printer and then scanned, OCR becomes far easier. The text is clean and crisp. There is sufficient contrast between the background and foreground. And most of the time, the text doesn’t exist on a complex background. That all changes once a piece of text is printed and scanned. From there, OCR becomes much more challenging. • The printer could be low on toner or ink, resulting in the text appearing faded and hard to read. • An old scanner could have been used when scanning the document, resulting in low image resolution and poor text contrast. • A mobile phone scanner app may have been used under poor lighting conditions, making it incredibly challenging for human eyes to read the text, let alone a computer. • And all too common are the clear signs that an actual human has handled the paper, including coffee mug stains on the corners, paper crinkling, rips, and tears, etc. For all the amazing things the human mind is capable of, it seems like we’re all just walking accidents waiting to happen when it comes to printed materials. Give us a piece of paper and enough time, and I guarantee that even the most organized of us will take that document from the pristine condition and eventually introduce some stains, rips, folds, and crinkles on it. 37

38

Chapter 4. Using Machine Learning to Denoise Images for Better OCR Accuracy

Inevitably, these problems will occur — and when they do, we need to utilize our computer vision, image processing, and OCR skills such that we can pre-process and improve the quality of these damaged documents. From there, we’ll be able to obtain higher OCR accuracy. In the remainder of this chapter, you’ll learn how even simple machine learning algorithms constructed in a novel way can help you denoise images before applying OCR.

4.1

Chapter Learning Objectives

In this chapter, you will: • Gain experience working with a dataset of noisy, damaged documents • Discover how machine learning can be used to denoise these damaged documents • Work with Kaggle’s Denoising Dirty Documents dataset [9] • Extract features from this dataset • Train a random forest regressor (RFR) on the features we extracted • Take the model and use it to denoise images in our test set (and then be able to denoise your datasets as well)

4.2

Image Denoising with Machine Learning

In the first part of this chapter, we will review the dataset we will be using to denoise documents. From there, we’ll review our project structure, including the five separate Python scripts we’ll be utilizing, including: • A configuration file to store variables utilized across multiple Python scripts • A helper function used to blur and threshold our documents • A script used to extract features and target values from our dataset • Another script used to train a RFR • And a final script used to apply our trained model to images in our test set This is one of the longer chapters in the text, and while it’s straightforward and follows a linear progression, there are also many nuanced details here. I suggest you review this chapter twice, once at a high-level to understand what we’re doing, and then again at a low-level to understand the implementation.

4.2. Image Denoising with Machine Learning

39

With that said, let’s get started!

4.2.1

Our Noisy Document Dataset

We’ll be using Kaggle’s Denoising Dirty Documents dataset (http://pyimg.co/9cpd6 [9]) in this chapter. The dataset is part of the UCI Machine Learning Repository [10] but converted to a Kaggle competition. We will be using three files for this chapter. Those files are a part of the Kaggle competition data and are named: test.zip, train.zip, and train_cleaned.zip. The dataset is relatively small, with only 144 training samples, making it easy to work with and use as an educational tool. However, don’t let the small dataset size fool you! What we’re going to do with this dataset is far from basic or introductory. Figure 4.1 shows a sample of the dirty documents dataset. For the sample document, the top shows the document’s noisy version, including stains, crinkles, folds, etc. The bottom then shows the target, pristine version of the document that we wish to generate.

Figure 4.1. Top: A sample image of a noisy document. Bottom: Target, cleaned version. Our goal is to create a computer vision pipeline that can automatically transform the noisy document into a cleaned one. Image credit: http://pyimg.co/j6ylr [11].

40

Chapter 4. Using Machine Learning to Denoise Images for Better OCR Accuracy

Our goal is to input the image on the top and train a machine learning model capable of producing a cleaned output on the bottom. It may seem impossible now, but once you see some of the tricks and techniques we’ll be using, it will be a lot more straightforward than you think.

4.2.2

The Denoising Document Algorithm

Our denoising algorithm hinges on training an RFR to accept a noisy image and automatically predict the output pixel values. This algorithm is inspired by a denoising technique introduced by Colin Priest (http://pyimg.co/dp06u [12]). These algorithms work by applying a 5 x 5 window that slides from left-to-right and top-to-bottom, one pixel at a time (Figure 4.2) across both the noisy image (i.e., the image we want to pre-process automatically and cleanup) and the target output image (i.e., the “gold standard” of what the image should look like after cleaning). At each sliding window stop, we extract: i. The 5 x 5 region of the noisy, input image. We then flatten the 5 x 5 region into a 25-d list and treat it like a feature vector. ii. The same 5 x 5 region of the cleaned image, but this time we only take the center (x, y)-coordinate, denoted by the location (2, 2). Given the 25-d (dimensional) feature vector from the noisy input image, this single pixel value is what we want our RFR to predict. To make this example more concrete, again consider Figure 4.2 where we have the following 5 x 5 grid of pixel values from the noisy image:

[[247 [244 [223 [242 [217

227 228 218 244 233

242 225 252 228 237

253 212 222 240 243

237] 219] 221] 230] 252]]

We then flatten that into a single list of 5 x 5 = 25-d values:

[247 227 242 253 237 244 228 225 212 219 223 218 252 222 221 242 244 228 240 230 217 233 237 243 252]

4.2. Image Denoising with Machine Learning

Figure 4.2. Our denoising document algorithm works by sliding a 5 x 5 window from left-to-right and top-to-bottom across the noisy input. We extract this 5 x 5 region, flatten it into a 25-d vector, and then treat it as our feature vector. This feature vector is passed into our RFR, and the output, a cleaned pixel, is predicted.

This 25-d vector is our feature vector upon which our RFR will be trained.

However, we still need to define the target output value of the RFR. Our regression model should accept the input 25-d vector and output the cleaned, denoised pixel.

Now, let’s assume that we have the following 5 x 5 window from our gold standard/target image:

41

42

Chapter 4. Using Machine Learning to Denoise Images for Better OCR Accuracy

[[0 [0 [0 [0 [0

0 0 0 0 0

0 0 1 1 0

0 0 1 1 1

0] 1] 1] 1] 1]]

We are only interested in the center of this 5 x 5 region, denoted as the location x = 2, y = 2. We extract this value of 1 (foreground, versus 0, which is background) and treat it as our target value that our RFR should predict. Putting this entire example together, we can think of the following as a sample training data point:

1 2 3

trainX = [[247 227 242 253 237 244 228 225 212 219 223 218 252 222 221 242 244 228 240 230 217 233 237 243 252]] trainY = [[1]]

Given our trainX variable (which is our raw pixel intensities), we want to be able to predict the corresponding cleaned/denoised pixel value in trainY. We will train our RFR in this manner, ultimately leading to a model that can accept a noisy document input and automatically denoise it by examining local 5 x 5 regions and then predicting the center (cleaned) pixel value.

4.2.3

Project Structure

This chapter’s project directory structure is a bit more complex than previous chapters as there are five Python scripts to review (three scripts, a helper function, and a configuration file). Before we get any farther, let’s familiarize ourselves with the files:

|-| | | | |-| | |--

pyimagesearch |-- __init__.py |-- denoising | |-- __init__.py | |-- helpers.py config |-- __init__.py |-- denoise_config.py build_features.py

4.2. Image Denoising with Machine Learning

|-|-|-| | | | | | | | | | | | | | | | | | |--

43

denoise_document.py denoiser.pickle denoising-dirty-documents |-- test | |-- 1.png | |-- 10.png | |-- ... | |-- 94.png | |-- 97.png |-- train | |-- 101.png | |-- 102.png | |-- ... | |-- 98.png | |-- 99.png |-- train_cleaned | |-- 101.png | |-- 102.png | |-- ... | |-- 98.png | |-- 99.png train_denoiser.py

The denoising-dirty-documents directory contains all images from the Kaggle Denoising Dirty Documents dataset [9]. Inside the denoising submodule of pyimagesearch, there is a helpers.py file. This file contains a single function, blur_and_threshold, which, as the name suggests, is used to apply a combination of smoothing and thresholding as a pre-processing step for our documents. We then have the denoise_config.py file, which stores a small number of configurations, namely specifying training data file paths, output feature CSV files, and the final serialized RFR model. There are three Python scripts that we’ll be reviewing in entirety: i. build_features.py: Accepts our input dataset and creates a CSV file that we’ll use to train our RFR. ii. train_denoiser.py: Trains the actual RFR model and serializes it to disk as denoiser.pickle. iii. denoise_document.py: Accepts an input image from disk, loads the trained RFR, and then denoises the input image. There are several Python scripts that we need to review in this chapter. I suggest you review this chapter twice to gain a higher-level understanding of what we are implementing and then again to grasp the implementation at a deeper level.

44

Chapter 4. Using Machine Learning to Denoise Images for Better OCR Accuracy

4.2.4

Implementing Our Configuration File

The first step in our denoising documents implementation is to create our configuration file. Open the denoise_config.py file in the config subdirectory of the project directory structure and insert the following code:

1 2

# import the necessary packages import os

3 4 5

# initialize the base path to the input documents dataset BASE_PATH = "denoising-dirty-documents"

6 7 8 9

# define the path to the training directories TRAIN_PATH = os.path.sep.join([BASE_PATH, "train"]) CLEANED_PATH = os.path.sep.join([BASE_PATH, "train_cleaned"])

Line 5 defines the base path to our denoising-dirty-documents dataset. If you download this dataset from Kaggle, be sure to unzip all .zip files within this directory to have all images in the dataset uncompressed and residing on disk. We then define the paths to both the original, noisy image directory and the corresponding cleaned image directory, respectively (Lines 8 and 9). The TRAIN_PATH images contain the noisy documents while the CLEANED_PATH images contain our “gold standard” of what, ideally, our output images should look like after applying document denoising via our trained model. We’ll construct our testing set inside our train_denoiser.py script. Let’s continue defining our configuration file:

11 12 13 14

# define the path to our output features CSV file then initialize # the sampling probability for a given row FEATURES_PATH = "features.csv" SAMPLE_PROB = 0.02

15 16 17

# define the path to our document denoiser model MODEL_PATH = "denoiser.pickle"

Line 13 defines the path to our output features.csv file. Our features here will consist of: i. A local 5 x 5 region sampled via sliding window from the noisy input image ii. The center of the 5 x 5 region, denoted as (x, y)-coordinate (2, 2), for the corresponding cleaned image

4.2. Image Denoising with Machine Learning

45

However, if we wrote every feature/target combination to disk, we would end up with millions of rows and a CSV many gigabytes in size. Instead of exhaustively computing all sliding window and target combinations, we’ll instead only write them to disk with SAMPLES_PROB probability. Finally, Line 17 specifies the path to MODEL_PATH, our output serialized model.

4.2.5

Creating Our Blur and Threshold Helper Function

To help our RFR predict background (i.e., noisy) from foreground (i.e., text) pixels, we need to define a helper function that will pre-process our images before we train the model and make predictions with it. The flow of our image processing operations can be seen in Figure 4.3. First, we take our input image, blur it (top-left), and then subtract the blurred image from the input image (top-right). We do this step to approximate the foreground of the image since, by nature, blurring will blur focused features and reveal more of the “structural” components of the image.

Figure 4.3. Top-left: Applying a small median blur to the input image. Top-right: Subtracting the blurred image from the original image. Bottom-left: Thresholding the subtracted image. Bottom-right: Performing min-max scaling to ensure the pixel intensities are in the range [0, 1].

46

Chapter 4. Using Machine Learning to Denoise Images for Better OCR Accuracy

Next, we threshold the approximate foreground region by setting any pixel values greater than zero to zero (Figure 4.3, bottom-left). The final step is to perform min-max scaling (bottom-right), which brings the pixel intensities back to the range [0, 1] (or [0, 255], depending on your data type). This final image will serve as noisy input when we perform our sliding window sampling. Now that we understand the general pre-processing steps, let’s implement them in Python code. Open the helpers.py file in the denoising submodule of pyimagesearch and let’s get to work defining our blur_and_threshold function:

1 2 3

# import the necessary packages import numpy as np import cv2

4 5 6 7 8 9

def blur_and_threshold(image, eps=1e-7): # apply a median blur to the image and then subtract the blurred # image from the original image to approximate the foreground blur = cv2.medianBlur(image, 5) foreground = image.astype("float") - blur

10 11 12 13

# threshold the foreground image by setting any pixels with a # value greater than zero to zero foreground[foreground > 0] = 0

The blur_and_threshold function accepts two parameters: i. image: Our input image that we’ll be pre-processing. ii. eps: An epsilon value used to prevent division by zero. We then apply a median blur to the image to reduce noise and then subtract the blur from the original image, resulting in a foreground approximation (Lines 8 and 9). From there, we threshold the foreground image by setting any pixel intensities greater than zero to zero (Line 13). The final step here is to perform min-max scaling:

15 16 17 18 19 20

# apply min/max scaling to bring the pixel intensities to the # range [0, 1] minVal = np.min(foreground) maxVal = np.max(foreground) foreground = (foreground - minVal) / (maxVal - minVal + eps)

4.2. Image Denoising with Machine Learning

47

# return the foreground-approximated image return foreground

21 22

Here we find the minimum and maximum values in the foreground image. We use these values to scale the pixel intensities in the foreground image to the range [0, 1]. This foreground-approximated image is then returned to the calling function.

4.2.6

Implementing the Feature Extraction Script

With our blur_and_threshold function defined, we can move on to our build_features.py script. As the name suggests, this script is responsible for creating our 5 x 5 - 25-d feature vectors from the noisy image and then extracting the target (i.e., cleaned) pixel value from the corresponding gold standard image. We’ll save these features to disk in CSV format and then train a Random Forest Regression model on them in Section 4.2.8. Let’s get started with our implementation now:

1 2 3 4 5 6 7

# import the necessary packages from config import denoise_config as config from pyimagesearch.denoising import blur_and_threshold from imutils import paths import progressbar import random import cv2

Line 2 imports our config to have access to our dataset file paths and output CSV file path. Notice that we’re using the blur_and_threshold function that we implemented in the previous section. The following code block grabs the paths to all images in our TRAIN_PATH (noisy images) and CLEANED_PATH (cleaned images that our RFR will learn to predict):

9 10 11

# grab the paths to our training images trainPaths = sorted(list(paths.list_images(config.TRAIN_PATH))) cleanedPaths = sorted(list(paths.list_images(config.CLEANED_PATH)))

12 13 14 15

# initialize the progress bar widgets = ["Creating Features: ", progressbar.Percentage(), " ", progressbar.Bar(), " ", progressbar.ETA()]

48

16 17

Chapter 4. Using Machine Learning to Denoise Images for Better OCR Accuracy

pbar = progressbar.ProgressBar(maxval=len(trainPaths), widgets=widgets).start()

Note that trainPaths contain all our noisy images. The cleanedPaths contain the corresponding cleaned images. Figure 4.4 shows an example. On the top is our input training image. On the bottom, we have the corresponding cleaned version of the image. We’ll take 5 x 5 regions from both the trainPaths and the cleanedPaths — the goal is to use the noisy 5 x 5 regions to predict the cleaned versions.

Figure 4.4. Top: A sample training image of a noisy input document. Bottom: The corresponding target/gold standard for the training image. Our goal is to train a model such that we can take the top input and automatically generate the bottom output.

Let’s start looping over these image combinations now:

19 20 21 22

# zip our training paths together, then open the output CSV file for # writing imagePaths = zip(trainPaths, cleanedPaths) csv = open(config.FEATURES_PATH, "w")

23 24

# loop over the training

images together

4.2. Image Denoising with Machine Learning

25 26 27 28 29 30 31

49

for (i, (trainPath, cleanedPath)) in enumerate(imagePaths): # load the noisy and corresponding gold-standard cleaned images # and convert them to grayscale trainImage = cv2.imread(trainPath) cleanImage = cv2.imread(cleanedPath) trainImage = cv2.cvtColor(trainImage, cv2.COLOR_BGR2GRAY) cleanImage = cv2.cvtColor(cleanImage, cv2.COLOR_BGR2GRAY)

On Line 21 we use Python’s zip function to combine the trainPaths and cleanedPaths together. We then open our output csv file for writing on Line 22. Line 25 starts a loop over our combinations of imagePaths. For each trainPath, we also have the corresponding cleanedPath. We load our trainImage and cleanImage from disk and then convert them to grayscale (Lines 28–31). Next, we need to pad both trainImage and cleanImage with a 2-pixel border in every direction:

33 34 35 36 37 38

# apply 2x2 padding to both images, replicating the pixels along # the border/boundary trainImage = cv2.copyMakeBorder(trainImage, 2, 2, 2, 2, cv2.BORDER_REPLICATE) cleanImage = cv2.copyMakeBorder(cleanImage, 2, 2, 2, 2, cv2.BORDER_REPLICATE)

39 40 41

# blur and threshold the noisy image trainImage = blur_and_threshold(trainImage)

42 43 44 45 46

# scale the pixel intensities in the cleaned image from the range # [0, 255] to [0, 1] (the noisy image is already in the range # [0, 1]) cleanImage = cleanImage.astype("float") / 255.0

Why do we need to bother with the padding? We’re sliding a window from left-to-right and top-to-bottom of the input image and using the pixels inside the window to predict the output center pixel located at x = 2, y = 2, not unlike a convolution operation (only with convolution our filters are fixed and defined). Like convolution, you need to pad your input images such that the output image is not smaller in size. You can refer to my guide on Convolutions with OpenCV and Python if you are unfamiliar with the concept: http://pyimg.co/8om5g [13]. After padding is complete, we blur and threshold the trainImage and manually scale the cleanImage to the range [0, 1]. The trainImage is already scaled to the range [0, 1] due to the min-max scaling inside blur_and_threshold.

50

Chapter 4. Using Machine Learning to Denoise Images for Better OCR Accuracy

With our images pre-processed, we can now slide a 5 x 5 window across them:

48 49 50 51 52 53 54 55 56

# slide a 5x5 window across the images for y in range(0, trainImage.shape[0]): for x in range(0, trainImage.shape[1]): # extract the window ROIs for both the train image and # clean image, then grab the spatial dimensions of the # ROI trainROI = trainImage[y:y + 5, x:x + 5] cleanROI = cleanImage[y:y + 5, x:x + 5] (rH, rW) = trainROI.shape[:2]

57 58 59 60

# if the ROI is not 5x5, throw it out if rW != 5 or rH != 5: continue

Lines 49 and 50 slide a 5 x 5 window from left-to-right and top-to-bottom across the trainImage and cleanImage. At each sliding window stop, we extract the 5 x 5 ROI of the training image and clean image (Lines 54 and 55). We grab the width and height of the trainROI on Line 56, and if either the width or height is not five pixels (due to us being on the borders of the image), we throw out the ROI (because we are only concerned with 5 x 5 regions). Next, we construct our feature vectors and save the row to our CSV file:

62 63 64 65 66

# our features will be the flattened 5x5=25 raw pixels # from the noisy ROI while the target prediction will # be the center pixel in the 5x5 window features = trainROI.flatten() target = cleanROI[2, 2]

67 68 69 70 71 72 73 74 75 76 77

# if we wrote *every* feature/target combination to disk # we would end up with millions of rows -- let's only # write rows to disk with probability N, thereby reducing # the total number of rows in the file if random.random() >> import cv2 >>> import easyocr >>>

Congrats on configuring your development environment for use with EasyOCR! Remark 5.1. If you are using the VM included with your purchase of this book, then you can use the pre-configured easyocr Python virtual environment instead of configuring your own. Just run the command workon easyocr to access it.

5.3

Using EasyOCR for Optical Character Recognition

Now that we’ve discussed the EasyOCR package and installed it on our machine, we can move on to our implementation. We’ll start with a review of our project directory structure and then implement easy_ocr.py, a script that will allow us to easily OCR images using the EasyOCR package.

5.3.1

Project Structure

Let’s inspect our project directory structure now:

|-| | | |-| | |--

images |-- arabic_sign.jpg |-- swedish_sign.jpg |-- turkish_sign.jpg pyimagesearch |--__init__.py |--helpers.py easy_ocr.py

This chapter’s EasyOCR project is already appearing to live up to its name. As you can see, we have three testing images/, two Python files in pyimagesearch/, and a single Python driver script, easy_ocr.py. Our driver script accepts any input image and the desired OCR language to get the job done quite easily, as we’ll see in the implementation section.

5.3. Using EasyOCR for Optical Character Recognition

5.3.2

65

Implementing our EasyOCR Script

With our development environment configured and our project directory structure reviewed, we are now ready to use the EasyOCR package in our Python script! Open the easy_ocr.py file in the project directory structure, and insert the following code:

1 2 3 4 5

# import the necessary packages from pyimagesearch.helpers import cleanup_text from easyocr import Reader import argparse import cv2

Our EasyOCR package should stand out here; notice how we’re importing Reader from the easyocr package. With our imports and convenience utility ready to go, let’s now define our command line arguments:

7 8 9 10 11 12 13 14 15

# construct the argument parser and parse the arguments ap = argparse.ArgumentParser() ap.add_argument("-i", "--image", required=True, help="path to input image to be OCR'd") ap.add_argument("-l", "--langs", type=str, default="en", help="comma separated list of languages to OCR") ap.add_argument("-g", "--gpu", type=int, default=-1, help="whether or not GPU should be used") args = vars(ap.parse_args())

Our script accepts three command line arguments: • --image: The path to the input image containing text for OCR. • --langs: A list of language codes separated by commas (no spaces). By default, our script assumes the English language (en). If you’d like to use the English and French models, you could pass en,fr. Or maybe you’d like to use Spanish, Portuguese, and Italian by using es,pt,it, respectively. Be sure to refer to EasyOCR’s listing of supported languages: http://pyimg.co/90nj9 [16]. • --gpu: Whether or not you’d like to use a GPU. Our default is -1, meaning that we’ll use our central processing unit (CPU) rather than a GPU. If you have a CUDA-capable GPU, enabling this option will allow faster OCR results. Given our command line arguments, let’s now perform OCR:

66

17 18 19

Chapter 5. Making OCR "Easy" with EasyOCR

# break the input languages into a comma separated list langs = args["langs"].split(",") print("[INFO] OCR'ing with the following languages: {}".format(langs))

20 21 22

# load the input image from disk image = cv2.imread(args["image"])

23 24 25 26 27

# OCR the input image using EasyOCR print("[INFO] OCR'ing input image...") reader = Reader(langs, gpu=args["gpu"] > 0) results = reader.readtext(image)

Line 18 breaks our --langs string (comma-delimited) into a Python list of languages for our EasyOCR engine. We then load the input --image via Line 22. Note: Unlike Tesseract, EasyOCR can work with OpenCV’s default BGR (Blue Green Red) color channel ordering. Therefore, we do not need to swap color channels after loading the image. To accomplish OCR with EasyOCR, we first instantiate a Reader object, passing the langs and --gpu Boolean to the constructor (Line 26). From there, we call the readtext method while passing our input image (Line 27). Both the Reader class and readtext method are documented in the EasyOCR website (http://pyimg.co/r24n1 [17]) if you’d like to customize your EasyOCR configuration. Let’s process our EasyOCR results now:

29 30 31 32

# loop over the results for (bbox, text, prob) in results: # display the OCR'd text and associated probability print("[INFO] {:.4f}: {}".format(prob, text))

33 34 35 36 37 38 39

# unpack the bounding box (tl, tr, br, bl) = bbox tl = (int(tl[0]), int(tl[1])) tr = (int(tr[0]), int(tr[1])) br = (int(br[0]), int(br[1])) bl = (int(bl[0]), int(bl[1]))

40 41 42 43 44 45 46

# cleanup the text and draw the box surrounding the text along # with the OCR'd text itself text = cleanup_text(text) cv2.rectangle(image, tl, br, (0, 255, 0), 2) cv2.putText(image, text, (tl[0], tl[1] - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 255, 0), 2)

47 48

# show the output image

5.3. Using EasyOCR for Optical Character Recognition

49 50

67

cv2.imshow("Image", image) cv2.waitKey(0)

Our EasyOCR results consist of a 3-tuple: • bbox: The bounding box coordinates of the localized text • text: Our OCR’d string • prob: The probability of the OCR results Looping over each EasyOCR result (Line 30), we first unpack the bounding box coordinates (Lines 35–39). To prepare our text for annotation, we sanitize it via our cleanup_text utility (Line 43). We then overlay our image with a bounding box surrounding the text and the text string itself (Lines 44–46). After all the results are processed and annotated, Lines 49 and 50 display the output image on our screen.

5.3.3

EasyOCR Results

We are now ready to see the results of applying OCR with the EasyOCR library. Open a terminal and execute the following command:

$ python easy_ocr.py --image images/arabic_sign.jpg --langs en,ar [INFO] OCR'ing with the following languages: ['en', 'ar'] [INFO] OCR'ing input image... Using CPU. Note: This module is much faster with a GPU. [INFO] 0.8129: [INFO] 0.7237: EXIT

From the output of this command (Figure 5.2), you can see that I am OCR’ing an airport sign containing both English and Arabic text. As the --langs en,ar arguments indicate, we’re instructing our script (and ultimately EasyOCR) to OCR in Arabic and English. You may pass a comma-separated list of languages that EasyOCR supports: http://pyimg.co/90nj9 [16]. EasyOCR can detect and correctly OCR the English and Arabic text in the input image. Note: If you are using EasyOCR for the first time, you’ll see an indication printed in your terminal that EasyOCR is “Downloading detection model[s].” Be patient while the files

68

Chapter 5. Making OCR "Easy" with EasyOCR

download. Once these models are cached on your system, you can use them again and again seamlessly and quickly.

Figure 5.2. To get started with EasyOCR for OCR, let’s try a picture of an “Exit” sign.

Let’s try another image, this one containing a Swedish sign:

$ python easy_ocr.py --image images/swedish_sign.jpg --langs en,sv [INFO] OCR'ing with the following languages: ['en', 'sv'] [INFO] OCR'ing input image... Using CPU. Note: This module is much faster with a GPU. [INFO] 0.7078: Fartkontrol

Figure 5.3 and the associated command above show that we ask EasyOCR to OCR both English (en) and Swedish (sv). “Fartkontrol” is a bit of a joke amongst the Swedes and Danes. Translated, “Fartkontrol” in English means “Speed Control” (or simply speed monitoring). However, when pronounced, “Fartkontrol” sounds like “fart control” — perhaps someone who

5.3. Using EasyOCR for Optical Character Recognition

69

is having an issue controlling their flatulence. In college, I had a friend who hung a Swedish “Fartkontrol” sign on their bathroom door — maybe you don’t find the joke funny, but anytime I see that sign, I chuckle to myself (perhaps I’m just an immature 8-year-old).

Figure 5.3. Call me immature, but the Swedish translation of “Speed Control” looks an awful lot like “Fart Control.” If I get a speeding ticket in Sweden in my lifetime, I don’t think the traffic cop “trafikpolis” will find my jokes funny (Image Source: http://pyimg.co/1lkum).

For our final example, let’s look at a Turkish stop sign:

$ python easy_ocr.py --image images/turkish_sign.jpg --langs en,tr [INFO] OCR'ing with the following languages: ['en', 'tr'] [INFO] OCR'ing input image... Using CPU. Note: This module is much faster with a GPU. [INFO] 0.9741: DUR

Here I ask EasyOCR to OCR both English (en) and Turkish (tr) texts by supplying those values as a comma-separated list via the --langs command line argument (Figure 5.4). EasyOCR can detect the text, ”DUR,” which, when translated from Turkish to English, is “STOP.” As you can see, EasyOCR lives up to its name — finally, an easy-to-use OCR package! Additionally, if you have a CUDA-capable GPU, you can obtain even faster OCR results by supplying the --gpu command line argument, as in the following:

$ python easy_ocr.py --image images/turkish_sign.jpg --langs en,tr --gpu 1

70

Chapter 5. Making OCR "Easy" with EasyOCR

But again, you will need to have a CUDA GPU configured for the PyTorch library (EasyOCR uses the PyTorch deep learning library under the hood).

Figure 5.4. The Turkish translation for “STOP” is properly OCR’d as “DUR” (image source: http://pyimg.co/2ta42).

5.4

Summary

In this chapter, you learned how to perform OCR using the EasyOCR Python package. Unlike the Tesseract OCR engine and the pytesseract package, which can be a bit tedious to work with if you are new to the world of OCR, the EasyOCR package lives up to its name — EasyOCR makes OCR with Python “easy.” Furthermore, EasyOCR has many benefits going for it: i. You can use your GPU to increase the speed of your OCR pipeline. ii. You can use EasyOCR to OCR text in multiple languages at the same time. iii. The EasyOCR API is Pythonic, making it simple and intuitive to use. When developing your OCR applications, definitely give EasyOCR a try. It could save you much time and headache, especially if you look for an OCR engine that is entirely pip-installable with no other dependencies or if you wish to apply OCR to multiple languages.

Chapter 6

Image/Document Alignment and Registration In this chapter, you will learn how to perform image alignment and image registration using OpenCV. Image alignment and registration have several practical, real-world use cases, including: • Medical: Magnetic resonance imaging (MRI) scans, single-photon emission computerized tomography (SPECT) scans, and other medical scans produce multiple images. Image registration can align multiple images together and overlay them on top of each other to help doctors and physicians better interpret these scans. From there, the doctor can read the results and provide a more accurate diagnosis. • Military: Automatic target recognition (ATR) algorithms accept multiple input images of the target, align them, and refine their internal parameters to improve target recognition. • Optical character recognition (OCR): Image alignment (often called document alignment in the context of OCR) can be used to build automatic form, invoice, or receipt scanners. We first align the input image to a template of the document we want to scan. From there, OCR algorithms can read the text from each field. In this chapter’s context, we’ll be looking at image alignment through the perspective of document alignment/registration, which is often used in OCR applications. This chapter will cover the fundamentals of image registration and alignment. Then, in the next chapter, we’ll incorporate image alignment with OCR, allowing us to create a document, form, and invoice scanner that aligns an input image with a template document and then extracts the text from each field in the document.

71

72

Chapter 6. Image/Document Alignment and Registration

6.1

Chapter Learning Objectives

In this chapter, you will: • Learn how to detect keypoints and extract local invariant descriptors using oriented FAST and rotated BRIEF (ORB) • Discover how to match keypoints together and draw correspondences • Compute a homography matrix using the correspondences • Take the homography matrix and use it to apply a perspective warp, thereby aligning the two images/documents together

6.2

Document Alignment and Registration with OpenCV

In the first part of this chapter, we’ll briefly discuss what image alignment and registration is. We’ll learn how OpenCV can help us align and register our images using keypoint detectors, local invariant descriptors, and keypoint matching. Next, we’ll implement a helper function, align_images, which, as the name suggests, will allow us to align two images based on keypoint correspondences. I’ll then show you how to use the align_document function to align an input image with a template.

6.2.1

What Is Document Alignment and Registration?

Image alignment and registration is the process of: i. Accepting two input images that contain the same object but at slightly different viewing angles ii. Automatically computing the homography matrix used to align the images (whether that be featured-based keypoint correspondences, similarity measures, or even deep neural networks that automatically learn the transformation) iii. Taking that homography matrix and applying a perspective warp to align the images together For example, consider Figure 6.1. On the top-left, we have a template of a W-4 form, which is a U.S. Internal Revenue Service (IRS) tax form that employees fill out so that employers know how much tax to withhold from their paycheck (depending on deductions, filing status, etc.).

6.2. Document Alignment and Registration with OpenCV

Figure 6.1. We have three scans of the well-known IRS W-4 form. We have an empty form (top-left), a partially completed form that is out of alignment (top-right), and our processed form that has been aligned by our algorithm (bottom).

73

74

Chapter 6. Image/Document Alignment and Registration

I have partially filled out a W-4 form with faux data and then captured a photo of it with my phone (top-right). Finally, you can see the output image alignment and registration on the bottom — notice how the input image is now aligned with the template. In the next chapter, you’ll learn how to OCR each of the individual fields from the input document and associate them with the template fields. For now, though, we’ll only be learning how to align a form with its template as an important pre-processing step before applying OCR.

6.2.2

How Can OpenCV Be Used for Document Alignment?

There are several image alignment and registration algorithms:

• The most popular image alignment algorithms are feature-based. They include keypoint detectors (DoG, Harris, good features to track, etc.), local invariant descriptors (SIFT, SURF, ORB, etc.), and keypoint matching such as random sample consensus (RANSAC) and its variants.

• Medical applications often use similarity measures for image registration, typically cross-correlation, the sum of squared intensity differences, and mutual information.

• With the resurgence of neural networks, deep learning can even be used for image alignment by automatically learning the homography transform.

We’ll be implementing image alignment and registration using feature-based methods. Feature-based methods start with detecting keypoints in our two input images (Figure 6.2). Keypoints are meant to identify salient regions of an input image. We extract local invariant descriptors for each keypoint, quantifying the region surrounding each keypoint in the input image. Scale-invariant feature transform (SIFT) features, for example, are 128-d, so if we detect 528 keypoints in a given input image, then we’ll have a total of 528 vectors, each of which is 128-d.

6.2. Document Alignment and Registration with OpenCV

75

Figure 6.2. We use a feature-based image alignment technique that detects keypoints that are the location of regions of interest. The keypoints found are highlighted in yellow. They will be used to align and register the two images (documents/forms).

Given our features, we apply algorithms such as RANSAC [18] to match our keypoints and determine their correspondences (Figure 6.3). Provided we have enough keypoint matches and correspondences. We can then compute a homography matrix, which allows us to apply a perspective warp to align the images. You’ll be learning how to build an OpenCV project that accomplishes image alignment and registration via a homography matrix in the remainder of this chapter.

76

Chapter 6. Image/Document Alignment and Registration

For more details on homography matrix construction and the role it plays in computer vision, be sure to refer to this OpenCV reference: http://pyimg.co/ogkky [19].

Figure 6.3. Here we see the keypoints matched between our partially completed form that is out of alignment with our original form that is properly aligned. You can see there are colored lines drawn using OpenCV between each corresponding keypoint pair.

6.2.3

Project Structure

Let’s start by inspecting our project directory structure:

|-| | | | |-| | |-| |

pyimagesearch |-- __init__.py |-- alignment | |-- __init__.py | |-- align_images.py forms |-- form_w4.pdf |-- form_w4_orig.pdf scans |-- scan_01.jpg |-- scan_02.jpg

6.2. Document Alignment and Registration with OpenCV

77

|-- align_document.py |-- form_w4.png

We have a simple project structure consisting of the following images: • scans/: Contains two JPG testing photos of a tax form • form_w4.png: Our template image of the official 2020 IRS W-4 form Additionally, we’ll be reviewing two Python files: • align_images.py: Holds our helper function, which aligns a scan to a template utilizing an OpenCV pipeline • align_document.py: Our driver file in the main directory brings all the pieces together to perform image alignment and registration with OpenCV In the next section, we’ll work on implementing our helper utility for aligning images.

6.2.4

Aligning Documents with Keypoint Matching

We are now ready to implement image alignment and registration using OpenCV. For this section, we’ll attempt to align the images in Figure 6.4. On the left, we have our template W-4 form, while on the right, we have a sample W-4 form I have filled out and captured with my phone. The end goal is to align these images such that their fields match up. Let’s get started! Open align_images.py inside the alignment submodule of pyimagesearch and let’s get to work:

1 2 3 4

# import the necessary packages import numpy as np import imutils import cv2

5 6 7 8 9 10

def align_images(image, template, maxFeatures=500, keepPercent=0.2, debug=False): # convert both the input image and template to grayscale imageGray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) templateGray = cv2.cvtColor(template, cv2.COLOR_BGR2GRAY)

78

Chapter 6. Image/Document Alignment and Registration

Figure 6.4. We have two similar W-4 forms in different orientations. We have our original template form in the desired orientation (left) and the partially completed form that needs to be aligned (right). Our goal is to use OpenCV to align the right image to the left template image using keypoint matching and a homography matrix.

Our align_images function begins on Line 6 and accepts five parameters: • image: Our input photo/scan of a form (e.g., IRS W-4). From an arbitrary viewpoint, the form itself should be identical to the template image but with form data present. • template: The template form image. • maxFeatures: Places an upper bound on the number of candidate keypoint regions to consider. • keepPercent: Designates the percentage of keypoint matches to keep, effectively allowing us to eliminate noisy keypoint matching results • debug: A flag indicating whether to display the matched keypoints. By default, keypoints are not displayed. However, I recommend setting this value to True for debugging purposes. Given that we have defined our function let’s implement our image processing pipeline. Diving in, the first action we take is converting both our image and template to grayscale (Lines 9 and 10).

6.2. Document Alignment and Registration with OpenCV

79

Next, we will detect keypoints, extract local binary features, and correlate these features between our input image and the template:

12 13 14 15 16

# use ORB to detect keypoints and extract (binary) local # invariant features orb = cv2.ORB_create(maxFeatures) (kpsA, descsA) = orb.detectAndCompute(imageGray, None) (kpsB, descsB) = orb.detectAndCompute(templateGray, None)

17 18 19 20 21

# match the features method = cv2.DESCRIPTOR_MATCHER_BRUTEFORCE_HAMMING matcher = cv2.DescriptorMatcher_create(method) matches = matcher.match(descsA, descsB, None)

We use the ORB algorithm (http://pyimg.co/46f2s [20]) to detect keypoints and extract binary local invariant features (Lines 14–16). The Hamming method computes the distance between these binary features to find the best matches (Lines 19–21). You can learn more about the keypoint detection and local binary patterns in my Local Binary Patterns with Python & OpenCV tutorial (http://pyimg.co/94p11 [21]) or the PyImageSearch Gurus course (http://pyimg.co/gurus [22]). As we now have our keypoint matches, our next steps include sorting, filtering, and displaying:

23 24 25

# sort the matches by their distance (the smaller the distance, # the "more similar" the features are) matches = sorted(matches, key=lambda x:x.distance)

26 27 28 29

# keep only the top matches keep = int(len(matches) * keepPercent) matches = matches[:keep]

30 31 32 33 34 35 36 37

# check to see if we should visualize the matched keypoints if debug: matchedVis = cv2.drawMatches(image, kpsA, template, kpsB, matches, None) matchedVis = imutils.resize(matchedVis, width=1000) cv2.imshow("Matched Keypoints", matchedVis) cv2.waitKey(0)

Here, we sort the matches (Line 25) by their distance. The smaller the distance, the more similar the two keypoint regions are. Lines 28 and 29 keep only the top matches — otherwise we risk introducing noise. If we are in debug mode, we will use cv2.drawMatches to visualize the matches using OpenCV drawing methods (Lines 32–37).

80

Chapter 6. Image/Document Alignment and Registration

Next, we will conduct a couple of steps before computing our homography matrix:

39 40 41 42 43

# allocate memory for the keypoints (x,y-coordinates) from the # top matches -- we'll use these coordinates to compute our # homography matrix ptsA = np.zeros((len(matches), 2), dtype="float") ptsB = np.zeros((len(matches), 2), dtype="float")

44 45 46 47 48 49 50

# loop over the top matches for (i, m) in enumerate(matches): # indicate that the two keypoints in the respective images # map to each other ptsA[i] = kpsA[m.queryIdx].pt ptsB[i] = kpsB[m.trainIdx].pt

Here we are: • Allocating memory to store the keypoints (ptsA and ptsB) for our top matches • Initiating a loop over our top matches and inside indicating that keypoints A and B map to one another Given our organized pairs of keypoint matches, now we’re ready to align our image:

52 53 54

# compute the homography matrix between the two sets of matched # points (H, mask) = cv2.findHomography(ptsA, ptsB, method=cv2.RANSAC)

55 56 57 58

# use the homography matrix to align the images (h, w) = template.shape[:2] aligned = cv2.warpPerspective(image, H, (w, h))

59 60 61

# return the aligned image return aligned

Aligning our image can be boiled down to our final two steps: • Find our homography matrix using the keypoints and RANSAC algorithm (Line 54). • Align our image by applying a warped perspective (cv2.warpPerspective) to our image and matrix, H (Lines 57 and 58). This aligned image result is returned to the caller via Line 61. Congratulations! You have completed the most technical part of the chapter. Note: A big thanks to Satya Mallick at LearnOpenCV for his concise implementation of keypoint matching (http://pyimg.co/16qi2 [23]), upon which ours is based.

6.2. Document Alignment and Registration with OpenCV

6.2.5

81

Implementing Our Document Alignment Script

Now that we have the align_images function at our disposal, we need to develop a driver script that: i. Loads an image and template from disk ii. Performs image alignment and registration iii. Displays the aligned images to our screen to verify that our image registration process is working properly Open align_document.py, and let’s review it to see how we can accomplish exactly that:

1 2 3 4 5 6

# import the necessary packages from pyimagesearch.alignment import align_images import numpy as np import argparse import imutils import cv2

7 8 9 10 11 12 13 14

# construct the argument parser and parse the arguments ap = argparse.ArgumentParser() ap.add_argument("-i", "--image", required=True, help="path to input image that we'll align to template") ap.add_argument("-t", "--template", required=True, help="path to input template image") args = vars(ap.parse_args())

The key import on Lines 2–6 that should stand out is align_images, a function which we implemented in the previous section. Our script requires two command line arguments: • --image: The path to the input image scan or photo • --template: Our template image path; this could be an official company form or in our case the 2020 IRS W-4 template image Our next step is to align our two input images:

16 17 18 19 20

# load the input image and template from disk print("[INFO] loading images...") image = cv2.imread(args["image"]) template = cv2.imread(args["template"])

82

21 22 23

Chapter 6. Image/Document Alignment and Registration

# align the images print("[INFO] aligning images...") aligned = align_images(image, template, debug=True)

After loading both our input --image and input --template (Lines 18 and 19), we take advantage of our helper routine, align_images, passing each as a parameter (Line 23). Notice how I’ve set the debug flag to True, indicating that I’d like the matches to be annotated. When you make the align_images function part of a real OCR pipeline you will turn the debugging option off. We’re going to visualize our results in two ways: i. Stacked side-by-side ii. Overlayed on top of one another These visual representations of our results will allow us to determine if the alignment is/was successful. Let’s prepare our aligned image for a stacked comparison with its template:

25 26 27 28

# resize both the aligned and template images so we can easily # visualize them on our screen aligned = imutils.resize(aligned, width=700) template = imutils.resize(template, width=700)

29 30 31 32 33

# our first output visualization of the image alignment will be a # side-by-side comparison of the output aligned image and the # template stacked = np.hstack([aligned, template])

Lines 27 and 28 resize the two images such that they will fit on our screen. We then use np.hstack to stack our images next to each other to easily inspect the results (Line 33). And now let’s overlay the template form on top of the aligned image:

35 36 37 38 39 40

# our second image alignment visualization will be *overlaying* the # aligned image on the template, that way we can obtain an idea of # how good our image alignment is overlay = template.copy() output = aligned.copy() cv2.addWeighted(overlay, 0.5, output, 0.5, 0, output)

41 42 43

# show the two output image alignment visualizations cv2.imshow("Image Alignment Stacked", stacked)

6.2. Document Alignment and Registration with OpenCV

44 45

83

cv2.imshow("Image Alignment Overlay", output) cv2.waitKey(0)

In addition to the side-by-side stacked visualization from above, an alternative visualization is to overlay the input image on the template, so we can readily see the amount of misalignment. Lines 38–40 use OpenCV’s cv2.addWeighted to transparently blend the two images into a single output image with the pixels from each image having equal weight. Finally, we display our two visualizations on-screen (Lines 43–45). Well done! It is now time to inspect our results.

6.2.6

Image and Document Alignment Results

We are now ready to apply image alignment and registration using OpenCV! Open a terminal and execute the following command:

$ python align_document.py --template form_w4.png --image scans/scan_01.jpg [INFO] loading images... [INFO] aligning images...

Figure 6.5 (top) shows our input image, scan_01.jpg — notice how this image has been captured with my smartphone at a non-90◦ viewing angle (i.e., not a top-down, bird’s-eye view of the input image). It’s very common for images to be captured from unpredictable angles when capturing documents with phone cameras. To get better light, people will often move to the side of an image creating an angle to their photo. Many times people won’t even be aware they are taking a picture at an angle. This means many images are captured from an angle. For OCR to work, we need a way to allow people to take photos from multiple angles. We are the ones who need to align the images after they are taken. To create an OCR pipeline that doesn’t do this alignment would result in a brittle and inaccurate system. We then apply image alignment and registration, resulting in Figure 6.5 (bottom). On the bottom-left, you can see the input image (after alignment), while the bottom-right shows the original W-4 template image. Notice how the two images have been automatically aligned using keypoint matching!

84

Chapter 6. Image/Document Alignment and Registration

Figure 6.5. Top: We have our partially completed form, which has been taken with our iPhone from the perspective of looking down at the image on the table. The resulting image is at an angle and rotated. Bottom: We have a side-by-side comparison. Our partially completed form has been aligned and registered (left) to be more similar to our template form (right). Our OpenCV keypoint matching algorithm did a pretty nice job!

6.2. Document Alignment and Registration with OpenCV

An alternative visualization can be seen in Figure 6.6. Here we have overlayed the output aligned image on top of the template.

Figure 6.6. This time, we overlay our aligned and registered image on our template with a 50/50 blend. Our overlay method makes the differences pop. Notice there is a slight effect of “double vision” where there are some minor differences.

85

86

Chapter 6. Image/Document Alignment and Registration

Our alignment isn’t perfect (obtaining a pixel-perfect alignment is incredibly challenging and, in some cases, unrealistic). Still, the form’s fields are sufficiently aligned such that we’ll be able to OCR the text and associate the fields together.

6.3

Summary

In this chapter, you learned how to perform image alignment and registration using OpenCV. Image alignment has many use cases, including medical scans, military-based automatic target acquisition, and satellite image analysis. We chose to examine image alignment for one of the most important (and most utilized) purposes — optical character recognition (OCR). Applying image alignment to an input image allows us to align it with a template document. Once we have the input image aligned with the template, we can apply OCR to recognize the text in each field. And since we know the location of each of the document fields, it becomes easy to associate the OCR’d text with each field.

Chapter 7

OCR’ing a Document, Form, or Invoice In this chapter, you will learn how to OCR a document, form, or invoice using Tesseract, OpenCV, and Python. Previously, we discovered how to accept an input image and align it to a template image. Recall that we had a template image — in our case, it was a form from the U.S. Internal Revenue Service. We also had the image that we wished to align with the template, allowing us to match fields. The process resulted in the alignment of both the template and the form for OCR. We just need to associate fields on the form such as name, address, EIN, and more. Knowing where and what the fields are allows us to OCR each field and keep track of them for further processing (e.g., automated database entry). However, that raises the questions: • How do we go about implementing this document OCR pipeline? • What OCR algorithms will we need to utilize? • And how complicated is this OCR application going to be? • And most importantly, can we exercise our computer vision and OCR skills to implement our entire document OCR pipeline in under 150 lines of code? Of course, I wouldn’t ask such a question unless the answer is “yes.” Here’s why we are spending so much time OCR’ing forms. Forms are everywhere. Schools have many forms for application and enrollment. When’s the last time you’ve gone through the scholarship process? Healthcare is also full of forms. Both the doctor and the dentist are likely to ask you to fill out a form at your next visit. 87

88

Chapter 7. OCR’ing a Document, Form, or Invoice

We also get forms for property like real estate and homeowners insurance and don’t forget about business forms. If you want to vote, get a permit, or get a passport, you are in for more forms. As you can see, forms are everywhere so we want to learn to OCR them, let’s dive in!

7.1

Automatically Registering and OCR’ing a Form

In the first part of this chapter, we’ll briefly discuss why we may want to OCR documents, forms, invoices, or any type of physical document. From there, we’ll review the steps required to implement a document OCR pipeline. We’ll then implement each of the individual steps in a Python script using OpenCV and Tesseract. Finally, we’ll review the results of applying image alignment and OCR to our sample images.

7.1.1

Why Use OCR on Forms, Invoices, and Documents?

Despite living in the digital age, we still have a strong reliance on physical paper trails, especially in large organizations such as government, enterprise companies, and universities/colleges. The need for physical paper trails combined with the fact that nearly every document needs to be organized, categorized, and even shared with multiple people in an organization requires that we also digitize the information on the document and store it in our databases. Large organizations employ data entry teams whose sole purpose is to take these physical documents, manually re-type the information, and then save it into the system. Optical character recognition (OCR) algorithms can automatically digitize these documents, extract the information, and pipe them into a database for storage, alleviating the need for large, expensive, and even error-prone manual entry teams. In this chapter, you’ll learn how to implement a basic document OCR pipeline using OpenCV and Tesseract.

7.1.2

Steps to Implementing a Document OCR Pipeline with OpenCV and Tesseract

Implementing a document OCR pipeline with OpenCV and Tesseract is a multi-step process. In this section, we’ll discover the five steps required for creating a pipeline to OCR a form. Step #1 involves defining the locations of fields in the input image document. We can do this by opening our template image in our favorite image editing software, such as Photoshop, GNU Image Manipulation Program (GIMP), or whatever photo application is built into your operating system. From there, we manually examine the image and determine the bounding box (x, y )-coordinates of each field we want to OCR, as shown on the top of Figure 7.1.

7.1. Automatically Registering and OCR’ing a Form

89

Figure 7.1. Top: Specifying the locations in a document (i.e., form fields) is Step #1 in implementing a document OCR pipeline with OpenCV, Tesseract, and Python. Bottom: Presenting an image (e.g., a document scan or smartphone photo of a document on a desk) to our OCR pipeline is Step #2 in our automated OCR system based on OpenCV, Tesseract, and Python.

Then, in Step #2, we accept an input image containing the document we want to OCR and present it to our OCR pipeline. Figure 7.1 shows both the image top-left and the original form top-right. We can then (Step #3) apply automatic image alignment/registration to align the input image with the template form (Figure 7.2, top-left). In Step #4, we loop over all text field locations (which we defined in Step #1), extract the ROI, and apply OCR to the ROI. During this step, we’re able to OCR the text itself and associate it with a text field in the original template document.

90

Chapter 7. OCR’ing a Document, Form, or Invoice

Figure 7.2. Top-left: Aligning a scanned document with its template using OpenCV and Python represents Step #3 of our OCR pipeline. Top-right, Middle, and Bottom: Knowing the form field locations from Step #1 allows us to perform Step #4, which consists of extracting ROIs from our aligned document and accomplishing OCR.

The final Step #5 is to display our output OCR’d document depicted in Figure 7.3.

Figure 7.3. Finally, Step #5 in our OCR pipeline takes action with the OCR’d text data. Given that this is a proof of concept, we’ll simply annotate the OCR’d text data on the aligned scan for verification. This is the point where a real-world system would pipe the information into a database or make a decision based upon it (e.g., perhaps you need to apply a mathematical formula to several fields in your document).

7.1. Automatically Registering and OCR’ing a Form

91

We’ll learn how to develop a Python script to accomplish Steps #1–#5 in this chapter by creating an OCR document pipeline using OpenCV and Tesseract.

7.1.3 |-| | | | | |-| | |-|--

Project Structure

pyimagesearch |-- alignment | |-- __init__.py | |-- align_images.py |-- __init__.py |-- helpers.py scans |-- scan_01.jpg |-- scan_02.jpg form_w4.png ocr_form.py

The directory and file structure for this chapter are very straightforward. We have three images: • scans/scan_01.jpg: A sample IRS W-4 has been filled with fake tax information, including my name. • scans/scan_02.jpg: A similar sample IRS W-4 document that has been populated with fake tax information. • form_w4.png: The official 2020 IRS W-4 form template. This empty form does not have any information entered into it. We need it and the field locations to line up the scans and ultimately extract information from the scans. We’ll manually determine the field locations with an external photo editing/previewing application. And we have just a single Python driver script to review: ocr_form.py. This form parser relies on two helper functions in the pyimagesearch module that we’re familiar with from previous chapters: • align_images: Contained within the alignment submodule • cleanup_text: In our helpers.py file If you’re ready to dive in, simply head to the implementation section next!

7.1.4

Implementing Our Document OCR Script with OpenCV and Tesseract

We are now ready to implement our document OCR Python script using OpenCV and Tesseract. Open a new file, name it ocr_form.py, and insert the following code:

92

1 2 3 4 5 6 7 8

Chapter 7. OCR’ing a Document, Form, or Invoice

# import the necessary packages from pyimagesearch.alignment import align_images from pyimagesearch.helpers import cleanup_text from collections import namedtuple import pytesseract import argparse import imutils import cv2

You should recognize each of the imports on Lines 2–8; however, let’s highlight a few of them. The cleanup_text helper function is used to strip out non-ASCII text from a string. We need to cleanse our text because OpenCV’s cv2.putText is unable to draw non-ASCII characters on an image (unfortunately, OpenCV replaces each unknown character with a ?). Of course, our effort is a lot easier when we use OpenCV, PyTesseract, and imutils. Next, let’s handle our command line arguments:

10 11 12 13 14 15 16

# construct the argument parser and parse the arguments ap = argparse.ArgumentParser() ap.add_argument("-i", "--image", required=True, help="path to input image that we'll align to template") ap.add_argument("-t", "--template", required=True, help="path to input template image") args = vars(ap.parse_args())

Our script requires two command line arguments: (1) --image is our input image of a form or invoice and (2) --template is the path to our template form or invoice. We’ll align our image to the template and then OCR various fields as needed. We aren’t creating a “smart form OCR system” in which all text is recognized and fields are designed based on regular expression patterns. That is certainly doable, but to keep this chapter lightweight, I’ve manually defined OCR_Locations for each field about which we are concerned. The benefit is that we’ll be able to give each field a name and specify the exact (x, y )-coordinates serving as the bounds of the field. Let’s work on defining the text field locations in Step #1 now:

18 19 20 21

# create a named tuple which we can use to create locations of the # input document which we wish to OCR OCRLocation = namedtuple("OCRLocation", ["id", "bbox", "filter_keywords"])

22 23 24 25 26

# define the locations of each area of the document we wish to OCR OCR_LOCATIONS = [ OCRLocation("step1_first_name", (265, 237, 751, 106), ["middle", "initial", "first", "name"]),

7.1. Automatically Registering and OCR’ing a Form

OCRLocation("step1_last_name", (1020, 237, 835, 106), ["last", "name"]), OCRLocation("step1_address", (265, 336, 1588, 106), ["address"]), OCRLocation("step1_city_state_zip", (265, 436, 1588, 106), ["city", "zip", "town", "state"]), OCRLocation("step5_employee_signature", (319, 2516, 1487, 156), ["employee", "signature", "form", "valid", "unless", "you", "sign"]), OCRLocation("step5_date", (1804, 2516, 504, 156), ["date"]), OCRLocation("employee_name_address", (265, 2706, 1224, 180), ["employer", "name", "address"]), OCRLocation("employee_ein", (1831, 2706, 448, 180), ["employer", "identification", "number", "ein"]),

27 28 29 30 31 32 33 34 35 36 37 38 39 40 41

93

]

Here, Lines 20 and 21 create a named tuple consisting of the following: • "OCRLocation": The name of our tuple. • "id": A short description of the field for easy reference. Use this field to describe what the form field is. For example, is it a zip-code field? • "bbox": The bounding box coordinates of a field in list form using the following order: [x, y, w, h]. In this case, x and y are the top-left coordinates, and w and h are the width and height. • "filter_keywords": A list of words that we do not wish to consider for OCR, such as form field instructions, as demonstrated in Figure 7.4.

Figure 7.4. A W-4 form with eight fields outlined in red.

94

Chapter 7. OCR’ing a Document, Form, or Invoice

Lines 24–41 define eight fields of an official 2020 IRS W-4 tax form, pictured in Figure 7.4. Bounding box coordinates ("bbox") were manually determined by inspecting the (x, y )-coordinates of the image. This can be accomplished using any photo editing application, including Photoshop, GIMP, or the basic preview/paint application built into your operating system. Alternatively, you could use OpenCV mouse click events per my blog post, Capturing Mouse Click Events with Python and OpenCV (http://pyimg.co/fmckl [24]). Now that we’ve handled imports, configured command line arguments, and defined our OCR field locations, let’s go ahead and load and align our input --image to our --template (Step #2 and Step #3):

43 44 45 46

# load the input image and template from disk print("[INFO] loading images...") image = cv2.imread(args["image"]) template = cv2.imread(args["template"])

47 48 49 50

# align the images print("[INFO] aligning images...") aligned = align_images(image, template)

As you can see, Lines 45 and 46 load both our input --image (e.g., a scan or snap from your smartphone camera) and our --template (e.g., a document straight from the IRS, your mortgage company, accounting department, or anywhere else depending on your needs). Remark 7.1. You may be wondering how I converted the form_w4.png from PDF form (most IRS documents are PDFs these days). This process is straightforward with a free OS-agnostic tool called ImageMagick (Figure 7.5). With ImageMagick installed, you can simply use the convert command (http://pyimg.co/ekwbu [25]). For example, you could enter the following command: $ convert /path/to/taxes/2020/forms/form_w4.pdf ./form_w4.png

As you can see, ImageMagick is smart enough to recognize that you want to convert a PDF to a PNG image based on a combination of the file extension as well as the file itself. You could alter the command quite easily to produce a JPG if you’d like. Once your form is in PNG or JPG form, you can use it with OpenCV and PyTesseract! Do you have a lot of forms? Simply use the mogrify command, which supports wildcard operators (http://pyimg.co/t16bj [26]). Once the image files are loaded into memory, we simply take advantage of our align_images helper utility (Line 50) to perform the alignment. Figure 7.6 shows the result of aligning our scan01.jpg input to the form template. Notice how our input image (left) has been aligned to the template document (right).

7.1. Automatically Registering and OCR’ing a Form

95

Figure 7.5. ImageMagick software webpage.

Figure 7.6. Left: an image of a W-4 form. Right: a W-4 Form.

The next step (Step #4) is to loop over each of our OCR_LOCATIONS and apply OCR to each of the text fields using the power of Tesseract and PyTesseract:

52 53

# initialize a results list to store the document OCR parsing results print("[INFO] OCR'ing document...")

96

54

Chapter 7. OCR’ing a Document, Form, or Invoice

parsingResults = []

55 56 57 58 59 60

# loop over the locations of the document we are going to OCR for loc in OCR_LOCATIONS: # extract the OCR ROI from the aligned image (x, y, w, h) = loc.bbox roi = aligned[y:y + h, x:x + w]

61 62 63 64

# OCR the ROI using Tesseract rgb = cv2.cvtColor(roi, cv2.COLOR_BGR2RGB) text = pytesseract.image_to_string(rgb)

First, we initialize the parsingResults list to store our OCR results for each field of text (Line 54). From there, we proceed to loop over each of the OCR_LOCATIONS (beginning on Line 57), which we have previously manually defined. Inside the loop (Lines 59–64), we begin by (1) extracting the particular text field ROI from the aligned image and (2) use PyTesseract to OCR the ROI. Remember, Tesseract expects an RGB format image, so Line 63 swaps color channels accordingly. Now let’s break each OCR’d text field into individual lines/rows:

66 67 68 69 70

# break the text into lines and loop over them for line in text.split("\n"): # if the line is empty, ignore it if len(line) == 0: continue

71 72 73 74 75 76

# convert the line to lowercase and then check to see if the # line contains any of the filter keywords (these keywords # are part of the *form itself* and should be ignored) lower = line.lower() count = sum([lower.count(x) for x in loc.filter_keywords])

77 78 79 80 81 82 83 84

# if the count is zero then we know we are *not* examining a # text field that is part of the document itself (ex., info, # on the field, an example, help text, etc.) if count == 0: # update our parsing results dictionary with the OCR'd # text if the line is *not* empty parsingResults.append((loc, line))

Line 67 begins a loop over the text lines where we immediately ignore empty lines (Lines 69 and 70). Assuming the line isn’t empty, we filter it for keywords (forcing to lower-case characters in the process) to ensure that we aren’t examining a part of the document itself. In other words, we only care about form-filled information and not the instructional text on the template form itself. Lines 75–84 accomplish the filtering process and add the OCR’d field to parsingResults accordingly.

7.1. Automatically Registering and OCR’ing a Form

97

For example, consider the “First name and middle initial” field in Figure 7.7. While I’ve filled out this field with my first name, “Adrian,” the text “(a) First name and middle initial” will still be OCR’d by Tesseract — the code above automatically filters out the instructional text inside the field, ensuring only the human inputted text is returned.

Figure 7.7. First and middle initial field filled in with Adrian

I hope you’re still with me. Let’s carry on by post-processing our parsingResults to clean them up:

86 87

# initialize a dictionary to store our final OCR results results = {}

88 89 90 91 92

# loop over the results of parsing the document for (loc, line) in parsingResults: # grab any existing OCR result for the current ID of the document r = results.get(loc.id, None)

93 94 95 96 97 98

# if the result is None, initialize it using the text and location # namedtuple (converting it to a dictionary as namedtuples are not # hashable) if r is None: results[loc.id] = (line, loc._asdict())

99 100 101 102 103 104 105 106

# otherwise, there exists a OCR result for the current area of the # document, so we should append our existing line else: # unpack the existing OCR result and append the line to the # existing text (existingText, loc) = r text = "{}\n{}".format(existingText, line)

107 108 109

# update our results dictionary results[loc["id"]] = (text, loc)

Our final results dictionary (Line 87) will soon hold the cleansed parsing results consisting of the unique ID of the text location (key) and the 2-tuple of the OCR’d text and its location

98

Chapter 7. OCR’ing a Document, Form, or Invoice

(value). Let’s begin populating our results by looping over our parsingResults on Line 90. Our loop accomplishes three tasks: • We grab any existing result for the current text field ID • If there is no current result, we simply store the text line and text loc (location) in the results dictionary • Otherwise, we append the line to any existingText separated by a newline for the field and update the results dictionary We’re finally ready to perform Step #5 — visualizing our OCR results:

111 112 113 114

# loop over the results for (locID, result) in results.items(): # unpack the result tuple (text, loc) = result

115 116 117 118 119

# display the OCR result to our terminal print(loc["id"]) print("=" * len(loc["id"])) print("{}\n\n".format(text))

120 121 122 123 124 125

# extract the bounding box coordinates of the OCR location and # then strip out non-ASCII text so we can draw the text on the # output image using OpenCV (x, y, w, h) = loc["bbox"] clean = cleanup_text(text)

126 127 128

# draw a bounding box around the text cv2.rectangle(aligned, (x, y), (x + w, y + h), (0, 255, 0), 2)

129 130 131 132 133 134 135

# loop over all lines in the text for (i, line) in enumerate(text.split("\n")): # draw the line on the output image startY = y + (i * 70) + 40 cv2.putText(aligned, line, (x, startY), cv2.FONT_HERSHEY_SIMPLEX, 1.8, (0, 0, 255), 5)

Looping over each of our results begins on Line 112. Our first task is to unpack the 2-tuple consisting of the OCR’d and parsed text as well as its loc (location) via Line 114. Both of these results are then printed in our terminal (Lines 117–119). From there, we extract the bounding box coordinates of the text field (Line 124). Subsequently, we strip out non-ASCII characters from the OCR’d text via our cleanup_text helper utility (Line 125). Cleaning up our text ensures we can use OpenCV’s cv2.putText function to annotate the output image.

7.1. Automatically Registering and OCR’ing a Form

99

We then proceed to draw the bounding box rectangle around the text (Line 128) and annotate each line of text (delimited by newlines) on the output image (Lines 131–135). Finally, we’ll display our (1) original input --image and (2) annotated output result:

137 138 139 140 141

# show the input and output images, resizing it such that they fit # on our screen cv2.imshow("Input", imutils.resize(image, width=700)) cv2.imshow("Output", imutils.resize(aligned, width=700)) cv2.waitKey(0)

As you can see, our cv2.imshow commands also apply aspect-aware resizing because high resolution scans tend not to fit on the average computer screen (Lines 139 and 140). To stop the program, simply press any key while one of the windows is in focus. Great job implementing your automated OCR system with Python, OpenCV, and Tesseract! In the next section, we’ll put it to the test.

7.1.5

OCR Results Using OpenCV and Tesseract

We are now ready to OCR our document using OpenCV and Tesseract. Fire up your terminal, find the chapter directory, and enter the following command:

$ python ocr_form.py --image scans/scan_01.jpg --template form_w4.png [INFO] loading images... [INFO] aligning images... [INFO] OCR'ing document... step1_first_name ================ Adrian step1_last_name =============== Rosebrock step1_address ============= PO Box 17598 #17900 step1_city_state_zip ==================== Baltimore, MD 21297-1598 step5_employee_signature ======================== Adrian Rosebrock

100

Chapter 7. OCR’ing a Document, Form, or Invoice

step5_date ========== 2020/06/10 employee_name_address ===================== PylmageSearch PO BOX 1234 Philadelphia, PA 19019 employee_ein ============ 12-3456789

As Figure 7.8 demonstrates, we have our input image and its corresponding template. And in Figure 7.9, we have the output of the image alignment and document the OCR pipeline. Notice how we’ve successfully aligned our input image with the document template, localize each of the fields, and then OCR each of the fields.

Figure 7.8. Left: an input image. Right: a corresponding template.

Beware that our pipeline ignores any line of text inside a field that is part of the document itself. For example, the first name field provides the instructional text “(a) First name and middle initial.” However, our OCR pipeline and keyword filtering process can detect that this is

7.1. Automatically Registering and OCR’ing a Form

101

part of the document itself (i.e., not something a human entered) and then simply ignores it. Overall, we’ve been able to successfully OCR the document!

Figure 7.9. Output of the image alignment and document OCR pipeline.

Let’s try another sample image, this time with a slightly different viewing angle:

$ python ocr_form.py --image scans/scan_02.jpg --template form_w4.png [INFO] loading images... [INFO] aligning images... [INFO] OCR'ing document... step1_first_name ================ Adrian step1_last_name =============== Rosebrock step1_address ============= PO Box 17598 #17900 step1_city_state_zip ==================== Baltimore, MD 21297-1598

102

Chapter 7. OCR’ing a Document, Form, or Invoice

step5_employee_signature ======================== Adrian Rosebrock step5_date ========== 2020/06/10 employee_name_address ===================== PyimageSearch PO BOX 1234 Philadelphia, PA 19019 employee_ein ============ 12-3456789

Figure 7.10 shows our input image along with its template. Figure 7.11 contains our output, where you can see that the image is aligned with the template and the OCR successfully applied to each of the fields. As you can see, we’ve successfully aligned the input image with the template document and then OCR each of the individual fields!

Figure 7.10. Left: an input image. Right: a corresponding template.

7.2. Summary

103

Figure 7.11. Output of the image alignment and document OCR pipeline.

7.2

Summary

In this chapter, you learned how to OCR a document, form, or invoice using OpenCV and Tesseract. Our method hinges on image alignment which we covered earlier in this book, which is the process of accepting an input image and a template image, and then aligning them such that they can neatly “overlay” on top of each other. Image alignment allows us to align each of the text fields in a template with our input image, meaning that once we’ve OCR’d the document, we can associate the OCR’d text to each field (e.g., name, address, etc.). Once image alignment was applied, we used Tesseract to localize text in the input image. Tesseract’s text localization procedure gave us the (x, y )-coordinates of each text location, which we then mapped to the appropriate text field in the template. In this manner, we scanned and recognized each important text field of the input document!

Chapter 8

Sudoku Solver and OCR In this chapter, you will create an automatic sudoku puzzle solver using OpenCV, deep learning, and optical character recognition (OCR). My wife is a huge sudoku nerd. Whether it be a 45-minute flight from Philadelphia to Albany or a 6-hour transcontinental flight to California, she always has a sudoku puzzle with her every time we travel. The funny thing is, she always has a printed puzzle book with her. She hates the digital versions and refuses to play them. I’m not a big puzzle person myself, but one time, we were sitting on a flight, and I asked: “How do you know if you solved the puzzle correctly? Is there a solution sheet in the back of the book? Or do you just do it and hope it’s correct?” That was a stupid question to ask for two reasons. First, yes, there is a solution key in the back. All you need to do is flip to the end of the book, locate the puzzle number, and see the solution. And most importantly, she doesn’t solve a puzzle incorrectly. My wife doesn’t get mad easily, but let me tell you, I touched a nerve when I innocently and unknowingly insulted her sudoku puzzle-solving skills. She then lectured me for 20 minutes on how she only solves “levels 4 and 5 puzzles,” followed by a lesson on the “X-wing” and “Y-wing” techniques to solving sudoku puzzles. I have a PhD in computer science, but all of that went over my head. But for those of you who aren’t married to a sudoku grandmaster like I am, it does raise the question: “Can OpenCV and OCR be used to solve and check sudoku puzzles?” If the sudoku puzzle manufacturers didn’t have to print the answer key in the back of the book and instead provided an app for users to check their puzzles, the printers could either pocket the savings or print additional puzzles at no cost. The sudoku puzzle company makes more money, and the end-users are happy. It seems like a win/win to me. 105

106

8.1

Chapter 8. Sudoku Solver and OCR

Chapter Learning Objectives

In this chapter, we’ll: i. Discover the steps required to build a sudoku puzzle solver using OpenCV, deep learning, and OCR techniques ii. Install the py-sudoku package iii. Implement and train a small CNN named SudokuNet, which can recognize digits 0–9 iv. Learn how to pre-process a photo of a sudoku puzzle and ultimately recognize and extract all digits while also identifying the blank spaces. We’ll feed the data into py-sudoku to solve the puzzle for us.

8.2

OpenCV Sudoku Solver and OCR

In the first part of this chapter, we’ll discuss the steps required to build a sudoku puzzle solver using OpenCV, deep learning, and OCR techniques. From there, you’ll configure your development environment and ensure the proper libraries and packages are installed. Before we write any code, we’ll first review our project directory structure, ensuring you know what files will be created, modified, and utilized throughout this chapter. You can see a solved sudoku puzzle in Figure 8.1.

Figure 8.1. In this chapter, we’ll use our OpenCV and deep learning skills to extract sudoku puzzle information and send it through a puzzle solver utility. Using a computer, we can solve a sudoku puzzle in less than a second compared to the many tens of minutes you would solve the puzzle by hand.

8.2. OpenCV Sudoku Solver and OCR

107

I’ll then show you how to implement SudokuNet, a basic convolutional neural network (CNN) used to OCR the digits on the sudoku puzzle board. We’ll then train that network to recognize digits using Keras and TensorFlow. But before we can check and solve a sudoku puzzle, we first need to locate where in the image the sudoku board is — we’ll implement helper functions and utilities to help with that task. Finally, we’ll put all the pieces together and implement our full OpenCV sudoku puzzle solver.

8.2.1

How to Solve Sudoku Puzzles with OpenCV and OCR

Creating an automatic sudoku puzzle solver with OpenCV is a 6-step process: • Step #1: Provide an input image containing a sudoku puzzle to our system. • Step #2: Locate where in the input image, the puzzle is and extract the board. • Step #3: Given the board, locate each of the sudoku board’s cells (most standard sudoku puzzles are a 9 x 9 grid, so we’ll need to localize each of these cells). • Step #4: Determine if a digit exists in the cell, and if so, OCR it. • Step #5: Apply a sudoku puzzle solver/checker algorithm to validate the puzzle. • Step #6: Display the output result to the user. The majority of these steps can be accomplished using OpenCV and basic computer vision and image processing operations. The most significant exception is Step #4, where we need to apply OCR. OCR can be a bit tricky to apply, but we have several options: i. Use the Tesseract OCR engine, the de facto standard for open-source OCR ii. Utilize cloud-based OCR APIs (e.g., Microsoft Cognitive Services, Amazon Rekognition API, or the Google Cloud Vision API) iii. Train our custom OCR model All of these are perfectly valid options; however, to make a complete end-to-end chapter, I’ve decided that we’ll train our custom sudoku OCR model using deep learning. Be sure to strap yourself in — this is going to be a wild ride.

108

Chapter 8. Sudoku Solver and OCR

8.2.2

Configuring Your Development Environment to Solve Sudoku Puzzles with OpenCV and OCR

This chapter requires that you install the py-sudoku package into your environment (http://pyimg.co/3ipyb [27]). This library allows us to solve sudoku puzzles (assuming we’ve already OCR’d all the digits). To install it, simply use pip.

$ pip install py-sudoku

8.2.3

Project Structure

We’ll get started by reviewing our project directory structure:

|-| |-| | | | | | | |-|-|--

output |-- digit_classifier.h5 pyimagesearch |-- models | |-- __init__.py | |-- sudokunet.py |-- sudoku | |-- __init__.py | |-- puzzle.py |-- __init__.py solve_sudoku_puzzle.py sudoku_puzzle.jpg train_digit_classifier.py

Inside, you’ll find a pyimagesearch module containing the following: • sudokunet.py: Holds the SudokuNet CNN architecture implemented with Keras and TensorFlow. • puzzle.py: Contains two helper utilities for finding the sudoku puzzle board itself as well as digits therein. As with all CNNs, SudokuNet needs to be trained with data. Our train_digit_ classifier.py script will train a digit OCR model on the MNIST dataset [1]. Once SudokuNet is successfully trained, we’ll deploy it with our solve_sudoku_puzzle.py script to solve a sudoku puzzle. When our system is working, you can impress your friends with the app. Or better yet, fool them on the airplane as you solve puzzles faster than they possibly can in the seat right behind you! Don’t worry, I won’t tell!

8.2. OpenCV Sudoku Solver and OCR

8.2.4

109

SudokuNet: A Digit OCR Model Implemented in Keras and TensorFlow

Every sudoku puzzle starts with an N x N grid (typically 9 x 9), where some cells are blank , and other cells already contain a digit. The goal is to use the knowledge about the existing digits to infer the other digits correctly. But before we can solve sudoku puzzles with OpenCV, we first need to implement a neural network architecture that will handle OCR’ing the digits on the sudoku puzzle board — given that information. It will become trivial to solve the actual puzzle. Fittingly, we’ll name our sudoku puzzle architecture SudokuNet. Let’s code our sudokunet.py file in the pyimagesearch module now:

1 2 3 4 5 6 7 8

# import the necessary packages from tensorflow.keras.models import from tensorflow.keras.layers import from tensorflow.keras.layers import from tensorflow.keras.layers import from tensorflow.keras.layers import from tensorflow.keras.layers import from tensorflow.keras.layers import

Sequential Conv2D MaxPooling2D Activation Flatten Dense Dropout

All of SudokuNet’s imports are from tf.keras. As you can see, we’ll be using Keras’ Sequential API (http://pyimg.co/mac01 [28]) as well as the layers shown. Now that our imports are taken care of, let’s dive right into the implementation of our CNN:

10 11 12 13 14 15

class SudokuNet: @staticmethod def build(width, height, depth, classes): # initialize the model model = Sequential() inputShape = (height, width, depth)

Our SudokuNet class is defined with a single static method (no constructor) on Lines 10–12. The build method accepts the following parameters: • width: The width of an MNIST digit (28 pixels) • height: The height of an MNIST digit (28 pixels) • depth: Channels of MNIST digit images (1 grayscale channel) • classes: The number of digits 0–9 (10 digits)

110

Chapter 8. Sudoku Solver and OCR

Lines 14 and 15 initialize our model to be built with the Sequential API and establish the inputShape, which we’ll need for our first CNN layer. Now that our model is initialized, let’s go ahead and build out our CNN:

17 18 19 20 21

# first set of CONV => RELU => POOL layers model.add(Conv2D(32, (5, 5), padding="same", input_shape=inputShape)) model.add(Activation("relu")) model.add(MaxPooling2D(pool_size=(2, 2)))

22 23 24 25 26

# second set of CONV => RELU => POOL layers model.add(Conv2D(32, (3, 3), padding="same")) model.add(Activation("relu")) model.add(MaxPooling2D(pool_size=(2, 2)))

27 28 29 30 31 32

# first set of FC => RELU layers model.add(Flatten()) model.add(Dense(64)) model.add(Activation("relu")) model.add(Dropout(0.5))

33 34 35 36 37

# second set of FC => RELU layers model.add(Dense(64)) model.add(Activation("relu")) model.add(Dropout(0.5))

38 39 40 41

# softmax classifier model.add(Dense(classes)) model.add(Activation("softmax"))

42 43 44

# return the constructed network architecture return model

The body of our network is composed of: • CONV => RELU => POOL layer set 1 • CONV => RELU => POOL layer set 2 • FC => RELU fully connected layer set with 50% dropout The head of the network consists of a softmax classifier with the number of outputs equal to the number of our classes (in our case: 10 digits). Great job implementing SudokuNet! If the CNN layers and working with the Sequential API were unfamiliar to you, I recommend checking out either of the following resources:

8.2. OpenCV Sudoku Solver and OCR

111

• Keras Tutorial: How to Get Started with Keras, Deep Learning, and Python (http://pyimg.co/8gzi2 [29]) • Deep Learning for Computer Vision with Python (Starter Bundle) (http://pyimg.co/dl4cv [6]) If you were building a CNN to classify 26 uppercase English letters plus the 10 digits (a total of 36 characters), you most certainly would need a deeper CNN.

8.2.5

Implementing with Keras and TensorFlow

With the SudokuNet model architecture implemented, we can create a Python script to train the model to recognize digits. Perhaps unsurprisingly, we’ll be using the MNIST dataset to train our digit recognizer, as it fits quite nicely in this use case. Open the train_digit_classifier.py file to get started:

1 2 3 4 5 6 7

# import the necessary packages from pyimagesearch.models import SudokuNet from tensorflow.keras.optimizers import Adam from tensorflow.keras.datasets import mnist from sklearn.preprocessing import LabelBinarizer from sklearn.metrics import classification_report import argparse

8 9 10 11 12 13

# construct the argument parser and parse the arguments ap = argparse.ArgumentParser() ap.add_argument("-m", "--model", required=True, help="path to output model after training") args = vars(ap.parse_args())

We begin our training script with a small handful of imports. Most notably, we’re importing SudokuNet and the mnist dataset. The MNIST dataset of handwritten digits (http://pyimg.co/zntl9 [1]) is built right into Keras/TensorFlow’s datasets module and will be cached to your machine on demand. Figure 8.2 shows a sample of the type of digits used.

Figure 8.2. A sample of digits from Yann LeCun’s MNIST dataset (http://pyimg.co/zntl9 [1]) of handwritten digits will be used to train a deep learning model to OCR/HWR handwritten digits with Keras/TensorFlow.

112

Chapter 8. Sudoku Solver and OCR

Our script requires a single command line argument. When you execute the training script from the command line, simply pass a filename for your output model file (I recommend using the .h5 file extension). Next, we’ll (1) set hyperparameters and (2) load and pre-process MNIST:

15 16 17 18 19

# initialize the initial learning rate, number of epochs to train # for, and batch size INIT_LR = 1e-3 EPOCHS = 10 BS = 128

20 21 22 23

# grab the MNIST dataset print("[INFO] accessing MNIST...") ((trainData, trainLabels), (testData, testLabels)) = mnist.load_data()

24 25 26 27

# add a channel (i.e., grayscale) dimension to the digits trainData = trainData.reshape((trainData.shape[0], 28, 28, 1)) testData = testData.reshape((testData.shape[0], 28, 28, 1))

28 29 30 31

# scale data to the range of [0, 1] trainData = trainData.astype("float32") / 255.0 testData = testData.astype("float32") / 255.0

32 33 34 35 36

# convert the labels from integers to vectors le = LabelBinarizer() trainLabels = le.fit_transform(trainLabels) testLabels = le.transform(testLabels)

You can configure training hyperparameters on Lines 17–19. Through experimentation, I’ve determined appropriate settings for the learning rate, number of training epochs, and batch size. Advanced users might wish to check out my Keras Learning Rate Finder tutorial (http://pyimg.co/9so5k [30]). To work with the MNIST digit dataset, we perform the following steps: • Load the dataset into memory (Line 23). This dataset is already split into training and testing data • Add a channel dimension to the digits to indicate that they are grayscale (Lines 26 and 27) • Scale data to the range of [0, 1] (Lines 30 and 31) • One-hot encode labels (Lines 34–36) The process of one-hot encoding means that an integer such as 3 would be represented as follows:

8.2. OpenCV Sudoku Solver and OCR

113

[0, 0, 0, 1, 0, 0, 0, 0, 0, 0] Or the integer 9 would be encoded like so: [0, 0, 0, 0, 0, 0, 0, 0, 0, 1] From here, we’ll go ahead and initialize and train SudokuNet on our digits data:

38 39 40 41 42 43

# initialize the optimizer and model print("[INFO] compiling model...") opt = Adam(lr=INIT_LR) model = SudokuNet.build(width=28, height=28, depth=1, classes=10) model.compile(loss="categorical_crossentropy", optimizer=opt, metrics=["accuracy"])

44 45 46 47 48 49 50 51 52

# train the network print("[INFO] training network...") H = model.fit( trainData, trainLabels, validation_data=(testData, testLabels), batch_size=BS, epochs=EPOCHS, verbose=1)

Lines 40–43 build and compile our model with the Adam optimizer and categorical cross-entropy loss. Remark 8.1. We’re focused on 10 digits. However, if you were only focused on recognizing binary numbers 0 and 1, then you would use loss="binary_crossentropy." Keep this in mind when working with two-class datasets or data subsets. Training is launched via a call to the fit method (Lines 47–52). Once training is complete, we’ll evaluate and export our model:

54 55 56 57 58 59 60

# evaluate the network print("[INFO] evaluating network...") predictions = model.predict(testData) print(classification_report( testLabels.argmax(axis=1), predictions.argmax(axis=1), target_names=[str(x) for x in le.classes_]))

61 62 63 64

# serialize the model to disk print("[INFO] serializing digit model...") model.save(args["model"], save_format="h5")

Using our newly trained model, we make predictions on our testData (Line 56). From there, we print a classification report to our terminal (Lines 57–60). Finally, we save our

114

Chapter 8. Sudoku Solver and OCR

model to disk (Line 64). Note that for TensorFlow 2.0+, we recommend explicitly setting the save_format="h5" (HDF5 format).

8.2.6

Training Our Sudoku Digit Recognizer with Keras and TensorFlow

We’re now ready to train our SudokuNet model to recognize digits. Go ahead and open your terminal, and execute the following command:

$ python train_digit_classifier.py --model output/digit_classifier.h5 [INFO] accessing MNIST... [INFO] compiling model... [INFO] training network... [INFO] training network... Epoch 1/10 469/469 [==============================] - 22s 47ms/step - loss: 0.7311 - accuracy: 0.7530 - val_loss: 0.0989 - val_accuracy: 0.9706 Epoch 2/10 469/469 [==============================] - 22s 47ms/step - loss: 0.2742 - accuracy: 0.9168 - val_loss: 0.0595 - val_accuracy: 0.9815 Epoch 3/10 469/469 [==============================] - 21s 44ms/step - loss: 0.2083 - accuracy: 0.9372 - val_loss: 0.0452 - val_accuracy: 0.9854 ... Epoch 8/10 469/469 [==============================] - 22s 48ms/step - loss: 0.1178 - accuracy: 0.9668 - val_loss: 0.0312 - val_accuracy: 0.9893 Epoch 9/10 469/469 [==============================] - 22s 47ms/step - loss: 0.1100 - accuracy: 0.9675 - val_loss: 0.0347 - val_accuracy: 0.9889 Epoch 10/10 469/469 [==============================] - 22s 47ms/step - loss: 0.1005 - accuracy: 0.9700 - val_loss: 0.0392 - val_accuracy: 0.9889 [INFO] evaluating network... precision recall f1-score support 0 1 2 3 4 5 6 7 8 9

0.98 0.99 0.99 0.99 0.99 0.98 0.99 0.98 1.00 0.99

1.00 1.00 0.98 0.99 0.99 0.99 0.98 1.00 0.98 0.98

0.99 0.99 0.99 0.99 0.99 0.98 0.99 0.99 0.99 0.99

980 1135 1032 1010 982 892 958 1028 974 1009

accuracy macro avg weighted avg

0.99 0.99

0.99 0.99

0.99 0.99 0.99

10000 10000 10000

[INFO] serializing digit model...

8.2. OpenCV Sudoku Solver and OCR

115

Here, you can see that our SudokuNet model has obtained 99% accuracy on our testing set. You can verify that the model is serialized to disk by inspecting your output directory:

$ ls -lh output total 2824 -rw-r--r--@ 1 adrian

staff

1.4M Jun

7 07:38 digit_classifier.h5

This digit_classifier.h5 file contains our Keras/TensorFlow model, which we’ll use to recognize the digits on a sudoku board later in this chapter. This model is quite small, making it a good candidate to deploy to low-power devices.

8.2.7

Finding the Sudoku Puzzle Board in an Image with OpenCV

At this point, we have a model that can recognize digits in an image; however, that digit recognizer doesn’t do us much good if it can’t locate the sudoku puzzle board in an image. For example, let’s say we presented the sudoku puzzle board in Figure 8.3 to our system:

Figure 8.3. An example of a sudoku puzzle that has been snapped with a camera (source: Aakash Jhawar, http://pyimg.co/tq5yz [31]). In this section, we’ll develop two computer vision helper utilities with OpenCV and scikit-image. The first helps us find and perform a perspective transform on the puzzle itself. And the second helper allows us to find the digit contours within a cell or determine that a cell is empty.

116

Chapter 8. Sudoku Solver and OCR

How are we going to locate the actual sudoku puzzle board in the image? And once we’ve found the puzzle, how do we identify each of the individual cells? To make our lives a bit easier, we’ll be implementing two helper utilities: • find_puzzle: Locates and extracts the sudoku puzzle board from the input image • extract_digit: Examines each cell of the sudoku puzzle board and extracts the digit from the cell (provided there is a digit) This section will show you how to implement the find_puzzle method, while the next section will show the extract_digit implementation. Open the puzzle.py file in the pyimagesearch module, and we’ll get started:

1 2 3 4 5 6

# import the necessary packages from imutils.perspective import four_point_transform from skimage.segmentation import clear_border import numpy as np import imutils import cv2

7 8 9 10 11

def find_puzzle(image, debug=False): # convert the image to grayscale and blur it slightly gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) blurred = cv2.GaussianBlur(gray, (7, 7), 3)

Our two helper functions require my imutils (http://pyimg.co/twcph [32]) implementation of a four_point_transform for deskewing an image to obtain a bird’s-eye view. Additionally, we’ll use the clear_border routine in our extract_digit function to clean up a sudoku cell’s edges. Most operations will be driven with OpenCV plus a little bit of help from NumPy and imutils. Our find_puzzle function comes first and accepts two parameters: • image: The photo of a sudoku puzzle. • debug: An optional Boolean indicating whether to show intermediate steps so you can better visualize what is happening under the hood of our computer vision pipeline. If you encounter any issues, I recommend setting debug=True and using your computer vision knowledge to iron out bugs. Our first step is to convert our image to grayscale and apply a Gaussian blur operation with a 7 x 7 kernel (http://pyimg.co/8om5g [13]) (Lines 10 and 11). And next, we’ll apply adaptive thresholding:

8.2. OpenCV Sudoku Solver and OCR

13 14 15 16

117

# apply adaptive thresholding and then invert the threshold map thresh = cv2.adaptiveThreshold(blurred, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 11, 2) thresh = cv2.bitwise_not(thresh)

17 18 19 20 21 22

# check to see if we are visualizing each step of the image # processing pipeline (in this case, thresholding) if debug: cv2.imshow("Puzzle Thresh", thresh) cv2.waitKey(0)

Binary adaptive thresholding operations allow us to peg grayscale pixels toward each end of the [0, 255] pixel range. In this case, we’ve both applied a binary threshold and then inverted the result, as shown in Figure 8.4.

Figure 8.4. OpenCV has been used to perform a binary inverse threshold operation on the input image.

118

Chapter 8. Sudoku Solver and OCR

Just remember, you’ll only see something similar to the inverted thresholded image if you have your debug option set to True. Now that our image is thresholded, let’s find and sort contours:

24 25 26 27 28 29

# find contours in the thresholded image and sort them by size in # descending order cnts = cv2.findContours(thresh.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) cnts = imutils.grab_contours(cnts) cnts = sorted(cnts, key=cv2.contourArea, reverse=True)

30 31 32

# initialize a contour that corresponds to the puzzle outline puzzleCnt = None

33 34 35 36 37 38

# loop over the contours for c in cnts: # approximate the contour peri = cv2.arcLength(c, True) approx = cv2.approxPolyDP(c, 0.02 * peri, True)

39 40 41 42 43 44

# if our approximated contour has four points, then we can # assume we have found the outline of the puzzle if len(approx) == 4: puzzleCnt = approx break

Here, we find contours and sort by area (http://pyimg.co/sbm9p [33]) in reverse order (Lines 26–29). One of our contours will correspond to the sudoku grid outline — puzzleCnt is initialized to None on Line 32. Let’s determine which of our cnts is our puzzleCnt using the following approach: • Loop over all contours beginning on Line 35. • Determine the perimeter of the contour (Line 37). • Approximate the contour (Line 38) (http://pyimg.co/c1tel [34]). • Check if the contour has four vertices, and if so, mark it as the puzzleCnt, and break out of the loop (Lines 42–44). It is possible that the outline of the sudoku grid isn’t found. In that case, let’s raise an Exception:

46 47 48

# if the puzzle contour is empty then our script could not find # the outline of the sudoku puzzle so raise an error if puzzleCnt is None:

8.2. OpenCV Sudoku Solver and OCR

49 50

119

raise Exception(("Could not find sudoku puzzle outline. " "Try debugging your thresholding and contour steps."))

51 52 53 54 55 56 57 58 59 60

# check to see if we are visualizing the outline of the detected # sudoku puzzle if debug: # draw the contour of the puzzle on the image and then display # it to our screen for visualization/debugging purposes output = image.copy() cv2.drawContours(output, [puzzleCnt], -1, (0, 255, 0), 2) cv2.imshow("Puzzle Outline", output) cv2.waitKey(0)

If the sudoku puzzle is not found, we raise an Exception to tell the user/developer what happened (Lines 48–50). And again, if we are debugging, we’ll visualize what is going on under the hood by drawing the puzzle contour outline on the image, as shown in Figure 8.5:

Figure 8.5. The sudoku puzzle board’s border is found by determining the largest contour with four points using OpenCV’s contour operations.

120

Chapter 8. Sudoku Solver and OCR

With the contour of the puzzle in hand (fingers crossed), we’re then able to deskew the image to obtain a top-down bird’s-eye view of the puzzle:

62 63 64 65 66

# apply a four-point perspective transform to both the original # image and grayscale image to obtain a top-down bird's-eye view # of the puzzle puzzle = four_point_transform(image, puzzleCnt.reshape(4, 2)) warped = four_point_transform(gray, puzzleCnt.reshape(4, 2))

67 68 69 70 71 72

# check to see if we are visualizing the perspective transform if debug: # show the output warped image (again, for debugging purposes) cv2.imshow("Puzzle Transform", puzzle) cv2.waitKey(0)

73 74 75

# return a 2-tuple of puzzle in both RGB and grayscale return (puzzle, warped)

Applying a four-point perspective transform (http://pyimg.co/7t1dj [35]) effectively deskews our sudoku puzzle grid, making it much easier for us to determine rows, columns, and cells as we move forward (Lines 65 and 66). This operation is performed on the original RGB image and gray image. The final result of our find_puzzle function is shown in Figure 8.6:

Figure 8.6. After applying a four-point perspective transform using OpenCV, we’re left with a top-down bird’s-eye view of the sudoku puzzle. At this point, we can begin working on finding characters and performing deep learning based OCR with Keras/TensorFlow.

8.2. OpenCV Sudoku Solver and OCR

121

Our find_puzzle return signature consists of a 2-tuple of the original RGB image and grayscale image after all operations, including the final four-point perspective transform. Great job so far! Let’s continue our forward march toward solving sudoku puzzles. Now we need a means to extract digits from sudoku puzzle cells, and we’ll do just that in the next section.

8.2.8

Extracting Digits from a Sudoku Puzzle with OpenCV

This section will show you how to examine each of the cells in a sudoku board, detect if there is a digit in the cell, and, if so, extract the digit. Let’s open the puzzle.py file once again and get to work:

77 78 79 80 81 82

def extract_digit(cell, debug=False): # apply automatic thresholding to the cell and then clear any # connected borders that touch the border of the cell thresh = cv2.threshold(cell, 0, 255, cv2.THRESH_BINARY_INV | cv2.THRESH_OTSU)[1] thresh = clear_border(thresh)

83 84 85 86 87

# check to see if we are visualizing the cell thresholding step if debug: cv2.imshow("Cell Thresh", thresh) cv2.waitKey(0)

Here, you can see we’ve defined our extract_digit function to accept two parameters: • cell: An ROI representing an individual cell of the sudoku puzzle (it may or may not contain a digit) • debug: A Boolean indicating whether intermediate step visualizations should be shown on your screen Our first step, on Lines 80–82, is to threshold and clear any foreground pixels that are touching the borders of the cell (e.g., any line markings from the cell dividers). The result of this operation can be shown via Lines 85–87. Let’s see if we can find the digit contour:

89 90 91 92

# find contours in the thresholded cell cnts = cv2.findContours(thresh.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) cnts = imutils.grab_contours(cnts)

93 94 95

# if no contours were found than this is an empty cell if len(cnts) == 0:

122

96

Chapter 8. Sudoku Solver and OCR

return None

97 98 99 100 101 102

# otherwise, find the largest contour in the cell and create a # mask for the contour c = max(cnts, key=cv2.contourArea) mask = np.zeros(thresh.shape, dtype="uint8") cv2.drawContours(mask, [c], -1, 255, -1)

Lines 90–92 find the contours in the thresholded cell. If no contours are found, we return None (Lines 95 and 96). Given our contours, cnts, we then find the largest contour by pixel area and construct an associated mask (Lines 100–102). From here, we’ll continue working on trying to isolate the digit in the cell:

104 105 106 107

# compute the percentage of masked pixels relative to the total # area of the image (h, w) = thresh.shape percentFilled = cv2.countNonZero(mask) / float(w * h)

108 109 110 111 112

# if less than 3% of the mask is filled then we are looking at # noise and can safely ignore the contour if percentFilled < 0.03: return None

113 114 115

# apply the mask to the thresholded cell digit = cv2.bitwise_and(thresh, thresh, mask=mask)

116 117 118 119 120

# check to see if we should visualize the masking step if debug: cv2.imshow("Digit", digit) cv2.waitKey(0)

121 122 123

# return the digit to the calling function return digit

Dividing the pixel area of our mask by the area of the cell itself (Lines 106 and 107) gives us the percentFilled value (i.e., how much our cell is “filled up” with white pixels). Given this percentage, we ensure the contour is not simply “noise” (i.e., a very small contour). Assuming we don’t have a noisy cell, Line 115 applies the mask to the thresholded cell. This mask is optionally shown on screen (Lines 118–120) and is finally returned to the caller. Three sample results are shown in Figure 8.7:

8.2. OpenCV Sudoku Solver and OCR

123

Figure 8.7. Top-left: a warped cell 8 on the left and pre-processed 8 on the right. Top-right: a warped cell 1 on the left and pre-processed 1 on the right. Bottom: a warped cell 9 on the left and pre-processed 9 on the right.

Great job implementing the digit extraction pipeline!

8.2.9

Implementing Our OpenCV Sudoku Puzzle Solver

At this point, we’re armed with the following components: • Our custom SudokuNet model trained on the MNIST dataset of digits and residing on disk ready for use • A means to extract the sudoku puzzle board and apply a perspective transform • A pipeline to extract digits within individual cells of the sudoku puzzle or ignore ones that we consider to be noise • The py-sudoku puzzle solver (http://pyimg.co/3ipyb [27]) installed in our Python virtual environment, which saves us from having to engineer an algorithm from hand and lets us focus solely on the computer vision challenge We are now ready to put each of the pieces together to build a working OpenCV sudoku solver! Open the solve_sudoku_puzzle.py file, and let’s complete our sudoku solver project:

1 2 3 4 5

# import the necessary packages from pyimagesearch.sudoku import extract_digit from pyimagesearch.sudoku import find_puzzle from tensorflow.keras.preprocessing.image import img_to_array from tensorflow.keras.models import load_model

124

6 7 8 9 10

Chapter 8. Sudoku Solver and OCR

from sudoku import Sudoku import numpy as np import argparse import imutils import cv2

11 12 13 14 15 16 17 18 19 20

# construct the argument parser and parse the arguments ap = argparse.ArgumentParser() ap.add_argument("-m", "--model", required=True, help="path to trained digit classifier") ap.add_argument("-i", "--image", required=True, help="path to input sudoku puzzle image") ap.add_argument("-d", "--debug", type=int, default=-1, help="whether or not we are visualizing each step of the pipeline") args = vars(ap.parse_args())

As with nearly all Python scripts, we have a selection of imports to get the party started. These include our custom computer vision helper functions: extract_digit and find_puzzle. We’ll use the Keras/TensorFlow load_model method to grab our trained SudokuNet model from disk and load it into memory. The sudoku import is made possible by py-sudoku (http://pyimg.co/3ipyb [27]), which we’ve previously installed. Let’s define three command line arguments (http://pyimg.co/vsapz [36]): • --model: The path to our trained digit classifier generated while following the instructions in “Training Our Sudoku Digit Recognizer with Keras and TensorFlow” (Section 8.2.6) • --image: Your path to a sudoku puzzle photo residing on disk. • --debug: A flag indicating whether to show intermediate pipeline step debugging visualizations As we’re now equipped with imports and our args dictionary, let’s load both our (1) digit classifier model and (2) input --image from disk:

22 23 24

# load the digit classifier from disk print("[INFO] loading digit classifier...") model = load_model(args["model"])

25 26 27 28 29

# load the input image from disk and resize it print("[INFO] processing image...") image = cv2.imread(args["image"]) image = imutils.resize(image, width=600)

8.2. OpenCV Sudoku Solver and OCR

125

From there, we’ll find our puzzle and prepare to isolate the cells therein:

31 32

# find the puzzle in the image and then (puzzleImage, warped) = find_puzzle(image, debug=args["debug"] > 0)

33 34 35

# initialize our 9x9 sudoku board board = np.zeros((9, 9), dtype="int")

36 37 38 39 40 41

# a sudoku puzzle is a 9x9 grid (81 individual cells), so we can # infer the location of each cell by dividing the warped image # into a 9x9 grid stepX = warped.shape[1] // 9 stepY = warped.shape[0] // 9

42 43 44 45

# initialize a list to store the (x, y)-coordinates of each cell # location cellLocs = []

Here, we: • Find the sudoku puzzle in the input --image via our find_puzzle helper (Line 32) • Initialize our sudoku board — a 9 x 9 array (Line 35) • Infer the step size for each of the cells by simple division (Lines 40 and 41) • Initialize a list to hold the (x, y )-coordinates of cell locations (Line 45) And now, let’s begin a nested loop over rows and columns of the sudoku board:

47 48 49 50

# loop over the grid locations for y in range(0, 9): # initialize the current list of cell locations row = []

51 52 53 54 55 56 57 58

for x in range(0, 9): # compute the starting and ending (x, y)-coordinates of the # current cell startX = x * stepX startY = y * stepY endX = (x + 1) * stepX endY = (y + 1) * stepY

59 60 61

# add the (x, y)-coordinates to our cell locations list row.append((startX, startY, endX, endY))

Accounting for every cell in the sudoku puzzle, we loop over rows (Line 48) and columns (Line 52) in a nested fashion.

126

Chapter 8. Sudoku Solver and OCR

Inside, we use our step values to determine the start/end (x, y )-coordinates of the current cell (Lines 55–58). Line 61 appends the coordinates as a tuple to this particular row. Each row will have nine entries (9 x 4-tuples). Now we’re ready to crop out the cell and recognize the digit therein (if one is present):

63 64 65 66

# crop the cell from the warped transform image and then # extract the digit from the cell cell = warped[startY:endY, startX:endX] digit = extract_digit(cell, debug=args["debug"] > 0)

67 68 69 70 71 72 73 74 75

# verify that the digit is not empty if digit is not None: # resize the cell to 28x28 pixels and then prepare the # cell for classification roi = cv2.resize(digit, (28, 28)) roi = roi.astype("float") / 255.0 roi = img_to_array(roi) roi = np.expand_dims(roi, axis=0)

76 77 78 79 80

# classify the digit and update the sudoku board with the # prediction pred = model.predict(roi).argmax(axis=1)[0] board[y, x] = pred

81 82 83

# add the row to our cell locations cellLocs.append(row)

Step by step, we proceed to: • Crop the cell from the transformed image and then extract the digit (Lines 65 and 66) • If the digit is not None, then we know there is an actual digit in the cell (rather than an empty space), at which point we: • Pre-process the digit roi in the same manner that we did for training (Lines 72–75) • Classify the digit roi with SudokuNet (Line 79) • Update the sudoku puzzle board array with the predicted value of the cell (Line 80) • Add the row’s (x, y )-coordinates to the cellLocs list (Line 83) — the last line of our nested loop over rows and columns And now, let’s solve the sudoku puzzle with py-sudoku:

8.2. OpenCV Sudoku Solver and OCR

85 86 87 88

127

# construct a sudoku puzzle from the board print("[INFO] OCR'd sudoku board:") puzzle = Sudoku(3, 3, board=board.tolist()) puzzle.show()

89 90 91 92 93

# solve the sudoku puzzle print("[INFO] solving sudoku puzzle...") solution = puzzle.solve() solution.show_full()

As you can see, first, we display the sudoku puzzle board as it was interpreted via OCR (Lines 87 and 88). Then, we make a call to puzzle.solve to solve the sudoku puzzle (Line 92). And again, this is where the py-sudoku package does the mathematical algorithm to solve our puzzle. We go ahead and print out the solved puzzle in our terminal (Line 93) And of course, what fun would this project be if we didn’t visualize the solution on the puzzle image itself? Let’s do that now:

95 96 97 98 99 100

# loop over the cell locations and board for (cellRow, boardRow) in zip(cellLocs, solution.board): # loop over individual cell in the row for (box, digit) in zip(cellRow, boardRow): # unpack the cell coordinates startX, startY, endX, endY = box

101 102 103 104 105 106 107

# compute the coordinates of where the digit will be drawn # on the output puzzle image textX = int((endX - startX) * 0.33) textY = int((endY - startY) * -0.2) textX += startX textY += endY

108 109 110 111

# draw the result digit on the sudoku puzzle image cv2.putText(puzzleImage, str(digit), (textX, textY), cv2.FONT_HERSHEY_SIMPLEX, 0.9, (0, 255, 255), 2)

112 113 114 115

# show the output image cv2.imshow("Sudoku Result", puzzleImage) cv2.waitKey(0)

To annotate our image with the solution numbers, we simply: • Loop over cell locations and the board (Lines 96–98) • Unpack cell coordinates (Line 100)

128

Chapter 8. Sudoku Solver and OCR

• Compute the coordinates of where text annotation will be drawn (Lines 104–107) • Draw each output digit on our puzzle board photo (Lines 110 and 111) • Display our solved sudoku puzzle image (Line 114) until any key is pressed (Line 115) Nice job! Let’s kick our project into gear in the next section. You’ll be very impressed with your hard work!

8.2.10

OpenCV Sudoku Puzzle Solver OCR Results

We are now ready to put our OpenCV sudoku puzzle solver to the test! From there, open a terminal, and execute the following command:

$ python solve_sudoku_puzzle.py --model output/digit_classifier.h5 \ --image sudoku_puzzle.jpg [INFO] loading digit classifier... [INFO] processing image... [INFO] OCR'd sudoku board: +-------+-------+-------+ | 8 | 1 | 9 | | 5 | 8 7 | 1 | | 4 | 9 | 7 | +-------+-------+-------+ | 6 | 7 1 | 2 | | 5 8 | 6 | 1 7 | | 1 | 5 2 | 9 | +-------+-------+-------+ | 7 | 4 | 6 | | 8 | 3 9 | 4 | | 3 | 5 | 8 | +-------+-------+-------+ [INFO] solving sudoku puzzle... --------------------------9x9 (3x3) SUDOKU PUZZLE Difficulty: SOLVED --------------------------+-------+-------+-------+ | 8 7 2 | 4 1 3 | 5 6 9 | | 9 5 6 | 8 2 7 | 3 1 4 | | 1 3 4 | 6 9 5 | 7 8 2 | +-------+-------+-------+ | 4 6 9 | 7 3 1 | 8 2 5 | | 5 2 8 | 9 6 4 | 1 3 7 | | 7 1 3 | 5 8 2 | 4 9 6 |

8.2. OpenCV Sudoku Solver and OCR

129

+-------+-------+-------+ | 2 9 7 | 1 4 8 | 6 5 3 | | 6 8 5 | 3 7 9 | 2 4 1 | | 3 4 1 | 2 5 6 | 9 7 8 | +-------+-------+-------+

You can see the solved sudoku in Figure 8.8. As you can see, we have successfully solved the sudoku puzzle using OpenCV, OCR, and deep learning! And now, if you’re the betting type, you could challenge a friend or significant other to see who can solve 10 sudoku puzzles the fastest on your next transcontinental airplane ride! Just don’t get caught snapping a few photos!

Figure 8.8. You’ll have to resist the temptation to say “Bingo!” (wrong game) when you achieve this solved sudoku puzzle result using OpenCV, OCR, and Keras/TensorFlow.

130

Chapter 8. Sudoku Solver and OCR

8.2.11

Credits

This chapter was inspired by Aakash Jhawar and by Part 1 (http://pyimg.co/tq5yz [31]) and Part 2 (http://pyimg.co/hwvq0 [37]) of his sudoku puzzle solver. Additionally, you’ll note that I used the same sample sudoku puzzle board that Aakash did, not out of laziness, but to demonstrate how the same puzzle can be solved with different computer vision and image processing techniques. I enjoyed Aakash Jhawar’s articles and recommend readers like you to check them out as well (especially if you want to implement a sudoku solver from scratch rather than using the py-sudoku library).

8.3

Summary

In this chapter, you learned how to implement a sudoku puzzle solver using OpenCV, deep learning, and OCR. To find and locate the sudoku puzzle board in the image, we utilized OpenCV and basic image processing techniques, including blurring, thresholding, and contour processing, just to name a few. To OCR the digits on the sudoku board, we trained a custom digit recognition model using Keras and TensorFlow. Combining the sudoku board locator with our digit OCR model allowed us to make quick work of solving the actual sudoku puzzle.

Chapter 9

Automatically OCR’ing Receipts and Scans In this chapter, you will learn how to use Tesseract and OpenCV to build an automatic receipt scanner. We’ll use OpenCV to build the actual image processing component of the system, including: • Detecting the receipt in the image • Finding the four corners of the receipt • And finally applying a perspective transform to obtain a top-down, bird’s-eye view of the receipt From there, we will use Tesseract to OCR the receipt itself and parse out each item, line-by-line, including both the item description and price. If you’re a business owner (like me) and need to report your expenses to your accountant, or if your job requires that you meticulously track your expenses for reimbursement, then you know how frustrating, tedious, and annoying it is to track your receipts. It’s hard to believe in this day of age that purchases are still tracked via a tiny, fragile piece of paper! Perhaps in the future, it will become less tedious to track and report our expenses. But until then, receipt scanners can save us a bunch of time and avoid the frustration of manually cataloging purchases. This chapter’s receipt scanner project serves as a starting point for building a full-fledged receipt scanner application. Using this chapter as a starting point — and then extend it by adding a GUI, integrating it with a mobile app, etc. Let’s get started! 131

132

9.1

Chapter 9. Automatically OCR’ing Receipts and Scans

Chapter Learning Objectives

In this chapter, you will learn: • How to use OpenCV to detect, extract, and transform a receipt in an input image • How to use Tesseract to OCR the receipt, line-by-line • See a real-world application of how choosing the correct Tesseract PSM can lead to better results

9.2

OCR’ing Receipts with OpenCV and Tesseract

In the first part of this chapter, we will review our directory structure for our receipt scanner project. We’ll then review our receipt scanner implementation, line-by-line. Most importantly, I’ll show you which Tesseract PSM to use when building a receipt scanner, such that you can easily detect and extract each item and price from the receipt. Finally, we’ll wrap up this chapter with a discussion of our results.

9.2.1

Project Structure

Let’s start by reviewing our project directory structure:

|-- scan_receipt.py |-- whole_foods.png

We have only one script to review today, scan_receipt.py, which will contain our receipt scanner implementation. The whole_foods.png image is a photo I took of my receipt when I went to Whole Foods, a grocery store chain in the United States. We’ll use our scan_receipt.py script to detect the receipt in the input image and then extract each item and price from the receipt.

9.2.2

Implementing Our Receipt Scanner

Before we dive into the implementation of our receipt scanner, let’s first review the basic algorithm that we’ll be implementing. When presented with an input image containing a receipt, we will:

9.2. OCR’ing Receipts with OpenCV and Tesseract

133

i. Apply edge detection to reveal the outline of the receipt against the background (this assumes that we’ll have sufficient contrast between the background and foreground, otherwise we won’t be able to detect the receipt) ii. Detect contours in the edge map iii. Loop over all contours and find the largest contour with four vertices (since a receipt is rectangular and will have four corners) iv. Apply a perspective transform, yielding a top-down, bird’s-eye view of the receipt (required to improve OCR accuracy) v. Apply the Tesseract OCR engine with --psm 4 to the top-down transform of the receipt, allowing us to OCR the receipt line-by-line vi. Use regular expressions to parse out the item name and price vii. Finally, display the results to our terminal That sounds like a lot of steps, but as you’ll see, we can accomplish all of them in less than 120 lines of code (including comments). With that said, let’s dive into the implementation. Open the scan_receipt.py file in your project directory structure, and let’s get to work:

1 2 3 4 5 6 7

# import the necessary packages from imutils.perspective import four_point_transform import pytesseract import argparse import imutils import cv2 import re

We start by importing our required Python packages on Lines 2–7. These imports most notably include: • four_point_transform: Applies a perspective transform to obtain a top-down, bird’s-eye view of an input ROI. We used this function in our previous chapter when obtaining a top-down view of a Sudoku board (such that we can automatically solve the puzzles) — we’ll be doing the same here today, only with a receipt instead of a Sudoku puzzle. • pytesseract: Provides an interface to the Tesseract OCR engine. • cv2: Our OpenCV bindings

134

Chapter 9. Automatically OCR’ing Receipts and Scans

• re: Python’s regular expression package (http://pyimg.co/bsmdc [38]) will allow us to easily parse out the item names and associated prices of each line of the receipt. Next up, we have our command line arguments:

9 10 11 12 13 14 15

# construct the argument parser and parse the arguments ap = argparse.ArgumentParser() ap.add_argument("-i", "--image", required=True, help="path to input receipt image") ap.add_argument("-d", "--debug", type=int, default=-1, help="whether or not we are visualizing each stpe of the pipeline") args = vars(ap.parse_args())

Our script requires one command line argument, followed by an optional one: • --image: The path to our input image contains the receipt we want to OCR (in this case, whole_foods.png). You can supply your receipt image here as well. • --debug: An integer value used to indicate whether or not we want to display debugging images through our pipeline, including the output of edge detection, receipt detection, etc. In particular, if you cannot find a receipt in an input image, it’s likely due to either the edge detection process failing to detect the edges of the receipt: meaning that you need to fine-tune your Canny edge detection parameters or use a different method (e.g., thresholding, the Hough line transform, etc.). Another possibility is that the contour approximation step fails to find the four corners of the receipt. If and when those situations happen, supplying a positive value for the --debug command line argument will show you the output of the steps, allowing you to debug the problem, tweak the parameters/algorithms, and then continue. Next, let’s load our --input image from disk and examine its spatial dimensions:

17 18 19 20 21 22

# load the input image from disk, resize it, and compute the ratio # of the *new* width to the *old* width orig = cv2.imread(args["image"]) image = orig.copy() image = imutils.resize(image, width=500) ratio = orig.shape[1] / float(image.shape[1])

Here we load our original (orig) image from disk and then make a clone. We need to clone the input image such that we have the original image to apply the perspective transform to,

9.2. OCR’ing Receipts with OpenCV and Tesseract

135

but we can apply our actual image processing operations (i.e., edge detection, contour detection, etc.) to the image. We resize our image to have a width of 500 pixels (thereby acting as a form of noise reduction) and then compute the ratio of the new width to the old width. This ratio value will be used when we need to apply a perspective transform to the orig image. Let’s start applying our image processing pipeline to the image now:

24 25 26 27 28

# convert the image to grayscale, blur it slightly, and then apply # edge detection gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) blurred = cv2.GaussianBlur(gray, (5, 5,), 0) edged = cv2.Canny(blurred, 75, 200)

29 30 31 32 33 34 35

# check to see if we should show the output of our edge detection # procedure if args["debug"] > 0: cv2.imshow("Input", image) cv2.imshow("Edged", edged) cv2.waitKey(0)

Here we perform edge detection by converting the image to grayscale, blurring it using a 5 x 5 Gaussian kernel (to reduce noise), and then applying edge detection using the Canny edge detector. If we have our --debug command line argument set, we will display both the input image and the output edge map on our screen. Figure 9.1 shows our input image (left), followed by our output edge map (right). Notice how our edge map clearly shows the outline of the receipt in the input image. Given our edge map, let’s detect contours in the edged image and process them:

37 38 39 40 41 42

# find contours in the edge map and sort them by size in descending # order cnts = cv2.findContours(edged.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) cnts = imutils.grab_contours(cnts) cnts = sorted(cnts, key=cv2.contourArea, reverse=True)

Take note of Line 42 where we are sorting our contours according to their area (size) from largest to smallest. This sorting step is important as we’re making the assumption that the largest contour in the input image with four corners is our receipt.

136

Chapter 9. Automatically OCR’ing Receipts and Scans

Figure 9.1. Left: Our input image containing the receipt we are going to OCR. Right: Applying edge detection reveals the receipt in the input image.

The sorting step takes care of our first requirement. But how do we know if we’ve found a contour that has four vertices? The following code block answers that question:

44 45

# initialize a contour that corresponds to the receipt outline receiptCnt = None

46 47 48 49 50 51

# loop over the contours for c in cnts: # approximate the contour peri = cv2.arcLength(c, True) approx = cv2.approxPolyDP(c, 0.02 * peri, True)

52 53 54 55 56 57

# if our approximated contour has four points, then we can # assume we have found the outline of the receipt if len(approx) == 4: receiptCnt = approx break

58 59 60

# if the receipt contour is empty then our script could not find the # outline and we should be notified

9.2. OCR’ing Receipts with OpenCV and Tesseract

61 62 63

137

if receiptCnt is None: raise Exception(("Could not find receipt outline. " "Try debugging your edge detection and contour steps."))

Line 45 initializes a variable to store the contour that corresponds to our receipt. We then start looping over all detected contours on Line 48. Lines 50 and 51 approximate the contour by reducing the number of points in the contour, thereby simplifying the shape. Lines 55–57 check to see if we’ve found a contour with four points. If so, we can safely assume that we’ve found our receipt as this is the largest contour with four vertices. Once we’ve found the contour, we store it in receiptCnt and break from the loop. Lines 61–63 provide a graceful way for our script exit if our receipt was not found. Typically, this happens when there is a problem during the edge detection phase of our script. Due to insufficient lighting conditions or simply not having enough contrast between the receipt and the background, the edge map may be “broken” by having gaps or holes in it. When that happens, the contour detection process does not “see” the receipt as a four-corner object. Instead, it sees a strange polygon object, and thus the receipt is not detected. If and when that happens, be sure to use the --debug command line argument to visually inspect your edge map’s output. With our receipt contour found, let’s apply our perspective transform to the image:

65 66 67 68 69 70 71

# check to see if we should draw the contour of the receipt on the # image and then display it to our screen if args["debug"] > 0: output = image.copy() cv2.drawContours(output, [receiptCnt], -1, (0, 255, 0), 2) cv2.imshow("Receipt Outline", output) cv2.waitKey(0)

72 73 74 75

# apply a four-point perspective transform to the *original* image to # obtain a top-down bird's-eye view of the receipt receipt = four_point_transform(orig, receiptCnt.reshape(4, 2) * ratio)

76 77 78 79

# show transformed image cv2.imshow("Receipt Transform", imutils.resize(receipt, width=500)) cv2.waitKey(0)

If we are in debug mode, Lines 67–71 draw the outline of the receipt on our output image. We then display that output image on our screen to validate that the receipt was detected correctly (Figure 9.2, left).

138

Chapter 9. Automatically OCR’ing Receipts and Scans

A top-down, bird’s-eye view of the receipt is accomplished on Line 75. Note that we are applying the transform to the higher resolution orig image — why is that? First and foremost, the image variable has already had edge detection and contour processing applied to it. Using a perspective transform the image and then OCR’ing it wouldn’t lead to correct results; all we would get is noise. Instead, we seek the high-resolution version of the receipt. Hence we apply the perspective transform to the orig image. To do so, we need to multiply our receiptCnt (x, y )-coordinates by our ratio, thereby scaling the coordinates back to the orig spatial dimensions. To validate that we’ve computed the top-down, bird’s-eye view of the original image, we display the high-resolution receipt on our screen on Lines 78 and 79 (Figure 9.2, right).

Figure 9.2. Left: Drawing the receipt’s outline/contour, indicating we have successfully found the receipt in the input image. Right: Applying a top-down perspective transform of the receipt such that we can OCR it with Tesseract.

Given our top-down view of the receipt, we can now OCR it:

81

# apply OCR to the receipt image by assuming column data, ensuring

9.2. OCR’ing Receipts with OpenCV and Tesseract

82 83 84 85 86 87 88

139

# the text is *concatenated across the row* (additionally, for your # own images you may need to apply additional processing to cleanup # the image, including resizing, thresholding, etc.) options = "--psm 4" text = pytesseract.image_to_string( cv2.cvtColor(receipt, cv2.COLOR_BGR2RGB), config=options)

89 90 91 92 93 94

# show the raw output of the OCR process print("[INFO] raw output:") print("==================") print(text) print("\n")

Lines 85–88 use Tesseract to OCR receipt, passing in the --psm 4 mode. Using --psm 4 allows us to OCR the receipt line-by-line. Each line will include the item name and the item price. Lines 91–94 then show the raw text after applying OCR. However, the problem is that Tesseract has no idea what an item is on the receipt versus just the name of the grocery store, address, telephone number, and all the other information you typically find on a receipt. That raises the question — how do we parse out the information we do not need, leaving us with just the item names and prices? The answer is to leverage regular expressions:

96 97 98

# define a regular expression that will match line items that include # a price component pricePattern = r'([0-9]+\.[0-9]+)'

99 100 101 102 103

# show the output of filtering out *only* the line items in the # receipt print("[INFO] price line items:") print("========================")

104 105 106 107 108 109 110

# loop over each of the line items in the OCR'd receipt for row in text.split("\n"): # check to see if the price regular expression matches the current # row if re.search(pricePattern, row) is not None: print(row)

If you’ve never used regular expressions before, they are a special tool that allows us to define a pattern of text. The regular expression library (in Python, the library is re) then matches all text to this pattern.

140

Chapter 9. Automatically OCR’ing Receipts and Scans

Line 98 defines our pricePattern. This pattern will match any number of occurrences of the digits 0–9, followed by the . character (meaning the decimal separator in a price value), followed again by any number of the digits 0–9. For example, this pricePattern would match the text $9.75 but would not match the text 7600 because the text 7600 does not contain a decimal point separator. If you are new to regular expressions or simply need a refresher on them, I suggest reading the following series by RealPython: http://pyimg.co/blheh [39]. Line 106 splits our raw OCR’d text and allows us to loop over each line individually. For each line, we check to see if the row matches our pricePattern (Line 109). If so, we know we’ve found a row that contains an item and price, so we print the row to our terminal (Line 110). Congrats on building your first receipt scanner OCR application!

9.2.3

Receipt Scanner and OCR Results

Now that we’ve implemented our scan_receipt.py script, let’s put it to work. Open a terminal and execute the following command: $ python scan_receipt.py --image whole_foods.png [INFO] raw output: ================== WHOLE FOODS WHOLE FOODS MARKET - WESTPORT, CT 06880 399 POST RD WEST - (203) 227-6858 365 BACON LS NP 4.99 365 BACON LS NP 4.99 365 BACON LS NP 4.99 365 BACON LS NP 4.99 BROTH CHIC NP 4.18 FLOUR ALMOND NP 11.99 CHKN BRST BNLSS SK NP 18.80 HEAVY CREAM NP 3 7 BALSMC REDUCT NP 6.49 BEEF GRND 85/15 NP 5.04

9.2. OCR’ing Receipts with OpenCV and Tesseract

141

JUICE COF CASHEW C NP 8.99 DOCS PINT ORGANIC NP 14.49 HNY ALMOND BUTTER NP 9.99 eee TAX .00 BAL 101.33

The raw output of the Tesseract OCR engine can be seen in our terminal. By specifying --psm 4, Tesseract has been able to OCR the receipt line-by-line, capturing both items: i. name/description ii. price However, there is a bunch of other “noise” in the output, including the grocery store’s name, address, phone number, etc. How do we parse out this information, leaving us with only the items and their prices? The answer is to use regular expressions which filter on lines that have numerical values similar to prices — the output of these regular expressions can be seen below: [INFO] price line items: ======================== 365 BACON LS NP 4.99 365 BACON LS NP 4.99 365 BACON LS NP 4.99 365 BACON LS NP 4.99 BROTH CHIC NP 4.18 FLOUR ALMOND NP 11.99 CHKN BRST BNLSS SK NP 18.80 BALSMC REDUCT NP 6.49 BEEF GRND 85/15 NP 5.04 JUICE COF CASHEW C NP 8.99 DOCS PINT ORGANIC NP 14.49 HNY ALMOND BUTTER NP 9.99 eee TAX .00 BAL 101.33

By using regular expressions we’ve been able to extract only the items and prices, including the final balance due. Our receipt scanner application is an important implementation to see as it shows how OCR can be combined with a bit of text processing to extract the data of interest. There’s an entire computer science field dedicated to text processing called natural language processing (NLP). Just like computer vision is the advanced study of how to write software that can understand what’s in an image, NLP seeks to do the same, only for text. Depending on what you’re trying to build with computer vision and OCR, you may want to spend a few weeks to a few months just familiarizing yourself with NLP — that knowledge will better help you understand how to process the text returned from an OCR engine.

142

9.3

Chapter 9. Automatically OCR’ing Receipts and Scans

Summary

In this chapter, you learned how to implement a basic receipt scanner using OpenCV and Tesseract. Our receipt scanner implementation required basic image processing operations to detect the receipt, including: • Edge detection • Contour detection • Contour filtering using arc length and approximation From there, we used Tesseract, and most importantly, --psm 4, to OCR the receipt. By using --psm 4, we were able to extract each item from the receipt, line-by-line, including the item name and the cost of that particular item. The most significant limitation of our receipt scanner is that it requires: i. Sufficient contrast between the receipt and the background ii. All four corners of the receipt to be visible in the image If any of these cases do not hold, our script will not find the receipt. In our next chapter, we’ll look at another popular application of OCR, automatic license/number plate recognition.

Chapter 10

OCR’ing Business Cards In our previous chapter, we learned how to automatically OCR and scan receipts by: i. Detecting the receipt in the input image ii. Applying a perspective transform to obtain a top-down view of the receipt iii. Utilizing Tesseract to OCR the text on the receipt iv. Applying regular expressions to extract the price data In this chapter, we’re going to use a very similar workflow, but this time apply it to business card OCR. More specifically, we’ll learn how to extract the name, title, phone number, and email address from a business card. You’ll then be able to extend this implementation to your projects.

10.1

Chapter Learning Objectives

In this chapter, you will: i. Learn how to detect business cards in images ii. Apply OCR to a business card image iii. Utilize regular expressions to extract: a. Name b. Job title c. Phone number d. Email address 143

144

10.2

Chapter 10. OCR’ing Business Cards

Business Card OCR

In the first part of this chapter, we will review our project directory structure. We’ll then implement a simple yet effective Python script to allow us to OCR a business card. We’ll wrap up this chapter with a discussion of our results, along with the next steps.

10.2.1

Project Structure

Before we get too far, let’s take a look at our project directory structure: |-- larry_page.png |-- ocr_business_card.py |-- tony_stark.png

We only have a single Python script to review, ocr_business_card.py. This script will load example business card images (i.e., larry_page.png and tony_stark.png), OCR them, and then output the name, job title, phone number, and email address from the business card. Best of all, we’ll be able to accomplish our goal in under 120 lines of code (including comments)!

10.2.2

Implementing Business Card OCR

We are now ready to implement our business card OCR script! Open the ocr_business_card.py file in our project directory structure and insert the following code:

1 2 3 4 5 6 7

# import the necessary packages from imutils.perspective import four_point_transform import pytesseract import argparse import imutils import cv2 import re

Our imports here are similar to the ones in our previous chapter on OCR’ing receipts. We need our four_point_transform function to obtain a top-down, bird’s-eye view of the business card. Obtaining this view typically yields higher OCR accuracy. The pytesseract package is used to interface with the Tesseract OCR engine. We then have Python’s regular expression library, re, which will allow us to parse the names, job titles, email addresses, and phone numbers from business cards.

10.2. Business Card OCR

145

With the imports taken care of, we can move on to command line arguments: 9 10 11 12 13 14 15 16 17

# construct the argument parser and parse the arguments ap = argparse.ArgumentParser() ap.add_argument("-i", "--image", required=True, help="path to input image") ap.add_argument("-d", "--debug", type=int, default=-1, help="whether or not we are visualizing each stpe of the pipeline") ap.add_argument("-c", "--min-conf", type=int, default=0, help="minimum confidence value to filter weak text detection") args = vars(ap.parse_args())

Our first command line argument, --image, is the path to our input image on disk. We assume that this image contains a business card with sufficient contrast between the foreground and background, ensuring we can apply edge detection and contour processing to successfully extract the business card. We then have two optional command line arguments, --debug and --min-conf. The --debug command line argument is used to indicate if we are debugging our image processing pipeline and showing more of the processed images on our screen (useful for when you can’t determine why a business card was detected or not). We then have --min-conf, which is the minimum confidence (on a scale of 0–100) required for successful text detection. You can increase --min-conf to prune out weak text detections. Let’s now load our input image from disk: 19 20 21 22 23 24

# load the input image from disk, resize it, and compute the ratio # of the *new* width to the *old* width orig = cv2.imread(args["image"]) image = orig.copy() image = imutils.resize(image, width=600) ratio = orig.shape[1] / float(image.shape[1])

Here we load our input --image from disk and then clone it. We make it a clone so that we can extract the original high-resolution version of the business card after contour processing. We then resize our image to have a width of 600px and then compute the ratio of the new width to the old width (a requirement for when we want to obtain a top-down view of the original high-resolution business card). We continue our image processing pipeline below. 26 27

# convert the image to grayscale, blur it, and apply edge detection # to reveal the outline of the business card

146

28 29 30

Chapter 10. OCR’ing Business Cards

gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) blurred = cv2.GaussianBlur(gray, (5, 5), 0) edged = cv2.Canny(blurred, 30, 150)

31 32 33 34 35 36 37

# detect contours in the edge map, sort them by size (in descending # order), and grab the largest contours cnts = cv2.findContours(edged.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) cnts = imutils.grab_contours(cnts) cnts = sorted(cnts, key=cv2.contourArea, reverse=True)[:5]

38 39 40

# initialize a contour that corresponds to the business card outline cardCnt = None

First, we take our original image and then convert it to grayscale, blur it, and then apply edge detection, the result of which can be seen in Figure 10.1.

Figure 10.1. Left: Our input image containing a business card. Right: Computing the edge map of the input image reveals the outlines of the business card.

Note that the outline/border of the business card is visible on the edge map. Suppose there are any gaps in the edge map. In that case, the business card will not be detectable via our contour processing technique, so you may need to tweak the parameters to the Canny edge detector or capture your image in an environment with better lighting conditions. From there, we detect contours and sort them in descending order (largest to smallest) based on the area of the computed contour. Our assumption here will be the business card contour will be one of the largest detected contours, hence this operation.

10.2. Business Card OCR

147

We also initialize cardCnt (Line 40), which is the contour that corresponds to the business card. Let’s now loop over the largest contours:

42 43 44 45 46

# loop over the contours for c in cnts: # approximate the contour peri = cv2.arcLength(c, True) approx = cv2.approxPolyDP(c, 0.02 * peri, True)

47 48 49 50 51 52

# if this is the first contour we've encountered that has four # vertices, then we can assume we've found the business card if len(approx) == 4: cardCnt = approx break

53 54 55 56 57 58

# if the business card contour is empty then our script could not # find the outline of the card, so raise an error if cardCnt is None: raise Exception(("Could not find receipt outline. " "Try debugging your edge detection and contour steps."))

Lines 45 and 46 perform contour approximation. If our approximated contour has four vertices, then we can assume that we found the business card. If that happens, we break from the loop and update our cardCnt. If we reach the end of the for loop and still haven’t found a valid cardCnt, then we gracefully exit the script. Remember, we cannot process the business card if one cannot be found in the image! Our next code block handles showing some debugging images as well as obtaining our top-down view of the business card:

60 61 62 63 64 65 66

# check to see if we should draw the contour of the business card # on the image and then display it to our screen if args["debug"] > 0: output = image.copy() cv2.drawContours(output, [cardCnt], -1, (0, 255, 0), 2) cv2.imshow("Business Card Outline", output) cv2.waitKey(0)

67 68 69 70

# apply a four-point perspective transform to the *original* image to # obtain a top-down bird's-eye view of the business card card = four_point_transform(orig, cardCnt.reshape(4, 2) * ratio)

71 72

# show transformed image

148

73 74

Chapter 10. OCR’ing Business Cards

cv2.imshow("Business Card Transform", card) cv2.waitKey(0)

Lines 62–66 make a check to see if we are in --debug mode, and if so, we draw the contour of the business card on the output image. We then apply a four-point perspective transform to the original, high-resolution image, thus obtaining the top-down, bird’s-eye view of the business card (Line 70). We multiply the cardCnt by our computed ratio here since cardCnt was computed for the reduced image dimensions. Multiplying by ratio scales the cardCnt back into the dimensions of the orig image. We then display the transformed image to our screen (Lines 73 and 74). With our top-down view of the business card obtain, we can move on to OCR’ing it:

76 77 78 79

# convert the business card from BGR to RGB channel ordering and then # OCR it rgb = cv2.cvtColor(card, cv2.COLOR_BGR2RGB) text = pytesseract.image_to_string(rgb)

80 81 82 83 84

# use regular expressions to parse out phone numbers and email # addresses from the business card phoneNums = re.findall(r'[\+\(]?[1-9][0-9 .\-\(\)]{8,}[0-9]', text) emails = re.findall(r"[a-z0-9\.\-+_]+@[a-z0-9\.\-+_]+\.[a-z]+", text)

85 86 87 88 89

# attempt to use regular expressions to parse out names/titles (not # necessarily reliable) nameExp = r"^[\w'\-,.][^0-9_!¡?÷?¿/\\+=@#$%^&*(){}|~;:[\]]{2,}" names = re.findall(nameExp, text)

Lines 78 and 79 OCR the business card, resulting in the text output. But the question remains, how are we going to extract the information from the business card itself? The answer is to utilize regular expressions. Lines 83 and 84 utilize regular expressions to extract phone numbers and email addresses (http://pyimg.co/eb5kf [40]) from the text, while Lines 88 and 89 do the same for names and job titles (http://pyimg.co/oox3v [41]). A review of regular expressions is outside the scope of this chapter, but the gist is that they can be used to match particular patterns in text. For example, a phone number consists of a specific digits pattern, and sometimes includes dashes and parentheses. Email addresses also follow a pattern, including a string of text, followed by an “@” symbol, and then the domain name.

10.2. Business Card OCR

149

Any time you can reliably guarantee a pattern of text, regular expressions can work quite well. That said, they aren’t perfect either, so you may want to look into more advanced natural language processing (NLP) algorithms if you find your business card OCR accuracy is suffering significantly. The final step here is to display our output to the terminal:

91 92 93

# show the phone numbers header print("PHONE NUMBERS") print("=============")

94 95 96 97

# loop over the detected phone numbers and print them to our terminal for num in phoneNums: print(num.strip())

98 99 100 101 102

# show the email addresses header print("\n") print("EMAILS") print("======")

103 104 105 106 107

# loop over the detected email addresses and print them to our # terminal for email in emails: print(email.strip())

108 109 110 111 112

# show the name/job title header print("\n") print("NAME/JOB TITLE") print("==============")

113 114 115 116 117

# loop over the detected name/job titles and print them to our # terminal for name in names: print(name.strip())

This final code block loops over the extracted phone numbers (Lines 96 and 97), email addresses (Lines 106 and 107), and names/job titles (Lines 116 and 117), displaying each to our terminal. Of course, you could take this extracted information, write to disk, save it to a database, etc. Still, for the sake of simplicity (and not knowing your project specifications of business card OCR), we’ll leave it as an exercise to you to save the data as you see fit.

10.2.3

Business Card OCR Results

We are now ready to apply OCR to business cards. Open a terminal and execute the following command:

150

Chapter 10. OCR’ing Business Cards

$ python ocr_business_card.py --image tony_stark.png --debug 1 PHONE NUMBERS ============= 562-555-0100 562-555-0150 EMAILS ====== NAME/JOB TITLE ============== Tony Stark Chief Executive Officer Stark Industries

Figure 10.2 (top) shows the results of our business card localization. Notice how we have correctly detected the business card in the input image. From there, Figure 10.2 (bottom) displays the results of applying a perspective transform of the business card, thus resulting in the top-down, bird’s-eye view of the image.

Figure 10.2. Top: An input image containing Tony Stark’s business card. Bottom: Obtaining a top-down, bird’s-eye view of the input business card.

10.2. Business Card OCR

151

Once we have the top-down view of the image (typically required to obtain higher OCR accuracy), we can apply Tesseract to OCR it, the results of which can be seen in our terminal output above. Note that our script has successfully extracted both phone numbers on Tony Stark’s business card. No email addresses are reported as there is no email address on the business card. We then have the name and job title displayed as well. It’s interesting to see that we are able to OCR all the text successfully because the text of the name is more distorted than the phone number text. Our perspective transform was able to deal with all the text effectively even though the amount of distortion changes as you go further away from the camera. That’s the point of perspective transform and why it’s important to the accuracy of our OCR. Let’s try another example image, this one of an old Larry Page (co-founder of Google) business card:

$ python ocr_business_card.py --image larry_page.png --debug 1 PHONE NUMBERS ============= 650 330-0100 650 618-1499 EMAILS ====== [email protected] NAME/JOB TITLE ============== Larry Page CEO Google

Figure 10.3 (top) displays the output of localizing Page’s business card. The bottom then shows the top-down transform of the image. This top-down transform is passed through Tesseract OCR, yielding the OCR’d text as output. We take this OCR’d text, apply a regular expression, and thus obtain the results above. Examining the results, you can see that we have successfully extracted Larry Page’s two phone numbers, email address, and name/job title from the business card.

152

Chapter 10. OCR’ing Business Cards

Figure 10.3. Top: An input image containing Larry Page’s business card. Bottom: Obtaining a top-down view of the business card, which we can then OCR.

10.3

Summary

In this chapter, you learned how to build a basic business card OCR system. This system was, essentially, an extension of our receipt scanner, but with different regular expressions and text localization strategies. If you ever need to build a business card OCR system, I recommend that you use this chapter as a starting point, but keep in mind that you may want to utilize more advanced text post-processing techniques, such as true natural language processing (NLP) algorithms, rather than regular expressions. Regular expressions can work very well for email addresses and phone numbers, but for names and job titles that may fail to obtain high accuracy. If and when that time comes, you should consider leveraging NLP as much as possible to improve your results.

Chapter 11

OCR’ing License Plates with ANPR/ALPR In this chapter, you will build a basic automatic license/number plate recognition (ALPR/ANPR) system using OpenCV and Python. The ALPR/ANPR systems come in all shapes and sizes: • ALPR/ANPR performed in controlled lighting conditions with predictable license plate types can use basic image processing techniques. • More advanced ANPR systems utilize dedicated object detectors (e.g., Histogram of Oriented Gradient (HOG) + Linear Support Vector Machine (SVM), Faster R-CNN, Single-Shot Detectors (SSDs), and you only look once (YOLO)) to localize license plates in images. • State-of-the-art ANPR software utilizes Recurrent Neural Networks (RNNs) and Long Short-Term Memory (LSTM) networks to better OCR text from the license plates themselves. • And even more advanced ANPR systems use specialized neural network architectures to pre-process, and clean images before they are OCR’d, thereby improving ANPR accuracy. The ALPR/ANPR system is further complicated because it may need to operate in real-time. For example, suppose an ANPR system is mounted on a toll road. It needs to detect the license plate of each car passing by, optical character recognition (OCR) the characters on the plate, and then store this information in a database so the owner of the vehicle can be billed for the toll. Several compounding factors make ANPR incredibly challenging, including finding a dataset 153

154

Chapter 11. OCR’ing License Plates with ANPR/ALPR

you can use to train a custom ANPR model! Large, robust ANPR datasets that are used to train state-of-the-art models are closely guarded and rarely (if ever) released publicly: • These datasets contain sensitive identifying information related to the vehicle, driver, and location. • ANPR datasets are tedious to curate, requiring an incredible investment of time and staff hours to annotate. • ANPR contracts with local and federal governments tend to be highly competitive. Because of that, it’s often not the trained model that is valuable, but instead the dataset that a given company has curated. For that reason, you’ll see ANPR companies acquired not for their ANPR system but for the data itself! In the remainder of this chapter, we’ll be building a basic ALPR/ANPR system. By the end of this chapter, you’ll have a template/starting point to use when building your ANPR projects.

11.1

Chapter Learning Objectives

In this chapter, you will: • Discover what ALPR/ANPR is • Implement a function used to detect license plates in images • Create a second function to OCR the license plate • Glue the pieces together and form a basic yet complete ANPR project

11.2

ALPR/ANPR and OCR

My first run-in with ALPR/ANPR was about six years ago. After a grueling three-day marathon consulting project in Maryland, where it did nothing but rain the entire time, I hopped on I-95 to drive back to Connecticut to visit friends for the weekend. It was a beautiful summer day. Sun shining. Not a cloud in the sky. A soft breeze blowing. Perfect. Of course, I had my windows down, my music turned up, and I had totally zoned out — not a care in the world.

11.2. ALPR/ANPR and OCR

155

I didn’t even notice when I drove past a small gray box discreetly positioned along the side of the highway. Two weeks later . . . I got the speeding ticket in the mail. Sure enough, I had unknowingly driven past a speed-trap camera doing 78 miles per hour (mph) in a 65-mph zone. That speeding camera caught me with my foot on the pedal, quite literally, and it had pictures to prove it too. There it was, clear as day! You could see the license plate number on my old Honda Civic (before it got burnt to a crisp in an electrical fire). Now, here’s the ironic part. I knew exactly how their ALPR/ANPR system worked. I knew which image processing techniques the developers used to automatically localize my license plate in the image and extract the plate number via OCR. In this chapter, my goal is to teach you one of the quickest ways to build such an ALPR/ANPR system. Using a bit of OpenCV, Python, and Tesseract OCR knowledge, you could help your homeowners’ association monitor cars that come and go from your neighborhood. Or maybe you want to build a camera-based (radar-less) system that determines the speed of cars that drive by your house using a Raspberry Pi (http://pyimg.co/5xigt [42]). If the car exceeds the speed limit, you can analyze the license plate, apply OCR to it, and log the license plate number to a database. Such a system could help reduce speeding violations and create better neighborhood safety. In the first part of this chapter, you’ll learn and define what ALPR/ANPR is. From there, we’ll review our project structure. I’ll then show you how to implement a basic Python class (aptly named PyImageSearchANPR) that will localize license plates in images and then OCR the characters. We’ll wrap up the chapter by examining the results of our ANPR system.

11.2.1

What Is Automatic License Plate Recognition?

ALPR/ANPR is a process involving the following steps: • Step #1: Detect and localize a license plate in an input image/frame • Step #2: Extract the characters from the license plate • Step #3: Apply some form of OCR to recognize the extracted characters

156

Chapter 11. OCR’ing License Plates with ANPR/ALPR

ANPR tends to be an extremely challenging subfield of computer vision due to the vast diversity and assortment of license plate types across states and countries. License plate recognition systems are further complicated by: • Dynamic lighting conditions including reflections, shadows, and blurring • Fast-moving vehicles • Obstructions Additionally, large and robust ANPR datasets for training/testing are difficult to obtain due to: i. These datasets containing sensitive, personal information, including the time and location of a vehicle and its driver ii. ANPR companies and government entities closely guarding these datasets as proprietary information Therefore, the first part of an ANPR project is to collect data and amass enough sample plates under various conditions. Let’s assume we don’t have a license plate dataset (quality datasets are hard to come by). That rules out deep learning object detection, which means we will have to exercise our traditional computer vision knowledge. I agree that it would be nice to have a trained object detection model, but today I want you to rise to the occasion. Before long, we’ll be able to ditch the training wheels and consider working for a toll technology company, red-light camera integrator, speed ticketing system, or parking garage ticketing firm in which we need 99.97% accuracy. Given these limitations, we’ll be building a basic ANPR system that you can use as a starting point for your projects.

11.2.2

Project Structure

Before we write any code, let’s first review our project directory structure:

|-| | | |

license_plates |-- group1 | |-- 001.jpg | |-- 002.jpg | |-- 003.jpg

11.2. ALPR/ANPR and OCR

| | | | | | |-| | | | | |--

157

| |-- 004.jpg | |-- 005.jpg |-- group2 | |-- 001.jpg | |-- 002.jpg | |-- 003.jpg pyimagesearch |-- __init__.py |-- helpers.py |-- anpr | |-- __init__.py | |-- anpr.py ocr_license_plate.py

The project folder contains: • license_plates: Directory containing two subdirectories of JPEG images • anpr.py: Contains the PyImageSearchANPR class responsible for localizing license/number plates and performing OCR • ocr_license_plate.py: Our main driver Python script, which uses our PyImageSearchANPR class to OCR entire groups of images Now that we have the lay of the land, let’s walk through our two Python scripts, which locate and OCR groups of license/number plates and display the results.

11.2.3

Implementing ALPR/ANPR with OpenCV and OCR

We’re ready to start implementing our ALPR/ANPR script. As I mentioned before, we’ll keep our code neat and organized using a Python class appropriately named PyImageSearchANPR. This class provides a reusable means for license plate localization and character OCR operations. Open anpr.py and let’s get to work reviewing the script:

1 2 3 4 5 6

# import the necessary packages from skimage.segmentation import clear_border import pytesseract import numpy as np import imutils import cv2

7 8 9 10

class PyImageSearchANPR: def __init__(self, minAR=4, maxAR=5, debug=False): # store the minimum and maximum rectangular aspect ratio

158

Chapter 11. OCR’ing License Plates with ANPR/ALPR

# values along with whether or not we are in debug mode self.minAR = minAR self.maxAR = maxAR self.debug = debug

11 12 13 14

At this point, these imports should feel pretty standard and straightforward. The exception is perhaps scikit-image’s clear_border function — this method helps clean up the borders of images, thereby improving Tesseract’s OCR accuracy. Our PyImageSearchANPR class begins on Line 8. The constructor accepts three parameters: • minAR: The minimum aspect ratio used to detect and filter rectangular license plates, which has a default value of 4 • maxAR: The maximum aspect ratio of the license plate rectangle, which has a default value of 5 • debug: A flag to indicate whether we should display intermediate results in our image processing pipeline The aspect ratio range (minAR to maxAR) corresponds to a license plate’s typical rectangular dimensions. Keep the following considerations in mind if you need to alter the aspect ratio parameters: • European and international plates are often longer and not as tall as United States license plates. We’re not considering U.S. license plates. • Sometimes, motorcycles and large dumpster trucks mount their plates sideways; this is a true edge case that would have to be considered for a highly accurate license plate system. • Some countries and regions allow for multi-line plates with a near 1:1 aspect ratio; again, we won’t consider this edge case. Each of our constructor parameters becomes a class variable on Lines 12–14 so the methods in the class can access them.

11.2.4

Debugging Our Computer Vision Pipeline

With our constructor ready to go, let’s define a helper function to display results at various points in the imaging pipeline when in debug mode:

11.2. ALPR/ANPR and OCR

16 17 18 19 20

159

def debug_imshow(self, title, image, waitKey=False): # check to see if we are in debug mode, and if so, show the # image with the supplied title if self.debug: cv2.imshow(title, image)

21

# check to see if we should wait for a keypress if waitKey: cv2.waitKey(0)

22 23 24

Our helper function debug_imshow (Line 16) accepts three parameters: i. title: The desired OpenCV window title. Window titles should be unique; otherwise OpenCV will replace the image in the same-titled window rather than creating a new one. ii. image: The image to display inside the OpenCV graphical user interface (GUI) window. iii. waitKey: A flag to see if the display should wait for a keypress before completing. Lines 19–24 display the debugging image in an OpenCV window. Typically, the waitKey Boolean will be False. However, we have set it to True to inspect debugging images and dismiss them when we are ready.

11.2.5

Locating Potential License Plate Candidates

Our first ANPR method helps us to find the license plate candidate contours in an image:

26 27 28 29 30 31 32

def locate_license_plate_candidates(self, gray, keep=5): # perform a blackhat morphological operation which will allow # us to reveal dark regions (i.e., text) on light backgrounds # (i.e., the license plate itself) rectKern = cv2.getStructuringElement(cv2.MORPH_RECT, (13, 5)) blackhat = cv2.morphologyEx(gray, cv2.MORPH_BLACKHAT, rectKern) self.debug_imshow("Blackhat", blackhat)

Our locate_license_plate_candidates expects two parameters: i. gray: This function assumes that the driver script will provide a grayscale image containing a potential license plate. ii. keep: We’ll only return up to this many sorted license plate candidate contours. We’re now going to make a generalization to help us simplify our ANPR pipeline. Let’s assume from here forward that most license plates have a light background (typically it is highly reflective) and a dark foreground (characters).

160

Chapter 11. OCR’ing License Plates with ANPR/ALPR

I realize there are plenty of cases where this generalization does not hold, but let’s continue working on our proof of concept, and we can make accommodations for inverse plates in the future. Lines 30 and 31 perform a blackhat morphological operation to reveal dark characters (letters, digits, and symbols) against light backgrounds (the license plate itself). As you can see, our kernel has a rectangular shape of 13 pixels wide x 5 pixels tall, which corresponds to the shape of a typical international license plate. If your debug option is on, you’ll see a blackhat visualization similar to the one in Figure 11.1 (bottom-left). As you can see from above, the license plate characters are visible!

Figure 11.1. Top-left: Our sample input image containing a license plate that we wish to detect and OCR. Bottom-left: OpenCV’s blackhat morphological operator highlights the license plate numbers against the rest of the photo of the rear end of the car. You can see that the license plate numbers “pop” as white text against the black background and most of the background noise is washed out. Top-right: Perform a closing and threshold operation — notice how the regions where the license plate is located are almost one large white surface. Bottom-right: Applying Scharr’s algorithm in the x-direction emphasizes the edges in our blackhat image as another ANPR image processing pipeline step.

In our next step, we’ll find regions in the image that are light and may contain license plate characters:

34 35

# next, find regions in the image that are light squareKern = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))

11.2. ALPR/ANPR and OCR

36 37 38 39

161

light = cv2.morphologyEx(gray, cv2.MORPH_CLOSE, squareKern) light = cv2.threshold(light, 0, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU)[1] self.debug_imshow("Light Regions", light)

Using a small square kernel (Line 35), we apply a closing operation (Lines 36) to fill small holes and identify larger image structures. Lines 37 and 38 perform a binary threshold on our image using Otsu’s method to reveal the light regions in the image that may contain license plate characters. Figure 11.1 (top-right) shows the effect of the closing operation combined with Otsu’s inverse binary thresholding. Notice how the regions where the license plate is located are almost one large white surface. The Scharr gradient will detect edges in the image and emphasize the boundaries of the characters in the license plate:

41 42 43 44 45 46 47 48 49 50

# compute the Scharr gradient representation of the blackhat # image in the x-direction and then scale the result back to # the range [0, 255] gradX = cv2.Sobel(blackhat, ddepth=cv2.CV_32F, dx=1, dy=0, ksize=-1) gradX = np.absolute(gradX) (minVal, maxVal) = (np.min(gradX), np.max(gradX)) gradX = 255 * ((gradX - minVal) / (maxVal - minVal)) gradX = gradX.astype("uint8") self.debug_imshow("Scharr", gradX)

Using cv2.Sobel, we compute the Scharr gradient magnitude representation in the x-direction of our blackhat image (Lines 44 and 45). We then scale the resulting intensities back to the range [0, 255] (Lines 46–49). Figure 11.1 (bottom-right) demonstrates an emphasis on the edges of the license plate characters. As you can see above, the license plate characters appear noticeably different from the rest of the image. We can now smooth to group the regions that may contain boundaries to license plate characters: 52 53 54 55 56 57 58

# blur the gradient representation, applying a closing # operation, and threshold the image using Otsu's method gradX = cv2.GaussianBlur(gradX, (5, 5), 0) gradX = cv2.morphologyEx(gradX, cv2.MORPH_CLOSE, rectKern) thresh = cv2.threshold(gradX, 0, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU)[1] self.debug_imshow("Grad Thresh", thresh)

162

Chapter 11. OCR’ing License Plates with ANPR/ALPR

Here we apply a Gaussian blur to the gradient magnitude image (gradX) (Line 54). Again, we apply a closing operation (Line 55) and another binary threshold using Otsu’s method (Lines 56 and 57). Figure 11.2 (top-left) shows a contiguous white region where the license plate characters are located. At first glance, these results look cluttered. The license plate region is somewhat defined, but there are many other large white regions as well.

Figure 11.2. Top-left: Blurring, closing, and thresholding operations result in a contiguous white region on top of the license plate/number plate characters. Top-right: Erosions and dilations with clean up our thresholded image, making it easier to find our license plate characters for our ANPR system. Bottom: After a series of image processing pipeline steps, we can see the region with the license plate characters is one of the larger contours.

Let’s see if we can eliminate some of the noise:

60 61 62 63 64

# perform a series of erosions and dilations to clean up the # thresholded image thresh = cv2.erode(thresh, None, iterations=2) thresh = cv2.dilate(thresh, None, iterations=2) self.debug_imshow("Grad Erode/Dilate", thresh)

Lines 62 and 63 perform a series of erosions and dilations in an attempt to denoise the thresholded image. As you can see in Figure 11.2 (top-right), the erosion and dilation

11.2. ALPR/ANPR and OCR

163

operations cleaned up a lot of noise in the previous result from Figure 11.1. We aren’t done yet, though. Let’s add another step to the pipeline, in which we’ll put our light region’s image to use: 66 67 68 69 70 71

# take the bitwise AND between the threshold result and the # light regions of the image thresh = cv2.bitwise_and(thresh, thresh, mask=light) thresh = cv2.dilate(thresh, None, iterations=2) thresh = cv2.erode(thresh, None, iterations=1) self.debug_imshow("Final", thresh, waitKey=True)

Back on Lines 35–38, we devised a method to highlight lighter regions in the image (keeping in mind our established generalization that license plates will have a light background and dark foreground). This light image serves as our mask for a bitwise-AND between the thresholded result and the image’s light regions to reveal the license plate candidates (Line 68). We follow with a couple of dilations and an erosion to fill holes and clean up the image (Lines 69 and 70). Our final debugging image is shown in Figure 11.2 (bottom). Notice that the last call to debug_imshow overrides waitKey to True, ensuring that as a user, we can inspect all debugging images up until this point and press a key when we are ready. You should notice that our license plate contour is not the largest, but it’s far from being the smallest. At a glance, I’d say it is the second or third largest contour in the image, and I also notice the plate contour is not touching the edge of the image. Speaking of contours, let’s find and sort them: 73 74 75 76 77 78 79

# find contours in the thresholded image and sort them by # their size in descending order, keeping only the largest # ones cnts = cv2.findContours(thresh.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) cnts = imutils.grab_contours(cnts) cnts = sorted(cnts, key=cv2.contourArea, reverse=True)[:keep]

80 81 82

# return the list of contours return cnts

To close out our locate_license_plate_candidates method, we: • Find all contours (Lines 76–78) • Reverse-sort them according to their pixel area while only retaining at most keep contours

164

Chapter 11. OCR’ing License Plates with ANPR/ALPR

• Return the resulting sorted and pruned list of cnts (Line 82). Take a step back to think about what we’ve accomplished in this method. We’ve accepted a grayscale image and used traditional image processing techniques with an emphasis on morphological operations to find a selection of candidate contours that might contain a license plate. I know what you are thinking: “Why haven’t we applied deep learning object detection to find the license plate? Wouldn’t that be easier?” While that is perfectly acceptable (and don’t get me wrong, I love deep learning!), it is a lot of work to train such an object detector on your own. What we are considering by training our own object detector requires countless hours to annotate thousands of images in your dataset. But remember, we didn’t have the luxury of a dataset in the first place, so the method we’ve developed so far relies on so-called “traditional” image processing techniques.

11.2.6

Pruning License Plate Candidates

In this next method, our goal is to find the most likely contour containing a license plate from our set of candidates. Let’s see how it works:

84 85 86 87 88

def locate_license_plate(self, gray, candidates, clearBorder=False): # initialize the license plate contour and ROI lpCnt = None roi = None

89 90 91 92 93 94 95

# loop over the license plate candidate contours for c in candidates: # compute the bounding box of the contour and then use # the bounding box to derive the aspect ratio (x, y, w, h) = cv2.boundingRect(c) ar = w / float(h)

Our locate_license_plate function accepts three parameters: i. gray: Our input grayscale image ii. candidates: The license plate contour candidates returned by the previous method in this class

11.2. ALPR/ANPR and OCR

165

iii. clearBorder: A Boolean indicating whether our pipeline should eliminate any contours that touch the edge of the image Before we begin looping over the license plate contour candidates, first we initialize variables that will soon hold our license plate contour (lpCnt) and license plate region of interest (roi) on Lines 87 and 88. Starting on Line 91, our loop begins. This loop aims to isolate the contour that contains the license plate and extract the region of interest of the license plate itself. We proceed by determining the bounding box rectangle of the contour, c (Line 94). Computing the aspect ratio of the contour’s bounding box (Line 95) will ensure our contour is the proper rectangular shape of a license plate. As you can see in the equation, the aspect ratio is a relationship between the rectangle’s width and height. Let’s inspect the aspect ratio now and filter out bounding boxes that are not good candidates for license plates:

97 98 99 100 101 102 103 104 105

# check to see if the aspect ratio is rectangular if ar >= self.minAR and ar args["min_conf"]: # process the text by stripping out non-ASCII # characters text = cleanup_text(text)

163 164 165 166 167 168 169 170 171 172

# if the cleaned up text is not empty, draw a # bounding box around the text along with the # text itself if len(text) > 0: cv2.rectangle(card, (x, y), (x + w, y + h), (0, 255, 0), 2) cv2.putText(ocr, text, (x, y - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 255), 1)

Line 145 loops over all text detections. We then proceed to: • Grab the bounding box coordinates of the text ROI (Lines 148–151) • Extract the OCR’d text and its corresponding confidence/probability (Lines 155 and 156) • Verify that the text detection has sufficient confidence, followed by stripping out non-ASCII characters from the text (Lines 159–162) • Draw the OCR’d text on the ocr visualization (Lines 167–172) The rest of the code blocks in this example focus more on bookkeeping variables and output:

174 175

# build our final video OCR output visualization output = outputBuilder.build(frame, card, ocr)

176 177 178 179

# check if the video writer is None *and* an output video file # path was supplied if args["output"] is not None and writer is None:

14.2. OCR’ing Real-Time Video Streams

180 181 182 183 184 185

221

# grab the output frame dimensions and initialize our video # writer (outputH, outputW) = output.shape[:2] fourcc = cv2.VideoWriter_fourcc(*"MJPG") writer = cv2.VideoWriter(args["output"], fourcc, 27, (outputW, outputH), True)

186 187 188 189 190 191 192 193

# if the writer is not None, we need to write the output video # OCR visualization to disk if writer is not None: # force resize the video OCR visualization to match the # dimensions of the output video outputFrame = cv2.resize(output, (outputW, outputH)) writer.write(outputFrame)

194 195 196 197 198

# show the output video OCR visualization cv2.imshow("Output", output) cv2.moveWindow("Output", 0, 0) key = cv2.waitKey(1) & 0xFF

199 200 201 202

# if the `q` key was pressed, break from the loop if key == ord("q"): break

Line 175 creates our output frame using the .build method our VideoOCROutputBuilder class. We then check to see if an --output video file path is supplied, and if so, instantiate our cv2.VideoWriter so we can write the output frame visualizations to disk (Lines 179–185). Similarly, if the writer object has been instantiated, we then write the output frame to disk (Lines 189–193). Lines 196–202 display the output frame to our screen: Our final code block releases video pointers: 204 205 206

# if we are using a webcam, stop the camera video stream if webcam: vs.stop()

207 208 209 210

# otherwise, release the video file pointer else: vs.release()

211 212 213

# close any open windows cv2.destroyAllWindows()

Taken as a whole, this may seem like a complicated script. But keep in mind that we just

222

Chapter 14. OCR’ing Video Streams

implemented an entire video OCR pipeline in under 225 lines of code (including comments). That’s not much code when you think about it — and it’s all made possible by using OpenCV and Tesseract!

14.2.4

Real-Time Video OCR Results

We are now ready to put our video OCR script to the test! Open a terminal and execute the following command:

$ python ocr_video.py --input video/business_card.mp4 --output ,→ output/ocr_video_output.avi [INFO] opening video file...

I clearly cannot include a video demo or animated graphics interchange format (GIF) inside a book/PDF, so I’ve taken screen captures from our ocr_video_output.avi file in the output directory — the screen captures can be seen in Figure 14.2.

Figure 14.2. Left: Detecting a frame that is too blurry to OCR. Instead of attempting to OCR this frame, which would lead to incorrect or nonsensical results, we instead wait for a higher-quality frame. Right: Detecting a frame that is of sufficient quality to OCR. Here you see that we have correctly OCR’d the business card.

Notice on the left that our script has correctly detected a blurry frame and did not OCR it. If we had attempted to OCR this frame, the results would have been nonsensical, confusing the end-user.

14.3. Summary

223

Instead, we wait for a higher-quality frame (right) and then OCR it. As you can see, by waiting for the higher-quality frame, we were able to correctly OCR the business card. If you ever need to apply OCR to video streams, I strongly recommend that you implement some type of low-quality versus high-quality frame detector. Do not attempt to OCR every single frame of the video stream unless you are 100% confident that the video was captured under ideal, controlled conditions and that every frame is high-quality.

14.3

Summary

In this chapter, you learned how to OCR video streams. However, to OCR the video streams, we need to detect blurry, low-quality frames. Videos naturally have low-quality frames due to rapid changes in lighting conditions, camera lens autofocusing, and motion blur. Instead of trying to OCR these low-quality frames, which would ultimately lead to low OCR accuracy (or worse, totally nonsensical results), we instead need to detect these low-quality frames and discard them. One easy way to detect low-quality frames is to use blur detection. We utilized our FFT blur detector to work with our video stream. The result is an OCR pipeline capable of operating on video streams while still maintaining high accuracy. I hope you enjoyed this chapter! And I hope that you can apply this method to your projects.

Chapter 15

Improving Text Detection Speed with OpenCV and GPUs up to this point, everything except the EasyOCR material has focused on performing OCR on our CPU — but what if we could instead apply OCR on our GPU? Since many state-of-the-art text detection and OCR models are deep learning-based, couldn’t these models run faster and more efficiently on a GPU? The answer is yes; they absolutely can. This chapter will show you how to take the efficient and accurate scene text detector (EAST) model and run it on OpenCV’s dnn (deep neural network) module using an NVIDIA GPU. As we’ll see, our text detection throughput rate nearly triples, improving from ≈23 frames per second (FPS) to an astounding ≈97 FPS!

15.1

Chapter Learning Objectives

In this chapter, you will: • Learn how to use OpenCV’s dnn module to run deep neural networks on an NVIDIA CUDA-based GPU • Implement a Python script to benchmark text detection speed on both a CPU and GPU • Implement a second Python script, this one that performs text detection in real-time video streams • Compare the results of running text detection on a CPU versus a GPU

225

226

15.2

Chapter 15. Improving Text Detection Speed with OpenCV and GPUs

Using Your GPU for OCR with OpenCV

The first part of this chapter covers reviewing our directory structure for the project. We’ll then implement a Python script that will benchmark running text detection on a CPU versus a GPU. We’ll run this script and measure just how much of a difference running text detection on a GPU improves our FPS throughput rate. Once we’ve measured our FPS increase, we’ll implement a second Python script, this one to perform text detection in real-time video streams. We’ll wrap up the chapter with a discussion of our results.

15.2.1

Project Structure

Before we can apply text detection with our GPU, we first need to review our project directory structure:

|-- pyimagesearch | |-- __init__.py | |-- east | | |-- __init__.py | | |-- east.py |-- ../models | |-- east | | |-- frozen_east_text_detection.pb -- images | |-- car_wash.png |-- text_detection_speed.py |-- text_detection_video.py

We’ll be reviewing two Python scripts in this chapter: i. text_detection_speed.py: Benchmarks text detection speed on a CPU versus a GPU using the car_wash.png image in our images directory. ii. text_detection_video.py: Demonstrates how to perform real-time text detection on your GPU.

15.2.2

Implementing Our OCR GPU Benchmark Script

Before we implement text detection in real-time video streams with our GPU, let’s first benchmark how much of a speedup we get by running the EAST detection model on our CPU versus our GPU.

15.2. Using Your GPU for OCR with OpenCV

227

To find out, open the text_detection_speed.py file in our project directory, and let’s get started:

1 2 3 4 5 6

# import the necessary packages from pyimagesearch.east import EAST_OUTPUT_LAYERS import numpy as np import argparse import time import cv2

Lines 2–6 handle importing our required Python packages. We need the EAST model’s output layers (Line 2) to grab the text detection outputs. If you need a refresher on these output values, be sure to refer to “OCR with OpenCV, Tesseract, and Python: Intro to OCR” book. Next, we have our command line arguments:

8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24

# construct the argument parser and parse the arguments ap = argparse.ArgumentParser() ap.add_argument("-i", "--image", required=True, help="path to input image") ap.add_argument("-e", "--east", required=True, help="path to input EAST text detector") ap.add_argument("-w", "--width", type=int, default=320, help="resized image width (should be multiple of 32)") ap.add_argument("-t", "--height", type=int, default=320, help="resized image height (should be multiple of 32)") ap.add_argument("-c", "--min-conf", type=float, default=0.5, help="minimum probability required to inspect a text region") ap.add_argument("-n", "--nms-thresh", type=float, default=0.4, help="non-maximum suppression threshold") ap.add_argument("-g", "--use-gpu", type=bool, default=False, help="boolean indicating if CUDA GPU should be used") args = vars(ap.parse_args())

The --image command line argument specifies the path to the input image that we’ll be performing text detection on. Lines 12–21 then specify command line arguments for the EAST text detection model. Finally, we have our --use-gpu command line argument. By default, we’ll use our CPU — but by specifying this argument (and provided we have a CUDA-capable GPU and OpenCV’s dnn module compiled with NVIDIA GPU support), then we can use our GPU for text detection inference. With our command line arguments taken care of, we can now load the EAST text detection model and set whether we are using the CPU or GPU:

228

26 27 28

Chapter 15. Improving Text Detection Speed with OpenCV and GPUs

# load the pre-trained EAST text detector print("[INFO] loading EAST text detector...") net = cv2.dnn.readNet(args["east"])

29 30 31 32 33 34 35

# check if we are going to use GPU if args["use_gpu"]: # set CUDA as the preferable backend and target print("[INFO] setting preferable backend and target to CUDA...") net.setPreferableBackend(cv2.dnn.DNN_BACKEND_CUDA) net.setPreferableTarget(cv2.dnn.DNN_TARGET_CUDA)

36 37 38 39

# otherwise we are using our CPU else: print("[INFO] using CPU for inference...")

Line 28 loads our EAST text detection model from disk. Lines 31–35 makes a check to see if the --use-gpu command line argument was supplied, and if so, indicates that we want to use our NVIDIA CUDA-capable GPU. Note: To use your GPU for neural network inference, you need to have OpenCV’s dnn module compiled with NVIDIA CUDA support. OpenCV’s dnn module does not have NVIDIA support via a pip install. Instead, you need to compile OpenCV with GPU support explicitly. I cover how to do that in the tutorial on PyImageSearch: pyimg.co/jfftm [57]. Next, let’s load our sample image from disk:

41 42 43 44

# load the input image and then set the new width and height values # based on our command line arguments image = cv2.imread(args["image"]) (newW, newH) = (args["width"], args["height"])

45 46 47 48 49 50 51 52 53

# construct a blob from the image, set the blob as input to the # network, and initialize a list that records the amount of time # each forward pass takes print("[INFO] running timing trials...") blob = cv2.dnn.blobFromImage(image, 1.0, (newW, newH), (123.68, 116.78, 103.94), swapRB=True, crop=False) net.setInput(blob) timings = []

Line 43 loads our input --image from disk while Lines 50 and 51 construct a blob object such that we can pass it through the EAST text detection model. Line 52 sets our blob as input to the EAST network, while Line 53 initializes a timings list to measure how long the inference takes. When using a GPU for inference, your first prediction tends to be very slow compared to the

15.2. Using Your GPU for OCR with OpenCV

229

rest of the predictions, the reason being that your GPU hasn’t “warmed up” yet. Therefore, when taking measurements on your GPU, you typically want to take an average over several predictions. In the following code block, we perform text detection for 500 trials, recording how long each prediction takes:

55 56 57 58 59 60 61 62

# loop over 500 trials to obtain a good approximation to how long # each forward pass will take for i in range(0, 500): # time the forward pass start = time.time() (scores, geometry) = net.forward(EAST_OUTPUT_LAYERS) end = time.time() timings.append(end - start)

63 64 65 66

# show average timing information on text prediction avg = np.mean(timings) print("[INFO] avg. text detection took {:.6f} seconds".format(avg))

After all trials are complete, we compute the average of the timings and then display our average text detection time on our terminal.

15.2.3

Speed Test: OCR With and Without GPU

Let’s now measure our EAST text detection FPS throughput rate without a GPU (i.e., running on a CPU): $ python text_detection_speed.py --image images/car_wash.png --east ,→ ../models/east/frozen_east_text_detection.pb [INFO] loading EAST text detector... [INFO] using CPU for inference... [INFO] running timing trials... [INFO] avg. text detection took 0.108568 seconds

Our average text detection speed is ≈0.1 seconds, which equates to ≈9–10 FPS. A deep learning model running on a CPU is quite fast and sufficient for many applications. However, as Tim Taylor (played by Tim Allen of Toy Story ) from the 1990s TV show, Home Improvement, says, “More power!” Let’s now break out the GPUs: $ python text_detection_speed.py --image images/car_wash.png --east ,→ ../models/east/frozen_east_text_detection.pb --use-gpu 1

230

[INFO] [INFO] [INFO] [INFO]

Chapter 15. Improving Text Detection Speed with OpenCV and GPUs

loading EAST text detector... setting preferable backend and target to CUDA... running timing trials... avg. text detection took 0.004763 seconds

Using an NVIDIA V100 GPU, our average frame processing rate decreases to ≈0.004 seconds, meaning that we can now process ≈250 FPS! As you can see, using your GPU makes a substantial difference!

15.2.4

OCR on GPU for Real-Time Video Streams

Ready to implement our script to perform text detection in real-time video streams using your GPU? Open the text_detection_video.py file in your project directory, and let’s get started:

1 2 3 4 5 6 7 8 9 10

# import the necessary packages from pyimagesearch.east import EAST_OUTPUT_LAYERS from pyimagesearch.east import decode_predictions from imutils.video import VideoStream from imutils.video import FPS import numpy as np import argparse import imutils import time import cv2

Lines 2–10 import our required Python packages. The EAST_OUTPUT_LAYERS and decode_predictions function come from our implementation of the EAST text detector in Chapter 18, “Rotated Text Bounding Box Localization with OpenCV” from Book One of this two-part book series — be sure to review that chapter if you need a refresher on the EAST detection model. Line 4 imports our VideoStream to access our webcam, while Line 5 provides our FPS class to measure the FPS throughput rate of our pipeline. Let’s now proceed to our command line arguments:

12 13 14 15 16 17

# construct the argument parser and parse the arguments ap = argparse.ArgumentParser() ap.add_argument("-i", "--input", type=str, help="path to optional input video file") ap.add_argument("-e", "--east", required=True, help="path to input EAST text detector")

15.2. Using Your GPU for OCR with OpenCV

18 19 20 21 22 23 24 25 26 27 28

231

ap.add_argument("-w", "--width", type=int, default=320, help="resized image width (should be multiple of 32)") ap.add_argument("-t", "--height", type=int, default=320, help="resized image height (should be multiple of 32)") ap.add_argument("-c", "--min-conf", type=float, default=0.5, help="minimum probability required to inspect a text region") ap.add_argument("-n", "--nms-thresh", type=float, default=0.4, help="non-maximum suppression threshold") ap.add_argument("-g", "--use-gpu", type=bool, default=False, help="boolean indicating if CUDA GPU should be used") args = vars(ap.parse_args())

These command line arguments are nearly the same as previous command line arguments, the only exception is that we swapped out the --image command line argument for an --input argument, which specifies the path to an optional video file on disk (just in case we wanted to use a video file rather than our webcam). Next, we have a few initializations:

30 31 32 33 34

# initialize the original frame dimensions, new frame dimensions, # and ratio between the dimensions (W, H) = (None, None) (newW, newH) = (args["width"], args["height"]) (rW, rH) = (None, None)

Here we initialize our original frame’s width and height, the new frame dimensions for the EAST model, followed by the ratio between the original and the new dimensions. This next code block handles loading the EAST text detection model from disk and then setting whether or not we are using our CPU or GPU for inference:

36 37 38

# load the pre-trained EAST text detector print("[INFO] loading EAST text detector...") net = cv2.dnn.readNet(args["east"])

39 40 41 42 43 44 45

# check if we are going to use GPU if args["use_gpu"]: # set CUDA as the preferable backend and target print("[INFO] setting preferable backend and target to CUDA...") net.setPreferableBackend(cv2.dnn.DNN_BACKEND_CUDA) net.setPreferableTarget(cv2.dnn.DNN_TARGET_CUDA)

46 47 48 49

# otherwise we are using our CPU else: print("[INFO] using CPU for inference...")

Our text detection model needs frames to operate on, so the next code block accesses either

232

Chapter 15. Improving Text Detection Speed with OpenCV and GPUs

our webcam or a video file residing on disk, depending on whether or not the --input command line argument was supplied:

51 52 53 54 55

# if a video path was not supplied, grab the reference to the webcam if not args.get("input", False): print("[INFO] starting video stream...") vs = VideoStream(src=0).start() time.sleep(1.0)

56 57 58 59

# otherwise, grab a reference to the video file else: vs = cv2.VideoCapture(args["input"])

60 61 62

# start the FPS throughput estimator fps = FPS().start()

Line 62 starts measuring our FPS throughput rates to get a good idea of the number of frames our text detection pipeline can process in a single second. Let’s start looping over frames from the video stream now:

64 65 66 67 68 69

# loop over frames from the video stream while True: # grab the current frame, then handle if we are using a # VideoStream or VideoCapture object frame = vs.read() frame = frame[1] if args.get("input", False) else frame

70 71 72 73

# check to see if we have reached the end of the stream if frame is None: break

74 75 76 77

# resize the frame, maintaining the aspect ratio frame = imutils.resize(frame, width=1000) orig = frame.copy()

78 79 80 81 82 83 84

# if our frame dimensions are None, we still need to compute the # ratio of old frame dimensions to new frame dimensions if W is None or H is None: (H, W) = frame.shape[:2] rW = W / float(newW) rH = H / float(newH)

Lines 68 and 69 read the next frame from either our webcam or our video file. If we are indeed processing a video file, Lines 72 makes a check to see if we are at the end of the video and if so, we break from the loop.

15.2. Using Your GPU for OCR with OpenCV

233

Lines 81–84 grab the spatial dimensions of the input frame and then compute the ratio of the original frame dimensions to the dimensions required by the EAST model. Now that we have these dimensions, we can construct our input to the EAST text detector:

86 87 88 89 90 91

# construct a blob from the image and then perform a forward pass # of the model to obtain the two output layer sets blob = cv2.dnn.blobFromImage(frame, 1.0, (newW, newH), (123.68, 116.78, 103.94), swapRB=True, crop=False) net.setInput(blob) (scores, geometry) = net.forward(EAST_OUTPUT_LAYERS)

92 93 94 95 96 97 98 99

# decode the predictions form OpenCV's EAST text detector and # then apply non-maximum suppression (NMS) to the rotated # bounding boxes (rects, confidences) = decode_predictions(scores, geometry, minConf=args["min_conf"]) idxs = cv2.dnn.NMSBoxesRotated(rects, confidences, args["min_conf"], args["nms_thresh"])

Lines 88–91 build blob from the input frame. We then set this blob as input to our EAST text detection net. A forward pass of the network is performed, resulting in our raw text detections. However, our raw text detections are unusable in our current state, so we call decode_predictions on them, yielding a 2-tuple of the bounding box coordinates of the text detections along with the associated probabilities (Lines 96 and 97). We then apply non-maxima suppression to suppress weak, overlapping bounding boxes (otherwise, there would be multiple bounding boxes for each detection). If you need more details on this code block, including how the decode_predictions function is implemented, be sure to review Chapter 18, Rotated Text Bounding Box Localization with OpenCV of the “Intro to OCR” Bundle, where I cover the EAST text detector in far more detail. After non-maximum suppression (NMS), we can now loop over each of the bounding boxes:

101 102 103 104 105 106 107 108 109

# ensure that at least one text bounding box was found if len(idxs) > 0: # loop over the valid bounding box indexes after applying NMS for i in idxs.flatten(): # compute the four corners of the bounding box, scale the # coordinates based on the respective ratios, and then # convert the box to an integer NumPy array box = cv2.boxPoints(rects[i]) box[:, 0] *= rW

234

110 111

Chapter 15. Improving Text Detection Speed with OpenCV and GPUs

box[:, 1] *= rH box = np.int0(box)

112 113 114

# draw a rotated bounding box around the text cv2.polylines(orig, [box], True, (0, 255, 0), 2)

115 116 117

# update the FPS counter fps.update()

118 119 120 121

# show the output frame cv2.imshow("Text Detection", orig) key = cv2.waitKey(1) & 0xFF

122 123 124 125

# if the `q` key was pressed, break from the loop if key == ord("q"): break

Line 102 verifies that at least one text bounding box was found, and if so, we loop over the indexes of the kept bounding boxes after applying NMS. For each resulting index, we compute the bounding box of the text ROI, scale the bounding box (x, y)-coordinates back to the orig input frame dimensions, and then draw the bounding box on the orig frame (Lines 108–114). Line 117 updates our FPS throughput estimator while Lines 120–125 display the output text detection on our screen. The final step here is to stop our FPS time, approximate the throughput rate, and release any video file pointers:

127 128 129 130

# stop the timer and display FPS information fps.stop() print("[INFO] elapsed time: {:.2f}".format(fps.elapsed())) print("[INFO] approx. FPS: {:.2f}".format(fps.fps()))

131 132 133 134

# if we are using a webcam, release the pointer if not args.get("input", False): vs.stop()

135 136 137 138

# otherwise, release the file pointer else: vs.release()

139 140 141

# close all windows cv2.destroyAllWindows()

Lines 128–130 stop our FPS timer and approximate the FPS of our text detection pipeline. We then release any video file pointers and close any windows opened by OpenCV.

15.3. Summary

15.2.5

235

GPU and OCR Results

This section needs to be executed locally on a machine with a GPU. After running the text_detection_video.py script on an NVIDIA RTX 2070 SUPER GPU (coupled with an i9 9900K processor), I obtained ≈ 97 FPS:

$ python text_detection_video.py --east ,→ ../models/east/frozen_east_text_detection.pb --use-gpu 1 [INFO] loading EAST text detector... [INFO] setting preferable backend and target to CUDA... [INFO] starting video stream... [INFO] elapsed time: 74.71 [INFO] approx. FPS: 96.80

When I ran the same script without using any GPU, I reached an FPS of ≈23, which is ≈77% slower than the above results.

$ python text_detection_video.py --east ,→ ../models/east/frozen_east_text_detection.pb [INFO] loading EAST text detector... [INFO] using CPU for inference... [INFO] starting video stream... [INFO] elapsed time: 68.59 [INFO] approx. FPS: 22.70

As you can see, using your GPU can dramatically improve the throughput speeed of your text detection pipeline!

15.3

Summary

In this chapter, you learned how to perform text detection in real-time video streams using your GPU. Since many text detection and OCR models are deep learning-based, using your GPU (versus your CPU) can tremendously increase your frame processing throughput rate. Using our CPU, we were able to process ≈22–23 FPS. However, by running the EAST model on OpenCV’s dnn module, we could reach ≈97 FPS! If you have a GPU available to you, definitely consider utilizing it — you’ll be able to run text detection models in real-time!

Chapter 16

Text Detection and OCR with Amazon Rekognition API So far, we’ve primarily focused on using the Tesseract OCR engine. However, other optical character recognition (OCR) engines are available, some of which are far more accurate than Tesseract and capable of accurately OCR’ing text, even in complex, unconstrained conditions. Typically, these OCR engines live in the cloud. Many are proprietary-based. To keep these models and associated datasets proprietary, the companies do not distribute the models themselves and instead put them behind a REST API. While these models do tend to be more accurate than Tesseract, there are some downsides, including: • An internet connection is required to OCR images — that’s less of an issue for most laptops/desktops, but if you’re working on the edge, an internet connection may not be possible • Additionally, if you are working with edge devices, then you may not want to spend the power draw on a network connection • There will be latency introduced by the network connection • OCR results will take longer because the image has to be packaged into an API request and uploaded to the OCR API. The API will need to chew on the image and OCR it, and then finally return the results to the client • Due to the latency and amount of time it will take to OCR each image, it’s doubtful that these OCR APIs will be able to run in real-time • They cost money (but typically offer a free trial or are free up to a number of monthly API requests) 237

238

Chapter 16. Text Detection and OCR with Amazon Rekognition API

Looking at the previous list, you may be wondering why on earth would we cover these APIs at all — what is the benefit? As you’ll see, the primary benefit here is accuracy. Consider the amount of data that Google and Microsoft have from running their respective search engines. Then consider the amount of data Amazon generates daily from simply printing shipping labels. These companies have an incredible amount of image data — and when they train their novel, state-of-the-art OCR models on their data, the result is an incredibly robust and accurate OCR model. In this chapter, you’ll learn how to use the Amazon Rekognition API to OCR images. We’ll then cover Microsoft Azure Cognitive Services in Chapter 17 and Google Cloud Vision API in Chapter 18.

16.1

Chapter Learning Objectives

In this chapter, you will: • Learn about the Amazon Rekognition API • Discover how the Amazon Rekognition API can be used for OCR • Obtain your Amazon Web Services (AWS) Rekognition Keys • Install Amazon’s boto3 package to interface with the OCR API • Implement a Python script that interfaces with Amazon Rekognition API to OCR an image

16.2

Amazon Rekognition API for OCR

The first part of this chapter will focus on obtaining your AWS Rekognition Keys. These keys will include a public access key and a secret key, similar to SSH, SFTP, etc. I’ll then show you how to install the boto3, the Amazon Web Services (AWS) software development kit (SDK) for Python. We’ll use the boto3 package to interface with Amazon Rekognition OCR API. Next, we’ll implement our Python configuration file (which will store our access key, secret key, and AWS region) and then create our driver script used to: i. Load an input image from disk

16.2. Amazon Rekognition API for OCR

239

ii. Package it into an API request iii. Send the API request to AWS Rekognition for OCR iv. Retrieve the results from the API call v. Display our OCR results We’ll wrap up this chapter with a discussion of our results.

16.2.1

Obtaining Your AWS Rekognition Keys

You will need additional information from our companion site for instructions on how to obtain your AWS Rekognition keys. You can find the instructions here: http://pyimg.co/vxd51.

16.2.2

Installing Amazon’s Python Package

To interface with the Amazon Rekognition API, we need to use the boto3 package: the AWS SDK. Luckily, boto3 is incredibly simple to install, requiring only a single pip-install command:

$ pip install boto3

If you are using a Python virtual environment or an Anaconda environment, be sure to use the appropriate command to access your Python environment before running the above command (otherwise, boto3 will be installed in the system install of Python).

16.2.3

Project Structure

Before we can perform text detection and OCR with Amazon Rekognition API, we first need to review our project directory structure.

|-| | |-| | | | |--

config |-- __init__.py |-- aws_config.py images |-- aircraft.png |-- challenging.png |-- park.png |-- street_signs.png amazon_ocr.py

240

Chapter 16. Text Detection and OCR with Amazon Rekognition API

The aws_config.py file contains our AWS access key, secret key, and region. You learned how to obtain these values in Section 16.2.1. Our amazon_ocr.py script will take this aws_config, connect to the Amazon Rekognition API, and then perform text detection and OCR to each image in our images directory.

16.2.4

Creating Our Configuration File

To connect to the Amazon Rekognition API, we first need to supply our access key, secret key, and region. If you haven’t yet obtained these keys, go to Section 16.2.1 and be sure to follow the steps and note the values. Afterward, you can come back here, open aws_config.py, and update the code:

1 2 3 4

# define our AWS Access Key, Secret Key, and Region ACCESS_KEY = "YOUR_ACCESS_KEY" SECRET_KEY = "YOUR_SECRET_KEY" REGION = "YOUR_AWS_REGION"

Sharing my API keys would be a security breach, so I’ve left placeholder values here. Be sure to update them with your API keys; otherwise, you will be unable to connect to the Amazon Rekognition API.

16.2.5

Implementing the Amazon Rekognition OCR Script

With our aws_config implemented, let’s move on to the amazon_ocr.py script, which is responsible for: i. Connecting to the Amazon Rekognition API ii. Loading an input image from disk iii. Packaging the image in an API request iv. Sending the package to Amazon Rekognition API for OCR v. Obtaining the OCR results from Amazon Rekognition API vi. Displaying our output text detection and OCR results Let’s get started with our implementation:

16.2. Amazon Rekognition API for OCR

1 2 3 4 5

241

# import the necessary packages from config import aws_config as config import argparse import boto3 import cv2

Lines 2–5 import our required Python packages. Notably, we need our aws_config (defined in the previous section), along with boto3, which is Amazon’s Python package to interface with their API. Let’s now define draw_ocr_results, a simple Python utility used to draw the output OCR results from Amazon Rekognition API:

7 8 9 10 11 12 13 14 15 16 17 18

def draw_ocr_results(image, text, poly, color=(0, 255, 0)): # unpack the bounding box, taking care to scale the coordinates # relative to the input image size (h, w) = image.shape[:2] tlX = int(poly[0]["X"] * w) tlY = int(poly[0]["Y"] * h) trX = int(poly[1]["X"] * w) trY = int(poly[1]["Y"] * h) brX = int(poly[2]["X"] * w) brY = int(poly[2]["Y"] * h) blX = int(poly[3]["X"] * w) blY = int(poly[3]["Y"] * h)

The draw_ocr_results function accepts four parameters: i. image: The input image that we’re drawing the OCR’d text on ii. text: The OCR’d text itself iii. poly: The polygon object/coordinates of the text bounding box returned by Amazon Rekognition API iv. color: The color of the bounding box Line 10 grabs the width and height of the image that we’re drawing on. Lines 11–18 then grab the bounding box coordinates of the text ROI, taking care to scale the coordinates by the width and height. Why do we perform this scaling process? Well, as we’ll find out later in this chapter, Amazon Rekognition API returns bounding boxes in the range [0, 1]. Multiplying the bounding boxes by the original image width and height brings the bounding boxes back to the original image scale.

242

Chapter 16. Text Detection and OCR with Amazon Rekognition API

From there, we can now annotate the image:

20 21 22 23 24 25 26

# build a list of points and use it to construct each vertex # of the bounding box pts = ((tlX, tlY), (trX, trY), (brX, brY), (blX, blY)) topLeft = pts[0] topRight = pts[1] bottomRight = pts[2] bottomLeft = pts[3]

27 28 29 30 31 32

# draw the bounding box of the detected text cv2.line(image, topLeft, topRight, color, 2) cv2.line(image, topRight, bottomRight, color, 2) cv2.line(image, bottomRight, bottomLeft, color, 2) cv2.line(image, bottomLeft, topLeft, color, 2)

33 34 35 36

# draw the text itself cv2.putText(image, text, (topLeft[0], topLeft[1] - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.8, color, 2)

37 38 39

# return the output image return image

Lines 22–26 builds a list of points which correspond to each vertex of the bounding box. Given the vertices, Lines 29–32 draw the bounding box of the rotated text. Lines 35 and 36 draw the OCR’d text itself. We then return the annotated output image to the calling function. With our helper utility defined, let’s move on to command line arguments:

41 42 43 44 45 46 47 48

# construct the argument parser and parse the arguments ap = argparse.ArgumentParser() ap.add_argument("-i", "--image", required=True, help="path to input image that we'll submit to AWS Rekognition") ap.add_argument("-t", "--type", type=str, default="line", choices=["line", "word"], help="output text type (either 'line' or 'word')") args = vars(ap.parse_args())

The --image command line argument corresponds to the path to the input image we want to submit to the Amazon Rekognition OCR API. The --type argument can be either line or word, indicating whether or not we want the Amazon Rekognition API to return OCR’d results grouped into lines or as individual words. Next, let’s connect to Amazon Web Services:

16.2. Amazon Rekognition API for OCR

50 51 52 53 54 55

243

# connect to AWS so we can use the Amazon Rekognition OCR API client = boto3.client( "rekognition", aws_access_key_id=config.ACCESS_KEY, aws_secret_access_key=config.SECRET_KEY, region_name=config.REGION)

56 57 58 59 60 61

# load the input image as a raw binary file and make a request to # the Amazon Rekognition OCR API print("[INFO] making request to AWS Rekognition API...") image = open(args["image"], "rb").read() response = client.detect_text(Image={"Bytes": image})

62 63 64 65 66

# grab the text detection results from the API and load the input # image again, this time in OpenCV format detections = response["TextDetections"] image = cv2.imread(args["image"])

67 68 69

# make a copy of the input image for final output final = image.copy()

Lines 51–55 connect to AWS. Here we supply our access key, secret key, and region. Once connected, we load our input image from disk as a binary object (Line 60) and then submit it to AWS by calling the detect_text function and supplying our image. Calling detect_text results in a response from the Amazon Rekognition API. We then grab the TextDetections results (Line 65). Line 66 loads the input --image from disk in OpenCV format, while Line 69 clones the image to draw on it. We can now loop over the text detection bounding boxes from the Amazon Rekognition API:

71 72 73 74 75 76

# loop over the text detection bounding boxes for detection in detections: # extract the OCR'd text, text type, and bounding box coordinates text = detection["DetectedText"] textType = detection["Type"] poly = detection["Geometry"]["Polygon"]

77 78 79 80 81 82 83 84

# only draw show the output of the OCR process if we are looking # at the correct text type if args["type"] == textType.lower(): # draw the output OCR line-by-line output = image.copy() output = draw_ocr_results(output, text, poly) final = draw_ocr_results(final, text, poly)

85 86

# show the output OCR'd line

244

Chapter 16. Text Detection and OCR with Amazon Rekognition API

print(text) cv2.imshow("Output", output) cv2.waitKey(0)

87 88 89 90 91 92 93

# show the final output image cv2.imshow("Final Output", final) cv2.waitKey(0)

Line 72 loops over all detections returned by Amazon’s OCR API. We then extract the OCR’d text, textType (either “word” or “line” ), along with the bounding box coordinates of the OCR’d text (Lines 74 and 75). Line 80 makes a check to verify whether we are looking at either word or line OCR’d text. If the current textType matches our --type command line argument, we call our draw_ocr_results function on both the output image and our final cloned image (Lines 82–84). Lines 87–89 display the current OCR’d line or word on our terminal and screen. That way, we can easily visualize each line or word without the output image becoming too cluttered. Finally, Lines 92 and 93 show the result of drawing all text on our screen at once (for visualization purposes).

16.2.6

Amazon Rekognition OCR Results

Congrats on implementing a Python script to interface with Amazon Rekognition’s OCR API! Let’s see our results in action, first by OCR’ing the entire image, line-by-line:

$ python amazon_ocr.py --image images/aircraft.png [INFO] making request to AWS Rekognition API... WARNING! LOW FLYING AND DEPARTING AIRCRAFT BLAST CAN CAUSE PHYSICAL INJURY

Figure 16.1 shows that we have OCR’d our input aircraft.png image line-by-line successfully, thereby demonstrating that the Amazon Rekognition API was able to: i. Locate each block of text in the input image ii. OCR each text ROI iii. Group the blocks of text into lines But what if we wanted to obtain our OCR results at the word level instead of the line level?

16.2. Amazon Rekognition API for OCR

That’s as simple as supplying the --type command line argument:

$ python amazon_ocr.py --image images/aircraft.png --type word [INFO] making request to AWS Rekognition API... WARNING! LOW FLYING AND DEPARTING AIRCRAFT BLAST CAN CAUSE PHYSICAL INJURY

Figure 16.1. OCR’ing images line-by-line using the Amazon Rekognition API.

245

246

Chapter 16. Text Detection and OCR with Amazon Rekognition API

As our output and Figure 16.2 shows, we’re now OCR’ing text at the word level. I’m a big fan of Amazon Rekognition’s OCR API. While AWS, EC2, etc., do have a bit of a learning curve, the benefit is that once you know it, you then understand Amazon’s entire web services ecosystem, which makes it far easier to start integrating different services. I also appreciate that it’s super easy to get started with Amazon Rekognition API. Other services (e.g., Google Cloud Vision API) make it a bit harder to get that “first easy win.” If this is your first foray into using cloud service APIs, definitely consider using Amazon Rekognition API first before moving on to Microsoft Cognitive Services or the Google Cloud Vision API.

Figure 16.2. OCR’ing an image word-by-word using the Amazon Rekognition OCR API.

16.3

Summary

In this chapter, you learned how to create your Amazon Rekognition keys, install boto3, a Python package used to interface with AWS, and then implemented a Python script to make calls to the Amazon Rekognition API. The Python script was simple and straightforward, requiring less than 100 lines to implement (including comments).

16.3. Summary

247

Not only were our Amazon Rekognition OCR API results correct, but we could also parse the results at both the line and word level, giving us finer granularity than what the EAST text detection model and Tesseract OCR engine give us (at least without fine-tuning several options). In Chapter 17, we’ll look at the Microsoft Cognitive Services API for OCR.

Chapter 17

Text Detection and OCR with Microsoft Cognitive Services In our previous chapter, you learned how to use the Amazon Rekognition API to OCR images. The hardest part of using the Amazon Rekognition API was obtaining your API keys. Once you had your API keys, it was smooth sailing. This chapter focuses on a different cloud-based API called Microsoft Cognitive Services (MCS), part of Microsoft Azure. Like Amazon Rekognition API, MCS is also capable of high OCR accuracy — but unfortunately, the implementation is slightly more complex (as is both Microsoft’s login and admin dashboard). I prefer the Amazon Web Services (AWS) Rekognition API over MCS, both for the admin dashboard and the API itself. However, if you are already ingrained into the MCS/Azure ecosystem, you should consider staying there. The MCS API isn’t that hard to use (it’s just not as easy and straightforward as Amazon Rekognition API).

17.1

Chapter Learning Objectives

In this chapter, you will: • Learn how to obtain your MCS API keys • Create a configuration file to store your subscription key and API endpoint URL • Implement a Python script to make calls to the MCS OCR API • Review the results of applying the MCS OCR API to sample images

249

250

17.2

Chapter 17. Text Detection and OCR with Microsoft Cognitive Services

Microsoft Cognitive Services for OCR

We’ll start this chapter with a review of how you can obtain your MCS API keys. You will need these API keys to make requests to the MCS API to OCR images. Once we have our API keys, we’ll review our project directory structure and then implement a Python configuration file to store our subscription key and OCR API endpoint URL. With our configuration file implemented, we’ll move on to creating a second Python script, this one acting as a driver script that: • Imports our configuration file • Loads an input image to disk • Packages the image into an API call • Makes a request to the MCS OCR API • Retrieves the results • Annotates our output image • Displays the OCR results to our screen and terminal Let’s dive in!

17.2.1

Obtaining Your Microsoft Cognitive Services Keys

Before proceeding to the rest of the sections, be sure to obtain the API keys by following the instructions shown here: pyimg.co/53rux.

17.2.2

Project Structure

The directory structure for our MCS OCR API is similar to the structure of the Amazon Rekognition API project in Chapter 16: |-- config | |-- __init__.py |-- microsoft_cognitive_services.py |-- images | |-- aircraft.png | |-- challenging.png | |-- park.png | |-- street_signs.png |-- microsoft_ocr.py

17.2. Microsoft Cognitive Services for OCR

251

Inside the config, we have our microsoft_cognitive_services.py file, which stores our subscription key and endpoint URL (i.e., the URL of the API we’re submitting our images to). The microsoft_ocr.py script will take our subscription key and endpoint URL, connect to the API, submit the images in our images directory for OCR, and then display our screen results.

17.2.3

Creating Our Configuration File

Ensure you have followed Section 17.2.1 to obtain your subscription keys to the MCS API. From there, open the microsoft_cognitive_services.py file and update your SUBSCRPTION_KEY:

# define our Microsoft Cognitive Services subscription key SUBSCRIPTION_KEY = "YOUR_SUBSCRIPTION_KEY" # define the ACS endpoint ENDPOINT_URL = "YOUR_ENDPOINT_URL"

You should replace the string "YOUR_SUBSCRPTION_KEY" with your subscription key obtained from Section 17.2.1. Additionally, ensure you double-check your ENDPOINT_URL. At the time of this writing, the endpoint URL points to the most recent version of the MCS API; however, as Microsoft releases new versions of the API, this endpoint URL may change, so it’s worth double-checking.

17.2.4

Implementing the Microsoft Cognitive Services OCR Script

Let’s now learn how to submit images for text detection and OCR to the MCS API. Open the microsoft_ocr.py script in the project directory structure and insert the following code:

1 2 3 4 5 6 7

# import the necessary packages from config import microsoft_cognitive_services as config import requests import argparse import time import sys import cv2

252

Chapter 17. Text Detection and OCR with Microsoft Cognitive Services

Note on Line 2 that we import our microsoft_cognitive_services configuration to supply our subscription key and endpoint URL. We’ll use the requests Python package to send requests to the API. Next, let’s define draw_ocr_results, a helper function used to annotate our output images with the OCR’d text:

9 10 11 12 13 14

def draw_ocr_results(image, text, pts, color=(0, 255, 0)): # unpack the points list topLeft = pts[0] topRight = pts[1] bottomRight = pts[2] bottomLeft = pts[3]

15 16 17 18 19 20

# draw the bounding box of the detected text cv2.line(image, topLeft, topRight, color, 2) cv2.line(image, topRight, bottomRight, color, 2) cv2.line(image, bottomRight, bottomLeft, color, 2) cv2.line(image, bottomLeft, topLeft, color, 2)

21 22 23 24

# draw the text itself cv2.putText(image, text, (topLeft[0], topLeft[1] - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.8, color, 2)

25 26 27

# return the output image return image

Our draw_ocr_results function has four parameters: i. image: The input image that we are going to draw on. ii. text: The OCR’d text. iii. pts: The top-left, top-right, bottom-right, and bottom-left (x, y)-coordinates of the text ROI iv. color: The BGR color we’re using to draw on the image Lines 11–14 unpack our bounding box coordinates. From there, Lines 17–20 draw the bounding box surrounding the text in the image. We then draw the OCR’d text itself on Lines 23–24. We wrap up this function by returning the output image to the calling function. We can now parse our command line arguments:

29 30

# construct the argument parser and parse the arguments ap = argparse.ArgumentParser()

17.2. Microsoft Cognitive Services for OCR

31 32 33

253

ap.add_argument("-i", "--image", required=True, help="path to input image that we'll submit to Microsoft OCR") args = vars(ap.parse_args())

34 35 36 37 38

# load the input image from disk, both in a byte array and OpenCV # format imageData = open(args["image"], "rb").read() image = cv2.imread(args["image"])

We only need a single argument here, --image, which is the path to the input image on disk. We read this image from disk, both as a binary byte array (so we can submit it to the MCS API, and then again in OpenCV/NumPy format (so we can draw on/annotate it). Let’s now construct a request to the MCS API:

40 41 42 43 44 45 46

# construct our headers dictionary that will include our Microsoft # Cognitive Services API Key (required in order to submit requests # to the API) headers = { "Ocp-Apim-Subscription-Key": config.SUBSCRIPTION_KEY, "Content-Type": "application/octet-stream", }

47 48 49 50 51 52 53

# make the request to the Azure Cognitive Services API and wait for # a response print("[INFO] making request to Microsoft Cognitive Services API...") response = requests.post(config.ENDPOINT_URL, headers=headers, data=imageData) response.raise_for_status()

54 55 56

# initialize whether or not the API request was a success success = False

Lines 43–46 define our headers dictionary. Note that we are supplying our SUBSCRIPTION _KEY here — now is a good time to go back to microsoft_cognitive_services.py and ensure you have correctly inserted your subscription key (otherwise, the request to the MCS API will fail). We then submit the image for OCR to the MCS API on Lines 51–53. We also initialize a Boolean, success, to indicate if submitting the request was successful or not. We now have to wait and poll for results from the MCS API:

58 59 60 61

# continue to poll the Cognitive Services API for a response until # either (1) we receive a "success" response or (2) the request fails while True: # check for a response and convert it to a JSON object

254

62 63 64 65

Chapter 17. Text Detection and OCR with Microsoft Cognitive Services

responseFinal = requests.get( response.headers["Operation-Location"], headers=headers) result = responseFinal.json()

66 67 68 69 70

# if the results are available, stop the polling operation if "analyzeResult" in result.keys(): success = True break

71 72 73 74

# check to see if the request failed if "status" in result.keys() and result["status"] == "failed": break

75 76 77

# sleep for a bit before we make another request to the API time.sleep(1.0)

78 79 80 81 82 83

# if the request failed, show an error message and exit if not success: print("[INFO] Microsoft Cognitive Services API request failed") print("[INFO] Attempting to gracefully exit") sys.exit(0)

I’ll be honest — polling for results is not my favorite way to work with an API. It requires more code, it’s a bit more tedious, and it can be potentially error-prone if the programmer isn’t careful to break out of the loop properly. Of course, there are pros to this approach, including maintaining a connection, submitting larger chunks of data, and having results returned in batches rather than all at once. Regardless, this is how Microsoft has implemented its API, so we must play by their rules. Line 60 starts a while loop that continuously checks for responses from the MCS API (Lines 62–65). If we find the text "analyzeResult" in the result dictionary’s keys, we can safely break from the loop and process our results (Lines 68–70). Otherwise, if we find the string "status" in the result dictionary and the status is "failed", then the API request failed (and we should break from the loop). If neither of these conditions is met, we sleep for a small amount of time and then poll again. Lines 80–83 handle the case in which we could not obtain a result from the MCS API. If that happens, then we have no OCR results to show (since the image could not be processed), and then we exit gracefully from our script. Provided our OCR request succeeded, let’s now process the results:

17.2. Microsoft Cognitive Services for OCR

85 86

255

# grab all OCR'd lines returned by Microsoft's OCR API lines = result["analyzeResult"]["readResults"][0]["lines"]

87 88 89

# make a copy of the input image for final output final = image.copy()

90 91 92 93 94 95 96 97 98

# loop over the lines for line in lines: # extract the OCR'd line from Microsoft's API and unpack the # bounding box coordinates text = line["text"] box = line["boundingBox"] (tlX, tlY, trX, trY, brX, brY, blX, blY) = box pts = ((tlX, tlY), (trX, trY), (brX, brY), (blX, blY))

99

# draw the output OCR line-by-line output = image.copy() output = draw_ocr_results(output, text, pts) final = draw_ocr_results(final, text, pts)

100 101 102 103 104

# show the output OCR'd line print(text) cv2.imshow("Output", output) cv2.waitKey(0)

105 106 107 108 109 110 111 112

# show the final output image cv2.imshow("Final Output", final) cv2.waitKey(0)

Line 86 grabs all OCR’d lines returned by the MCS API. Line 89 initializes our final output image with all text drawn on it. We start looping through all lines of OCR’d text on Line 92. We extract the OCR’d text and bounding box coordinates for each line, followed by constructing a list of the top-left, top-right, bottom-right, and bottom-left corners, respectively (Lines 95–98). We then draw the OCR’d text line-by-line on the output and final image (Lines 101–103). We display the current line of text on our screen and terminal (Lines 106–108) — the final output image, with all OCR’d text drawn on it, is displayed on Lines 111–112.

17.2.5

Microsoft Cognitive Services OCR Results

Let’s now put the MCS OCR API to work for us. Open a terminal and execute the following command:

$ python microsoft_ocr.py --image images/aircraft.png [INFO] making request to Microsoft Cognitive Services API...

256

Chapter 17. Text Detection and OCR with Microsoft Cognitive Services

WARNING! LOW FLYING AND DEPARTING AIRCRAFT BLAST CAN CAUSE PHYSICAL INJURY

Figure 17.1 shows the output of applying the MCS OCR API to our aircraft warning sign. If you recall, this is the same image we used in Chapter 16 when applying the Amazon Rekognition API [58]. I included the same image here in this chapter to demonstrate that the MCS OCR API can correctly OCR this image.

Figure 17.1. Image of a plane flying over a warning sign. OCR results displayed on the sign in green.

Let’s try a different image, this one containing several challenging pieces of text:

$ python microsoft_ocr.py --image images/challenging.png [INFO] making request to Microsoft Cognitive Services API... LITTER EMERGENCY First

17.2. Microsoft Cognitive Services for OCR

257

Eastern National Bus Times STOP

Figure 17.2 shows the results of applying the MCS OCR API to our input image — and as we can see, MCS does a great job OCR’ing the image.

Figure 17.2. Left: Sample text from the First Eastern National bus timetable. The sample text is a tough image to OCR due to the low image quality and glossy print. Still, Microsoft’s OCR API can correctly OCR it! Middle: The Microsoft OCR API can correctly OCR the “Emergency Stop” text. Right: A trashcan with the text “Litter.” We’re able to OCR the text, but the text at the bottom of the trashcan is unreadable, even to the human eye.

On the left, we have a sample image from the First Eastern National bus timetable (i.e., schedule of when a bus will arrive). The document is printed with a glossy finish (likely to prevent water damage). Still, due to the gloss, there is a significant reflection in the image, particularly in the “Bus Times” text. Still, the MCS OCR API can correctly OCR the image. In the middle, the “Emergency Stop” text is highly pixelated and low-quality, but that doesn’t phase the MCS OCR API! It’s able to correctly OCR the image. Finally, the right shows a trashcan with the text “Litter.” The text is tiny, and due to the low-quality image, it is challenging to read without squinting a bit. That said, the MCS OCR API can still OCR the text (although the text at the bottom of the trashcan is illegible — neither human nor API could read that text). The next sample image contains a national park sign shown in Figure 17.3:

$ python microsoft_ocr.py --image images/park.png [INFO] making request to Microsoft Cognitive Services API...

258

Chapter 17. Text Detection and OCR with Microsoft Cognitive Services

PLEASE TAKE NOTHING BUT PICTURES LEAVE NOTHING BUT FOOT PRINTS

Figure 17.3. OCR’ing a park sign using the Microsoft Cognitive Services OCR API. Notice that API can give us rotated text bounding boxes along with the OCR’d text itself.

The MCS OCR API can OCR each sign, line-by-line (Figure 17.3). Note that we’re also able to compute rotated text bounding box/polygons for each line. The final example we have contains traffic signs:

$ python microsoft_ocr.py --image images/street_signs.png [INFO] making request to Microsoft Cognitive Services API... Old Town Rd

17.3. Summary

259

STOP ALL WAY

Figure 17.4 shows that we can correctly OCR each piece of text on both the stop sign and street name sign.

Figure 17.4. The Microsoft Cognitive Services OCR API can detect the text on traffic signs.

17.3

Summary

In this chapter, you learned about Microsoft Cognitive Services (MCS) OCR API. Despite being slightly harder to implement and use than the Amazon Rekognition API, the Microsoft Cognitive Services OCR API demonstrated that it’s quite robust and able to OCR text in many situations, including low-quality images. When working with low-quality images, the MCS API shined. Typically, I would recommend that you programmatically detect and discard low-quality images (as we did in Chapter 13). However, if you find yourself in a situation where you have to work with low-quality images, it may be worth your while to use the Microsoft Azure Cognitive Services OCR API.

Chapter 18

Text Detection and OCR with Google Cloud Vision API In our previous two chapters, we learned how to use the cloud-based optical character recognition (OCR) APIs, Amazon Rekognition API, and Microsoft Cognitive Services (MCS). Amazon Rekognition API required less code and was a bit more straightforward to implement; however, MCS showed its utility could correctly OCR low-quality images. This chapter will look at one final cloud-based OCR service, the Google Cloud Vision API. In terms of code, the Google Cloud Vision API is easy to utilize. Still, it requires that we use their admin panel to generate a client JavaScript Object Notation (JSON) file that contains all the necessary information to access the Vision API. I have mixed feelings about the JSON file. On the one hand, it’s nice not to have to hardcode our private and public keys. But on the other hand, it’s cumbersome to have to use the admin panel to generate the JSON file itself. Realistically, it’s a situation of “six of one, half a dozen of the other.” It doesn’t make that much of a difference (just something to be aware of). And as we’ll find out, the Google Cloud Vision API, just like the others, tends to be quite accurate and does a good job OCR’ing complex images. Let’s dive in!

18.1

Chapter Learning Objectives

In this chapter, you will: 261

262

Chapter 18. Text Detection and OCR with Google Cloud Vision API

• Learn how to obtain your Google Cloud Vision API keys/JSON configuration file from the Google cloud admin panel • Configure your development environment for use with the Google Cloud Vision API • Implement a Python script used to make requests to the Google Cloud Vision API

18.2

Google Cloud Vision API for OCR

In the first part of this chapter, you’ll learn about the Google Cloud Vision API and how to obtain your API keys and generate your JSON configuration file for authentication with the API. From there, we’ll be sure to have your development environment correctly configured with the required Python packages to interface with the Google Cloud Vision API. We’ll then implement a Python script that takes an input image, packages it within an API request, and sends it to the Google Cloud Vision API for OCR. We’ll wrap up this chapter with a discussion of our results.

18.2.1 18.2.1.1

Obtaining Your Google Cloud Vision API Keys Prerequisite

A Google Cloud account with billing enabled is all you’ll need to use the Google Cloud Vision API. You can find the Google Cloud guide on how to modify your billing settings here: http://pyimg.co/y0a0d.

18.2.1.2

Steps to Enable Google Cloud Vision API and Download Credentials

You can find our guide to getting your keys at the following site: http://pyimg.co/erftn. These keys are required if you want to follow this chapter or use the Google Cloud Vision API in your projects.

18.2.2

Configuring Your Development Environment for the Google Cloud Vision API

If you have not already installed the google-cloud-vision Python package in your Python development environment, take a second to do so now:

18.2. Google Cloud Vision API for OCR

263

$ pip install --upgrade google-cloud-vision

If you are using a Python virtual environment or an Anaconda package manager, be sure to use the appropriate command to access your Python environment before running the above pip-install command. Otherwise, the google-cloud-vision package will be installed in your system Python rather than your Python environment.

18.2.3

Project Structure

Let’s inspect the project directory structure for our Google Cloud Vision API OCR project:

|-| | | |-|--

images |-- aircraft.png |-- challenging.png |-- street_signs.png client_id.json google_ocr.py

We will apply our google_ocr.py script to several examples in the images directory. The client_id.json file provides all necessary credentials and authentication information. The google_ocr.py script will load this file and supply it to the Google Cloud Vision API to perform OCR.

18.2.4

Implementing the Google Cloud Vision API Script

With our project directory structure reviewed, we can move on to implementing google_ocr.py, the Python script responsible for: i. Loading the contents of our client_id.json file ii. Connecting to the Google Cloud Vision API iii. Loading and submitting our input image to the API iv. Retrieving the text detection and OCR results v. Drawing and displaying the OCR’d text to our screen Let’s dive in:

264

1 2 3 4 5 6

Chapter 18. Text Detection and OCR with Google Cloud Vision API

# import the necessary packages from google.oauth2 import service_account from google.cloud import vision import argparse import cv2 import io

Lines 2–6 import our required Python packages. Note that we need the service_account to connect to the Google Cloud Vision API while the vision package contains the text_detection function responsible for OCR. Next, we have draw_ocr_results, a convenience function used to annotate our output image:

8 9 10 11 12 13 14

def draw_ocr_results(image, text, rect, color=(0, 255, 0)): # unpacking the bounding box rectangle and draw a bounding box # surrounding the text along with the OCR'd text itself (startX, startY, endX, endY) = rect cv2.rectangle(image, (startX, startY), (endX, endY), color, 2) cv2.putText(image, text, (startX, startY - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.8, color, 2)

15 16 17

# return the output image return image

The draw_ocr_results function accepts four parameters: i. image: The input image we are drawing on ii. text: The OCR’d text iii. rect: The bounding box coordinates of the text ROI iv. color: The color of the drawn bounding box and text Line 11 unpacks the (x, y)-coordinates of our text ROI. We use these coordinates to draw a bounding box surrounding the text along with the OCR’d text itself (Lines 12–14). We then return the image to the calling function. Let’s examine our command line arguments:

19 20 21

# construct the argument parser and parse the arguments ap = argparse.ArgumentParser() ap.add_argument("-i", "--image", required=True,

18.2. Google Cloud Vision API for OCR

22 23 24 25

265

help="path to input image that we'll submit to Google Vision API") ap.add_argument("-c", "--client", required=True, help="path to input client ID JSON configuration file") args = vars(ap.parse_args())

We have two command line arguments here: • --image: The path to the input image that we’ll be submitting to the Google Cloud Vision API for OCR. • --client: The client ID JSON file containing our authentication information (be sure to follow Section 18.2.1 to generate this JSON file). It’s time to connect to the Google Cloud Vision API:

27 28 29 30 31

# create the client interface to access the Google Cloud Vision API credentials = service_account.Credentials.from_service_account_file( filename=args["client"], scopes=["https://www.googleapis.com/auth/cloud-platform"]) client = vision.ImageAnnotatorClient(credentials=credentials)

32 33 34 35 36

# load the input image as a raw binary file (this file will be # submitted to the Google Cloud Vision API) with io.open(args["image"], "rb") as f: byteImage = f.read()

Lines 28–30 connect to the Google Cloud Vision API, supplying the --client path to the JSON authentication file on disk. Line 31 then creates our client for all image processing/computer vision operations. We then load our input --image from disk as a byte array (byteImage) to submit it to Google Cloud Vision API. Let’s submit our byteImage to the API now:

38 39 40 41 42

# create an image object from the binary file and then make a request # to the Google Cloud Vision API to OCR the input image print("[INFO] making request to Google Cloud Vision API...") image = vision.Image(content=byteImage) response = client.text_detection(image=image)

43 44 45 46 47 48 49

# check to see if there was an error when making a request to the API if response.error.message: raise Exception( "{}\nFor more info on errors, check:\n" "https://cloud.google.com/apis/design/errors".format( response.error.message))

266

Chapter 18. Text Detection and OCR with Google Cloud Vision API

Line 41 creates an Image data object, which is then submitted to the text_detection function of the Google Cloud Vision API (Line 42). Lines 45–49 check to see if there was an error OCR’ing our input image and if so, we raise the error and exit from the script. Otherwise, we can process the results of the OCR step: 51 52 53 54

# read the image again, this time in OpenCV format and make a copy of # the input image for final output image = cv2.imread(args["image"]) final = image.copy()

55 56 57 58 59 60 61 62 63 64

# loop over the Google Cloud Vision API OCR results for text in response.text_annotations[1::]: # grab the OCR'd text and extract the bounding box coordinates of # the text region ocr = text.description startX = text.bounding_poly.vertices[0].x startY = text.bounding_poly.vertices[0].y endX = text.bounding_poly.vertices[1].x endY = text.bounding_poly.vertices[2].y

65 66 67

# construct a bounding box rectangle from the box coordinates rect = (startX, startY, endX, endY)

Line 53 loads our input image from disk in OpenCV/NumPy array format (so that we can draw on it). Line 57 loops over all OCR’d text from the Google Cloud Vision API response. Line 60 extracts the ocr text itself, while Lines 61–64 extract the text region’s bounding box coordinates. Line 67 then constructs a rectangle (rect) from these coordinates. The final step is to draw the OCR results on the output and final images: 69 70 71 72

# draw the output OCR line-by-line output = image.copy() output = draw_ocr_results(output, ocr, rect) final = draw_ocr_results(final, ocr, rect)

73 74 75 76 77

# show the output OCR'd line print(ocr) cv2.imshow("Output", output) cv2.waitKey(0)

78 79 80 81

# show the final output image cv2.imshow("Final Output", final) cv2.waitKey(0)

Each piece of OCR’d text is displayed on our screen on Lines 75–77. The final image, with all OCR’d text, is displayed on Lines 80 and 81.

18.2. Google Cloud Vision API for OCR

18.2.5

267

Google Cloud Vision API OCR Results

Let’s now put the Google Cloud Vision API to work! Open a terminal and execute the following command:

$ python google_ocr.py --image images/aircraft.png --client client_id.json [INFO] making request to Google Cloud Vision API... WARNING! LOW FLYING AND DEPARTING AIRCRAFT BLAST CAN CAUSE PHYSICAL INJURY

Figure 18.1 shows the results of applying the Google Cloud Vision API to our aircraft image, the same image we have been benchmarking OCR performance across all three cloud services. Like Amazon Rekognition API and Microsoft Cognitive Services, the Google Cloud Vision API can correctly OCR the image.

Figure 18.1. OCR’ing a warning sign line-by-line using the Google Cloud Vision API.

268

Chapter 18. Text Detection and OCR with Google Cloud Vision API

Let’s try a more challenging image, which you can see in Figure 18.2:

$ python google_ocr.py --image images/challenging.png --client client_id.json [INFO] making request to Google Cloud Vision API... LITTER First Eastern National Bus Fimes EMERGENCY STOP

Figure 18.2. OCR’ing a more challenging example with the Google Cloud Vision API. The OCR results are nearly 100% correct, the exception being that the API was thinking that the “T” in “Times” is an “F.”

Just like the Microsoft Cognitive Services API, the Google Cloud Vision API performs well on our challenging, low-quality image with pixelation and low readability (even by human standards, let alone a machine). The results are in Figure 18.2. Interestingly, the Google Cloud Vision API does make a mistake, thinking that the “T” in “Times” is an “F.” Let’s look at one final image, this one of a street sign:

$ python google_ocr.py --image images/street_signs.png --client client_id.json [INFO] making request to Google Cloud Vision API... Old Town Rd STOP ALL WAY

18.3. Summary

269

Figure 18.3 displays the output of applying the Google Cloud Vision API to our street sign image. Microsoft Cognitive Services API OCRs the image line-by-line, resulting in the text “Old Town Rd” and “All Way” to be OCR’d as a single line. Alternatively, Google Cloud Vision API OCRs the text word-by-word (the default setting in the Google Cloud Vision API).

Figure 18.3. The Google Cloud Vision API OCRs our street signs but, by default, returns the results word-by-word.

18.3

Summary

In this chapter, you learned how to utilize the cloud-based Google Cloud Vision API for OCR. Like the other cloud-based OCR APIs we’ve covered in this book, the Google Cloud Vision API can obtain high OCR accuracy with little effort. The downside, of course, is that you need an internet connection to leverage the API. When choosing a cloud-based API, I wouldn’t focus on the amount of Python code required to interface with the API. Instead, consider the overall ecosystem of the cloud platform you are using. Suppose you’re building an application that requires you to interface with Amazon Simple Storage Service (Amazon S3) for data storage. In that case, it makes a lot more sense to use Amazon Rekognition API. This enables you to keep everything under the Amazon umbrella. On the other hand, if you are using the Google Cloud Platform (GCP) instances to train deep learning models in the cloud, it makes more sense to use the Google Cloud Vision API.

270

Chapter 18. Text Detection and OCR with Google Cloud Vision API

These are all design and architectural decisions when building your application. Suppose you’re just “testing the waters” of each of these APIs. You are not bound to these considerations. However, if you’re developing a production-level application, then it’s well worth your time to consider the trade-offs of each cloud service. You should consider more than just OCR accuracy; consider the compute, storage, etc., services that each cloud platform offers.

Chapter 19

Training a Custom Tesseract Model So far, in this text, we have learned how to create OCR models by: • Leveraging basic computer vision and image processing techniques to detect and extract characters • Utilizing OpenCV to recognize extracted characters • Training custom Keras/TensorFlow models • Using Tesseract to detect and recognize text within images However, we have yet to train/fine-tune a Tesseract model on our custom dataset. This chapter, along with the next, takes us deep down the rabbit hole into the world of Tesseract training commands, tools, and proper dataset/project directory structures. As you’ll see, training a custom Tesseract model has absolutely nothing to do with programming and writing code and instead involves: i. Properly configuring your development environment with the required Tesseract training tools ii. Structuring your OCR dataset in a manner Tesseract can understand iii. Correctly setting Tesseract environment variables in your shell iv. Running the correct Tesseract training commands Suppose you’ve ever worked with Caffe [59] or the TensorFlow Object Detection API [60], then you know these tools typically don’t require you to open an integrated development environment (IDE) and write hundreds of lines of code. 271

272

Chapter 19. Training a Custom Tesseract Model

Instead, they are far more focused on ensuring the dataset is correctly structured, followed by issuing a series of (sometimes complicated) commands. Training a custom Tesseract model is the same — and in the remainder of this chapter, you will learn how to fine-tune a Tesseract model on a custom dataset.

19.1

Chapter Learning Objectives

In this chapter, you will learn how to: i. Configure your development environment for Tesseract training ii. Install the tesstrain package, which utilizes make files to construct training commands and commence training easily iii. Understand the dataset directory structure required when training a custom Tesseract model iv. Review the dataset we’ll be using when training our Tesseract model v. Configure the base Tesseract model that we’ll be training vi. Train the actual Tesseract OCR model on our custom dataset

19.2

Your Tesseract Training Development Environment

In the first part of this section, we’ll configure our system with the required packages to train a custom Tesseract model. Next, we’ll compile Tesseract from source. Compiling Tesseract from source is a requirement when training a custom Tesseract OCR model. The tesseract binary installed via apt-get or brew will not be sufficient. Finally, we’ll install tesstrain, a set of Python and makefile utilities to train the custom Tesseract model. If possible, I suggest using a virtual machine (VM) or a cloud-based instance the first time you configure a system to train a Tesseract model. The reasoning behind this suggestion is simple — you will make a mistake once, twice, and probably even three times before you properly configure your environment. Configuring Tesseract to train a custom model for the first time is hard, so having an environment where you can snapshot a base operating system install, do your work with

19.2. Your Tesseract Training Development Environment

273

Tesseract, and when something goes wrong, instead of having to reconfigure your bare metal machine, you can instead revert to your previous state. I genuinely hope you take my advice — this process is not for the faint of heart. You need extensive Unix/Linux command line experience to train custom Tesseract models due to the very nature that everything we are doing in this chapter involves the command line. If you are new to the command line, or you don’t have much experience working with package managers such as apt-get (Linux) or brew (macOS), I suggest you table this chapter for a bit while you improve your command line skills. With all that said, let’s start configuring our development environment such that we can train custom Tesseract models.

19.2.1

Step #1: Installing Required Packages

This section provides instructions to install operating-system-level packages required to (1) compile Tesseract from source and (2) train custom OCR models with Tesseract. Unfortunately, compiling Tesseract from source is a requirement to use the training utilities. Luckily, installing our required packages is as simple as a few apt-get and brew commands.

19.2.1.1

Ubuntu

The following set of commands installs various C++ libraries, compiles (g++), package managers (git), image I/O utilities (so we can load images from disk), and other tools required to compile Tesseract from source:

$ sudo apt-get install $ sudo apt-get install ,→ make pkg-config $ sudo apt-get install $ sudo apt-get install

libicu-dev libpango1.0-dev libcairo2-dev automake ca-certificates g++ git libtool libleptonica-dev --no-install-recommends asciidoc docbook-xsl xsltproc libpng-dev libjpeg8-dev libtiff5-dev zlib1g-dev

Assuming none of these commands error out, your Ubuntu machine should have the proper OS-level packages installed.

19.2.1.2

macOS

If you’re on a macOS machine, installing the required OS-level packages is almost as easy — all we need is to use Homebrew (https://brew.sh [61]) and the brew command:

274

Chapter 19. Training a Custom Tesseract Model

$ brew install icu4c pango cairo $ brew install automake gcc git libtool leptonica make pkg-config $ brew install libpng jpeg libtiff zlib

However, there is an additional step required. The icu4c package, which contains C/C++ and Java libraries for working with Unicode, requires that we manually create a symbolic link (sym-link) for it into our /usr/local/opt directory:

$ ln -hfs /usr/local/Cellar/icu4c/67.1 /usr/local/opt/icu4c

Note that the current version (as of this writing) for icu4c is 67.1. I suggest you use tab completion when running the command above. New versions of icu4c will require the command to be updated to utilize the correct version (otherwise, the path will be invalid, and thus the sym-link will be invalid as well).

19.2.2

Step #2: Building and Installing Tesseract Training Tools

Now that our OS-level packages are installed, we can move on to compiling Tesseract from source, giving us access to Tesseract’s training utilities.

19.2.2.1

Ubuntu

On an Ubuntu machine, you can use the following set of commands to: i. Download Tesseract v4.1.1 ii. Configure the build iii. Compile Tesseract iv. Install Tesseract and it’s training utilities The commands follow:

$ $ $ $ $ $ $ $

wget -O tesseract.zip https://github.com/tesseract-ocr/tesseract/archive/4.1.1.zip unzip tesseract.zip mv tesseract-4.1.1 tesseract cd tesseract ./autogen.sh ./configure make sudo make install

19.2. Your Tesseract Training Development Environment

275

$ sudo ldconfig $ make training $ sudo make training-install

Provided that you configured your Ubuntu development environment correctly in Section 19.2.1.1, you shouldn’t have any issues compiling Tesseract from source. However, if you run into an error message, don’t worry! All hope is not lost yet. Keep in mind what I said in this chapter’s introduction — training a custom Tesseract model is hard work. The majority of the effort involves configuring your development environment. Once your development environment is configured correctly, training your Tesseract model becomes a breeze. Tesseract is the world’s most popular open-source OCR engine. Someone else has likely encountered the same error as you have. Copy and paste your error message in Google and start reading GitHub Issues threads. It may take some time and patience, but you will eventually find the solution. Lastly, we will be downloading the English language and OSD Tesseract model files that would otherwise be installed automatically if you were installing Tesseract using apt-get: $ cd /usr/local/share/tessdata/ $ sudo wget ,→ https://github.com/tesseract-ocr/tessdata_fast/raw/master/eng.traineddata $ sudo wget ,→ https://github.com/tesseract-ocr/tessdata_fast/raw/master/osd.traineddata

19.2.2.2

macOS

Installing Tesseract on macOS has a near identical set of commands as our Ubuntu install: $ $ $ $ $ $ $ $ $ $ $

wget -O tesseract.zip https://github.com/tesseract-ocr/tesseract/archive/4.1.1.zip unzip tesseract.zip mv tesseract-4.1.1 tesseract cd tesseract ./autogen.sh export PKG_CONFIG_PATH="/usr/local/opt/icu4c/lib/pkgconfig" ./configure make sudo make install make training sudo make training-install

The big difference here is that we need to explicitly set our PKG_CONFIG_PATH environment variable to point to the icu4c sym-link we created in Section 19.2.1.2.

276

Chapter 19. Training a Custom Tesseract Model

Do not forget this export command — if you do, your Tesseract compile will error out. Lastly, we will be downloading the English language and OSD Tesseract model files that would otherwise be installed automatically if you were installing Tesseract using brew:

$ cd /usr/local/share/tessdata/ $ sudo wget ,→ https://github.com/tesseract-ocr/tessdata_fast/raw/master/eng.traineddata $ sudo wget ,→ https://github.com/tesseract-ocr/tessdata_fast/raw/master/osd.traineddata

19.2.3

Step #3: Cloning tesstrain and Installing Requirements

At this point, you should have Tesseract v4.1.1, compiled from source, installed on your system. The next step is installing tesstrain, a set of Python tools that allow us to work with make files to train custom Tesseract models. Using tesstrain (http://pyimg.co/n6pdk [62]), we can train and fine-tune Tesseract’s deep learning-based LSTM models (the same model we’ve used to obtain high accuracy OCR results throughout this text). A detailed review of the LSTM architecture is outside the scope of this text, but if you need a refresher, I recommend this fantastic intro tutorial by Christopher Olah (http://pyimg.co/9hmte [63]). Perhaps most importantly, the tesstrain package provides instructions on how our training dataset should be structured, making it far easier to build our dataset and train the model. Let’s install the required Python packages required by tesstrain now:

$ $ $ $ $

cd ~ git clone https://github.com/tesseract-ocr/tesstrain workon ocr cd tesstrain pip install -r requirements.txt

As you can see, installing tesstrain follows a basic setup.py install. All we have to do is clone the repository, change directory, and install the packages. I am using the workon command to access my ocr Python virtual environment, thus keeping my Tesseract training Python libraries separate from my system Python packages. Using Python virtual environments is the best practice and one I recommend you use. Still, if

19.3. Training Your Custom Tesseract Model

277

you want to install these packages into your system Python install, that’s okay as well (but again, not recommended).

19.3

Training Your Custom Tesseract Model

Take a second to congratulate yourself! You’ve done a lot of work configuring your Tesseract development environment — it was not easy, but you fought through. And the good news is, it’s essentially all downhill from here! In the first part of this section, we’ll review our project directory structure. We’ll then review the dataset we’ll be using for this chapter. I’ll also provide tips and suggestions for building your Tesseract training dataset. Next, I’ll show you how to download a pre-trained Tesseract model that we’ll fine-tune on our dataset. Once the model is downloaded, all that’s left is to set a few environment variables, copy our training dataset into the proper directory, and then commence training!

19.3.1

Project Structure

Now that Tesseract and tesstrain are configured, we can move on to defining our directory structure for our project:

|-| | | | ... | | |--

training_data |-- 01.gt.txt |-- 01.png |-- 02.gt.txt |-- 02.png |-- 18.gt.txt |-- 18.png training_setup.sh

Our training_data directory is arguably the most important part of this project structure for you to understand. Inside this directory, you’ll see that we have pairs of .txt and .png files. The .png files are our images, containing the text in image format. The .txt files then have the plaintext version of the text, such that a computer can read it from disk. When training, Tesseract will load each .png image and .txt file, pass it through the LSTM, and then update the model’s weights, ensuring that it makes better predictions during the next batch.

278

Chapter 19. Training a Custom Tesseract Model

Most importantly, notice the naming conventions of the files in training_data. For each .png file, you also have a text file named XX.gt.txt. For each image file, you must have a text file with the same filename, but instead of a .png, .jpg, etc., extension, it must have a .gt.txt extension. Tesseract can associate the images and ground-truth (gt) text files together, but you must have the correct file names and extensions. We’ll cover the actual dataset itself in the next section, but simply understand the naming convention for the time being. We then have a shell script, training_setup.sh. This shell script handles setting an important environment variable, TESSDATA_PREFIX, which allows us to perform training and inference.

19.3.2

Our Tesseract Training Dataset

Our Tesseract training dataset consists of 18 lines of random, handwritten words (Figure 19.1). This dataset was put together by PyImageSearch reader Jayanth Rasamsetti, who helped us put together all the pieces for this chapter. To construct our training set, we manually cropped each row of text, creating 18 separate images. We cropped each row using basic image editors (e.g., macOS Preview, Microsoft Paint, GIMP, Photoshop, etc.).

Figure 19.1. A few images of handwritten text from our training dataset.

19.3. Training Your Custom Tesseract Model

279

Then, for each row, we created a .txt file containing the ground-truth text. For example, let’s examine the second row (02.png) and its corresponding 02.gt.txt file (Figure 19.2). As Figure 19.2 displays, we have a row of text in an image (top), while the bottom contains the ground-truth text as a plaintext file. We then have the same file structure for all 18 rows of text. We’ll use this data to train our custom Tesseract model.

Figure 19.2. Top: A sample image from our training dataset. Bottom: Ground-truth label of the sample image in .gt.txt format.

19.3.3

Creating Your Tesseract Training Dataset

You might be wondering why we are using Tesseract to train a custom handwriting recognition model. Astute OCR practitioners will know that Tesseract doesn’t perform well on handwriting recognition compared to typed text. Why bother training a handwriting recognition model at all? It boils down to three reasons: i. Readers ask all the time if it’s possible to train a Tesseract model for handwriting recognition. As you’ll see, it’s possible, and our custom model’s accuracy will be ≈10x better than the default model, but it still leaves accuracy to be desired. ii. Training a custom Tesseract model for typed text recognition is comparatively “easy.” Our goal in this chapter is to show you something you haven’t seen before. iii. Most importantly, once you’ve trained a custom Tesseract model, you can train the model on your data. I suggest replicating the results of this chapter first to get a feel for

280

Chapter 19. Training a Custom Tesseract Model

the training process. From there, training your model is as easy as swapping the training_data directory for your images and .gt.txt files.

19.3.4

Step #1: Setup the Base Model

Instead of training a Tesseract model from scratch, which is time-consuming, tedious, and harder to do, we’re going to fine-tune an existing LSTM-based model. The LSTM models included with Tesseract v4 are much more accurate than the non-deep learning models in Tesseract v3, so I suggest you use these LSTM models and fine-tune them rather than training from scratch. Additionally, it’s worth mentioning here that Tesseract has three different types of model types: i. tessdata_fast: These models balance speed vs. accuracy using integer-quantized weights. These are also the default models that are included when you install Tesseract on your machine. ii. tessdata_best: These models are the most accurate, using floating-point data types. When fine-tuning a Tesseract model, you must start with these models. iii. tessdata: The old legacy OCR models from 2016. We will not need or utilize these models. You can learn more about each of these three model types in Tesseract’s documentation (http://pyimg.co/frg2n [64]). You can use the following commands to change directory to ∼/tesseract/tessdata and then download the eng.traineddata file, which is the trained LSTM model for the English language:

$ cd ~/tesseract $ cd tessdata $ wget ,→ https://raw.githubusercontent.com/tesseract-ocr/tessdata_best/master/ ,→ eng.traineddata

Tesseract maintains a repository of all .traineddata model files for 124 languages here: http://pyimg.co/7n22b. We use the English language model in this chapter. Still, you can just as easily replace the English language model with a language of your choosing (provided there is a .traineddata model for that language, of course).

19.3. Training Your Custom Tesseract Model

19.3.5

281

Step #2: Set Your TESSDATA_PREFIX

With our .traineddata model downloaded (i.e., the weights of the model that we’ll be fine-tuning), we now need to set an important environment variable TESSDATA_PREFIX. The TESSDATA_PREFIX variable needs to point to our tesseract/tessdata directory. If you followed the install instructions (Section 19.2.2), where we compiled Tesseract and its training tools from scratch, then the tesseract/tessdata folder should be in your home directory. Take a second now to verify the location of tesseract/tessdata — double-check and triple-check this file path; setting the incorrect TESSDATA_PREFIX will result in Tesseract being unable to find your training data and thus unable to train your model. Inside the project directory structure for this chapter, you will find a file named training_ setup.sh. As the name suggests, this shell script sets your TESSDATA_PREFIX. Let’s open this file and inspect the contents now:

#!/bin/sh export TESSDATA_PREFIX="/home/pyimagesearch/tesseract/tessdata"

Here you can see that I’ve supplied the absolute path to my tesseract/tessdata directory on disk. You need to supply the absolute path; otherwise, the training command will error out. Additionally, my username on this machine is pyimagesearch. If you use the VM included with your purchase of this text, this VM already has Tesseract and its training tools installed. If your username is different, you will need to update the training_ setup.sh file and replace the pyimagesearch text with your username. To set the TESSDATA_PREFIX environment variable, you can run the following command:

$ source training_setup.sh

As a sanity check, let’s print the contents of the TESSDATA_PREFIX variable to our terminal:

$ echo $TESSDATA_PREFIX /home/pyimagesearch/tesseract/tessdata

The final step is to verify the tesseract/tessdata directory exists on disk by copying and pasting the above path into the ls command:

282

Chapter 19. Training a Custom Tesseract Model

$ ls -l /home/pyimagesearch/tesseract/tessdata total 15112 -rw-rw-r-- 1 ubuntu ubuntu 22456 Nov 24 08:29 -rw-rw-r-- 1 ubuntu ubuntu 184 Dec 26 2019 -rw-rw-r-- 1 ubuntu ubuntu 22008 Nov 24 08:29 drwxrwxr-x 2 ubuntu ubuntu 4096 Nov 24 08:29 -rw-rw-r-- 1 ubuntu ubuntu 15400601 Nov 24 08:41 -rw-rw-r-- 1 ubuntu ubuntu 33 Dec 26 2019 -rw-rw-r-- 1 ubuntu ubuntu 27 Dec 26 2019 -rw-rw-r-- 1 ubuntu ubuntu 572 Dec 26 2019 drwxrwxr-x 2 ubuntu ubuntu 4096 Nov 24 08:29

Makefile Makefile.am Makefile.in configs eng.traineddata eng.user-patterns eng.user-words pdf.ttf tessconfigs

If your output looks similar to mine, then you know your TESSDATA_PREFIX variable is set correctly. If your TESSDATA_PREFIX variable is set incorrectly, then ls will report an error: $ ls -l /home/pyimagesearch/tesseract/tessdata ls: /home/pyimagesearch/tesseract/tessdata: No such file or directory

This error implies that your path to tesseract/tessdata is incorrect. Go back and check your file paths. You need to have TESSDATA_PREFIX correctly set before continuing!

19.3.6

Step #3: Setup Your Training Data

In Section 19.3.1, we reviewed the directory structure and file organization Tesseract and tesstrain expect when training/fine-tuning a custom Tesseract OCR model. Section 19.3.3 then provided suggestions on how to build your training dataset. At this point, I’ll assume that you have your dataset properly organized on disk. For the sake of this example, I also suggest that you use the training data included in the directory structure of this project — walk before you run. Training a custom Tesseract model is hard, especially if this is your first time. I strongly encourage you to replicate our results before trying to train your models. The following commands copy our training data from the training_data directory of this project into tesstrain/handwriting-ground-truth: $ $ $ $ $ $

cd ~/tesstrain mkdir data cd data mkdir handwriting-ground-truth cd handwriting-ground-truth cp -r ~/OCRPractitionerBundle_Code/chapter19-training_tesseract_model/ ,→ training_data/*.* .

19.3. Training Your Custom Tesseract Model

283

The most important aspect of the above commands for you to understand is the naming convention. Let’s break each of these commands down into components to understand what we are doing: First, a cd ∼/tesstrain changes the directory into our base tesstrain directory. We then create a data directory and cd into it — we must create this data directory to store our training data. Inside the data directory, we then create a subdirectory named handwriting-groundtruth. You must understand the naming convention of this directory: i. The handwriting portion of the subdirectory is our model’s name (which we’ll use in our next section during training). You can name your model whatever you want, but I suggest making it something specific to the project/task at hand, so you remember what the intended use of the model is. ii. The second portion of the directory name, ground-truth, tells Tesseract that this is where our training data lives. When creating your training data directory structure, you must follow this naming convention. After creating the handwriting-ground-truth directory and cd into it, we copy all of our images and .txt files into it. Once that is complete, we can train the actual model.

19.3.7

Step #4: Fine-Tune the Tesseract Model

With our training data in place, we can now fine-tune our custom Tesseract model. Open a terminal and execute the following commands:

$ workon ocr $ cd ~/tesstrain $ make training MODEL_NAME=handwriting START_MODEL=eng ,→ TESSDATA=/home/pyimagesearch/tesseract/tessdata

First, I use the workon command to access the Python virtual environment where I have all my tesstrain dependencies installed. As I mentioned earlier in this chapter, using a Python virtual environment for these dependencies is optional but suggested (you should install the packages in the system Python, for instance). Next, we change directory into ∼/tesstrain, where we have our Tesseract training utilities.

284

Chapter 19. Training a Custom Tesseract Model

Training commences using the make command, the output of which you can see in Figure 19.3. Here you can see the model training. Training will continue until the default number of iterations, MAX_ITERATIONS=10000, is reached.

Figure 19.3. Model training information after we successfully launch the training.

On my machine, the entire training process took ≈10 minutes. The training logs are shown in Figure 19.4. After the training is completed, the final logs should look like Figure 19.4. But before we get too far, let’s break down the make command to ensure we understand each of the parameters:

i. MODEL_NAME: The name of the Tesseract model we are training. This value should be the same as the directory you created in tesstrain/data; that way, Tesseract knows to associate the tesstrain/data/handwriting-ground-truth dataset with the model we are training. ii. START_MODEL: The name of the model we are fine-tuning. Recall that back in Section 19.3.4, and we downloaded eng.traineddata to tesseract/tessdata — that file holds the initial model weights that we will be fine-tuning.

19.3. Training Your Custom Tesseract Model

285

iii. TESSDATA: The absolute path to our tesseract/tessdata directory where the handwriting subdirectory and training data are stored.

Figure 19.4. Model training completion information.

The previous three parameters are required to fine-tune the Tesseract model; however, it’s worth noting that there are another two important hyperparameters that you may want to consider fine-tuning: i. MAX_ITERATIONS: The total number of training iterations (similar to how Caffe iterations default to 10000). ii. LEARNING_RATE: The learning rate when fine-tuning our Tesseract model. This value defaults to 0.0001. I recommend keeping the learning rate a relatively low value. If you raise it too high, then the model’s pre-trained weights may be broken down too quickly (i.e., before the model can take advantage of the pre-trained weights to learn patterns specific to the current task). iii. PSM: The default PSM mode to use when segmenting the characters from your training images. You should use the tesseract command to test your example images with varying PSM modes until you find the correct one for segmentation (see Chapter 11, Improving OCR Results with Tesseract Options of the “Intro to OCR” Bundle for more information on page segmentation modes). I assume that as a deep learning practitioner, you understand the importance of fine-tuning both your iterations and learning rate — fine-tuning these hyperparameters is crucial in obtaining a higher accuracy OCR model. A full list of hyperparameters available to you can be found on the GitHub repository for the tesstrain package (http://pyimg.co/n6pdk). I’ll wrap up this chapter by saying if your training command errors out and you need to start

286

Chapter 19. Training a Custom Tesseract Model

again (e.g., you accidentally supplied the incorrect TESSDATA path), all you need do is run the following command:

$ make clean MODEL_NAME=handwriting

Running make clean allows you to restart the training process. Make sure you specify the MODEL_NAME. Otherwise, Tesseract will assume the default model name (which is foo). If you make training command errors out, I suggest you run make clean to clean up and allow Tesseract a fresh start to train your model.

19.3.8

Training Tips and Suggestions

When training your custom Tesseract models, the first thing you need to do is set your expectations. For your first time around, it’s going to be hard, and the only way it will get easier is to suffer through it, learning from your mistakes. There’s no “one easy trick.” There’s no shortcut. There’s no quick and dirty hack. Instead, you need to put in the hours and the time. Secondly, accept that you’ll run into errors. It’s inevitable. Don’t fight it, and don’t get discouraged when it happens. You’ll miss/forget commands, and you may run into development environment issues specific to your machine. When that happens, Google is your friend. Tesseract is the world’s most popular OCR engine. It’s more than likely that someone has encountered the same error as you have. Copy and paste the error from your terminal, search for it on Google, and do your due diligence. Yes, that is time-consuming. Yes, it’s a bit tedious. And yes, there will be more than one false-start along the way. Do you know what I have to say about that? Good. It means you’re learning. The notion that you can train a custom Tesseract model in your pajamas with a hot cup of tea and someone massaging your shoulders, relaxing you into the process, is false. Accept that it’s going to suck from time to time — if you can’t accept it, you won’t be able to learn how to do it. Training a custom Tesseract model comes with experience. You have to earn it first. Finally, if you run into an error that no one else has documented online, post it on the tesstrain GitHub Issues page (http://pyimg.co/f15kp) so that the Tesseract developers can

19.4. Summary

287

help you out. The Tesseract team is incredibly active and responds to bug reports and questions — utilize their knowledge to better yourself. And above all, take your time and be patient. I know it can be frustrating. But you got this — just one step at a time.

19.4

Summary

In this chapter, you learned how to train and fine-tune a Tesseract OCR model on your custom dataset. As you learned, fine-tuning a Tesseract model on your dataset has very little to do with writing code, and instead is all about: i. Properly configuring your development environment ii. Installing the tesstrain package iii. Structuring your OCR training dataset in a manner Tesseract can understand iv. Setting the proper environment variables correctly v. Running the correct training commands (and in the right order) If you’ve ever worked with Caffe or the TensorFlow Object Detection API, then you know that working with commands, rather than code, can be a catch-22: • On the one hand, it’s easier since you aren’t writing code, and therefore don’t have to worry about bugs, errors, logic issues, etc. • But on the other hand, it’s far harder because you are left at the mercy of the Tesseract training tools Instead, I suggest you remove the word “easy” from your vocabulary when training a Tesseract model — it’s going to be hard — plain and simple. You need to adjust your mindset accordingly and walk in with an open mind, knowing that: • The first (and probably the second and third) time you try to configure your development environment for Tesseract, training will likely fail due to missed commands, environment-specific error messages, etc. • You’ll need to double-check and triple-check that your training data is stored correctly in the tessdata directory

288

Chapter 19. Training a Custom Tesseract Model

• Your training commands will error out if even the slightest configuration is correct • Additionally, the Tesseract training command will error out because you forgot to be in a Python environment with the Python Imaging Library (PIL)/Pillow installed • You’ll forget to run make clean when running the training command and then won’t be able to get training to start again (and you may spend an hour or so banging your head against the wall before you realize it) Accept that this is all part of the process. It takes time and a lot of patience to get used to training custom Tesseract models, but the more you do it, the easier it gets. In the following chapter, we will take our trained custom Tesseract model and OCR an input image.

Chapter 20

OCR’ing Text with Your Custom Tesseract Model In our previous chapter, you learned how to train a Tesseract model on your custom dataset. We did so by annotating a sample set of images, providing their ground-truth plaintext versions, configuring a Tesseract training job, and then fine-tuning Tesseract’s LSTM OCR model on our custom dataset. After training is completed, we were left with a serialized OCR model. However, the question remains: “How do we take this trained Tesseract model and use it to OCR input images?” The remainder of this chapter will address that exact question.

20.1

Chapter Learning Objectives

Inside this chapter, you will: • Learn how to set your TESSDATA_PREFIX path for inference • Load our custom trained Tesseract OCR model • Use the model to OCR input images

20.2

OCR with Your Custom Tesseract Model

In the first part of this chapter, we’ll review our project directory structure. We’ll then implement a Python script to take our custom trained OCR model and then use it to OCR an 289

290

Chapter 20. OCR’ing Text with Your Custom Tesseract Model

input image. We’ll wrap up the chapter by comparing our custom model’s accuracy with the default Tesseract model, noting that our custom model can obtain higher OCR accuracy.

20.2.1

Project Structure

Let’s start this chapter by reviewing our project directory structure:

|-|-|-|-|--

full.png full.txt handwriting.traineddata handwritten_to_text.py inference_setup.sh

Here’s what you need to know about the directory structure: • The inference_setup.sh script is used to set our TESSDATA_PREFIX environment variable for testing • The handwritten_to_text.py file takes an input image (full.png), applies OCR to it, and then compares it to the ground-truth text (full.txt) • The handwriting.traineddata is our custom trained Tesseract model serialized to disk (I included it here in the project structure just in case you do not wish to train the model yourself and instead just want to perform inference) By the end of this chapter, you’ll be able to take your custom trained Tesseract model and use it to OCR input images.

20.2.2

Implementing Your Tesseract OCR Script

With our custom Tesseract model trained, all that’s left is to learn how to implement a Python script to load the model and apply it to a sample image. Open the handwritten_to_text.py file in your project directory structure, and let’s get to work:

1 2 3 4 5

# import the necessary packages from difflib import SequenceMatcher as SQ import pytesseract import argparse import cv2

20.2. OCR with Your Custom Tesseract Model

291

Lines 2–5 import our required Python packages. The difflib module provides functions for comparing sequences of files, strings, etc. We’ll specifically use the SequenceMatcher, which can accept a pair of input strings (i.e., the ground-truth text and the predicted OCR’d text) and then compare the sequences for overlap, resulting in an accuracy percentage. The pytesseract package will be used to interface with the Tesseract OCR engine, and more specifically, to set the name of our custom trained Tesseract model. Let’s now move on to our command line arguments:

7 8 9 10 11 12 13 14 15

# construct the argument parser and parse the arguments ap = argparse.ArgumentParser() ap.add_argument("-n", "--name", required=True, help="name of the OCR model") ap.add_argument("-i", "--image", required=True, help="path to input image of handwritten text") ap.add_argument("-g", "--ground-truth", required=True, help="path to text file containing the ground-truth labels") args = vars(ap.parse_args())

Our script requires three command line arguments, including: i. --name: The name of our OCR model. In the previous chapter, our output, a serialized OCR model, had the filename handwriting.traineddata, so the --name of the model is simply handwriting (i.e., leaving out the file extension). ii. --image: The path to the input image we want to which our custom OCR model applies. iii. --ground-truth: The plaintext file containing the ground-truth text in the input image; we’ll compare the ground-truth text to the text produced by our custom OCR model. With our command line arguments taken care of, let’s proceed to OCR the input image:

17 18 19 20

# load the input image and convert it from BGR to RGB channel # ordering image = cv2.imread(args["image"]) image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)

21 22 23 24 25

# use Tesseract to OCR the image print("[INFO] OCR'ing the image...") predictedText = pytesseract.image_to_string(image, lang=args["name"]) print(predictedText)

292

Chapter 20. OCR’ing Text with Your Custom Tesseract Model

Lines 19 and 20 load our input --image from disk and convert it from the BGR to RGB color channel order. We then apply our custom trained OCR model on Line 24. Take note how the lang argument points to the --name of the custom OCR model — doing so instructs Tesseract to use our custom model rather than a default model. The resulting predictedText is then displayed on our screen. Next, we can evaluate the accuracy of our custom model: 27 28 29

# read text from the ground-truth file with open(args["ground_truth"], "r") as f: target = f.read()

30 31 32 33

# calculate the accuracy of the model with respect to the ratio of # sequences matched in between the predicted and ground-truth labels accuracyScore = SQ(None, target, predictedText).ratio() * 100

34 35 36 37 38

# round off the accuracy score and print it out accuracyScore = round(accuracyScore, 2) print("[INFO] accuracy of {} model: {}%...".format(args["name"], accuracyScore))

Lines 28 and 29 load the contents of the plaintext --ground-truth file. We then use our SequenceMatcher to compute the ratio of matched sequences between the predicted and ground-truth texts, resulting in our accuracyScore. The final accuracyStore is then displayed to our terminal.

20.2.3

Custom Tesseract OCR Results

We are now ready to use our Python script to OCR handwritten text with our custom trained Tesseract model. However, before we can run our handwritten_to_text.py script, we first need to set our TESSDATA_PREFIX to point to our custom trained OCR model. $ source inference_setup.sh

After executing inference_setup.sh, take a second to validate that your TESSDATA_PREFIX is properly set: $ echo $TESSDATA_PREFIX /home/pyimagesearch/tesstrain/data

20.2. OCR with Your Custom Tesseract Model

$ ls -l /home/pyimagesearch/tesstrain/data drwxrwxr-x 2 ubuntu ubuntu 4096 Nov 24 drwxrwxr-x 3 ubuntu ubuntu 4096 Nov 24 drwxrwxr-x 2 ubuntu ubuntu 4096 Nov 24 -rw-rw-r-- 1 ubuntu ubuntu 11696673 Nov 24 -rw-rw-r-- 1 ubuntu ubuntu 330874 Nov 24

293

15:44 15:46 15:46 15:50 15:41

eng handwriting handwriting-ground-truth handwriting.traineddata radical-stroke.txt

Provided that you can (1) successfully list the contents of the directory and (2) the custom handwriting model is present, we can then move on to executing the handwritten_to_text.py script. Optionally, if you did not train your Tesseract model in the previous chapter and instead want to utilize the pre-trained model included in the downloads associated with this chapter, you can manually set the TESSDATA_PREFIX in the following manner:

$ export ,→ TESSDATA_PREFIX=/PATH/TO/PRACTITIONER/BUNDLE/CODE/chapter20-ocr_custom_ ,→ tesseract_model

You will need to update the path above based on where you downloaded the source code for this text on your own system. Now that your TESSDATA_PREFIX is set, you can execute the following command:

$ python handwritten_to_text.py --name handwriting --image full.png ,→ --ground-truth full.txt [INFO] OCR'ing the image... 1t wandered relatipn no sureprise of screened doulptful. Overcame No mstead ye of ‘rifling husbands. Might om older hours on found. Or digsimilar companions fryendship impossible, at diminumon. Did yoursetp Carrriage learning rate she man s replying. gister pidued living her gou enable mrs off spirit really. Parish oOppose rerair is me miasery. Quick may saw zsiyte afier money mis Now oldest new tostes lblenty mother calleq misery yer. Longer exatse or county motr exceprt met its trung2. Narrow enoughn sex. mornent desire are. Heid iiho that come 4hat seen read age its, Contmimed or estimotes [INFO] accuracy of handwriting model: 23.28%...

294

Chapter 20. OCR’ing Text with Your Custom Tesseract Model

Figure 20.1 displays the sample handwriting text that we are trying to OCR. The terminal output shows the OCR process results, the accuracy of which is ≈23%.

Figure 20.1. The sample handwriting text we are OCR’ing with Tesseract.

OCR accuracy of 23% may feel extremely low, but let’s compare it to the default English OCR model included with Tesseract. To start, make sure you open a new terminal so that our TESSDATA_PREFIX environment variable is empty. Otherwise, Tesseract will attempt to search the TESSDATA_PREFIX directory for the default English model and be unable to find it.

20.2. OCR with Your Custom Tesseract Model

295

You can validate that your TESSDATA_PREFIX variable is empty/not set by echo’ing its contents to your terminal:

$ echo $TESSDATA_PREFIX

If the output of your echo command is empty, you can proceed to run the following command:

$ python handwritten_to_text.py --name eng --image full.png --ground-truth ,→ full.txt [INFO] OCR'ing the image... IE wandered relation NO surerize of 2eveened deuotful. Overcane 10 Mstead ye of rifling husbands. Might om older hours on found. Or dissimilar companions friendship MPossinle, a4 diminuinon. Did yourserp Caxvrriage \egrnwg rate she man "= fer Ng- User pidues Wing her dou enave NTs off spirit really. Parish Opbose Tevrair 1s WwE& mserd. Quick Yaay saw ae aller mosey: Nous ©dest new tostes lent nother Coed Wiser Fer. Longer exase Lor county Mov exrery met vis Ang . Navrow Noun sey. noment-doswre ave. Yd he The Come A Nar Seen vead age Sis) Contmned OV Exhmoses [INFO] accuracy of eng model: 2.77%..

OCR’ing the same image (see Figure 20.1) with Tesseract’s default English model obtains only 2.77% accuracy, implying that our custom trained handwriting recognition model is nearly 10x more accurate! That said, I chose handwriting recognition as the example project for this chapter (as well as the previous one) to demonstrate that handwriting recognition with the Tesseract OCR engine, while technically possible, will struggle to obtain higher accuracy. It’s instead far better to train/fine-tune Tesseract models on typed text. Doing so will yield far higher accuracy with less effort. Note: For more information on how you can improve the accuracy of any custom Tesseract OCR model, refer to the previous chapter’s discussion on hyperparameter fine-tuning and gathering additional training data representative of what the model will see during testing. If you need a high accuracy OCR engine for handwriting text, be sure to follow Chapter 5,

296

Chapter 20. OCR’ing Text with Your Custom Tesseract Model

Making OCR “Easy” with EasyOCR, where the developers work with off-the-shelf handwriting recognition models in several general-purpose scenarios.

20.3

Summary

In this chapter, you learned how to apply a custom trained Tesseract model to an input image. The actual implementation was simple, requiring less than 40 lines of Python code. While accuracy wasn’t the best, that’s not a limitation of our inference script — to improve accuracy; we should spend more time training. Additionally, we applied Tesseract to the task of handwriting recognition to demonstrate that while it’s technically possible to train a Tesseract model for handwriting recognition, the results will be poor compared to typed-text OCR. Therefore, if you are planning on training your custom Tesseract models, be sure to apply Tesseract to typed-text datasets versus handwritten text — doing so will yield higher OCR accuracy.

Chapter 21

Conclusions Congratulations on completing this book! I feel so privileged and lucky to take this journey with you — and you should feel immensely proud of your accomplishments. I hope you will join me to keep learning new topics each week, including videos of me explaining computer vision and deep learning, at http://pyimg.co/bookweeklylearning. Learning optical character recognition (OCR) is not easy (if it were, this book wouldn’t exist!). You took the bull by the horns, fought some tough battles, and came out victorious. Let’s take a second to recap your newfound knowledge. Inside this bundle you’ve learned how to: • Train your custom Keras/TensorFlow deep learning OCR models • Perform handwriting recognition with Keras and TensorFlow • Use machine learning to denoise images to improve OCR accuracy • Perform image/document alignment and registration • Build on the image/document alignment to OCR structured documents, including forms, invoices, etc. • Use OCR to automatically solve sudoku puzzles • Create a project to OCR receipts, pulling out the item names and prices • Build an automatic license/number plate recognition (ALPR/ANPR) system • Detect (and discard) blurry text in images • Use our blurry text detector implementation to OCR real-time video streams • Leverage GPUs to improve the text detection speed of deep learning models 297

298

Chapter 21. Conclusions

• Connect to cloud-based OCR engines including Amazon Rekognition API, Microsoft Cognitive Services, and the Google Cloud Vision API • Train and fine-tune Tesseract models on your custom datasets • Use the EasyOCR Python package, which as the name suggests, makes working with text detection and OCR easy • . . . and much more!

Each of these projects was challenging and representative of the types of techniques and algorithms you’ll need when solving real-world OCR problems. At this point, you’re able to approach OCR problems with confidence. You may not know the solution immediately, but you’ll have the skills necessary to attack it head-on, find out what works/what doesn’t, and iterate until you are successful. Solving complex computer vision problems, such as OCR ones, aren’t like your intro to computer science projects — implementing a function to generate the Fibonacci sequence using recursion is comparatively “easy.” With computer vision and deep learning, some projects have a sense of ambiguity associated with them by their very nature. Your goal should never be to obtain 100% accuracy in all use cases — in the vast majority of problems, that’s simply impossible. Instead, relax your constraints as much as you reasonably can, and be willing to tolerate that there will be some margin of error in every project. The key to building successful computer vision and OCR software is identifying where these error points will occur, trying to handle them as edge cases, and then documenting them to the software owners. Complex fields such as computer vision, deep learning, and OCR will never obtain “perfect” accuracy. In fact, researchers are often wary of “100% accuracy” as that’s often a sign of a model overfitting to a specific problem, and therefore being unable to generalize to other use cases. Use the same approach when implementing your projects. Attack the problem head-on using the knowledge gained from this book. Accept a tolerance of error. Be patient. And then consistently and unrelentingly iterate until you fall within your error tolerance bounds. The key here is patience. I’m not a patient person, and more than likely, you aren’t patient either. But there are times when you need to step away from your keyboard, go for a walk, and just let your mind relax from the problem. It’s there that new ideas will form, creativity will spark, and you’ll have that “Ah-ha!” moment where the light bulb flips on, and you see the solution.

21.1. Where to Now?

299

Keep applying yourself, and I have no doubt that you’ll be successful when applying OCR to your projects.

21.1

Where to Now?

Optical character recognition is a subfield of computer vision that also overlaps with deep learning. There are new OCR algorithms and techniques published daily. The hands-down, best way to progress is to work on your projects with some help. I create weekly video updates, pre-configured Juypter Notebooks on Colab, and code downloads to help professionals, researchers, students, and hackers achieve their goals. I hope you will take a moment visit it here http://pyimg.co/bookweeklylearning. I think it is the only place for you to have a real PhD serve as your guide with new videos and resources published each week. I’m honestly kicking myself for not having done this earlier. Here’s why I think http://pyimg.co/bookweeklylearning is the perfect mix of theory, video explanation, pre-configured Jupyter Notebooks on Colab to help everyone achieve their goals. My mission is to bring computer vision and deep learning to the world. I started with my blog, then moved to books, and only now do I realize the power of http://pyimg.co/bookweeklylearning. I’ve had customers who are PhD professors in CS tell me http://pyimg.co/bookweeklylearning is helping them reshape the way they teach and massively accelerate student learning. I think it’s the best way to bring computer vision and deep learning to everyone, and I hope you’ll join me there. Below are some of my favorite third party resources. If you use them, you’ll need to assimilate and comb through all the various topics, which again, is why I’m so passionate about http://pyimg.co/bookweeklylearning where I do all the hard work for you. But, in case you want to do all that work yourself, here is my curated list of resources you may find useful. To keep up with OCR implementations, I suggest you follow the PyImageSearch blog (http://pyimg.co/blog), and more specifically, the “Optical Character Recognition” topic (http://pyimg.co/blogocr), which provides a curated list of all OCR topics I’ve published. To keep up with trends in both the deep learning and machine learning fields, I highly suggest following /r/machinelearning and /r/deeplearning on Reddit: • http://pyimg.co/oe3ki • http://pyimg.co/g2vb0 If you are a LinkedIn user, join the Computer Vision Online and Computer Vision and Pattern Recognition groups, respectively:

300

Chapter 21. Conclusions

• http://pyimg.co/s1tc3 • http://pyimg.co/6ep60 If you’re a researcher, I suggest you use Andrej Karpathy’s Arxiv Sanity Preserver (http://pyimg.co/ykstc [65]), which you can use to find papers on optical character recognition. As a researcher, you’ll want to keep up with the state-of-the-art in the field.

21.2

Thank You

Truly, it’s been a privilege and an honor to accompany you on your journey to learning optical character recognition and Tesseract. Without readers like you, PyImageSearch would not be possible. And even more importantly, without the work of thousands of researchers, professors, and developers before me, I would be unable to teach you today. I’m truly standing on the shoulders of giants. I am but one small dent in this computer vision field — and it’s my goal to ensure you leave an even bigger dent. If you have any questions or feedback on this book, please reach out to me at [email protected]. Rest assured, someone on my team will get back to you with a prompt reply. Cheers, — Adrian Rosebrock, Chief PyImageSearcher, Developer, and Author — Abhishek Thanki, Developer and Quality Assurance — Sayak Paul, Developer and Deep Learning Researcher — Jon Haase, Technical Project Manager

Bibliography [1] Yann LeCun, Corinna Cortes, and CJ Burges. “MNIST handwritten digit database”. In: ATT Labs [Online] 2 (2010). URL: http://yann.lecun.com/exdb/mnist (cited on pages 6, 108, 111). [2] Sachin Patel. A-Z Handwritten Alphabets in .csv format. https://www.kaggle.com/sachinpatel21/az-handwritten-alphabetsin-csv-format. 2018 (cited on pages 6, 7). [3] NIST Special Database 19. https://www.nist.gov/srd/nist-special-database-19. 2020 (cited on pages 6, 7). [4] Kaiming He et al. “Deep Residual Learning for Image Recognition”. In: CoRR abs/1512.03385 (2015). URL: http://arxiv.org/abs/1512.03385 (cited on page 11). [5] Adrian Rosebrock. Fine-tuning ResNet with Keras, TensorFlow, and Deep Learning. https://www.pyimagesearch.com/2020/04/27/fine-tuning-resnetwith-keras-TensorFlow-and-deep-learning/. 2020 (cited on page 11). [6] Adrian Rosebrock. Deep Learning for Computer Vision with Python, 3rd Ed. PyImageSearch, 2019. URL: https://www.pyimagesearch.com/deeplearning-computer-vision-python-book/ (cited on pages 11, 15, 111). [7] Adrian Rosebrock. Montages with OpenCV. https://www.pyimagesearch.com/2017/05/29/montages-with-opencv/. 2017 (cited on pages 12, 19). [8] Adrian Rosebrock. Keras ImageDataGenerator and Data Augmentation. https://www.pyimagesearch.com/2019/07/08/kerasimagedatagenerator-and-data-augmentation/. 2019 (cited on page 15). [9] Kaggle. Denoising Dirty Documents. https://www.kaggle.com/c/denoising-dirty-documents/data. 2015 (cited on pages 38, 39, 43, 55).

301

302

Bibliography

[10] Dheeru Dua and Casey Graff. UCI Machine Learning Repository. 2017. URL : http://archive.ics.uci.edu/ml (cited on page 39). [11] Colin Priest. Denoising Dirty Documents: Part 1. https: //colinpriest.com/2015/08/01/denoising-dirty-documents-part-1/. 2015 (cited on page 39). [12] Colin Priest. Denoising Dirty Documents: Part 6. https: //colinpriest.com/2015/09/07/denoising-dirty-documents-part-6/. 2015 (cited on page 40). [13] Adrian Rosebrock. Convolutions with OpenCV and Python. https://www.pyimagesearch.com/2016/07/25/convolutions-withopencv-and-python/. 2016 (cited on pages 49, 116). [14] JaidedAI. EasyOCR. https://github.com/JaidedAI/EasyOCR. 2020 (cited on page 62). [15] JaidedAI. JaidedAI - Distribute the benefits of AI to the world. https://jaided.ai. 2020 (cited on page 62). [16] JaidedAI. EasyOCR by JaidedAI. https://www.jaided.ai/easyocr. 2020 (cited on pages 62, 65, 67). [17] JaidedAI. API Documentation. https://www.jaided.ai/easyocr/documentation. 2020 (cited on page 66). [18] Martin A. Fischler and Robert C. Bolles. “Random Sample Consensus: A Paradigm for Model Fitting with Applications to Image Analysis and Automated Cartography”. In: Commun. ACM 24.6 (June 1981), pages 381–395. ISSN: 0001-0782. DOI : 10.1145/358669.358692. URL : https://doi.org/10.1145/358669.358692 (cited on page 75). [19] OpenCV. Basic concepts of the homography explained with code. https://docs.opencv.org/master/d9/dab/tutorial_homography.html (cited on page 76). [20] OpenCV. ORB (Oriented FAST and Rotated BRIEF). https://docs.opencv.org/3.4/d1/d89/tutorial_py_orb.html (cited on page 79). [21] Adrian Rosebrock. Local Binary Patterns with Python & OpenCV. https://www.pyimagesearch.com/2015/12/07/local-binary-patternswith-python-opencv/. 2015 (cited on page 79). [22] Adrian Rosebrock. PyImageSearch Gurus. https://www.pyimagesearch.com/pyimagesearch-gurus/. 2020 (cited on pages 79, 169).

Bibliography

303

[23] Satya Mallick. Image Alignment (Feature Based) using OpenCV (C++/Python). https://www.learnopencv.com/image-alignment-feature-based-usingopencv-c-python/. 2018 (cited on page 80). [24] Adrian Rosebrock. Capturing mouse click events with Python and OpenCV. https://www.pyimagesearch.com/2015/03/09/capturing-mouse-clickevents-with-python-and-opencv/. 2015 (cited on page 94). [25] Anthony Thyssen. ImageMagick v6 Examples - Convert. http://www.imagemagick.org/Usage/basics/#convert. 2020 (cited on page 94). [26] Anthony Thyssen. ImageMagick v6 Examples - Mogrify. http://www.imagemagick.org/Usage/basics/#mogrify. 2020 (cited on page 94). [27] Jeff Sieu. py-sudoku PyPI. https://pypi.org/project/py-sudoku/ (cited on pages 108, 123, 124). [28] Adrian Rosebrock. 3 ways to create a Keras model with TensorFlow 2.0 (Sequential, Functional, and Model Subclassing). https://www.pyimagesearch.com/2019/10/28/3-ways-to-create-akeras-model-with-TensorFlow-2-0-sequential-functional-andmodel-subclassing/. 2019 (cited on page 109). [29] Adrian Rosebrock. Keras Tutorial: How to get started with Keras, Deep Learning, and Python. https://www.pyimagesearch.com/2018/09/10/keras-tutorial-how-toget-started-with-keras-deep-learning-and-python/. 2018 (cited on page 111). [30] Adrian Rosebrock. Keras Learning Rate Finder. https: //www.pyimagesearch.com/2019/08/05/keras-learning-rate-finder/. 2019 (cited on page 112). [31] Aakash Jhawar. Sudoku Solver using OpenCV and DL — Part 1. https://medium.com/@aakashjhawar/sudoku-solver-using-opencv-anddl-part-1-490f08701179. 2019 (cited on pages 115, 130). [32] Adrian Rosebrock. imutils: A series of convenience functions to make basic image processing operations such as translation, rotation, resizing, skeletonization, and displaying Matplotlib images easier with OpenCV and Python. https://github.com/jrosebr1/imutils. 2020 (cited on page 116).

304

Bibliography

[33] Adrian Rosebrock. Sorting Contours using Python and OpenCV. https://www.pyimagesearch.com/2015/04/20/sorting-contours-usingpython-and-opencv/. 2015 (cited on page 118). [34] OpenCV. OpenCV Structural Analysis and Shape Descriptors. https://docs.opencv.org/4.4.0/d3/dc0/group__imgproc__shape.html (cited on page 118). [35] Adrian Rosebrock. 4 Point OpenCV getPerspective Transform Example. https://www.pyimagesearch.com/2014/08/25/4-point-opencvgetperspective-transform-example/. 2014 (cited on page 120). [36] Adrian Rosebrock. Python, argparse, and command line arguments. https://www.pyimagesearch.com/2018/03/12/python-argparsecommand-line-arguments/. 2018 (cited on page 124). [37] Aakash Jhawar. Sudoku Solver using OpenCV and DL — Part 2. https://medium.com/@aakashjhawar/sudoku-solver-using-opencv-anddl-part-2-bbe0e6ac87c5. 2019 (cited on page 130). [38] re — Regular expression operations. https://docs.python.org/3/library/re.html. 2020 (cited on page 134). [39] John Sturtz. Regular Expressions: Regexes in Python (Part 1). https://realpython.com/regex-python/. 2020 (cited on page 140). [40] Mrinal Walia. How to Extract Email & Phone Number from a Business Card Using Python, OpenCV and TesseractOCR. https://datascienceplus.com/how-to-extract-email-phone-numberfrom-a-business-card-using-python-opencv-and-tesseractocr/. 2019 (cited on page 148). [41] Regular expression for first and last name. https://stackoverflow.com/questions/2385701/regular-expressionfor-first-and-last-name. 2020 (cited on page 148). [42] Adrian Rosebrock. OpenCV Vehicle Detection, Tracking, and Speed Estimation. https://www.pyimagesearch.com/2019/12/02/opencv-vehicledetection-tracking-and-speed-estimation/. 2019 (cited on page 155). [43] Cards on Cards. Anatomy of a Baseball Card: Michael Jordan 1990 "White Sox". http://cardsoncards.blogspot.com/2016/04/anatomy-of-baseballcard-michael-jordan.html. 2016 (cited on page 179). [44] Scikit-Learn Open Source Contributors. Agglomerative Clustering. https://scikit-learn.org/stable/modules/generated/sklearn. cluster.AgglomerativeClustering.html. 2020 (cited on pages 180, 190).

Bibliography

305

[45] Cory Maklin. Hierarchical Agglomerative Clustering Algorithm Example In Python. https://towardsdatascience.com/machine-learning-algorithms-part12-hierarchical-agglomerative-clustering-example-in-python1e18e0075019. 2018 (cited on page 180). [46] In: Data Mining: Practical Machine Learning Tools and Techniques (Third Edition). Edited by Ian H. Witten, Eibe Frank, and Mark A. Hall. Third Edition. The Morgan Kaufmann Series in Data Management Systems. Boston: Morgan Kaufmann, 2011, pages 587–605. ISBN: 978-0-12-374856-0. DOI : https://doi.org/10.1016/B978-0-12-374856-0.00023-7. URL: http: //www.sciencedirect.com/science/article/pii/B9780123748560000237 (cited on page 180). [47] Sergey Astanin. tabulate PyPI. https://pypi.org/project/tabulate/ (cited on page 181). [48] Adrian Rosebrock. Blur detection with OpenCV. https: //www.pyimagesearch.com/2015/09/07/blur-detection-with-opencv/. 2015 (cited on pages 198, 207). [49] John M. Brayer. Introduction to the Fourier Transform. https://www.cs.unm.edu/~brayer/vision/fourier.html. 2020 (cited on page 199). [50] Fourier Transform. https://homepages.inf.ed.ac.uk/rbf/HIPR2/fourier.htm. 2003 (cited on page 201). [51] Aaron Bobick. Frequency and Fourier Transforms. https://www.cc.gatech.edu/~afb/classes/CS4495Fall2014/slides/CS4495-Frequency.pdf (cited on page 201). [52] Grant Sanderson. But what is a Fourier series? From heat flow to circle drawings. https://www.youtube.com/watch?v=r6sGWTCMz2k. 2019 (cited on page 201). [53] Wikipedia Contributors. Fourier transform. https://en.wikipedia.org/wiki/Fourier_transform. 2020 (cited on page 201). [54] whdcumt. BlurDetection: A Python-Based Blur Detector using Fast Fourier Transforms. https://github.com/whdcumt/BlurDetection. 2015 (cited on page 202). [55] Renting Liu, Zhaorong Li, and Jiaya Jia. “Image partial blur detection and classification”. In: 2008 IEEE conference on computer vision and pattern recognition. IEEE. 2008, pages 1–8 (cited on page 202).

306

Bibliography

[56] Adrian Rosebrock. Working with Video. https://www.pyimagesearch.com/start-here/#working_with_video. 2020 (cited on page 211). [57] Adrian Rosebrock. How to use OpenCV’s “dnn” module with NVIDIA GPUs, CUDA, and cuDNN. https://www.pyimagesearch.com/2020/02/03/how-to-use-opencvs-dnnmodule-with-nvidia-gpus-cuda-and-cudnn/. 2020 (cited on page 228). [58] Amazon Developers. Amazon Rekognition. https://aws.amazon.com/rekognition/. 2020 (cited on page 256). [59] Yangqing Jia et al. “Caffe: Convolutional Architecture for Fast Feature Embedding”. In: Proceedings of the 22Nd ACM International Conference on Multimedia. MM ’14. Orlando, Florida, USA: ACM, 2014, pages 675–678. ISBN: 978-1-4503-3063-3. DOI : 10.1145/2647868.2654889. URL : http://doi.acm.org/10.1145/2647868.2654889 (cited on page 271). [60] TensorFlow Community. TensorFlow Object Detection API. https://github.com/ TensorFlow/models/tree/master/research/object_detection. 2020 (cited on page 271). [61] Homebrew. https://brew.sh. 2020 (cited on page 273). [62] Train Tesseract LSTM with make. https://github.com/tesseract-ocr/tesstrain. 2020 (cited on page 276). [63] Christopher Olah. Understanding LSTM Networks. https://colah.github.io/posts/2015-08-Understanding-LSTMs/. 2015 (cited on page 276). [64] Traineddata Files for Version 4.00 +. https://tesseract-ocr.github.io/tessdoc/Data-Files.html. 2020 (cited on page 280). [65] Andrej Karpathy. Arxiv Sanity Preserver. http://www.arxiv-sanity.com. 2020 (cited on page 300).