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
Beispiel #5
0
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()
Beispiel #7
0
    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
Beispiel #10
0
 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
     }]
Beispiel #11
0
    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