""" Preprocessing pipeline: raw ROI crop -> clean grayscale binary image. Steps (tuned for Ukrainian dot-matrix passport series/number strips): 1. Grayscale 2. Non-local means denoising 3. (Optional) CLAHE / top-hat / sharpening 4. (Optional) Gamma correction 5. Otsu binarisation """ import cv2 import numpy as np def preprocess(image: np.ndarray) -> np.ndarray: """ Full preprocessing pipeline. Args: image: BGR or grayscale crop of the passport ROI. Returns: uint8 grayscale image after binarisation (0 = background, 255 = ink). """ # 1. Grayscale if image.ndim == 3: gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) else: gray = image.copy() # 2. Non-local means denoising denoised = cv2.fastNlMeansDenoising(gray, h=3, templateWindowSize=9, searchWindowSize=5) # 3. Enhancement (all disabled by default — add if needed per scan quality) enhanced = denoised # 4. Gamma correction (disabled — gamma=1.0 is identity) # gamma = 1.8 # lut = np.array([((i / 255.0) ** (1.0 / gamma)) * 255 # for i in range(256)], dtype=np.uint8) # enhanced = cv2.LUT(enhanced, lut) # 5. Otsu binarisation _, binary = cv2.threshold(enhanced, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU) # Invert if background is dark (dots should be white on black, # matching the original Step_5_Binary preprocessing) if binary.mean() > 127: binary = 255 - binary return binary