Development of the ocr part of AOI
Samo Penic
2018-11-17 6fde5fba87aaf2f2ad8c4af08a59454f06d8dc30
Improving the robustness of all three algorithms. Again...
2 files modified
234 ■■■■ changed files
aoiOcr.py 8 ●●●● patch | view | raw | blame | history
sid_process.py 226 ●●●● patch | view | raw | blame | history
aoiOcr.py
@@ -6,8 +6,8 @@
settings = {"sid_mask": "64xx0xxx", "answer_treshold": 0.25}
classifier = joblib.load("filename.joblib")
# p = Paper(filename="testpage300dpi_scan1.png")
#p=Paper(filename='sizif111.tif', sid_classifier=classifier, settings={"sid_mask": "11xx0xxx", "answer_treshold": 0.25})
#p = Paper(filename="testpage300dpi_scan1.png")
p=Paper(filename='sizif111.tif', sid_classifier=classifier, settings={"sid_mask": "11xx0xxx", "answer_treshold": 0.25})
#p=Paper(filename='processed_scans/20141016095134535_0006.tif', sid_classifier=classifier, settings=settings)
#p = Paper(filename="processed_scans/20151111080408825_0001.tif",sid_classifier=classifier,settings=settings,)
#p=Paper(filename='processed_scans/20151028145444607_0028.tif', sid_classifier=classifier, settings=settings)
@@ -20,7 +20,7 @@
    "processed_scans/20141021095744144_0009.tif",
    "processed_scans/20141028095553745_0018.tif",
]
p=Paper(filename=pa[6], sid_classifier=classifier, settings=settings)
#p=Paper(filename=pa[6], sid_classifier=classifier, settings=settings)
# print(p.QRData)
# print(p.errors)
@@ -33,7 +33,7 @@
print(p.get_paper_ocr_data())
exit(0)
filelist = glob("processed_scans/*.tif")
for f in sorted(filelist):
    print("processing: {}".format(f))
sid_process.py
@@ -3,70 +3,68 @@
from skimage import morphology, img_as_ubyte
"""
  (1) The text is an array of chars (in row-major order) where
 *          each char can be one of the following:
 *             'x': hit
 *             'o': miss
 *             ' ': don't-care
 *      (2) When the origin falls on a hit or miss, use an upper case
 *          char (e.g., 'X' or 'O') to indicate it.  When the origin
 *          falls on a don't-care, indicate this with a 'C'.
 *          The string must have exactly one origin specified.
 *      (3) The advantage of this method is that the text can be input
 *          in a format that shows the 2D layout of the Sel; e.g.,
    :::: AND ::::
   (10) The sequence string is formatted as follows:
 *            ~ An arbitrary number of operations,  each separated
 *              by a '+' character.  White space is ignored.
 *            ~ Each operation begins with a case-independent character
 *              specifying the operation:
 *                 d or D  (dilation)
 *                 e or E  (erosion)
 *                 o or O  (opening)
 *                 c or C  (closing)
 *                 r or R  (rank binary reduction)
 *                 x or X  (replicative binary expansion)
 *                 b or B  (add a border of 0 pixels of this size)
 *            ~ The args to the morphological operations are bricks of hits,
 *              and are formatted as a.b, where a and b are horizontal and
 *              vertical dimensions, rsp.
 *            ~ The args to the reduction are a sequence of up to 4 integers,
 *              each from 1 to 4.
 *            ~ The arg to the expansion is a power of two, in the set
 *              {2, 4, 8, 16}.
 *      (11) An example valid sequence is:
 *               "b32 + o1.3 + C3.1 + r23 + e2.2 + D3.2 + X4"
 *           In this example, the following operation sequence is carried out:
 *             * b32: Add a 32 pixel border around the input image
 *             * o1.3: Opening with vert sel of length 3 (e.g., 1 x 3)
 *             * C3.1: Closing with horiz sel of length 3  (e.g., 3 x 1)
 *             * r23: Two successive 2x2 reductions with rank 2 in the first
 *                    and rank 3 in the second.  The result is a 4x reduced pix.
 *             * e2.2: Erosion with a 2x2 sel (origin will be at x,y: 0,0)
 *             * d3.2: Dilation with a 3x2 sel (origin will be at x,y: 1,0)
 *             * X4: 4x replicative expansion, back to original resolution
"""
def kernel(x, y):
    """
    Function greates square kernel of size x and y
    """
    return np.ones((x, y), np.uint8)
def find_biggest_blob(image, original_image,sid_mask):
    if sid_mask[0] == "1":
        move_left = 45
    elif sid_mask[0] == "x":
        move_left = 55
    else:
        move_left = 0
       # Remove noise
    image2 = cv2.morphologyEx(
        original_image, cv2.MORPH_OPEN, kernel(2, 2), iterations=7
    )
    # find biggest block of pixels
    image1 = cv2.morphologyEx(image2, cv2.MORPH_DILATE, kernel(5, 25), iterations=4)
    image1 = img_as_ubyte(image1 > 50)
    cv2.imwrite("sidblock1.png", image1)
    im2, ctrs, hier = cv2.findContours(
        image1.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE
    )
    sorted_ctrs = sorted(
        ctrs, key=lambda ctr: cv2.contourArea(ctr)
    )  # get bigges contour
    x, y, w, h = cv2.boundingRect(sorted_ctrs[-1])
    image = image[y : y + h, x + 25 - move_left : x + w - 40]  # +25,-25
    return image
def sid_compare(sid_no, sid_mask):
    for s,es in zip(sid_mask,sid_no):
        if s!='x' and s!=es:
    """
    Function compares student id number with student id mask if the recognised number is valid according to the mask
    :param sid_no:
    :param sid_mask:
    :return: True if they match, else False
    """
    for s, es in zip(sid_mask, sid_no):
        if s != "x" and s != es:
            return False
    return True
def segment_by_contours(image, original_image, classifier,sid_mask):
    """
    First algorithm. it segments numerals with contours. It works with numbers where individual numerals does not touch.
    :param image:
    :param original_image:
    :param classifier:
    :return: student id as a string
    """
def segment_by_contours(image, sorted_ctrs, classifier):
    sid_no = ""
    image=find_biggest_blob(image,original_image,sid_mask)
    cv2.imwrite("sid_contour1.png",image)
    im2, ctrs, hier = cv2.findContours(
        image.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE
    )
    sorted_ctrs = sorted(ctrs, key=lambda ctr: cv2.boundingRect(ctr)[0])
    for i, ctr in enumerate(sorted_ctrs):
        # Get bounding box
        x, y, w, h = cv2.boundingRect(ctr)
@@ -85,28 +83,19 @@
def segment_by_sid_len(image, original_image, sid_mask, classifier):
    """
    Third algorithm. It trys to get biggest "blob" in the image and then it cuts it into individual numbers by force.
    It has some problems with finding individual numbers, so some tweaking must be done!
    :param image:
    :param original_image:
    :param sid_mask:
    :param classifier:
    :return: student id as a string
    """
    sid_no = ""
    sid_len = len(sid_mask)
    if sid_mask[0] == "1":
        move_left = 45
    elif sid_mask[0] == "x":
        move_left = 55
    else:
        move_left = 0
        # Remove noise
    image2 = cv2.morphologyEx(original_image, cv2.MORPH_OPEN, kernel(2, 2), iterations=7)
    # find biggest block of pixels
    image1 = cv2.morphologyEx(image2, cv2.MORPH_DILATE, kernel(5, 25), iterations=4)
    image1=img_as_ubyte(image1>50)
    cv2.imwrite("sidblock1.png", image1)
    im2, ctrs, hier = cv2.findContours(
        image1.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE
    )
    sorted_ctrs = sorted(
        ctrs, key=lambda ctr: cv2.contourArea(ctr)
    )  # get bigges contour
    x, y, w, h = cv2.boundingRect(sorted_ctrs[-1])
    image = image[y : y + h, x + 25 - move_left : x + w - 40] #+25,-25
    image=find_biggest_blob(image,original_image,sid_mask)
    cv2.imwrite("sidblock2.png", image)
    imgHeight, imgWidth = image.shape[0:2]
    numWidth = int(imgWidth / (sid_len))
@@ -114,35 +103,44 @@
        num = image[:, i * numWidth : (i + 1) * numWidth]
        num = img_as_ubyte(num < 128)
        num = cv2.resize(num, (32, 32))
        # cv2.rectangle(image,(x,y),( x + w, y + h ),(0,255,0),2)
        cv2.imwrite("sid_no_{}.png".format(i), num)
        sid_no = sid_no + str(classifier.predict(num.reshape(1, -1) / 255.0)[0])
    return sid_no
def segment_by_7segments(image,original_image,sid_mask,classifier):
    block_image = cv2.morphologyEx(original_image, cv2.MORPH_CLOSE, kernel(2, 2), iterations=10)
    block_image =img_as_ubyte(block_image<50)
def segment_by_7segments(image, original_image, sid_mask, classifier):
    """
    Second attempt. It dilates the image to get all 7 segments wisible as 8888888 then it does pattern matching of 8 with
    pattern image. It works if the scaned gray level is high enough.
    :param image:
    :param original_image:
    :param sid_mask:
    :param classifier:
    :return: student id number as a string
    """
    block_image = cv2.morphologyEx(
        original_image, cv2.MORPH_CLOSE, kernel(2, 2), iterations=10
    )
    block_image = img_as_ubyte(block_image < 50)
    cv2.imwrite("sid_3rd1.png", block_image)
    template = cv2.imread("template-8.png", 0)
    w, h = template.shape[::-1]
    res = cv2.matchTemplate(block_image, template, cv2.TM_CCOEFF_NORMED)
    loc = np.where(res >= 0.75)
    cimg = cv2.cvtColor(image, cv2.COLOR_GRAY2BGR)
    loc_filtered_x=[]
    loc_filtered_y=[]
    loc_filtered_x = []
    loc_filtered_y = []
    for pt in zip(*loc[::-1]):
        pt=(pt[0]-10,pt[1]-10)
        pt = (pt[0] - 10, pt[1] - 10)
        loc_filtered_y.append(pt[1])
        loc_filtered_x.append(pt[0])
#        points.append(pt)
    #filter points
    if(len(loc_filtered_x)==0):
    #        points.append(pt)
    # filter points
    if len(loc_filtered_x) == 0:
        return ""
    loc_filtered_x, loc_filtered_y = zip(
                *sorted(zip(loc_filtered_x, loc_filtered_y))
            )
    a = np.diff(loc_filtered_x) > int(w/2)
    loc_filtered_x, loc_filtered_y = zip(*sorted(zip(loc_filtered_x, loc_filtered_y)))
    a = np.diff(loc_filtered_x) > int(w / 2)
    a = np.append(a, True)
    loc_filtered_x = np.array(loc_filtered_x)
    loc_filtered_y = np.array(loc_filtered_y)
@@ -151,10 +149,10 @@
        cv2.rectangle(cimg, pt, (pt[0] + w, pt[1] + h), (0, 255, 255), 2)
    cv2.imwrite("sid_3rd2.png", cimg)
    sid_no=""
    for i,pt in enumerate(zip(*points[::-1])):
        num=image[pt[1]:pt[1] + h, pt[0]:pt[0]+w]
        #cv2.imwrite("sid_3no_{}.png".format(i), num)
    sid_no = ""
    for i, pt in enumerate(zip(*points[::-1])):
        num = image[pt[1] : pt[1] + h, pt[0] : pt[0] + w]
        # cv2.imwrite("sid_3no_{}.png".format(i), num)
        num = img_as_ubyte(num < 128)
        try:
            num = cv2.resize(num, (32, 32))
@@ -165,19 +163,34 @@
    return sid_no
def getSID(image, classifier, sid_mask):
    """
    Tries different approaches on image to get student id number. Firstly clears image of noise and then skeletonizes
    numbers and thickens it until it gets normalized image. It sends it to the segmentation and recognition functions.
    Tweak both MORPH_OPEN lines....
    :param image:
    :param classifier:
    :param sid_mask:
    :return: (student_id, error, warning) student id as a string, list of errors and list of warnings during the recognition
    """
    sid_warn = []
    sid_err=[]
    sid_err = []
    image = 255 - image
    image_original=image.copy()
    image_original = image.copy()
    image = img_as_ubyte(image > 100)
    cv2.imwrite("enSID0.png", image)
    # Remove noise
    image = cv2.morphologyEx(image, cv2.MORPH_OPEN, kernel(2, 2), iterations=3)
    # Closing. Connect non connected parts
    image = cv2.morphologyEx(image, cv2.MORPH_CLOSE, kernel(5, 3), iterations=4)
    # Again noise removal after closing
    # Again noise removal after closing
    # image = cv2.morphologyEx(image, cv2.MORPH_OPEN, kernel(8, 8), iterations=1)
    # don't do too much noise removal.
    image = cv2.morphologyEx(image, cv2.MORPH_OPEN, kernel(3, 3), iterations=1)
@@ -185,35 +198,28 @@
    # Skeletonization
    image = img_as_ubyte(morphology.thin(image > 128))
    cv2.imwrite("enSID1.png", image)
    # Stub removal (might not be necessary if thinning instead of skeletonize is used above
    # Making lines stronger
    image = cv2.morphologyEx(image, cv2.MORPH_DILATE, kernel(5, 5), iterations=1)
    image = cv2.morphologyEx(image, cv2.MORPH_CLOSE, kernel(10, 10))
    # Thining again
    image = img_as_ubyte(morphology.skeletonize(image > 0.5))
    image = cv2.morphologyEx(image, cv2.MORPH_DILATE, kernel(10, 10))
    cv2.imwrite("enhancedSID.png", image)
    im2, ctrs, hier = cv2.findContours(
        image.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE
    )
    sorted_ctrs = sorted(ctrs, key=lambda ctr: cv2.boundingRect(ctr)[0])
    print(len(sid_mask), len(sorted_ctrs))
    sid_no = segment_by_contours(
        image, sorted_ctrs[1:], classifier
    )  # we remove largest contour that surrounds whole image
    sid_no = segment_by_contours(image, image_original, classifier, sid_mask)
    print(sid_no)
    if len(sid_no) != len(sid_mask) or not sid_compare(sid_no,sid_mask):
    if len(sid_no) != len(sid_mask) or not sid_compare(sid_no, sid_mask):
        sid_warn.append("Trying second SID algorithm.")
        sid_no = segment_by_7segments(image, image_original, sid_mask, classifier)
    print(sid_no)
    if(len(sid_no))!=len(sid_mask):
    if (len(sid_no)) != len(sid_mask):
        sid_no = segment_by_sid_len(image, image_original, sid_mask, classifier)
        sid_warn.append("Trying third SID algorithm.")
    if not sid_compare(sid_no, sid_mask):
            sid_err=['Wrong SID!']
        sid_err = ["Wrong SID!"]
    return (sid_no, sid_err, sid_warn)
    return sid_no, sid_err, sid_warn