madmaze / pytesseract

A Python wrapper for Google Tesseract
Apache License 2.0
5.77k stars 714 forks source link

[Refactoring] Reduce numbers of identique calls to tesseract #459

Closed ELToulemonde closed 1 year ago

ELToulemonde commented 1 year ago

Context

Exploring the code base to develop the functionality proposed in #159 I discovered that the call to tesseract is often performed multiple times on exact same data with same arguments for formatting purposes.

Example :

For function image_to_string it is called 3 times. I checked in terms of process, 3 process are indeed launched.

def image_to_string(
    image,
    lang=None,
    config='',
    nice=0,
    output_type=Output.STRING,
    timeout=0,
):
    """
    Returns the result of a Tesseract OCR run on the provided image to string
    """
    args = [image, 'txt', lang, config, nice, timeout]

    return {
        Output.BYTES: lambda: run_and_get_output(*(args + [True])),
        Output.DICT: lambda: {'text': run_and_get_output(*args)},
        Output.STRING: lambda: run_and_get_output(*args),
    }[output_type]()

Consequences

Computation seems to occur in parallel so it doesn't have an immediate impact on computation time. But it is sub-optimal :

Proposition

A small refacto could allow us to reduce by 2 to 3 the number of calls.

Refactoring would look like this.

  1. Remove return_bytes=False option in run_and_get_output always return bytes Complete implementation would be

    def run_and_get_output(
    image,
    extension='',
    lang=None,
    config='',
    nice=0,
    timeout=0,
    ):
    with save(image) as (temp_name, input_filename):
        kwargs = {
            'input_filename': input_filename,
            'output_filename_base': temp_name,
            'extension': extension,
            'lang': lang,
            'config': config,
            'nice': nice,
            'timeout': timeout,
        }
    
        run_tesseract(**kwargs)
        filename = f"{kwargs['output_filename_base']}{extsep}{extension}"
        with open(filename, 'rb') as output_file:
            return output_file.read()

    NB: If we want to avoid changing the signature of this function, we could keep it as it is and always call it with return_bytes=True

  2. Implement a decode function

    def decode_result(result: bytes) -> str:
    return result.decode(DEFAULT_ENCODING)
  3. In interfaces functions make on call to run_and_get_output and manipulate output to have expected results

    def image_to_string(
            image,
            lang=None,
            config='',
            nice=0,
            output_type=Output.STRING,
            timeout=0,
    ):
    args = [image, 'txt', lang, config, nice, timeout]
    bytes_result = run_and_get_output(*args)
    decoded_result = decode_result(bytes_result)
    return {
        Output.BYTES: lambda: bytes_result,
        Output.DICT: lambda: {'text': decoded_result},
        Output.STRING: lambda: decoded_result,
    }[output_type]()

Finally we would need to modify function get_pandas_output

def get_pandas_output(tesseract_outputs, config=None):
    if not pandas_installed:
        raise PandasNotSupported()

    kwargs = {'quoting': QUOTE_NONE, 'sep': '\t'}
    try:
        kwargs.update(config)
    except (TypeError, ValueError):
        pass

    return pd.read_csv(BytesIO(decode_result(tesseract_outputs)), **kwargs)

NB: If we want to avoid changing the signature of this function we could create another one named get_pandas_from_tesseract_output.

Nice side effects from this refactoring

Conclusion

What do you think ? Did I miss something ?

stefan6419846 commented 1 year ago

It seems like you are mixing something up here: The above construct will only call the branch actually needed, while Tesseract will use multiple threads by default when actually running (this is what you observed).

You can verify this with the following basic example as well:

choice = {
    'key1': lambda: print('1'),
    'key2': lambda: print('2'),
    'key3': lambda: print('3'),
}['key2']

choice()

This will only print 2 as expected.

ELToulemonde commented 1 year ago

Indeed I miss-interpreted what I saw.

Thanks, I learn something and I close the issue.