├── model ├── binary_128_0.50_labels_ver2.txt └── binary_128_0.50_ver3.pb ├── README.md └── main.py /model/binary_128_0.50_labels_ver2.txt: -------------------------------------------------------------------------------- 1 | 0 2 | 1 3 | 2 4 | 3 5 | 4 6 | 5 7 | 6 8 | 7 9 | 8 10 | 9 11 | A 12 | C 13 | E 14 | F -------------------------------------------------------------------------------- /model/binary_128_0.50_ver3.pb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hritik7080/Car-License-Plate-Recognition/HEAD/model/binary_128_0.50_ver3.pb -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Car License Plate Recognition 2 | 3 | Download the video for testing from here 4 | 5 | For detailed explanation and requirements, check my article @GeekforGeeks here 6 | 7 | ### Tech Stack 8 | Tech Stack used in this projects are:
9 | * Python
10 | * Computer Vision
11 | * Machine Learning
12 | 13 | ### Dependencies: 14 |
15 | opencv-python 3.4.2
16 | numpy 1.17.2
17 | skimage 0.16.2
18 | tensorflow 1.15.0
19 | imutils 0.5.3
20 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import cv2 2 | import numpy as np 3 | from skimage.filters import threshold_local 4 | import tensorflow as tf 5 | from skimage import measure 6 | import imutils 7 | 8 | def sort_cont(character_contours): 9 | """ 10 | To sort contours from left to right 11 | """ 12 | i = 0 13 | boundingBoxes = [cv2.boundingRect(c) for c in character_contours] 14 | (character_contours, boundingBoxes) = zip(*sorted(zip(character_contours, boundingBoxes), 15 | key=lambda b: b[1][i], reverse=False)) 16 | return character_contours 17 | 18 | 19 | def segment_chars(plate_img, fixed_width): 20 | """ 21 | extract Value channel from the HSV format of image and apply adaptive thresholding 22 | to reveal the characters on the license plate 23 | """ 24 | V = cv2.split(cv2.cvtColor(plate_img, cv2.COLOR_BGR2HSV))[2] 25 | 26 | T = threshold_local(V, 29, offset=15, method='gaussian') 27 | 28 | thresh = (V > T).astype('uint8') * 255 29 | 30 | thresh = cv2.bitwise_not(thresh) 31 | 32 | # resize the license plate region to a canoncial size 33 | plate_img = imutils.resize(plate_img, width=fixed_width) 34 | thresh = imutils.resize(thresh, width=fixed_width) 35 | bgr_thresh = cv2.cvtColor(thresh, cv2.COLOR_GRAY2BGR) 36 | 37 | # perform a connected components analysis and initialize the mask to store the locations 38 | # of the character candidates 39 | labels = measure.label(thresh, neighbors=8, background=0) 40 | 41 | charCandidates = np.zeros(thresh.shape, dtype='uint8') 42 | 43 | # loop over the unique components 44 | characters = [] 45 | for label in np.unique(labels): 46 | # if this is the background label, ignore it 47 | if label == 0: 48 | continue 49 | # otherwise, construct the label mask to display only connected components for the 50 | # current label, then find contours in the label mask 51 | labelMask = np.zeros(thresh.shape, dtype='uint8') 52 | labelMask[labels == label] = 255 53 | 54 | cnts = cv2.findContours(labelMask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) 55 | cnts = cnts[0] if imutils.is_cv2() else cnts[1] 56 | 57 | # ensure at least one contour was found in the mask 58 | if len(cnts) > 0: 59 | 60 | # grab the largest contour which corresponds to the component in the mask, then 61 | # grab the bounding box for the contour 62 | c = max(cnts, key=cv2.contourArea) 63 | (boxX, boxY, boxW, boxH) = cv2.boundingRect(c) 64 | 65 | # compute the aspect ratio, solodity, and height ration for the component 66 | aspectRatio = boxW / float(boxH) 67 | solidity = cv2.contourArea(c) / float(boxW * boxH) 68 | heightRatio = boxH / float(plate_img.shape[0]) 69 | 70 | # determine if the aspect ratio, solidity, and height of the contour pass 71 | # the rules tests 72 | keepAspectRatio = aspectRatio < 1.0 73 | keepSolidity = solidity > 0.15 74 | keepHeight = heightRatio > 0.5 and heightRatio < 0.95 75 | 76 | # check to see if the component passes all the tests 77 | if keepAspectRatio and keepSolidity and keepHeight and boxW > 14: 78 | # compute the convex hull of the contour and draw it on the character 79 | # candidates mask 80 | hull = cv2.convexHull(c) 81 | 82 | cv2.drawContours(charCandidates, [hull], -1, 255, -1) 83 | 84 | _, contours, hier = cv2.findContours(charCandidates, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) 85 | if contours: 86 | contours = sort_cont(contours) 87 | addPixel = 4 # value to be added to each dimension of the character 88 | for c in contours: 89 | (x, y, w, h) = cv2.boundingRect(c) 90 | if y > addPixel: 91 | y = y - addPixel 92 | else: 93 | y = 0 94 | if x > addPixel: 95 | x = x - addPixel 96 | else: 97 | x = 0 98 | temp = bgr_thresh[y:y + h + (addPixel * 2), x:x + w + (addPixel * 2)] 99 | 100 | characters.append(temp) 101 | return characters 102 | else: 103 | return None 104 | 105 | 106 | 107 | class PlateFinder: 108 | def __init__(self): 109 | self.min_area = 4500 # minimum area of the plate 110 | self.max_area = 30000 # maximum area of the plate 111 | 112 | self.element_structure = cv2.getStructuringElement(shape=cv2.MORPH_RECT, ksize=(22, 3)) 113 | 114 | def preprocess(self, input_img): 115 | imgBlurred = cv2.GaussianBlur(input_img, (7, 7), 0) # old window was (5,5) 116 | gray = cv2.cvtColor(imgBlurred, cv2.COLOR_BGR2GRAY) # convert to gray 117 | sobelx = cv2.Sobel(gray, cv2.CV_8U, 1, 0, ksize=3) # sobelX to get the vertical edges 118 | ret2, threshold_img = cv2.threshold(sobelx, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU) 119 | 120 | element = self.element_structure 121 | morph_n_thresholded_img = threshold_img.copy() 122 | cv2.morphologyEx(src=threshold_img, op=cv2.MORPH_CLOSE, kernel=element, dst=morph_n_thresholded_img) 123 | return morph_n_thresholded_img 124 | 125 | def extract_contours(self, after_preprocess): 126 | _, contours, _ = cv2.findContours(after_preprocess, mode=cv2.RETR_EXTERNAL, 127 | method=cv2.CHAIN_APPROX_NONE) 128 | return contours 129 | 130 | def clean_plate(self, plate): 131 | gray = cv2.cvtColor(plate, cv2.COLOR_BGR2GRAY) 132 | thresh = cv2.adaptiveThreshold(gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 11, 2) 133 | _, contours, _ = cv2.findContours(thresh.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE) 134 | 135 | if contours: 136 | areas = [cv2.contourArea(c) for c in contours] 137 | max_index = np.argmax(areas) # index of the largest contour in the area array 138 | 139 | max_cnt = contours[max_index] 140 | max_cntArea = areas[max_index] 141 | x, y, w, h = cv2.boundingRect(max_cnt) 142 | rect = cv2.minAreaRect(max_cnt) 143 | rotatedPlate = plate 144 | if not self.ratioCheck(max_cntArea, rotatedPlate.shape[1], rotatedPlate.shape[0]): 145 | return plate, False, None 146 | return rotatedPlate, True, [x, y, w, h] 147 | else: 148 | return plate, False, None 149 | 150 | 151 | 152 | def check_plate(self, input_img, contour): 153 | min_rect = cv2.minAreaRect(contour) 154 | if self.validateRatio(min_rect): 155 | x, y, w, h = cv2.boundingRect(contour) 156 | after_validation_img = input_img[y:y + h, x:x + w] 157 | after_clean_plate_img, plateFound, coordinates = self.clean_plate(after_validation_img) 158 | if plateFound: 159 | characters_on_plate = self.find_characters_on_plate(after_clean_plate_img) 160 | if (characters_on_plate is not None and len(characters_on_plate) == 8): 161 | x1, y1, w1, h1 = coordinates 162 | coordinates = x1 + x, y1 + y 163 | after_check_plate_img = after_clean_plate_img 164 | return after_check_plate_img, characters_on_plate, coordinates 165 | return None, None, None 166 | 167 | 168 | 169 | def find_possible_plates(self, input_img): 170 | """ 171 | Finding all possible contours that can be plates 172 | """ 173 | plates = [] 174 | self.char_on_plate = [] 175 | self.corresponding_area = [] 176 | 177 | self.after_preprocess = self.preprocess(input_img) 178 | possible_plate_contours = self.extract_contours(self.after_preprocess) 179 | 180 | for cnts in possible_plate_contours: 181 | plate, characters_on_plate, coordinates = self.check_plate(input_img, cnts) 182 | if plate is not None: 183 | plates.append(plate) 184 | self.char_on_plate.append(characters_on_plate) 185 | self.corresponding_area.append(coordinates) 186 | 187 | if (len(plates) > 0): 188 | return plates 189 | else: 190 | return None 191 | 192 | def find_characters_on_plate(self, plate): 193 | 194 | charactersFound = segment_chars(plate, 400) 195 | if charactersFound: 196 | return charactersFound 197 | 198 | # PLATE FEATURES 199 | def ratioCheck(self, area, width, height): 200 | min = self.min_area 201 | max = self.max_area 202 | 203 | ratioMin = 3 204 | ratioMax = 6 205 | 206 | ratio = float(width) / float(height) 207 | if ratio < 1: 208 | ratio = 1 / ratio 209 | 210 | if (area < min or area > max) or (ratio < ratioMin or ratio > ratioMax): 211 | return False 212 | return True 213 | 214 | def preRatioCheck(self, area, width, height): 215 | min = self.min_area 216 | max = self.max_area 217 | 218 | ratioMin = 2.5 219 | ratioMax = 7 220 | 221 | ratio = float(width) / float(height) 222 | if ratio < 1: 223 | ratio = 1 / ratio 224 | 225 | if (area < min or area > max) or (ratio < ratioMin or ratio > ratioMax): 226 | return False 227 | return True 228 | 229 | def validateRatio(self, rect): 230 | (x, y), (width, height), rect_angle = rect 231 | 232 | if (width > height): 233 | angle = -rect_angle 234 | else: 235 | angle = 90 + rect_angle 236 | 237 | if angle > 15: 238 | return False 239 | if (height == 0 or width == 0): 240 | return False 241 | 242 | area = width * height 243 | if not self.preRatioCheck(area, width, height): 244 | return False 245 | else: 246 | return True 247 | 248 | 249 | 250 | 251 | 252 | class NeuralNetwork: 253 | def __init__(self): 254 | self.model_file = "./model/binary_128_0.50_ver3.pb" 255 | self.label_file = "./model/binary_128_0.50_labels_ver2.txt" 256 | self.label = self.load_label(self.label_file) 257 | self.graph = self.load_graph(self.model_file) 258 | self.sess = tf.Session(graph=self.graph) 259 | 260 | def load_graph(self, modelFile): 261 | graph = tf.Graph() 262 | graph_def = tf.GraphDef() 263 | with open(modelFile, "rb") as f: 264 | graph_def.ParseFromString(f.read()) 265 | with graph.as_default(): 266 | tf.import_graph_def(graph_def) 267 | return graph 268 | 269 | def load_label(self, labelFile): 270 | label = [] 271 | proto_as_ascii_lines = tf.gfile.GFile(labelFile).readlines() 272 | for l in proto_as_ascii_lines: 273 | label.append(l.rstrip()) 274 | return label 275 | 276 | def convert_tensor(self, image, imageSizeOuput): 277 | """ 278 | takes an image and tranform it in tensor 279 | """ 280 | image = cv2.resize(image, dsize=(imageSizeOuput, imageSizeOuput), interpolation=cv2.INTER_CUBIC) 281 | np_image_data = np.asarray(image) 282 | np_image_data = cv2.normalize(np_image_data.astype('float'), None, -0.5, .5, cv2.NORM_MINMAX) 283 | np_final = np.expand_dims(np_image_data, axis=0) 284 | return np_final 285 | 286 | def label_image(self, tensor): 287 | 288 | input_name = "import/input" 289 | output_name = "import/final_result" 290 | 291 | input_operation = self.graph.get_operation_by_name(input_name) 292 | output_operation = self.graph.get_operation_by_name(output_name) 293 | 294 | results = self.sess.run(output_operation.outputs[0], 295 | {input_operation.outputs[0]: tensor}) 296 | results = np.squeeze(results) 297 | labels = self.label 298 | top = results.argsort()[-1:][::-1] 299 | return labels[top[0]] 300 | 301 | def label_image_list(self, listImages, imageSizeOuput): 302 | plate = "" 303 | for img in listImages: 304 | if cv2.waitKey(25) & 0xFF == ord('q'): 305 | break 306 | plate = plate + self.label_image(self.convert_tensor(img, imageSizeOuput)) 307 | return plate, len(plate) 308 | 309 | 310 | if __name__ == "__main__": 311 | findPlate = PlateFinder() 312 | 313 | # Initialize the Neural Network 314 | model = NeuralNetwork() 315 | 316 | cap = cv2.VideoCapture('test_videos/test.MOV') 317 | while (cap.isOpened()): 318 | ret, img = cap.read() 319 | if ret == True: 320 | cv2.imshow('original video', img) 321 | if cv2.waitKey(25) & 0xFF == ord('q'): 322 | break 323 | # cv2.waitKey(0) 324 | possible_plates = findPlate.find_possible_plates(img) 325 | if possible_plates is not None: 326 | for i, p in enumerate(possible_plates): 327 | chars_on_plate = findPlate.char_on_plate[i] 328 | recognized_plate, _ = model.label_image_list(chars_on_plate, imageSizeOuput=128) 329 | print(recognized_plate) 330 | cv2.imshow('plate', p) 331 | if cv2.waitKey(25) & 0xFF == ord('q'): 332 | break 333 | 334 | 335 | else: 336 | break 337 | cap.release() 338 | cv2.destroyAllWindows() 339 | --------------------------------------------------------------------------------