├── DetectChars.py ├── DetectPlates.py ├── DocsAndPresentation ├── Steps With Images.docx ├── Steps With Images.pdf ├── steps.png └── steps.txt ├── LicPlateImages ├── 1.png ├── 10.png ├── 11.png ├── 12.png ├── 13.png ├── 14.png ├── 15.png ├── 16.png ├── 2.png ├── 3.png ├── 4.png ├── 5.png ├── 6.png ├── 7.png ├── 8.png └── 9.png ├── Main.py ├── PossibleChar.py ├── PossiblePlate.py ├── Preprocess.py ├── classifications.txt ├── flattened_images.txt └── readme.txt /DetectChars.py: -------------------------------------------------------------------------------- 1 | # DetectChars.py 2 | import os 3 | 4 | import cv2 5 | import numpy as np 6 | import math 7 | import random 8 | 9 | import Main 10 | import Preprocess 11 | import PossibleChar 12 | 13 | # module level variables ########################################################################## 14 | 15 | kNearest = cv2.ml.KNearest_create() 16 | 17 | # constants for checkIfPossibleChar, this checks one possible char only (does not compare to another char) 18 | MIN_PIXEL_WIDTH = 2 19 | MIN_PIXEL_HEIGHT = 8 20 | 21 | MIN_ASPECT_RATIO = 0.25 22 | MAX_ASPECT_RATIO = 1.0 23 | 24 | MIN_PIXEL_AREA = 80 25 | 26 | # constants for comparing two chars 27 | MIN_DIAG_SIZE_MULTIPLE_AWAY = 0.3 28 | MAX_DIAG_SIZE_MULTIPLE_AWAY = 5.0 29 | 30 | MAX_CHANGE_IN_AREA = 0.5 31 | 32 | MAX_CHANGE_IN_WIDTH = 0.8 33 | MAX_CHANGE_IN_HEIGHT = 0.2 34 | 35 | MAX_ANGLE_BETWEEN_CHARS = 12.0 36 | 37 | # other constants 38 | MIN_NUMBER_OF_MATCHING_CHARS = 3 39 | 40 | RESIZED_CHAR_IMAGE_WIDTH = 20 41 | RESIZED_CHAR_IMAGE_HEIGHT = 30 42 | 43 | MIN_CONTOUR_AREA = 100 44 | 45 | ################################################################################################### 46 | def loadKNNDataAndTrainKNN(): 47 | allContoursWithData = [] # declare empty lists, 48 | validContoursWithData = [] # we will fill these shortly 49 | 50 | try: 51 | npaClassifications = np.loadtxt("classifications.txt", np.float32) # read in training classifications 52 | except: # if file could not be opened 53 | print("error, unable to open classifications.txt, exiting program\n") # show error message 54 | os.system("pause") 55 | return False # and return False 56 | # end try 57 | 58 | try: 59 | npaFlattenedImages = np.loadtxt("flattened_images.txt", np.float32) # read in training images 60 | except: # if file could not be opened 61 | print("error, unable to open flattened_images.txt, exiting program\n") # show error message 62 | os.system("pause") 63 | return False # and return False 64 | # end try 65 | 66 | npaClassifications = npaClassifications.reshape((npaClassifications.size, 1)) # reshape numpy array to 1d, necessary to pass to call to train 67 | 68 | kNearest.setDefaultK(1) # set default K to 1 69 | 70 | kNearest.train(npaFlattenedImages, cv2.ml.ROW_SAMPLE, npaClassifications) # train KNN object 71 | 72 | return True # if we got here training was successful so return true 73 | # end function 74 | 75 | ################################################################################################### 76 | def detectCharsInPlates(listOfPossiblePlates): 77 | intPlateCounter = 0 78 | imgContours = None 79 | contours = [] 80 | 81 | if len(listOfPossiblePlates) == 0: # if list of possible plates is empty 82 | return listOfPossiblePlates # return 83 | # end if 84 | 85 | # at this point we can be sure the list of possible plates has at least one plate 86 | 87 | for possiblePlate in listOfPossiblePlates: # for each possible plate, this is a big for loop that takes up most of the function 88 | 89 | possiblePlate.imgGrayscale, possiblePlate.imgThresh = Preprocess.preprocess(possiblePlate.imgPlate) # preprocess to get grayscale and threshold images 90 | 91 | if Main.showSteps == True: # show steps ################################################### 92 | cv2.imshow("5a", possiblePlate.imgPlate) 93 | cv2.imshow("5b", possiblePlate.imgGrayscale) 94 | cv2.imshow("5c", possiblePlate.imgThresh) 95 | # end if # show steps ##################################################################### 96 | 97 | # increase size of plate image for easier viewing and char detection 98 | possiblePlate.imgThresh = cv2.resize(possiblePlate.imgThresh, (0, 0), fx = 1.6, fy = 1.6) 99 | 100 | # threshold again to eliminate any gray areas 101 | thresholdValue, possiblePlate.imgThresh = cv2.threshold(possiblePlate.imgThresh, 0.0, 255.0, cv2.THRESH_BINARY | cv2.THRESH_OTSU) 102 | 103 | if Main.showSteps == True: # show steps ################################################### 104 | cv2.imshow("5d", possiblePlate.imgThresh) 105 | # end if # show steps ##################################################################### 106 | 107 | # find all possible chars in the plate, 108 | # this function first finds all contours, then only includes contours that could be chars (without comparison to other chars yet) 109 | listOfPossibleCharsInPlate = findPossibleCharsInPlate(possiblePlate.imgGrayscale, possiblePlate.imgThresh) 110 | 111 | if Main.showSteps == True: # show steps ################################################### 112 | height, width, numChannels = possiblePlate.imgPlate.shape 113 | imgContours = np.zeros((height, width, 3), np.uint8) 114 | del contours[:] # clear the contours list 115 | 116 | for possibleChar in listOfPossibleCharsInPlate: 117 | contours.append(possibleChar.contour) 118 | # end for 119 | 120 | cv2.drawContours(imgContours, contours, -1, Main.SCALAR_WHITE) 121 | 122 | cv2.imshow("6", imgContours) 123 | # end if # show steps ##################################################################### 124 | 125 | # given a list of all possible chars, find groups of matching chars within the plate 126 | listOfListsOfMatchingCharsInPlate = findListOfListsOfMatchingChars(listOfPossibleCharsInPlate) 127 | 128 | if Main.showSteps == True: # show steps ################################################### 129 | imgContours = np.zeros((height, width, 3), np.uint8) 130 | del contours[:] 131 | 132 | for listOfMatchingChars in listOfListsOfMatchingCharsInPlate: 133 | intRandomBlue = random.randint(0, 255) 134 | intRandomGreen = random.randint(0, 255) 135 | intRandomRed = random.randint(0, 255) 136 | 137 | for matchingChar in listOfMatchingChars: 138 | contours.append(matchingChar.contour) 139 | # end for 140 | cv2.drawContours(imgContours, contours, -1, (intRandomBlue, intRandomGreen, intRandomRed)) 141 | # end for 142 | cv2.imshow("7", imgContours) 143 | # end if # show steps ##################################################################### 144 | 145 | if (len(listOfListsOfMatchingCharsInPlate) == 0): # if no groups of matching chars were found in the plate 146 | 147 | if Main.showSteps == True: # show steps ############################################### 148 | print("chars found in plate number " + str( 149 | intPlateCounter) + " = (none), click on any image and press a key to continue . . .") 150 | intPlateCounter = intPlateCounter + 1 151 | cv2.destroyWindow("8") 152 | cv2.destroyWindow("9") 153 | cv2.destroyWindow("10") 154 | cv2.waitKey(0) 155 | # end if # show steps ################################################################# 156 | 157 | possiblePlate.strChars = "" 158 | continue # go back to top of for loop 159 | # end if 160 | 161 | for i in range(0, len(listOfListsOfMatchingCharsInPlate)): # within each list of matching chars 162 | listOfListsOfMatchingCharsInPlate[i].sort(key = lambda matchingChar: matchingChar.intCenterX) # sort chars from left to right 163 | listOfListsOfMatchingCharsInPlate[i] = removeInnerOverlappingChars(listOfListsOfMatchingCharsInPlate[i]) # and remove inner overlapping chars 164 | # end for 165 | 166 | if Main.showSteps == True: # show steps ################################################### 167 | imgContours = np.zeros((height, width, 3), np.uint8) 168 | 169 | for listOfMatchingChars in listOfListsOfMatchingCharsInPlate: 170 | intRandomBlue = random.randint(0, 255) 171 | intRandomGreen = random.randint(0, 255) 172 | intRandomRed = random.randint(0, 255) 173 | 174 | del contours[:] 175 | 176 | for matchingChar in listOfMatchingChars: 177 | contours.append(matchingChar.contour) 178 | # end for 179 | 180 | cv2.drawContours(imgContours, contours, -1, (intRandomBlue, intRandomGreen, intRandomRed)) 181 | # end for 182 | cv2.imshow("8", imgContours) 183 | # end if # show steps ##################################################################### 184 | 185 | # within each possible plate, suppose the longest list of potential matching chars is the actual list of chars 186 | intLenOfLongestListOfChars = 0 187 | intIndexOfLongestListOfChars = 0 188 | 189 | # loop through all the vectors of matching chars, get the index of the one with the most chars 190 | for i in range(0, len(listOfListsOfMatchingCharsInPlate)): 191 | if len(listOfListsOfMatchingCharsInPlate[i]) > intLenOfLongestListOfChars: 192 | intLenOfLongestListOfChars = len(listOfListsOfMatchingCharsInPlate[i]) 193 | intIndexOfLongestListOfChars = i 194 | # end if 195 | # end for 196 | 197 | # suppose that the longest list of matching chars within the plate is the actual list of chars 198 | longestListOfMatchingCharsInPlate = listOfListsOfMatchingCharsInPlate[intIndexOfLongestListOfChars] 199 | 200 | if Main.showSteps == True: # show steps ################################################### 201 | imgContours = np.zeros((height, width, 3), np.uint8) 202 | del contours[:] 203 | 204 | for matchingChar in longestListOfMatchingCharsInPlate: 205 | contours.append(matchingChar.contour) 206 | # end for 207 | 208 | cv2.drawContours(imgContours, contours, -1, Main.SCALAR_WHITE) 209 | 210 | cv2.imshow("9", imgContours) 211 | # end if # show steps ##################################################################### 212 | 213 | possiblePlate.strChars = recognizeCharsInPlate(possiblePlate.imgThresh, longestListOfMatchingCharsInPlate) 214 | 215 | if Main.showSteps == True: # show steps ################################################### 216 | print("chars found in plate number " + str( 217 | intPlateCounter) + " = " + possiblePlate.strChars + ", click on any image and press a key to continue . . .") 218 | intPlateCounter = intPlateCounter + 1 219 | cv2.waitKey(0) 220 | # end if # show steps ##################################################################### 221 | 222 | # end of big for loop that takes up most of the function 223 | 224 | if Main.showSteps == True: 225 | print("\nchar detection complete, click on any image and press a key to continue . . .\n") 226 | cv2.waitKey(0) 227 | # end if 228 | 229 | return listOfPossiblePlates 230 | # end function 231 | 232 | ################################################################################################### 233 | def findPossibleCharsInPlate(imgGrayscale, imgThresh): 234 | listOfPossibleChars = [] # this will be the return value 235 | contours = [] 236 | imgThreshCopy = imgThresh.copy() 237 | 238 | # find all contours in plate 239 | contours, npaHierarchy = cv2.findContours(imgThreshCopy, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE) 240 | 241 | for contour in contours: # for each contour 242 | possibleChar = PossibleChar.PossibleChar(contour) 243 | 244 | if checkIfPossibleChar(possibleChar): # if contour is a possible char, note this does not compare to other chars (yet) . . . 245 | listOfPossibleChars.append(possibleChar) # add to list of possible chars 246 | # end if 247 | # end if 248 | 249 | return listOfPossibleChars 250 | # end function 251 | 252 | ################################################################################################### 253 | def checkIfPossibleChar(possibleChar): 254 | # this function is a 'first pass' that does a rough check on a contour to see if it could be a char, 255 | # note that we are not (yet) comparing the char to other chars to look for a group 256 | if (possibleChar.intBoundingRectArea > MIN_PIXEL_AREA and 257 | possibleChar.intBoundingRectWidth > MIN_PIXEL_WIDTH and possibleChar.intBoundingRectHeight > MIN_PIXEL_HEIGHT and 258 | MIN_ASPECT_RATIO < possibleChar.fltAspectRatio and possibleChar.fltAspectRatio < MAX_ASPECT_RATIO): 259 | return True 260 | else: 261 | return False 262 | # end if 263 | # end function 264 | 265 | ################################################################################################### 266 | def findListOfListsOfMatchingChars(listOfPossibleChars): 267 | # with this function, we start off with all the possible chars in one big list 268 | # the purpose of this function is to re-arrange the one big list of chars into a list of lists of matching chars, 269 | # note that chars that are not found to be in a group of matches do not need to be considered further 270 | listOfListsOfMatchingChars = [] # this will be the return value 271 | 272 | for possibleChar in listOfPossibleChars: # for each possible char in the one big list of chars 273 | listOfMatchingChars = findListOfMatchingChars(possibleChar, listOfPossibleChars) # find all chars in the big list that match the current char 274 | 275 | listOfMatchingChars.append(possibleChar) # also add the current char to current possible list of matching chars 276 | 277 | if len(listOfMatchingChars) < MIN_NUMBER_OF_MATCHING_CHARS: # if current possible list of matching chars is not long enough to constitute a possible plate 278 | continue # jump back to the top of the for loop and try again with next char, note that it's not necessary 279 | # to save the list in any way since it did not have enough chars to be a possible plate 280 | # end if 281 | 282 | # if we get here, the current list passed test as a "group" or "cluster" of matching chars 283 | listOfListsOfMatchingChars.append(listOfMatchingChars) # so add to our list of lists of matching chars 284 | 285 | listOfPossibleCharsWithCurrentMatchesRemoved = [] 286 | 287 | # remove the current list of matching chars from the big list so we don't use those same chars twice, 288 | # make sure to make a new big list for this since we don't want to change the original big list 289 | listOfPossibleCharsWithCurrentMatchesRemoved = list(set(listOfPossibleChars) - set(listOfMatchingChars)) 290 | 291 | recursiveListOfListsOfMatchingChars = findListOfListsOfMatchingChars(listOfPossibleCharsWithCurrentMatchesRemoved) # recursive call 292 | 293 | for recursiveListOfMatchingChars in recursiveListOfListsOfMatchingChars: # for each list of matching chars found by recursive call 294 | listOfListsOfMatchingChars.append(recursiveListOfMatchingChars) # add to our original list of lists of matching chars 295 | # end for 296 | 297 | break # exit for 298 | 299 | # end for 300 | 301 | return listOfListsOfMatchingChars 302 | # end function 303 | 304 | ################################################################################################### 305 | def findListOfMatchingChars(possibleChar, listOfChars): 306 | # the purpose of this function is, given a possible char and a big list of possible chars, 307 | # find all chars in the big list that are a match for the single possible char, and return those matching chars as a list 308 | listOfMatchingChars = [] # this will be the return value 309 | 310 | for possibleMatchingChar in listOfChars: # for each char in big list 311 | if possibleMatchingChar == possibleChar: # if the char we attempting to find matches for is the exact same char as the char in the big list we are currently checking 312 | # then we should not include it in the list of matches b/c that would end up double including the current char 313 | continue # so do not add to list of matches and jump back to top of for loop 314 | # end if 315 | # compute stuff to see if chars are a match 316 | fltDistanceBetweenChars = distanceBetweenChars(possibleChar, possibleMatchingChar) 317 | 318 | fltAngleBetweenChars = angleBetweenChars(possibleChar, possibleMatchingChar) 319 | 320 | fltChangeInArea = float(abs(possibleMatchingChar.intBoundingRectArea - possibleChar.intBoundingRectArea)) / float(possibleChar.intBoundingRectArea) 321 | 322 | fltChangeInWidth = float(abs(possibleMatchingChar.intBoundingRectWidth - possibleChar.intBoundingRectWidth)) / float(possibleChar.intBoundingRectWidth) 323 | fltChangeInHeight = float(abs(possibleMatchingChar.intBoundingRectHeight - possibleChar.intBoundingRectHeight)) / float(possibleChar.intBoundingRectHeight) 324 | 325 | # check if chars match 326 | if (fltDistanceBetweenChars < (possibleChar.fltDiagonalSize * MAX_DIAG_SIZE_MULTIPLE_AWAY) and 327 | fltAngleBetweenChars < MAX_ANGLE_BETWEEN_CHARS and 328 | fltChangeInArea < MAX_CHANGE_IN_AREA and 329 | fltChangeInWidth < MAX_CHANGE_IN_WIDTH and 330 | fltChangeInHeight < MAX_CHANGE_IN_HEIGHT): 331 | 332 | listOfMatchingChars.append(possibleMatchingChar) # if the chars are a match, add the current char to list of matching chars 333 | # end if 334 | # end for 335 | 336 | return listOfMatchingChars # return result 337 | # end function 338 | 339 | ################################################################################################### 340 | # use Pythagorean theorem to calculate distance between two chars 341 | def distanceBetweenChars(firstChar, secondChar): 342 | intX = abs(firstChar.intCenterX - secondChar.intCenterX) 343 | intY = abs(firstChar.intCenterY - secondChar.intCenterY) 344 | 345 | return math.sqrt((intX ** 2) + (intY ** 2)) 346 | # end function 347 | 348 | ################################################################################################### 349 | # use basic trigonometry (SOH CAH TOA) to calculate angle between chars 350 | def angleBetweenChars(firstChar, secondChar): 351 | fltAdj = float(abs(firstChar.intCenterX - secondChar.intCenterX)) 352 | fltOpp = float(abs(firstChar.intCenterY - secondChar.intCenterY)) 353 | 354 | if fltAdj != 0.0: # check to make sure we do not divide by zero if the center X positions are equal, float division by zero will cause a crash in Python 355 | fltAngleInRad = math.atan(fltOpp / fltAdj) # if adjacent is not zero, calculate angle 356 | else: 357 | fltAngleInRad = 1.5708 # if adjacent is zero, use this as the angle, this is to be consistent with the C++ version of this program 358 | # end if 359 | 360 | fltAngleInDeg = fltAngleInRad * (180.0 / math.pi) # calculate angle in degrees 361 | 362 | return fltAngleInDeg 363 | # end function 364 | 365 | ################################################################################################### 366 | # if we have two chars overlapping or to close to each other to possibly be separate chars, remove the inner (smaller) char, 367 | # this is to prevent including the same char twice if two contours are found for the same char, 368 | # for example for the letter 'O' both the inner ring and the outer ring may be found as contours, but we should only include the char once 369 | def removeInnerOverlappingChars(listOfMatchingChars): 370 | listOfMatchingCharsWithInnerCharRemoved = list(listOfMatchingChars) # this will be the return value 371 | 372 | for currentChar in listOfMatchingChars: 373 | for otherChar in listOfMatchingChars: 374 | if currentChar != otherChar: # if current char and other char are not the same char . . . 375 | # if current char and other char have center points at almost the same location . . . 376 | if distanceBetweenChars(currentChar, otherChar) < (currentChar.fltDiagonalSize * MIN_DIAG_SIZE_MULTIPLE_AWAY): 377 | # if we get in here we have found overlapping chars 378 | # next we identify which char is smaller, then if that char was not already removed on a previous pass, remove it 379 | if currentChar.intBoundingRectArea < otherChar.intBoundingRectArea: # if current char is smaller than other char 380 | if currentChar in listOfMatchingCharsWithInnerCharRemoved: # if current char was not already removed on a previous pass . . . 381 | listOfMatchingCharsWithInnerCharRemoved.remove(currentChar) # then remove current char 382 | # end if 383 | else: # else if other char is smaller than current char 384 | if otherChar in listOfMatchingCharsWithInnerCharRemoved: # if other char was not already removed on a previous pass . . . 385 | listOfMatchingCharsWithInnerCharRemoved.remove(otherChar) # then remove other char 386 | # end if 387 | # end if 388 | # end if 389 | # end if 390 | # end for 391 | # end for 392 | 393 | return listOfMatchingCharsWithInnerCharRemoved 394 | # end function 395 | 396 | ################################################################################################### 397 | # this is where we apply the actual char recognition 398 | def recognizeCharsInPlate(imgThresh, listOfMatchingChars): 399 | strChars = "" # this will be the return value, the chars in the lic plate 400 | 401 | height, width = imgThresh.shape 402 | 403 | imgThreshColor = np.zeros((height, width, 3), np.uint8) 404 | 405 | listOfMatchingChars.sort(key = lambda matchingChar: matchingChar.intCenterX) # sort chars from left to right 406 | 407 | cv2.cvtColor(imgThresh, cv2.COLOR_GRAY2BGR, imgThreshColor) # make color version of threshold image so we can draw contours in color on it 408 | 409 | for currentChar in listOfMatchingChars: # for each char in plate 410 | pt1 = (currentChar.intBoundingRectX, currentChar.intBoundingRectY) 411 | pt2 = ((currentChar.intBoundingRectX + currentChar.intBoundingRectWidth), (currentChar.intBoundingRectY + currentChar.intBoundingRectHeight)) 412 | 413 | cv2.rectangle(imgThreshColor, pt1, pt2, Main.SCALAR_GREEN, 2) # draw green box around the char 414 | 415 | # crop char out of threshold image 416 | imgROI = imgThresh[currentChar.intBoundingRectY : currentChar.intBoundingRectY + currentChar.intBoundingRectHeight, 417 | currentChar.intBoundingRectX : currentChar.intBoundingRectX + currentChar.intBoundingRectWidth] 418 | 419 | imgROIResized = cv2.resize(imgROI, (RESIZED_CHAR_IMAGE_WIDTH, RESIZED_CHAR_IMAGE_HEIGHT)) # resize image, this is necessary for char recognition 420 | 421 | npaROIResized = imgROIResized.reshape((1, RESIZED_CHAR_IMAGE_WIDTH * RESIZED_CHAR_IMAGE_HEIGHT)) # flatten image into 1d numpy array 422 | 423 | npaROIResized = np.float32(npaROIResized) # convert from 1d numpy array of ints to 1d numpy array of floats 424 | 425 | retval, npaResults, neigh_resp, dists = kNearest.findNearest(npaROIResized, k = 1) # finally we can call findNearest !!! 426 | 427 | strCurrentChar = str(chr(int(npaResults[0][0]))) # get character from results 428 | 429 | strChars = strChars + strCurrentChar # append current char to full string 430 | 431 | # end for 432 | 433 | if Main.showSteps == True: # show steps ####################################################### 434 | cv2.imshow("10", imgThreshColor) 435 | # end if # show steps ######################################################################### 436 | 437 | return strChars 438 | # end function 439 | 440 | 441 | 442 | 443 | 444 | 445 | 446 | 447 | -------------------------------------------------------------------------------- /DetectPlates.py: -------------------------------------------------------------------------------- 1 | # DetectPlates.py 2 | 3 | import cv2 4 | import numpy as np 5 | import math 6 | import Main 7 | import random 8 | 9 | import Preprocess 10 | import DetectChars 11 | import PossiblePlate 12 | import PossibleChar 13 | 14 | # module level variables ########################################################################## 15 | PLATE_WIDTH_PADDING_FACTOR = 1.3 16 | PLATE_HEIGHT_PADDING_FACTOR = 1.5 17 | 18 | ################################################################################################### 19 | def detectPlatesInScene(imgOriginalScene): 20 | listOfPossiblePlates = [] # this will be the return value 21 | 22 | height, width, numChannels = imgOriginalScene.shape 23 | 24 | imgGrayscaleScene = np.zeros((height, width, 1), np.uint8) 25 | imgThreshScene = np.zeros((height, width, 1), np.uint8) 26 | imgContours = np.zeros((height, width, 3), np.uint8) 27 | 28 | cv2.destroyAllWindows() 29 | 30 | if Main.showSteps == True: # show steps ####################################################### 31 | cv2.imshow("0", imgOriginalScene) 32 | # end if # show steps ######################################################################### 33 | 34 | imgGrayscaleScene, imgThreshScene = Preprocess.preprocess(imgOriginalScene) # preprocess to get grayscale and threshold images 35 | 36 | if Main.showSteps == True: # show steps ####################################################### 37 | cv2.imshow("1a", imgGrayscaleScene) 38 | cv2.imshow("1b", imgThreshScene) 39 | # end if # show steps ######################################################################### 40 | 41 | # find all possible chars in the scene, 42 | # this function first finds all contours, then only includes contours that could be chars (without comparison to other chars yet) 43 | listOfPossibleCharsInScene = findPossibleCharsInScene(imgThreshScene) 44 | 45 | if Main.showSteps == True: # show steps ####################################################### 46 | print("step 2 - len(listOfPossibleCharsInScene) = " + str( 47 | len(listOfPossibleCharsInScene))) # 131 with MCLRNF1 image 48 | 49 | imgContours = np.zeros((height, width, 3), np.uint8) 50 | 51 | contours = [] 52 | 53 | for possibleChar in listOfPossibleCharsInScene: 54 | contours.append(possibleChar.contour) 55 | # end for 56 | 57 | cv2.drawContours(imgContours, contours, -1, Main.SCALAR_WHITE) 58 | cv2.imshow("2b", imgContours) 59 | # end if # show steps ######################################################################### 60 | 61 | # given a list of all possible chars, find groups of matching chars 62 | # in the next steps each group of matching chars will attempt to be recognized as a plate 63 | listOfListsOfMatchingCharsInScene = DetectChars.findListOfListsOfMatchingChars(listOfPossibleCharsInScene) 64 | 65 | if Main.showSteps == True: # show steps ####################################################### 66 | print("step 3 - listOfListsOfMatchingCharsInScene.Count = " + str( 67 | len(listOfListsOfMatchingCharsInScene))) # 13 with MCLRNF1 image 68 | 69 | imgContours = np.zeros((height, width, 3), np.uint8) 70 | 71 | for listOfMatchingChars in listOfListsOfMatchingCharsInScene: 72 | intRandomBlue = random.randint(0, 255) 73 | intRandomGreen = random.randint(0, 255) 74 | intRandomRed = random.randint(0, 255) 75 | 76 | contours = [] 77 | 78 | for matchingChar in listOfMatchingChars: 79 | contours.append(matchingChar.contour) 80 | # end for 81 | 82 | cv2.drawContours(imgContours, contours, -1, (intRandomBlue, intRandomGreen, intRandomRed)) 83 | # end for 84 | 85 | cv2.imshow("3", imgContours) 86 | # end if # show steps ######################################################################### 87 | 88 | for listOfMatchingChars in listOfListsOfMatchingCharsInScene: # for each group of matching chars 89 | possiblePlate = extractPlate(imgOriginalScene, listOfMatchingChars) # attempt to extract plate 90 | 91 | if possiblePlate.imgPlate is not None: # if plate was found 92 | listOfPossiblePlates.append(possiblePlate) # add to list of possible plates 93 | # end if 94 | # end for 95 | 96 | print("\n" + str(len(listOfPossiblePlates)) + " possible plates found") # 13 with MCLRNF1 image 97 | 98 | if Main.showSteps == True: # show steps ####################################################### 99 | print("\n") 100 | cv2.imshow("4a", imgContours) 101 | 102 | for i in range(0, len(listOfPossiblePlates)): 103 | p2fRectPoints = cv2.boxPoints(listOfPossiblePlates[i].rrLocationOfPlateInScene) 104 | 105 | cv2.line(imgContours, tuple(p2fRectPoints[0]), tuple(p2fRectPoints[1]), Main.SCALAR_RED, 2) 106 | cv2.line(imgContours, tuple(p2fRectPoints[1]), tuple(p2fRectPoints[2]), Main.SCALAR_RED, 2) 107 | cv2.line(imgContours, tuple(p2fRectPoints[2]), tuple(p2fRectPoints[3]), Main.SCALAR_RED, 2) 108 | cv2.line(imgContours, tuple(p2fRectPoints[3]), tuple(p2fRectPoints[0]), Main.SCALAR_RED, 2) 109 | 110 | cv2.imshow("4a", imgContours) 111 | 112 | print("possible plate " + str(i) + ", click on any image and press a key to continue . . .") 113 | 114 | cv2.imshow("4b", listOfPossiblePlates[i].imgPlate) 115 | cv2.waitKey(0) 116 | # end for 117 | 118 | print("\nplate detection complete, click on any image and press a key to begin char recognition . . .\n") 119 | cv2.waitKey(0) 120 | # end if # show steps ######################################################################### 121 | 122 | return listOfPossiblePlates 123 | # end function 124 | 125 | ################################################################################################### 126 | def findPossibleCharsInScene(imgThresh): 127 | listOfPossibleChars = [] # this will be the return value 128 | 129 | intCountOfPossibleChars = 0 130 | 131 | imgThreshCopy = imgThresh.copy() 132 | 133 | contours, npaHierarchy = cv2.findContours(imgThreshCopy, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE) # find all contours 134 | 135 | height, width = imgThresh.shape 136 | imgContours = np.zeros((height, width, 3), np.uint8) 137 | 138 | for i in range(0, len(contours)): # for each contour 139 | 140 | if Main.showSteps == True: # show steps ################################################### 141 | cv2.drawContours(imgContours, contours, i, Main.SCALAR_WHITE) 142 | # end if # show steps ##################################################################### 143 | 144 | possibleChar = PossibleChar.PossibleChar(contours[i]) 145 | 146 | if DetectChars.checkIfPossibleChar(possibleChar): # if contour is a possible char, note this does not compare to other chars (yet) . . . 147 | intCountOfPossibleChars = intCountOfPossibleChars + 1 # increment count of possible chars 148 | listOfPossibleChars.append(possibleChar) # and add to list of possible chars 149 | # end if 150 | # end for 151 | 152 | if Main.showSteps == True: # show steps ####################################################### 153 | print("\nstep 2 - len(contours) = " + str(len(contours))) # 2362 with MCLRNF1 image 154 | print("step 2 - intCountOfPossibleChars = " + str(intCountOfPossibleChars)) # 131 with MCLRNF1 image 155 | cv2.imshow("2a", imgContours) 156 | # end if # show steps ######################################################################### 157 | 158 | return listOfPossibleChars 159 | # end function 160 | 161 | 162 | ################################################################################################### 163 | def extractPlate(imgOriginal, listOfMatchingChars): 164 | possiblePlate = PossiblePlate.PossiblePlate() # this will be the return value 165 | 166 | listOfMatchingChars.sort(key = lambda matchingChar: matchingChar.intCenterX) # sort chars from left to right based on x position 167 | 168 | # calculate the center point of the plate 169 | fltPlateCenterX = (listOfMatchingChars[0].intCenterX + listOfMatchingChars[len(listOfMatchingChars) - 1].intCenterX) / 2.0 170 | fltPlateCenterY = (listOfMatchingChars[0].intCenterY + listOfMatchingChars[len(listOfMatchingChars) - 1].intCenterY) / 2.0 171 | 172 | ptPlateCenter = fltPlateCenterX, fltPlateCenterY 173 | 174 | # calculate plate width and height 175 | intPlateWidth = int((listOfMatchingChars[len(listOfMatchingChars) - 1].intBoundingRectX + listOfMatchingChars[len(listOfMatchingChars) - 1].intBoundingRectWidth - listOfMatchingChars[0].intBoundingRectX) * PLATE_WIDTH_PADDING_FACTOR) 176 | 177 | intTotalOfCharHeights = 0 178 | 179 | for matchingChar in listOfMatchingChars: 180 | intTotalOfCharHeights = intTotalOfCharHeights + matchingChar.intBoundingRectHeight 181 | # end for 182 | 183 | fltAverageCharHeight = intTotalOfCharHeights / len(listOfMatchingChars) 184 | 185 | intPlateHeight = int(fltAverageCharHeight * PLATE_HEIGHT_PADDING_FACTOR) 186 | 187 | # calculate correction angle of plate region 188 | fltOpposite = listOfMatchingChars[len(listOfMatchingChars) - 1].intCenterY - listOfMatchingChars[0].intCenterY 189 | fltHypotenuse = DetectChars.distanceBetweenChars(listOfMatchingChars[0], listOfMatchingChars[len(listOfMatchingChars) - 1]) 190 | fltCorrectionAngleInRad = math.asin(fltOpposite / fltHypotenuse) 191 | fltCorrectionAngleInDeg = fltCorrectionAngleInRad * (180.0 / math.pi) 192 | 193 | # pack plate region center point, width and height, and correction angle into rotated rect member variable of plate 194 | possiblePlate.rrLocationOfPlateInScene = ( tuple(ptPlateCenter), (intPlateWidth, intPlateHeight), fltCorrectionAngleInDeg ) 195 | 196 | # final steps are to perform the actual rotation 197 | 198 | # get the rotation matrix for our calculated correction angle 199 | rotationMatrix = cv2.getRotationMatrix2D(tuple(ptPlateCenter), fltCorrectionAngleInDeg, 1.0) 200 | 201 | height, width, numChannels = imgOriginal.shape # unpack original image width and height 202 | 203 | imgRotated = cv2.warpAffine(imgOriginal, rotationMatrix, (width, height)) # rotate the entire image 204 | 205 | imgCropped = cv2.getRectSubPix(imgRotated, (intPlateWidth, intPlateHeight), tuple(ptPlateCenter)) 206 | 207 | possiblePlate.imgPlate = imgCropped # copy the cropped plate image into the applicable member variable of the possible plate 208 | 209 | return possiblePlate 210 | # end function 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | -------------------------------------------------------------------------------- /DocsAndPresentation/Steps With Images.docx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MicrocontrollersAndMore/OpenCV_3_License_Plate_Recognition_Python/9cd2bf01ba00269056ddc41c5fe1f5c052250308/DocsAndPresentation/Steps With Images.docx -------------------------------------------------------------------------------- /DocsAndPresentation/Steps With Images.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MicrocontrollersAndMore/OpenCV_3_License_Plate_Recognition_Python/9cd2bf01ba00269056ddc41c5fe1f5c052250308/DocsAndPresentation/Steps With Images.pdf -------------------------------------------------------------------------------- /DocsAndPresentation/steps.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MicrocontrollersAndMore/OpenCV_3_License_Plate_Recognition_Python/9cd2bf01ba00269056ddc41c5fe1f5c052250308/DocsAndPresentation/steps.png -------------------------------------------------------------------------------- /DocsAndPresentation/steps.txt: -------------------------------------------------------------------------------- 1 | 2 | 2 classes: 3 | 4 | PossiblePlate 5 | 6 | PossibleChar 7 | 8 | 9 | 10 | find plates 11 | 12 | find chars within plates 13 | 14 | 15 | detectPlatesInScene() 16 | 17 | detectCharsInPlates() 18 | 19 | 20 | imgOriginalScene 21 | 22 | preprocess() 23 | 24 | imgGrayscaleScene, imgThreshScene 25 | 26 | findPossibleCharsInScene() 27 | 28 | listOfPossibleCharsInScene 29 | 30 | findListOfListsOfMatchingChars() 31 | 32 | listOfListsOfMatchingCharsInScene 33 | 34 | extractPlate() 35 | 36 | listOfPossiblePlates 37 | 38 | 39 | 40 | 41 | loadKNNDataAndTrainKNN() 42 | 43 | 44 | listOfPossiblePlates 45 | 46 | preprocess() 47 | 48 | possiblePlate.imgGrayscale, possiblePlate.imgThresh 49 | 50 | findPossibleCharsInPlate() 51 | 52 | listOfPossibleCharsInPlate 53 | 54 | findListOfListsOfMatchingChars() 55 | 56 | listOfListsOfMatchingCharsInPlate 57 | 58 | removeInnerOverlappingChars() 59 | 60 | listOfListsOfMatchingCharsInPlate 61 | 62 | within each possible plate, suppose the longest list of potential matching chars is the actual list of chars 63 | 64 | longestListOfMatchingCharsInPlate 65 | 66 | recognizeCharsInPlate() 67 | 68 | possiblePlate.strChars 69 | 70 | 71 | 72 | listOfPossiblePlates 73 | 74 | suppose the plate with the most recognized chars is the actual plate 75 | 76 | licPlate 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /LicPlateImages/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MicrocontrollersAndMore/OpenCV_3_License_Plate_Recognition_Python/9cd2bf01ba00269056ddc41c5fe1f5c052250308/LicPlateImages/1.png -------------------------------------------------------------------------------- /LicPlateImages/10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MicrocontrollersAndMore/OpenCV_3_License_Plate_Recognition_Python/9cd2bf01ba00269056ddc41c5fe1f5c052250308/LicPlateImages/10.png -------------------------------------------------------------------------------- /LicPlateImages/11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MicrocontrollersAndMore/OpenCV_3_License_Plate_Recognition_Python/9cd2bf01ba00269056ddc41c5fe1f5c052250308/LicPlateImages/11.png -------------------------------------------------------------------------------- /LicPlateImages/12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MicrocontrollersAndMore/OpenCV_3_License_Plate_Recognition_Python/9cd2bf01ba00269056ddc41c5fe1f5c052250308/LicPlateImages/12.png -------------------------------------------------------------------------------- /LicPlateImages/13.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MicrocontrollersAndMore/OpenCV_3_License_Plate_Recognition_Python/9cd2bf01ba00269056ddc41c5fe1f5c052250308/LicPlateImages/13.png -------------------------------------------------------------------------------- /LicPlateImages/14.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MicrocontrollersAndMore/OpenCV_3_License_Plate_Recognition_Python/9cd2bf01ba00269056ddc41c5fe1f5c052250308/LicPlateImages/14.png -------------------------------------------------------------------------------- /LicPlateImages/15.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MicrocontrollersAndMore/OpenCV_3_License_Plate_Recognition_Python/9cd2bf01ba00269056ddc41c5fe1f5c052250308/LicPlateImages/15.png -------------------------------------------------------------------------------- /LicPlateImages/16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MicrocontrollersAndMore/OpenCV_3_License_Plate_Recognition_Python/9cd2bf01ba00269056ddc41c5fe1f5c052250308/LicPlateImages/16.png -------------------------------------------------------------------------------- /LicPlateImages/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MicrocontrollersAndMore/OpenCV_3_License_Plate_Recognition_Python/9cd2bf01ba00269056ddc41c5fe1f5c052250308/LicPlateImages/2.png -------------------------------------------------------------------------------- /LicPlateImages/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MicrocontrollersAndMore/OpenCV_3_License_Plate_Recognition_Python/9cd2bf01ba00269056ddc41c5fe1f5c052250308/LicPlateImages/3.png -------------------------------------------------------------------------------- /LicPlateImages/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MicrocontrollersAndMore/OpenCV_3_License_Plate_Recognition_Python/9cd2bf01ba00269056ddc41c5fe1f5c052250308/LicPlateImages/4.png -------------------------------------------------------------------------------- /LicPlateImages/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MicrocontrollersAndMore/OpenCV_3_License_Plate_Recognition_Python/9cd2bf01ba00269056ddc41c5fe1f5c052250308/LicPlateImages/5.png -------------------------------------------------------------------------------- /LicPlateImages/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MicrocontrollersAndMore/OpenCV_3_License_Plate_Recognition_Python/9cd2bf01ba00269056ddc41c5fe1f5c052250308/LicPlateImages/6.png -------------------------------------------------------------------------------- /LicPlateImages/7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MicrocontrollersAndMore/OpenCV_3_License_Plate_Recognition_Python/9cd2bf01ba00269056ddc41c5fe1f5c052250308/LicPlateImages/7.png -------------------------------------------------------------------------------- /LicPlateImages/8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MicrocontrollersAndMore/OpenCV_3_License_Plate_Recognition_Python/9cd2bf01ba00269056ddc41c5fe1f5c052250308/LicPlateImages/8.png -------------------------------------------------------------------------------- /LicPlateImages/9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MicrocontrollersAndMore/OpenCV_3_License_Plate_Recognition_Python/9cd2bf01ba00269056ddc41c5fe1f5c052250308/LicPlateImages/9.png -------------------------------------------------------------------------------- /Main.py: -------------------------------------------------------------------------------- 1 | # Main.py 2 | 3 | import cv2 4 | import numpy as np 5 | import os 6 | 7 | import DetectChars 8 | import DetectPlates 9 | import PossiblePlate 10 | 11 | # module level variables ########################################################################## 12 | SCALAR_BLACK = (0.0, 0.0, 0.0) 13 | SCALAR_WHITE = (255.0, 255.0, 255.0) 14 | SCALAR_YELLOW = (0.0, 255.0, 255.0) 15 | SCALAR_GREEN = (0.0, 255.0, 0.0) 16 | SCALAR_RED = (0.0, 0.0, 255.0) 17 | 18 | showSteps = False 19 | 20 | ################################################################################################### 21 | def main(): 22 | 23 | blnKNNTrainingSuccessful = DetectChars.loadKNNDataAndTrainKNN() # attempt KNN training 24 | 25 | if blnKNNTrainingSuccessful == False: # if KNN training was not successful 26 | print("\nerror: KNN traning was not successful\n") # show error message 27 | return # and exit program 28 | # end if 29 | 30 | imgOriginalScene = cv2.imread("LicPlateImages/1.png") # open image 31 | 32 | if imgOriginalScene is None: # if image was not read successfully 33 | print("\nerror: image not read from file \n\n") # print error message to std out 34 | os.system("pause") # pause so user can see error message 35 | return # and exit program 36 | # end if 37 | 38 | listOfPossiblePlates = DetectPlates.detectPlatesInScene(imgOriginalScene) # detect plates 39 | 40 | listOfPossiblePlates = DetectChars.detectCharsInPlates(listOfPossiblePlates) # detect chars in plates 41 | 42 | cv2.imshow("imgOriginalScene", imgOriginalScene) # show scene image 43 | 44 | if len(listOfPossiblePlates) == 0: # if no plates were found 45 | print("\nno license plates were detected\n") # inform user no plates were found 46 | else: # else 47 | # if we get in here list of possible plates has at leat one plate 48 | 49 | # sort the list of possible plates in DESCENDING order (most number of chars to least number of chars) 50 | listOfPossiblePlates.sort(key = lambda possiblePlate: len(possiblePlate.strChars), reverse = True) 51 | 52 | # suppose the plate with the most recognized chars (the first plate in sorted by string length descending order) is the actual plate 53 | licPlate = listOfPossiblePlates[0] 54 | 55 | cv2.imshow("imgPlate", licPlate.imgPlate) # show crop of plate and threshold of plate 56 | cv2.imshow("imgThresh", licPlate.imgThresh) 57 | 58 | if len(licPlate.strChars) == 0: # if no chars were found in the plate 59 | print("\nno characters were detected\n\n") # show message 60 | return # and exit program 61 | # end if 62 | 63 | drawRedRectangleAroundPlate(imgOriginalScene, licPlate) # draw red rectangle around plate 64 | 65 | print("\nlicense plate read from image = " + licPlate.strChars + "\n") # write license plate text to std out 66 | print("----------------------------------------") 67 | 68 | writeLicensePlateCharsOnImage(imgOriginalScene, licPlate) # write license plate text on the image 69 | 70 | cv2.imshow("imgOriginalScene", imgOriginalScene) # re-show scene image 71 | 72 | cv2.imwrite("imgOriginalScene.png", imgOriginalScene) # write image out to file 73 | 74 | # end if else 75 | 76 | cv2.waitKey(0) # hold windows open until user presses a key 77 | 78 | return 79 | # end main 80 | 81 | ################################################################################################### 82 | def drawRedRectangleAroundPlate(imgOriginalScene, licPlate): 83 | 84 | p2fRectPoints = cv2.boxPoints(licPlate.rrLocationOfPlateInScene) # get 4 vertices of rotated rect 85 | 86 | cv2.line(imgOriginalScene, tuple(p2fRectPoints[0]), tuple(p2fRectPoints[1]), SCALAR_RED, 2) # draw 4 red lines 87 | cv2.line(imgOriginalScene, tuple(p2fRectPoints[1]), tuple(p2fRectPoints[2]), SCALAR_RED, 2) 88 | cv2.line(imgOriginalScene, tuple(p2fRectPoints[2]), tuple(p2fRectPoints[3]), SCALAR_RED, 2) 89 | cv2.line(imgOriginalScene, tuple(p2fRectPoints[3]), tuple(p2fRectPoints[0]), SCALAR_RED, 2) 90 | # end function 91 | 92 | ################################################################################################### 93 | def writeLicensePlateCharsOnImage(imgOriginalScene, licPlate): 94 | ptCenterOfTextAreaX = 0 # this will be the center of the area the text will be written to 95 | ptCenterOfTextAreaY = 0 96 | 97 | ptLowerLeftTextOriginX = 0 # this will be the bottom left of the area that the text will be written to 98 | ptLowerLeftTextOriginY = 0 99 | 100 | sceneHeight, sceneWidth, sceneNumChannels = imgOriginalScene.shape 101 | plateHeight, plateWidth, plateNumChannels = licPlate.imgPlate.shape 102 | 103 | intFontFace = cv2.FONT_HERSHEY_SIMPLEX # choose a plain jane font 104 | fltFontScale = float(plateHeight) / 30.0 # base font scale on height of plate area 105 | intFontThickness = int(round(fltFontScale * 1.5)) # base font thickness on font scale 106 | 107 | textSize, baseline = cv2.getTextSize(licPlate.strChars, intFontFace, fltFontScale, intFontThickness) # call getTextSize 108 | 109 | # unpack roatated rect into center point, width and height, and angle 110 | ( (intPlateCenterX, intPlateCenterY), (intPlateWidth, intPlateHeight), fltCorrectionAngleInDeg ) = licPlate.rrLocationOfPlateInScene 111 | 112 | intPlateCenterX = int(intPlateCenterX) # make sure center is an integer 113 | intPlateCenterY = int(intPlateCenterY) 114 | 115 | ptCenterOfTextAreaX = int(intPlateCenterX) # the horizontal location of the text area is the same as the plate 116 | 117 | if intPlateCenterY < (sceneHeight * 0.75): # if the license plate is in the upper 3/4 of the image 118 | ptCenterOfTextAreaY = int(round(intPlateCenterY)) + int(round(plateHeight * 1.6)) # write the chars in below the plate 119 | else: # else if the license plate is in the lower 1/4 of the image 120 | ptCenterOfTextAreaY = int(round(intPlateCenterY)) - int(round(plateHeight * 1.6)) # write the chars in above the plate 121 | # end if 122 | 123 | textSizeWidth, textSizeHeight = textSize # unpack text size width and height 124 | 125 | ptLowerLeftTextOriginX = int(ptCenterOfTextAreaX - (textSizeWidth / 2)) # calculate the lower left origin of the text area 126 | ptLowerLeftTextOriginY = int(ptCenterOfTextAreaY + (textSizeHeight / 2)) # based on the text area center, width, and height 127 | 128 | # write the text on the image 129 | cv2.putText(imgOriginalScene, licPlate.strChars, (ptLowerLeftTextOriginX, ptLowerLeftTextOriginY), intFontFace, fltFontScale, SCALAR_YELLOW, intFontThickness) 130 | # end function 131 | 132 | ################################################################################################### 133 | if __name__ == "__main__": 134 | main() 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | -------------------------------------------------------------------------------- /PossibleChar.py: -------------------------------------------------------------------------------- 1 | # PossibleChar.py 2 | 3 | import cv2 4 | import numpy as np 5 | import math 6 | 7 | ################################################################################################### 8 | class PossibleChar: 9 | 10 | # constructor ################################################################################# 11 | def __init__(self, _contour): 12 | self.contour = _contour 13 | 14 | self.boundingRect = cv2.boundingRect(self.contour) 15 | 16 | [intX, intY, intWidth, intHeight] = self.boundingRect 17 | 18 | self.intBoundingRectX = intX 19 | self.intBoundingRectY = intY 20 | self.intBoundingRectWidth = intWidth 21 | self.intBoundingRectHeight = intHeight 22 | 23 | self.intBoundingRectArea = self.intBoundingRectWidth * self.intBoundingRectHeight 24 | 25 | self.intCenterX = (self.intBoundingRectX + self.intBoundingRectX + self.intBoundingRectWidth) / 2 26 | self.intCenterY = (self.intBoundingRectY + self.intBoundingRectY + self.intBoundingRectHeight) / 2 27 | 28 | self.fltDiagonalSize = math.sqrt((self.intBoundingRectWidth ** 2) + (self.intBoundingRectHeight ** 2)) 29 | 30 | self.fltAspectRatio = float(self.intBoundingRectWidth) / float(self.intBoundingRectHeight) 31 | # end constructor 32 | 33 | # end class 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /PossiblePlate.py: -------------------------------------------------------------------------------- 1 | # PossiblePlate.py 2 | 3 | import cv2 4 | import numpy as np 5 | 6 | ################################################################################################### 7 | class PossiblePlate: 8 | 9 | # constructor ################################################################################# 10 | def __init__(self): 11 | self.imgPlate = None 12 | self.imgGrayscale = None 13 | self.imgThresh = None 14 | 15 | self.rrLocationOfPlateInScene = None 16 | 17 | self.strChars = "" 18 | # end constructor 19 | 20 | # end class 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /Preprocess.py: -------------------------------------------------------------------------------- 1 | # Preprocess.py 2 | 3 | import cv2 4 | import numpy as np 5 | import math 6 | 7 | # module level variables ########################################################################## 8 | GAUSSIAN_SMOOTH_FILTER_SIZE = (5, 5) 9 | ADAPTIVE_THRESH_BLOCK_SIZE = 19 10 | ADAPTIVE_THRESH_WEIGHT = 9 11 | 12 | ################################################################################################### 13 | def preprocess(imgOriginal): 14 | imgGrayscale = extractValue(imgOriginal) 15 | 16 | imgMaxContrastGrayscale = maximizeContrast(imgGrayscale) 17 | 18 | height, width = imgGrayscale.shape 19 | 20 | imgBlurred = np.zeros((height, width, 1), np.uint8) 21 | 22 | imgBlurred = cv2.GaussianBlur(imgMaxContrastGrayscale, GAUSSIAN_SMOOTH_FILTER_SIZE, 0) 23 | 24 | imgThresh = cv2.adaptiveThreshold(imgBlurred, 255.0, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY_INV, ADAPTIVE_THRESH_BLOCK_SIZE, ADAPTIVE_THRESH_WEIGHT) 25 | 26 | return imgGrayscale, imgThresh 27 | # end function 28 | 29 | ################################################################################################### 30 | def extractValue(imgOriginal): 31 | height, width, numChannels = imgOriginal.shape 32 | 33 | imgHSV = np.zeros((height, width, 3), np.uint8) 34 | 35 | imgHSV = cv2.cvtColor(imgOriginal, cv2.COLOR_BGR2HSV) 36 | 37 | imgHue, imgSaturation, imgValue = cv2.split(imgHSV) 38 | 39 | return imgValue 40 | # end function 41 | 42 | ################################################################################################### 43 | def maximizeContrast(imgGrayscale): 44 | 45 | height, width = imgGrayscale.shape 46 | 47 | imgTopHat = np.zeros((height, width, 1), np.uint8) 48 | imgBlackHat = np.zeros((height, width, 1), np.uint8) 49 | 50 | structuringElement = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3)) 51 | 52 | imgTopHat = cv2.morphologyEx(imgGrayscale, cv2.MORPH_TOPHAT, structuringElement) 53 | imgBlackHat = cv2.morphologyEx(imgGrayscale, cv2.MORPH_BLACKHAT, structuringElement) 54 | 55 | imgGrayscalePlusTopHat = cv2.add(imgGrayscale, imgTopHat) 56 | imgGrayscalePlusTopHatMinusBlackHat = cv2.subtract(imgGrayscalePlusTopHat, imgBlackHat) 57 | 58 | return imgGrayscalePlusTopHatMinusBlackHat 59 | # end function 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /classifications.txt: -------------------------------------------------------------------------------- 1 | 9.000000000000000000e+01 2 | 8.400000000000000000e+01 3 | 8.200000000000000000e+01 4 | 8.000000000000000000e+01 5 | 7.700000000000000000e+01 6 | 7.000000000000000000e+01 7 | 6.900000000000000000e+01 8 | 6.800000000000000000e+01 9 | 6.600000000000000000e+01 10 | 6.500000000000000000e+01 11 | 8.900000000000000000e+01 12 | 8.800000000000000000e+01 13 | 8.700000000000000000e+01 14 | 8.600000000000000000e+01 15 | 8.500000000000000000e+01 16 | 8.300000000000000000e+01 17 | 8.100000000000000000e+01 18 | 7.900000000000000000e+01 19 | 7.800000000000000000e+01 20 | 7.600000000000000000e+01 21 | 7.500000000000000000e+01 22 | 7.400000000000000000e+01 23 | 7.300000000000000000e+01 24 | 7.200000000000000000e+01 25 | 7.100000000000000000e+01 26 | 6.700000000000000000e+01 27 | 5.700000000000000000e+01 28 | 5.600000000000000000e+01 29 | 5.500000000000000000e+01 30 | 5.400000000000000000e+01 31 | 5.300000000000000000e+01 32 | 5.100000000000000000e+01 33 | 5.000000000000000000e+01 34 | 4.800000000000000000e+01 35 | 5.200000000000000000e+01 36 | 4.900000000000000000e+01 37 | 8.200000000000000000e+01 38 | 8.000000000000000000e+01 39 | 7.700000000000000000e+01 40 | 6.800000000000000000e+01 41 | 6.600000000000000000e+01 42 | 6.500000000000000000e+01 43 | 9.000000000000000000e+01 44 | 8.900000000000000000e+01 45 | 8.800000000000000000e+01 46 | 8.700000000000000000e+01 47 | 8.600000000000000000e+01 48 | 8.500000000000000000e+01 49 | 8.400000000000000000e+01 50 | 8.300000000000000000e+01 51 | 8.100000000000000000e+01 52 | 7.900000000000000000e+01 53 | 7.800000000000000000e+01 54 | 7.600000000000000000e+01 55 | 7.500000000000000000e+01 56 | 7.400000000000000000e+01 57 | 7.300000000000000000e+01 58 | 7.200000000000000000e+01 59 | 7.100000000000000000e+01 60 | 7.000000000000000000e+01 61 | 6.900000000000000000e+01 62 | 6.700000000000000000e+01 63 | 5.200000000000000000e+01 64 | 4.900000000000000000e+01 65 | 5.700000000000000000e+01 66 | 5.600000000000000000e+01 67 | 5.500000000000000000e+01 68 | 5.400000000000000000e+01 69 | 5.300000000000000000e+01 70 | 5.100000000000000000e+01 71 | 5.000000000000000000e+01 72 | 4.800000000000000000e+01 73 | 9.000000000000000000e+01 74 | 8.900000000000000000e+01 75 | 8.800000000000000000e+01 76 | 8.700000000000000000e+01 77 | 8.600000000000000000e+01 78 | 8.500000000000000000e+01 79 | 8.400000000000000000e+01 80 | 8.300000000000000000e+01 81 | 8.200000000000000000e+01 82 | 8.100000000000000000e+01 83 | 8.000000000000000000e+01 84 | 7.900000000000000000e+01 85 | 7.800000000000000000e+01 86 | 7.700000000000000000e+01 87 | 7.600000000000000000e+01 88 | 7.500000000000000000e+01 89 | 7.400000000000000000e+01 90 | 7.300000000000000000e+01 91 | 7.200000000000000000e+01 92 | 7.100000000000000000e+01 93 | 7.000000000000000000e+01 94 | 6.900000000000000000e+01 95 | 6.800000000000000000e+01 96 | 6.700000000000000000e+01 97 | 6.600000000000000000e+01 98 | 6.500000000000000000e+01 99 | 4.900000000000000000e+01 100 | 5.700000000000000000e+01 101 | 5.600000000000000000e+01 102 | 5.500000000000000000e+01 103 | 5.400000000000000000e+01 104 | 5.300000000000000000e+01 105 | 5.200000000000000000e+01 106 | 5.100000000000000000e+01 107 | 5.000000000000000000e+01 108 | 4.800000000000000000e+01 109 | 9.000000000000000000e+01 110 | 8.900000000000000000e+01 111 | 8.800000000000000000e+01 112 | 8.700000000000000000e+01 113 | 8.600000000000000000e+01 114 | 8.500000000000000000e+01 115 | 8.400000000000000000e+01 116 | 8.200000000000000000e+01 117 | 8.000000000000000000e+01 118 | 7.800000000000000000e+01 119 | 7.700000000000000000e+01 120 | 7.600000000000000000e+01 121 | 7.500000000000000000e+01 122 | 7.400000000000000000e+01 123 | 7.300000000000000000e+01 124 | 7.200000000000000000e+01 125 | 7.000000000000000000e+01 126 | 6.900000000000000000e+01 127 | 6.800000000000000000e+01 128 | 6.600000000000000000e+01 129 | 6.500000000000000000e+01 130 | 8.300000000000000000e+01 131 | 8.100000000000000000e+01 132 | 7.900000000000000000e+01 133 | 7.100000000000000000e+01 134 | 6.700000000000000000e+01 135 | 5.500000000000000000e+01 136 | 5.100000000000000000e+01 137 | 5.000000000000000000e+01 138 | 4.900000000000000000e+01 139 | 5.700000000000000000e+01 140 | 5.600000000000000000e+01 141 | 5.400000000000000000e+01 142 | 5.300000000000000000e+01 143 | 5.200000000000000000e+01 144 | 4.800000000000000000e+01 145 | 8.200000000000000000e+01 146 | 8.000000000000000000e+01 147 | 7.700000000000000000e+01 148 | 6.800000000000000000e+01 149 | 6.600000000000000000e+01 150 | 6.500000000000000000e+01 151 | 9.000000000000000000e+01 152 | 8.900000000000000000e+01 153 | 8.800000000000000000e+01 154 | 8.700000000000000000e+01 155 | 8.600000000000000000e+01 156 | 8.500000000000000000e+01 157 | 8.400000000000000000e+01 158 | 8.300000000000000000e+01 159 | 8.100000000000000000e+01 160 | 7.900000000000000000e+01 161 | 7.800000000000000000e+01 162 | 7.600000000000000000e+01 163 | 7.500000000000000000e+01 164 | 7.400000000000000000e+01 165 | 7.300000000000000000e+01 166 | 7.200000000000000000e+01 167 | 7.100000000000000000e+01 168 | 7.000000000000000000e+01 169 | 6.900000000000000000e+01 170 | 6.700000000000000000e+01 171 | 5.500000000000000000e+01 172 | 5.300000000000000000e+01 173 | 5.200000000000000000e+01 174 | 5.700000000000000000e+01 175 | 5.600000000000000000e+01 176 | 5.400000000000000000e+01 177 | 5.100000000000000000e+01 178 | 5.000000000000000000e+01 179 | 4.900000000000000000e+01 180 | 4.800000000000000000e+01 181 | -------------------------------------------------------------------------------- /readme.txt: -------------------------------------------------------------------------------- 1 | The video pretty much explains it all: 2 | https://www.youtube.com/watch?v=fJcl6Gw1D8k 3 | --------------------------------------------------------------------------------