پردازش تصویر ( سیستم تشخیص پلاک خودرو ) + پایتون

پردازش تصویر این روزا علاقه مند های زیادی رو به خودش جلب کرده ، خصوصا اگه بشه که از این مبحث توی زندگی واقعی بهره برد .قبل تر من یک پایان نامه در رابطه با پردازش تصویر و تصاویر ویدیویی نوشتم و اون رو به رایگان توی نت منتشر کردم . توی اون پایان نامه زبان مورد نظر زبان شیرین سی شارپ بود . اما آدما کم کم که تجربشون بالا میره می فهمند که برای اهداف خاص برنامه نویسی از چه زبان برنامه نویسی استفاده کنند .پایتون یه شاهکار مهندسی هستش که توی پردازش تصاویر و کلا پردازش محاسباتی با قدرت هرچه تمام ظاهر شده و در عین حال سادگی خودشو از دست نداده.توی این آموزش ما می خواهیم یکم در رابطه با سیستم تشخیص پلاک خودرو با کمک زبان پایتون صحبت کنیم .مفهوم ANPR چیست؟انگلیسی زبان ها ، استاد خلاصه سازی کلمات و جملات هستند و اصولا برای هر چیزی یک کلمه خلاصه شده دارند .در اینجا هم ANPR مخفف Automatic number plate recognition که ترجمش “تشخیص اتوماتیک شماره ی پلاک” میشه . برای اطلاعات بیشتر می تونید به ویکی پدیا مراجعه کنید و اطلاعات دقیق تر رو اونجا بخونید .حرف از خلاصه سازی و مخفف سازی جملات شد این نکته رو هم اشاره کنم که کلمه wc رو ما به عنوان دستشویی توی زبان انگلیسی بکار میبریم مخفف water closet هست که به عبارت قدیمی “دست به آب” خیلی خیلی نزدیک تر بوده . ترجمه wc میشه “گنجه/منبع آب” .این نکته برای سرگرمی بود .روش تشخیص پلاک می شه گفت که تعداد روش های شناسایی پلاک خیلی خیلی زیاده و هرکس اصولا بسته به هدفش یکی از این روش ها رو انتخاب میکنه .پیشنهاد می کنم که حتما اون لینک ویکی پدیا رو بخونید تا حساب کار دستتون بیاد.اما مرسوم ترین روش های پردازش تصویر ، classification یا طبقه بندی هستش .توی نت می تونید سرچ کنید و مطالب مربوط به اون رو دنبال کنید . اما من می خوام توی این آموزش یک مثال عملی و نسبتا ساده از سیستم تشخیص پلاک رو پیاده سازی کنم .برای همین وارد مباحث دانشگاهیش نمیشم . فقط برای کسایی که علاقه به جنبه علمیش هم دارند بگم که الگوریتم مورد استفاده ما KNN هستش .عکس زیر یکم علمی قضیه رو توضیح میده .(می تونید رو عکس کلیک راست کنید و توی یک تب جدید با کیفیت کامل عکس رو ببینید) نحوه کارکرد برنامه :این برنامه (مجموعه برنامه ) یک عکس پلاک رو به عنوان ورودی می گیره و کاراکتر و حروف روی پلاک رو به صورت تک تک تشخیص میده .سپس معادل کاراکتری هر حرف یا عدد رو توی برنامه جایگزین میکنه مثلا اگه عکس زیر رو بهش بدیم : خروجی برنامه میشه : DEF456این رو هم بگم که برنامه ابتدا “جدول عدد وحروف پایه” رو به عنوان یک اصل از ما میگیره و سپس کاراکتر های تشخیصی خودش رو با اون “جدول عدد وحروف پایه” مقایسه میکنه . توی پیاده سازی ، کامل تر توضیح میدم .پیاده سازی برنامه :کد زیر (زیر عکس) به منظور تولید فایل های مربوط به طبقه بندی classification ، یک عکس رو به عنوان ورودی از ما میگیره .مثلا ما عکس زیر رو می دیم به برنامه و اون دو تا فایل طبقه بندی شده که حاصل پردازش این عکس هست به ما میده . اون دوتا فایل تولیدی از عکس بالا به ترتیب به نام های classifications.txt و flattened_images.txt هستند . # GenData.py import sys import numpy as np import cv2 import os # module level variables ########################################################################## MIN_CONTOUR_AREA = 100 RESIZED_IMAGE_WIDTH = 20 RESIZED_IMAGE_HEIGHT = 30 ################################################################################################### def main(): imgTrainingNumbers = cv2.imread("training_chars.png") # read in training numbers image if imgTrainingNumbers is None: # if image was not read successfully print "error: image not read from file \n\n" # print error message to std out os.system("pause") # pause so user can see error message return # and exit function (which exits program) # end if imgGray = cv2.cvtColor(imgTrainingNumbers, cv2.COLOR_BGR2GRAY) # get grayscale image imgBlurred = cv2.GaussianBlur(imgGray, (5,5), 0) # blur # filter image from grayscale to black and white imgThresh = cv2.adaptiveThreshold(imgBlurred, # input image 255, # make pixels that pass the threshold full white cv2.ADAPTIVE_THRESH_GAUSSIAN_C, # use gaussian rather than mean, seems to give better results cv2.THRESH_BINARY_INV, # invert so foreground will be white, background will be black 11, # size of a pixel neighborhood used to calculate threshold value 2) # constant subtracted from the mean or weighted mean cv2.imshow("imgThresh", imgThresh) # show threshold image for reference imgThreshCopy = imgThresh.copy() # make a copy of the thresh image, this in necessary b/c findContours modifies the image imgContours, npaContours, npaHierarchy = cv2.findContours(imgThreshCopy, # input image, make sure to use a copy since the function will modify this image in the course of finding contours cv2.RETR_EXTERNAL, # retrieve the outermost contours only cv2.CHAIN_APPROX_SIMPLE) # compress horizontal, vertical, and diagonal segments and leave only their end points # declare empty numpy array, we will use this to write to file later # zero rows, enough cols to hold all image data npaFlattenedImages = np.empty((0, RESIZED_IMAGE_WIDTH * RESIZED_IMAGE_HEIGHT)) intClassifications = [] # declare empty classifications list, this will be our list of how we are classifying our chars from user input, we will write to file at the end # possible chars we are interested in are digits 0 through 9, put these in list intValidChars intValidChars = [ord('0'), ord('1'), ord('2'), ord('3'), ord('4'), ord('5'), ord('6'), ord('7'), ord('8'), ord('9'), ord('A'), ord('B'), ord('C'), ord('D'), ord('E'), ord('F'), ord('G'), ord('H'), ord('I'), ord('J'), ord('K'), ord('L'), ord('M'), ord('N'), ord('O'), ord('P'), ord('Q'), ord('R'), ord('S'), ord('T'), ord('U'), ord('V'), ord('W'), ord('X'), ord('Y'), ord('Z')] for npaContour in npaContours: # for each contour if cv2.contourArea(npaContour) > MIN_CONTOUR_AREA: # if contour is big enough to consider [intX, intY, intW, intH] = cv2.boundingRect(npaContour) # get and break out bounding rect # draw rectangle around each contour as we ask user for input cv2.rectangle(imgTrainingNumbers, # draw rectangle on original training image (intX, intY), # upper left corner (intX+intW,intY+intH), # lower right corner (0, 0, 255), # red 2) # thickness imgROI = imgThresh[intY:intY+intH, intX:intX+intW] # crop char out of threshold image imgROIResized = cv2.resize(imgROI, (RESIZED_IMAGE_WIDTH, RESIZED_IMAGE_HEIGHT)) # resize image, this will be more consistent for recognition and storage cv2.imshow("imgROI", imgROI) # show cropped out char for reference cv2.imshow("imgROIResized", imgROIResized) # show resized image for reference cv2.imshow("training_numbers.png", imgTrainingNumbers) # show training numbers image, this will now have red rectangles drawn on it intChar = cv2.waitKey(0) # get key press if intChar == 27: # if esc key was pressed sys.exit() # exit program elif intChar in intValidChars: # else if the char is in the list of chars we are looking for . . . intClassifications.append(intChar) # append classification char to integer list of chars (we will convert to float later before writing to file) npaFlattenedImage = imgROIResized.reshape((1, RESIZED_IMAGE_WIDTH * RESIZED_IMAGE_HEIGHT)) # flatten image to 1d numpy array so we can write to file later npaFlattenedImages = np.append(npaFlattenedImages, npaFlattenedImage, 0) # add current flattened impage numpy array to list of flattened image numpy arrays # end if # end if # end for fltClassifications = np.array(intClassifications, np.float32) # convert classifications list of ints to numpy array of floats npaClassifications = fltClassifications.reshape((fltClassifications.size, 1)) # flatten numpy array of floats to 1d so we can write to file later print "\n\ntraining complete !!\n" np.savetxt("classifications.txt", npaClassifications) # write flattened images to file np.savetxt("flattened_images.txt", npaFlattenedImages) # cv2.destroyAllWindows() # remove windows from memory return ################################################################################################### if __name__ == "__main__": main() # end if این برنامه جدول حروف استاندارد خودشو میسازه که هر بار از طریق این جدول حروف پلاک رو تشخیص بده . باید بگم که این برنامه بالا هر حرف از جدول رو که تشخیص داد ، شما باید کلمه متناظر اون رو روی کیبورد فشار بدید .مثلا اگه روی حرف Z داخل عکس قرار گرفت شما باید z رو روی کیبورد فشار بدید.در نهایت وقتی کد اجراش تموم بشه سیستم تشخیص پلاک ما دیگه با عکس بالا کاری نداره و برای تشخیص به فایل های classifications.txt و flattened_images.txt رجوع میکنه .اما برنامه تشخیص پلاک :این کد پلاک رو از ما میگره و در نهایت به عنوان خروجی حروف و اعداد روی پلاک رو برای ما چاپ میکنه # TrainAndTest.py import cv2 import numpy as np import operator import os # module level variables ########################################################################## MIN_CONTOUR_AREA = 100 RESIZED_IMAGE_WIDTH = 20 RESIZED_IMAGE_HEIGHT = 30 ################################################################################################### class ContourWithData(): # member variables ############################################################################ npaContour = None # contour boundingRect = None # bounding rect for contour intRectX = 0 # bounding rect top left corner x location intRectY = 0 # bounding rect top left corner y location intRectWidth = 0 # bounding rect width intRectHeight = 0 # bounding rect height fltArea = 0.0 # area of contour def calculateRectTopLeftPointAndWidthAndHeight(self): # calculate bounding rect info [intX, intY, intWidth, intHeight] = self.boundingRect self.intRectX = intX self.intRectY = intY self.intRectWidth = intWidth self.intRectHeight = intHeight def checkIfContourIsValid(self): # this is oversimplified, for a production grade program if self.fltArea < MIN_CONTOUR_AREA: return False # much better validity checking would be necessary return True ################################################################################################### def main(): allContoursWithData = [] # declare empty lists, validContoursWithData = [] # we will fill these shortly try: npaClassifications = np.loadtxt("classifications.txt", np.float32) # read in training classifications except: print "error, unable to open classifications.txt, exiting program\n" os.system("pause") return # end try try: npaFlattenedImages = np.loadtxt("flattened_images.txt", np.float32) # read in training images except: print "error, unable to open flattened_images.txt, exiting program\n" os.system("pause") return # end try npaClassifications = npaClassifications.reshape((npaClassifications.size, 1)) # reshape numpy array to 1d, necessary to pass to call to train kNearest = cv2.ml.KNearest_create() # instantiate KNN object kNearest.train(npaFlattenedImages, cv2.ml.ROW_SAMPLE, npaClassifications) imgTestingNumbers = cv2.imread("test1.png") # read in testing numbers image if imgTestingNumbers is None: # if image was not read successfully print "error: image not read from file \n\n" # print error message to std out os.system("pause") # pause so user can see error message return # and exit function (which exits program) # end if imgGray = cv2.cvtColor(imgTestingNumbers, cv2.COLOR_BGR2GRAY) # get grayscale image imgBlurred = cv2.GaussianBlur(imgGray, (5,5), 0) # blur # filter image from grayscale to black and white imgThresh = cv2.adaptiveThreshold(imgBlurred, # input image 255, # make pixels that pass the threshold full white cv2.ADAPTIVE_THRESH_GAUSSIAN_C, # use gaussian rather than mean, seems to give better results cv2.THRESH_BINARY_INV, # invert so foreground will be white, background will be black 11, # size of a pixel neighborhood used to calculate threshold value 2) # constant subtracted from the mean or weighted mean imgThreshCopy = imgThresh.copy() # make a copy of the thresh image, this in necessary b/c findContours modifies the image imgContours, npaContours, npaHierarchy = cv2.findContours(imgThreshCopy, # input image, make sure to use a copy since the function will modify this image in the course of finding contours cv2.RETR_EXTERNAL, # retrieve the outermost contours only cv2.CHAIN_APPROX_SIMPLE) # compress horizontal, vertical, and diagonal segments and leave only their end points for npaContour in npaContours: # for each contour contourWithData = ContourWithData() # instantiate a contour with data object contourWithData.npaContour = npaContour # assign contour to contour with data contourWithData.boundingRect = cv2.boundingRect(contourWithData.npaContour) # get the bounding rect contourWithData.calculateRectTopLeftPointAndWidthAndHeight() # get bounding rect info contourWithData.fltArea = cv2.contourArea(contourWithData.npaContour) # calculate the contour area allContoursWithData.append(contourWithData) # add contour with data object to list of all contours with data # end for for contourWithData in allContoursWithData: # for all contours if contourWithData.checkIfContourIsValid(): # check if valid validContoursWithData.append(contourWithData) # if so, append to valid contour list # end if # end for validContoursWithData.sort(key = operator.attrgetter("intRectX")) # sort contours from left to right strFinalString = "" # declare final string, this will have the final number sequence by the end of the program for contourWithData in validContoursWithData: # for each contour # draw a green rect around the current char cv2.rectangle(imgTestingNumbers, # draw rectangle on original testing image (contourWithData.intRectX, contourWithData.intRectY), # upper left corner (contourWithData.intRectX + contourWithData.intRectWidth, contourWithData.intRectY + contourWithData.intRectHeight), # lower right corner (0, 255, 0), # green 2) # thickness imgROI = imgThresh[contourWithData.intRectY : contourWithData.intRectY + contourWithData.intRectHeight, # crop char out of threshold image contourWithData.intRectX : contourWithData.intRectX + contourWithData.intRectWidth] imgROIResized = cv2.resize(imgROI, (RESIZED_IMAGE_WIDTH, RESIZED_IMAGE_HEIGHT)) # resize image, this will be more consistent for recognition and storage npaROIResized = imgROIResized.reshape((1, RESIZED_IMAGE_WIDTH * RESIZED_IMAGE_HEIGHT)) # flatten image into 1d numpy array npaROIResized = np.float32(npaROIResized) # convert from 1d numpy array of ints to 1d numpy array of floats retval, npaResults, neigh_resp, dists = kNearest.findNearest(npaROIResized, k = 1) # call KNN function find_nearest strCurrentChar = str(chr(int(npaResults[0][0]))) # get character from results strFinalString = strFinalString + strCurrentChar # append current char to full string # end for print "\n" + strFinalString + "\n" # show the full string cv2.imshow("imgTestingNumbers", imgTestingNumbers) # show input image with green boxes drawn around found digits cv2.waitKey(0) # wait for user key press cv2.destroyAllWindows() # remove windows from memory return ################################################################################################### if __name__ == "__main__": main() # end if نکته: برای اجرای این برنامه ها شما نیاز دارید که کتابخانه opencv 3 به مجموعه کتابخانه های پایتون تون اضافه بشه.برای نصب opencv ابتدا باید کل کتابخانه opencv رو دانلود کنید سپس توی فولدر های مربوط به کتابخانه هاش یه فایل هست به اسم cv2.pyd یا همچین چیزی .سپس این فایل رو کپی کنید و ببیرید توی شاخه ای که پایتون رو نصب کردید .مثلا برای من میشه :C:\Python27\Lib\site-packagesو فایل رو داخل این شاخه قرار بدید. همین .دانلود کامل سورس کد ها و تصاویر به صورت یکجا در اینجا یا از گیت هابدرپایان یک ویدیو هم از تمام این چیز هایی که گفتم براتون گذاشتم .ولی زبانش انگلیسیه . https://www.aparat.com/v/2L3K8 برای تشخیص پلاک های ایرانی با همین آموزش ، فقط کافیه کمی ابتکار بخرج بدید. پ.ن: باقی مطالب رو از www.nikitv.ir هم میتونید دنبال کنید. موفق باشید.