From ddc5244f74a4d7bde79a12e0fa5cfccca3eb289c Mon Sep 17 00:00:00 2001 From: ZeroBone Date: Wed, 17 Jan 2024 11:58:02 +0100 Subject: [PATCH] Added new `file_temp` interpretation method, improved documentation, refactor. --- docs/dev/changelog.md | 16 ++ docs/usage/getting-started/index.md | 44 +++-- mkdocs.yml | 2 + pyproject.toml | 4 +- src/officialeye/__init__.py | 154 +++++++++++++++++- src/officialeye/interpretation/__init__.py | 7 +- .../interpretations/__init__.py | 1 - src/officialeye/interpretation/loader.py | 6 +- .../interpretation/methods/__init__.py | 3 + .../interpretation/methods/file_temp.py | 27 +++ .../tesseract.py => methods/ocr_tesseract.py} | 6 +- src/officialeye/matching/matcher.py | 2 +- .../matching/matchers/sift_flann.py | 33 ++-- src/officialeye/meta.py | 4 +- src/officialeye/officialeye.py | 149 ----------------- src/officialeye/template/template.py | 10 +- src/officialeye/util/cv2_utils.py | 18 -- 17 files changed, 275 insertions(+), 211 deletions(-) delete mode 100644 src/officialeye/interpretation/interpretations/__init__.py create mode 100644 src/officialeye/interpretation/methods/__init__.py create mode 100644 src/officialeye/interpretation/methods/file_temp.py rename src/officialeye/interpretation/{interpretations/tesseract.py => methods/ocr_tesseract.py} (85%) delete mode 100644 src/officialeye/officialeye.py delete mode 100644 src/officialeye/util/cv2_utils.py diff --git a/docs/dev/changelog.md b/docs/dev/changelog.md index 364d2c5..e4755aa 100644 --- a/docs/dev/changelog.md +++ b/docs/dev/changelog.md @@ -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. diff --git a/docs/usage/getting-started/index.md b/docs/usage/getting-started/index.md index eb8e609..9339f00 100644 --- a/docs/usage/getting-started/index.md +++ b/docs/usage/getting-started/index.md @@ -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 diff --git a/mkdocs.yml b/mkdocs.yml index ac30942..d744b49 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -89,6 +89,8 @@ markdown_extensions: - pymdownx.details - pymdownx.superfences - pymdownx.mark + - pymdownx.tabbed: + alternate_style: true - attr_list - md_in_html - pymdownx.emoji: diff --git a/pyproject.toml b/pyproject.toml index 861fa9f..204a3cd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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 = "zb@zerobone.net"}, @@ -27,7 +27,7 @@ classifiers = [ ] [project.scripts] -officialeye = "officialeye.officialeye:cli" +officialeye = "officialeye:cli" [project.urls] Homepage = "https://github.com/ZeroBone/OfficialEye" diff --git a/src/officialeye/__init__.py b/src/officialeye/__init__.py index 72f26f5..791f415 100644 --- a/src/officialeye/__init__.py +++ b/src/officialeye/__init__.py @@ -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() diff --git a/src/officialeye/interpretation/__init__.py b/src/officialeye/interpretation/__init__.py index d4a9bd3..33c6b45 100644 --- a/src/officialeye/interpretation/__init__.py +++ b/src/officialeye/interpretation/__init__.py @@ -1,5 +1,5 @@ import abc -from typing import Dict +from typing import Dict, TypeAlias # noinspection PyPackageRequirements import cv2 @@ -7,6 +7,9 @@ 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], /): @@ -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() diff --git a/src/officialeye/interpretation/interpretations/__init__.py b/src/officialeye/interpretation/interpretations/__init__.py deleted file mode 100644 index 6a68ecd..0000000 --- a/src/officialeye/interpretation/interpretations/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# Comment needed to avoid this file being empty diff --git a/src/officialeye/interpretation/loader.py b/src/officialeye/interpretation/loader.py index 025f348..18234a7 100644 --- a/src/officialeye/interpretation/loader.py +++ b/src/officialeye/interpretation/loader.py @@ -2,7 +2,8 @@ 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: @@ -10,6 +11,9 @@ def load_interpretation_method(method_id: str, config_dict: Dict[str, any], /) - 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." diff --git a/src/officialeye/interpretation/methods/__init__.py b/src/officialeye/interpretation/methods/__init__.py new file mode 100644 index 0000000..53c9fbd --- /dev/null +++ b/src/officialeye/interpretation/methods/__init__.py @@ -0,0 +1,3 @@ +""" +Package containing all intepretation methods built into OfficialEye. +""" \ No newline at end of file diff --git a/src/officialeye/interpretation/methods/file_temp.py b/src/officialeye/interpretation/methods/file_temp.py new file mode 100644 index 0000000..06ea40d --- /dev/null +++ b/src/officialeye/interpretation/methods/file_temp.py @@ -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 diff --git a/src/officialeye/interpretation/interpretations/tesseract.py b/src/officialeye/interpretation/methods/ocr_tesseract.py similarity index 85% rename from src/officialeye/interpretation/interpretations/tesseract.py rename to src/officialeye/interpretation/methods/ocr_tesseract.py index 32350ea..f3bcbbf 100644 --- a/src/officialeye/interpretation/interpretations/tesseract.py +++ b/src/officialeye/interpretation/methods/ocr_tesseract.py @@ -4,7 +4,7 @@ import cv2 from pytesseract import pytesseract -from officialeye.interpretation import InterpretationMethod +from officialeye.interpretation import InterpretationMethod, Serializable class TesseractInterpretationMethod(InterpretationMethod): @@ -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() diff --git a/src/officialeye/matching/matcher.py b/src/officialeye/matching/matcher.py index d70af02..c074e5d 100644 --- a/src/officialeye/matching/matcher.py +++ b/src/officialeye/matching/matcher.py @@ -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__() diff --git a/src/officialeye/matching/matchers/sift_flann.py b/src/officialeye/matching/matchers/sift_flann.py index d1975c1..e7ce44c 100644 --- a/src/officialeye/matching/matchers/sift_flann.py +++ b/src/officialeye/matching/matchers/sift_flann.py @@ -4,18 +4,18 @@ from officialeye.error.errors.matching import ErrMatchingInvalidEngineConfig from officialeye.matching.match import Match -from officialeye.matching.matcher import KeypointMatcher +from officialeye.matching.matcher import Matcher from officialeye.matching.result import KeypointMatchingResult _FLANN_INDEX_KDTREE = 1 -class SiftFlannKeypointMatcher(KeypointMatcher): +class SiftFlannMatcher(Matcher): ENGINE_ID = "sift_flann" def __init__(self, template_id: str, img: cv2.Mat, /): - super().__init__(SiftFlannKeypointMatcher.ENGINE_ID, template_id, img) + super().__init__(SiftFlannMatcher.ENGINE_ID, template_id, img) def _preprocess_sensitivity(value: any) -> float: @@ -23,13 +23,13 @@ def _preprocess_sensitivity(value: any) -> float: if value < 0.0: raise ErrMatchingInvalidEngineConfig( - f"while loading the '{SiftFlannKeypointMatcher.ENGINE_ID}' keypoint matcher", + f"while loading the '{SiftFlannMatcher.ENGINE_ID}' keypoint matcher", f"The `sensitivity` value ({self._sensitivity}) cannot be negative." ) if value > 1.0: raise ErrMatchingInvalidEngineConfig( - f"while loading the '{SiftFlannKeypointMatcher.ENGINE_ID}' keypoint matcher", + f"while loading the '{SiftFlannMatcher.ENGINE_ID}' keypoint matcher", f"The `sensitivity` value ({self._sensitivity}) cannot exceed 1.0." ) @@ -72,19 +72,22 @@ def match_keypoint(self, pattern: cv2.Mat, keypoint_id: str, /): # filter matches for i, (m, n) in enumerate(matches): - if m.distance < self._sensitivity * n.distance: - matches_mask[i] = [1, 0] - pattern_point = keypoints_pattern[m.queryIdx].pt - target_point = self._keypoints_target[m.trainIdx].pt + if m.distance >= self._sensitivity * n.distance: + continue - # maybe one should consider rounding values here, instead of simply stripping the floating-point part - pattern_point = np.array(pattern_point, dtype=int) - target_point = np.array(target_point, dtype=int) + matches_mask[i] = [1, 0] - match = Match(self.template_id, keypoint_id, pattern_point, target_point) - match.set_score(self._sensitivity * n.distance - m.distance) - self._result.add_match(match) + pattern_point = keypoints_pattern[m.queryIdx].pt + target_point = self._keypoints_target[m.trainIdx].pt + + # maybe one should consider rounding values here, instead of simply stripping the floating-point part + pattern_point = np.array(pattern_point, dtype=int) + target_point = np.array(target_point, dtype=int) + + match = Match(self.template_id, keypoint_id, pattern_point, target_point) + match.set_score(self._sensitivity * n.distance - m.distance) + self._result.add_match(match) if self.in_debug_mode(): # noinspection PyTypeChecker diff --git a/src/officialeye/meta.py b/src/officialeye/meta.py index a13f9e6..83b5878 100644 --- a/src/officialeye/meta.py +++ b/src/officialeye/meta.py @@ -1,8 +1,10 @@ import click +__version__ = "1.1.3" + OFFICIALEYE_NAME = "OfficialEye" OFFICIALEYE_GITHUB = "https://github.com/ZeroBone/OfficialEye" -OFFICIALEYE_VERSION = "1.1.2" +OFFICIALEYE_VERSION = __version__ OFFICIALEYE_CLI_LOGO = """ ____ _________ _ __ ______ / __ \\/ __/ __(_)____(_)___ _/ / / ____/_ _____ / / / / /_/ /_/ / ___/ / __ `/ / / __/ / / / / _ \\ diff --git a/src/officialeye/officialeye.py b/src/officialeye/officialeye.py deleted file mode 100644 index 6e66814..0000000 --- a/src/officialeye/officialeye.py +++ /dev/null @@ -1,149 +0,0 @@ -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: v{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() diff --git a/src/officialeye/template/template.py b/src/officialeye/template/template.py index c2853f0..23ea9bf 100644 --- a/src/officialeye/template/template.py +++ b/src/officialeye/template/template.py @@ -9,8 +9,8 @@ from officialeye.error.errors.io import ErrIOInvalidPath from officialeye.error.errors.template import ErrTemplateInvalidSupervisionEngine, ErrTemplateInvalidMatchingEngine, ErrTemplateInvalidKeypoint, \ ErrTemplateInvalidFeature -from officialeye.matching.matcher import KeypointMatcher -from officialeye.matching.matchers.sift_flann import SiftFlannKeypointMatcher +from officialeye.matching.matcher import Matcher +from officialeye.matching.matchers.sift_flann import SiftFlannMatcher from officialeye.matching.result import KeypointMatchingResult from officialeye.mutator.loader import load_mutator_from_dict from officialeye.mutator.mutator import Mutator @@ -117,11 +117,11 @@ def get_matching_config(self) -> dict: def get_feature_classes(self) -> FeatureClassManager: return self._feature_class_manager - def load_keypoint_matcher(self, target_img: cv2.Mat, /) -> KeypointMatcher: + def load_keypoint_matcher(self, target_img: cv2.Mat, /) -> Matcher: matching_engine = self.get_matching_engine() - if matching_engine == SiftFlannKeypointMatcher.ENGINE_ID: - return SiftFlannKeypointMatcher(self.template_id, target_img) + if matching_engine == SiftFlannMatcher.ENGINE_ID: + return SiftFlannMatcher(self.template_id, target_img) raise ErrTemplateInvalidMatchingEngine( "while loading keypoint matcher", diff --git a/src/officialeye/util/cv2_utils.py b/src/officialeye/util/cv2_utils.py deleted file mode 100644 index 320e44a..0000000 --- a/src/officialeye/util/cv2_utils.py +++ /dev/null @@ -1,18 +0,0 @@ -import cv2 - - -def remove_noise(img): - # blur - blur = cv2.GaussianBlur(img, (0, 0), sigmaX=33, sigmaY=33) - - # divide - divide = cv2.divide(img, blur, scale=255) - - # otsu threshold - thresh = cv2.threshold(divide, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)[1] - - # apply morphology - kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3)) - morph = cv2.morphologyEx(thresh, cv2.MORPH_CLOSE, kernel) - - return morph