| | 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 |
| |
|
| |
|