|
|
""" |
|
|
Signature preprocessing module for image normalization and preparation. |
|
|
""" |
|
|
|
|
|
import cv2 |
|
|
import numpy as np |
|
|
import torch |
|
|
from PIL import Image |
|
|
from typing import Tuple, Union, Optional |
|
|
import albumentations as A |
|
|
from albumentations.pytorch import ToTensorV2 |
|
|
|
|
|
|
|
|
class SignaturePreprocessor: |
|
|
""" |
|
|
Handles preprocessing of signature images for the verification model. |
|
|
""" |
|
|
|
|
|
def __init__(self, target_size: Tuple[int, int] = (224, 224)): |
|
|
""" |
|
|
Initialize the preprocessor. |
|
|
|
|
|
Args: |
|
|
target_size: Target size for signature images (height, width) |
|
|
""" |
|
|
self.target_size = target_size |
|
|
self.transform = A.Compose([ |
|
|
A.Resize(target_size[0], target_size[1]), |
|
|
A.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]), |
|
|
ToTensorV2() |
|
|
]) |
|
|
|
|
|
def load_image(self, image_path: str) -> np.ndarray: |
|
|
""" |
|
|
Load image from file path. |
|
|
|
|
|
Args: |
|
|
image_path: Path to the image file |
|
|
|
|
|
Returns: |
|
|
Loaded image as numpy array |
|
|
""" |
|
|
try: |
|
|
image = cv2.imread(image_path) |
|
|
if image is None: |
|
|
raise ValueError(f"Could not load image from {image_path}") |
|
|
image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) |
|
|
return image |
|
|
except Exception as e: |
|
|
raise ValueError(f"Error loading image {image_path}: {str(e)}") |
|
|
|
|
|
def preprocess_image(self, image: Union[str, np.ndarray, Image.Image]) -> torch.Tensor: |
|
|
""" |
|
|
Preprocess a signature image for model input. |
|
|
|
|
|
Args: |
|
|
image: Image as file path, numpy array, or PIL Image |
|
|
|
|
|
Returns: |
|
|
Preprocessed image as torch tensor |
|
|
""" |
|
|
|
|
|
if isinstance(image, str): |
|
|
image = self.load_image(image) |
|
|
elif isinstance(image, Image.Image): |
|
|
image = np.array(image) |
|
|
elif isinstance(image, torch.Tensor): |
|
|
image = image.numpy() |
|
|
|
|
|
|
|
|
if len(image.shape) == 3 and image.shape[2] == 3: |
|
|
pass |
|
|
elif len(image.shape) == 2: |
|
|
image = cv2.cvtColor(image, cv2.COLOR_GRAY2RGB) |
|
|
else: |
|
|
raise ValueError(f"Unsupported image format with shape: {image.shape}") |
|
|
|
|
|
|
|
|
transformed = self.transform(image=image) |
|
|
return transformed['image'] |
|
|
|
|
|
def enhance_signature(self, image: np.ndarray) -> np.ndarray: |
|
|
""" |
|
|
Enhance signature image quality. |
|
|
|
|
|
Args: |
|
|
image: Input signature image |
|
|
|
|
|
Returns: |
|
|
Enhanced signature image |
|
|
""" |
|
|
|
|
|
if len(image.shape) == 3: |
|
|
gray = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY) |
|
|
else: |
|
|
gray = image.copy() |
|
|
|
|
|
|
|
|
binary = cv2.adaptiveThreshold( |
|
|
gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 11, 2 |
|
|
) |
|
|
|
|
|
|
|
|
kernel = np.ones((2, 2), np.uint8) |
|
|
cleaned = cv2.morphologyEx(binary, cv2.MORPH_CLOSE, kernel) |
|
|
cleaned = cv2.morphologyEx(cleaned, cv2.MORPH_OPEN, kernel) |
|
|
|
|
|
|
|
|
if len(image.shape) == 3: |
|
|
enhanced = cv2.cvtColor(cleaned, cv2.COLOR_GRAY2RGB) |
|
|
else: |
|
|
enhanced = cleaned |
|
|
|
|
|
return enhanced |
|
|
|
|
|
def normalize_signature(self, image: np.ndarray) -> np.ndarray: |
|
|
""" |
|
|
Normalize signature image for consistent processing. |
|
|
|
|
|
Args: |
|
|
image: Input signature image |
|
|
|
|
|
Returns: |
|
|
Normalized signature image |
|
|
""" |
|
|
|
|
|
if len(image.shape) == 3: |
|
|
gray = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY) |
|
|
else: |
|
|
gray = image.copy() |
|
|
|
|
|
|
|
|
contours, _ = cv2.findContours(gray, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) |
|
|
|
|
|
if not contours: |
|
|
return image |
|
|
|
|
|
|
|
|
x, y, w, h = cv2.boundingRect(max(contours, key=cv2.contourArea)) |
|
|
|
|
|
|
|
|
padding = 10 |
|
|
x1 = max(0, x - padding) |
|
|
y1 = max(0, y - padding) |
|
|
x2 = min(image.shape[1], x + w + padding) |
|
|
y2 = min(image.shape[0], y + h + padding) |
|
|
|
|
|
cropped = image[y1:y2, x1:x2] |
|
|
|
|
|
|
|
|
h_orig, w_orig = cropped.shape[:2] |
|
|
aspect_ratio = w_orig / h_orig |
|
|
|
|
|
if aspect_ratio > 1: |
|
|
new_w = self.target_size[1] |
|
|
new_h = int(new_w / aspect_ratio) |
|
|
else: |
|
|
new_h = self.target_size[0] |
|
|
new_w = int(new_h * aspect_ratio) |
|
|
|
|
|
resized = cv2.resize(cropped, (new_w, new_h)) |
|
|
|
|
|
|
|
|
canvas = np.ones((self.target_size[0], self.target_size[1], 3), dtype=np.uint8) * 255 |
|
|
|
|
|
|
|
|
y_offset = (self.target_size[0] - new_h) // 2 |
|
|
x_offset = (self.target_size[1] - new_w) // 2 |
|
|
|
|
|
canvas[y_offset:y_offset+new_h, x_offset:x_offset+new_w] = resized |
|
|
|
|
|
return canvas |
|
|
|
|
|
def preprocess_batch(self, images: list) -> torch.Tensor: |
|
|
""" |
|
|
Preprocess a batch of signature images. |
|
|
|
|
|
Args: |
|
|
images: List of images to preprocess |
|
|
|
|
|
Returns: |
|
|
Batch of preprocessed images as torch tensor |
|
|
""" |
|
|
processed_images = [] |
|
|
for image in images: |
|
|
processed = self.preprocess_image(image) |
|
|
processed_images.append(processed) |
|
|
|
|
|
return torch.stack(processed_images) |
|
|
|
|
|
|
|
|
class SignatureAugmentation: |
|
|
""" |
|
|
Data augmentation for signature images during training. |
|
|
""" |
|
|
|
|
|
def __init__(self, target_size: Tuple[int, int] = (224, 224)): |
|
|
""" |
|
|
Initialize augmentation pipeline. |
|
|
|
|
|
Args: |
|
|
target_size: Target size for signature images |
|
|
""" |
|
|
self.target_size = target_size |
|
|
|
|
|
|
|
|
self.train_transform = A.Compose([ |
|
|
A.Resize(target_size[0], target_size[1]), |
|
|
A.HorizontalFlip(p=0.3), |
|
|
A.Rotate(limit=15, p=0.5), |
|
|
A.RandomBrightnessContrast( |
|
|
brightness_limit=0.2, |
|
|
contrast_limit=0.2, |
|
|
p=0.5 |
|
|
), |
|
|
A.GaussNoise(var_limit=(10.0, 50.0), p=0.3), |
|
|
A.ElasticTransform( |
|
|
alpha=1, |
|
|
sigma=50, |
|
|
alpha_affine=50, |
|
|
p=0.3 |
|
|
), |
|
|
A.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]), |
|
|
ToTensorV2() |
|
|
]) |
|
|
|
|
|
|
|
|
self.val_transform = A.Compose([ |
|
|
A.Resize(target_size[0], target_size[1]), |
|
|
A.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]), |
|
|
ToTensorV2() |
|
|
]) |
|
|
|
|
|
def augment(self, image: np.ndarray, is_training: bool = True) -> torch.Tensor: |
|
|
""" |
|
|
Apply augmentation to signature image. |
|
|
|
|
|
Args: |
|
|
image: Input signature image |
|
|
is_training: Whether to apply training augmentations |
|
|
|
|
|
Returns: |
|
|
Augmented image as torch tensor |
|
|
""" |
|
|
transform = self.train_transform if is_training else self.val_transform |
|
|
transformed = transform(image=image) |
|
|
return transformed['image'] |
|
|
|
|
|
def augment_batch(self, images: list, is_training: bool = True) -> torch.Tensor: |
|
|
""" |
|
|
Apply augmentation to a batch of signature images. |
|
|
|
|
|
Args: |
|
|
images: List of images to augment |
|
|
is_training: Whether to apply training augmentations |
|
|
|
|
|
Returns: |
|
|
Batch of augmented images as torch tensor |
|
|
""" |
|
|
augmented_images = [] |
|
|
for image in images: |
|
|
augmented = self.augment(image, is_training) |
|
|
augmented_images.append(augmented) |
|
|
|
|
|
return torch.stack(augmented_images) |
|
|
|