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