diff --git a/Disso Code/DetectChars.py b/Disso Code/DetectChars.py new file mode 100644 index 0000000..8f15511 --- /dev/null +++ b/Disso Code/DetectChars.py @@ -0,0 +1,243 @@ +import os + +import cv2 +import numpy as np +import math +import random + +import Main +import Preprocess +import PossibleChar + +kNearest = cv2.ml.KNearest_create() + +m_p_w = 2 # min pixel width +m_p_h = 8 #min pixel height + +min_a_r = 0.25 # min aspect ratio +max_a_r = 1.0 # max aspect ratio + +m_p_a = 80 # min pixel area + +# min and max diag size multiple away +min_d_s_m_a = 0.3 +max_d_s_m_a = 5.0 + +max_c_i_a = 0.5 + +max_c_i_w = 0.8 +max_c_i_h = 0.2 + +max_a_b_c = 12.0 + +min_n_o_m_c = 3 + +r_c_i_w = 20 +r_c_i_h = 30 + +MIN_CONTOUR_AREA = 100 + +def findPossibleCharsInPlate(imgGrayscale, imgThresh): + PossCharsList = [] + contours = [] + imgThreshCopy = imgThresh.copy() + + contours, npaHierarchy = cv2.findContours(imgThreshCopy, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE) + + for contour in contours: + possibleChar = PossibleChar.PossibleChar(contour) + + if checkIfPossibleChar(possibleChar): + PossCharsList.append(possibleChar) + + return PossCharsList + +def checkIfPossibleChar(possibleChar): + + if (possibleChar.intBoundingRectArea > m_p_a and + possibleChar.intBoundingRectWidth > m_p_w and possibleChar.intBoundingRectHeight > m_p_h and + min_a_r < possibleChar.fltAspectRatio and possibleChar.fltAspectRatio < max_a_r): + return True + else: + return False + +def findrecursLOMatchChars(PossCharsList): + + recursLOMatchChars = [] + + for possibleChar in PossCharsList: + listOfMatchingChars = findListOfMatchingChars(possibleChar, PossCharsList) + + listOfMatchingChars.append(possibleChar) + + if len(listOfMatchingChars) < min_n_o_m_c: + continue + + recursLOMatchChars.append(listOfMatchingChars) + + PossCharsListWithCurrentMatchesRemoved = [] + PossCharsListWithCurrentMatchesRemoved = list(set(PossCharsList) - set(listOfMatchingChars)) + + recursiveListOfMatchingChars = findrecursLOMatchChars(PossCharsListWithCurrentMatchesRemoved) + + for recursiveListOfMatchingChars in recursiveListOfMatchingChars: + recursLOMatchChars.append(recursiveListOfMatchingChars) + break + + return recursLOMatchChars + +def detectCharsInPlates(listOfpossPlates): + intPlateCounter = 0 + imgContours = None + contours = [] + + if len(listOfpossPlates) == 0: + return listOfpossPlates + + + + + for possPlate in listOfpossPlates: + + possPlate.imgGrayscale, possPlate.imgThresh = Preprocess.preprocess(possPlate.imgPlate) + possPlate.imgThresh = cv2.resize(possPlate.imgThresh, (0, 0), fx = 1.6, fy = 1.6) + thresholdValue, possPlate.imgThresh = cv2.threshold(possPlate.imgThresh, 0.0, 255.0, cv2.THRESH_BINARY | cv2.THRESH_OTSU) + PossCharsListInPlate = findPossibleCharsInPlate(possPlate.imgGrayscale, possPlate.imgThresh) + recursLOMatchCharsInPlate = findrecursLOMatchChars(PossCharsListInPlate) + + if (len(recursLOMatchCharsInPlate) == 0): + possPlate.strChars = "" + continue + + for i in range(0, len(recursLOMatchCharsInPlate)): + recursLOMatchCharsInPlate[i].sort(key = lambda matchingChar: matchingChar.intCenterX) + recursLOMatchCharsInPlate[i] = removeInnerOverlappingChars(recursLOMatchCharsInPlate[i]) + intLenOfLongestListOfChars = 0 + intIndexOfLongestListOfChars = 0 + for i in range(0, len(recursLOMatchCharsInPlate)): + if len(recursLOMatchCharsInPlate[i]) > intLenOfLongestListOfChars: + intLenOfLongestListOfChars = len(recursLOMatchCharsInPlate[i]) + intIndexOfLongestListOfChars = i + + longestListOfMatchingCharsInPlate = recursLOMatchCharsInPlate[intIndexOfLongestListOfChars] + + possPlate.strChars = recognizeCharsInPlate(possPlate.imgThresh, longestListOfMatchingCharsInPlate) + + return listOfpossPlates + +def loadKNNDataAndTrainKNN(): + try: + npaClass = np.loadtxt("classifications.txt", np.float32) + except: + print("error, cant open classifications.txt, exiting program\n") + os.system("pause") + return False + + try: + npaFlattenedImages = np.loadtxt("flattened_images.txt", np.float32) + except: + print("error, cant open flattened_images.txt, exiting program\n") + os.system("pause") + return False + + npaClass = npaClass.reshape((npaClass.size, 1)) + kNearest.setDefaultK(1) + kNearest.train(npaFlattenedImages, cv2.ml.ROW_SAMPLE, npaClass) + + return True + + + +def findListOfMatchingChars(possibleChar, listOfChars): + + listOfMatchingChars = [] + + for possibleMatchingChar in listOfChars: + if possibleMatchingChar == possibleChar: + continue + + fltDistanceBetweenChars = distanceBetweenChars(possibleChar, possibleMatchingChar) + + fltAngleBetweenChars = angleBetweenChars(possibleChar, possibleMatchingChar) + + fltChangeInArea = float(abs(possibleMatchingChar.intBoundingRectArea - possibleChar.intBoundingRectArea)) / float(possibleChar.intBoundingRectArea) + + fltChangeInWidth = float(abs(possibleMatchingChar.intBoundingRectWidth - possibleChar.intBoundingRectWidth)) / float(possibleChar.intBoundingRectWidth) + fltChangeInHeight = float(abs(possibleMatchingChar.intBoundingRectHeight - possibleChar.intBoundingRectHeight)) / float(possibleChar.intBoundingRectHeight) + + if (fltDistanceBetweenChars < (possibleChar.fltDiagonalSize * max_d_s_m_a) and + fltAngleBetweenChars < max_a_b_c and + fltChangeInArea < max_c_i_a and + fltChangeInWidth < max_c_i_w and + fltChangeInHeight < max_c_i_h): + + listOfMatchingChars.append(possibleMatchingChar) + return listOfMatchingChars + +def distanceBetweenChars(firstChar, secondChar): + intX = abs(firstChar.intCenterX - secondChar.intCenterX) + intY = abs(firstChar.intCenterY - secondChar.intCenterY) + + return math.sqrt((intX ** 2) + (intY ** 2)) + +def angleBetweenChars(firstChar, secondChar): + fltAdj = float(abs(firstChar.intCenterX - secondChar.intCenterX)) + fltOpp = float(abs(firstChar.intCenterY - secondChar.intCenterY)) + + if fltAdj != 0.0: + fltAngleInRad = math.atan(fltOpp / fltAdj) + else: + fltAngleInRad = 1.5708 + + fltAngleInDeg = fltAngleInRad * (180.0 / math.pi) + + return fltAngleInDeg + +def removeInnerOverlappingChars(listOfMatchingChars): + listOfMatchingCharsWithInnerCharRemoved = list(listOfMatchingChars) + + for currentChar in listOfMatchingChars: + for otherChar in listOfMatchingChars: + if currentChar != otherChar: + if distanceBetweenChars(currentChar, otherChar) < (currentChar.fltDiagonalSize * min_d_s_m_a): + if currentChar.intBoundingRectArea < otherChar.intBoundingRectArea: + if currentChar in listOfMatchingCharsWithInnerCharRemoved: + listOfMatchingCharsWithInnerCharRemoved.remove(currentChar) + else: + if otherChar in listOfMatchingCharsWithInnerCharRemoved: + listOfMatchingCharsWithInnerCharRemoved.remove(otherChar) + return listOfMatchingCharsWithInnerCharRemoved + +def recognizeCharsInPlate(imgThresh, listOfMatchingChars): + strChars = "" + + height, width = imgThresh.shape + + imgThreshColor = np.zeros((height, width, 3), np.uint8) + + listOfMatchingChars.sort(key = lambda matchingChar: matchingChar.intCenterX) + + cv2.cvtColor(imgThresh, cv2.COLOR_GRAY2BGR, imgThreshColor) + + for currentChar in listOfMatchingChars: + pt1 = (currentChar.intBoundingRectX, currentChar.intBoundingRectY) + pt2 = ((currentChar.intBoundingRectX + currentChar.intBoundingRectWidth), (currentChar.intBoundingRectY + currentChar.intBoundingRectHeight)) + + cv2.rectangle(imgThreshColor, pt1, pt2, Main.SCALAR_GREEN, 2) + + imgROI = imgThresh[currentChar.intBoundingRectY : currentChar.intBoundingRectY + currentChar.intBoundingRectHeight, + currentChar.intBoundingRectX : currentChar.intBoundingRectX + currentChar.intBoundingRectWidth] + + imgROIResized = cv2.resize(imgROI, (r_c_i_w, r_c_i_h)) + + npaROIResized = imgROIResized.reshape((1, r_c_i_w * r_c_i_h)) + + npaROIResized = np.float32(npaROIResized) + + retval, npaResults, neigh_resp, dists = kNearest.findNearest(npaROIResized, k = 1) + + strCurrentChar = str(chr(int(npaResults[0][0]))) + + strChars = strChars + strCurrentChar + + return strChars diff --git a/Disso Code/DetectPlates.py b/Disso Code/DetectPlates.py new file mode 100644 index 0000000..96734b5 --- /dev/null +++ b/Disso Code/DetectPlates.py @@ -0,0 +1,98 @@ +# DetectPlates.py + +import cv2 +import numpy as np +import math +import Main +import random + +import Preprocess +import DetectChars +import PossiblePlate +import PossibleChar + +PLATE_WIDTH_PADDING_FACTOR = 1.3 +PLATE_HEIGHT_PADDING_FACTOR = 1.5 + + +def findPossibleCharsInScene(imgThresh): + listOfPossibleChars = [] + + intCountOfPossibleChars = 0 + + imgThreshCopy = imgThresh.copy() + + contours, npaHierarchy = cv2.findContours(imgThreshCopy, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE) + + height, width = imgThresh.shape + imgContours = np.zeros((height, width, 3), np.uint8) + + for i in range(0, len(contours)): + + possibleChar = PossibleChar.PossibleChar(contours[i]) + + if DetectChars.checkIfPossibleChar(possibleChar): + intCountOfPossibleChars = intCountOfPossibleChars + 1 + listOfPossibleChars.append(possibleChar) + return listOfPossibleChars + + +def extractPlate(imgOriginal, listOfMatchingChars): + possiblePlate = PossiblePlate.PossiblePlate() + + listOfMatchingChars.sort(key = lambda matchingChar: matchingChar.intCenterX) + + fltPlateCenterX = (listOfMatchingChars[0].intCenterX + listOfMatchingChars[len(listOfMatchingChars) - 1].intCenterX) / 2.0 + fltPlateCenterY = (listOfMatchingChars[0].intCenterY + listOfMatchingChars[len(listOfMatchingChars) - 1].intCenterY) / 2.0 + + ptPlateCenter = fltPlateCenterX, fltPlateCenterY + + intPlateWidth = int((listOfMatchingChars[len(listOfMatchingChars) - 1].intBoundingRectX + listOfMatchingChars[len(listOfMatchingChars) - 1].intBoundingRectWidth - listOfMatchingChars[0].intBoundingRectX) * PLATE_WIDTH_PADDING_FACTOR) + + intTotalOfCharHeights = 0 + + for matchingChar in listOfMatchingChars: + intTotalOfCharHeights = intTotalOfCharHeights + matchingChar.intBoundingRectHeight + + fltAverageCharHeight = intTotalOfCharHeights / len(listOfMatchingChars) + + intPlateHeight = int(fltAverageCharHeight * PLATE_HEIGHT_PADDING_FACTOR) + + fltOpposite = listOfMatchingChars[len(listOfMatchingChars) - 1].intCenterY - listOfMatchingChars[0].intCenterY + fltHypotenuse = DetectChars.distanceBetweenChars(listOfMatchingChars[0], listOfMatchingChars[len(listOfMatchingChars) - 1]) + fltCorrectionAngleInRad = math.asin(fltOpposite / fltHypotenuse) + fltCorrectionAngleInDeg = fltCorrectionAngleInRad * (180.0 / math.pi) + + possiblePlate.rrLocationOfPlateInScene = ( tuple(ptPlateCenter), (intPlateWidth, intPlateHeight), fltCorrectionAngleInDeg ) + + rotationMatrix = cv2.getRotationMatrix2D(tuple(ptPlateCenter), fltCorrectionAngleInDeg, 1.0) + + height, width, numChannels = imgOriginal.shape + + imgRotated = cv2.warpAffine(imgOriginal, rotationMatrix, (width, height)) + + imgCropped = cv2.getRectSubPix(imgRotated, (intPlateWidth, intPlateHeight), tuple(ptPlateCenter)) + + possiblePlate.imgPlate = imgCropped + + return possiblePlate + +def detectPlatesInScene(imgOriginalScene): + listOfPossiblePlates = [] + height, width, numChannels = imgOriginalScene.shape + + imgGrayscaleScene = np.zeros((height, width, 1), np.uint8) + imgThreshScene = np.zeros((height, width, 1), np.uint8) + imgContours = np.zeros((height, width, 3), np.uint8) + + cv2.destroyAllWindows() + imgGrayscaleScene, imgThreshScene = Preprocess.preprocess(imgOriginalScene) + listOfPossibleCharsInScene = findPossibleCharsInScene(imgThreshScene) + listOfListsOfMatchingCharsInScene = DetectChars.findListOfListsOfMatchingChars(listOfPossibleCharsInScene) + print(5) + for listOfMatchingChars in listOfListsOfMatchingCharsInScene: + possiblePlate = extractPlate(imgOriginalScene, listOfMatchingChars) + + if possiblePlate.imgPlate is not None: + listOfPossiblePlates.append(possiblePlate) + return listOfPossiblePlates diff --git a/Disso Code/Main.py b/Disso Code/Main.py new file mode 100644 index 0000000..27a4cab --- /dev/null +++ b/Disso Code/Main.py @@ -0,0 +1,109 @@ +# Main.py + +import cv2 +import numpy as np +import os + +import DetectChars +import DetectPlates +import PossiblePlate + +SCALAR_BLACK = (0.0, 0.0, 0.0) +SCALAR_WHITE = (255.0, 255.0, 255.0) +SCALAR_YELLOW = (0.0, 255.0, 255.0) +SCALAR_GREEN = (0.0, 255.0, 0.0) +SCALAR_RED = (0.0, 0.0, 255.0) + +showSteps = False + +def main(): + + blnKNNTrainingSuccessful = DetectChars.loadKNNDataAndTrainKNN() + if blnKNNTrainingSuccessful == False: + print("\nerror: KNN traning was not successful\n") + return + + imgOriginalScene = cv2.imread("WebCams/WebCamTaxi/snakes-lane-720.png") + + if imgOriginalScene is None: + print("\nerror: image not read from file \n\n") + os.system("pause") + return + + listOfPossiblePlates = DetectPlates.detectPlatesInScene(imgOriginalScene) + + listOfPossiblePlates = DetectChars.detectCharsInPlates(listOfPossiblePlates) + + cv2.imshow("imgOriginalScene", imgOriginalScene) + + if len(listOfPossiblePlates) == 0: + print("\nno license plates were detected\n") + else: + listOfPossiblePlates.sort(key = lambda possiblePlate: len(possiblePlate.strChars), reverse = True) + licPlate = listOfPossiblePlates[0] + + cv2.imshow("imgPlate", licPlate.imgPlate) + cv2.imshow("imgThresh", licPlate.imgThresh) + + if len(licPlate.strChars) == 0: + print("\nno characters were detected\n\n") + return + drawRedRectangleAroundPlate(imgOriginalScene, licPlate) + + print("\nlicense plate read from image = " + licPlate.strChars + "\n") + print("----------------------------------------") + + writeLicensePlateCharsOnImage(imgOriginalScene, licPlate) + + cv2.imshow("imgOriginalScene", imgOriginalScene) + + cv2.imwrite("imgOriginalScene.png", imgOriginalScene) + + cv2.waitKey(0) + return + +def drawRedRectangleAroundPlate(imgOriginalScene, licPlate): + + p2fRectPoints = cv2.boxPoints(licPlate.rrLocationOfPlateInScene) + + cv2.line(imgOriginalScene, tuple(p2fRectPoints[0]), tuple(p2fRectPoints[1]), SCALAR_RED, 2) + cv2.line(imgOriginalScene, tuple(p2fRectPoints[1]), tuple(p2fRectPoints[2]), SCALAR_RED, 2) + cv2.line(imgOriginalScene, tuple(p2fRectPoints[2]), tuple(p2fRectPoints[3]), SCALAR_RED, 2) + cv2.line(imgOriginalScene, tuple(p2fRectPoints[3]), tuple(p2fRectPoints[0]), SCALAR_RED, 2) + +def writeLicensePlateCharsOnImage(imgOriginalScene, licPlate): + ptCenterOfTextAreaX = 0 + ptCenterOfTextAreaY = 0 + + ptLowerLeftTextOriginX = 0 + ptLowerLeftTextOriginY = 0 + + sceneHeight, sceneWidth, sceneNumChannels = imgOriginalScene.shape + plateHeight, plateWidth, plateNumChannels = licPlate.imgPlate.shape + + intFontFace = cv2.FONT_HERSHEY_SIMPLEX + fltFontScale = float(plateHeight) / 30.0 + intFontThickness = int(round(fltFontScale * 1.5)) + + textSize, baseline = cv2.getTextSize(licPlate.strChars, intFontFace, fltFontScale, intFontThickness) + ( (intPlateCenterX, intPlateCenterY), (intPlateWidth, intPlateHeight), fltCorrectionAngleInDeg ) = licPlate.rrLocationOfPlateInScene + + intPlateCenterX = int(intPlateCenterX) + intPlateCenterY = int(intPlateCenterY) + + ptCenterOfTextAreaX = int(intPlateCenterX) + + if intPlateCenterY < (sceneHeight * 0.75): + ptCenterOfTextAreaY = int(round(intPlateCenterY)) + int(round(plateHeight * 1.6)) + else: + ptCenterOfTextAreaY = int(round(intPlateCenterY)) - int(round(plateHeight * 1.6)) + + textSizeWidth, textSizeHeight = textSize + + ptLowerLeftTextOriginX = int(ptCenterOfTextAreaX - (textSizeWidth / 2)) + ptLowerLeftTextOriginY = int(ptCenterOfTextAreaY + (textSizeHeight / 2)) + + cv2.putText(imgOriginalScene, licPlate.strChars, (ptLowerLeftTextOriginX, ptLowerLeftTextOriginY), intFontFace, fltFontScale, SCALAR_YELLOW, intFontThickness) + +if __name__ == "__main__": + main() diff --git a/Disso Code/PossibleChar.py b/Disso Code/PossibleChar.py new file mode 100644 index 0000000..70e9edd --- /dev/null +++ b/Disso Code/PossibleChar.py @@ -0,0 +1,28 @@ +# PossibleChar.py + +import cv2 +import numpy as np +import math + +class PossibleChar: + + def __init__(self, _contour): + self.contour = _contour + + self.boundingRect = cv2.boundingRect(self.contour) + + [intX, intY, intWidth, intHeight] = self.boundingRect + + self.intBoundingRectX = intX + self.intBoundingRectY = intY + self.intBoundingRectWidth = intWidth + self.intBoundingRectHeight = intHeight + + self.intBoundingRectArea = self.intBoundingRectWidth * self.intBoundingRectHeight + + self.intCenterX = (self.intBoundingRectX + self.intBoundingRectX + self.intBoundingRectWidth) / 2 + self.intCenterY = (self.intBoundingRectY + self.intBoundingRectY + self.intBoundingRectHeight) / 2 + + self.fltDiagonalSize = math.sqrt((self.intBoundingRectWidth ** 2) + (self.intBoundingRectHeight ** 2)) + + self.fltAspectRatio = float(self.intBoundingRectWidth) / float(self.intBoundingRectHeight) diff --git a/Disso Code/Preproccess.py b/Disso Code/Preproccess.py new file mode 100644 index 0000000..05b38d9 --- /dev/null +++ b/Disso Code/Preproccess.py @@ -0,0 +1,51 @@ + +import cv2 +import numpy as np +import math + +GAUSSIAN_SMOOTH_FILTER_SIZE = (5, 5) +ADAPTIVE_THRESH_BLOCK_SIZE = 19 +ADAPTIVE_THRESH_WEIGHT = 9 + +def preprocess(imgOriginal): + imgGrayscale = extractValue(imgOriginal) + + imgMaxContrastGrayscale = maximizeContrast(imgGrayscale) + + height, width = imgGrayscale.shape + + imgBlurred = np.zeros((height, width, 1), np.uint8) + + imgBlurred = cv2.GaussianBlur(imgMaxContrastGrayscale, GAUSSIAN_SMOOTH_FILTER_SIZE, 0) + + imgThresh = cv2.adaptiveThreshold(imgBlurred, 255.0, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY_INV, ADAPTIVE_THRESH_BLOCK_SIZE, ADAPTIVE_THRESH_WEIGHT) + + return imgGrayscale, imgThresh + +def extractValue(imgOriginal): + height, width, numChannels = imgOriginal.shape + + imgHSV = np.zeros((height, width, 3), np.uint8) + + imgHSV = cv2.cvtColor(imgOriginal, cv2.COLOR_BGR2HSV) + + imgHue, imgSaturation, imgValue = cv2.split(imgHSV) + + return imgValue + +def maximizeContrast(imgGrayscale): + + height, width = imgGrayscale.shape + + imgTopHat = np.zeros((height, width, 1), np.uint8) + imgBlackHat = np.zeros((height, width, 1), np.uint8) + + structuringElement = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3)) + + imgTopHat = cv2.morphologyEx(imgGrayscale, cv2.MORPH_TOPHAT, structuringElement) + imgBlackHat = cv2.morphologyEx(imgGrayscale, cv2.MORPH_BLACKHAT, structuringElement) + + imgGrayscalePlusTopHat = cv2.add(imgGrayscale, imgTopHat) + imgGrayscalePlusTopHatMinusBlackHat = cv2.subtract(imgGrayscalePlusTopHat, imgBlackHat) + + return imgGrayscalePlusTopHatMinusBlackHat