├── README.md ├── common.py ├── detect.py ├── in.jpg ├── model.py └── out.jpg /README.md: -------------------------------------------------------------------------------- 1 | # Tensorflow-Number-Plate-Recognition 2 | 3 | ## Dependencies 4 | ``` 5 | TensorFlow 1.0 6 | OpenCV 7 | NumPy 8 | ``` 9 | 10 | ## Usage 11 | 1. Download pretrained model `weights.npz` from [here](https://drive.google.com/file/d/0B-MtVXQMUxQiZElfSy1ON09QQ0U/view?usp=sharing). 12 | 2. Place `weights.npz` in the same directory of `source.py`. 13 | 3. `./detect.py in.jpg weights.npz out.jpg` 14 | 15 | ## Other 16 | 1. Codes are credited to [Matthew Earl](https://github.com/matthewearl)'s [Deep Anpr](https://github.com/matthewearl/deep-anpr). 17 | 2. To genereate your own training dataset, please refer to the original repository and retrain you model. 18 | -------------------------------------------------------------------------------- /common.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2016 Matthew Earl 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included 11 | # in all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 14 | # OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 15 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN 16 | # NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 17 | # DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 18 | # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE 19 | # USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | 21 | """ 22 | Definitions that don't fit elsewhere. 23 | 24 | """ 25 | 26 | __all__ = ( 27 | 'DIGITS', 28 | 'LETTERS', 29 | 'CHARS', 30 | 'sigmoid', 31 | 'softmax', 32 | ) 33 | 34 | import numpy 35 | 36 | 37 | DIGITS = "0123456789" 38 | LETTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" 39 | CHARS = LETTERS + DIGITS 40 | 41 | def softmax(a): 42 | exps = numpy.exp(a.astype(numpy.float64)) 43 | return exps / numpy.sum(exps, axis=-1)[:, numpy.newaxis] 44 | 45 | def sigmoid(a): 46 | return 1. / (1. + numpy.exp(-a)) 47 | 48 | -------------------------------------------------------------------------------- /detect.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright (c) 2016 Matthew Earl 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included 13 | # in all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 16 | # OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN 18 | # NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 19 | # DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 20 | # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE 21 | # USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | 23 | 24 | """ 25 | Routines to detect number plates. 26 | 27 | Use `detect` to detect all bounding boxes, and use `post_process` on the output 28 | of `detect` to filter using non-maximum suppression. 29 | 30 | """ 31 | 32 | 33 | __all__ = ( 34 | 'detect', 35 | 'post_process', 36 | ) 37 | 38 | 39 | import collections 40 | import itertools 41 | import math 42 | import sys 43 | 44 | import cv2 45 | import numpy 46 | import tensorflow as tf 47 | 48 | import common 49 | import model 50 | 51 | 52 | def make_scaled_ims(im, min_shape): 53 | ratio = 1. / 2 ** 0.5 54 | shape = (im.shape[0] / ratio, im.shape[1] / ratio) 55 | 56 | while True: 57 | shape = (int(shape[0] * ratio), int(shape[1] * ratio)) 58 | if shape[0] < min_shape[0] or shape[1] < min_shape[1]: 59 | break 60 | yield cv2.resize(im, (shape[1], shape[0])) 61 | 62 | 63 | def detect(im, param_vals): 64 | """ 65 | Detect number plates in an image. 66 | 67 | :param im: 68 | Image to detect number plates in. 69 | 70 | :param param_vals: 71 | Model parameters to use. These are the parameters output by the `train` 72 | module. 73 | 74 | :returns: 75 | Iterable of `bbox_tl, bbox_br, letter_probs`, defining the bounding box 76 | top-left and bottom-right corners respectively, and a 7,36 matrix 77 | giving the probability distributions of each letter. 78 | 79 | """ 80 | 81 | # Convert the image to various scales. 82 | scaled_ims = list(make_scaled_ims(im, model.WINDOW_SHAPE)) 83 | 84 | # Load the model which detects number plates over a sliding window. 85 | x, y, params = model.get_detect_model() 86 | 87 | # Execute the model at each scale. 88 | with tf.Session(config=tf.ConfigProto()) as sess: 89 | y_vals = [] 90 | for scaled_im in scaled_ims: 91 | feed_dict = {x: numpy.stack([scaled_im])} 92 | feed_dict.update(dict(zip(params, param_vals))) 93 | y_vals.append(sess.run(y, feed_dict=feed_dict)) 94 | writer = tf.summary.FileWriter("logs/", sess.graph) 95 | 96 | # Interpret the results in terms of bounding boxes in the input image. 97 | # Do this by identifying windows (at all scales) where the model predicts a 98 | # number plate has a greater than 50% probability of appearing. 99 | # 100 | # To obtain pixel coordinates, the window coordinates are scaled according 101 | # to the stride size, and pixel coordinates. 102 | for i, (scaled_im, y_val) in enumerate(zip(scaled_ims, y_vals)): 103 | for window_coords in numpy.argwhere(y_val[0, :, :, 0] > 104 | -math.log(1./0.99 - 1)): 105 | letter_probs = (y_val[0, 106 | window_coords[0], 107 | window_coords[1], 1:].reshape( 108 | 7, len(common.CHARS))) 109 | letter_probs = common.softmax(letter_probs) 110 | 111 | img_scale = float(im.shape[0]) / scaled_im.shape[0] 112 | 113 | bbox_tl = window_coords * (8, 4) * img_scale 114 | bbox_size = numpy.array(model.WINDOW_SHAPE) * img_scale 115 | 116 | present_prob = common.sigmoid( 117 | y_val[0, window_coords[0], window_coords[1], 0]) 118 | 119 | yield bbox_tl, bbox_tl + bbox_size, present_prob, letter_probs 120 | 121 | 122 | def _overlaps(match1, match2): 123 | bbox_tl1, bbox_br1, _, _ = match1 124 | bbox_tl2, bbox_br2, _, _ = match2 125 | return (bbox_br1[0] > bbox_tl2[0] and 126 | bbox_br2[0] > bbox_tl1[0] and 127 | bbox_br1[1] > bbox_tl2[1] and 128 | bbox_br2[1] > bbox_tl1[1]) 129 | 130 | 131 | def _group_overlapping_rectangles(matches): 132 | matches = list(matches) 133 | num_groups = 0 134 | match_to_group = {} 135 | for idx1 in range(len(matches)): 136 | for idx2 in range(idx1): 137 | if _overlaps(matches[idx1], matches[idx2]): 138 | match_to_group[idx1] = match_to_group[idx2] 139 | break 140 | else: 141 | match_to_group[idx1] = num_groups 142 | num_groups += 1 143 | 144 | groups = collections.defaultdict(list) 145 | for idx, group in match_to_group.items(): 146 | groups[group].append(matches[idx]) 147 | 148 | return groups 149 | 150 | 151 | def post_process(matches): 152 | """ 153 | Take an iterable of matches as returned by `detect` and merge duplicates. 154 | 155 | Merging consists of two steps: 156 | - Finding sets of overlapping rectangles. 157 | - Finding the intersection of those sets, along with the code 158 | corresponding with the rectangle with the highest presence parameter. 159 | 160 | """ 161 | groups = _group_overlapping_rectangles(matches) 162 | 163 | for group_matches in groups.values(): 164 | mins = numpy.stack(numpy.array(m[0]) for m in group_matches) 165 | maxs = numpy.stack(numpy.array(m[1]) for m in group_matches) 166 | present_probs = numpy.array([m[2] for m in group_matches]) 167 | letter_probs = numpy.stack(m[3] for m in group_matches) 168 | 169 | yield (numpy.max(mins, axis=0).flatten(), 170 | numpy.min(maxs, axis=0).flatten(), 171 | numpy.max(present_probs), 172 | letter_probs[numpy.argmax(present_probs)]) 173 | 174 | 175 | def letter_probs_to_code(letter_probs): 176 | return "".join(common.CHARS[i] for i in numpy.argmax(letter_probs, axis=1)) 177 | 178 | 179 | if __name__ == "__main__": 180 | im = cv2.imread(sys.argv[1]) 181 | im_gray = cv2.cvtColor(im, cv2.COLOR_BGR2GRAY) / 255. 182 | 183 | f = numpy.load(sys.argv[2]) 184 | param_vals = [f[n] for n in sorted(f.files, key=lambda s: int(s[4:]))] 185 | print ("RUN") 186 | 187 | for pt1, pt2, present_prob, letter_probs in post_process( 188 | detect(im_gray, param_vals)): 189 | pt1 = tuple(reversed(map(int, pt1))) 190 | pt2 = tuple(reversed(map(int, pt2))) 191 | 192 | code = letter_probs_to_code(letter_probs) 193 | 194 | color = (0.0, 255.0, 0.0) 195 | cv2.rectangle(im, pt1, pt2, color) 196 | 197 | cv2.putText(im, 198 | code, 199 | pt1, 200 | cv2.FONT_HERSHEY_PLAIN, 201 | 1.5, 202 | (0, 0, 0), 203 | thickness=5) 204 | 205 | cv2.putText(im, 206 | code, 207 | pt1, 208 | cv2.FONT_HERSHEY_PLAIN, 209 | 1.5, 210 | (255, 255, 255), 211 | thickness=2) 212 | 213 | cv2.imwrite(sys.argv[3], im) 214 | -------------------------------------------------------------------------------- /in.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnjieCheng/Tensorflow-Number-Plate-Recognition/a22fc225a568625dc0709bf34977244f13b4d455/in.jpg -------------------------------------------------------------------------------- /model.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2016 Matthew Earl 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included 11 | # in all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 14 | # OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 15 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN 16 | # NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 17 | # DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 18 | # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE 19 | # USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | 21 | 22 | """ 23 | Definition of the neural networks. 24 | 25 | """ 26 | 27 | 28 | __all__ = ( 29 | 'get_training_model', 30 | 'get_detect_model', 31 | 'WINDOW_SHAPE', 32 | ) 33 | 34 | 35 | import tensorflow as tf 36 | 37 | import common 38 | 39 | 40 | WINDOW_SHAPE = (64, 128) 41 | 42 | 43 | # Utility functions 44 | def weight_variable(shape): 45 | initial = tf.truncated_normal(shape, stddev=0.1) 46 | return tf.Variable(initial) 47 | 48 | 49 | def bias_variable(shape): 50 | initial = tf.constant(0.1, shape=shape) 51 | return tf.Variable(initial) 52 | 53 | 54 | def conv2d(x, W, stride=(1, 1), padding='SAME'): 55 | return tf.nn.conv2d(x, W, strides=[1, stride[0], stride[1], 1], 56 | padding=padding) 57 | 58 | 59 | def max_pool(x, ksize=(2, 2), stride=(2, 2)): 60 | return tf.nn.max_pool(x, ksize=[1, ksize[0], ksize[1], 1], 61 | strides=[1, stride[0], stride[1], 1], padding='SAME') 62 | 63 | 64 | def avg_pool(x, ksize=(2, 2), stride=(2, 2)): 65 | return tf.nn.avg_pool(x, ksize=[1, ksize[0], ksize[1], 1], 66 | strides=[1, stride[0], stride[1], 1], padding='SAME') 67 | 68 | 69 | def convolutional_layers(): 70 | """ 71 | Get the convolutional layers of the model. 72 | 73 | """ 74 | x = tf.placeholder(tf.float32, [None, None, None]) 75 | 76 | # First layer 77 | W_conv1 = weight_variable([5, 5, 1, 48]) 78 | b_conv1 = bias_variable([48]) 79 | x_expanded = tf.expand_dims(x, 3) 80 | h_conv1 = tf.nn.relu(conv2d(x_expanded, W_conv1) + b_conv1) 81 | h_pool1 = max_pool(h_conv1, ksize=(2, 2), stride=(2, 2)) 82 | 83 | # Second layer 84 | W_conv2 = weight_variable([5, 5, 48, 64]) 85 | b_conv2 = bias_variable([64]) 86 | 87 | h_conv2 = tf.nn.relu(conv2d(h_pool1, W_conv2) + b_conv2) 88 | h_pool2 = max_pool(h_conv2, ksize=(2, 1), stride=(2, 1)) 89 | 90 | # Third layer 91 | W_conv3 = weight_variable([5, 5, 64, 128]) 92 | b_conv3 = bias_variable([128]) 93 | 94 | h_conv3 = tf.nn.relu(conv2d(h_pool2, W_conv3) + b_conv3) 95 | h_pool3 = max_pool(h_conv3, ksize=(2, 2), stride=(2, 2)) 96 | 97 | return x, h_pool3, [W_conv1, b_conv1, 98 | W_conv2, b_conv2, 99 | W_conv3, b_conv3] 100 | 101 | 102 | def get_training_model(): 103 | """ 104 | The training model acts on a batch of 128x64 windows, and outputs a (1 + 105 | 7 * len(common.CHARS) vector, `v`. `v[0]` is the probability that a plate is 106 | fully within the image and is at the correct scale. 107 | 108 | `v[1 + i * len(common.CHARS) + c]` is the probability that the `i`'th 109 | character is `c`. 110 | 111 | """ 112 | x, conv_layer, conv_vars = convolutional_layers() 113 | 114 | # Densely connected layer 115 | W_fc1 = weight_variable([32 * 8 * 128, 2048]) 116 | b_fc1 = bias_variable([2048]) 117 | 118 | conv_layer_flat = tf.reshape(conv_layer, [-1, 32 * 8 * 128]) 119 | h_fc1 = tf.nn.relu(tf.matmul(conv_layer_flat, W_fc1) + b_fc1) 120 | 121 | # Output layer 122 | W_fc2 = weight_variable([2048, 1 + 7 * len(common.CHARS)]) 123 | b_fc2 = bias_variable([1 + 7 * len(common.CHARS)]) 124 | 125 | y = tf.matmul(h_fc1, W_fc2) + b_fc2 126 | 127 | return (x, y, conv_vars + [W_fc1, b_fc1, W_fc2, b_fc2]) 128 | 129 | 130 | def get_detect_model(): 131 | """ 132 | The same as the training model, except it acts on an arbitrarily sized 133 | input, and slides the 128x64 window across the image in 8x8 strides. 134 | 135 | The output is of the form `v`, where `v[i, j]` is equivalent to the output 136 | of the training model, for the window at coordinates `(8 * i, 4 * j)`. 137 | 138 | """ 139 | x, conv_layer, conv_vars = convolutional_layers() 140 | 141 | # Fourth layer 142 | W_fc1 = weight_variable([8 * 32 * 128, 2048]) 143 | W_conv1 = tf.reshape(W_fc1, [8, 32, 128, 2048]) 144 | b_fc1 = bias_variable([2048]) 145 | h_conv1 = tf.nn.relu(conv2d(conv_layer, W_conv1, 146 | stride=(1, 1), padding="VALID") + b_fc1) 147 | # Fifth layer 148 | W_fc2 = weight_variable([2048, 1 + 7 * len(common.CHARS)]) 149 | W_conv2 = tf.reshape(W_fc2, [1, 1, 2048, 1 + 7 * len(common.CHARS)]) 150 | b_fc2 = bias_variable([1 + 7 * len(common.CHARS)]) 151 | h_conv2 = conv2d(h_conv1, W_conv2) + b_fc2 152 | 153 | return (x, h_conv2, conv_vars + [W_fc1, b_fc1, W_fc2, b_fc2]) 154 | 155 | -------------------------------------------------------------------------------- /out.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnjieCheng/Tensorflow-Number-Plate-Recognition/a22fc225a568625dc0709bf34977244f13b4d455/out.jpg --------------------------------------------------------------------------------