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() |