Skip to content

Commit

Permalink
Added new file_temp interpretation method, improved documentation, …
Browse files Browse the repository at this point in the history
…refactor.
  • Loading branch information
ZeroBone committed Jan 17, 2024
1 parent 70d8b23 commit ddc5244
Show file tree
Hide file tree
Showing 17 changed files with 275 additions and 211 deletions.
16 changes: 16 additions & 0 deletions docs/dev/changelog.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,27 @@
# Changelog

## Release 1.1.3 (beta)

* The `tesseract_ocr` interpretation method no longer has default Tesseract OCR configuration values predefined.
* Added a new `file_temp` interpretation method that allows one to save features as temporary files.
* Substrantial refactor & architecture improvements.

[View on GitHub](https://github.com/ZeroBone/OfficialEye/releases/tag/1.1.3){ .md-button }

## Release 1.1.2 (beta)

There are only minor improvements compared to the previous release (version 1.1.1). This release serves as an initial release for PyPI.

[View on GitHub](https://github.com/ZeroBone/OfficialEye/releases/tag/1.1.2){ .md-button }

## Release 1.1.1 (beta)

* Fixed many minor bugs, typos and inconsistencies.
* Refactor.
* Many documentation improvements.

[View on GitHub](https://github.com/ZeroBone/OfficialEye/releases/tag/1.1.1){ .md-button }

## Release 1.1.0 (beta)

This release features a new mutation and interpretation system, and a feature class system.
Expand Down
44 changes: 32 additions & 12 deletions docs/usage/getting-started/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,31 +13,51 @@ OfficialEye requires Python 3.10+ to be installed. It works on multiple platform

### Installation for usage

The tool can be installed with the standard `pip` installation command:
#### Recommended installation method

```shell
pip install officialeye
```
Start by installing [PIPX](https://github.com/pypa/pipx) (if you haven't installed it already).

=== "MacOS"

```shell
brew install pipx
pipx ensurepath
```

Especially if you are deploying the tool on a production server, you might want to set up `OfficialEye` in a `venv` virtual environment, which is an isolated Python runtime:
=== "Linux"

```shell
sudo apt install pipx
pipx ensurepath
```

=== "Windows"

```shell
scoop install pipx
pipx ensurepath
```

Next, use `pipx` to install OfficialEye.

```shell
python3 -m venv venv
source venv/bin/activate
pip install officialeye
pipx install officialeye
```

To leave the virtual environment, execute
#### Installation via PIP

The tool can also be installed with the standard `pip` installation command:

```shell
deactivate
pip install officialeye --break-system-packages
```

For more information about `venv` virtual environments, see the [official documentation](https://packaging.python.org/en/latest/guides/installing-using-pip-and-virtual-environments/#creating-a-virtual-environment).
!!! warning
The above command installs the package globally, which is not recommended due to possible conflicts between OS package managers and python-specific package management tools (see [PEP 668](https://peps.python.org/pep-0668/)).

### Installation for development

To se tup the development environment, start by cloning the [GitHub repository](https://github.com/ZeroBone/OfficialEye) and navigating to the projects' root directory:
To set up the development environment on a Linux (prefferably Ubuntu) computer, start by cloning the [GitHub repository](https://github.com/ZeroBone/OfficialEye) and navigating to the projects' root directory:

```shell
git clone https://github.com/ZeroBone/OfficialEye.git
Expand Down
2 changes: 2 additions & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,8 @@ markdown_extensions:
- pymdownx.details
- pymdownx.superfences
- pymdownx.mark
- pymdownx.tabbed:
alternate_style: true
- attr_list
- md_in_html
- pymdownx.emoji:
Expand Down
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "officialeye"
version = "1.1.2"
version = "1.1.3"
description = "AI-powered generic document-analysis tool"
authors = [
{name = "Alexander Mayorov", email = "[email protected]"},
Expand All @@ -27,7 +27,7 @@ classifiers = [
]

[project.scripts]
officialeye = "officialeye.officialeye:cli"
officialeye = "officialeye:cli"

[project.urls]
Homepage = "https://github.com/ZeroBone/OfficialEye"
Expand Down
154 changes: 153 additions & 1 deletion src/officialeye/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,153 @@
__version__ = "1.1.2"
"""
OfficialEye main entry point.
"""

from typing import List

import click
# noinspection PyPackageRequirements
import cv2

from officialeye.context.singleton import oe_context
from officialeye.error import OEError
from officialeye.io.drivers.run import RunIODriver
from officialeye.io.drivers.test import TestIODriver
from officialeye.meta import OFFICIALEYE_GITHUB, OFFICIALEYE_VERSION, print_logo
from officialeye.template.analyze import do_analyze
from officialeye.template.create import create_example_template_config_file
from officialeye.template.parser.loader import load_template
from officialeye.util.logger import oe_info, oe_warn


@click.group()
@click.option("-d", "--debug", is_flag=True, show_default=True, default=False, help="Enable debug mode.")
@click.option("--dedir", type=click.Path(exists=True, file_okay=True, readable=True), help="Specify debug export directory.")
@click.option("--edir", type=click.Path(exists=True, file_okay=True, readable=True), help="Specify export directory.")
@click.option("-q", "--quiet", is_flag=True, show_default=True, default=False, help="Disable standard output messages.")
@click.option("-v", "--verbose", is_flag=True, show_default=True, default=False, help="Enable verbose logging.")
@click.option("-dl", "--disable-logo", is_flag=True, show_default=True, default=False, help="Disable the officialeye logo.")
def cli(debug: bool, dedir: str, edir: str, quiet: bool, verbose: bool, disable_logo: bool):

oe_context().debug_mode = debug
oe_context().quiet_mode = quiet
oe_context().verbose_mode = verbose
oe_context().disable_logo = disable_logo

oe_context().io_driver = TestIODriver()

if not quiet and not disable_logo:
print_logo()

if dedir is not None:
oe_context().debug_export_directory = dedir

if edir is not None:
oe_context().export_directory = edir

if oe_context().debug_mode:
oe_warn("Debug mode enabled. Disable for production use to improve performance.")


# noinspection PyShadowingBuiltins
@click.command()
@click.argument("template_path", type=click.Path(exists=False, file_okay=True, readable=True, writable=True))
@click.argument("template_image", type=click.Path(exists=True, file_okay=True, readable=True, writable=False))
@click.option("--id", type=str, show_default=False, default="example", help="Specify the template identifier.")
@click.option("--name", type=str, show_default=False, default="Example", help="Specify the template name.")
@click.option("--force", is_flag=True, show_default=True, default=False, help="Create missing directories and overwrite file.")
def create(template_path: str, template_image: str, id: str, name: str, force: bool):
"""Creates a new template configuration file at the specified path."""

try:
create_example_template_config_file(template_path, template_image, id, name, force)
except OEError as err:
oe_context().io_driver.output_error(err)
finally:
oe_context().dispose()


@click.command()
@click.argument("template_path", type=click.Path(exists=True, file_okay=True, readable=True))
@click.option("--hide-features", is_flag=True, show_default=False, default=False, help="Do not visualize the locations of features.")
@click.option("--hide-keypoints", is_flag=True, show_default=False, default=False, help="Do not visualize the locations of keypoints.")
def show(template_path: str, hide_features: bool, hide_keypoints: bool):
"""Exports template as an image with features visualized."""
try:
template = load_template(template_path)
img = template.show(hide_features=hide_features, hide_keypoints=hide_keypoints)
oe_context().io_driver.output_show_result(template, img)
except OEError as err:
oe_context().io_driver.output_error(err)
finally:
oe_context().dispose()


@click.command()
@click.argument("target_path", type=click.Path(exists=True, file_okay=True, readable=True))
@click.argument("template_paths", type=click.Path(exists=True, file_okay=True, readable=True), nargs=-1)
@click.option("--workers", type=int, default=4, show_default=True)
@click.option("--show-features", is_flag=True, show_default=False, default=False, help="Visualize the locations of features.")
def test(target_path: str, template_paths: List[str], workers: int, show_features: bool):
"""Visualizes the analysis of an image using one or more templates."""

assert isinstance(oe_context().io_driver, TestIODriver)
oe_context().io_driver.visualize_features = show_features

# load target image
target = cv2.imread(target_path, cv2.IMREAD_COLOR)

try:
templates = [load_template(template_path) for template_path in template_paths]
do_analyze(target, templates, num_workers=workers)
except OEError as err:
oe_context().io_driver.output_error(err)
finally:
oe_context().dispose()


@click.command()
@click.argument("target_path", type=click.Path(exists=True, file_okay=True, readable=True))
@click.argument("template_paths", type=click.Path(exists=True, file_okay=True, readable=True), nargs=-1)
@click.option("--workers", type=int, default=4, show_default=True)
def run(target_path: str, template_paths: List[str], workers: int):
"""Applies one or more templates to an image."""

# load target image
target = cv2.imread(target_path, cv2.IMREAD_COLOR)

# overwrite the IO driver
oe_context().io_driver = RunIODriver()

try:
templates = [load_template(template_path) for template_path in template_paths]
do_analyze(target, templates, num_workers=workers)
except OEError as err:
oe_context().io_driver.output_error(err)
finally:
oe_context().dispose()


@click.command()
def homepage():
"""Go to the officialeye's official GitHub homepage."""
oe_info(f"Opening {OFFICIALEYE_GITHUB}")
click.launch(OFFICIALEYE_GITHUB)
oe_context().dispose()


@click.command()
def version():
"""Print the version of OfficialEye."""
oe_info(f"Version: {OFFICIALEYE_VERSION}")
oe_context().dispose()


cli.add_command(create)
cli.add_command(show)
cli.add_command(test)
cli.add_command(run)
cli.add_command(homepage)
cli.add_command(version)

if __name__ == "__main__":
cli()
7 changes: 5 additions & 2 deletions src/officialeye/interpretation/__init__.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import abc
from typing import Dict
from typing import Dict, TypeAlias

# noinspection PyPackageRequirements
import cv2

from officialeye.interpretation.config import InterpretationMethodConfig


Serializable: TypeAlias = dict[str, "Serializable"] | list["Serializable"] | str | int | float | bool | None


class InterpretationMethod(abc.ABC):

def __init__(self, method_id: str, config_dict: Dict[str, any], /):
Expand All @@ -20,5 +23,5 @@ def get_config(self) -> InterpretationMethodConfig:
return self._config

@abc.abstractmethod
def interpret(self, feature_img: cv2.Mat, feature_id: str, /) -> any:
def interpret(self, feature_img: cv2.Mat, feature_id: str, /) -> Serializable:
raise NotImplementedError()
1 change: 0 additions & 1 deletion src/officialeye/interpretation/interpretations/__init__.py

This file was deleted.

6 changes: 5 additions & 1 deletion src/officialeye/interpretation/loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,18 @@

from officialeye.error.errors.template import ErrTemplateInvalidInterpretation
from officialeye.interpretation import InterpretationMethod
from officialeye.interpretation.interpretations.tesseract import TesseractInterpretationMethod
from officialeye.interpretation.methods.file_temp import FileTempMethod
from officialeye.interpretation.methods.ocr_tesseract import TesseractInterpretationMethod


def load_interpretation_method(method_id: str, config_dict: Dict[str, any], /) -> InterpretationMethod:

if method_id == TesseractInterpretationMethod.METHOD_ID:
return TesseractInterpretationMethod(config_dict)

if method_id == FileTempMethod.METHOD_ID:
return FileTempMethod(config_dict)

raise ErrTemplateInvalidInterpretation(
f"while loading interpretation method '{method_id}'.",
"Unknown interpretation method id."
Expand Down
3 changes: 3 additions & 0 deletions src/officialeye/interpretation/methods/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
"""
Package containing all intepretation methods built into OfficialEye.
"""
27 changes: 27 additions & 0 deletions src/officialeye/interpretation/methods/file_temp.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import tempfile
from typing import Dict

# noinspection PyPackageRequirements
import cv2
from pytesseract import pytesseract

from officialeye.interpretation import InterpretationMethod, Serializable


class FileTempMethod(InterpretationMethod):

METHOD_ID = "file_temp"

def __init__(self, config_dict: Dict[str, any]):
super().__init__(FileTempMethod.METHOD_ID, config_dict)

self._format = self.get_config().get("format", default="png")

def interpret(self, feature_img: cv2.Mat, feature_id: str, /) -> Serializable:

with tempfile.NamedTemporaryFile(prefix="officialeye_", suffix=f".{self._format}", delete=False) as fp:
fp.close()

cv2.imwrite(fp.name, feature_img)

return fp.name
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import cv2
from pytesseract import pytesseract

from officialeye.interpretation import InterpretationMethod
from officialeye.interpretation import InterpretationMethod, Serializable


class TesseractInterpretationMethod(InterpretationMethod):
Expand All @@ -15,7 +15,7 @@ def __init__(self, config_dict: Dict[str, any]):
super().__init__(TesseractInterpretationMethod.METHOD_ID, config_dict)

self._tesseract_lang = self.get_config().get("lang", default="eng")
self._tesseract_config = self.get_config().get("config", default="--dpi 10000 --oem 3 --psm 6")
self._tesseract_config = self.get_config().get("config", default="")

def interpret(self, feature_img: cv2.Mat, feature_id: str, /) -> any:
def interpret(self, feature_img: cv2.Mat, feature_id: str, /) -> Serializable:
return pytesseract.image_to_string(feature_img, lang=self._tesseract_lang, config=self._tesseract_config).strip()
2 changes: 1 addition & 1 deletion src/officialeye/matching/matcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from officialeye.util.logger import oe_warn


class KeypointMatcher(ABC, Debuggable):
class Matcher(ABC, Debuggable):

def __init__(self, engine_id: str, template_id: str, img: cv2.Mat, /):
super().__init__()
Expand Down
Loading

0 comments on commit ddc5244

Please sign in to comment.