File size: 6,040 Bytes
9faa360
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e878f56
 
 
 
 
 
 
9faa360
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
import numpy as np
from skimage import exposure, color, util
from matplotlib import pyplot as plt
import gradio as gr

# https://en.wikipedia.org/wiki/Rotation_matrix#General_rotations
def _rotation_matrix(yaw, pitch, roll):
    yaw_matrix = np.array([
        [np.cos(yaw), -np.sin(yaw), 0],
        [np.sin(yaw), np.cos(yaw), 0],
        [0, 0, 1],
    ])
    pitch_matrix = np.array([
        [np.cos(pitch), 0, np.sin(pitch)],
        [0, 1, 0],
        [-np.sin(pitch), 0, np.cos(pitch)],
    ])
    roll_matrix = np.array([
        [1, 0, 0],
        [0, np.cos(roll), -np.sin(roll)],
        [0, np.sin(roll), np.cos(roll)],
    ])

    return yaw_matrix @ pitch_matrix @ roll_matrix

def _calculate_transform():
    t_cie = np.array([50, 0, 0]) # center of CIELAB color space
    # lightness axis in CIELAB space is spanned by the vector [1, 0, 0]
    t_sol = np.array([55.5, -6.125, -2.875]) # center of Solarized base palette in CIELAB space
    v_sol = np.array([0.951, 0.145, 0.272]) # principal component of Solarized base palette in CIELAB space

    # find the rotation matrix that rotates [1, 0, 0] to v_sol
    pitch = -np.arcsin(v_sol[2])
    yaw = np.arcsin(v_sol[1]/np.cos(pitch))
    roll = 0 # roll is a free parameter
    R = _rotation_matrix(yaw, pitch, roll)

    def rotate(x):
        return (x-t_cie) @ R.T + t_sol

    return rotate

transform = _calculate_transform()

# light_min and light_max define a range of lightnesss between 0 and 100
# chroma_attenuation is a factor between 0 and 1
def preprocess_image(image, light_min, light_max, chroma_attenutation):
    lightness_range = (light_min, light_max)
    chroma_range = (-128*chroma_attenutation, 128*chroma_attenutation)

    image_lab = color.rgb2lab(image)
    image_lab[:, :, 0] = exposure.rescale_intensity(image_lab[:, :, 0], in_range=(0, 100), out_range=lightness_range)
    image_lab[:, :, 1] = exposure.rescale_intensity(image_lab[:, :, 1], in_range=(-128, 128), out_range=chroma_range)
    image_lab[:, :, 2] = exposure.rescale_intensity(image_lab[:, :, 2], in_range=(-128, 128), out_range=chroma_range)
    image = color.lab2rgb(image_lab)

    return image

def preprocess_image_parallel(image, light_min, light_max, chroma_attenutation):
    preprocess_kwargs = {'light_min': light_min, 'light_max': light_max, 'chroma_attenutation': chroma_attenutation}
    image = util.apply_parallel(
        preprocess_image, image,
        (1024, 1024), # restricted chunk size to prevent OOM-kill
        dtype=np.float64, # required, according to error message
        extra_keywords=preprocess_kwargs,
        channel_axis=2, # third axis holds RGB channels
    )
    return image

def lightness_hist(image):
    fig = plt.figure(figsize=(12, 12/5)) # set aspect ratio of figure to 5:1
    ax = fig.add_subplot()
    
    image_lightness = color.rgb2lab(image)[:, :, 0].flatten()

    ax.hist(image_lightness, bins=64, label=None)
    ax.axvline(x=8.13974087, color='#586e75', label='Solarized dark target range')
    ax.axvline(x=59.4372606, color='#586e75', label=None)
    ax.axvline(x=38.76215165, color='#93a1a1', label='Solarized light target range')
    ax.axvline(x=93.86995897, color='#93a1a1', label=None)
    ax.set_xlim(0, 100)

    ax.legend()
    ax.set_xlabel('Lightness')
    ax.set_ylabel('Frequency')

    # set aspect ratio of final plot to 7:1 (different from figure aspect ratio to fit other elements)
    x_left, x_right = ax.get_xlim()
    y_bottom, y_top = ax.get_ylim()
    ax.set_aspect((x_right-x_left)/(y_top-y_bottom)/7)
    
    return fig

def transform_image(image):
    shape = image.shape # record shape

    workmem = color.rgb2lab(image) # convert to CIELAB
    workmem = workmem.reshape(-1, 3)
    workmem = transform(workmem) # transform is a function defined globally
    workmem = workmem.reshape(shape) # undo flatten
    workmem = color.lab2rgb(workmem) # convert back to RGB

    workmem = util.img_as_ubyte(workmem) # convert back to uint8 rgb
    
    return workmem

def transform_image_parallel(image):
    image = util.apply_parallel(
        transform_image, image, 
        (1024, 1024), # restricted chunk size to prevent OOM-kill
        dtype=np.uint8, # required, according to error message
        channel_axis=2, # third axis holds RGB channels
    )
    return image

with gr.Blocks() as demo:
    gr.Markdown('# background-solarizer')
    gr.Markdown(
        'Align your desktop background to the Solarized color palette. Upload an image, adjust the sliders, and click '
        '"Preprocess into workspace" to check whether the light mode or dark mode target range is satisfied. Click '
        '"Transform workspace" to apply the transformation. For more information, check read the blog post at '
        'https://kenny-peng.com/2023/06/02/solarized_background_2.html.'
    )
    with gr.Row():
        with gr.Column(scale=1, min_width=320):
            input_image = gr.Image(label='Input')
            light_min_slider = gr.Slider(minimum=0, maximum=100, value=10, label='Lightness minimum')
            light_max_slider = gr.Slider(minimum=0, maximum=100, value=70, label='Lightness maximum')
            chroma_attenutation_slider = gr.Slider(minimum=0, maximum=1, value=0.25, label='Chroma attenuation')
            preprocess_button = gr.Button(value='Preprocess into workspace')
            transform_button = gr.Button(value='Transform workspace')
        with gr.Column(scale=2, min_width=640):
            workspace_image = gr.Image(label='Workspace', interactive=False)
            hist = gr.Plot(label='Lightness histogram')
    
    preprocess_button.click(
        preprocess_image_parallel, 
        inputs=[input_image, light_min_slider, light_max_slider, chroma_attenutation_slider],
        outputs=[workspace_image]
    ).then(
        lightness_hist,
        inputs=[workspace_image],
        outputs=[hist]
    )

    transform_button.click(
        transform_image_parallel,
        inputs=[workspace_image],
        outputs=[workspace_image]
    )

demo.launch()