├── .streamlit └── config.toml ├── logo.jpg ├── requirements.txt ├── media ├── example_adopt.png ├── example_plotly.png ├── example_matplotlib.png └── example_sophisticated_palette.png ├── images ├── Nighthawks-(Edward-Hopper).jpg ├── Mona-Lisa-(Leonardo-da-Vinci).jpg ├── Pretty-Night-(Leonid-Afremov).jpg ├── A-Bigger-Splash-(David-Hockney).jpg ├── Impression-Sunrise-(Claude-Monet).jpg ├── Cafe-Terrace-at-Night-(Vincent-van-Gogh).jpg └── Girl-With-a-Pearl-Earring-(Johannes-Vermeer).jpg ├── README.md ├── sophisticated_palette └── utils.py └── app.py /.streamlit/config.toml: -------------------------------------------------------------------------------- 1 | [theme] 2 | base="light" -------------------------------------------------------------------------------- /logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/syasini/sophisticated_palette/HEAD/logo.jpg -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | streamlit==1.25.0 2 | numpy 3 | pandas 4 | matplotlib 5 | plotly 6 | scikit-learn 7 | -------------------------------------------------------------------------------- /media/example_adopt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/syasini/sophisticated_palette/HEAD/media/example_adopt.png -------------------------------------------------------------------------------- /media/example_plotly.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/syasini/sophisticated_palette/HEAD/media/example_plotly.png -------------------------------------------------------------------------------- /media/example_matplotlib.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/syasini/sophisticated_palette/HEAD/media/example_matplotlib.png -------------------------------------------------------------------------------- /images/Nighthawks-(Edward-Hopper).jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/syasini/sophisticated_palette/HEAD/images/Nighthawks-(Edward-Hopper).jpg -------------------------------------------------------------------------------- /media/example_sophisticated_palette.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/syasini/sophisticated_palette/HEAD/media/example_sophisticated_palette.png -------------------------------------------------------------------------------- /images/Mona-Lisa-(Leonardo-da-Vinci).jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/syasini/sophisticated_palette/HEAD/images/Mona-Lisa-(Leonardo-da-Vinci).jpg -------------------------------------------------------------------------------- /images/Pretty-Night-(Leonid-Afremov).jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/syasini/sophisticated_palette/HEAD/images/Pretty-Night-(Leonid-Afremov).jpg -------------------------------------------------------------------------------- /images/A-Bigger-Splash-(David-Hockney).jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/syasini/sophisticated_palette/HEAD/images/A-Bigger-Splash-(David-Hockney).jpg -------------------------------------------------------------------------------- /images/Impression-Sunrise-(Claude-Monet).jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/syasini/sophisticated_palette/HEAD/images/Impression-Sunrise-(Claude-Monet).jpg -------------------------------------------------------------------------------- /images/Cafe-Terrace-at-Night-(Vincent-van-Gogh).jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/syasini/sophisticated_palette/HEAD/images/Cafe-Terrace-at-Night-(Vincent-van-Gogh).jpg -------------------------------------------------------------------------------- /images/Girl-With-a-Pearl-Earring-(Johannes-Vermeer).jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/syasini/sophisticated_palette/HEAD/images/Girl-With-a-Pearl-Earring-(Johannes-Vermeer).jpg -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [Sophisticated Palette](https://sophisticated-palette.streamlit.app/) 2 | 3 | A [Streamlit](https://streamlit.io/) 🎈 web app to help you tell your data story in style! Do you have a sophisticated palette? 4 | 5 | 6 | 7 | [](https://sophisticated-palette.streamlit.app/) 8 | 9 | 10 | --- 11 | 12 | **Sophisticated Palette** is a machine learning powered web app that allows you to infer color palettes from any input image. Simply upload your `jpg` or `png` files, or copy the url of your favorite artwork in the input box, click the button and let the magic happen. 13 | 14 | 15 | 16 | 17 | Use the provided code snippets in the app to adopt the color palette and port it to `matplotlib` 18 | 19 | 20 | 21 | or `plotly` 22 | 23 | 24 | 25 | It's that simple! Now why are you still here? Go check out the [app](https://sophisticated-palette.streamlit.app/) and have fun. 26 | 27 | Don't forget to ⭐️ the repo 👆 so you can find it easily later. 28 | -------------------------------------------------------------------------------- /sophisticated_palette/utils.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import pandas as pd 3 | import matplotlib.pyplot as plt 4 | from PIL import ImageColor 5 | import colorsys 6 | import streamlit as st 7 | import plotly.express as px 8 | 9 | from sklearn.cluster import KMeans, BisectingKMeans, MiniBatchKMeans 10 | from sklearn.mixture import GaussianMixture 11 | from sklearn.manifold import TSNE 12 | 13 | 14 | model_dict = { 15 | "KMeans": KMeans, 16 | "BisectingKMeans" : BisectingKMeans, 17 | "GaussianMixture": GaussianMixture, 18 | "MiniBatchKMeans": MiniBatchKMeans, 19 | } 20 | 21 | center_method = { 22 | "KMeans": "cluster_centers_", 23 | "BisectingKMeans" : "cluster_centers_", 24 | "GaussianMixture": "means_", 25 | "MiniBatchKMeans": "cluster_centers_", 26 | } 27 | 28 | n_cluster_arg = { 29 | "KMeans": "n_clusters", 30 | "BisectingKMeans" : "n_clusters", 31 | "GaussianMixture": "n_components", 32 | "MiniBatchKMeans": "n_clusters", 33 | 34 | } 35 | 36 | enhancement_range = { 37 | "Color": [0., 5., 0.2], 38 | "Sharpness": [0., 3., 0.2], 39 | "Contrast": [0.5, 1.5, 0.1], 40 | "Brightness": [0.5, 1.5, 0.1] 41 | } 42 | 43 | sort_func_dict = { 44 | "rgb": (lambda r,g,b: (r, g, b)), 45 | "sum_rgb": (lambda r,g,b: r+g+b), 46 | "sqr_rgb": (lambda r,g,b: r**2+g**2+b**2), 47 | "hsv": (lambda r, g, b : colorsys.rgb_to_hsv(r, g, b)), 48 | "random": (lambda r, g, b: np.random.random()) 49 | } 50 | 51 | def get_df_rgb(img, sample_size): 52 | """construct a sample RGB dataframe from image""" 53 | 54 | n_dims = np.array(img).shape[-1] 55 | r,g,b = np.array(img).reshape(-1,n_dims).T 56 | df = pd.DataFrame({"R": r, "G": g, "B": b}).sample(n=sample_size) 57 | return df 58 | 59 | @st.cache_data 60 | def get_palette(df_rgb, model_name, palette_size, sort_func="random"): 61 | """cluster pixels together and return a sorted color palette.""" 62 | params = {n_cluster_arg[model_name]: palette_size} 63 | model = model_dict[model_name](**params) 64 | 65 | clusters = model.fit_predict(df_rgb) 66 | 67 | palette = getattr(model, center_method[model_name]).astype(int).tolist() 68 | 69 | palette.sort(key=lambda rgb : sort_func_dict[sort_func.rstrip("_r")](*rgb), 70 | reverse=bool(sort_func.endswith("_r"))) 71 | 72 | return palette 73 | 74 | def rgb_to_hex(rgb): 75 | return '#%02x%02x%02x' % tuple(rgb) 76 | 77 | def show_palette(palette_hex): 78 | """show palette strip""" 79 | palette = np.array([ImageColor.getcolor(color, "RGB") for color in palette_hex]) 80 | fig, ax = plt.subplots(dpi=100) 81 | ax.imshow(palette[np.newaxis, :, :]) 82 | ax.axis('off') 83 | return fig 84 | 85 | 86 | def store_palette(palette): 87 | """store palette colors in session state""" 88 | palette_size = len(palette) 89 | columns = st.columns(palette_size) 90 | for i, col in enumerate(columns): 91 | with col: 92 | st.session_state[f"col_{i}"]= st.color_picker(label=str(i), value=rgb_to_hex(palette[i]), key=f"pal_{i}") 93 | 94 | def display_matplotlib_code(palette_hex): 95 | 96 | st.write('Use this snippet in your code to make your color palette more sophisticated!') 97 | code = st.code(f""" 98 | import matplotlib as mpl 99 | from cycler import cycler 100 | 101 | palette = {palette_hex} 102 | mpl.rcParams["axes.prop_cycle"] = cycler(color=palette) 103 | """ 104 | ) 105 | 106 | def display_plotly_code(palette_hex): 107 | st.write('Use this snippet in your code to make your color palette more sophisticated!') 108 | st.code(f""" 109 | import plotly.io as pio 110 | import plotly.graph_objects as go 111 | pio.templates["sophisticated"] = go.layout.Template( 112 | layout=go.Layout( 113 | colorway={palette_hex} 114 | ) 115 | ) 116 | pio.templates.default = 'sophisticated' 117 | """) 118 | 119 | def plot_rgb_3d(df_rgb): 120 | """plot the sampled pixels in 3D RGB space""" 121 | 122 | if df_rgb.shape[0] > 2000: 123 | st.error("RGB plot can only be used for less than 2000 sample pixels.") 124 | else: 125 | colors = df_rgb.apply(rgb_to_hex, axis=1) 126 | fig = px.scatter_3d(df_rgb, x='R', y='G', z='B', 127 | color=colors, size=[1]*df_rgb.shape[0], 128 | opacity=0.7) 129 | 130 | st.plotly_chart(fig) 131 | 132 | 133 | def plot_hsv_3d(df): 134 | """plot the sampled pixels in 3D RGB space""" 135 | df_rgb = df.copy() 136 | if df_rgb.shape[0] > 2000: 137 | st.error("RGB plot can only be used for less than 2000 sample pixels.") 138 | 139 | else: 140 | df_rgb[["H","S",'V']]= df_rgb.apply(lambda x: pd.Series(colorsys.rgb_to_hsv(x.R/255.,x.G/255.,x.B/255.)).T, axis=1) 141 | st.dataframe(df_rgb[["H","S",'V']]) 142 | colors = df_rgb[["R","G","B"]].apply(rgb_to_hex, axis=1) 143 | fig = px.scatter_3d(df_rgb, x='H', y='S', z='V', 144 | color=colors, size=[1]*df_rgb.shape[0], 145 | opacity=0.7) 146 | 147 | st.plotly_chart(fig) 148 | 149 | def print_praise(): 150 | """Yes, I'm that vain and superficial! 🙄 """ 151 | 152 | praise_quotes = [ 153 | '"When I stumbled upon this app, it was like I found a *pearl* among the oysetrs. Absolutely stunning! "\n\n-- Johannes Merveer', 154 | '"I wish *Mona* was alive to see this masterpiece! I\'m sure she would have *smiled* at it..."\n\n-- Leonarda va Dinci', 155 | '"I\'m sorry, what was that? Ah yes, great app. I use it every *night*. Five *stars*!"\n\n-- Vincent van Vogue', 156 | '"We\'ve all been waiting years for an app to make a *big splash* like this, and now it\'s finally here!\n[Can you hand me that towel please?]"\n\n-- David Hockknee', 157 | '"It makes such a great *impression* on you, doesn\'t it? I know where I\'ll be getting my palette for painting the next *sunrise*!"\n\n-- Cloud Moanet', 158 | '"Maybe some other time... [Can I get a gin and tonic please?]"\n\n-- Edward Jumper', 159 | ] 160 | 161 | title = "[imaginary] **Praise for Sophisticated Palette**\n\n" 162 | # random_index = np.random.randint(len(praise_quotes)) 163 | weights = np.array([2, 3.5, 3, 3, 3, 1]) 164 | weights = weights/np.sum(weights) 165 | 166 | return title + np.random.choice(praise_quotes, p=weights) -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import pandas as pd 4 | import numpy as np 5 | import requests 6 | 7 | from io import BytesIO 8 | from glob import glob 9 | from PIL import Image, ImageEnhance 10 | 11 | import streamlit as st 12 | 13 | sys.path.insert(0, ".") 14 | from sophisticated_palette.utils import show_palette, model_dict, get_palette, \ 15 | sort_func_dict, store_palette, display_matplotlib_code, display_plotly_code,\ 16 | get_df_rgb, enhancement_range, plot_rgb_3d, plot_hsv_3d, print_praise 17 | 18 | 19 | gallery_files = glob(os.path.join(".", "images", "*")) 20 | gallery_dict = {image_path.split("/")[-1].split(".")[-2].replace("-", " "): image_path 21 | for image_path in gallery_files} 22 | 23 | st.image("logo.jpg") 24 | st.sidebar.title("Sophisticated Palette 🎨") 25 | st.sidebar.caption("Tell your data story with style.") 26 | st.sidebar.markdown("Made by [Siavash Yasini](https://www.linkedin.com/in/siavash-yasini/)") 27 | st.sidebar.caption("Look behind the scenes of Sophisticated Palette [here](https://blog.streamlit.io/create-a-color-palette-from-any-image/).") 28 | 29 | 30 | with st.sidebar.expander("See My Other Streamlit Apps"): 31 | st.caption("Snowflake Cheat Sheet: [App](https://snow-flake-cheat-sheet.streamlit.app/) 🎈, [Blog Post](https://medium.com/snowflake/the-ungifted-amateurs-guide-to-snowflake-449284e4bd72) 📝") 32 | st.caption("Wordler: [App](https://wordler.streamlit.app/) 🎈, [Blog Post](https://blog.streamlit.io/the-ultimate-wordle-cheat-sheet/) 📝") 33 | st.caption("Koffee of the World: [App](https://koffee.streamlit.app/) 🎈") 34 | 35 | st.sidebar.markdown("---") 36 | 37 | toggle = st.sidebar.checkbox("Toggle Update", value=True, help="Continuously update the pallete with every change in the app.") 38 | click = st.sidebar.button("Find Palette", disabled=bool(toggle)) 39 | 40 | st.sidebar.markdown("---") 41 | st.sidebar.header("Settings") 42 | palette_size = int(st.sidebar.number_input("palette size", min_value=1, max_value=20, value=5, step=1, help="Number of colors to infer from the image.")) 43 | sample_size = int(st.sidebar.number_input("sample size", min_value=5, max_value=3000, value=500, step=500, help="Number of sample pixels to pick from the image.")) 44 | 45 | # Image Enhancement 46 | enhancement_categories = enhancement_range.keys() 47 | enh_expander = st.sidebar.expander("Image Enhancements", expanded=False) 48 | with enh_expander: 49 | 50 | if st.button("reset"): 51 | for cat in enhancement_categories: 52 | if f"{cat}_enhancement" in st.session_state: 53 | st.session_state[f"{cat}_enhancement"] = 1.0 54 | enhancement_factor_dict = { 55 | cat: enh_expander.slider(f"{cat} Enhancement", 56 | value=1., 57 | min_value=enhancement_range[cat][0], 58 | max_value=enhancement_range[cat][1], 59 | step=enhancement_range[cat][2], 60 | key=f"{cat}_enhancement") 61 | for cat in enhancement_categories 62 | } 63 | enh_expander.info("**Try the following**\n\nColor Enhancements = 2.6\n\nContrast Enhancements = 1.1\n\nBrightness Enhancements = 1.1") 64 | 65 | # Clustering Model 66 | model_name = st.sidebar.selectbox("machine learning model", model_dict.keys(), help="Machine Learning model to use for clustering pixels and colors together.") 67 | sklearn_info = st.sidebar.empty() 68 | 69 | sort_options = sorted(list(sort_func_dict.keys()) + [key + "_r" for key in sort_func_dict.keys() if key!="random"]) 70 | sort_func = st.sidebar.selectbox("palette sort function", options=sort_options, index=5) 71 | 72 | # Random Number Seed 73 | seed = int(st.sidebar.number_input("random seed", value=42, help="Seed used for all random samplings.")) 74 | np.random.seed(seed) 75 | st.sidebar.markdown("---") 76 | 77 | 78 | # ======= 79 | # App 80 | # ======= 81 | 82 | # provide options to either select an image form the gallery, upload one, or fetch from URL 83 | gallery_tab, upload_tab, url_tab = st.tabs(["Gallery", "Upload", "Image URL"]) 84 | with gallery_tab: 85 | options = list(gallery_dict.keys()) 86 | file_name = st.selectbox("Select Art", 87 | options=options, index=options.index("Mona Lisa (Leonardo da Vinci)")) 88 | file = gallery_dict[file_name] 89 | 90 | if st.session_state.get("file_uploader") is not None: 91 | st.warning("To use the Gallery, remove the uploaded image first.") 92 | if st.session_state.get("image_url") not in ["", None]: 93 | st.warning("To use the Gallery, remove the image URL first.") 94 | 95 | img = Image.open(file) 96 | 97 | with upload_tab: 98 | file = st.file_uploader("Upload Art", key="file_uploader") 99 | if file is not None: 100 | try: 101 | img = Image.open(file) 102 | except: 103 | st.error("The file you uploaded does not seem to be a valid image. Try uploading a png or jpg file.") 104 | if st.session_state.get("image_url") not in ["", None]: 105 | st.warning("To use the file uploader, remove the image URL first.") 106 | 107 | with url_tab: 108 | url_text = st.empty() 109 | 110 | # FIXME: the button is a bit buggy, but it's worth fixing this later 111 | 112 | # url_reset = st.button("Clear URL", key="url_reset") 113 | # if url_reset and "image_url" in st.session_state: 114 | # st.session_state["image_url"] = "" 115 | # st.write(st.session_state["image_url"]) 116 | 117 | url = url_text.text_input("Image URL", key="image_url") 118 | 119 | if url!="": 120 | try: 121 | response = requests.get(url) 122 | img = Image.open(BytesIO(response.content)) 123 | except: 124 | st.error("The URL does not seem to be valid.") 125 | 126 | # convert RGBA to RGB if necessary 127 | n_dims = np.array(img).shape[-1] 128 | if n_dims == 4: 129 | background = Image.new("RGB", img.size, (255, 255, 255)) 130 | background.paste(img, mask=img.split()[3]) # 3 is the alpha channel 131 | img = background 132 | 133 | # apply image enhancements 134 | for cat in enhancement_categories: 135 | img = getattr(ImageEnhance, cat)(img) 136 | img = img.enhance(enhancement_factor_dict[cat]) 137 | 138 | # show the image 139 | with st.expander("🖼 Artwork", expanded=True): 140 | st.image(img, use_column_width=True) 141 | 142 | 143 | if click or toggle: 144 | 145 | df_rgb = get_df_rgb(img, sample_size) 146 | 147 | # (optional for later) 148 | # plot_rgb_3d(df_rgb) 149 | # plot_hsv_3d(df_rgb) 150 | 151 | # calculate the RGB palette and cache it to session_state 152 | st.session_state["palette_rgb"] = get_palette(df_rgb, model_name, palette_size, sort_func=sort_func) 153 | 154 | if "palette_rgb" in st.session_state: 155 | 156 | # store individual colors in session state 157 | store_palette(st.session_state["palette_rgb"]) 158 | 159 | st.write("---") 160 | 161 | # sort the colors based on the selected option 162 | colors = {k: v for k, v in st.session_state.items() if k.startswith("col_")} 163 | sorted_colors = {k: colors[k] for k in sorted(colors, key=lambda k: int(k.split("_")[-1]))} 164 | 165 | # find the hex representation for matplotlib and plotly settings 166 | palette_hex = [color for color in sorted_colors.values()][:palette_size] 167 | with st.expander("Adopt this Palette", expanded=False): 168 | st.pyplot(show_palette(palette_hex)) 169 | 170 | matplotlib_tab, plotly_tab = st.tabs(["matplotlib", "plotly"]) 171 | 172 | with matplotlib_tab: 173 | display_matplotlib_code(palette_hex) 174 | 175 | import matplotlib as mpl 176 | from cycler import cycler 177 | 178 | mpl.rcParams["axes.prop_cycle"] = cycler(color=palette_hex) 179 | import matplotlib.pyplot as plt 180 | 181 | x = np.arange(5) 182 | y_list = np.random.random((len(palette_hex), 5))+2 183 | df = pd.DataFrame(y_list).T 184 | 185 | area_tab, bar_tab = st.tabs(["area chart", "bar chart"]) 186 | 187 | with area_tab: 188 | fig_area , ax_area = plt.subplots() 189 | df.plot(kind="area", ax=ax_area, backend="matplotlib", ) 190 | st.header("Example Area Chart") 191 | st.pyplot(fig_area) 192 | 193 | with bar_tab: 194 | fig_bar , ax_bar = plt.subplots() 195 | df.plot(kind="bar", ax=ax_bar, stacked=True, backend="matplotlib", ) 196 | st.header("Example Bar Chart") 197 | st.pyplot(fig_bar) 198 | 199 | 200 | with plotly_tab: 201 | display_plotly_code(palette_hex) 202 | 203 | import plotly.io as pio 204 | import plotly.graph_objects as go 205 | pio.templates["sophisticated"] = go.layout.Template( 206 | layout=go.Layout( 207 | colorway=palette_hex 208 | ) 209 | ) 210 | pio.templates.default = 'sophisticated' 211 | 212 | area_tab, bar_tab = st.tabs(["area chart", "bar chart"]) 213 | 214 | with area_tab: 215 | fig_area = df.plot(kind="area", backend="plotly", ) 216 | st.header("Example Area Chart") 217 | st.plotly_chart(fig_area, use_container_width=True) 218 | 219 | with bar_tab: 220 | fig_bar = df.plot(kind="bar", backend="plotly", barmode="stack") 221 | st.header("Example Bar Chart") 222 | st.plotly_chart(fig_bar, use_container_width=True) 223 | 224 | 225 | else: 226 | st.info("👈 Click on 'Find Palette' ot turn on 'Toggle Update' to see the color palette.") 227 | 228 | st.sidebar.success(print_praise()) 229 | st.sidebar.write("---\n") 230 | st.sidebar.caption("""You can check out the source code [here](https://github.com/syasini/sophisticated_palette). 231 | The `matplotlib` and `plotly` code snippets have been borrowed from [here](https://matplotlib.org/stable/users/prev_whats_new/dflt_style_changes.html) and [here](https://stackoverflow.com/questions/63011674/plotly-how-to-change-the-default-color-pallete-in-plotly).""") 232 | st.sidebar.write("---\n") 233 | 234 | --------------------------------------------------------------------------------