def __init__(self): logger.debug("Looking for the templates in directory: " + TEMPLATE_DIR) self.template = [ (1034, cv2.imread(TEMPLATE_DIR + "temp_flag.jpg"), 0.75, "idcard"), (875, cv2.imread(TEMPLATE_DIR + "wap.jpg"), 0.60, "idbook"), (1280, cv2.imread(TEMPLATE_DIR + "pp2.jpg"), 0.60, "studentcard") ]
def __init__(self): """ Initialises the TextVerify object. Authors: Jan-Justin van Tonder """ # Logging for debugging purposes. logger.debug('Initialising TextVerify...')
def construct_face_extract_pipeline(): """ This function constructs the pipeline for face extraction. This includes building different managers with their specific parameters. These managers will be called within the pipeline when executed. Author(s): Stephan Nell Returns: :Pipeline (Constructed pipeline) """ logger.debug("Shape Predictor path: " + SHAPE_PREDICTOR_PATH) builder = PipelineBuilder() face_detector = FaceDetector(SHAPE_PREDICTOR_PATH) builder.set_face_detector(face_detector) return builder.get_result()
def validate_id_number(self, id_number, valid_length=13): """ Determines whether a given id number is valid or not. Args: id_number (str): valid_length (int): Specifies the length of a given id number to be considered as valid. Returns: (bool): True if the id number is valid, False otherwise. Raises: TypeError: If id_number is not a string containing only numeric characters. TypeError: If valid_length is not an integer. """ if (type(id_number) is not str) or (type(id_number) is str and not id_number.isnumeric()): raise TypeError( 'Bad type for arg id_number - expected string of ONLY numeric characters. Received type "%s"' % type(id_number).__name__) if type(valid_length) is not int: raise TypeError( 'Bad type for arg valid_length - expected integer. Received type "%s"' % type(valid_length).__name__) # Logging for debugging purposes. logger.debug('Checking if extracted id number is valid...') # Determine if the id number is of a valid length. is_valid_length = len(id_number) == valid_length logger.debug('Extracted id number length appears %s' % ('valid' if is_valid_length else 'invalid')) # Return early since the result will be false anyways. # Do not calculate the checksum if it is not required. if not is_valid_length: logger.debug('Extracted id number appears invalid') return False # Determine if the id number checksum is valid. is_valid_id_checksum = self._compute_checksum(id_number) == 0 # Both the length and the checksum must be valid for the entire id number to be valid. is_valid_id_number = is_valid_length and is_valid_id_checksum # Logging for debugging purposes. logger.debug('Extracted id number checksum appears %s' % ('valid' if is_valid_id_checksum else 'invalid')) logger.debug('Extracted id number appears %s' % ('valid' if is_valid_id_number else 'invalid')) # Return final result of validation. return is_valid_id_number
def grab_image(path=None, stream=None, url=None): """ This function grabs the image from URL, or image path and applies necessary changes to the grabbed images so that the image is compatible with OpenCV operation. Author(s): Stephan Nell Args: path (str): The path to the image if it reside on disk stream (str): A stream of text representing where on the internet the image resides url(str): Url representing a path to where an image should be fetched. Returns: (:obj:'OpenCV image'): Image that is now compatible with OpenCV operations TODO: Return a Json error indicating file not found """ # If the path is not None, then load the image from disk. Example: payload = {"image": open("id.jpg", "rb")} if path is not None: logger.debug("Grabbing from Disk") image = cv2.imread(path) # otherwise, the image does not reside on disk else: # if the URL is not None, then download the image if url is not None: logger.debug("Downloading image from URL") return url_to_image(url) # if the stream is not None, then the image has been uploaded elif stream is not None: # Example: "http://www.pyimagesearch.com/wp-content/uploads/2015/05/obama.jpg" logger.debug("Downloading from image stream") data = stream.read() image = np.asarray(bytearray(data), dtype="uint8") image = cv2.imdecode(image, cv2.IMREAD_COLOR) return image
def construct_text_extract_pipeline(preferences, identification_type): """ This function constructs the pipeline for text extraction. This includes building different managers with their specific parameters. These managers will be called within the pipeline when executed. Author(s): Nicolai van Niekerk and Marno Hermann Args: preferences (dict): User-specified techniques to use in pipeline. identification_type (string): Containts the type of identification, this is used to determine which techniques are used. Returns: :Pipeline (Constructed pipeline) """ builder = PipelineBuilder() # Use template matching to identify type here if 'blur_method' in preferences: blur_method = preferences['blur_method'] elif identification_type == 'idcard': blur_method = 'gaussian' elif identification_type == 'idbook': blur_method = 'gaussian' elif identification_type == 'studentcard': blur_method = 'median' else: # Default blur_method = 'median' if blur_method == 'median': blur_kernel_size = [3] else: if identification_type == 'idbook': blur_kernel_size = [(3, 3)] elif identification_type == 'idcard': blur_kernel_size = [(3, 3)] else: blur_kernel_size = [(3, 3)] if 'threshold_method' in preferences: threshold_method = preferences['threshold_method'] elif identification_type == 'idcard': threshold_method = 'adaptive' elif identification_type == 'idbook': threshold_method = 'adaptive' elif identification_type == 'studentcard': threshold_method = 'adaptive' else: # Default threshold_method = 'adaptive' if 'color' in preferences: color_extraction_type = 'extract' color = preferences['color'] elif identification_type == 'idcard': color_extraction_type = 'extract' color = 'red_blue' elif identification_type == 'idbook': color_extraction_type = 'extract' color = 'red_blue' elif identification_type == 'studentcard': color_extraction_type = 'extract' color = 'red' else: # Default color_extraction_type = 'extract' color = 'red' logger.debug("Blur Method: " + blur_method) logger.debug("Kernel Size: " + str(blur_kernel_size)) logger.debug("ColorXType: " + color_extraction_type) logger.debug("Color: " + color) logger.debug("Threshold Method: " + threshold_method) blur_manager = BlurManager(blur_method, blur_kernel_size) color_manager = ColorManager(color_extraction_type, color) threshold_manager = ThresholdingManager(threshold_method) face_detector = FaceDetector(SHAPE_PREDICTOR_PATH) builder.set_blur_manager(blur_manager) builder.set_color_manager(color_manager) builder.set_face_detector(face_detector) builder.set_threshold_manager(threshold_manager) return builder.get_result()
def verify(self, face1, face2, threshold=0.55): """ This function determines a percentage value of how close the faces in the images passed are to each other if the determined value if below the threshold value passed by the user a boolean value of True is returned indicating that the faces in the images passed indeed match. The Verify function makes use of the dlib library which guarantees 99.38% accuracy on the standard Labeled Faces in the Wild benchmark. Author(s): Stephan Nell Args: face1 (:obj:'OpenCV image'): The first image containing the face that should be compared. face2 (:obj:'OpenCV image'): The second image containing the face that should be compared threshold (float): The threshold value determines at what distance the two images are considered the same person. If a verify score is below the threshold value the faces are considered a match. The Labled Faces in the Wild benchmark recommend a default threshold of 0.6 but a threshold of 0.55 was decided on since a threshold of 0.55 represents the problem better. Returns: bool: Represent if two face indeed match True if distance calculated is below threshold value. False if the distance calculated is above threshold value. float: Return Euclidean distance between the vector representation of the two faces Raises: ValueError: If no face can be detected no faces can be matched and operation should be aborted. """ logger.debug('Getting frontal face detector') detector = dlib.get_frontal_face_detector() logger.debug('Getting shape predictor') shape_predictor = dlib.shape_predictor(self.shape_predictor_path) logger.debug('Getting facial recogniser') facial_recogniser = dlib.face_recognition_model_v1( self.face_recognition_path) logger.info('Getting face in first image') face_detections = detector(face1, 1) if face_detections is None: logger.error('Could not find a face in the first image') raise ValueError('Face could not be detected') logger.debug('Getting the shape') shape = shape_predictor(face1, face_detections[0]) logger.debug('Getting the first face descriptor') face_descriptor1 = facial_recogniser.compute_face_descriptor( face1, shape) logger.info('Getting face in second image') face_detections = detector(face2, 1) if face_detections is None: logger.error('Could not find a face in the first image') raise ValueError('Face could not be detected') logger.debug('Getting the shape') shape = shape_predictor(face2, face_detections[0]) logger.debug('Getting the second face descriptor') face_descriptor2 = facial_recogniser.compute_face_descriptor( face2, shape) logger.info('Calculating the euclidean distance between the two faces') match_distance = distance.euclidean(face_descriptor1, face_descriptor2) logger.info('Matching distance: ' + str(match_distance)) # Any distance below our threshold of 0.55 is a very good match. # We map 0.55 to 85% and 0 to 100%. if match_distance < threshold: match_distance = 1 - match_distance threshold = 1 - threshold + 0.05 percentage_match = ( (match_distance - threshold) * 15 / 50) * 100 + 85 logger.info('Matching percentage: ' + str(percentage_match) + "%") return True, percentage_match elif match_distance < threshold + 0.05: # In this if we map (0.55-0.60] we map 0.549 to 70% match match_distance = 1 - match_distance threshold = 1 - threshold + 0.05 percentage_match = ( (match_distance - threshold) * 30 / 55) * 100 + 70 logger.info('Matching percentage: ' + str(percentage_match) + "%") return True, percentage_match else: # If the distance is higher than 0.65 we map it to 60% and below percentage_match = 60 - ( (match_distance - threshold) * 60 / 40) * 100 logger.info('Matching percentage: ' + str(percentage_match) + "%") return False, percentage_match
def verify(self, extracted, verifier, threshold=75.00, min_matches=4, verbose=False): """ This function is responsible for the verification of text that is extracted from an ID and is passed in, along with information that is to be used to verify the extracted text. Args: extracted (dict): A dictionary containing the information that was extracted from an ID. verifier (dict): A dictionary containing the information against which the extracted data is to be verified. threshold (float): A threshold percentage (out of 100) that is used to determine whether or not the final match percentage is accepted as verified. min_matches (int): The minimum number of matches that have to be calculated for the final result to be considered as verified. verbose (bool): Indicates whether or not to return all of the calculated match percentages. Returns: (bool, float | dict): The first value returned is a bool that indicates whether or not the total percentage match is above the specified threshold value, while the second return value is the total percentage match value if verbose is False, or returns a dict of all the determined percentage match values if verbose is True. Raises: TypeError: If extracted is not a dictionary. TypeError: If verifier is not a dictionary. TypeError: If threshold is not a float. TypeError: If min_matches is not an integer. TypeError: If verbose is not a boolean. """ if type(extracted) is not dict: raise TypeError( 'Bad type for arg extracted - expected dict. Received type "%s"' % type(extracted).__name__) if type(verifier) is not dict: raise TypeError( 'Bad type for arg verifier - expected dict. Received type "%s"' % type(verifier).__name__) if type(threshold) is not float: raise TypeError( 'Bad type for arg threshold - expected float. Received type "%s"' % type(threshold).__name__) if type(min_matches) is not int: raise TypeError( 'Bad type for arg min_matches - expected int. Received type "%s"' % type(min_matches).__name__) if type(verbose) is not bool: raise TypeError( 'Bad type for arg verbose - expected bool. Received type "%s"' % type(verbose).__name__) # Set minimum number of matches, if zero or less set to one. min_matches = min_matches if min_matches > 0 else 1 # Logging for debugging and verbose purposes. logger.debug('Threshold for verification set as: %.2f' % threshold) logger.debug('Minimum number of matches for verification set as: %d' % min_matches) logger.debug('Simplified percentages to be returned' if not verbose else 'Verbose percentages to be returned') logger.debug('-' * 50) logger.debug('Verifying:') logger.debug('-' * 50) # Prettify and log the extracted information. [ logger.debug(log_line) for log_line in prettify_json_message(extracted).split('\n') ] logger.debug('-' * 50) logger.debug('Against:') logger.debug('-' * 50) # Prettify and log the verifier information. [ logger.debug(log_line) for log_line in prettify_json_message(verifier).split('\n') ] logger.debug('-' * 50) # Initialise a dictionary to house the final matching percentages. match_percentages = {} # Iterate over the verifier and calculate a percentage match for the values, # if the keys match and the corresponding values exist. for key, value in verifier.items(): if key in extracted and extracted[key] is not None: # Compute the match percentage. logger.debug('Computing match "%s" and "%s"...' % (value, extracted[key])) match_percentages[key] = { 'match_percentage': self._match_percentage(value, extracted[key]), 'verifier_field_value': value, 'extracted_field_value': extracted[key] } logger.debug('"%s" and "%s" match percentage: %.2f' % (value, extracted[key], match_percentages[key]['match_percentage'])) else: logger.warning( 'Could not find corresponding field "%s" in extracted information to verify' % key) # Determine the number of percentages calculated and initialise a default value for the total match score. num_scores = len(match_percentages) total_match_percentage = 0.0 # Check if enough matches were found. if num_scores >= min_matches: # Calculate the total match score. total_match_percentage = self._total_percentage_match( match_percentages) # Either the minimum number of percentages criteria was not met. else: logger.warning( 'A total of %d matches were found, which is less than the minimum' % num_scores) # Determine whether or not the text is verified. is_verified = total_match_percentage >= threshold # Logging for debugging purposes. logger.debug('-' * 50) logger.debug('Intermediate match percentages:') logger.debug('-' * 50) [ logger.debug(log_line) for log_line in prettify_json_message( match_percentages).split('\n') ] logger.debug('-' * 50) logger.debug('Final match percentage: %.2f' % total_match_percentage) logger.debug('Threshold to pass: %.2f' % threshold) logger.debug('Result: ' + 'Passed' if is_verified else 'Failed') # Return the final result. if not verbose: return is_verified, total_match_percentage # Append the total and non-matches to the existing percentages for verbose purposes, # and return all percentage values. match_percentages.update(self._get_non_matches(extracted, verifier)) match_percentages['total'] = total_match_percentage return is_verified, match_percentages
def extract(self, img): """ This function is a sample that demonstrates how text would be extracted Author(s): Nicolai van Niekerk Args: img: The image of the ID that contains the text to be extracted Returns: id_details: JSON obj (The extracted information) """ if 'remove_face' in self.preferences: self.remove_face = self.preferences['remove_face'] == 'true' logger.debug('self.remove_face: ' + str(self.remove_face)) simplification_manager = SimplificationManager() barcode_manager = BarCodeManager() data = {} # Perform perspective transformation and read from barcode. logger.info('Performing perspective transformation...') image = simplification_manager.perspectiveTransformation(img) cv2.imwrite(DESKTOP + "/output/3.png", image) barcode_data_found, barcode_scan_data, barcoded_image = barcode_manager.get_barcode_info( image) if barcode_data_found: logger.info('Barcode successfully scanned') data = { 'identity_number': barcode_scan_data.decode('utf-8'), } # Process image if 'id_type' in self.preferences: identification_type = self.preferences['id_type'] logger.info("No template matching required") logger.info("Identification type: " + identification_type) else: template_match = TemplateMatching() logger.info('Performing template matching...') identification_type = template_match.identify(barcoded_image) logger.info('Constructing text extraction pipeline') pipeline = BuildDirector.construct_text_extract_pipeline( self.preferences, identification_type) image = pipeline.process_text_extraction(barcoded_image, self.remove_face) # Extract and return text filename = "{}.png".format(os.getpid()) cv2.imwrite(filename, image) text = pytesseract.image_to_string(Image.open(filename)) os.remove(filename) text_manager = TextManager() # Log the uncleaned string to terminal. # This is for demonstration purposes. logger.debug('-' * 50) logger.debug('String to clean:') logger.debug('-' * 50) [logger.debug(log_line) for log_line in text.split('\n')] logger.debug('-' * 50) logger.info('Cleaning up text...') # Clean the OCR output text. clean_text = text_manager.clean_up(text) # Log the cleaned string to terminal. # This is for demonstration purposes. logger.debug('-' * 50) logger.debug('Cleaned text:') logger.debug('-' * 50) [logger.debug(log_line) for log_line in clean_text.split('\n')] logger.debug('-' * 50) # Cater for UP student/staff cards. if identification_type == 'studentcard': return { 'up_card': True, # Used to be able to reliably check if a response is a UP card from client-side. 'text_dump': clean_text, # Dump extracted and cleaned text. 'barcode_dump': data['identity_number'] if data else None # Dump the barcode data. } # Dictify cleaned text. logger.info('Placing extracted text in a dictionary...') id_details = text_manager.dictify(clean_text, data) # Log the dictified extracted text to terminal. # This is for demonstration purposes. logger.debug('-' * 50) logger.debug('Extracted ID details:') logger.debug('-' * 50) [ logger.debug(id_details_line) for id_details_line in prettify_json_message(id_details).split('\n') ] logger.debug('-' * 50) # Return the extracted ID information. return id_details
def __init__(self): """ Responsible for initialising the TextManager object. """ # Logging for debugging purposes. logger.debug('Initialising TextManager...') # Specify initial list of undesirable characters. self._deplorables = ['_'] # Specify initial list of contexts for string image_processing when populating # the ID information dictionary to send as output. self.match_contexts = [{ 'field': 'identity_number', 'find': ['id no', 'identity number'], 'field_type': FieldType.NUMERIC_ONLY, 'multi_line': False }, { 'field': 'surname', 'find': ['surname', 'vansurname'], 'field_type': FieldType.TEXT_ONLY, 'to_uppercase': False, 'multi_line': True, 'multi_line_end': ['forenames', 'names', 'voornameforenames'] }, { 'field': 'names', 'find': ['forenames', 'names', 'voornameforenames'], 'field_type': FieldType.TEXT_ONLY, 'to_uppercase': False, 'multi_line': True, 'multi_line_end': ['country of birth', 'sex', 'geboortedistrik of-land'] }, { 'field': 'sex', 'find': ['sex'], 'field_type': FieldType.TEXT_ONLY, 'to_uppercase': True, 'multi_line': False }, { 'field': 'date_of_birth', 'find': ['date of birth'], 'field_type': FieldType.MIXED, 'to_uppercase': False, 'multi_line': False }, { 'field': 'country_of_birth', 'find': ['country of birth', 'district or country of birth'], 'field_type': FieldType.TEXT_ONLY, 'to_uppercase': True, 'multi_line': False }, { 'field': 'status', 'find': ['status'], 'field_type': FieldType.TEXT_ONLY, 'to_uppercase': False, 'multi_line': False }, { 'field': 'nationality', 'find': ['nationality'], 'field_type': FieldType.TEXT_ONLY, 'to_uppercase': True, 'multi_line': False }]
def dictify(self, id_string, barcode_data=None, fuzzy_min_ratio=60.0, max_multi_line=2): """ This function is responsible for generating a dictionary object containing the relevant ID information, such as names, surname, ID number, etc., from a given input string containing said relevant information. Authors: Jan-Justin van Tonder Args: id_string (str): A string containing some ID information. barcode_data (dict, Optional): A dictionary object containing information extracted from a barcode. fuzzy_min_ratio (float): The threshold ratio for a minimum, acceptable ratio of fuzziness when comparing two strings. max_multi_line (int): Specifies the maximum number of lines that is to be extracted from fields that are noted as running onto multiple lines. Returns: (dict): A dictionary object containing the relevant, extracted ID information. Raises: TypeError: If id_string is not a string. TypeError: If barcode_data is not a dictionary. """ # Check if arguments passed in are the correct type. if type(id_string) is not str: raise TypeError( 'Bad type for arg id_string - expected string. Received type "%s".' % type(id_string).__name__) if barcode_data and type(barcode_data) is not dict: raise TypeError( 'Bad type for arg barcode_data - expected dictionary. Received type "%s".' % type(barcode_data).__name__) if type(fuzzy_min_ratio) is not float: raise TypeError( 'Bad type for arg fuzzy_min_ratio - expected float. Received type "%s".' % type(fuzzy_min_ratio).__name__) if type(max_multi_line) is not int: raise TypeError( 'Bad type for arg max_multi_line - expected int. Received type "%s".' % type(max_multi_line).__name__) # Given a string containing extracted ID text, # create a dictionary object and populate it with # relevant information from said text. id_info = {} # Check if barcode data, containing the id number, exists and # if so, save it and extract some relevant information from it. # It should overwrite any existing fields that can be extracted from the id number, since # the information embedded within the id number is more reliable, at least theoretically. if barcode_data: logger.debug('Extracting details from barcode data...') id_info['identity_number'] = barcode_data['identity_number'] self._id_number_information_extraction( id_info, barcode_data['identity_number']) # Attempt to populate id_info with information from the given ID string. logger.debug('Extracting details from the given text string...') self._populate_id_information(id_string, id_info, fuzzy_min_ratio, max_multi_line) # Perform some custom post-processing on the information that was extracted. logger.debug('Post-processing some field values...') self._post_process(id_info) # Return the info that was found. return id_info