Download inference/src/ax_doclayout/preprocess.py from AutomatosX/AXDocLayout-M-v1.2-CUDA-TF32: direct link, hf CLI and curl.
- Browser
- Download file 5.1 kB
-
https://huggingface.co/AutomatosX/AXDocLayout-M-v1.2-CUDA-TF32/resolve/main/inference/src/ax_doclayout/preprocess.py
- Command line
-
hf download hf://AutomatosX/AXDocLayout-M-v1.2-CUDA-TF32/inference/src/ax_doclayout/preprocess.py
-
curl -L -o preprocess.py https://huggingface.co/AutomatosX/AXDocLayout-M-v1.2-CUDA-TF32/resolve/main/inference/src/ax_doclayout/preprocess.py
5.1 kB
| # Copyright 2026 AutomatosX | |
| # SPDX-License-Identifier: AGPL-3.0-only | |
| # Modified 2026-10-17 for v1.2; prior component notices are in NOTICE. | |
| """Public ``rgb-letterbox-1024/1`` geometry. Independent of any detector graph. | |
| This is the signed AXDocLayout-M v1.2 preprocess: HWC uint8 RGB, bilinear resize of the | |
| longer side to 1024, white pad, odd remainder bottom/right, then inverse | |
| mapping of letterbox-space boxes back onto the original raster. | |
| It is not a DocLayout-YOLO drop-in. That incumbent uses a different colour | |
| order, a different pad value, and a different class vocabulary. Those | |
| differences are left unmapped on purpose. | |
| """ | |
| from __future__ import annotations | |
| from dataclasses import dataclass | |
| import numpy as np | |
| from PIL import Image | |
| from ax_doclayout.contract import PREPROCESS_ID, PageImage | |
| from ax_doclayout.errors import InvalidPageImageError | |
| SIZE = 1024 | |
| PAD_RGB = (255, 255, 255) | |
| FILL_SCALE = 1.0 / 255.0 | |
| class Letterbox: | |
| """One 1024-square canvas plus the exact inverse-mapping terms.""" | |
| canvas: np.ndarray | |
| scale: float | |
| pad_left: int | |
| pad_top: int | |
| pad_right: int | |
| pad_bottom: int | |
| size: int = SIZE | |
| def __post_init__(self) -> None: | |
| canvas = np.ascontiguousarray(self.canvas) | |
| if canvas.ndim != 3 or canvas.shape != (self.size, self.size, 3): | |
| raise InvalidPageImageError("letterbox canvas must be size x size x 3") | |
| if canvas.dtype != np.uint8: | |
| raise InvalidPageImageError("letterbox canvas must be uint8") | |
| object.__setattr__(self, "canvas", canvas) | |
| def letterbox_rgb(rgb: np.ndarray, *, size: int = SIZE) -> Letterbox: | |
| """Letterbox a decoded HWC uint8 RGB array under ``rgb-letterbox-1024/1``.""" | |
| if not isinstance(rgb, np.ndarray): | |
| raise InvalidPageImageError("rgb must be a numpy array") | |
| if rgb.ndim != 3 or rgb.shape[2] != 3: | |
| raise InvalidPageImageError("rgb must be HWC with three channels") | |
| if rgb.dtype != np.uint8: | |
| raise InvalidPageImageError("rgb must be uint8") | |
| height, width = (int(rgb.shape[0]), int(rgb.shape[1])) | |
| if height < 1 or width < 1: | |
| raise InvalidPageImageError("rgb must not be empty") | |
| scale = size / max(width, height) | |
| new_width = max(1, round(width * scale)) | |
| new_height = max(1, round(height * scale)) | |
| image = Image.fromarray(np.ascontiguousarray(rgb), mode="RGB") | |
| resized = image.resize((new_width, new_height), Image.BILINEAR) | |
| pad_width = size - new_width | |
| pad_height = size - new_height | |
| pad_left = pad_width // 2 | |
| pad_right = pad_width - pad_left | |
| pad_top = pad_height // 2 | |
| pad_bottom = pad_height - pad_top | |
| if pad_right < 0 or pad_bottom < 0: | |
| raise InvalidPageImageError("letterbox overflow") | |
| canvas = Image.new("RGB", (size, size), PAD_RGB) | |
| canvas.paste(resized, (pad_left, pad_top)) | |
| return Letterbox( | |
| canvas=np.asarray(canvas, dtype=np.uint8), | |
| scale=float(scale), | |
| pad_left=int(pad_left), | |
| pad_top=int(pad_top), | |
| pad_right=int(pad_right), | |
| pad_bottom=int(pad_bottom), | |
| size=int(size), | |
| ) | |
| def letterbox_page(page: PageImage, *, size: int = SIZE) -> Letterbox: | |
| """Letterbox a validated ``PageImage``.""" | |
| return letterbox_rgb(page.rgb, size=size) | |
| def letterbox_nchw(letterbox: Letterbox) -> np.ndarray: | |
| """Return a batch-1 NCHW float32 tensor in ``[0, 1]``.""" | |
| chw = np.transpose(letterbox.canvas, (2, 0, 1)).astype(np.float32) | |
| chw *= np.float32(FILL_SCALE) | |
| return np.ascontiguousarray(chw[None, ...]) | |
| def inverse_letterbox_box( | |
| box: tuple[float, float, float, float] | list[float], | |
| letterbox: Letterbox, | |
| *, | |
| width_px: int, | |
| height_px: int, | |
| ) -> tuple[float, float, float, float] | None: | |
| """Map a letterbox xyxy box onto the original raster, or drop it.""" | |
| if width_px < 1 or height_px < 1: | |
| return None | |
| try: | |
| x_min, y_min, x_max, y_max = (float(value) for value in box) | |
| except (TypeError, ValueError): | |
| return None | |
| if not all(np.isfinite(value) for value in (x_min, y_min, x_max, y_max)): | |
| return None | |
| scale = letterbox.scale | |
| if scale <= 0.0: | |
| return None | |
| mapped = ( | |
| (x_min - letterbox.pad_left) / scale, | |
| (y_min - letterbox.pad_top) / scale, | |
| (x_max - letterbox.pad_left) / scale, | |
| (y_max - letterbox.pad_top) / scale, | |
| ) | |
| clipped_x_min = min(max(mapped[0], 0.0), float(width_px)) | |
| clipped_y_min = min(max(mapped[1], 0.0), float(height_px)) | |
| clipped_x_max = min(max(mapped[2], 0.0), float(width_px)) | |
| clipped_y_max = min(max(mapped[3], 0.0), float(height_px)) | |
| if clipped_x_min >= clipped_x_max or clipped_y_min >= clipped_y_max: | |
| return None | |
| return clipped_x_min, clipped_y_min, clipped_x_max, clipped_y_max | |
| def orig_target_sizes(letterbox: Letterbox) -> np.ndarray: | |
| """Sizes fed to the exported graph. The graph is bound to the letterbox square.""" | |
| side = np.int64(letterbox.size) | |
| return np.ascontiguousarray([[side, side]]) | |
| PREPROCESS = PREPROCESS_ID | |