From 5cdf93b923f376f15f7a3ddd7967df9582db4915 Mon Sep 17 00:00:00 2001 From: Matthias Bisping Date: Fri, 10 Feb 2023 08:33:13 +0100 Subject: [PATCH] Pull request #39: RED-6084 Improve image extraction speed Merge in RR/image-prediction from RED-6084-adhoc-scanned-pages-filtering-refactoring to master Squashed commit of the following: commit bd6d83e7363b1c1993babcceb434110a6312c645 Author: Matthias Bisping Date: Thu Feb 9 16:08:25 2023 +0100 Tweak logging commit 55bdd48d2a3462a8b4a6b7194c4a46b21d74c455 Author: Matthias Bisping Date: Thu Feb 9 15:47:31 2023 +0100 Update dependencies commit 970275b25708c05e4fbe78b52aa70d791d5ff17a Author: Matthias Bisping Date: Thu Feb 9 15:35:37 2023 +0100 Refactoring Make alpha channel check monadic to streamline error handling commit e99e97e23fd8ce16f9a421d3e5442fccacf71ead Author: Matthias Bisping Date: Tue Feb 7 14:32:29 2023 +0100 Refactoring - Rename - Refactor image extraction functions commit 76b1b0ca2401495ec03ba2b6483091b52732eb81 Author: Matthias Bisping Date: Tue Feb 7 11:55:30 2023 +0100 Refactoring commit cb1c461049d7c43ec340302f466447da9f95a499 Author: Matthias Bisping Date: Tue Feb 7 11:44:01 2023 +0100 Refactoring commit 092069221a85ac7ac19bf838dcbc7ab1fde1e12b Author: Matthias Bisping Date: Tue Feb 7 10:18:53 2023 +0100 Add to-do commit 3cea4dad2d9703b8c79ddeb740b66a3b8255bb2a Author: Matthias Bisping Date: Tue Feb 7 10:11:35 2023 +0100 Refactoring - Rename - Add typehints everywhere commit 865e0819a14c420bc2edff454d41092c11c019a4 Author: Matthias Bisping Date: Mon Feb 6 19:38:57 2023 +0100 Add type explanation commit 01d3d5d33f1ccb05aea1cec1d1577572b1a4deaa Author: Matthias Bisping Date: Mon Feb 6 19:37:49 2023 +0100 Formatting commit dffe1c18fc3a322a6b08890d4438844e8122faaf Author: Matthias Bisping Date: Mon Feb 6 19:34:13 2023 +0100 [WIP] Either refactoring Add alternative formulation for monadic chain commit 066cf17add404a313520cd794c06e3264cf971c9 Author: Matthias Bisping Date: Mon Feb 6 18:40:30 2023 +0100 [WIP] Either refactoring commit f53f0fea298cdab88deb090af328b34d37e0198e Author: Matthias Bisping Date: Mon Feb 6 18:18:34 2023 +0100 [WIP] Either refactoring Propagate error and metadata commit 274a5f56d4fcb9c67fac5cf43e9412ec1ab5179e Author: Matthias Bisping Date: Mon Feb 6 17:51:35 2023 +0100 [WIP] Either refactoring Fix test assertion commit 3235a857f6e418e50484cbfff152b0f63efb2f53 Author: Matthias Bisping Date: Mon Feb 6 16:57:31 2023 +0100 [WIP] Either-refactoring Replace Maybe with Either to allow passing on error information or metadata which otherwise get sucked up by Nothing. commit 89989543d87490f8b20a0a76055605d34345e8f4 Author: Matthias Bisping Date: Mon Feb 6 16:12:40 2023 +0100 [WIP] Monadic refactoring Integrate image validation step into monadic chain. At the moment we lost the error information through this. Refactoring to Either monad can bring it back. commit 022bd4856a51aa085df5fe983fd77b99b53d594c Author: Matthias Bisping Date: Mon Feb 6 15:16:41 2023 +0100 [WIP] Monadic refactoring commit ca3898cb539607c8c3dd01c57e60211a5fea8a7d Author: Matthias Bisping Date: Mon Feb 6 15:10:34 2023 +0100 [WIP] Monadic refactoring commit d8f37bed5cbd6bdd2a0b52bae46fcdbb50f9dff2 Author: Matthias Bisping Date: Mon Feb 6 15:09:51 2023 +0100 [WIP] Monadic refactoring commit 906fee0e5df051f38076aa1d2725e52a182ade13 Author: Matthias Bisping Date: Mon Feb 6 15:03:35 2023 +0100 [WIP] Monadic refactoring ... and 35 more commits --- config.yaml | 4 +- image_prediction/default_objects.py | 15 +- image_prediction/exceptions.py | 4 + image_prediction/formatter/formatter.py | 20 -- .../image_extractor/extractors/parsable.py | 323 ++++++++++++------ image_prediction/info.py | 1 + image_prediction/locations.py | 7 +- image_prediction/pipeline.py | 8 +- .../transformer/transformers/response.py | 13 +- image_prediction/utils/generic.py | 69 ++++ requirements.txt | 2 + scripts/run_pipeline.py | 15 +- src/serve.py | 23 +- test/.gitignore | 1 + test/data.dvc | 5 + .../data/f2dc689ca794fccb8cd38b95f2bf6ba9.pdf | Bin 93305 -> 0 bytes ...ca794fccb8cd38b95f2bf6ba9_predictions.json | 44 --- test/data/stitching_with_tolerance.json | 92 ----- test/fixtures/input.py | 14 + test/fixtures/pdf.py | 12 +- test/fixtures/target.py | 2 +- test/unit_tests/image_extractor_test.py | 28 +- test/unit_tests/image_stitching_test.py | 4 +- test/utils/comparison.py | 8 +- test/utils/generation/pdf.py | 5 + 25 files changed, 395 insertions(+), 324 deletions(-) create mode 100644 test/.gitignore create mode 100644 test/data.dvc delete mode 100644 test/data/f2dc689ca794fccb8cd38b95f2bf6ba9.pdf delete mode 100644 test/data/f2dc689ca794fccb8cd38b95f2bf6ba9_predictions.json delete mode 100644 test/data/stitching_with_tolerance.json diff --git a/config.yaml b/config.yaml index 6a6111a..9bfcaf1 100644 --- a/config.yaml +++ b/config.yaml @@ -1,6 +1,6 @@ webserver: - host: $SERVER_HOST|"127.0.0.1" # webserver address - port: $SERVER_PORT|5000 # webserver port + host: $SERVER_HOST|"127.0.0.1" # Webserver address + port: $SERVER_PORT|5000 # Webserver port service: logging_level: $LOGGING_LEVEL_ROOT|INFO # Logging level for service logger diff --git a/image_prediction/default_objects.py b/image_prediction/default_objects.py index d66d477..1c40d56 100644 --- a/image_prediction/default_objects.py +++ b/image_prediction/default_objects.py @@ -1,5 +1,3 @@ -from typing import Iterable - from funcy import juxt from image_prediction.classifier.classifier import Classifier @@ -7,7 +5,6 @@ from image_prediction.classifier.image_classifier import ImageClassifier from image_prediction.compositor.compositor import TransformerCompositor from image_prediction.encoder.encoders.hash_encoder import HashEncoder from image_prediction.estimator.adapter.adapter import EstimatorAdapter -from image_prediction.formatter.formatter import format_image_plus from image_prediction.formatter.formatters.camel_case import Snake2CamelCaseKeyFormatter from image_prediction.formatter.formatters.enum import EnumFormatter from image_prediction.image_extractor.extractors.parsable import ParsablePDFImageExtractor @@ -17,7 +14,6 @@ from image_prediction.model_loader.loaders.mlflow import MlflowConnector from image_prediction.redai_adapter.mlflow import MlflowModelReader from image_prediction.transformer.transformers.coordinate.pdfnet import PDFNetCoordinateTransformer from image_prediction.transformer.transformers.response import ResponseTransformer -from pdf2img.extraction import extract_images_via_metadata def get_mlflow_model_loader(mlruns_dir): @@ -30,17 +26,10 @@ def get_image_classifier(model_loader, model_identifier): return ImageClassifier(Classifier(EstimatorAdapter(model), ProbabilityMapper(classes))) -def get_dispatched_extract(**kwargs): +def get_extractor(**kwargs): image_extractor = ParsablePDFImageExtractor(**kwargs) - def extract(pdf: bytes, page_range: range = None, metadata_per_image: Iterable[dict] = None): - if metadata_per_image: - image_pluses = extract_images_via_metadata(pdf, metadata_per_image) - yield from map(format_image_plus, image_pluses) - else: - yield from image_extractor.extract(pdf, page_range) - - return extract + return image_extractor def get_formatter(): diff --git a/image_prediction/exceptions.py b/image_prediction/exceptions.py index f03b42a..9c9ca49 100644 --- a/image_prediction/exceptions.py +++ b/image_prediction/exceptions.py @@ -36,3 +36,7 @@ class InvalidBox(Exception): class ParsingError(Exception): pass + + +class BadXref(ValueError): + pass diff --git a/image_prediction/formatter/formatter.py b/image_prediction/formatter/formatter.py index 53306a9..3f3a1f8 100644 --- a/image_prediction/formatter/formatter.py +++ b/image_prediction/formatter/formatter.py @@ -1,10 +1,6 @@ import abc -from image_prediction.image_extractor.extractor import ImageMetadataPair -from image_prediction.info import Info - from image_prediction.transformer.transformer import Transformer -from pdf2img.default_objects.image import ImagePlus class Formatter(Transformer): @@ -17,19 +13,3 @@ class Formatter(Transformer): def __call__(self, obj): return self.format(obj) - - -def format_image_plus(image: ImagePlus) -> ImageMetadataPair: - enum_metadata = { - Info.PAGE_WIDTH: image.info.pageInfo.width, - Info.PAGE_HEIGHT: image.info.pageInfo.height, - Info.PAGE_IDX: image.info.pageInfo.number, - Info.ALPHA: image.info.alpha, - Info.WIDTH: image.info.boundingBox.width, - Info.HEIGHT: image.info.boundingBox.height, - Info.X1: image.info.boundingBox.x0, - Info.X2: image.info.boundingBox.x1, - Info.Y1: image.info.boundingBox.y0, - Info.Y2: image.info.boundingBox.y1, - } - return ImageMetadataPair(image.aspil(), enum_metadata) diff --git a/image_prediction/image_extractor/extractors/parsable.py b/image_prediction/image_extractor/extractors/parsable.py index eac09e1..a911951 100644 --- a/image_prediction/image_extractor/extractors/parsable.py +++ b/image_prediction/image_extractor/extractors/parsable.py @@ -1,26 +1,32 @@ import atexit -import io -import json -import traceback from functools import partial, lru_cache -from itertools import chain, starmap, filterfalse -from operator import itemgetter, truth -from typing import List, Iterable, Iterator +from itertools import chain, filterfalse +from operator import itemgetter, attrgetter +from typing import List, Union, Iterable, Any import fitz +import numpy as np from PIL import Image -from funcy import rcompose, merge, pluck, curry, compose +from funcy import merge, compose, rcompose, keep, lkeep, notnone, iffy, rpartial +from pymonad.either import Right, Left, Either +from pymonad.tools import curry, identity +from image_prediction.exceptions import InvalidBox, BadXref from image_prediction.formatter.formatters.enum import EnumFormatter from image_prediction.image_extractor.extractor import ImageExtractor, ImageMetadataPair from image_prediction.info import Info from image_prediction.stitching.stitching import stitch_pairs -from image_prediction.stitching.utils import validate_box_coords, validate_box_size +from image_prediction.stitching.utils import validate_box from image_prediction.utils import get_logger -from image_prediction.utils.generic import lift +from image_prediction.utils.generic import bottom, left, right, lift, wrap_right logger = get_logger() +Doc = fitz.fitz.Document +Pge = fitz.fitz.Page +Img = Image.Image +Pxm = fitz.fitz.Pixmap + class ParsablePDFImageExtractor(ImageExtractor): def __init__(self, verbose=False, tolerance=0): @@ -31,7 +37,7 @@ class ParsablePDFImageExtractor(ImageExtractor): tolerance: The tolerance in pixels for the distance between images, beyond which they will not be stitched together """ - self.doc: fitz.fitz.Document = None + self.doc: Union[Doc, None] = None self.verbose = verbose self.tolerance = tolerance @@ -44,80 +50,215 @@ class ParsablePDFImageExtractor(ImageExtractor): yield from image_metadata_pairs - def __process_images_on_page(self, page: fitz.fitz.Page): - images = get_images_on_page(self.doc, page) - metadata = get_metadata_for_images_on_page(self.doc, page) + def __process_images_on_page(self, page: Pge): + metadata = extract_valid_metadata(self.doc, page) + + either_image_metadata_pair_or_error_per_image = map(self.__metadatum_to_image_metadata_pair, metadata) + valid_image_metadata_pairs = lkeep( + rpartial(take_good_log_bad, format_context), either_image_metadata_pair_or_error_per_image + ) + valid_image_metadata_pairs_stitched = stitch_pairs(valid_image_metadata_pairs, tolerance=self.tolerance) + clear_caches() - image_metadata_pairs = starmap(ImageMetadataPair, filter(all, zip(images, metadata))) - # TODO: In the future, consider to introduce an image validator as a pipeline component rather than doing the - # validation here. Invalid images can then be split into a different stream and joined with the intact images - # again for the formatting step. - image_metadata_pairs = self.__filter_valid_images(image_metadata_pairs) - image_metadata_pairs = stitch_pairs(list(image_metadata_pairs), tolerance=self.tolerance) + yield from valid_image_metadata_pairs_stitched - yield from image_metadata_pairs - - @staticmethod - def __filter_valid_images(image_metadata_pairs: Iterable[ImageMetadataPair]) -> Iterator[ImageMetadataPair]: - def validate(image: Image.Image, metadata: dict): - try: - # TODO: stand-in heuristic for testing if image is valid => find cleaner solution (RED-5148) - image.resize((100, 100)).convert("RGB") - return ImageMetadataPair(image, metadata) - except (OSError, Exception) as err: - metadata = json.dumps(EnumFormatter()(metadata), indent=2) - logger.warning(f"Invalid image encountered. Image metadata:\n{metadata}\n\n{traceback.format_exc()}") - return None - - return filter(truth, starmap(validate, image_metadata_pairs)) + def __metadatum_to_image_metadata_pair(self, metadatum: dict) -> Either: + return metadatum_to_image_metadata_pair(self.doc, metadatum) -def extract_pages(doc, page_range): +def extract_pages(doc: Doc, page_range: range) -> Iterable[Pge]: page_range = range(page_range.start + 1, page_range.stop + 1) pages = map(doc.load_page, page_range) yield from pages -@lru_cache(maxsize=None) -def get_images_on_page(doc, page: fitz.Page): - image_infos = get_image_infos(page) - xrefs = map(itemgetter("xref"), image_infos) - images = map(partial(xref_to_image, doc), xrefs) - - yield from images +def validate_image(image: Img) -> Either: + try: + # TODO: stand-in heuristic for testing if image is valid => find cleaner solution (RED-5148) + image.resize((100, 100)).convert("RGB") + return Right(image) + except (OSError, Exception): + logger.warning(f"Invalid image encountered.") + return Left("Invalid image.") -def get_metadata_for_images_on_page(doc, page: fitz.Page): +def extract_valid_metadata(doc: Doc, page: Pge) -> List[dict]: + return compose( + list, + partial(add_alpha_channel_info, doc), + filter_valid_metadata, + get_metadata_for_images_on_page, + )(page) - metadata = map(get_image_metadata, get_image_infos(page)) - metadata = validate_coords_and_passthrough(metadata) - metadata = filter_out_tiny_images(metadata) - metadata = validate_size_and_passthrough(metadata) +def take_good_log_bad(item: Either, log_formatter=identity) -> Any: + return item.either(rpartial(log_error_context, log_formatter), identity) - metadata = add_page_metadata(page, metadata) - metadata = add_alpha_channel_info(doc, page, metadata) +def format_context(context: dict) -> str: + return f"Reason: {context['reason'].rstrip('.')}. Metadata: {EnumFormatter()(context['metadata'])}" + + +def log_error_context(context: dict, formatter=identity) -> None: + logger.warning(f"Skipping bad image. {formatter(context)}") + return None + + +def metadatum_to_image_metadata_pair(doc: Doc, metadatum: dict) -> Either: + image: Either = eith_extract_image(doc, metadatum[Info.XREF]).bind(validate_image) + image_metadata_pair: Either = eith_make_image_metadata_pair(image, Right(metadatum)) + return image_metadata_pair + + +def add_alpha_channel_info(doc: Doc, metadata: Iterable[dict]) -> Iterable[dict]: + def add_alpha_value_to_metadatum(metadatum: dict) -> dict: + alpha = metadatum_to_alpha_value(metadatum) + return {**metadatum, Info.ALPHA: alpha} + + xref_to_alpha = partial(has_alpha_channel, doc) + metadatum_to_alpha_value = compose(xref_to_alpha, itemgetter(Info.XREF)) + + yield from map(add_alpha_value_to_metadatum, metadata) + + +def filter_valid_metadata(metadata: Iterable[dict]) -> Iterable[dict]: + yield from compose(filter_out_tiny_images, filter_out_invalid_metadata)(metadata) + + +def get_metadata_for_images_on_page(page: fitz.Page) -> Iterable[dict]: + metadata = compose( + partial(add_page_metadata, page), + lift(get_image_metadata), + get_image_infos, + )(page) yield from metadata @lru_cache(maxsize=None) -def get_image_infos(page: fitz.Page) -> List[dict]: - return page.get_image_info(xrefs=True) +@curry(2) +def eith_extract_image(doc: Doc, xref: int) -> Either: + try: + return Right(extract_image(doc, xref)) + except BadXref: + return Left("Bad xref.") -@lru_cache(maxsize=None) -def xref_to_image(doc, xref) -> Image: - maybe_image = load_image_handle_from_xref(doc, xref) - return Image.open(io.BytesIO(maybe_image["image"])) if maybe_image else None +def eith_make_image_metadata_pair(image: Either, metadata: Either) -> Either: + """Reference: haskell.org/tutorial/monads.html""" + + def context(value): + return {"reason": value, "metadata": metadata.either(bottom, identity)} + + # Explicitly we are doing the following. (1) and (2) are equivalent. + + # a := Image + # b := Metadata + # c := ImageMetadataPair + # m := Either monad + + # fmt: off + # 1) + # pair: Either = ( + # Right(make_image_metadata_pair) # m (a -> b -> c) + # .amap(image) # m (a -> b -> c) <*> m a = m (b -> c) + # .amap(metadata) # m (b -> c) <*> m b = m c + # ) + + # 2) + # pair: Either = ( + # image.bind(right(make_image_metadata_pair)) # m a >>= m (a -> b -> c) = m (b -> c) + # .amap(metadata) # m (b -> c) <*> m b = m c + # ) + # fmt: on + + # Syntactic sugar variant with details hidden + pair: Either = Either.apply(make_image_metadata_pair).to_arguments(image, metadata) + + return pair.either(left(context), right(identity)) -def get_image_metadata(image_info): +@curry(2) +def make_image_metadata_pair(image: Img, metadatum: dict) -> ImageMetadataPair: + return ImageMetadataPair(image, metadatum) - x1, y1, x2, y2 = map(rounder, image_info["bbox"]) + +def extract_image(doc: Doc, xref: int) -> Any: + return compose(pixmap_to_image, extract_pixmap)(doc, xref) + + +def pixmap_to_image(pixmap: Pxm) -> Img: + array = np.frombuffer(pixmap.samples, dtype=np.uint8).reshape((pixmap.h, pixmap.w, pixmap.n)) + array = normalize_channels(array) + return Image.fromarray(array) + + +def extract_pixmap(doc: Doc, xref: int) -> Pxm: + try: + return fitz.Pixmap(doc, xref) + except ValueError as err: + msg = f"Cross reference {xref} is invalid, skipping extraction." + logger.error(err) + logger.trace(msg) + raise BadXref(msg) from err + + +def has_alpha_channel(doc: Doc, xref: int) -> bool: + + _get_image_handle = wrap_right(get_image_handle, success_condition=notnone)(doc) + _extract_pixmap = wrap_right(extract_pixmap)(doc) + + def get_soft_mask_reference(cross_reference: int) -> Either: + def error(value) -> str: + return f"Invalid soft mask {value} for cross reference {cross_reference}." + + logger.trace(f"Getting soft mask handle for cross reference {cross_reference}.") + pass_on_if_not_none = iffy(notnone, right(identity), left(error)) + return _get_image_handle(cross_reference).then(itemgetter("smask")).either(left(identity), pass_on_if_not_none) + + def mask_exists(soft_mask_reference: int) -> Either: + logger.trace(f"Checking if soft mask exists for soft mask reference {soft_mask_reference}.") + return _get_image_handle(soft_mask_reference).then(notnone) + + def image_has_alpha_channel(reference: int) -> Either: + logger.trace(f"Checking if image with reference {reference} has alpha channel.") + return _extract_pixmap(reference).then(attrgetter("alpha")).then(bool) + + logger.debug(f"Checking if image with cross reference {xref} has alpha channel.") + + cross_reference = Right(xref) + soft_mask_reference = cross_reference.bind(get_soft_mask_reference) + + return any( + take_good_log_bad(reference.bind(check)) + for reference, check in [ + (soft_mask_reference, mask_exists), + (soft_mask_reference, image_has_alpha_channel), + (cross_reference, image_has_alpha_channel), + ] + ) + + +def filter_out_tiny_images(metadata: Iterable[dict]) -> Iterable[dict]: + yield from filterfalse(tiny, metadata) + + +def filter_out_invalid_metadata(metadata: Iterable[dict]) -> Iterable[dict]: + def __validate_box(box): + try: + return validate_box(box) + except InvalidBox as err: + logger.debug(f"Dropping invalid metadatum, reason: {err}") + + yield from keep(__validate_box, metadata) + + +def get_image_metadata(image_info: dict) -> dict: + + xref, coords = itemgetter("xref", "bbox")(image_info) + x1, y1, x2, y2 = map(rounder, coords) width = abs(x2 - x1) height = abs(y2 - y1) @@ -129,47 +270,40 @@ def get_image_metadata(image_info): Info.X2: x2, Info.Y1: y1, Info.Y2: y2, + Info.XREF: xref, } -def validate_coords_and_passthrough(metadata): - yield from map(validate_box_coords, metadata) +@lru_cache(maxsize=None) +def get_image_infos(page: Pge) -> List[dict]: + return page.get_image_info(xrefs=True) -def filter_out_tiny_images(metadata): - yield from filterfalse(tiny, metadata) - - -def validate_size_and_passthrough(metadata): - yield from map(validate_box_size, metadata) - - -def add_page_metadata(page, metadata): +def add_page_metadata(page: Pge, metadata: Iterable[dict]) -> Iterable[dict]: yield from map(partial(merge, get_page_metadata(page)), metadata) -def add_alpha_channel_info(doc, page, metadata): +def normalize_channels(array: np.ndarray): + if not array.ndim == 3: + array = np.expand_dims(array, axis=-1) - page_to_xrefs = compose(curry(pluck)("xref"), get_image_infos) - xref_to_alpha = partial(has_alpha_channel, doc) - page_to_alpha_value_per_image = compose(lift(xref_to_alpha), page_to_xrefs) - alpha_to_dict = compose(dict, lambda a: [(Info.ALPHA, a)]) - page_to_alpha_mapping_per_image = compose(lift(alpha_to_dict), page_to_alpha_value_per_image) + if array.shape[-1] == 4: + array = array[..., :3] + elif array.shape[-1] == 1: + array = np.concatenate([array, array, array], axis=-1) + elif array.shape[-1] != 3: + logger.warning(f"Unexpected image format: {array.shape}.") + raise ValueError(f"Unexpected image format: {array.shape}.") - metadata = starmap(merge, zip(page_to_alpha_mapping_per_image(page), metadata)) - - yield from metadata + return array @lru_cache(maxsize=None) -def load_image_handle_from_xref(doc, xref): +def get_image_handle(doc: Doc, xref: int) -> Union[dict, None]: return doc.extract_image(xref) -rounder = rcompose(round, int) - - -def get_page_metadata(page): +def get_page_metadata(page: Pge) -> dict: page_width, page_height = map(rounder, page.mediabox_size) return { @@ -179,30 +313,17 @@ def get_page_metadata(page): } -def has_alpha_channel(doc, xref): - - maybe_image = load_image_handle_from_xref(doc, xref) - maybe_smask = maybe_image["smask"] if maybe_image else None - - if maybe_smask: - return any([doc.extract_image(maybe_smask) is not None, bool(fitz.Pixmap(doc, maybe_smask).alpha)]) - else: - try: - return bool(fitz.Pixmap(doc, xref).alpha) - except ValueError: - logger.debug(f"Encountered invalid xref `{xref}` in {doc.metadata.get('title', '')}.") - return False +rounder = rcompose(round, int) -def tiny(metadata): - return metadata[Info.WIDTH] * metadata[Info.HEIGHT] <= 4 +def tiny(metadatum: dict) -> bool: + return metadatum[Info.WIDTH] * metadatum[Info.HEIGHT] <= 4 -def clear_caches(): +def clear_caches() -> None: get_image_infos.cache_clear() - load_image_handle_from_xref.cache_clear() - get_images_on_page.cache_clear() - xref_to_image.cache_clear() + get_image_handle.cache_clear() + eith_extract_image.cache_clear() atexit.register(clear_caches) diff --git a/image_prediction/info.py b/image_prediction/info.py index 344274a..987779e 100644 --- a/image_prediction/info.py +++ b/image_prediction/info.py @@ -12,3 +12,4 @@ class Info(Enum): Y1 = "y1" Y2 = "y2" ALPHA = "alpha" + XREF = "xref" diff --git a/image_prediction/locations.py b/image_prediction/locations.py index 1f14c1a..9374ace 100644 --- a/image_prediction/locations.py +++ b/image_prediction/locations.py @@ -3,15 +3,14 @@ from pathlib import Path MODULE_DIR = Path(__file__).resolve().parents[0] - PACKAGE_ROOT_DIR = MODULE_DIR.parents[0] CONFIG_FILE = PACKAGE_ROOT_DIR / "config.yaml" - BANNER_FILE = PACKAGE_ROOT_DIR / "banner.txt" DATA_DIR = PACKAGE_ROOT_DIR / "data" - MLRUNS_DIR = str(DATA_DIR / "mlruns") -TEST_DATA_DIR = PACKAGE_ROOT_DIR / "test" / "data" +TEST_DIR = PACKAGE_ROOT_DIR / "test" +TEST_DATA_DIR = TEST_DIR / "data" +TEST_DATA_DIR_DVC = TEST_DIR / "data.dvc" diff --git a/image_prediction/pipeline.py b/image_prediction/pipeline.py index f9383a1..704a88f 100644 --- a/image_prediction/pipeline.py +++ b/image_prediction/pipeline.py @@ -11,8 +11,8 @@ from image_prediction.default_objects import ( get_formatter, get_mlflow_model_loader, get_image_classifier, + get_extractor, get_encoder, - get_dispatched_extract, ) from image_prediction.locations import MLRUNS_DIR from image_prediction.utils.generic import lift, starlift @@ -41,7 +41,7 @@ class Pipeline: def __init__(self, model_loader, model_identifier, batch_size=16, verbose=True, **kwargs): self.verbose = verbose - extract = get_dispatched_extract(**kwargs) + extract = get_extractor(**kwargs) classifier = get_image_classifier(model_loader, model_identifier) reformat = get_formatter() represent = get_encoder() @@ -63,9 +63,9 @@ class Pipeline: reformat, # ... the items ) - def __call__(self, pdf: bytes, page_range: range = None, metadata_per_image: Iterable[dict] = None): + def __call__(self, pdf: bytes, page_range: range = None): yield from tqdm( - self.pipe(pdf, page_range=page_range, metadata_per_image=metadata_per_image), + self.pipe(pdf, page_range=page_range), desc="Processing images from document", unit=" images", disable=not self.verbose, diff --git a/image_prediction/transformer/transformers/response.py b/image_prediction/transformer/transformers/response.py index 378fe7b..288c510 100644 --- a/image_prediction/transformer/transformers/response.py +++ b/image_prediction/transformer/transformers/response.py @@ -21,11 +21,6 @@ class ResponseTransformer(Transformer): def build_image_info(data: dict) -> dict: - def compute_geometric_quotient(): - page_area_sqrt = math.sqrt(abs(page_width * page_height)) - image_area_sqrt = math.sqrt(abs(x2 - x1) * abs(y2 - y1)) - return image_area_sqrt / page_area_sqrt - page_width, page_height, x1, x2, y1, y2, width, height, alpha = itemgetter( "page_width", "page_height", "x1", "x2", "y1", "y2", "width", "height", "alpha" )(data) @@ -34,7 +29,7 @@ def build_image_info(data: dict) -> dict: label = classification["label"] representation = data["representation"] - geometric_quotient = round(compute_geometric_quotient(), 4) + geometric_quotient = round(compute_geometric_quotient(page_width, page_height, x2, x1, y2, y1), 4) min_image_to_page_quotient_breached = bool( geometric_quotient < get_class_specific_min_image_to_page_quotient(label) @@ -89,6 +84,12 @@ def build_image_info(data: dict) -> dict: return image_info +def compute_geometric_quotient(page_width, page_height, x2, x1, y2, y1): + page_area_sqrt = math.sqrt(abs(page_width * page_height)) + image_area_sqrt = math.sqrt(abs(x2 - x1) * abs(y2 - y1)) + return image_area_sqrt / page_area_sqrt + + def get_class_specific_min_image_to_page_quotient(label, table=None): return get_class_specific_value( "REL_IMAGE_SIZE", label, "min", CONFIG.filters.image_to_page_quotient.min, table=table diff --git a/image_prediction/utils/generic.py b/image_prediction/utils/generic.py index de71a5c..ffdf7b7 100644 --- a/image_prediction/utils/generic.py +++ b/image_prediction/utils/generic.py @@ -1,6 +1,15 @@ +from functools import wraps +from inspect import signature from itertools import starmap +from typing import Callable from funcy import iterate, first, curry, map +from pymonad.either import Left, Right, Either +from pymonad.tools import curry as pmcurry + +from image_prediction.utils import get_logger + +logger = get_logger() def until(cond, func, *args, **kwargs): @@ -13,3 +22,63 @@ def lift(fn): def starlift(fn): return curry(starmap)(fn) + + +def bottom(*args, **kwargs): + return False + + +def top(*args, **kwargs): + return True + + +def left(fn): + @wraps(fn) + def inner(x): + return Left(fn(x)) + + return inner + + +def right(fn): + @wraps(fn) + def inner(x): + return Right(fn(x)) + + return inner + + +def wrap_left(fn, success_condition=top, error_message=None) -> Callable: + return wrap_either(Left, Right, success_condition=success_condition, error_message=error_message)(fn) + + +def wrap_right(fn, success_condition=top, error_message=None) -> Callable: + return wrap_either(Right, Left, success_condition=success_condition, error_message=error_message)(fn) + + +def wrap_either(success_type, failure_type, success_condition=top, error_message=None) -> Callable: + @wraps(wrap_either) + def wrapper(fn) -> Callable: + + n_params = len(signature(fn).parameters) + + @pmcurry(n_params) + @wraps(fn) + def wrapper(*args, **kwargs) -> Either: + try: + result = fn(*args, **kwargs) + if success_condition(result): + return success_type(result) + else: + return failure_type({"error": error_message, "result": result}) + except Exception as err: + logger.error(err) + return failure_type({"error": error_message or err, "result": Void}) + + return wrapper + + return wrapper + + +class Void: + pass diff --git a/requirements.txt b/requirements.txt index da99202..3559e63 100644 --- a/requirements.txt +++ b/requirements.txt @@ -23,3 +23,5 @@ pdf2image==1.16.0 frozendict==2.3.0 protobuf<=3.20.* prometheus-client==0.13.1 +fsspec==2022.11.0 +PyMonad==2.4.0 diff --git a/scripts/run_pipeline.py b/scripts/run_pipeline.py index 29d3199..c2b4bb0 100644 --- a/scripts/run_pipeline.py +++ b/scripts/run_pipeline.py @@ -2,7 +2,6 @@ import argparse import json import os from glob import glob -from operator import truth from image_prediction.pipeline import load_pipeline from image_prediction.utils import get_logger @@ -15,7 +14,6 @@ def parse_args(): parser = argparse.ArgumentParser() parser.add_argument("input", help="pdf file or directory") - parser.add_argument("--metadata", help="optional figure detection metadata") parser.add_argument("--print", "-p", help="print output to terminal", action="store_true", default=False) parser.add_argument("--page_interval", "-i", help="page interval [i, j), min index = 0", nargs=2, type=int) @@ -24,17 +22,13 @@ def parse_args(): return args -def process_pdf(pipeline, pdf_path, metadata=None, page_range=None): - if metadata: - with open(metadata) as f: - metadata = json.load(f) - +def process_pdf(pipeline, pdf_path, page_range=None): with open(pdf_path, "rb") as f: logger.info(f"Processing {pdf_path}") - predictions = list(pipeline(f.read(), page_range=page_range, metadata_per_image=metadata)) + predictions = list(pipeline(f.read(), page_range=page_range)) annotate_pdf( - pdf_path, predictions, os.path.join("/tmp", os.path.basename(pdf_path.replace(".pdf", f"_{truth(metadata)}_annotated.pdf"))) + pdf_path, predictions, os.path.join("/tmp", os.path.basename(pdf_path.replace(".pdf", "_annotated.pdf"))) ) return predictions @@ -48,10 +42,9 @@ def main(args): else: pdf_paths = glob(os.path.join(args.input, "*.pdf")) page_range = range(*args.page_interval) if args.page_interval else None - metadata = args.metadata if args.metadata else None for pdf_path in pdf_paths: - predictions = process_pdf(pipeline, pdf_path, metadata, page_range=page_range) + predictions = process_pdf(pipeline, pdf_path, page_range=page_range) if args.print: print(pdf_path) print(json.dumps(predictions, indent=2)) diff --git a/src/serve.py b/src/serve.py index ece6a0b..a865c7d 100644 --- a/src/serve.py +++ b/src/serve.py @@ -1,5 +1,4 @@ import gzip -import io import json import logging @@ -29,36 +28,28 @@ logger.setLevel(PYINFRA_CONFIG.logging_level_root) def process_request(request_message): dossier_id = request_message["dossierId"] file_id = request_message["fileId"] + logger.info(f"Processing {dossier_id=} {file_id=} ...") target_file_name = f"{dossier_id}/{file_id}.{request_message['targetFileExtension']}" response_file_name = f"{dossier_id}/{file_id}.{request_message['responseFileExtension']}" - figure_data_file_name = f"{dossier_id}/{file_id}.FIGURE.json.gz" bucket = PYINFRA_CONFIG.storage_bucket storage = get_storage(PYINFRA_CONFIG) pipeline = load_pipeline(verbose=IMAGE_CONFIG.service.verbose, batch_size=IMAGE_CONFIG.service.batch_size) - if storage.exists(bucket, target_file_name): - should_publish_result = True + if not storage.exists(bucket, target_file_name): + publish_result = False + else: + publish_result = True object_bytes = storage.get_object(bucket, target_file_name) object_bytes = gzip.decompress(object_bytes) classifications = list(pipeline(pdf=object_bytes)) - if storage.exists(bucket, figure_data_file_name): - metadata_bytes = storage.get_object(bucket, figure_data_file_name) - metadata_bytes = gzip.decompress(metadata_bytes) - metadata_per_image = json.load(io.BytesIO(metadata_bytes))["data"] - classifications_cv = list(pipeline(pdf=object_bytes, metadata_per_image=metadata_per_image)) - else: - classifications_cv = [] - - result = {**request_message, "data": classifications, "dataCV": classifications_cv} + result = {**request_message, "data": classifications} storage_bytes = gzip.compress(json.dumps(result).encode("utf-8")) storage.put_object(bucket, response_file_name, storage_bytes) - else: - should_publish_result = False - return should_publish_result, {"dossierId": dossier_id, "fileId": file_id} + return publish_result, {"dossierId": dossier_id, "fileId": file_id} def main(): diff --git a/test/.gitignore b/test/.gitignore new file mode 100644 index 0000000..3af0ccb --- /dev/null +++ b/test/.gitignore @@ -0,0 +1 @@ +/data diff --git a/test/data.dvc b/test/data.dvc new file mode 100644 index 0000000..c7040fe --- /dev/null +++ b/test/data.dvc @@ -0,0 +1,5 @@ +outs: +- md5: 4b0fec291ce0661b3efbbd8b80f4f514.dir + size: 107332 + nfiles: 4 + path: data diff --git a/test/data/f2dc689ca794fccb8cd38b95f2bf6ba9.pdf b/test/data/f2dc689ca794fccb8cd38b95f2bf6ba9.pdf deleted file mode 100644 index 41f0d70c122d660772f60239ca6ec434027d78d5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 93305 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