colors from wallpapers
This commit is contained in:
152
home-modules/wallpaper-to-colors.nix
Normal file
152
home-modules/wallpaper-to-colors.nix
Normal file
@@ -0,0 +1,152 @@
|
||||
{ pkgs-unstable, ... }:
|
||||
{
|
||||
home.packages = with pkgs-unstable; [
|
||||
(writers.writePython3Bin "change_colors_json" {
|
||||
libraries = with python3Packages; [ numpy pillow scikit-learn ];
|
||||
flakeIgnore = [ "E302" "E305" "E226" "E501" ];
|
||||
} /*python */ ''
|
||||
from colorsys import hls_to_rgb, rgb_to_hls
|
||||
import json
|
||||
import sys
|
||||
from typing import Literal, cast
|
||||
from numpy.typing import NDArray
|
||||
from sklearn.cluster import KMeans
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
|
||||
def fc(c: int) -> str:
|
||||
assert c < 256
|
||||
s = str(hex(c))[2:]
|
||||
if c < 16:
|
||||
return "0" + s
|
||||
elif len(s) == 1:
|
||||
return s + s
|
||||
else:
|
||||
return s
|
||||
|
||||
class Color(object):
|
||||
def __init__(self, rgb: tuple[int, ...], frequency: float):
|
||||
assert len(rgb) == 3, "RGB values must be a tuple of length 3"
|
||||
self.rgb = cast(tuple[int, int, int], rgb)
|
||||
self.freq: float = frequency
|
||||
|
||||
def __lt__(self, other: "Color") -> bool:
|
||||
return self.freq < other.freq
|
||||
|
||||
@property
|
||||
def hls(self) -> tuple[float, float, float]:
|
||||
return rgb_to_hls(r=self.rgb[0] / 255, g=self.rgb[1] / 255, b=self.rgb[2] / 255)
|
||||
|
||||
@property
|
||||
def luminance(self) -> float:
|
||||
return np.dot(np.array([0.2126, 0.7152, 0.0722]), self.rgb)
|
||||
|
||||
def k_means_extraction(arr: NDArray[float], height: int, width: int, palette_size: int) -> list[Color]:
|
||||
arr = np.reshape(arr, (width * height, -1))
|
||||
model = KMeans(n_clusters=palette_size, n_init="auto", init="k-means++", random_state=2024)
|
||||
labels = model.fit_predict(arr)
|
||||
palette = np.array(model.cluster_centers_, dtype=int)
|
||||
color_count = np.bincount(labels)
|
||||
color_frequency = color_count / float(np.sum(color_count))
|
||||
colors = []
|
||||
for color, freq in zip(palette, color_frequency):
|
||||
colors.append(Color(color, freq))
|
||||
return colors
|
||||
|
||||
|
||||
class Palette:
|
||||
def __init__(self, colors: list[Color]):
|
||||
self.colors = colors
|
||||
self.frequencies = [c.freq for c in colors]
|
||||
|
||||
def __getitem__(self, item: int) -> Color:
|
||||
return self.colors[item]
|
||||
|
||||
def __len__(self) -> int:
|
||||
return self.number_of_colors
|
||||
|
||||
def ensure_color(c: Color, alter_sat: bool) -> list[int]:
|
||||
hue, lum, sat = c.hls
|
||||
if alter_sat:
|
||||
new_sat = min(max(sat, 0.6) + 0.3, 1)
|
||||
else:
|
||||
new_sat = sat
|
||||
new_lum = max(lum, 0.5)
|
||||
r, g, b = hls_to_rgb(h=hue, l=new_lum, s=new_sat)
|
||||
return [int(r*255), int(g*255), int(b*255)]
|
||||
|
||||
def list_to_hex(ilist: list[int]) -> str:
|
||||
return f"#{fc(ilist[0])}{fc(ilist[1])}{fc(ilist[2])}"
|
||||
|
||||
def alter_hue(ilist: list[int], hue: int) -> list[int]:
|
||||
assert hue >= 0 and hue <= 360
|
||||
r, g, b = ilist
|
||||
h, l, s = rgb_to_hls((r/255), (g/255), (b/255))
|
||||
new_hue = (((h*360) + hue) % 360) / 360
|
||||
r, g, b = hls_to_rgb(h=new_hue, l=l, s=s)
|
||||
return [int(r*255), int(g*255), int(b*255)]
|
||||
|
||||
def alter_l(ilist: list[int], l_in_1_0: float) -> list[int]:
|
||||
assert l_in_1_0 >= 0 and l_in_1_0 <= 1
|
||||
r, g, b = ilist
|
||||
h, _, s = rgb_to_hls((r/255), (g/255), (b/255))
|
||||
r, g, b = hls_to_rgb(h=h, l=l_in_1_0, s=s)
|
||||
return [int(r*255), int(g*255), int(b*255)]
|
||||
|
||||
def extract_colors(
|
||||
image: str,
|
||||
palette_size: int = 5,
|
||||
resize: bool = True,
|
||||
sort_mode: Literal["luminance", "frequency"] | None = None,
|
||||
) -> Palette:
|
||||
|
||||
img = Image.open(image).convert("RGB")
|
||||
|
||||
# open the image
|
||||
img = img.resize((256, 256))
|
||||
width, height = img.size
|
||||
arr = np.asarray(img)
|
||||
|
||||
colors = k_means_extraction(arr, height, width, palette_size)
|
||||
|
||||
if sort_mode == "luminance":
|
||||
colors.sort(key=lambda c: c.luminance, reverse=False)
|
||||
else:
|
||||
colors.sort(reverse=True)
|
||||
|
||||
return Palette(colors)
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
img = sys.argv[1]
|
||||
except IndexError:
|
||||
img = "/home/nx2/Pictures/wallpapers/absolute-cinema-acid.png"
|
||||
|
||||
palette = extract_colors(image=img, palette_size=3)
|
||||
|
||||
accent = ensure_color(c=palette[0], alter_sat=False)
|
||||
secondary = ensure_color(c=palette[1], alter_sat=True)
|
||||
tertiary = ensure_color(c=palette[2], alter_sat=False)
|
||||
weird = alter_hue(ilist=secondary, hue=180)
|
||||
special = alter_hue(ilist=accent, hue=180)
|
||||
foreground = alter_l(accent, 0.9)
|
||||
background = alter_l(accent, 0.1)
|
||||
|
||||
d = {
|
||||
"base": {
|
||||
"foreground": list_to_hex(foreground),
|
||||
"background": list_to_hex(background)
|
||||
},
|
||||
"to_alter": {
|
||||
"accent": list_to_hex(accent),
|
||||
"secondary": list_to_hex(secondary),
|
||||
"tertiary": list_to_hex(tertiary),
|
||||
"special": list_to_hex(special),
|
||||
"weird": list_to_hex(weird)
|
||||
}
|
||||
}
|
||||
|
||||
with open("/home/nx2/nix-dots/flake-modules/colors.json", "w") as f:
|
||||
f.write(json.dumps(d, indent=4))
|
||||
'')
|
||||
];
|
||||
}
|
||||
Reference in New Issue
Block a user