| """ |
| ROI detection: full passport image -> bounding box of the dotted series/number strip. |
| Uses a YOLO model fine-tuned on Ukrainian passport images (class: "Dotted"). |
| """ |
|
|
| from pathlib import Path |
| import cv2 |
| import numpy as np |
|
|
|
|
| def load_detector(model_path: str | Path): |
| from ultralytics import YOLO |
| return YOLO(str(model_path)) |
|
|
|
|
| def detect_roi(image: np.ndarray, model, conf: float = 0.30) -> tuple[int, int, int, int] | None: |
| """ |
| Detect the dotted passport series/number strip in a full passport image. |
| |
| Args: |
| image: BGR image (numpy array) or grayscale — will be converted as needed. |
| model: loaded YOLO model (from load_detector). |
| conf: minimum confidence threshold. |
| |
| Returns: |
| (x1, y1, x2, y2) pixel coordinates of the best detection, |
| or None if nothing found. |
| """ |
| results = model(image, conf=conf, verbose=False) |
| boxes = results[0].boxes |
|
|
| if boxes is None or len(boxes) == 0: |
| return None |
|
|
| |
| best = boxes[boxes.conf.argmax()] |
| x1, y1, x2, y2 = map(int, best.xyxy[0].tolist()) |
| return x1, y1, x2, y2 |
|
|
|
|
| def crop_roi(image: np.ndarray, box: tuple[int, int, int, int], |
| padding: float = 0.05) -> np.ndarray: |
| """ |
| Crop the ROI from the image with optional relative padding. |
| |
| Args: |
| image: BGR or grayscale image. |
| box: (x1, y1, x2, y2) |
| padding: fraction of box size to add as margin on each side. |
| |
| Returns: |
| Cropped numpy array (same channels as input). |
| """ |
| h, w = image.shape[:2] |
| x1, y1, x2, y2 = box |
|
|
| pw = int((x2 - x1) * padding) |
| ph = int((y2 - y1) * padding) |
|
|
| x1 = max(0, x1 - pw) |
| y1 = max(0, y1 - ph) |
| x2 = min(w, x2 + pw) |
| y2 = min(h, y2 + ph) |
|
|
| return image[y1:y2, x1:x2] |
|
|