diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e6789ad --- /dev/null +++ b/.gitignore @@ -0,0 +1,13 @@ +data/custom.weights +data/classes/custom.names +core/__pycache__/config.cpython-38.pyc +core/__pycache__/utils.cpython-38.pyc +core/__pycache__/yolov4.cpython-38.pyc +detections/detection1.png +core/__pycache__/functions.cpython-38.pyc +core/__pycache__/common.cpython-38.pyc +core/__pycache__/backbone.cpython-38.pyc +checkpoints/yolov4-416/variables/variables.index +checkpoints/yolov4-416/variables/variables.data-00000-of-00001 +checkpoints/yolov4-416/saved_model.pb +checkpoints/yolov4-416.tflite diff --git a/conda-cpu.yml b/conda-cpu.yml index 9cd74b5..004c74b 100644 --- a/conda-cpu.yml +++ b/conda-cpu.yml @@ -9,7 +9,7 @@ dependencies: - opencv-python==4.1.1.26 - lxml - tqdm - - tensorflow==2.3.0rc0 + - tensorflow==2.3.0 - absl-py - easydict - pillow diff --git a/core/config.py b/core/config.py index 2cddea1..8e97ff3 100644 --- a/core/config.py +++ b/core/config.py @@ -11,7 +11,7 @@ # YOLO options __C.YOLO = edict() -__C.YOLO.CLASSES = "./data/classes/coco.names" +__C.YOLO.CLASSES = "./data/classes/custom.names" __C.YOLO.ANCHORS = [12,16, 19,36, 40,28, 36,75, 76,55, 72,146, 142,110, 192,243, 459,401] __C.YOLO.ANCHORS_V3 = [10,13, 16,30, 33,23, 30,61, 62,45, 59,119, 116,90, 156,198, 373,326] __C.YOLO.ANCHORS_TINY = [23,27, 37,58, 81,82, 81,82, 135,169, 344,319] diff --git a/core/functions.py b/core/functions.py index c00683b..b7e0193 100644 --- a/core/functions.py +++ b/core/functions.py @@ -57,7 +57,25 @@ def crop_objects(img, data, path, allowed_classes): cv2.imwrite(img_path, cropped_img) else: continue - + +def custom_crop_and_return_objects(img, data, allowed_classes): + boxes, scores, classes, num_objects = data + class_names = read_class_names(cfg.YOLO.CLASSES) + #create dictionary to hold count of objects for image name + counts = dict() + for i in range(num_objects): + # get count of class for part of image name + class_index = int(classes[i]) + class_name = class_names[class_index] + if class_name in allowed_classes: + counts[class_name] = counts.get(class_name, 0) + 1 + # get box coords + xmin, ymin, xmax, ymax = boxes[i] + # crop detection from image (take an additional 5 pixels around all edges) + return img[int(ymin)-5:int(ymax)+5, int(xmin)-5:int(xmax)+5] + else: + continue + # function to run general Tesseract OCR on any detections def ocr(img, data): boxes, scores, classes, num_objects = data diff --git a/core/utils.py b/core/utils.py index ae2159e..b2c513f 100644 --- a/core/utils.py +++ b/core/utils.py @@ -6,6 +6,12 @@ import pytesseract from core.config import cfg import re +import statistics + +# Deskew +import math +from typing import Tuple, Union +from deskew import determine_skew # If you don't have tesseract executable in your PATH, include the following: # pytesseract.pytesseract.tesseract_cmd = r'' @@ -85,6 +91,218 @@ def recognize_plate(img, coords): #cv2.waitKey(0) return plate_num +def custom_recognize_plate(img, fast_ocr, deskew): + def optical_image_recognition(image, char_whitelist = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ", psm = 13, oem = 1): + config = f"-c tessedit_char_whitelist={char_whitelist} --psm {psm} --oem {oem}" + + extracted_text = pytesseract.image_to_string(image, config=config) + + # clean tesseract text by removing any unwanted blank spaces + clean_text = re.sub('[\W_]+', '', extracted_text) + return clean_text + + def hconcat_resize_min(im_list, interpolation=cv2.INTER_CUBIC): + h_min = min(im.shape[0] for im in im_list) + im_list_resize = [cv2.resize(im, (int(im.shape[1] * h_min / im.shape[0]), h_min), interpolation=interpolation) + for im in im_list] + return cv2.hconcat(im_list_resize) + + def find_average_and_check_contours(sorted_contours, height_org, width_org): + correct_contours = [] + average_height_list = [] + + for cnt in sorted_contours: + x,y,w,h = cv2.boundingRect(cnt) + + # if height of box is not a quarter of total height then skip + if height_org / float(h) > 6: continue + + ratio = h / float(w) + # if height to width ratio is less than 1.4 skip + if ratio < 1.25: continue + + area = h * w + # if width is not more than 25 pixels skip + if width_org / float(w) > 30: continue + # if area is less than 100 pixels skip + if area < 100: continue + + average_height_list.append(h) + correct_contours.append(cnt) + + # remove smallest and largest number for a better average + average_height_list.remove(max(average_height_list)) + average_height_list.remove(min(average_height_list)) + + # calculate letter area average + average_height = statistics.mean(average_height_list) + + return correct_contours, average_height + + def find_check_contours(sorted_contours, height_org, width_org, image): + correct_contours = [] + for idx, cnt in enumerate(sorted_contours): + x,y,w,h = cv2.boundingRect(cnt) + + # if height of box is not a quarter of total height then skip + if height_org / float(h) > 6: continue + + ratio = h / float(w) + # if height to width ratio is less than 1.25 skip + if ratio < 1.25: continue + + area = h * w + # if width is not more than 30 pixels skip + if width_org / float(w) > 30: continue + # if area is less than 100 pixels skip + if area < 100: continue + + value_index = [cnt, idx] + correct_contours.append(value_index) + + return correct_contours + + def calculate_average_height_of_letter(contour_list): + average_height_list = contour_list + + # remove smallest and largest number for a better average + average_height_list.remove(max(average_height_list)) + average_height_list.remove(min(average_height_list)) + + # calculate letter area average + return statistics.mean(average_height_list) + + def most_frequent(list): + counter = 0 + num = list[0] + + for i in list: + curr_frequency = list.count(i) + if(curr_frequency> counter): + counter = curr_frequency + num = i + + return num + + def rotate(image: np.ndarray, angle: float, background: Union[int, Tuple[int, int, int]]) -> np.ndarray: + old_width, old_height = image.shape[:2] + angle_radian = math.radians(angle) + width = abs(np.sin(angle_radian) * old_height) + abs(np.cos(angle_radian) * old_width) + height = abs(np.sin(angle_radian) * old_width) + abs(np.cos(angle_radian) * old_height) + + image_center = tuple(np.array(image.shape[1::-1]) / 2) + rot_mat = cv2.getRotationMatrix2D(image_center, angle, 1.0) + rot_mat[1, 2] += (width - old_width) / 2 + rot_mat[0, 2] += (height - old_height) / 2 + return cv2.warpAffine(image, rot_mat, (int(round(height)), int(round(width))), borderValue=background) + + # Setup parameters + enable_fast = fast_ocr + enable_deskew = deskew + plate_num = "" + offset_size_letter = 0.1 # Maximum offset of the average letter + + #region Filters + # Grayscale image + gray = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY) + + # Deskew image + if enable_deskew: + angle = determine_skew(gray) + rotated = rotate(gray, angle, (0, 0, 0)) + else: + rotated = gray + + # Resize image to three times as large as original for better readability + resize = cv2.resize(rotated, None, fx = 3, fy = 3, interpolation = cv2.INTER_CUBIC) + + # threshold the image using Otsus method to preprocess for tesseract + ret, thresh = cv2.threshold(resize, 0, 255, cv2.THRESH_OTSU | cv2.THRESH_BINARY_INV) + + # create rectangular kernel for dilation + rect_kern = cv2.getStructuringElement(cv2.MORPH_RECT, (3,3)) + + # apply dilation to make regions more clear + dilation = cv2.dilate(thresh, rect_kern, iterations = 1) + #endregion + + #region Contours + # find contours of regions of interest within license plate + contours, hierarchy = cv2.findContours(dilation, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) + #endregion + + # create copy of gray image + copy_orginal = resize.copy() + + # Help to calculate if a contours is valid, this gives the height and width of the orginal image + height, width = copy_orginal.shape + + # Returns the correct contours and the average height of a letter + correct_contours = find_check_contours(contours, height, width, resize.copy()) + + # Get Plate Level + + list_plate_level = [] + for cnt, index in correct_contours: + list_plate_level.append(hierarchy[0,index,3]) + plate_level = most_frequent(list_plate_level) + + print(f"PlateLv {plate_level}") + + correct_contours_no_child = [] + correct_contours_height_list = [] + for cnt, index in correct_contours: + if plate_level == hierarchy[0,index,3]: + x,y,w,h = cv2.boundingRect(cnt) + correct_contours_no_child.append(cnt) + correct_contours_height_list.append(h) + + # Calculate the average height of a letter to increase the accuracy + average_height = calculate_average_height_of_letter(correct_contours_height_list) + + # sort contours left-to-right + sorted_correct_contours = sorted(correct_contours_no_child, key=lambda ctr: cv2.boundingRect(ctr)[0]) + + images = [] + for cnt in sorted_correct_contours: + x,y,w,h = cv2.boundingRect(cnt) + + if h/average_height > (1 + offset_size_letter) or h/average_height < (1 - offset_size_letter): + continue + + # draw the rectangle + cv2.rectangle(copy_orginal, (x,y), (x+w, y+h), (0,255,0),2) + roi = thresh[y-5:y+h+5, x-5:x+w+5] + roi = cv2.bitwise_not(roi) + roi = cv2.medianBlur(roi, 5) + images.append(roi) + + if enable_fast: + # Concatenate all the valid images horizontally to save time + con_h_valid_images = hconcat_resize_min(images) + # Try to read what the license plate is + try: + text = optical_image_recognition(con_h_valid_images, psm=7) + # clean tesseract text by removing any unwanted blank spaces + clean_text = re.sub('[\W_]+', '', text) + plate_num += clean_text + except: + text = None + else: + for image in images: + # Try to read what the license plate is + try: + text = optical_image_recognition(image) + # clean tesseract text by removing any unwanted blank spaces + clean_text = re.sub('[\W_]+', '', text) + plate_num += clean_text + except: + text = None + + if plate_num != None: + print("License Plate #: ", plate_num) + return plate_num + def load_freeze_layer(model='yolov4', tiny=False): if tiny: if model == 'yolov3': @@ -151,7 +369,6 @@ def load_weights(model, weights_file, model_name='yolov4', is_tiny=False): # assert len(wf.read()) == 0, 'failed to read all data' wf.close() - def read_class_names(class_file_name): names = {} with open(class_file_name, 'r') as data: @@ -213,7 +430,18 @@ def format_boxes(bboxes, image_height, image_width): box[0], box[1], box[2], box[3] = xmin, ymin, xmax, ymax return bboxes -def draw_bbox(image, bboxes, info = False, counted_classes = None, show_label=True, allowed_classes=list(read_class_names(cfg.YOLO.CLASSES).values()), read_plate = False): +def extract_and_correct_license_plate(img, area): + license_plate = None + boxes, scores, classes, num_objects = area + for i in range(num_objects): + # separate coordinates from box + xmin, ymin, xmax, ymax = boxes[i] + # get the subimage that makes up the bounded region and take an additional 5 pixels on each side + license_plate = img[int(ymin)-5:int(ymax)+5, int(xmin)-5:int(xmax)+5] + license_plate = cv2.cvtColor(np.array(license_plate), cv2.COLOR_BGR2RGB) + return license_plate + +def draw_bbox(image, bboxes, info = False, counted_classes = None, show_label=True, allowed_classes=list(read_class_names(cfg.YOLO.CLASSES).values()), read_plate = False, custom_reco = False, fast_ocr = False): classes = read_class_names(cfg.YOLO.CLASSES) num_classes = len(classes) image_h, image_w, _ = image.shape @@ -238,7 +466,12 @@ def draw_bbox(image, bboxes, info = False, counted_classes = None, show_label=Tr else: if read_plate: height_ratio = int(image_h / 25) - plate_number = recognize_plate(image, coor) + plate_number = None + if custom_reco: + license_plate = extract_and_correct_license_plate(image, bboxes) + plate_number = custom_recognize_plate(license_plate, fast_ocr) + else: + plate_number = recognize_plate(image, coor) if plate_number != None: cv2.putText(image, plate_number, (int(coor[0]), int(coor[1]-height_ratio)), cv2.FONT_HERSHEY_SIMPLEX, 1.25, (255,255,0), 2) diff --git a/detect.py b/detect.py index b74cffb..d445344 100644 --- a/detect.py +++ b/detect.py @@ -1,13 +1,16 @@ + import os # comment out below line to enable tensorflow outputs os.environ['TF_CPP_MIN_LOG_LEVEL'] = '3' import tensorflow as tf + physical_devices = tf.config.experimental.list_physical_devices('GPU') if len(physical_devices) > 0: tf.config.experimental.set_memory_growth(physical_devices[0], True) from absl import app, flags, logging from absl.flags import FLAGS import core.utils as utils + from core.yolov4 import filter_boxes from core.functions import * from tensorflow.python.saved_model import tag_constants @@ -16,6 +19,7 @@ import numpy as np from tensorflow.compat.v1 import ConfigProto from tensorflow.compat.v1 import InteractiveSession +import time flags.DEFINE_string('framework', 'tf', '(tf, tflite, trt') flags.DEFINE_string('weights', './checkpoints/yolov4-416', @@ -33,8 +37,14 @@ flags.DEFINE_boolean('crop', False, 'crop detections from images') flags.DEFINE_boolean('ocr', False, 'perform generic OCR on detection regions') flags.DEFINE_boolean('plate', False, 'perform license plate recognition') +flags.DEFINE_boolean('custom', False, 'perform custom license plate recognition') +flags.DEFINE_boolean('fast', False, 'perform less accurate but fast ocr') + def main(_argv): + # Setup Timing + start_time = time.time() + config = ConfigProto() config.gpu_options.allow_growth = True session = InteractiveSession(config=config) @@ -48,6 +58,9 @@ def main(_argv): else: saved_model_loaded = tf.saved_model.load(FLAGS.weights, tags=[tag_constants.SERVING]) + # Print the model load time + print("--- Modal Load Time: %s seconds ---" % (time.time() - start_time)) + # loop through images in list and run Yolov4 model on each for count, image_path in enumerate(images, 1): original_image = cv2.imread(image_path) @@ -131,9 +144,9 @@ def main(_argv): # loop through dict and print for key, value in counted_classes.items(): print("Number of {}s: {}".format(key, value)) - image = utils.draw_bbox(original_image, pred_bbox, FLAGS.info, counted_classes, allowed_classes=allowed_classes, read_plate = FLAGS.plate) + image = utils.draw_bbox(original_image, pred_bbox, FLAGS.info, counted_classes, allowed_classes=allowed_classes, read_plate = FLAGS.plate, custom_reco = FLAGS.custom, fast_ocr = FLAGS.fast) else: - image = utils.draw_bbox(original_image, pred_bbox, FLAGS.info, allowed_classes=allowed_classes, read_plate = FLAGS.plate) + image = utils.draw_bbox(original_image, pred_bbox, FLAGS.info, allowed_classes=allowed_classes, read_plate = FLAGS.plate, custom_reco = FLAGS.custom, fast_ocr = FLAGS.fast) image = Image.fromarray(image.astype(np.uint8)) if not FLAGS.dont_show: @@ -141,8 +154,14 @@ def main(_argv): image = cv2.cvtColor(np.array(image), cv2.COLOR_BGR2RGB) cv2.imwrite(FLAGS.output + 'detection' + str(count) + '.png', image) + + # Print the execution time + print("--- Total Execution Time: %s seconds ---" % (time.time() - start_time)) + if __name__ == '__main__': try: + # Run main script app.run(main) + except SystemExit: pass diff --git a/detect_api.py b/detect_api.py new file mode 100644 index 0000000..455e890 --- /dev/null +++ b/detect_api.py @@ -0,0 +1,108 @@ + +# API imports +from flask import Flask, request, jsonify, send_file +from PIL import Image +import numpy as np +import cv2 +import time + +#Detect imports +import os +# comment out below line to enable tensorflow outputs +os.environ['TF_CPP_MIN_LOG_LEVEL'] = '3' +import tensorflow as tf +from tensorflow.compat.v1 import ConfigProto +from tensorflow.compat.v1 import InteractiveSession + +from core.yolov4 import filter_boxes +import core.utils as utils +from core.functions import * + +app = Flask(__name__) + +def find_and_ocr(image): + # Setup Parameters + weights_path = './checkpoints/custom-416.tflite' + input_size = 416 + iou = 0.45 + score = 0.5 + fast_ocr = False + deskew = False + + config = ConfigProto() + config.gpu_options.allow_growth = True + + interpreter = tf.lite.Interpreter(model_path=weights_path) + + + original_image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) + image_data = cv2.resize(original_image, (input_size, input_size)) + image_data = image_data / 255. + images_data = [] + for i in range(1): + images_data.append(image_data) + images_data = np.asarray(images_data).astype(np.float32) + + interpreter.allocate_tensors() + input_details = interpreter.get_input_details() + output_details = interpreter.get_output_details() + interpreter.set_tensor(input_details[0]['index'], images_data) + interpreter.invoke() + pred = [interpreter.get_tensor(output_details[i]['index']) for i in range(len(output_details))] + boxes, pred_conf = filter_boxes(pred[0], pred[1], score_threshold=0.25, input_shape=tf.constant([input_size, input_size])) + + # run non max suppression on detections + boxes, scores, classes, valid_detections = tf.image.combined_non_max_suppression( + boxes=tf.reshape(boxes, (tf.shape(boxes)[0], -1, 1, 4)), + scores=tf.reshape( + pred_conf, (tf.shape(pred_conf)[0], -1, tf.shape(pred_conf)[-1])), + max_output_size_per_class=50, + max_total_size=50, + iou_threshold=iou, + score_threshold=score + ) + + # format bounding boxes from normalized ymin, xmin, ymax, xmax ---> xmin, ymin, xmax, ymax + original_h, original_w, _ = original_image.shape + bboxes = utils.format_boxes(boxes.numpy()[0], original_h, original_w) + + # hold all detection data in one variable + pred_bbox = [bboxes, scores.numpy()[0], classes.numpy()[0], valid_detections.numpy()[0]] + + # read in all class names from config + class_names = utils.read_class_names(cfg.YOLO.CLASSES) + + # by default allow all classes in .names file + allowed_classes = list(class_names.values()) + + # Get license plate image + crop_license_plate = custom_crop_and_return_objects(original_image, pred_bbox, allowed_classes) + + license_plate = utils.custom_recognize_plate(crop_license_plate, fast_ocr, deskew) + + return license_plate + +@app.route("/anpr", methods=["POST"]) +def process_image(): + start_time = time.time() + file = request.files.get('image') + print("file = ", file) + # Read the image via file.stream + img = Image.open(file.stream).convert('RGB') + + image = np.array(img) + + license_plate = find_and_ocr(image) + + elapsed_time = (time.time() - start_time) + + if license_plate is not None: + return jsonify({ + 'license_plate':license_plate, + 'time': elapsed_time + }) + else: + return jsonify({'vechileId':'unable to decode'}) + +if __name__ == "__main__": + app.run(debug=True, host="127.0.0.1", port=3333) \ No newline at end of file diff --git a/detections/detection1.png b/detections/detection1.png index 9609a47..2a23571 100644 Binary files a/detections/detection1.png and b/detections/detection1.png differ diff --git a/license_plate_recognizer.py b/license_plate_recognizer.py index 5f05367..315401c 100644 --- a/license_plate_recognizer.py +++ b/license_plate_recognizer.py @@ -1,65 +1,309 @@ -# test file if you want to quickly try tesseract on a license plate image + import pytesseract import cv2 import os +from os import path import numpy as np +import re +import statistics +import time +import logging +import sys, getopt + +# Deskew +import math +from typing import Tuple, Union +from deskew import determine_skew + +def optical_image_recognition(image, char_whitelist = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ", psm = 13, oem = 1): + config = f"-c tessedit_char_whitelist={char_whitelist} --psm {psm} --oem {oem}" + if logging.DEBUG >= logging.root.level: + print(f"Optical Image Recognition config: {config}") + return pytesseract.image_to_string(image, config=config) + +def hconcat_resize_min(im_list, interpolation=cv2.INTER_CUBIC): + h_min = min(im.shape[0] for im in im_list) + im_list_resize = [cv2.resize(im, (int(im.shape[1] * h_min / im.shape[0]), h_min), interpolation=interpolation) + for im in im_list] + return cv2.hconcat(im_list_resize) + +def find_check_contours(sorted_contours, height_org, width_org, image): + correct_contours = [] + for idx, cnt in enumerate(sorted_contours): + x,y,w,h = cv2.boundingRect(cnt) + if logging.DEBUG >= logging.root.level: + cv2.rectangle(image, (x,y), (x+w, y+h), (255,255,255),2) + cv2.imshow("Highlight Contour", image) + cv2.waitKey(0) + + + # if height of box is not a quarter of total height then skip + if height_org / float(h) > 6: + if logging.DEBUG >= logging.root.level: + print("Height Failed") + continue + + ratio = h / float(w) + # if height to width ratio is less than 1.4 skip + if ratio < 1.25: + if logging.DEBUG >= logging.root.level: + print("Ratio Failed") + continue + + area = h * w + # if width is not more than 25 pixels skip + if width_org / float(w) > 30: + if logging.DEBUG >= logging.root.level: + print("Width Failed") + continue + # if area is less than 100 pixels skip + if area < 100: + if logging.DEBUG >= logging.root.level: + print("Area Failed") + continue + + value_index = [cnt, idx] + correct_contours.append(value_index) + + return correct_contours + +def calculate_average_height_of_letter(contour_list): + average_height_list = contour_list + + # remove smallest and largest number for a better average + average_height_list.remove(max(average_height_list)) + average_height_list.remove(min(average_height_list)) + + # calculate letter area average + return statistics.mean(average_height_list) + +def most_frequent(list): + counter = 0 + num = list[0] + + for i in list: + curr_frequency = list.count(i) + if(curr_frequency> counter): + counter = curr_frequency + num = i + + return num + +def rotate(image: np.ndarray, angle: float, background: Union[int, Tuple[int, int, int]]) -> np.ndarray: + old_width, old_height = image.shape[:2] + angle_radian = math.radians(angle) + width = abs(np.sin(angle_radian) * old_height) + abs(np.cos(angle_radian) * old_width) + height = abs(np.sin(angle_radian) * old_width) + abs(np.cos(angle_radian) * old_height) + + image_center = tuple(np.array(image.shape[1::-1]) / 2) + rot_mat = cv2.getRotationMatrix2D(image_center, angle, 1.0) + rot_mat[1, 2] += (width - old_width) / 2 + rot_mat[0, 2] += (height - old_height) / 2 + return cv2.warpAffine(image, rot_mat, (int(round(height)), int(round(width))), borderValue=background) + + +def main(args): + # Prepare image parameter + image = None + enable_fast = False + + # Get full command-line arguments + full_cmd_arguments = sys.argv + + # Keep all but the first + argument_list = full_cmd_arguments[1:] + + short_options = "hdfi:" + long_options = ["help", "debug", "fast", "input="] + + try: + arguments, values = getopt.getopt(argument_list, short_options, long_options) + except getopt.error as err: + # Output error, and return with an error code + print (str(err)) + sys.exit(2) + + if(len(argument_list) == 0): + print( + "Usage: \n" + " python license_plate_recognizer.py [option]" + "\n \n" + "Options: \n" + " -h, --help \t \t Show help. \n" + " -d, --debug\t \t Enable debugmode.\n" + " -f, --fast\t \t Less accuracy, but faster" + ) + sys.exit(2) + + # Evaluate given options + for current_argument, current_value in arguments: + if current_argument in ("-d", "--debug"): + print ("Enabling debug mode") + logging.basicConfig(format='%(levelname)s:%(message)s', level=logging.DEBUG) + elif current_argument in ("-h", "--help"): + print( + "Usage: \n" + " python license_plate_recognizer.py [option]" + "\n \n" + "Options: \n" + " -h, --help \t \t Show help. \n" + " -d, --debug\t \t Enable debugmode.\n" + " -f, --fast\t \t Less accuracy, but faster" + ) + sys.exit(2) + elif current_argument in ("-i", "--input"): + if(path.exists(current_value)): + image = current_value + else: + print("File doesn't exist!") + sys.exit(2) + elif current_argument in ("-f", "--fast"): + enable_fast = True + + # Setup Parameters + image = cv2.imread(image) + plate_num = "" + offset_size_letter = 0.1 # Maximum offset of the average letter + + #region Filters + # Grayscale image + gray = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY) + if logging.DEBUG >= logging.root.level: + cv2.imshow("Grayscale Image", gray) + cv2.waitKey(0) + + # angle = determine_skew(gray) + # rotated = rotate(gray, angle, (0, 0, 0)) + # if logging.DEBUG >= logging.root.level: + # cv2.imshow("Rotated Image", rotated) + # print(f"Image Angle:{ angle}") + # cv2.waitKey(0) + + # Resize image to three times as large as original for better readability + resize = cv2.resize(gray, None, fx = 3, fy = 3, interpolation = cv2.INTER_CUBIC) + if logging.DEBUG >= logging.root.level: + cv2.imshow("Resize Image", resize) + cv2.waitKey(0) + + # perform gaussian blur to smoothen image + # blur = cv2.GaussianBlur(resize, (5,5), 0) + # if logging.DEBUG >= logging.root.level: + # cv2.imshow("Blur Image", blur) + # cv2.waitKey(0) + + # threshold the image using Otsus method to preprocess for tesseract + ret, thresh = cv2.threshold(resize, 0, 255, cv2.THRESH_OTSU | cv2.THRESH_BINARY_INV) + + # create rectangular kernel for dilation + rect_kern = cv2.getStructuringElement(cv2.MORPH_RECT, (3,3)) -# If you don't have tesseract executable in your PATH, include the following: -# pytesseract.pytesseract.tesseract_cmd = r'' -# Example tesseract_cmd = r'C:\Program Files (x86)\Tesseract-OCR\tesseract' - -# point to license plate image (works well with custom crop function) -gray = cv2.imread("./detections/crop/car3/license_plate_.png", 0) -gray = cv2.resize( gray, None, fx = 3, fy = 3, interpolation = cv2.INTER_CUBIC) -blur = cv2.GaussianBlur(gray, (5,5), 0) -gray = cv2.medianBlur(gray, 3) -# perform otsu thresh (using binary inverse since opencv contours work better with white text) -ret, thresh = cv2.threshold(blur, 0, 255, cv2.THRESH_OTSU | cv2.THRESH_BINARY_INV) -cv2.imshow("Otsu", thresh) -cv2.waitKey(0) -rect_kern = cv2.getStructuringElement(cv2.MORPH_RECT, (3,3)) - -# apply dilation -dilation = cv2.dilate(thresh, rect_kern, iterations = 1) -#cv2.imshow("dilation", dilation) -#cv2.waitKey(0) -# find contours -try: + # apply dilation to make regions more clear + dilation = cv2.dilate(thresh, rect_kern, iterations = 1) + if logging.DEBUG >= logging.root.level: + cv2.imshow("Dilation Image", dilation) + cv2.waitKey(0) + + #endregion + + #region Contours + # find contours of regions of interest within license plate contours, hierarchy = cv2.findContours(dilation, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) -except: - ret_img, contours, hierarchy = cv2.findContours(dilation, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) -sorted_contours = sorted(contours, key=lambda ctr: cv2.boundingRect(ctr)[0]) - -# create copy of image -im2 = gray.copy() - -plate_num = "" -# loop through contours and find letters in license plate -for cnt in sorted_contours: - x,y,w,h = cv2.boundingRect(cnt) - height, width = im2.shape + #endregion + + # create copy of gray image + copy_orginal = resize.copy() + + # Help to calculate if a contours is valid, this gives the height and width of the orginal image + height, width = copy_orginal.shape + + # Returns the correct contours and the average height of a letter + correct_contours = find_check_contours(contours, height, width, resize.copy()) + + # Get Plate Level - # if height of box is not a quarter of total height then skip - if height / float(h) > 6: continue - ratio = h / float(w) - # if height to width ratio is less than 1.5 skip - if ratio < 1.5: continue - area = h * w - # if width is not more than 25 pixels skip - if width / float(w) > 15: continue - # if area is less than 100 pixels skip - if area < 100: continue - # draw the rectangle - rect = cv2.rectangle(im2, (x,y), (x+w, y+h), (0,255,0),2) - roi = thresh[y-5:y+h+5, x-5:x+w+5] - roi = cv2.bitwise_not(roi) - roi = cv2.medianBlur(roi, 5) - #cv2.imshow("ROI", roi) - #cv2.waitKey(0) - text = pytesseract.image_to_string(roi, config='-c tessedit_char_whitelist=0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ --psm 8 --oem 3') - #print(text) - plate_num += text -print(plate_num) -cv2.imshow("Character's Segmented", im2) -cv2.waitKey(0) -cv2.destroyAllWindows() \ No newline at end of file + list_plate_level = [] + for cnt, index in correct_contours: + list_plate_level.append(hierarchy[0,index,3]) + plate_level = most_frequent(list_plate_level) + + print(f"PlateLv {plate_level}") + + correct_contours_no_child = [] + correct_contours_height_list = [] + for cnt, index in correct_contours: + if plate_level == hierarchy[0,index,3]: + x,y,w,h = cv2.boundingRect(cnt) + correct_contours_no_child.append(cnt) + correct_contours_height_list.append(h) + + # Calculate the average height of a letter to increase the accuracy + average_height = calculate_average_height_of_letter(correct_contours_height_list) + + # sort contours left-to-right + sorted_correct_contours = sorted(correct_contours_no_child, key=lambda ctr: cv2.boundingRect(ctr)[0]) + + images = [] + for cnt in sorted_correct_contours: + x,y,w,h = cv2.boundingRect(cnt) + + if h/average_height > (1 + offset_size_letter) or h/average_height < (1 - offset_size_letter): + continue + + # draw the rectangle + cv2.rectangle(copy_orginal, (x,y), (x+w, y+h), (0,255,0),2) + roi = thresh[y-5:y+h+5, x-5:x+w+5] + roi = cv2.bitwise_not(roi) + roi = cv2.medianBlur(roi, 5) + images.append(roi) + + if enable_fast: + # Concatenate all the valid images horizontally to save time + con_h_valid_images = hconcat_resize_min(images) + if logging.DEBUG >= logging.root.level: + cv2.imshow("Concatenated Valid Images", con_h_valid_images) + cv2.waitKey(0) + # Try to read what the license plate is + try: + text = optical_image_recognition(con_h_valid_images) + # clean tesseract text by removing any unwanted blank spaces + clean_text = re.sub('[\W_]+', '', text) + plate_num += clean_text + except: + text = None + else: + for image in images: + # Try to read what the license plate is + if logging.DEBUG >= logging.root.level: + cv2.imshow("Valid Images", image) + cv2.waitKey(0) + try: + text = optical_image_recognition(image) + # clean tesseract text by removing any unwanted blank spaces + clean_text = re.sub('[\W_]+', '', text) + plate_num += clean_text + except: + text = None + + if plate_num != None: + print("License Plate #: ", plate_num) + + if logging.DEBUG >= logging.root.level: + cv2.imshow("All Found Characters", copy_orginal) + cv2.waitKey(0) + + #If there is a window open destroy it or it will wait forever + cv2.destroyAllWindows() + +if __name__ == '__main__': + # Setup Timing + start_time = time.time() + + # Run main script + main(sys.argv[1:]) + + # Print Execution time + print("--- Total Execution Time: %s seconds ---" % (time.time() - start_time)) + + + +