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