File size: 5,118 Bytes
bcc2f7b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
"""
Image processing utilities for the FastAPI application.
"""

import base64
import io
from typing import Tuple
import numpy as np
from PIL import Image
import cv2

from app.core.config import settings
from app.core.logging import get_logger

logger = get_logger(__name__)


def decode_base64_image(image_data: str) -> Tuple[np.ndarray, Tuple[int, int]]:
    """
    Decode base64 image data to numpy array.
    
    Args:
        image_data: Base64 encoded image string
        
    Returns:
        Tuple of (image_array, (width, height))
    """
    try:
        # Remove data URL prefix if present
        if image_data.startswith('data:image'):
            image_data = image_data.split(',')[1]
        
        # Decode base64
        image_bytes = base64.b64decode(image_data)
        
        # Open with PIL
        pil_image = Image.open(io.BytesIO(image_bytes))
        
        # Convert to RGB if necessary
        if pil_image.mode != 'RGB':
            pil_image = pil_image.convert('RGB')
        
        # Get original dimensions
        original_dims = pil_image.size  # (width, height)
        
        # Convert to numpy array
        image_array = np.array(pil_image)
        
        logger.debug(f"Decoded image with shape: {image_array.shape}")
        
        return image_array, original_dims
        
    except Exception as e:
        logger.error(f"Failed to decode base64 image: {str(e)}")
        raise ValueError(f"Invalid image data: {str(e)}")


def encode_image_to_base64(image: np.ndarray, format: str = "JPEG", quality: int = 95) -> str:
    """
    Encode numpy array image to base64 string.
    
    Args:
        image: Image as numpy array
        format: Image format (JPEG, PNG, etc.)
        quality: JPEG quality (1-100)
        
    Returns:
        Base64 encoded image string
    """
    try:
        # Convert numpy array to PIL Image
        if image.dtype != np.uint8:
            image = (image * 255).astype(np.uint8)
        
        pil_image = Image.fromarray(image)
        
        # Save to bytes buffer
        buffer = io.BytesIO()
        save_kwargs = {"format": format}
        
        if format.upper() == "JPEG":
            save_kwargs["quality"] = quality
            save_kwargs["optimize"] = True
        
        pil_image.save(buffer, **save_kwargs)
        
        # Encode to base64
        image_bytes = buffer.getvalue()
        base64_string = base64.b64encode(image_bytes).decode('utf-8')
        
        return base64_string
        
    except Exception as e:
        logger.error(f"Failed to encode image to base64: {str(e)}")
        raise ValueError(f"Image encoding failed: {str(e)}")


def validate_image_size(image: np.ndarray) -> bool:
    """
    Validate image dimensions.
    
    Args:
        image: Image as numpy array
        
    Returns:
        True if image size is valid
    """
    height, width = image.shape[:2]
    
    # Check minimum and maximum dimensions
    min_dim = min(width, height)
    max_dim = max(width, height)
    
    if min_dim < 32:  # Too small
        return False
    
    if max_dim > 4096:  # Too large
        return False
    
    return True


def resize_image_if_needed(image: np.ndarray, max_size: int = 1280) -> np.ndarray:
    """
    Resize image if it's too large while maintaining aspect ratio.
    
    Args:
        image: Image as numpy array
        max_size: Maximum dimension size
        
    Returns:
        Resized image
    """
    height, width = image.shape[:2]
    
    if max(height, width) <= max_size:
        return image
    
    # Calculate new dimensions
    if width > height:
        new_width = max_size
        new_height = int(height * (max_size / width))
    else:
        new_height = max_size
        new_width = int(width * (max_size / height))
    
    # Resize using PIL for better quality
    pil_image = Image.fromarray(image)
    resized_pil = pil_image.resize((new_width, new_height), Image.Resampling.LANCZOS)
    
    return np.array(resized_pil)


def validate_image_format(image_bytes: bytes) -> bool:
    """
    Validate if the image format is supported.
    
    Args:
        image_bytes: Raw image bytes
        
    Returns:
        True if format is supported
    """
    try:
        with Image.open(io.BytesIO(image_bytes)) as img:
            # Check if format is in allowed extensions
            format_lower = img.format.lower() if img.format else ""
            allowed_formats = {"jpeg", "jpg", "png", "bmp", "tiff", "webp"}
            return format_lower in allowed_formats
    except Exception:
        return False


def get_image_info(image: np.ndarray) -> dict:
    """
    Get information about an image.
    
    Args:
        image: Image as numpy array
        
    Returns:
        Dictionary with image information
    """
    height, width = image.shape[:2]
    channels = image.shape[2] if len(image.shape) > 2 else 1
    
    return {
        "width": width,
        "height": height,
        "channels": channels,
        "dtype": str(image.dtype),
        "size_mb": image.nbytes / (1024 * 1024)
    }