File size: 8,426 Bytes
f6a9f5a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
from PIL import Image, ImageEnhance, ImageChops
import numpy as np

def normalize_gray(image: Image) -> Image:
    """Normalize a grayscale image using histogram equalization."""
    if image.mode != 'L':
        image = image.convert('L')
    img = np.asarray(image)
    balanced_img = img.copy()
    hist, bins = np.histogram(img.reshape(-1), 256, (0, 256))
    bmin = np.min(np.where(hist > (hist.sum() * 0.0005)))
    bmax = np.max(np.where(hist > (hist.sum() * 0.0005)))
    balanced_img = np.clip(img, bmin, bmax)
    balanced_img = ((balanced_img - bmin) / (bmax - bmin) * 255)
    return Image.fromarray(balanced_img).convert('L')

def image_channel_split(image: Image, mode: str = 'RGBA') -> tuple:
    """Split image into channels based on color mode."""
    _image = image.convert('RGBA')
    channel1 = Image.new('L', size=_image.size, color='black')
    channel2 = Image.new('L', size=_image.size, color='black')
    channel3 = Image.new('L', size=_image.size, color='black')
    channel4 = Image.new('L', size=_image.size, color='black')
    
    if mode == 'RGBA':
        channel1, channel2, channel3, channel4 = _image.split()
    elif mode == 'RGB':
        channel1, channel2, channel3 = _image.convert('RGB').split()
    elif mode == 'YCbCr':
        channel1, channel2, channel3 = _image.convert('YCbCr').split()
    elif mode == 'LAB':
        channel1, channel2, channel3 = _image.convert('LAB').split()
    elif mode == 'HSV':
        channel1, channel2, channel3 = _image.convert('HSV').split()
    
    return channel1, channel2, channel3, channel4

def image_channel_merge(channels: tuple, mode: str = 'RGB') -> Image:
    """Merge channels back into an image based on color mode."""
    channel1 = channels[0].convert('L')
    channel2 = channels[1].convert('L')
    channel3 = channels[2].convert('L')
    channel4 = Image.new('L', size=channel1.size, color='white')
    
    if mode == 'RGBA':
        if len(channels) > 3:
            channel4 = channels[3].convert('L')
        ret_image = Image.merge('RGBA', [channel1, channel2, channel3, channel4])
    elif mode == 'RGB':
        ret_image = Image.merge('RGB', [channel1, channel2, channel3])
    elif mode == 'YCbCr':
        ret_image = Image.merge('YCbCr', [channel1, channel2, channel3]).convert('RGB')
    elif mode == 'LAB':
        ret_image = Image.merge('LAB', [channel1, channel2, channel3]).convert('RGB')
    elif mode == 'HSV':
        ret_image = Image.merge('HSV', [channel1, channel2, channel3]).convert('RGB')
    
    return ret_image

def balance_to_gamma(balance: int) -> float:
    """Convert color balance value to gamma value."""
    return 0.00005 * balance * balance - 0.01 * balance + 1

def gamma_trans(image: Image, gamma: float) -> Image:
    """Apply gamma correction to an image."""
    if gamma == 1.0:
        return image
    img_array = np.array(image)
    img_array = np.power(img_array / 255.0, gamma) * 255.0
    return Image.fromarray(img_array.astype(np.uint8))

def RGB2RGBA(image: Image, mask: Image) -> Image:
    """Convert RGB image to RGBA using provided mask."""
    if image.mode != 'RGB':
        image = image.convert('RGB')
    if mask.mode != 'L':
        mask = mask.convert('L')
    return Image.merge('RGBA', (*image.split(), mask))

def chop_image_v2(background_image: Image, layer_image: Image, blend_mode: str, opacity: int) -> Image:
    """Blend two images together with specified blend mode and opacity."""
    if background_image.mode != 'RGB':
        background_image = background_image.convert('RGB')
    if layer_image.mode != 'RGB':
        layer_image = layer_image.convert('RGB')
    
    # Convert opacity to float (0-1)
    opacity = opacity / 100.0
    
    # Create a copy of the background image
    result = background_image.copy()
    
    # Apply blend mode
    if blend_mode == "normal":
        result = Image.blend(background_image, layer_image, opacity)
    elif blend_mode == "multiply":
        result = ImageChops.multiply(background_image, layer_image)
        result = Image.blend(background_image, result, opacity)
    elif blend_mode == "screen":
        result = ImageChops.screen(background_image, layer_image)
        result = Image.blend(background_image, result, opacity)
    elif blend_mode == "overlay":
        result = ImageChops.overlay(background_image, layer_image)
        result = Image.blend(background_image, result, opacity)
    
    return result

def auto_adjust(image: Image, strength: int = 100, brightness: int = 0, 
                contrast: int = 0, saturation: int = 0, 
                red: int = 0, green: int = 0, blue: int = 0,
                mode: str = 'RGB') -> Image:
    """
    Apply automatic adjustments to an image.
    
    Args:
        image: PIL Image to adjust
        strength: Overall strength of the adjustment (0-100)
        brightness: Brightness adjustment (-100 to 100)
        contrast: Contrast adjustment (-100 to 100)
        saturation: Saturation adjustment (-100 to 100)
        red: Red channel adjustment (-100 to 100)
        green: Green channel adjustment (-100 to 100)
        blue: Blue channel adjustment (-100 to 100)
        mode: Color mode for processing ('RGB', 'lum + sat', 'luminance', 'saturation', 'mono')
    
    Returns:
        Adjusted PIL Image
    """
    def auto_level_gray(image):
        """Apply auto levels to a grayscale image."""
        gray_image = Image.new("L", image.size, color='gray')
        gray_image.paste(image.convert('L'))
        return normalize_gray(gray_image)

    # Calculate adjustment factors
    if brightness < 0:
        brightness_offset = brightness / 100 + 1
    else:
        brightness_offset = brightness / 50 + 1
        
    if contrast < 0:
        contrast_offset = contrast / 100 + 1
    else:
        contrast_offset = contrast / 50 + 1
        
    if saturation < 0:
        saturation_offset = saturation / 100 + 1
    else:
        saturation_offset = saturation / 50 + 1

    # Get color channel gammas
    red_gamma = balance_to_gamma(red)
    green_gamma = balance_to_gamma(green)
    blue_gamma = balance_to_gamma(blue)

    # Process image based on mode
    if mode == 'RGB':
        r, g, b, _ = image_channel_split(image, mode='RGB')
        r = auto_level_gray(r)
        g = auto_level_gray(g)
        b = auto_level_gray(b)
        ret_image = image_channel_merge((r, g, b), 'RGB')
    elif mode == 'lum + sat':
        h, s, v, _ = image_channel_split(image, mode='HSV')
        s = auto_level_gray(s)
        ret_image = image_channel_merge((h, s, v), 'HSV')
        l, a, b, _ = image_channel_split(ret_image, mode='LAB')
        l = auto_level_gray(l)
        ret_image = image_channel_merge((l, a, b), 'LAB')
    elif mode == 'luminance':
        l, a, b, _ = image_channel_split(image, mode='LAB')
        l = auto_level_gray(l)
        ret_image = image_channel_merge((l, a, b), 'LAB')
    elif mode == 'saturation':
        h, s, v, _ = image_channel_split(image, mode='HSV')
        s = auto_level_gray(s)
        ret_image = image_channel_merge((h, s, v), 'HSV')
    else:  # mono
        gray = image.convert('L')
        ret_image = auto_level_gray(gray).convert('RGB')

    # Apply color channel adjustments if not in mono mode
    if (red or green or blue) and mode != "mono":
        r, g, b, _ = image_channel_split(ret_image, mode='RGB')
        if red:
            r = gamma_trans(r, red_gamma).convert('L')
        if green:
            g = gamma_trans(g, green_gamma).convert('L')
        if blue:
            b = gamma_trans(b, blue_gamma).convert('L')
        ret_image = image_channel_merge((r, g, b), 'RGB')

    # Apply brightness, contrast, and saturation
    if brightness:
        brightness_image = ImageEnhance.Brightness(ret_image)
        ret_image = brightness_image.enhance(factor=brightness_offset)
        
    if contrast:
        contrast_image = ImageEnhance.Contrast(ret_image)
        ret_image = contrast_image.enhance(factor=contrast_offset)
        
    if saturation:
        color_image = ImageEnhance.Color(ret_image)
        ret_image = color_image.enhance(factor=saturation_offset)

    # Blend with original image based on strength
    ret_image = chop_image_v2(image, ret_image, blend_mode="normal", opacity=strength)
    
    # Handle RGBA mode
    if image.mode == 'RGBA':
        ret_image = RGB2RGBA(ret_image, image.split()[-1])
    
    return ret_image