init repo
This commit is contained in:
139
app/services/image_processor.py
Normal file
139
app/services/image_processor.py
Normal file
@@ -0,0 +1,139 @@
|
||||
"""Image preprocessing service using OpenCV."""
|
||||
|
||||
import base64
|
||||
import io
|
||||
from urllib.request import urlopen
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
|
||||
from app.core.config import get_settings
|
||||
|
||||
settings = get_settings()
|
||||
|
||||
|
||||
class ImageProcessor:
|
||||
"""Service for image preprocessing operations."""
|
||||
|
||||
def __init__(self, padding_ratio: float | None = None):
|
||||
"""Initialize with padding ratio.
|
||||
|
||||
Args:
|
||||
padding_ratio: Ratio for padding on each side (default from settings).
|
||||
0.15 means 15% padding on each side = 30% total expansion.
|
||||
"""
|
||||
self.padding_ratio = padding_ratio or settings.image_padding_ratio
|
||||
|
||||
def load_image_from_url(self, url: str) -> np.ndarray:
|
||||
"""Load image from URL.
|
||||
|
||||
Args:
|
||||
url: Image URL to fetch.
|
||||
|
||||
Returns:
|
||||
Image as numpy array in BGR format.
|
||||
|
||||
Raises:
|
||||
ValueError: If image cannot be loaded from URL.
|
||||
"""
|
||||
try:
|
||||
with urlopen(url, timeout=30) as response:
|
||||
image_data = response.read()
|
||||
image = Image.open(io.BytesIO(image_data))
|
||||
return cv2.cvtColor(np.array(image), cv2.COLOR_RGB2BGR)
|
||||
except Exception as e:
|
||||
raise ValueError(f"Failed to load image from URL: {e}") from e
|
||||
|
||||
def load_image_from_base64(self, base64_str: str) -> np.ndarray:
|
||||
"""Load image from base64 string.
|
||||
|
||||
Args:
|
||||
base64_str: Base64-encoded image data.
|
||||
|
||||
Returns:
|
||||
Image as numpy array in BGR format.
|
||||
|
||||
Raises:
|
||||
ValueError: If image cannot be decoded.
|
||||
"""
|
||||
try:
|
||||
# Handle data URL format
|
||||
if "," in base64_str:
|
||||
base64_str = base64_str.split(",", 1)[1]
|
||||
|
||||
image_data = base64.b64decode(base64_str)
|
||||
image = Image.open(io.BytesIO(image_data))
|
||||
return cv2.cvtColor(np.array(image), cv2.COLOR_RGB2BGR)
|
||||
except Exception as e:
|
||||
raise ValueError(f"Failed to decode base64 image: {e}") from e
|
||||
|
||||
def add_padding(self, image: np.ndarray) -> np.ndarray:
|
||||
"""Add whitespace padding around the image.
|
||||
|
||||
Adds padding equal to padding_ratio * max(height, width) on each side.
|
||||
This expands the image by approximately 30% total (15% on each side).
|
||||
|
||||
Args:
|
||||
image: Input image as numpy array in BGR format.
|
||||
|
||||
Returns:
|
||||
Padded image as numpy array.
|
||||
"""
|
||||
height, width = image.shape[:2]
|
||||
padding = int(max(height, width) * self.padding_ratio)
|
||||
|
||||
# Add white padding on all sides
|
||||
padded_image = cv2.copyMakeBorder(
|
||||
image,
|
||||
top=padding,
|
||||
bottom=padding,
|
||||
left=padding,
|
||||
right=padding,
|
||||
borderType=cv2.BORDER_CONSTANT,
|
||||
value=[255, 255, 255], # White
|
||||
)
|
||||
|
||||
return padded_image
|
||||
|
||||
def preprocess(self, image_url: str | None, image_base64: str | None) -> np.ndarray:
|
||||
"""Load and preprocess image with padding.
|
||||
|
||||
Args:
|
||||
image_url: URL to fetch image from (optional).
|
||||
image_base64: Base64-encoded image (optional).
|
||||
|
||||
Returns:
|
||||
Preprocessed image with padding.
|
||||
|
||||
Raises:
|
||||
ValueError: If neither input is provided or loading fails.
|
||||
"""
|
||||
if image_url:
|
||||
image = self.load_image_from_url(image_url)
|
||||
elif image_base64:
|
||||
image = self.load_image_from_base64(image_base64)
|
||||
else:
|
||||
raise ValueError("Either image_url or image_base64 must be provided")
|
||||
|
||||
return self.add_padding(image)
|
||||
|
||||
def image_to_base64(self, image: np.ndarray, format: str = "PNG") -> str:
|
||||
"""Convert numpy image to base64 string.
|
||||
|
||||
Args:
|
||||
image: Image as numpy array in BGR format.
|
||||
format: Output format (PNG, JPEG).
|
||||
|
||||
Returns:
|
||||
Base64-encoded image string.
|
||||
"""
|
||||
image_rgb = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
|
||||
pil_image = Image.fromarray(image_rgb)
|
||||
|
||||
buffer = io.BytesIO()
|
||||
pil_image.save(buffer, format=format)
|
||||
buffer.seek(0)
|
||||
|
||||
return base64.b64encode(buffer.getvalue()).decode("utf-8")
|
||||
|
||||
Reference in New Issue
Block a user