diff --git a/app/__init__.py b/app/__init__.py index 563960e..dadcf94 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -2,5 +2,9 @@ app = Flask(__name__) +from app import database +from app import utils +from app import models +from app import services from app import views from app import error_handlers diff --git a/app/database.py b/app/database.py new file mode 100644 index 0000000..582ab77 --- /dev/null +++ b/app/database.py @@ -0,0 +1,9 @@ +import os +from app import app +from flask_sqlalchemy import SQLAlchemy + + +db_path = os.path.join(os.path.dirname(__file__), '..', 'database.sqlite') +app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///{}'.format(db_path) +app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False +db = SQLAlchemy(app) diff --git a/app/models.py b/app/models.py new file mode 100644 index 0000000..241a346 --- /dev/null +++ b/app/models.py @@ -0,0 +1,10 @@ +from app import database + + +class LicencePlate(database.db.Model): + plate = database.db.Column(database.db.String(8), primary_key=True) + time = database.db.Column(database.db.BigInteger, primary_key=True) + + def save(self): + database.db.session.add(self) + database.db.session.commit() diff --git a/app/services.py b/app/services.py new file mode 100644 index 0000000..c9c6561 --- /dev/null +++ b/app/services.py @@ -0,0 +1,78 @@ +from app import models +from app import utils +import cv2 +import imutils +import numpy +import pytesseract +import time + + +def contains_licence_plates(licence_plates, date): + # noinspection PyUnresolvedReferences + stored_licence_plates = models.LicencePlate.query.filter( + models.LicencePlate.time >= date + ).all() + resp = [] + for licence_plate in licence_plates: + plate = next((x for x in stored_licence_plates if x.plate == licence_plate), None) + # Add date check + resp.append({ + "plate": licence_plate, + "detected": plate is not None, + "time": utils.convert_timestamp_to_iso(plate.time) if plate is not None else "" + }) + + return resp + + +def parse_image(image): + models.database.db.create_all() + if image is None: + return None + + image = cv2.imdecode(numpy.fromstring(image, numpy.uint8), cv2.IMREAD_UNCHANGED) + image = cv2.resize(image, (640, 480)) # reduce image size + + gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) # remove colors + gray = cv2.bilateralFilter(gray, 13, 15, 15) # blur image to remove noise + + # find image contours + edges = cv2.Canny(gray, 30, 200) # detect image edges + contours = cv2.findContours(edges.copy(), cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) + contours = imutils.grab_contours(contours) + contours = sorted(contours, key=cv2.contourArea, reverse=True)[:10] + + plate = None + for contour in contours: + # approximate the contour + approx = cv2.approxPolyDP(contour, 0.018 * cv2.arcLength(contour, True), True) + # if our approximated contour has four points, then we can assume that it is plate + if len(approx) == 4: + plate = approx + break + + if plate is None: + return None + + # Masking the part other than the number plate + mask = numpy.zeros(gray.shape, numpy.uint8) + cv2.drawContours(mask, [plate], 0, 255, -1, ) + cv2.bitwise_and(image, image, mask=mask) + + # Now crop + (x, y) = numpy.where(mask == 255) + (top_x, top_y) = (numpy.min(x), numpy.min(y)) + (bottom_x, bottom_y) = (numpy.max(x), numpy.max(y)) + cropped = gray[top_x:bottom_x + 1, top_y:bottom_y + 1] + + # Read the number plate + config = utils.get_tesseract_config(pytesseract) + text = pytesseract.image_to_string(cropped, config=config) + + # remove new lines and remove '-' character + return text.partition("\n")[0].replace('-', '') + + +def save_licence_plate(plate): + licence_plate = models.LicencePlate(plate=plate, time=int(time.time())) + licence_plate.save() diff --git a/app/utils.py b/app/utils.py new file mode 100644 index 0000000..c87ac02 --- /dev/null +++ b/app/utils.py @@ -0,0 +1,23 @@ +from dateutil import parser +from datetime import datetime +import time +import os + + +def convert_iso_to_timestamp(date): + return int(time.mktime(parser.isoparse(date).timetuple())) + + +def convert_timestamp_to_iso(timestamp): + return datetime.fromtimestamp(timestamp).strftime('%Y-%m-%dT%H:%M:%S') + + +# noinspection SpellCheckingInspection +def get_tesseract_config(pytesseract): + # for Linux it's usually available trough environment path + if os.name == 'nt': + pytesseract.pytesseract.tesseract_cmd = r'C:\Program Files\Tesseract-OCR\tesseract.exe' + + # we could also use user-pattern like \A\A \d\d\d-\A\A + config = '--psm 11 -c tessedit_char_whitelist=123456789ABCDEFGHIJKLMNOPRSTUVZ-' + return config diff --git a/app/views.py b/app/views.py index 3130a05..68809fa 100644 --- a/app/views.py +++ b/app/views.py @@ -1,5 +1,7 @@ from app import app from flask import jsonify, request +from app import services +from app import utils def error_response(message): @@ -21,24 +23,16 @@ def upload_camera_image(): if image.mimetype not in ["image/png", "image/jpg", "image/jpeg"]: return error_response("Allowed image files are in PNG or JPG format") - # TODO add OCR logic here - return jsonify({"message": "Image was successfully stored"}) - - -# TODO use database instead of hardcoded value -sk_lp = { - "RI1234AB": "2020-12-01T22:45:37" -} + plate = services.parse_image(image.read()) + if plate is None: + return jsonify({"message": "No licence plate found", "status": 404}) + else: + services.save_licence_plate(plate) + return jsonify({"message": f"Licence plate '{plate}' found", "status": 200}) @app.route("/check-licence-plate//", methods=["GET"]) def upload_image(licence_plates, date): - resp = {} - for licence_plate in licence_plates.split(","): - # TODO Fetch data rom database for given date and licence plate - resp[licence_plate] = { - "detected": licence_plate in sk_lp, - "time": sk_lp[licence_plate] if licence_plate in sk_lp else "" - } - - return jsonify(resp) + licence_plates = licence_plates.split(',') + date = utils.convert_iso_to_timestamp(date) + return jsonify(services.contains_licence_plates(licence_plates, date)) diff --git a/database.sqlite b/database.sqlite new file mode 100644 index 0000000..c7e3e21 Binary files /dev/null and b/database.sqlite differ diff --git a/requirements.txt b/requirements.txt index 7deaf3b..3e7ed7b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,6 @@ -Flask==1.1.2 \ No newline at end of file +Flask==1.1.2 +python-dateutil==2.8.1 +opencv-python==4.5.1.48 +imutils==0.5.4 +numpy==1.20.1 +pytesseract==0.3.7 \ No newline at end of file diff --git a/run.py b/run.py index 357912d..5e74554 100644 --- a/run.py +++ b/run.py @@ -1,4 +1,4 @@ from app import app if __name__ == '__main__': - app.run(debug=True) + app.run(debug=True, port=80)