|  | import cv2 | 
					
						
						|  | import numpy as np | 
					
						
						|  | from pathlib import Path | 
					
						
						|  | from PIL import Image | 
					
						
						|  | import pillow_avif | 
					
						
						|  | import logging | 
					
						
						|  | from typing import Any, Optional, Dict, List, Union, Tuple | 
					
						
						|  |  | 
					
						
						|  | from ..config import NORMALIZE_IMAGES_TO, JPEG_QUALITY | 
					
						
						|  |  | 
					
						
						|  | logger = logging.getLogger(__name__) | 
					
						
						|  |  | 
					
						
						|  | def normalize_image(input_path: Path, output_path: Path) -> bool: | 
					
						
						|  | """Convert image to normalized format (PNG or JPEG) and optionally remove black bars | 
					
						
						|  |  | 
					
						
						|  | Args: | 
					
						
						|  | input_path: Source image path | 
					
						
						|  | output_path: Target path | 
					
						
						|  |  | 
					
						
						|  | Returns: | 
					
						
						|  | bool: True if successful, False otherwise | 
					
						
						|  | """ | 
					
						
						|  | try: | 
					
						
						|  |  | 
					
						
						|  | with Image.open(input_path) as img: | 
					
						
						|  |  | 
					
						
						|  | if img.mode in ('RGBA', 'LA'): | 
					
						
						|  | background = Image.new('RGB', img.size, (255, 255, 255)) | 
					
						
						|  | if img.mode == 'RGBA': | 
					
						
						|  | background.paste(img, mask=img.split()[3]) | 
					
						
						|  | else: | 
					
						
						|  | background.paste(img, mask=img.split()[1]) | 
					
						
						|  | img = background | 
					
						
						|  | elif img.mode != 'RGB': | 
					
						
						|  | img = img.convert('RGB') | 
					
						
						|  |  | 
					
						
						|  |  | 
					
						
						|  | img_np = np.array(img) | 
					
						
						|  |  | 
					
						
						|  |  | 
					
						
						|  | top, bottom, left, right = detect_black_bars(img_np) | 
					
						
						|  |  | 
					
						
						|  |  | 
					
						
						|  | if any([top > 0, bottom < img_np.shape[0] - 1, | 
					
						
						|  | left > 0, right < img_np.shape[1] - 1]): | 
					
						
						|  | img = img.crop((left, top, right, bottom)) | 
					
						
						|  |  | 
					
						
						|  |  | 
					
						
						|  | if NORMALIZE_IMAGES_TO == 'png': | 
					
						
						|  | img.save(output_path, 'PNG', optimize=True) | 
					
						
						|  | else: | 
					
						
						|  | img.save(output_path, 'JPEG', quality=JPEG_QUALITY, optimize=True) | 
					
						
						|  | return True | 
					
						
						|  |  | 
					
						
						|  | except Exception as e: | 
					
						
						|  | logger.error(f"Error converting image {input_path}: {str(e)}") | 
					
						
						|  | return False | 
					
						
						|  |  | 
					
						
						|  | def detect_black_bars(img: np.ndarray) -> Tuple[int, int, int, int]: | 
					
						
						|  | """Detect black bars in image | 
					
						
						|  |  | 
					
						
						|  | Args: | 
					
						
						|  | img: numpy array of image (HxWxC) | 
					
						
						|  |  | 
					
						
						|  | Returns: | 
					
						
						|  | Tuple of (top, bottom, left, right) crop coordinates | 
					
						
						|  | """ | 
					
						
						|  |  | 
					
						
						|  | if len(img.shape) == 3: | 
					
						
						|  | gray = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY) | 
					
						
						|  | else: | 
					
						
						|  | gray = img | 
					
						
						|  |  | 
					
						
						|  |  | 
					
						
						|  | threshold = 20 | 
					
						
						|  | black_mask = gray < threshold | 
					
						
						|  |  | 
					
						
						|  |  | 
					
						
						|  | row_means = np.mean(black_mask, axis=1) | 
					
						
						|  | col_means = np.mean(black_mask, axis=0) | 
					
						
						|  |  | 
					
						
						|  |  | 
					
						
						|  | black_threshold = 0.95 | 
					
						
						|  |  | 
					
						
						|  |  | 
					
						
						|  | top = 0 | 
					
						
						|  | bottom = img.shape[0] | 
					
						
						|  |  | 
					
						
						|  | for i, mean in enumerate(row_means): | 
					
						
						|  | if mean > black_threshold: | 
					
						
						|  | top = i + 1 | 
					
						
						|  | else: | 
					
						
						|  | break | 
					
						
						|  |  | 
					
						
						|  | for i, mean in enumerate(reversed(row_means)): | 
					
						
						|  | if mean > black_threshold: | 
					
						
						|  | bottom = img.shape[0] - i - 1 | 
					
						
						|  | else: | 
					
						
						|  | break | 
					
						
						|  |  | 
					
						
						|  |  | 
					
						
						|  | left = 0 | 
					
						
						|  | right = img.shape[1] | 
					
						
						|  |  | 
					
						
						|  | for i, mean in enumerate(col_means): | 
					
						
						|  | if mean > black_threshold: | 
					
						
						|  | left = i + 1 | 
					
						
						|  | else: | 
					
						
						|  | break | 
					
						
						|  |  | 
					
						
						|  | for i, mean in enumerate(reversed(col_means)): | 
					
						
						|  | if mean > black_threshold: | 
					
						
						|  | right = img.shape[1] - i - 1 | 
					
						
						|  | else: | 
					
						
						|  | break | 
					
						
						|  |  | 
					
						
						|  | return top, bottom, left, right | 
					
						
						|  |  | 
					
						
						|  |  |