📜  通过视频实时检测和识别车牌

📅  最后修改于: 2021-04-16 06:03:01             🧑  作者: Mango

对于基于摄像机监控的安全系统来说,识别车牌是一项非常重要的任务。我们可以使用某些计算机视觉技术从图像中提取车牌,然后使用光学字符识别来识别车牌号。在这里,我将指导您完成此任务的整个过程。

要求:

例子:

方法:

  • 找到图像中的所有轮廓。
  • 找到每个轮廓的边界矩形。
  • 用平均车牌比较并验证每个边界矩形的边长和面积。
  • 在经过验证的轮廓内的图像中应用图像分割,以在其中找到字符。
  • 使用OCR识别字符。

方法:

  1. 为了减少噪声,我们需要使用高斯模糊对输入图像进行模糊处理,然后将其转换为灰度。
  2. 查找图像中的垂直边缘。
  3. 要显示印版,我们必须对图像进行二值化处理。为此,在垂直边缘图像上应用Otsu的阈值。在其他阈值方法中,我们必须选择一个阈值以对图像进行二值化,但是Otsu的阈值自动确定该值。
  4. 在阈值图像上应用闭合形态转换。关闭可用于填充阈值图像中白色区域之间的黑色小区域。它显示了牌照的矩形白色框。

    以上四个步骤是通过PlateFinder类的预处理方法执行的

    def preprocess(self, input_img):
      
        imgBlurred = cv2.GaussianBlur(input_img, (7, 7), 0)
          
        # convert to gray
        gray = cv2.cvtColor(imgBlurred,
                            cv2.COLOR_BGR2GRAY) 
          
        # sobelX to get the vertical edges
        sobelx = cv2.Sobel(gray, cv2.CV_8U, 
                           1, 0, ksize = 3)  
          
         # otsu's thresholding
        ret2, threshold_img = cv2.threshold(sobelx,0, 255,
                               cv2.THRESH_BINARY + cv2.THRESH_OTSU)
     
        element = self.element_structure
        morph_n_thresholded_img = threshold_img.copy()
        cv2.morphologyEx(src = threshold_img,
                         op = cv2.MORPH_CLOSE,
                         kernel = element, 
                         dst = morph_n_thresholded_img)
          
        return morph_n_thresholded_img
    
  5. 要检测印版,我们需要在图像中找到轮廓。重要的是在找到轮廓之前对图像进行二值化和变形处理,以便它可以在图像中找到更多相关且更少数量的轮廓。如果您在原始图像上绘制所有提取的轮廓,则将如下所示:

    此步骤由PlateFinder类的extract_contours方法执行

    def extract_contours(self, after_preprocess):
      
        _, contours, _ = cv2.findContours(after_preprocess,
                                          mode = cv2.RETR_EXTERNAL,
                                          method = cv2.CHAIN_APPROX_NONE)
        return contours
    
  6. 现在找到每个轮廓所包围的最小面积矩形,并验证其边长比和面积。我们将板的最小和最大面积分别定义为4500和30000。

    代码:验证最小面积矩形的面积和边长比的方法是validateRatio
    PlateFinder类的preRatioCheck

    def validateRatio(self, rect):
      
        (x, y), (width, height), rect_angle = rect
     
        if (width > height):
            angle = -rect_angle
              
        else:
            angle = 90 + rect_angle
     
        if angle > 15:
            return False
          
        if (height == 0 or width == 0):
            return False
     
        area = width * height
          
        if not self.preRatioCheck(area, width, height):
            return False
          
        else:
            return True
     
    def preRatioCheck(self, area, width, height):
          
        min = self.min_area
        max = self.max_area
     
        ratioMin = 2.5
        ratioMax = 7
     
        ratio = float(width) / float(height)
          
        if ratio < 1:
            ratio = 1 / ratio
     
        if (area < min or area > max) or (ratio < ratioMin or ratio > ratioMax):
            return False
          
        return True
    
  7. 现在在经过验证的区域中找到轮廓,并验证该区域中最大轮廓的边长比和矩形区域。验证后,您将获得车牌的完美轮廓。现在从原始图像中提取轮廓。您将获得印版的图像:
  8. 代码:此步骤由PlateFinder类的clean_plateratioCheck方法执行
    def clean_plate(self, plate):
      
        gray = cv2.cvtColor(plate, cv2.COLOR_BGR2GRAY)
        thresh = cv2.adaptiveThreshold(gray, 255, 
                                       cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
                                       cv2.THRESH_BINARY, 11, 2)
          
        _, contours, _ = cv2.findContours(thresh.copy(), 
                                          cv2.RETR_EXTERNAL,
                                          cv2.CHAIN_APPROX_NONE)
     
        if contours:
              
            areas = [cv2.contourArea(c) for c in contours]
              
            # index of the largest contour in the 
            # areas array
            max_index = np.argmax(areas)  
     
            max_cnt = contours[max_index]
            max_cntArea = areas[max_index]
            x, y, w, h = cv2.boundingRect(max_cnt)
     
            if not self.ratioCheck(max_cntArea,
                                   plate.shape[1],
                                   plate.shape[0]):
                return plate, False, None
              
            return plate, True, [x, y, w, h]
          
        else:
            return plate, False, None
     
    def ratioCheck(self, area, width, height):
          
        min = self.min_area
        max = self.max_area
     
        ratioMin = 3
        ratioMax = 6
     
        ratio = float(width) / float(height)
          
        if ratio < 1:
            ratio = 1 / ratio
     
        if (area < min or area > max) or (ratio < ratioMin or ratio > ratioMax):
            return False
          
        return True
    
  9. 为了准确识别车牌上的字符,我们必须应用图像分割。为此,第一步是从印版图像的HSV格式中提取值通道。它看起来像-

  10. 现在,在印版的价值通道图像上应用自适应阈值以将其二值化并显示字符。印版的图像在不同区域可能具有不同的闪电条件,在这种情况下,自适应阈值处理可能更适合于二值化,因为它会根据其周围区域中像素的亮度对不同区域使用不同的阈值。

  11. 二值化后,对图像应用按位非运算,以在图像中找到连接的组件,以便我们提取候选字符。

  12. 构造一个遮罩以显示所有字符组件,然后在遮罩中找到轮廓。提取等高线后,取最大的等高线,找到其边界矩形并验证边长比。
  13. 在验证了边长比之后,找到轮廓的凸包并将其绘制在候选字符蒙版上。面具看起来像-

  14. 现在,在字符候选遮罩中找到所有轮廓,并从车牌的阈值图像中提取这些轮廓区域,您将分别获得所有字符。

    步骤813是由segment_chars函数执行的,您可以在下面的完整源代码中找到该函数。步骤6至13中使用的功能的驱动程序代码编写在PlateFinder类的check_plate方法中。
  15. 现在,使用OCR逐个识别字符。

完整的源代码及其工作原理首先,创建PlateFinder类,该类可查找车牌并验证其尺寸比例和面积。

import cv2
import numpy as np
from skimage.filters import threshold_local
import tensorflow as tf
from skimage import measure
import imutils
  
  
def sort_cont(character_contours):
    """
    To sort contours
    """
    i = 0
    boundingBoxes = [cv2.boundingRect(c) for c in character_contours]
      
    (character_contours, boundingBoxes) = zip(*sorted(zip(character_contours,
                                                          boundingBoxes),
                                                      key = lambda b: b[1][i],
                                                      reverse = False))
      
    return character_contours
  
  
def segment_chars(plate_img, fixed_width):
      
    """
    extract Value channel from the HSV format
    of image and apply adaptive thresholding
    to reveal the characters on the license plate
    """
    V = cv2.split(cv2.cvtColor(plate_img, cv2.COLOR_BGR2HSV))[2]
  
    thresh = cv2.adaptiveThreshold(value, 255, 
                                   cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
                                   cv2.THRESH_BINARY, 
                                   11, 2)
  
    thresh = cv2.bitwise_not(thresh)
  
    # resize the license plate region to
    # a canoncial size
    plate_img = imutils.resize(plate_img, width = fixed_width)
    thresh = imutils.resize(thresh, width = fixed_width)
    bgr_thresh = cv2.cvtColor(thresh, cv2.COLOR_GRAY2BGR)
  
    # perform a connected components analysis 
    # and initialize the mask to store the locations
    # of the character candidates
    labels = measure.label(thresh, neighbors = 8, background = 0)
  
    charCandidates = np.zeros(thresh.shape, dtype ='uint8')
  
    # loop over the unique components
    characters = []
    for label in np.unique(labels):
          
        # if this is the background label, ignore it
        if label == 0:
            continue
        # otherwise, construct the label mask to display
        # only connected components for the current label,
        # then find contours in the label mask
        labelMask = np.zeros(thresh.shape, dtype ='uint8')
        labelMask[labels == label] = 255
  
        cnts = cv2.findContours(labelMask, 
                     cv2.RETR_EXTERNAL, 
                     cv2.CHAIN_APPROX_SIMPLE)
  
        cnts = cnts[0] if imutils.is_cv2() else cnts[1]
  
        # ensure at least one contour was found in the mask
        if len(cnts) > 0:
  
            # grab the largest contour which corresponds 
            # to the component in the mask, then grab the
            # bounding box for the contour
            c = max(cnts, key = cv2.contourArea)
            (boxX, boxY, boxW, boxH) = cv2.boundingRect(c)
  
            # compute the aspect ratio, solodity, and 
            # height ration for the component
            aspectRatio = boxW / float(boxH)
            solidity = cv2.contourArea(c) / float(boxW * boxH)
            heightRatio = boxH / float(plate_img.shape[0])
  
            # determine if the aspect ratio, solidity, 
            # and height of the contour pass the rules
            # tests
            keepAspectRatio = aspectRatio < 1.0
            keepSolidity = solidity > 0.15
            keepHeight = heightRatio > 0.5 and heightRatio < 0.95
  
            # check to see if the component passes
            # all the tests
            if keepAspectRatio and keepSolidity and keepHeight and boxW > 14:
                  
                # compute the convex hull of the contour
                # and draw it on the character candidates
                # mask
                hull = cv2.convexHull(c)
  
                cv2.drawContours(charCandidates, [hull], -1, 255, -1)
  
    _, contours, hier = cv2.findContours(charCandidates,
                                         cv2.RETR_EXTERNAL,
                                         cv2.CHAIN_APPROX_SIMPLE)
      
    if contours:
        contours = sort_cont(contours)
          
        # value to be added to each dimension 
        # of the character
        addPixel = 4  
        for c in contours:
            (x, y, w, h) = cv2.boundingRect(c)
            if y > addPixel:
                y = y - addPixel
            else:
                y = 0
            if x > addPixel:
                x = x - addPixel
            else:
                x = 0
            temp = bgr_thresh[y:y + h + (addPixel * 2),
                              x:x + w + (addPixel * 2)]
  
            characters.append(temp)
              
        return characters
      
    else:
        return None
  
  
  
class PlateFinder:
    def __init__(self):
          
        # minimum area of the plate
        self.min_area = 4500  
          
        # maximum area of the plate
        self.max_area = 30000  
  
        self.element_structure = cv2.getStructuringElement(
                              shape = cv2.MORPH_RECT, ksize =(22, 3))
  
    def preprocess(self, input_img):
          
        imgBlurred = cv2.GaussianBlur(input_img, (7, 7), 0)
          
        # convert to gray
        gray = cv2.cvtColor(imgBlurred, cv2.COLOR_BGR2GRAY) 
          
        # sobelX to get the vertical edges
        sobelx = cv2.Sobel(gray, cv2.CV_8U, 1, 0, ksize = 3)  
          
        # otsu's thresholding
        ret2, threshold_img = cv2.threshold(sobelx, 0, 255,
                         cv2.THRESH_BINARY + cv2.THRESH_OTSU) 
  
        element = self.element_structure
        morph_n_thresholded_img = threshold_img.copy()
        cv2.morphologyEx(src = threshold_img, 
                         op = cv2.MORPH_CLOSE,
                         kernel = element,
                         dst = morph_n_thresholded_img)
          
        return morph_n_thresholded_img
  
    def extract_contours(self, after_preprocess):
          
        _, contours, _ = cv2.findContours(after_preprocess, 
                                          mode = cv2.RETR_EXTERNAL,
                                          method = cv2.CHAIN_APPROX_NONE)
        return contours
  
    def clean_plate(self, plate):
          
        gray = cv2.cvtColor(plate, cv2.COLOR_BGR2GRAY)
        thresh = cv2.adaptiveThreshold(gray,
                                       255, 
                                       cv2.ADAPTIVE_THRESH_GAUSSIAN_C, 
                                       cv2.THRESH_BINARY,
                                       11, 2)
          
        _, contours, _ = cv2.findContours(thresh.copy(), 
                                          cv2.RETR_EXTERNAL,
                                          cv2.CHAIN_APPROX_NONE)
  
        if contours:
            areas = [cv2.contourArea(c) for c in contours]
              
            # index of the largest contour in the area
            # array
            max_index = np.argmax(areas)  
  
            max_cnt = contours[max_index]
            max_cntArea = areas[max_index]
            x, y, w, h = cv2.boundingRect(max_cnt)
            rect = cv2.minAreaRect(max_cnt)
              
            if not self.ratioCheck(max_cntArea, plate.shape[1], 
                                                plate.shape[0]):
                return plate, False, None
              
            return plate, True, [x, y, w, h]
          
        else:
            return plate, False, None
  
  
  
    def check_plate(self, input_img, contour):
          
        min_rect = cv2.minAreaRect(contour)
          
        if self.validateRatio(min_rect):
            x, y, w, h = cv2.boundingRect(contour)
            after_validation_img = input_img[y:y + h, x:x + w]
            after_clean_plate_img, plateFound, coordinates = self.clean_plate(
                                                        after_validation_img)
              
            if plateFound:
                characters_on_plate = self.find_characters_on_plate(
                                              after_clean_plate_img)
                  
                if (characters_on_plate is not None and len(characters_on_plate) == 8):
                    x1, y1, w1, h1 = coordinates
                    coordinates = x1 + x, y1 + y
                    after_check_plate_img = after_clean_plate_img
                      
                    return after_check_plate_img, characters_on_plate, coordinates
          
        return None, None, None
  
  
  
    def find_possible_plates(self, input_img):
          
        """
        Finding all possible contours that can be plates
        """
        plates = []
        self.char_on_plate = []
        self.corresponding_area = []
  
        self.after_preprocess = self.preprocess(input_img)
        possible_plate_contours = self.extract_contours(self.after_preprocess)
  
        for cnts in possible_plate_contours:
            plate, characters_on_plate, coordinates = self.check_plate(input_img, cnts)
              
            if plate is not None:
                plates.append(plate)
                self.char_on_plate.append(characters_on_plate)
                self.corresponding_area.append(coordinates)
  
        if (len(plates) > 0):
            return plates
          
        else:
            return None
  
    def find_characters_on_plate(self, plate):
  
        charactersFound = segment_chars(plate, 400)
        if charactersFound:
            return charactersFound
  
    # PLATE FEATURES
    def ratioCheck(self, area, width, height):
          
        min = self.min_area
        max = self.max_area
  
        ratioMin = 3
        ratioMax = 6
  
        ratio = float(width) / float(height)
          
        if ratio < 1:
            ratio = 1 / ratio
          
        if (area < min or area > max) or (ratio < ratioMin or ratio > ratioMax):
            return False
          
        return True
  
    def preRatioCheck(self, area, width, height):
          
        min = self.min_area
        max = self.max_area
  
        ratioMin = 2.5
        ratioMax = 7
  
        ratio = float(width) / float(height)
          
        if ratio < 1:
            ratio = 1 / ratio
  
        if (area < min or area > max) or (ratio < ratioMin or ratio > ratioMax):
            return False
          
        return True
  
    def validateRatio(self, rect):
        (x, y), (width, height), rect_angle = rect
  
        if (width > height):
            angle = -rect_angle
        else:
            angle = 90 + rect_angle
  
        if angle > 15:
            return False
          
        if (height == 0 or width == 0):
            return False
  
        area = width * height
          
        if not self.preRatioCheck(area, width, height):
            return False
        else:
            return True

这是PlateFinder类的每种方法的说明
预处理方法中,已完成以下步骤:

  • 模糊影像
  • 转换为灰度
  • 查找垂直边缘
  • 阈值垂直边缘图像。
  • 关闭“变形阈值”图像。

方法extract_contours返回预处理图像中的所有外部轮廓。
方法find_possible_plates使用预处理方法对图像进行预处理,然后通过extract_contours方法提取轮廓,然后检查所有提取轮廓的边长比和面积,并使用check_plateclean_plate方法清洁轮廓内部的图像。用clean_plate方法清洗轮廓图像后,它使用find_characters_on_plate方法在板上找到所有字符。
find_characters_on_plate方法使用segment_chars函数查找字符。它通过计算阈值的值的图像的轮廓的凸包和绘图它上的字符,揭示他们发现字符。
代码:创建另一个类来初始化神经网络,以预测提取的车牌上的字符。

class OCR:
      
    def __init__(self):
          
        self.model_file = "./model / binary_128_0.50_ver3.pb"
        self.label_file = "./model / binary_128_0.50_labels_ver2.txt"
        self.label = self.load_label(self.label_file)
        self.graph = self.load_graph(self.model_file)
        self.sess = tf.Session(graph = self.graph)
  
    def load_graph(self, modelFile):
          
        graph = tf.Graph()
        graph_def = tf.GraphDef()
          
        with open(modelFile, "rb") as f:
            graph_def.ParseFromString(f.read())
          
        with graph.as_default():
            tf.import_graph_def(graph_def)
          
        return graph
  
    def load_label(self, labelFile):
        label = []
        proto_as_ascii_lines = tf.gfile.GFile(labelFile).readlines()
          
        for l in proto_as_ascii_lines:
            label.append(l.rstrip())
          
        return label
  
    def convert_tensor(self, image, imageSizeOuput):
        """
        takes an image and tranform it in tensor
        """
        image = cv2.resize(image, 
                           dsize =(imageSizeOuput,
                                  imageSizeOuput),
                           interpolation = cv2.INTER_CUBIC)
          
        np_image_data = np.asarray(image)
        np_image_data = cv2.normalize(np_image_data.astype('float'),
                                      None, -0.5, .5,
                                      cv2.NORM_MINMAX)
          
        np_final = np.expand_dims(np_image_data, axis = 0)
          
        return np_final
  
    def label_image(self, tensor):
  
        input_name = "import / input"
        output_name = "import / final_result"
  
        input_operation = self.graph.get_operation_by_name(input_name)
        output_operation = self.graph.get_operation_by_name(output_name)
  
        results = self.sess.run(output_operation.outputs[0],
                                {input_operation.outputs[0]: tensor})
        results = np.squeeze(results)
        labels = self.label
        top = results.argsort()[-1:][::-1]
          
        return labels[top[0]]
  
    def label_image_list(self, listImages, imageSizeOuput):
        plate = ""
          
        for img in listImages:
              
            if cv2.waitKey(25) & 0xFF == ord('q'):
                break
            plate = plate + self.label_image(self.convert_tensor(img, imageSizeOuput))
          
        return plate, len(plate)

它将预训练的OCR模型及其标签文件加载到load_graphload_label函数中。 label_image_list方法使用convert_tensor方法将图像转换为张量,然后使用label_image_list函数预测张量的标签并返回许可证号。
代码:创建一个主要函数来按顺序执行整个任务。

if __name__ == "__main__":
      
    findPlate = PlateFinder()
    model = OCR()
  
    cap = cv2.VideoCapture('test_videos / video.MOV')
      
    while (cap.isOpened()):
        ret, img = cap.read()
          
        if ret == True:
            cv2.imshow('original video', img)
              
            if cv2.waitKey(25) & 0xFF == ord('q'):
                break
              
            possible_plates = findPlate.find_possible_plates(img)
              
            if possible_plates is not None:
                  
                for i, p in enumerate(possible_plates):
                    chars_on_plate = findPlate.char_on_plate[i]
                    recognized_plate, _ = model.label_image_list(
                               chars_on_plate, imageSizeOuput = 128)
  
                    print(recognized_plate)
                    cv2.imshow('plate', p)
                      
                    if cv2.waitKey(25) & 0xFF == ord('q'):
                        break
        else:
            break
              
    cap.release()
    cv2.destroyAllWindows()

您可以从我的GitHub下载带有OCR模型的源代码和测试视频。
如何改进模型?

  • 您可以在车架上设置一个特定的小区域以查找车内的车牌(确保所有车辆都必须通过该区域)。
  • 您可以训练自己的机器学习模型来识别字符,因为给定的模型无法识别所有字母。

参考:
自动车牌识别系统(ANPR):Chirag Indravadanbhai Patel进行的调查。
OpenCV文档中的图像预处理技术。