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