Last active
March 10, 2024 20:50
-
-
Save todbot/0bf32a6bf8dd21983a32bafc173b3223 to your computer and use it in GitHub Desktop.
Color summarizer / color palette finder for CircuitPython
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# code.py example for color_palette_finder.py | |
# 9 Mar 2024 - @todbot / Tod Kurt | |
# Needs 'color_palette_finder.py' library | |
# video demo: https://youtu.be/dSR6IVxeaTg | |
import time | |
import board | |
import displayio | |
import vectorio | |
import gc | |
from color_palette_finder import load_jpeg_to_bitmap, color_palette_for_bitmap | |
# the jpegs we'll create color summaries for | |
jpeg_fnames = ( | |
"/imgs/test1.jpg", | |
"/imgs/compose_plus_RGB.jpg", | |
"/imgs/Mixed-forest-256.jpg", | |
"/imgs/album_cover1.jpg", | |
"/imgs/The_Uknown.jpg", | |
"/imgs/Adirondacks_in_May_2008-256.jpg", | |
) | |
# text only demo | |
# while True: | |
# jpeg_fname = jpeg_fnames[3] | |
# print("loading jpeg", jpeg_fname) | |
# bitmap = load_jpeg_to_bitmap(jpeg_fname) # ,bitmap) maybe? | |
# print("getting colors") | |
# color_palette = color_palette_for_bitmap(bitmap) | |
# print("color_palette len:", len(color_palette)) | |
# print("color_palette =", color_palette) | |
# time.sleep(1) | |
display = board.DISPLAY | |
main_group = displayio.Group() | |
display.root_group = main_group | |
# start out with a blank screen, we'll replace later | |
bitmap = displayio.Bitmap(240, 240, 65535) | |
pixel_shader = displayio.ColorConverter(input_colorspace=displayio.Colorspace.RGB565_SWAPPED) | |
tile_grid = displayio.TileGrid(bitmap, pixel_shader=pixel_shader) | |
main_group.append(tile_grid) | |
# separator between image and color swatches | |
mypal = displayio.Palette(1) | |
mypal[0] = 0x888888 | |
square_sep = vectorio.Rectangle(pixel_shader=mypal, width=240, height=2, x=0, y=198) | |
main_group.append(square_sep) | |
# make our little squares that hold the colors we find | |
num_swatches = 8 | |
swatches = displayio.Group() | |
main_group.append(swatches) | |
for i in range(num_swatches): | |
mypal = displayio.Palette(1) | |
mypal[0] = 0x1a1a1a * i | |
swatch = vectorio.Rectangle(pixel_shader=mypal, width=29, height=40, x=i*30, y=200) | |
swatches.append(swatch) | |
def slide_show(time_delay=1): | |
for jpeg_fname in jpeg_fnames: | |
# reset color palette swatches | |
for swatch in swatches: | |
swatch.pixel_shader[0] = 0x000000 | |
print("----\njpeg file:", jpeg_fname) | |
bitmap = load_jpeg_to_bitmap(jpeg_fname) | |
tile_grid = displayio.TileGrid(bitmap, pixel_shader=pixel_shader) | |
main_group[0] = tile_grid | |
dt = time.monotonic() | |
color_palette = color_palette_for_bitmap(bitmap) | |
dt = time.monotonic() - dt | |
print("time to process: %.2f secs" % dt) | |
print("color_palette len:", len(color_palette)) | |
print("color_palette =", color_palette) | |
# update our palette swatches at the bottom of the screen | |
colors = list(color_palette.keys()) | |
for i in range(num_swatches): | |
c = colors[i] if i < len(colors) else 0x000000 | |
swatches[i].pixel_shader[0] = c | |
gc.collect() | |
time.sleep(time_delay) | |
while True: | |
slide_show(time_delay=2) | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# color_palette_finder.py -- Attempt to find most popular colors in a (JPEG) image | |
# 9 Mar 2024 - @todbot / Tod Kurt | |
# | |
import math | |
import ulab.numpy as np | |
import displayio | |
import jpegio | |
def load_jpeg_to_bitmap(jpeg_fname): | |
"""Load a JPEG into a displayio.Bitmap""" | |
decoder = jpegio.JpegDecoder() | |
width, height = decoder.open(jpeg_fname) | |
bitmap = displayio.Bitmap(width, height, 65535) # RGB565_SWAPPED is 16-bit | |
decoder.decode(bitmap) | |
return bitmap | |
def rgb565_to_rgb888(v, swapped=True): | |
"""Convert RGB565 color int (normal or swapped) to RGB888 tuple""" | |
if swapped: | |
v = ((v & 0xff) << 8) | ((v >> 8) & 0xff) | |
r8 = (v >> 11) << 3 | |
g8 = ((v >> 5) << 2) & 0xff | |
b8 = (v << 3) & 0xff | |
return r8, g8, b8 | |
def count_colors_in_bitmap(bitmap, min_count=5): | |
"""Count the number of colors in a bitmap, ignoring colors with counts | |
less than 'min_count'. Does not need to know about colorspace. | |
Returns a dict with key=color, value=count""" | |
color_counts = {} | |
last_color = bitmap[0] | |
for y in range(bitmap.height): | |
for x in range(bitmap.width): | |
c = bitmap[x,y] | |
if c == last_color: # use spatial-locality of pixels to save work | |
color_counts[c] = 1 + color_counts.get(c,0) | |
else: | |
last_color = c | |
# filter out the <min_count colors | |
color_counts = dict(filter(lambda x: x[1] > min_count, color_counts.items())) | |
return color_counts # dict: key = color, val = count | |
def color_distance(c1,c2): | |
"""Euclidean distance between two RGB888 colors""" | |
r1,g1,b1 = c1 | |
r2,g2,b2 = c2 | |
color_distance = math.sqrt( (r2-r1)**2 + (g2-g1)**2 + (b2-b1)**2 ) | |
return color_distance | |
def color_distance_manhattan(c1,c2): | |
"""Manhattan color distance, not as accurate but much faster""" | |
r1,g1,b1 = c1 | |
r2,g2,b2 = c2 | |
color_distance_man = abs(r2-r1) + abs(g2-g1) + abs(b2-b1) | |
return color_distance_man | |
def color_palette_for_bitmap(bitmap, similarity=30, min_percent=0.01, min_count=5): | |
"""For a given bitmap in RGB565_SWAPPED colorspace, determine | |
most common colors based on color distance ('similarity') and | |
the occurrence ('min_count'). | |
Returns dict of common colors, keys = color, val = popularity | |
""" | |
colorval_counts = count_colors_in_bitmap(bitmap, min_count) | |
print("num colorval_counts:", len(colorval_counts)) | |
# convert colors (in RGB565_SWAPPED colorspae) to RGB888 for similarity analysis | |
color_counts = {} # bins of colors, key = color, val = popularity | |
for c in colorval_counts: | |
crgb = rgb565_to_rgb888(c) # convert colorval to RGB88 colorspace tuple | |
color_counts[crgb] = colorval_counts[c] # copy over popularity count | |
# sort colors by count | |
colors_ranked = sorted(color_counts.keys(), key=lambda x: color_counts[x], reverse=True) | |
# holder for final binning of colors based on similarity | |
color_bins = {} | |
print("finding similar colors") | |
# this is O(n^2) blech, there must be smarter way | |
for c1 in colors_ranked: # go through each color, from most common | |
color_bins[c1] = 0 | |
for c2 in colors_ranked: # and compare against every other color | |
# sum up counts based on similarity | |
#if color_distance(c1,c2) < similarity: | |
if color_distance_manhattan(c1,c2) < similarity: | |
color_bins[c1] += color_counts[c2] | |
color_counts[c2] = 0 # indicate we used it up | |
# only allow percentage of most popular color | |
max_rank = color_bins[colors_ranked[0]] | |
min_count = max_rank * min_percent | |
# filter out low-popularity / zero count colors | |
color_bins = dict(filter(lambda x: x[1] > min_count, color_bins.items())) | |
return color_bins |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Video demo:
https://youtu.be/dSR6IVxeaTg