├── .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 |
--------------------------------------------------------------------------------