. Cleanbg.pyscipy.ndimage import measurementsnumpy as npcv2_WIDTH = 10_HEIGHT = 10clean_bg_v1(img, hist_max_ix, deviation, coeff):
""" cleans image bg by washing colors[hist_max_ix +/- coeff*deviation] """, cols, _ = img.shape= [coeff * deviation[0], coeff * deviation[1], coeff * deviation[2]]row in range(0, rows):col in range(0, cols):channel in [0, 1, 2]:img[row][col][channel] > hist_max_ix[channel] - thres[channel]:[row][col][channel] = 255
# bitwise remaining colorsimg[row][col][0] != 255 or img[row][col][1] != 255 or img[row][col][2] != 255:[row][col] = [0, 0, 0]clean_bg_v2(img, coeff):
"""
# the algorithm breaks colors on the box on three areas:
# - upper area, which is to be cleaned up, this must be bg
# - middle area, area of interest which colors constructs characters
# - lowers area, area with colors below characters colors, nothing must be here in the begging
# upper and lower areas are to be cleaned up and middle area is to be stretched from 0 up to 225
# upper cutting threshold is fixed for the whole picture
# lower threeshold is floating from pixel to pixel
#
# the idea is that character pixels with overlights from watermarks should be scaled down to
# more lower value than usual pixels
"""= cv2.cvtColor(img, cv2.COLOR_BGR2GRAY), cols = img.shape
# statistics for overall box= np.average(img)= np.std(img)= np.median(img)
# print 'average: ', avr
# print 'median: ', median
# print 'std: ', std
# statistics for character colors
# colors below 110 are considered related to the characters
# TODO: to try calculate 110 dynamically= []row in range(0, rows):col in range(0, cols):img[row][col] < 110:.append(img[row][col])
# blacks_avr = np.average(blacks)_median = np.median(blacks)_std = np.std(blacks)
# print 'black average: ', blacks_avr
# print 'black median: ', blacks_median
# print 'black std: ', blacks_std
# statistics for character colors
# colors higher 110 are considered related to the bg
# TODO: to try calculate 110 dynamically= []row in range(0, rows):col in range(0, cols):img[row][col] > 110:.append(img[row][col])
# bg_avr = np.average(bg)
# bg_median = np.median(bg)
# bg_std = np.std(bg)
# print 'bg average: ', bg_avr
# print 'bg median: ', bg_median
# print 'bg std: ', bg_std
# comparing meadian values for the whole box and for the characters only
# we can try to assume if the box contains watermarks.
# due to high watermaks colors, diff between above values are less on
# the pictures with watermaks compared to the ones without them.
# empirically detected values:
# - with watermaks: bg =~ 200, charaters =~ 100
# - without watermaks: bg =~ 220, charaters =~ 90
# TODO: to be verified on other samples
# watermark detection is needed to set propper upper threshold.
# the recognition result is very sensitive for the upper threshold.
# magic numbers 1.1 and 0.7 are also empirically settled and may not work on othen pictures.
# TODO: is it possible to figure out some common algorith which would set propper upper threshold?= median - blacks_mediandelta > (100 + 130) / 2:_thresh = avr - 1.0 * coeff * std:_thresh = median - 0.7 * coeff * std
# print 'delta: ', delta
# print 'coeff: ', coeff
# print 'up_thresh: ', up_thresh
# print img.shape= cv2.copyMakeBorder(img, BORDER_HEIGHT, BORDER_HEIGHT, BORDER_WIDTH, BORDER_WIDTH, cv2.BORDER_REPLICATE), cols = img.shape= np.zeros(img.shape, np.float32)_rows = 10_cols = 10
# the lower threshold is calculated dynamically for each pixel
# as min value for some locality of that pixelrow in range(box_rows / 2, rows - box_rows / 2):col in range(box_cols / 2, cols - box_cols / 2):[row][col] = np.min(img[row - box_rows / 2:row + box_rows / 2, col - box_cols / 2:col + box_cols / 2])
# scale up colors from 0...up_thresh to 0...255row in range(box_rows / 2, rows - box_rows / 2):col in range(box_cols / 2, cols - box_cols / 2):img[row][col] > up_thresh:[row][col] = 255:[row][col] = img[row][col] * 255 / up_thresh
# scale down colors from mask[row][col]...255 to 0...255row in range(box_rows / 2, rows - box_rows / 2):col in range(box_cols / 2, cols - box_cols / 2):img[row][col] < mask[row][col]:[row][col] = 0mask[row][col] != 255:[row][col] = 255 - (255 - img[row][col]) * 255 / (255 - mask[row][col])
# cut box borders= img[BORDER_HEIGHT:rows-BORDER_HEIGHT, BORDER_WIDTH:cols-BORDER_WIDTH], cols = img.shape
# this magic stuff is needed to get rid of the noise happened to appear in the box.
# proper setting of upper threshold could help to clean that noise also, but
# but there is no common algorithm to calculate upper threshold for each box personally.
# condition to do cleaning: no watermarks and characters are fat enoughdelta > (100 + 130) / 2 and blacks_std > 6:
# print 'do noise clean'= np.ones((3, 3), np.float32) / 9= cv2.filter2D(img, -1, kernel)row in range(0, rows):col in range(0, cols):dst[row][col] > 255 * (9 - 3) / 9:[row][col] = 255= cv2.cvtColor(img, cv2.COLOR_GRAY2RGB)imgget_char_dot_height(img):
""" calculates apprx charater heightdot height on the image """= np.float32(img.copy())= cv2.cornerHarris(fimg, 20, 31, 0.04)= cv2.dilate(dst, None)= img.copy()[dst < 0.01*dst.max()] = 0, hierarchy = cv2.findContours(fimg.copy(), cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)= sorted(contours, key=cv2.contourArea, reverse=True)= 0= 0c in cnts:= cv2.arcLength(c, True)peri > 100:, y, w, h = cv2.boundingRect(c)+= h+= 1hcount != 0:_height = hsum/hcount/2:_height = 20_height = char_height/3 - 2
# print char_height
# print dot_heightchar_height, dot_heightclean_bg_v3(img, coeff, clean_noise):
"""
# the algorithm breaks colors on the box on three areas:
# - upper area, which is to be cleaned up, this must be bg
# - middle area, area of interest which colors constructs characters
# - lowers area, area with colors below characters colors, nothing must be here in the begging
# upper and lower areas are to be cleaned up and middle area is to be stretched from 0 up to 225
# upper cutting threshold is fixed for the whole picture
# lower threeshold is floating from pixel to pixel
#
# the idea is that character pixels with overlights from watermarks should be scaled down to
# more lower value than usual pixels
#
# diff against v2:
# - adaptive upper threshold
# - noise cleaning
"""= cv2.cvtColor(img, cv2.COLOR_BGR2GRAY), cols = img.shape
# calculate an apprx charater height and dot height on the orig image
# used later in noise filteringclean_noise:_height, dot_height = get_char_dot_height(img)
# calculate upper threshold by picking one row and analazing its colors:
# - filter image saving characters border
# - pick the most black line
# - colors on the line should be easily clasterized into bg color and char color
# - take upper threshold as charaters color minus one third from distance between
# bg color and char colors (one third is an empirical coeff)
# apply bilateral filter, TODO: kernel size to be calculated in runtime= np.std(img.flatten())= cv2.bilateralFilter(img, 51, sigmacolor, 21)
# pick the most black line_per_row = []rr in range(0, rows):
_sum_per_row = 0cc in range(0, cols):
_sum_per_row += (255 - bltf[rr][cc])_per_row.append(_sum_per_row)= sum_per_row.index(max(sum_per_row))
# print 'row ix: ', rix
# extract the line
# almost sure numpy can do this as well= []cc in range(0, cols):.append(bltf[rix][cc])
# clusterize color on that line= np.float32(rowx)= (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 100, 0.1), labels, centers = cv2.kmeans(rowx, 2, criteria, 100, cv2.KMEANS_RANDOM_CENTERS)
# print ret, labels, centers
# determine if watermarks are present as it is done in v2 algo= np.median(img)= []row in range(0, rows):col in range(0, cols):img[row][col] < 110:.append(img[row][col])_median = np.median(blacks)= median - blacks_median= (delta > (100 + 130) / 2)whatermarks:_thresh = max(centers) - (max(centers)-min(centers))/2.0*coeff:_thresh = max(centers) - (max(centers)-min(centers))/4.0*coeffcoeff, up_thresh
# expand the box to be able to do 'convolution' properly= cv2.copyMakeBorder(img, BORDER_HEIGHT, BORDER_HEIGHT, BORDER_WIDTH, BORDER_WIDTH, cv2.BORDER_REPLICATE), cols = img.shape
# mask for the lower threshold= np.zeros(img.shape, np.float32)_rows = 10_cols = 10
# the lower threshold is calculated dynamically for each pixel
# as min value for some locality of that pixelrow in range(box_rows / 2, rows - box_rows / 2):col in range(box_cols / 2, cols - box_cols / 2):[row][col] = np.min(img[row - box_rows / 2:row + box_rows / 2, col - box_cols / 2:col + box_cols / 2])
# scale up colors from 0...up_thresh to 0...255row in range(box_rows / 2, rows - box_rows / 2):col in range(box_cols / 2, cols - box_cols / 2):img[row][col] > up_thresh:[row][col] = 255:[row][col] = img[row][col] * 255 / up_thresh
# scale down colors from mask[row][col]...255 to 0...255row in range(box_rows / 2, rows - box_rows / 2):col in range(box_cols / 2, cols - box_cols / 2):img[row][col] < mask[row][col]:[row][col] = 0mask[row][col] != 255:[row][col] = 255 - (255 - img[row][col]) * 255 / (255 - mask[row][col])clean_noise:
# fill the borders with the white, need later on.rectangle(img, (0, 0), (cols, BORDER_HEIGHT/2), (255, 255, 255), -1).rectangle(img, (0, rows-BORDER_HEIGHT/2), (cols, rows), (255, 255, 255), -1).rectangle(img, (0, 0), (BORDER_WIDTH/2, rows), (255, 255, 255), -1).rectangle(img, (cols-BORDER_WIDTH/2, 0), (cols, rows), (255, 255, 255), -1)
# binarize into tmp image= img.copy()row in range(0, rows):col in range(0, cols):tmp[row][col] < 255:[row][col] = 0
# find contours on binarized image
# one of returned contours is the box itself, how to get rid of it?, hierarchy = cv2.findContours(tmp.copy(), cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)= float(rows)/cols if rows < cols else float(cols)/rowscnt in contours[1:]:, y, w, h = cv2.boundingRect(cnt)
# loop over the contours and filter them by the condition below= float(w)/h if w < h else float(h)/ww < dot_height or h < dot_height or ((w > 1.5*char_height or h > 1.5*char_height) and cratio < 0.1) or \
((w < char_height or h < char_height) and cratio < bratio):
# -1 in the last param fills the contour with given color.drawContours(img, [cnt], -1, (255, 255, 255), -1)
# else:
# print cratio
# cv2.rectangle(img, (x, y), (x+w, y+h), (0, 255, 0), 1)
# cut box borders= img[BORDER_HEIGHT:rows-BORDER_HEIGHT, BORDER_WIDTH:cols-BORDER_WIDTH]= cv2.cvtColor(img, cv2.COLOR_GRAY2RGB)img
# noinspection PyPep8Naminglocally_adaptive_binarization(img, window_size):
"""
__author__ = 'Vladan Krstic'the image according to the algorithm briefly explained in://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.19.4933&rep=rep1&type=pdfalgorithm is better than OpenCV's locally adaptive thresholding because it adopts a global minimum for. This will leave white areas in the image to be white (it will not calculate a threshold based on local'at all cost' - half on one side and half of other side).
:param img: Image array to be binarized. Expecting 8bit gray image (vals = 0..255, ndims = 2)
:param window_size: Size of the window used to calculate locally adaptive binarization threshold. Must be odd!
:return: binarized image as np.uint8
"""window_size % 2 == 0 or window_size <= 0:ValueError("window_size must be odd and positive")= (window_size - 1) / 2_expanded = cv2.copyMakeBorder(img, k, k, k, k, cv2.BORDER_REPLICATE), cols = img_expanded.shape= 0= np.min(img)= np.zeros(img.shape)= np.zeros(img.shape)= 0.2i in range(k, rows - k):j in range(k, cols - k):= img_expanded[i - k:i + k + 1, j - k:j + k + 1][i - k, j - k] = np.std(w)[i - k, j - k] = np.mean(w)s[i - k, j - k] > R:= s[i - k, j - k] # global maximum of stdevR == 0:ValueError("maximum stdev is zero - this shouldn't happen")
# binarization threshold:= m - c * (1 - s / R) * (m - M) # per-element matrix operations (avoids having another pair of nested for loops)= np.zeros(img.shape, dtype=np.uint8)[img > T] = 255resultestimate_text_height(img, print_msg=False):
"""
__author__ = 'Vladan Krstic'text height by looking at the pixel variance profile. The idea is to go row by row and observe pixel. The rows with higher variance are assumed to be areas with text information. If multiple lines of text are, median of their height is taken and returned as result. Areas smaller than 9px are ignored.image should contain area bigger than the text who's height is to be determined (text shouldn't touch upper oredge of the image).
:param img: Grayscale image of the text (dtype=uint8)
:param print_msg: Bool switch - Controls whether debug messages are printed to console.
:return: Estimated text height in pixels
"""_var = np.var(img, 1)= np.mean(h_var)h_var[0] >= mh and print_msg:"Warning: text probably touching the edge of the image."= []= 0i in range(1, h_var.size):h_var[i - 1] < mh <= h_var[i]:= i # top of the texth_var[i - 1] >= mh > h_var[i]:i-a < 9:
# expect text to be at least 9px high
# todo: this is not universal solution, consider algorithms for outlier detection on heights list.append(i - a) # bottom of the textnot heights:print_msg:"Warning: unable to determine text height"
# raise ValueError("Unable to determine text height.")a == 0: # appears like text height is equal to image height._height = img.shape[0]: # or text is touching the lower edge of the image, take helf the image height as text height_height = img.shape[0] / 2:_height = int(np.median(heights)) # text line heightprint_msg:"Estimated text height:", t_heightt_height
# noinspection PyPep8Naming,PyUnresolvedReferencesremove_lines(img, textHeight):
"""
__author__ = 'Vladan Krstic'thin horizontal lines from binary image.
. Take binary image, do the dilatation with a row-vector kernel to ensure that adjacent letters will be recognizedone blob.
. Take the convolution of the dilated image with a square kernel size = text height
. Threshold the convolution result to get binary blobs. Find bounding boxes for those blobs. -> words bboxes
. Remove the bboxes from the image and analyze the rezulting image to find the lines
. Remove the lines found from the original binary image.image should contain area wide enough to still have background lines after the text area is removed.minimum width is two times text height on each (lef/right) side of the text.
:param img: binarized image, dtype=uint8
:param textHeight: height of text - used as basis for other parameters
:return: image with lines removed
"""
# image form suitable for operations= 1 - img / 255.
# dilate with a row kernel= np.ones((1, textHeight / 2), np.float32)_tmp = cv2.dilate(imgbw, kernel)
# convolve with a box xernel= textHeight= np.ones((k, k), np.float32) / (k * k)= cv2.filter2D(blobs_tmp, -1, kernel)= 0.25 # todo: experiment with this thresh (must be between 0 and 1)_tmp = boxconv > t
# find bounding boxes, hierarchy = cv2.findContours(np.uint8(blobs_tmp), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)= []cnt in contours:, y, w, h = cv2.boundingRect(cnt).append([x, y, w, h])
# get the image without text_notext = imgbw.copy()rect in rects:, y, w, h = recti in range(y, y + h):j in range(x, x + w):_notext[i, j] = 0
# find lines_lines = np.dstack((np.uint8(255 * img_notext), np.uint8(255 * img_notext), np.uint8(255 * img_notext)))_notext_8bit = np.uint8(img_notext * 255)= img.shape[1] / 2= textHeight / 2
# maxgap = int(img.shape[1] * 0.95)
# threshold = int(textHeight * 0.1)= cv2.HoughLinesP(img_notext_8bit, 1, np.pi / 2, threshold, maxLineGap=maxgap)_pos = set() # i want unique valueslines is not None:x1, y1, x2, y2 in lines[0]:.line(img_lines, (x1, y1), (x2, y2), (0, 255, 0), 1)y1 == y2:_pos.add(y1)
# for each line found, do the filtering on the image with text
# during filtering, make variations of the column mask to accomodate varying thickness of the line_thickness = (textHeight + 5) / 10_padded = np.pad(imgbw, ((max_thickness, max_thickness), (0, 0)), mode='constant')= np.zeros(imgbw_padded.shape)line_pos:y in line_pos:+= max_thickness # compensate for paddingt in range(1, max_thickness+1):_mask = np.ones(t+2)_mask[0] = 0_mask[-1] = 0v in range(0, t):
# variations are formed by moving the column mask up/down by a number of pixels up
# to the thickness of the line.= imgbw_padded[y-t+v:y+v+2, :]_rows = deletemask[y-t+v:y+v+2, :] # creates a view of the rows of deletmaskj in range(0, rows.shape[1]):all(rows[:, j] == col_mask):_rows[:, j] += col_mask= deletemask[max_thickness:-max_thickness] # remove padding from the mask
# remove line pixels from the original image_cleaned = img.copy()_cleaned[deletemask > 0] = 255img_cleaned
# noinspection PyUnusedLocalclean_bg_v4(img, a, b):
"""
__author__ = 'Vladan Krstic'steps:
. Text height is estimated.
. Image is locally adaptively binarized with regards to text height.
. Background lines are removed.algorithm assumes that:
. The text doesn't touch img edges.
. Background lines are horizontal and extend beyond the width of the text bounding boxes.should have at least 5px area above and below the text. Image should contain enough of the passport formleft and/or right of the text so it can be detected when the text is removed.of thumb: image should contain area bigger than text bounding box by at least two heights of the text (morebetter, but not too much so it doesn't pick up too much background noise).
"""= cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# blur background while keeping edges (bilateral filter)= np.std(grayimg.flatten()) / 2= cv2.bilateralFilter(grayimg, 5, sigmacolor, 5) # todo: calculate kernel size automatically
# determine text height_height = estimate_text_height(grayimg, True)
# locally adaptive thresholding_size = t_height / 2 # set window size relative to line height.window_size % 2 == 0: # must be odd_size += 1_result = locally_adaptive_binarization(grayimg, window_size)_cleaned = remove_lines(bin_result, t_height)
# remove all blobs smaller than some value...= t_height / 2= img_cleaned_blobs, num_blobs = measurements.label(blobsies == 0, np.ones((3, 3)))i in range(1, num_blobs + 1):np.sum((labeled_blobs == i) * np.ones(labeled_blobs.shape)) < minpixels:[labeled_blobs == i] = 255
# labeled_blobs[labeled_blobs == i] = 0= cv2.cvtColor(blobsies, cv2.COLOR_GRAY2RGB)result
# noinspection PyUnusedLocalclean_bg(arg):clean_bg_v3(*arg)