Spaces:
Sleeping
Sleeping
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() |