Skip to content

Instantly share code, notes, and snippets.

@todbot
Last active March 10, 2024 20:50
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save todbot/0bf32a6bf8dd21983a32bafc173b3223 to your computer and use it in GitHub Desktop.
Save todbot/0bf32a6bf8dd21983a32bafc173b3223 to your computer and use it in GitHub Desktop.
Color summarizer / color palette finder for CircuitPython
# 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)
# 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
@todbot
Copy link
Author

todbot commented Mar 10, 2024

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment