############################### # # (c) Vlad Zat 2017 # Student No: C14714071 # Course: DT228 # Date: 13-10-2017 # # Title: Signature Extractor # # Introduction: # Extract the signature from an image by firstly extracting the page in it (if any) # then extracting a signature block, and in the end use thresholding to extract only the # signature in the correct colour. Allow the user to import an image from file or capture # from the camera. The result can be viewed on the screen or exported to file. # # Structure: # 1. Extract the Page # a. Convert the image to Grayscale # * Using only one channel is necessary for both edge detection and segmentation # b. Find the edges of the image using Canny # * Edge detection is used to be able to find the main objects in the image # * The range for Canny is approximated to be from half the treshhold to # the value of the treshhold. This threshold is calculated using Otsu's Algorithm. # This provides consident results and is recommended by Mei Fang (et al.) in # "The Study on An Application of Otsu Method in Canny Operator" [1] # c. Getting the contours of the objects in the image # d. Finding the biggest contour with 4 edges # * The perimeter of the contour is calculated and then used to approximate a # polygon around it. To decrese the ammound of edges detected, the permited # is multiplied with 0.1 as recommended in the OpenCV Documentation [2] # e. Detecting if the biggest contour has any contours in it # * If the biggest contour does not have any other contours in it (such as words # or the signature) then it's a false alarm and there is no complete page in the image # so the whole image is used in the next step # 2. Extracting the Signature # a. Convert the image to Grayscale # b. Find the edges of the image using Canny # c. Getting the contours of the objects in the image # d. Closing the image so the signatures is more filled and pronounced # e. Finding the contour with the most number of edges # * Opposed to the method from the first step, here the permimeter of the # signature is multipled with 0.01 to give it a better shape of the signature [2] # * Signatures have more edges than normal text in a page so the contour with # the maximum ammount of edges should be the signature # f. Pad the image so the signature can be viewed more easily # 3. Remove the background from the signature # a. Convert the image to Grayscale # b. Use adaptive thresholding to extract only the signature # * A block of 1/8 of the width of the image is used as it provides # consistent results # * If the image is too small then the width of the image is used instead # # Experiments: # * Blur the image using Median Blur and Bilateral Blur to maintain the edges # but smooth the objects. It requires hardcoded values for the kernel size used # and it doesn't provide good enough results # * Use thresholding to extract the page. While it does provide similar results to # the edge detection method, it requires hard coded values # * Try to use Otsu's Algorithm for the final step. It does not provide good results, # even for small parts of the image # * Use Hough Line to get the lines in the image and extracting zones where they are # intersecting. It does not detect lines in the images # * Use a feature detection algorithm such as AKAZE to get the the features of the image. # It doesn't provide better results than using Canny # * Use different colour spaces to better extract the paper. The luminance or saturation # provide similar results to grayscale # * Make the image smaller if the image is too big as the signature extraction does not # work properly with large images. It causes problems for some images. # # References: # [1] M.Fang, GX.Yue1, QC.Yu, 'The Study on An Application of Otsu Method in Canny Operator', # International Symposium on Information Processing, Huangshan, P. R. China, # August 21-23, 2009, pp. 109-112 # [2] OpenCV, 'Contour Approximation', 2015. [Online]. # Available: http://docs.opencv.org/3.1.0/dd/d49/tutorial_py_contour_features.html # [Accessed: 2017-10-05] import numpy as np import cv2 import easygui def getImageFromFile(): file = easygui.fileopenbox() img = cv2.imread(file) if img is None: errorPrompt('Not a valid image type') return img def getImageFromCamera(): camera = cv2.VideoCapture(0) (success, img) = camera.read() if not success: errorPrompt('Cannot capture image') return img def writeImageToFile(img, mask): # The mask of the signature can be used as the alpha channel of the image b, g, r = cv2.split(img) imgWithAlpha = cv2.merge((b, g, r, mask)) file = easygui.filesavebox() fileName = file + '.png' if fileName is None: errorPrompt('No Name Selected') cv2.imwrite(fileName, imgWithAlpha) def displayImageToScreen(img, mask): imgSize = np.shape(img) bg = np.zeros((imgSize[0], imgSize[1], 3), np.uint8) bg[:, :] = (255, 255, 255) # Add a white background to the signature rmask = cv2.bitwise_not(mask) bgROI = cv2.bitwise_and(bg, bg, mask = rmask) sigROI = cv2.bitwise_and(signature, signature, mask = mask) roi = cv2.bitwise_or(bgROI, sigROI) cv2.imshow('Signature', roi) cv2.waitKey(0) def errorPrompt(errorMsg): print 'Error: ' + errorMsg easygui.msgbox(msg = errorMsg, title = 'ERROR', ok_button = 'OK') quit() def scaleImageDown(img): imgSize = np.shape(img) maxSize = (1920, 1080) if imgSize[0] > maxSize[0] or imgSize[1] > maxSize[1]: print 'Warning: Image too big' wRatio = float(float(maxSize[0]) / float(imgSize[0])) hRatio = float(float(maxSize[1]) / float(imgSize[1])) ratio = 1.0 if wRatio > hRatio: ratio = wRatio else: ratio = hRatio return cv2.resize(img, (int(ratio * imgSize[1]), int(ratio * imgSize[0]))) return img def addPadding(rect, padding, imgSize): rect['x'] -= padding rect['y'] -= padding rect['w'] += 2 * padding rect['h'] += 2 * padding if rect['x'] < 0: rect['x'] = 0 if rect['y'] < 0: rect['y'] = 0 if rect['x'] + rect['w'] > imgSize[0]: rect['w'] = imgSize[0] - rect['x'] if rect['y'] + rect['h'] > imgSize[1]: rect['h'] = imgSize[1] - rect['y'] return rect def getPageFromImage(img): imgSize = np.shape(img) gImg = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) threshold, _ = cv2.threshold(src = gImg, thresh = 0, maxval = 255, type = cv2.THRESH_BINARY | cv2.THRESH_OTSU) cannyImg = cv2.Canny(image = gImg, threshold1 = 0.5 * threshold, threshold2 = threshold) # findContours() is a distructive function so a copy is passed as a parameter _, contours, _ = cv2.findContours(image = cannyImg.copy(), mode = cv2.RETR_TREE, method = cv2.CHAIN_APPROX_SIMPLE) if len(contours) == 0: print 'Warning: No Page Found' return img maxRect = { 'x': 0, 'y': 0, 'w': 0, 'h': 0 } coordinates = [] for contour in contours: # Perimeter accuracy arcPercentage = 0.1 # Contour Perimeter epsilon = cv2.arcLength(curve = contour, closed = True) * arcPercentage corners = cv2.approxPolyDP(curve = contour, epsilon = epsilon, closed = True) x, y, w, h = cv2.boundingRect(points = corners) currentArea = w * h if len(corners) == 4: coordinates.append((x, y)) if currentArea > maxRect['w'] * maxRect['h']: maxRect['x'] = x maxRect['y'] = y maxRect['w'] = w maxRect['h'] = h if maxRect['w'] <= 1 or maxRect['h'] <= 1: print 'Warning: No Page Found' return img contoursInPage = 0 for coordinate in coordinates: x = coordinate[0] y = coordinate[1] if (x > maxRect['x'] and x < maxRect['x'] + maxRect['w']) and (y > maxRect['y'] and y < maxRect['y'] + maxRect['h']): contoursInPage += 1 if contoursInPage <= 0: print 'Warning: No Page Found' return img return img[maxRect['y'] : maxRect['y'] + maxRect['h'], maxRect['x'] : maxRect['x'] + maxRect['w']] def getSignatureFromPage(img): imgSize = np.shape(img) gImg = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) threshold, _ = cv2.threshold(src = gImg, thresh = 0, maxval = 255, type = cv2.THRESH_BINARY | cv2.THRESH_OTSU) cannyImg = cv2.Canny(image = gImg, threshold1 = 0.5 * threshold, threshold2 = threshold) # The kernel is wide as most signatures are wide kernel = cv2.getStructuringElement(shape = cv2.MORPH_RECT, ksize = (30, 1)) cannyImg = cv2.morphologyEx(src = cannyImg, op = cv2.MORPH_CLOSE, kernel = kernel) # findContours() is a distructive function so a copy is passed as a parameter _, contours, _ = cv2.findContours(image = cannyImg.copy(), mode = cv2.RETR_TREE, method = cv2.CHAIN_APPROX_SIMPLE) if len(contours) == 0: print 'Warning: No Signature Found' return img maxRect = { 'x': 0, 'y': 0, 'w': 0, 'h': 0 } maxCorners = 0 for contour in contours: # Perimeter accuracy arcPercentage = 0.01 # Contour perimeter epsilon = cv2.arcLength(curve = contour, closed = True) * arcPercentage corners = cv2.approxPolyDP(curve = contour, epsilon = epsilon, closed = True) x, y, w, h = cv2.boundingRect(points = corners) if len(corners) > maxCorners: maxCorners = len(corners) maxRect['x'] = x maxRect['y'] = y maxRect['w'] = w maxRect['h'] = h if maxRect['w'] <= 1 or maxRect['h'] <= 1: print 'Warning: No Signature Found' return img # Add padding so the signature is more visible maxRect = addPadding(rect = maxRect, padding = 10, imgSize = imgSize) return img[maxRect['y'] : maxRect['y'] + maxRect['h'], maxRect['x'] : maxRect['x'] + maxRect['w']] def getSignature(img): imgSize = np.shape(img) gImg = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # Adaptive Thresholding requires the blocksize to be odd and bigger than 1 blockSize = 1 / 8 * imgSize[0] / 2 * 2 + 1 if blockSize <= 1: blockSize = imgSize[0] / 2 * 2 + 1 const = 10 mask = cv2.adaptiveThreshold(gImg, maxValue = 255, adaptiveMethod = cv2.ADAPTIVE_THRESH_MEAN_C, thresholdType = cv2.THRESH_BINARY, blockSize = blockSize, C = const) rmask = cv2.bitwise_not(mask) return (cv2.bitwise_and(img, img, mask=rmask), rmask) # First Prompt title = 'Image Selection' message = 'Choose a method of getting the picture' buttons = ['File', 'Camera'] selection = easygui.indexbox(msg = message, title = title, choices = buttons) if selection == 0: img = getImageFromFile() elif selection == 1: img = getImageFromCamera() else: quit() # Extract Signature page = getPageFromImage(img = img) signatureBlock = getSignatureFromPage(img = page) (signature, mask) = getSignature(img = signatureBlock) # Second Prompt title = 'Display or Export' message = 'Choose a method of showing the signature' buttons = ['Display on Screen', 'Export to File'] selection = easygui.indexbox(msg = message, title = title, choices = buttons) if selection == 0: displayImageToScreen(signature, mask) elif selection == 1: writeImageToFile(signature, mask) else: quit()