Skip to content

Commit

Permalink
Version 1.1.4
Browse files Browse the repository at this point in the history
  • Loading branch information
ZeroBone committed Jan 21, 2024
1 parent 12bc47c commit 8b0e60e
Show file tree
Hide file tree
Showing 38 changed files with 251 additions and 84 deletions.
13 changes: 13 additions & 0 deletions docs/dev/changelog.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
# Changelog

## Release 1.1.4 (beta)

* Added a new `rotate` mutator, that rotates an image by a multiple of 90 degrees.
* Added a new `file` interpretation method that is similar to `file_temp`, but can save a file at the specified path.
* Improved the error handling system.
* Fixed multiple bugs, including one critical bug.
* Added a possibility of using external errors as causes of OfficialEye errors.
* Added support for non-shape-preserving mutators.
* Refactor.

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

## Release 1.1.3 (beta)

* The `tesseract_ocr` interpretation method no longer has default Tesseract OCR configuration values predefined.
Expand All @@ -11,6 +23,7 @@
* Rewritten the context and context management system, making the API much closer to being stable.
* Other substrantial refactor.
* Other architecture improvements.
* Fixed numerous typos and inconsistencies in comments and strings.

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

Expand Down
2 changes: 1 addition & 1 deletion src/officialeye/__version__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
__title__ = "officialeye"
__description__ = "An AI-powered generic document-analysis tool."
__url__ = "https://officialeye.zerobone.net"
__version__ = "1.1.3"
__version__ = "1.1.4"
__author__ = "Alexander Mayorov"
__author_email__ = "[email protected]"
__license__ = "GPL-3.0"
Expand Down
14 changes: 9 additions & 5 deletions src/officialeye/_internal/context/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,10 @@
from typing import TYPE_CHECKING

import click
# noinspection PyPackageRequirements
import cv2
# noinspection PyPackageRequirements
import z3

from officialeye._internal.error.error import OEError
from officialeye._internal.error.errors.internal import ErrInternal
from officialeye._internal.error.errors.template import ErrTemplateIdNotUnique
from officialeye._internal.logger.singleton import get_logger
Expand Down Expand Up @@ -39,9 +38,6 @@ def __init__(self, manager, /, *, visualization_generation: bool = False):
# values: template
self._loaded_templates: Dict[str, Template] = {}

# make the logger inherit manager's settings
self._manager.configure_logger(get_logger())

z3.set_param("parallel.enable", True)

def visualization_generation_enabled(self) -> bool:
Expand Down Expand Up @@ -71,6 +67,14 @@ def add_template(self, template: Template, /):

self._loaded_templates[template.template_id] = template

try:
template.validate()
except OEError as err:
# rollback the loaded template
del self._loaded_templates[template.template_id]
# reraise the cause
raise err

def get_template(self, template_id: str, /) -> Template:
assert template_id in self._loaded_templates, "Unknown template id"
return self._loaded_templates[template_id]
Expand Down
12 changes: 7 additions & 5 deletions src/officialeye/_internal/context/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ def __init__(self, /, *, handle_exceptions: bool = True, visualization_generatio

self._context: Union[Context, None] = None

self._handle_exceptions = handle_exceptions
self.handle_exceptions = handle_exceptions

self.visualization_generation = visualization_generation

Expand Down Expand Up @@ -41,7 +41,7 @@ def __exit__(self, exception_type: any, exception_value: BaseException, tracebac

self._context.dispose()

if not self._handle_exceptions:
if not self.handle_exceptions:
return

# handle the possible exception
Expand All @@ -51,14 +51,16 @@ def __exit__(self, exception_type: any, exception_value: BaseException, tracebac

if isinstance(exception_value, OEError):
oe_error = exception_value
else:
elif isinstance(exception_value, BaseException):
oe_error = ErrInternal(
"while leaving an officialeye context.",
"An internal error occurred.",
external_cause=exception_value
)
oe_error.add_external_cause(exception_value)
else:
assert False

self._context.get_io_driver().output_error(oe_error)
self._context.get_io_driver().handle_error(oe_error)

# tell python that we have handled the exception ourselves
return True
23 changes: 20 additions & 3 deletions src/officialeye/_internal/error/error.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
from typing import List


class OEError(Exception):
"""
Base class for all officialeye-related errors.
Expand All @@ -21,11 +24,22 @@ def __init__(self, module: str, code: int, code_text: str, while_text: str, prob
# on the other hand, the template configuration file does not count as end user input
self.is_regular = is_regular

self._causes = []
self._causes: List[OEError] = []
self._external_causes: List[BaseException] = []

def add_cause(self, cause: Exception, /):
def add_cause(self, cause, /):
assert isinstance(cause, OEError)
self._causes.append(cause)

def get_causes(self):
return self._causes

def add_external_cause(self, cause: BaseException, /):
self._external_causes.append(cause)

def get_external_causes(self) -> List[BaseException]:
return self._external_causes

def serialize(self) -> dict:

causes = [
Expand All @@ -40,5 +54,8 @@ def serialize(self) -> dict:
"while_text": self.while_text,
"problem_text": self.problem_text,
"is_regular": self.is_regular,
"causes": causes
"causes": causes,
"external_causes": [
str(ec) for ec in self._external_causes
]
}
4 changes: 1 addition & 3 deletions src/officialeye/_internal/error/errors/internal.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,5 @@

class ErrInternal(OEError):

def __init__(self, while_text: str, problem_text: str, /, *, external_cause: Union[BaseException, None] = None):
def __init__(self, while_text: str, problem_text: str, /):
super().__init__(ERR_MODULE_INTERNAL, ERR_INTERNAL[0], ERR_INTERNAL[1], while_text, problem_text, is_regular=False)

self.external_cause = external_cause
4 changes: 4 additions & 0 deletions src/officialeye/_internal/interpretation/loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from officialeye._internal.context.context import Context
from officialeye._internal.error.errors.template import ErrTemplateInvalidInterpretation
from officialeye._internal.interpretation.method import InterpretationMethod
from officialeye._internal.interpretation.methods.file import FileMethod
from officialeye._internal.interpretation.methods.file_temp import FileTempMethod
from officialeye._internal.interpretation.methods.ocr_tesseract import TesseractMethod

Expand All @@ -15,6 +16,9 @@ def load_interpretation_method(context: Context, method_id: str, config_dict: Di
if method_id == FileTempMethod.METHOD_ID:
return FileTempMethod(context, config_dict)

if method_id == FileMethod.METHOD_ID:
return FileMethod(context, config_dict)

raise ErrTemplateInvalidInterpretation(
f"while loading interpretation method '{method_id}'.",
"Unknown interpretation method id."
Expand Down
5 changes: 2 additions & 3 deletions src/officialeye/_internal/interpretation/method.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import abc
from typing import Dict

# noinspection PyPackageRequirements
import cv2

from officialeye._internal.context.context import Context
Expand All @@ -14,14 +13,14 @@ class InterpretationMethod(abc.ABC):
def __init__(self, context: Context, method_id: str, config_dict: Dict[str, any], /):
super().__init__()

self._context = context
self.method_id = method_id

self._config = InterpretationMethodConfig(config_dict, method_id)
self._context = context

def get_config(self) -> InterpretationMethodConfig:
return self._config

@abc.abstractmethod
def interpret(self, feature_img: cv2.Mat, feature_id: str, /) -> Serializable:
def interpret(self, feature_img: cv2.Mat, template_id: str, feature_id: str, /) -> Serializable:
raise NotImplementedError()
35 changes: 35 additions & 0 deletions src/officialeye/_internal/interpretation/methods/file.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
from typing import Dict

import cv2

from officialeye._internal.context.context import Context
from officialeye._internal.error.errors.template import ErrTemplateInvalidInterpretation
from officialeye._internal.interpretation.method import InterpretationMethod
from officialeye._internal.interpretation.serializable import Serializable


class FileMethod(InterpretationMethod):

METHOD_ID = "file"

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

self._path = self.get_config().get("path")

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

feature = self._context.get_template(template_id).get_feature(feature_id)

feature_class_generator = feature.get_feature_class().get_features()

# check if the generator generates at least two elements
if sum(1 for _ in zip(range(2), feature_class_generator)) == 2:
raise ErrTemplateInvalidInterpretation(
"while applying the '{FileMethod.METHOD_ID}' interpretation method.",
f"This method cannot be applied if there are at least two features inheriting the feature class defining this method."
)

cv2.imwrite(self._path, feature_img)

return None
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import tempfile
from typing import Dict

# noinspection PyPackageRequirements
import cv2

from officialeye._internal.context.context import Context
Expand All @@ -18,7 +17,7 @@ def __init__(self, context: Context, config_dict: Dict[str, any]):

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

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

with tempfile.NamedTemporaryFile(prefix="officialeye_", suffix=f".{self._format}", delete=False) as fp:
fp.close()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
from typing import Dict

# noinspection PyPackageRequirements
import cv2
from pytesseract import pytesseract

Expand All @@ -19,5 +18,5 @@ def __init__(self, context: Context, config_dict: Dict[str, any]):
self._tesseract_lang = self.get_config().get("lang", default="eng")
self._tesseract_config = self.get_config().get("config", default="")

def interpret(self, feature_img: cv2.Mat, feature_id: str, /) -> Serializable:
def interpret(self, feature_img: cv2.Mat, template_id: str, feature_id: str, /) -> Serializable:
return pytesseract.image_to_string(feature_img, lang=self._tesseract_lang, config=self._tesseract_config).strip()
7 changes: 3 additions & 4 deletions src/officialeye/_internal/io/driver.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import abc
from abc import ABC

# noinspection PyPackageRequirements
import cv2

from officialeye._internal.context.context import Context
Expand All @@ -16,13 +15,13 @@ def __init__(self, context: Context):
self._context = context

@abc.abstractmethod
def output_supervision_result(self, target: cv2.Mat, result: SupervisionResult, /):
def handle_supervision_result(self, target: cv2.Mat, result: SupervisionResult, /):
raise NotImplementedError()

@abc.abstractmethod
def output_show_result(self, template: Template, img: cv2.Mat, /):
def handle_show_result(self, template: Template, img: cv2.Mat, /):
raise NotImplementedError()

@abc.abstractmethod
def output_error(self, error: OEError, /):
def handle_error(self, error: OEError, /):
raise NotImplementedError()
9 changes: 3 additions & 6 deletions src/officialeye/_internal/io/drivers/run.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import json
import sys

# noinspection PyPackageRequirements
import cv2

from officialeye._internal.context.context import Context
Expand All @@ -23,19 +22,19 @@ class RunIODriver(IODriver):
def __init__(self, context: Context, /):
super().__init__(context)

def output_show_result(self, template: Template, img: cv2.Mat, /):
def handle_show_result(self, template: Template, img: cv2.Mat, /):
raise ErrIOOperationNotSupportedByDriver(
f"while trying to output the result of showing the template '{template.template_id}'",
f"Driver 'run' does not support this operation."
)

def output_error(self, error: OEError, /):
def handle_error(self, error: OEError, /):
_output_dict({
"ok": False,
"err": error.serialize()
})

def output_supervision_result(self, target: cv2.Mat, result: SupervisionResult, /):
def handle_supervision_result(self, target: cv2.Mat, result: SupervisionResult, /):

assert result is not None

Expand All @@ -52,9 +51,7 @@ def output_supervision_result(self, target: cv2.Mat, result: SupervisionResult,
continue

feature_img = result.get_feature_warped_region(target, feature)

feature_img_mutated = feature.apply_mutators_to_image(feature_img)

interpretation = feature.interpret_image(feature_img_mutated)

interpretation_dict[feature.region_id] = interpretation
Expand Down
21 changes: 16 additions & 5 deletions src/officialeye/_internal/io/drivers/test.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
# noinspection PyPackageRequirements
import cv2

from officialeye._internal.context.context import Context
Expand All @@ -16,7 +15,7 @@ def __init__(self, context: Context, /):

self.visualize_features: bool = True

def output_supervision_result(self, target: cv2.Mat, result: SupervisionResult, /):
def handle_supervision_result(self, target: cv2.Mat, result: SupervisionResult, /):

assert result is not None

Expand All @@ -27,8 +26,20 @@ def output_supervision_result(self, target: cv2.Mat, result: SupervisionResult,
# extract the features from the target image
for feature in template.features():
feature_img = result.get_feature_warped_region(target, feature)

feature_img_mutated = feature.apply_mutators_to_image(feature_img)
feature.insert_into_image(application_image, feature_img_mutated)

if feature_img.shape == feature_img_mutated.shape:
# mutators didn't change the shape of the image
feature.insert_into_image(application_image, feature_img_mutated)
else:
# some mutator has altered the shape of the feature image.
# this means that we can no longer safely insert the mutated feature into the visualization.
# therefore, we have to fall back to inserting the feature image unmutated
get_logger().warn(f"Could not visualize the '{feature.region_id}' feature of the '{feature.get_template().template_id}' template, "
f"because one of the mutators (corresponding to this feature) did not preserve the shape of the image. "
f"Falling back to the non-mutated version of the feature image.")
feature.insert_into_image(application_image, feature_img)

if self.visualize_features:
# visualize features on the image
Expand All @@ -37,8 +48,8 @@ def output_supervision_result(self, target: cv2.Mat, result: SupervisionResult,

self._context.export_primary_image(application_image, file_name="supervision_result.png")

def output_show_result(self, template: Template, img: cv2.Mat, /):
def handle_show_result(self, template: Template, img: cv2.Mat, /):
self._context.export_primary_image(img, file_name=f"{template.template_id}.png")

def output_error(self, error: OEError, /):
def handle_error(self, error: OEError, /):
get_logger().error_oe_error(error)
Loading

0 comments on commit 8b0e60e

Please sign in to comment.