colonelwatch's picture
Add documentation
e878f56
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()