Browse Source

identity card ocr

Tinger 2 years ago
parent
commit
71a0536a81

+ 3 - 1
.gitignore

@@ -1,4 +1,6 @@
 __pycache__
 venv
 .idea
-static/images/*
+static/images/*
+
+test.py

+ 20 - 94
app.py

@@ -1,108 +1,34 @@
-from flask import Flask, render_template, request
-from utils.util import *
-from utils.conf import MAX_CONTENT_LENGTH
+from flask import Flask, render_template, Response
+from blues import com, idc
 import logging
-import numpy as np
-import cv2
-from paddleocr import PaddleOCR
 
-app = Flask(__name__)
-app.config["JSON_AS_ASCII"] = False
 
-logging.getLogger("ppocr").setLevel(logging.WARN)
-engine = PaddleOCR(
-    use_gpu=True,
-    det_model_dir="models/det/",
-    rec_model_dir="models/rec/",
-    cls_model_dir="models/cls/",
-    use_angle_cls=True,
-    use_space_char=True
-)
+def init() -> "Flask":
+    logging.getLogger("ppocr").setLevel(logging.WARN)
+    this = Flask(__name__)
+    this.config["JSON_AS_ASCII"] = False
+    this.config["TEMPLATES_AUTO_RELOAD"] = True
 
+    this.register_blueprint(com)
+    this.register_blueprint(idc)
 
-@app.route("/")
-def index():
-    return render_template("index.html")
-
-
-@app.route("/ocr-raw-api", methods=["POST"])
-def ocr_raw():
-    # 文件处理
-    pic = request.files.get("picture")
-    if pic is None:
-        return Response("empty body")
-    ext = get_ext_name(pic.filename)
-    if not is_image_ext(ext):
-        return Response("文件类型错误")
-    content = pic.read()
-    if len(content) > MAX_CONTENT_LENGTH:
-        return Response("文件过大,请压缩后尝试")
-    path = f"static/images/{current_time()}_{rand_str()}.{ext}"
-    with open(path, "wb") as fp:
-        fp.write(content)
-        fp.close()
-
-    # 内容识别
-    array = cv2.imdecode(np.fromstring(content, np.uint8), 1)  # noqa
-    ocr_res = engine.ocr(array)[0]
-    res = [{"pos": it[0], "word": it[1][0], "rate": it[1][1]} for it in ocr_res]
-    return Response(data=res)
+    return this
 
 
-@app.route("/ocr-filter", methods=["POST"])
-def ocr_filter():
-    # 文件处理
-    pic = request.files.get("picture")
-    if pic is None:
-        return Response("empty body")
-    ext = get_ext_name(pic.filename)
-    if not is_image_ext(ext):
-        return Response("文件类型错误")
-    content = pic.read()
-    if len(content) > MAX_CONTENT_LENGTH:
-        return Response("文件过大,请压缩后尝试")
-    path = f"static/images/{current_time()}_{rand_str()}.{ext}"
-    with open(path, "wb") as fp:
-        fp.write(content)
-        fp.close()
+app = init()
 
-    # 内容识别
-    array = cv2.imdecode(np.fromstring(content, np.uint8), 1)  # noqa
-    ocr_res = engine.ocr(array)[0]
 
-    # 过滤出想要的数据
-    res = [it[1][0] for it in ocr_res]
-    return Response(data=res)
-
-
-@app.route("/ocr-html", methods=["POST"])
-def ocr_html():
-    # 文件处理
-    pic = request.files.get("picture")
-    if pic is None:
-        return Response("empty body")
-    ext = get_ext_name(pic.filename)
-    if not is_image_ext(ext):
-        return Response("文件类型错误")
-    content = pic.read()
-    if len(content) > MAX_CONTENT_LENGTH:
-        return Response("文件过大,请压缩后尝试")
-    cur, rnd = current_time(), rand_str()
-    raw_path = f"static/images/{cur}_{rnd}.{ext}"
-    rec_path = f"static/images/{cur}_{rnd}-rec.{ext}"
-    with open(raw_path, "wb") as fp:
-        fp.write(content)
-        fp.close()
+@app.route("/")
+def index():
+    return render_template("navigate.html")
 
-    # 内容识别
-    array = cv2.imdecode(np.fromstring(content, np.uint8), 1)  # noqa
-    ocr_res = engine.ocr(array)[0]
-    res = [{"pos": it[0], "word": it[1][0], "rate": it[1][1]} for it in ocr_res]
 
-    # 画图
-    rec_img_data = draw_img(array.shape, res)
-    cv2.imwrite(rec_path, rec_img_data)  # noqa
-    return render_template("result.html", raw=raw_path, rec=rec_path, data=res)
+@app.route("/favicon.ico")
+def icon():
+    with open("static/favicon.png", "rb") as f:
+        img = f.read()
+        f.close()
+    return Response(img, mimetype="image/x-png")
 
 
 if __name__ == "__main__":

+ 2 - 0
blues/__init__.py

@@ -0,0 +1,2 @@
+from .com import com
+from .idc import idc

+ 45 - 0
blues/com.py

@@ -0,0 +1,45 @@
+from flask import Blueprint, views, render_template, request
+from utils.util import *
+from utils.conf import MAX_CONTENT_LENGTH
+
+com = Blueprint("com", __name__, url_prefix="/com")
+
+
+class ComView(views.MethodView):
+    @staticmethod
+    def get():
+        return render_template("com_index.html")
+
+    @staticmethod
+    def post():
+        pic = request.files.get("picture")
+        if pic is None:
+            return Response("empty body")
+        ext = get_ext_name(pic.filename)
+        if not is_image_ext(ext):
+            return Response("文件类型错误")
+        content = pic.read()
+        if len(content) > MAX_CONTENT_LENGTH:
+            return Response("文件过大,请重新选择")
+        cur, rnd = current_time(), rand_str()
+        raw_path = f"static/images/{cur}_{rnd}.{ext}"
+        rec_path = f"static/images/{cur}_{rnd}-rec.{ext}"
+        with open(raw_path, "wb") as fp:
+            fp.write(content)
+            fp.close()
+
+        ocr_res, img_shape = recognize(content)
+        kind = request.form.get("type")
+        if kind is not None:
+            kind = kind.lower()
+        if kind == "raw":
+            return Response(data=[{"pos": it[0], "word": it[1][0], "rate": it[1][1]} for it in ocr_res])
+        elif kind == "html":
+            data = [{"pos": it[0], "word": it[1][0], "rate": it[1][1]} for it in ocr_res]
+            draw_img(img_shape, data, rec_path)
+            return render_template("com_result.html", raw=raw_path, rec=rec_path, data=data)
+        else:
+            return Response(data=[it[1][0] for it in ocr_res])
+
+
+com.add_url_rule("/", view_func=ComView.as_view("com"))

+ 126 - 0
blues/idc.py

@@ -0,0 +1,126 @@
+from flask import Blueprint, views, render_template, request
+from utils.util import *
+import re
+from utils.conf import MAX_CONTENT_LENGTH
+
+idc = Blueprint("idc", __name__, url_prefix="/idc")
+
+__name_ptn = r"姓 *名 *(?P<name>.+) *"
+__gender_nation_ptn = r"性 *别 *(?P<gender>男|女) *民 *族 *(?P<nation>.+) *"
+__birth_ymd_ptn = r"出 *生 *(?P<year>\d{4}) *年 *(?P<month>\d{2}) *月 *(?P<day>\d{2}) *日 *"
+__addr_start_ptn = r"住 *址 *(?P<addr>.+) *"
+__idn_ptn = r"公 *民 *身 *份 *号 *码 *(?P<idn>\d{18}) *"
+__agent_ptn = r"签 *发 *机 *关 *(?P<agent>.*) *"
+__valid_date_ptn = r"有 *效 *期 *限 *(?P<from_year>\d{4})\.(?P<from_month>\d{2})\.(?P<from_day>\d{2})" \
+                   r"[^\d]+(?P<to_year>\d{4})\.(?P<to_month>\d{2})\.(?P<to_day>\d{2}) *"
+
+
+def get_face_info(data: "list[str]"):
+    res = {"name": "", "gender": "", "nation": "", "addr": "", "idn": "", "birth": {"year": "", "month": "", "day": ""}}
+    for item in data:
+        if name := re.match(__name_ptn, item):
+            res["name"] = name.group("name")
+        elif gender_nation := re.match(__gender_nation_ptn, item):
+            res["gender"] = gender_nation.group("gender")
+            res["nation"] = gender_nation.group("nation")
+        elif birth_ymd := re.match(__birth_ymd_ptn, item):
+            res["birth"]["year"] = birth_ymd.group("year")
+            res["birth"]["month"] = birth_ymd.group("month")
+            res["birth"]["day"] = birth_ymd.group("day")
+        elif addr := re.match(__addr_start_ptn, item):
+            res["addr"] = addr.group("addr")
+        elif idn := re.match(__idn_ptn, item):
+            res["idn"] = idn.group("idn")
+        else:
+            res["addr"] += item
+    return res
+
+
+def get_icon_info(data: "list[str]"):
+    res = {"agent": "", "from": {"year": "", "month": "", "day": ""}, "to": {"year": "", "month": "", "day": ""}}
+    for item in data:
+        if agent := re.match(__agent_ptn, item):
+            res["agent"] = agent.group("agent")
+        elif valid_date := re.match(__valid_date_ptn, item):
+            res["from"]["year"] = valid_date.group("from_year")
+            res["from"]["month"] = valid_date.group("from_month")
+            res["from"]["day"] = valid_date.group("from_day")
+            res["to"]["year"] = valid_date.group("to_year")
+            res["to"]["month"] = valid_date.group("to_month")
+            res["to"]["day"] = valid_date.group("to_day")
+    return res
+
+
+class IdcView(views.MethodView):
+    @staticmethod
+    def get():
+        return render_template("idc_index.html")
+
+    @staticmethod
+    def post():
+        pic = request.files.get("picture")
+        if pic is None:
+            return Response("empty body")
+        ext = get_ext_name(pic.filename)
+        if not is_image_ext(ext):
+            return Response("文件类型错误")
+        content = pic.read()
+        if len(content) > MAX_CONTENT_LENGTH:
+            return Response("文件过大,请重新选择")
+        raw_path = f"static/images/{current_time()}_{rand_str()}.{ext}"
+        with open(raw_path, "wb") as fp:
+            fp.write(content)
+            fp.close()
+
+        ocr_res, _ = recognize(content)
+        words = [it[1][0] for it in ocr_res]
+
+        which = request.form.get("which")
+        if which is not None:
+            which = which.lower()
+        if which == "face":
+            return Response(data=get_face_info(words))
+        elif which == "icon":
+            return Response(data=get_icon_info(words))
+        else:
+            return Response(f"not recognized arg <which>: '{which}'")
+
+
+class IdcHtmlView(views.MethodView):
+    @staticmethod
+    def post():
+        pic = request.files.get("picture")
+        if pic is None:
+            return Response("empty body")
+        ext = get_ext_name(pic.filename)
+        if not is_image_ext(ext):
+            return Response("文件类型错误")
+        content = pic.read()
+        if len(content) > MAX_CONTENT_LENGTH:
+            return Response("文件过大,请重新选择")
+        cut, rnd = current_time(), rand_str()
+        raw_path = f"static/images/{cut}_{rnd}.{ext}"
+        rec_path = f"static/images/{cut}_{rnd}_rec.{ext}"
+        with open(raw_path, "wb") as fp:
+            fp.write(content)
+            fp.close()
+
+        which = request.form.get("which")
+        if which is not None:
+            which = which.lower()
+        if which not in ["face", "icon"]:
+            return Response(f"not recognized arg <which>: '{which}'")
+
+        ocr_res, img_shape = recognize(content)
+        words = [it[1][0] for it in ocr_res]
+        draw_img(img_shape, [{"pos": it[0], "word": it[1][0], "rate": it[1][1]} for it in ocr_res], rec_path)
+        if which == "face":
+            info = get_face_info(words)
+        else:
+            info = get_icon_info(words)
+        print(info)
+        return render_template("k-v_result.html", raw=raw_path, rec=rec_path, data=info)
+
+
+idc.add_url_rule("/", view_func=IdcView.as_view("idc"))
+idc.add_url_rule("/html/", view_func=IdcHtmlView.as_view("idc-html"))

File diff suppressed because it is too large
+ 9 - 0
static/common.svg


BIN
static/favicon.png


+ 11 - 0
static/identity.svg

@@ -0,0 +1,11 @@
+<?xml version="1.0" standalone="no"?>
+<svg viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" width="128" height="128">
+    <path d="M903.56 847.62H120.44c-30.89 0-55.94-25.04-55.94-55.94V232.31c0-30.89 25.04-55.94 55.94-55.94h783.12c30.89 0 55.94 25.04 55.94 55.94v559.38c0 30.89-25.04 55.93-55.94 55.93z"
+          fill="#8C9EFF"/>
+    <path d="M847.63 421.1H651.84c-11.59 0-20.98-9.39-20.98-20.98s9.39-20.98 20.98-20.98h195.78c11.59 0 20.98 9.39 20.98 20.98s-9.39 20.98-20.97 20.98zM847.63 532.97H651.84c-11.59 0-20.98-9.39-20.98-20.98s9.39-20.98 20.98-20.98h195.78c11.59 0 20.98 9.39 20.98 20.98s-9.39 20.98-20.97 20.98zM763.72 644.85H651.84c-11.59 0-20.98-9.39-20.98-20.98s9.39-20.98 20.98-20.98h111.88c11.59 0 20.98 9.39 20.98 20.98s-9.39 20.98-20.98 20.98z"
+          fill="#E1F5FF"/>
+    <path d="M344.19 410.61m-78.66 0a78.66 78.66 0 1 0 157.32 0 78.66 78.66 0 1 0-157.32 0Z"
+          fill="#FFD600"/>
+    <path d="M487.09 692.05c16.25 0 28.15-12.94 24.12-26.22-21.14-69.7-87.9-120.62-167.02-120.62-79.13 0-145.88 50.92-167.02 120.62-4.03 13.28 7.88 26.22 24.12 26.22h285.8z"
+          fill="#FFD600"/>
+</svg>

File diff suppressed because it is too large
+ 13 - 0
static/more.svg


+ 27 - 0
templates/com_index.html

@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<html lang="zh">
+<head>
+    <meta charset="UTF-8">
+    <title>upload index</title>
+</head>
+<body>
+<h2>原始OCR:</h2>
+<form action="/com/" method="POST" enctype="multipart/form-data">
+    <input type="file" name="picture">
+    <input type="text" name="type" value="raw" hidden>
+    <input type="submit" value="上传并识别">
+</form>
+<h2>过滤结果:</h2>
+<form action="/com/" method="POST" enctype="multipart/form-data">
+    <input type="file" name="picture">
+    <input type="text" name="type" value="filter" hidden>
+    <input type="submit" value="上传并识别">
+</form>
+<h2>在线演示:</h2>
+<form action="/com/" method="POST" enctype="multipart/form-data">
+    <input type="file" name="picture">
+    <input type="text" name="type" value="html" hidden>
+    <input type="submit" value="上传并识别">
+</form>
+</body>
+</html>

+ 1 - 1
templates/result.html

@@ -2,7 +2,7 @@
 <html lang="zh">
 <head>
     <meta charset="UTF-8">
-    <title>ocr result</title>
+    <title>通用OCR结果展示</title>
     <style>
         html, body {
             width: 100%;

+ 33 - 0
templates/idc_index.html

@@ -0,0 +1,33 @@
+<!DOCTYPE html>
+<html lang="zh">
+<head>
+    <meta charset="UTF-8">
+    <title>upload index</title>
+</head>
+<body>
+<h2>API 人像面:</h2>
+<form action="/idc/" method="POST" enctype="multipart/form-data">
+    <input type="file" name="picture">
+    <input type="text" name="which" value="face" hidden>
+    <input type="submit" value="上传并识别">
+</form>
+<h2>API 国徽面:</h2>
+<form action="/idc/" method="POST" enctype="multipart/form-data">
+    <input type="file" name="picture">
+    <input type="text" name="which" value="icon" hidden>
+    <input type="submit" value="上传并识别">
+</form>
+<h2>在线 人像面:</h2>
+<form action="/idc/html/" method="POST" enctype="multipart/form-data">
+    <input type="file" name="picture">
+    <input type="text" name="which" value="face" hidden>
+    <input type="submit" value="上传并识别">
+</form>
+<h2>在线 国徽面:</h2>
+<form action="/idc/html/" method="POST" enctype="multipart/form-data">
+    <input type="file" name="picture">
+    <input type="text" name="which" value="icon" hidden>
+    <input type="submit" value="上传并识别">
+</form>
+</body>
+</html>

+ 0 - 24
templates/index.html

@@ -1,24 +0,0 @@
-<!DOCTYPE html>
-<html lang="zh">
-<head>
-    <meta charset="UTF-8">
-    <title>upload index</title>
-</head>
-<body>
-<h2>原始OCR:</h2>
-<form action="/ocr-raw-api" method="POST" enctype="multipart/form-data">
-    <input type="file" name="picture">
-    <input type="submit" value="上传">
-</form>
-<h2>过滤结果:</h2>
-<form action="/ocr-filter" method="POST" enctype="multipart/form-data">
-    <input type="file" name="picture">
-    <input type="submit" value="上传">
-</form>
-<h2>图片演示:</h2>
-<form action="/ocr-html" method="POST" enctype="multipart/form-data">
-    <input type="file" name="picture">
-    <input type="submit" value="上传">
-</form>
-</body>
-</html>

+ 96 - 0
templates/k-v_result.html

@@ -0,0 +1,96 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <title>键值对OCR结果展示</title>
+    <style>
+        html, body {
+            width: 100%;
+            padding: 0;
+            margin: 0;
+        }
+
+        .img-line {
+            width: 100%;
+            box-sizing: border-box;
+            padding: 0 100px;
+            display: flex;
+            justify-content: space-between;
+            margin-top: 50px;
+        }
+
+        .img-box {
+            margin: 0 20px;
+        }
+
+        img {
+            width: 500px;
+            height: auto;
+            box-sizing: border-box;
+            padding: 5px;
+            border: 1px solid #000;
+        }
+
+        .data-table {
+            width: 100%;
+            justify-content: center;
+            box-sizing: border-box;
+            margin: 50px 0;
+            padding: 0 20px;
+        }
+
+        table {
+            width: 100%;
+            border: none;
+            background-color: aqua;
+        }
+
+        .col-key {
+            width: 20%;
+        }
+
+        .col-value {
+            width: 80%;
+        }
+
+        td, th {
+            background-color: white;
+        }
+
+        .center {
+            text-align: center;
+        }
+    </style>
+</head>
+<body>
+<h1>识别结果展示页面</h1>
+<div class="img-line">
+    <div class="img-box">
+        <h1>原图</h1>
+        <a target="_blank" href="/{{ raw }}"><img src="/{{ raw }}" alt="raw"></a>
+    </div>
+    <div class="img-box">
+        <h1>结果</h1>
+        <a target="_blank" href="/{{ rec }}"><img src="/{{ rec }}" alt="rec"></a>
+    </div>
+</div>
+<div class="data-table">
+    <table>
+        <thead>
+        <tr>
+            <th class="col-key">JSON键</th>
+            <th class="col-value">数据值</th>
+        </tr>
+        </thead>
+        <tbody>
+        {% for key, value in data.items() %}
+            <tr>
+                <td class="center">{{ key }}</td>
+                <td>{{ value }}</td>
+            </tr>
+        {% endfor %}
+        </tbody>
+    </table>
+</div>
+</body>
+</html>

+ 77 - 0
templates/navigate.html

@@ -0,0 +1,77 @@
+<!DOCTYPE html>
+<html lang="zh">
+<head>
+    <meta charset="UTF-8">
+    <title>HuiMv OCR Navigator</title>
+    <style>
+        html, body {
+            width: 100%;
+            background-color: #eeeeee;
+            margin: 0;
+            padding: 0;
+        }
+
+        h1 {
+            padding-left: 50px;
+            margin-bottom: 40px;
+        }
+
+        .demos {
+            width: 100%;
+            display: flex;
+            justify-content: space-evenly;
+
+            box-sizing: border-box;
+            padding: 20px 100px;
+        }
+
+        .demo {
+            width: 220px;
+            height: 260px;
+            border: 1px solid #000000;
+            background-color: white;
+            border-radius: 4px;
+            cursor: pointer;
+            display: flex;
+            flex-direction: column;
+            align-items: center;
+            box-sizing: border-box;
+            padding: 4px;
+        }
+
+        .demo > img {
+            width: 220px;
+            height: 150px;
+            margin-bottom: 10px;
+            border-bottom: 1px solid gray;
+            box-sizing: border-box;
+            padding: 20px;
+        }
+
+
+    </style>
+</head>
+<body>
+<h1>HuiMv OCR Navigator</h1>
+<div class="demos">
+    <div rel="/com" class="demo link">
+        <img src="/static/common.svg" alt="common">
+        <span>通用内容识别模型,可识别任意图片中的所有文字内容,对图片要求不高。</span>
+    </div>
+    <div rel="/idc" class="demo link">
+        <img src="/static/identity.svg" alt="identity">
+        <span>中国大陆身份证内容识别,主要识别:姓名、性别、民族、身份证号、地址、有效日期。</span>
+    </div>
+    <div class="demo">
+        <img src="/static/more.svg" alt="more">
+        <span>更多内容,有待开发...<br>更多内容,有待开发...<br>更多内容,有待开发...</span>
+    </div>
+</div>
+<script>
+    let $links = document.getElementsByClassName("link");
+    for (let $link of $links) $link.onclick = function () {
+        window.location.href = this.getAttribute("rel");
+    }
+</script>
+</body>
+</html>

+ 29 - 12
utils/util.py

@@ -1,15 +1,26 @@
-from time import localtime, strftime
-from random import randint, seed
 import cv2
-from paddleocr.tools.infer.utility import draw_box_txt_fine
 import numpy as np
 from flask import jsonify
+from paddleocr import PaddleOCR
+from random import randint, seed
+from time import localtime, strftime
+from paddleocr.tools.infer.utility import draw_box_txt_fine
 
-__all__ = ["Args", "Response", "rand_str", "current_time", "get_ext_name", "is_image_ext", "draw_img"]
+__all__ = [
+    "Args", "Response", "rand_str", "current_time", "get_ext_name", "is_image_ext", "recognize", "draw_img"
+]
 
-StrBase = "qwertyuioplkjhgfdsazxcvbnm1234567890ZXCVBNMLKJHGFDSAQWERTYUIOP"
-StrBaseLen = len(StrBase) - 1
-AcceptExtNames = ["jpg", "jpeg", "bmp", "png", "rgb", "tif", "tiff", "gif", "pdf"]
+__StrBase = "qwertyuioplkjhgfdsazxcvbnm1234567890ZXCVBNMLKJHGFDSAQWERTYUIOP"
+__StrBaseLen = len(__StrBase) - 1
+__AcceptExtNames = ["jpg", "jpeg", "bmp", "png", "rgb", "tif", "tiff", "gif", "pdf"]
+__OcrEngine = PaddleOCR(
+    use_gpu=True,
+    det_model_dir="models/det/",
+    rec_model_dir="models/rec/",
+    cls_model_dir="models/cls/",
+    use_angle_cls=True,
+    use_space_char=True
+)
 
 
 class Args:
@@ -25,7 +36,7 @@ class Args:
             det_pse_scale=1, scales=[8, 16, 32], alpha=1.0, beta=1.0, fourier_degree=5,
             rec_algorithm="SVTR_LCNet", rec_model_dir="models/rec/", rec_image_inverse=True,
             rec_image_shape="3, 48, 320", rec_batch_num=6, max_text_length=25,
-            rec_char_dict_path="E:/Project/Python/PaddleOCR/venv/lib/site-packages/paddleocr/ppocr/utils/ppocr_keys_v1.txt",
+            rec_char_dict_path="venv/lib/site-packages/paddleocr/ppocr/utils/ppocr_keys_v1.txt",
             use_space_char=True, vis_font_path="static/simfang.ttf", drop_score=0.5,
             e2e_algorithm="PGNet", e2e_model_dir=None, e2e_limit_side_len=768, e2e_limit_type="max",
             e2e_pgnet_score_thresh=0.5, e2e_char_dict_path="./ppocr/utils/ic15_dict.txt",
@@ -60,7 +71,7 @@ class Args:
 
 
 def rand_str(size: "int" = 8) -> "str":
-    return "".join([StrBase[randint(0, StrBaseLen)] for _ in range(size)])
+    return "".join([__StrBase[randint(0, __StrBaseLen)] for _ in range(size)])
 
 
 def current_time() -> "str":
@@ -72,7 +83,7 @@ def get_ext_name(name: "str") -> "str":
 
 
 def is_image_ext(ext: "str") -> bool:
-    return ext in AcceptExtNames
+    return ext in __AcceptExtNames
 
 
 def Response(message: "str" = None, data=None):
@@ -81,7 +92,13 @@ def Response(message: "str" = None, data=None):
     return jsonify(success=False, message=message, data=data)
 
 
-def draw_img(shape: "tuple", data: "list[dict]", drop: "float" = 0.5):
+def recognize(content: "str") -> "tuple[list, tuple]":
+    img = cv2.imdecode(np.fromstring(content, np.uint8), 1)  # noqa
+
+    return __OcrEngine.ocr(img)[0], img.shape
+
+
+def draw_img(shape: "tuple", data: "list[dict]", path: "str", drop: "float" = 0.5):
     img = np.ones(shape, dtype=np.uint8) * 255
     seed(0)
 
@@ -94,4 +111,4 @@ def draw_img(shape: "tuple", data: "list[dict]", drop: "float" = 0.5):
         cv2.polylines(text, [pts], True, color, 1)  # noqa
         img = cv2.bitwise_and(img, text)  # noqa
 
-    return np.array(img)
+    cv2.imwrite(path, np.array(img))  # noqa