Detecta y reconoce matrículas de automóviles a partir de un video en tiempo real

Reconocer la matrícula de un automóvil es una tarea muy importante para un sistema de seguridad basado en cámaras de vigilancia. Podemos extraer la matrícula de una imagen usando algunas técnicas de visión por computadora y luego podemos usar el Reconocimiento Óptico de Caracteres para reconocer el número de licencia. Aquí te guiaré a través de todo el procedimiento de esta tarea.
Requisitos: 

opencv-python 3.4.2
numpy
1.17.2 skimage
0.16.2 tensorflow 1.15.0
imutils 0.5.3

Ejemplo: 
 

Aporte: 
 

Salida: 
29A33185 
 

Acercarse: 
 

  • Encuentra todos los contornos en la imagen.
  • Encuentre el rectángulo delimitador de cada contorno.
  • Compare y valide la proporción de lados y el área de cada rectángulo delimitador con una matrícula promedio.
  • Aplicar segmentación de imagen en la imagen dentro del contorno validado para encontrar caracteres en ella.
  • Reconoce caracteres usando un OCR.

Metodología: 
 

1 . Para reducir el ruido, debemos desenfocar la imagen de entrada con Gaussian Blur y luego convertirla a escala de grises. 
 

2 . Encuentra bordes verticales en la imagen. 
 

3. Para revelar la placa tenemos que binarizar la imagen. Para esto, aplique la Umbralización de Otsu en la imagen del borde vertical. En otros métodos de umbralización, tenemos que elegir un valor de umbral para binarizar la imagen, pero la Umbralización de Otsu determina el valor automáticamente. 
 

4 . Aplicar transformación morfológica de cierre en la imagen con umbral. El cierre es útil para rellenar pequeñas regiones negras entre regiones blancas en una imagen con umbral. Revela la caja blanca rectangular de matrículas. 
 

5. Para detectar la placa necesitamos encontrar contornos en la imagen. Es importante binarizar y transformar la imagen antes de encontrar los contornos para que pueda encontrar un número menor y más relevante de contornos en la imagen. Si dibuja todos los contornos extraídos en la imagen original, se vería así: 
 

6. Ahora encuentre el rectángulo de área mínima encerrado por cada uno de los contornos y valide sus proporciones de lado y área. Hemos definido el área mínima y máxima de la placa como 4500 y 30000 respectivamente.

7. Ahora encuentre los contornos en la región validada y valide las proporciones de los lados y el área del rectángulo delimitador del contorno más grande en esa región. Después de validar, obtendrá un contorno perfecto de una placa de matrícula. Ahora extrae ese contorno de la imagen original. Obtendrás la imagen de la placa: 
 

  • Código: este paso lo realiza clean_plate y el método ratioCheck de la clase PlateFinder
     

Python3

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

8. Para reconocer con precisión los caracteres de la matrícula, tenemos que aplicar la segmentación de imágenes. Ese primer paso es extraer el canal de valor del formato HSV de la imagen de la placa. parecería.

9. Ahora aplique un umbral adaptativo en la imagen del canal de valor de la placa para binarizarla y revelar los caracteres. La imagen de la placa puede tener diferentes condiciones de iluminación en diferentes áreas, en ese caso, el umbral adaptativo puede ser más adecuado para binarizar porque utiliza diferentes valores de umbral para diferentes regiones en función del brillo de los píxeles en la región que lo rodea.

10. Después de binarizar, aplique la operación no bit a bit en la imagen para encontrar los componentes conectados en la imagen para que podamos extraer los caracteres candidatos.

11. Construya una máscara para mostrar todos los componentes del carácter y luego busque contornos en la máscara. Después de extraer los contornos, tome el más grande, encuentre su rectángulo delimitador y valide las proporciones de los lados.

12. Después de validar las proporciones de los lados, encuentre el casco convexo del anuncio de contorno y dibújelo en la máscara del candidato del personaje. La máscara se vería como-

13. Ahora encuentre todos los contornos en la máscara de candidato de carácter y extraiga esas áreas de contorno de la imagen con umbral de valor de la placa, obtendrá todos los caracteres por separado. 
 

Los pasos 8 a 13 los realiza la función segment_chars que puede encontrar a continuación en el código fuente completo. El código del controlador para las funciones utilizadas en los pasos 6 a 13 está escrito en el método check_plate de la clase PlateFinder
 

Ahora use OCR para reconocer el carácter uno por uno.

Código fuente completo con su funcionamiento: primero, cree una clase PlateFinder que encuentre las placas y valide su relación de tamaño y área. 

Python3

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

Aquí está la explicación de todos y cada uno de los métodos de la clase PlateFinder
En el método de preprocesamiento , se ha realizado el siguiente paso: 
 

  • Desenfocar la imagen
  • Convertir a escala de grises
  • Buscar bordes verticales
  • Umbral de la imagen de borde vertical.
  • Cierre Morph the Threshold image.

El método extract_contours devuelve todos los contornos externos de la imagen preprocesada. 
El método find_possible_plates preprocesa la imagen con el método de preprocesamiento , luego extrae los contornos con el método extract_contours , luego verifica las proporciones laterales y el área de todos los contornos extraídos y limpia la imagen dentro del contorno con los métodos check_plate y clean_plate . Después de limpiar la imagen de contorno con el método clean_plate , encuentra todos los caracteres en la placa con el método find_characters_on_plate
El método find_characters_on_plate usa segment_charsfunción para encontrar los caracteres. Encuentra caracteres calculando el casco convexo de los contornos de una imagen de valor umbral y dibujándolo en los caracteres para revelarlos. 
Código: cree otra clase para inicializar la red neuronal para predecir los caracteres en la matrícula extraída.
 

Python3

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 transform 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)

Carga el modelo OCR preentrenado y su archivo de etiquetas en las funciones load_graph y load_label . El método label_image_list transforma la imagen en tensor con el método convert_tensor y luego predice la etiqueta del tensor con la función label_image_list y devuelve el número de licencia. 
Código: cree una función principal para realizar toda la tarea en una secuencia. 
 

Python3

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()

Puede descargar el código fuente con el modelo OCR y el video de prueba desde mi GitHub
¿Cómo mejorar el modelo? 
 

  • Puede establecer una región pequeña en particular en el marco para encontrar las placas dentro de ella (asegúrese de que todos los vehículos deben pasar por esa región).
  • Puede entrenar su propio modelo de aprendizaje automático para reconocer caracteres porque el modelo dado no reconoce todos los alfabetos.

Referencias: 
Sistema de reconocimiento automático de matrículas (ANPR): una encuesta de Chirag Indravadanbhai Patel.
Técnicas de preprocesamiento de imágenes en la documentación de OpenCV .
 

Publicación traducida automáticamente

Artículo escrito por hritikgupta7080 y traducido por Barcelona Geeks. The original can be accessed here. Licence: CCBY-SA

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *