extracting color themes (very poorly)
8
.gitignore
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
.idea
|
||||
.venv
|
||||
__pycache__
|
||||
answer.png
|
||||
thumbnails
|
||||
themes
|
||||
monitor.png
|
||||
|
62
apply_colorscheme.py
Normal file
@ -0,0 +1,62 @@
|
||||
import re
|
||||
import sys
|
||||
import argparse
|
||||
import os
|
||||
import stat
|
||||
from dataclasses import dataclass
|
||||
from typing import List, Union, Tuple
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
# One element per monitor
|
||||
# swaybar: background
|
||||
sway_color_bar_background = 0
|
||||
# swaybar: statusline
|
||||
sway_color_bar_statusline = 1
|
||||
#swaybar: focused_background
|
||||
sway_color_bar_focused_background = 2
|
||||
#swaybar: focused_statusbar
|
||||
sway_color_bar_focused_statusline = 3
|
||||
#swaybar: focused_workspace
|
||||
sway_color_bar_workspace_focused_border = 4
|
||||
sway_color_bar_workspace_focused_background = 5
|
||||
sway_color_bar_workspace_focused_text = 6
|
||||
#swaybar: active_workspace
|
||||
sway_color_bar_workspace_active_border = 7
|
||||
sway_color_bar_workspace_active_background = 8
|
||||
sway_color_bar_workspace_active_text = 9
|
||||
#swaybar: inactive_workspace
|
||||
sway_color_bar_workspace_inactive_border = 10
|
||||
sway_color_bar_workspace_inactive_background = 11
|
||||
sway_color_bar_workspace_inactive_text = 12
|
||||
#swaybar: urgent_workspace
|
||||
sway_color_bar_workspace_urgent_border = 13
|
||||
sway_color_bar_workspace_urgent_background = 14
|
||||
sway_color_bar_workspace_urgent_text = 15
|
||||
#swaybar: binding_mode
|
||||
sway_color_bar_mode_indicator_border = 16
|
||||
sway_color_bar_mode_indicator_background = 17
|
||||
sway_color_bar_mode_indicator_text = 18
|
||||
# n when constructing my ColorSchemeSearchConf
|
||||
sway_color_n = 19
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
arg_parser = argparse.ArgumentParser(description="Activate a precompiled colorscheme for sway")
|
||||
# todo: continue from here (We almost finished)
|
||||
arg_parser.add_argument('wallpaper_path')
|
||||
arg_parser.add_argument('themes_path', help='Path to the colorscheme file')
|
||||
arg_parser.add_argument('config_output_path', help='Path for the output config file')
|
||||
arg_parser.add_argument('bar_name', help="Which bar to configure", nargs='?', default='*')
|
||||
|
||||
# Optional flag arguments (no arguments)
|
||||
arg_parser.add_argument('--message', action='store_true', help='Use swaymsg to send colorscheme immediately')
|
||||
args = arg_parser.parse_args()
|
||||
|
||||
with open(args.colorscheme_path, 'r') as f:
|
||||
colors = list(filter(lambda line: len(line) > 0, f.readlines()))
|
||||
|
||||
config_text = f"""bar {args.bar_name} colors """
|
||||
assert len(colors) == sway_color_n
|
||||
|
319
extract_colors.py
Normal file
@ -0,0 +1,319 @@
|
||||
import re
|
||||
import sys
|
||||
import argparse
|
||||
import os
|
||||
import stat
|
||||
from math import *
|
||||
from dataclasses import dataclass
|
||||
from typing import List, Union, Tuple
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
import random
|
||||
|
||||
from PIL import Image, ImageDraw
|
||||
import numpy as np
|
||||
|
||||
def read_image(filename):
|
||||
rgb_img = Image.open(filename).convert('RGB')
|
||||
pixel_array = np.array(rgb_img, dtype=np.float32) / 255.0
|
||||
return pixel_array
|
||||
|
||||
# We only care about color distribution
|
||||
def read_and_compress_image(filename):
|
||||
img = Image.open(filename).convert('RGB')
|
||||
width = min(img.width, 50)
|
||||
height = min(img.height, 50)
|
||||
img = img.resize((width, height))
|
||||
pixel_array = np.array(img, dtype=np.float32) / 255.0
|
||||
return pixel_array
|
||||
|
||||
def conv_color(color: List[float]) -> Tuple[int, int, int, int]:
|
||||
return (int(round(255 * color[0])), int(round(255 * color[1])), int(round(255 * color[2])), 255)
|
||||
|
||||
def create_color_row_image(colors, output_filename, rect_width = 5, rect_height = 3):
|
||||
num_colors = len(colors)
|
||||
assert num_colors > 0
|
||||
img_width = num_colors * rect_width + (num_colors - 1)
|
||||
img_height = rect_height
|
||||
new_image = Image.new('RGBA', (img_width, img_height))
|
||||
pixels = new_image.load()
|
||||
|
||||
for X, color in enumerate(colors):
|
||||
for i in range(rect_width):
|
||||
for j in range(rect_height):
|
||||
pixels[X + X * rect_width + i, j] = conv_color(color)
|
||||
|
||||
new_image.save(output_filename)
|
||||
print(f"Image saved as {output_filename}")
|
||||
|
||||
def euclidian_distance(c1: List[float], c2: List[float]):
|
||||
return sqrt(sum(map(lambda i: (c1[i] - c2[i]) * (c1[i] - c2[i]), range(3))))
|
||||
|
||||
@dataclass
|
||||
class ColorDistancePrice:
|
||||
def apply(self, c1: List[float], c2: List[float]) -> float:
|
||||
pass
|
||||
|
||||
def show_image_quality(self, c2: List[float], image):
|
||||
new_image = Image.new('RGB', (image.shape[1], image.shape[0]))
|
||||
pixels = new_image.load()
|
||||
for y in range(image.shape[0]):
|
||||
for x in range(image.shape[1]):
|
||||
cost: float = self.apply(image[y, x], c2)
|
||||
cc: int = int(round(255 * cost))
|
||||
pixels[x, y] = (min(cc, 255), max(0, min(cc - 255, 255)), 0)
|
||||
new_image.show()
|
||||
|
||||
|
||||
@dataclass
|
||||
class OmegaPrice(ColorDistancePrice):
|
||||
d: float
|
||||
|
||||
def apply(self, c1: List[float], c2: List[float]) -> float:
|
||||
e = euclidian_distance(c1, c2)
|
||||
if e <= self.d:
|
||||
return pow(self.d - e, 2)
|
||||
return 0
|
||||
|
||||
@dataclass
|
||||
class EtaPrice(ColorDistancePrice):
|
||||
d: float
|
||||
|
||||
def apply(self, c1: List[float], c2: List[float]) -> float:
|
||||
e = euclidian_distance(c1, c2)
|
||||
if e <= self.d:
|
||||
return 1 - e / self.d
|
||||
return 1 - exp(self.d - e)
|
||||
|
||||
@dataclass
|
||||
class AlphaPrice(ColorDistancePrice):
|
||||
dd: float
|
||||
dp: float
|
||||
def apply(self, c1: List[float], c2: List[float]) -> float:
|
||||
e = euclidian_distance(c1, c2)
|
||||
if e < self.dd:
|
||||
return 0
|
||||
return self.dp + pow(e / sqrt(3), 3)
|
||||
|
||||
@dataclass
|
||||
class ColorDistanceRule:
|
||||
i: int
|
||||
j: int
|
||||
func: ColorDistancePrice
|
||||
|
||||
def annealing(T: float, cost_delta: float) -> bool:
|
||||
if cost_delta < 0:
|
||||
return True
|
||||
P = exp(-(cost_delta / T))
|
||||
return random.random() < P
|
||||
|
||||
class ColorSchemeSearchConf:
|
||||
def __init__(self, image100: np.ndarray, n, pair_rules: List[ColorDistanceRule], image_pixel_price: ColorDistancePrice):
|
||||
self.n = n
|
||||
self.pair_price = [[[] for i in range(m)] for m in range(n) ]
|
||||
for rule in pair_rules:
|
||||
self.pair_price[max(rule.i, rule.j)][min(rule.i, rule.j)].append(rule.func)
|
||||
self.image_cost_c = len(pair_rules)
|
||||
self.image_pixel_price = image_pixel_price
|
||||
self.image100 = image100
|
||||
|
||||
def drift(self, sample_pos: List[Tuple[int, int]], coef: float) -> List[Tuple[int, int]]:
|
||||
w: int = self.image100.shape[1]
|
||||
h: int = self.image100.shape[0]
|
||||
def drift_point(pos: Tuple[int, int]):
|
||||
return (
|
||||
max(0, min(pos[0] + int(round(coef * (random.random() - .5))), w - 1)),
|
||||
max(0, min(pos[1] + int(round(coef * (random.random() - .5))), h - 1))
|
||||
)
|
||||
|
||||
return list(map(drift_point, sample_pos))
|
||||
|
||||
def sample_image(self, sample_positions: List[Tuple[int, int]]) -> List[List[float]]:
|
||||
return list(map(lambda pos: self.image100[pos[1], pos[0]], sample_positions))
|
||||
|
||||
def evaluate_colorscheme(self, colorscheme: List[List[float]]):
|
||||
w: int = self.image100.shape[1]
|
||||
h: int = self.image100.shape[0]
|
||||
assert len(colorscheme) == self.n
|
||||
C_with_image = 0
|
||||
for y in range(h):
|
||||
for x in range(w):
|
||||
c1 = self.image100[x, y]
|
||||
for i in range(self.n):
|
||||
C_with_image += self.image_pixel_price.apply(c1, colorscheme[i])
|
||||
C_with_image *= (self.image_cost_c / w / h)
|
||||
C_with_each_other = 0
|
||||
for i, func_arr_arr in enumerate(self.pair_price):
|
||||
for j, func_arr in enumerate(func_arr_arr):
|
||||
for func in func_arr:
|
||||
a = func.apply(colorscheme[i], colorscheme[j])
|
||||
C_with_each_other += a
|
||||
return C_with_image + C_with_each_other
|
||||
|
||||
def find_best_colorscheme(self, steps: int, initial_temperature: float):
|
||||
positions = [(0, 0) for i in range(self.n)]
|
||||
colors: List[List[float]] = self.sample_image(positions)
|
||||
cost = self.evaluate_colorscheme(colors)
|
||||
T = initial_temperature
|
||||
for i in range(steps):
|
||||
print(f"Temperature {T}. Cost: {cost}. Positions: {positions}")
|
||||
new_positions = self.drift(positions, 200 * (1 - exp(-T)))
|
||||
new_colorscheme = self.sample_image(new_positions)
|
||||
new_cost = self.evaluate_colorscheme(new_colorscheme)
|
||||
if annealing(T, new_cost - cost):
|
||||
positions = new_positions
|
||||
colors = new_colorscheme
|
||||
cost = new_cost
|
||||
T *= (1 - 10/steps)
|
||||
return colors
|
||||
|
||||
def color_to_hex(clr: List[float]) -> str:
|
||||
return f"#{int(round(255 * clr[0])):02X}{int(round(255 * clr[1])):02X}{int(round(255 * clr[2])):02X}"
|
||||
|
||||
|
||||
# One element per monitor
|
||||
# swaybar: background
|
||||
sway_color_bar_background = 0
|
||||
# swaybar: statusline
|
||||
sway_color_bar_statusline = 1
|
||||
#swaybar: focused_background
|
||||
sway_color_bar_focused_background = 2
|
||||
#swaybar: focused_statusbar
|
||||
sway_color_bar_focused_statusline = 3
|
||||
#swaybar: focused_workspace
|
||||
sway_color_bar_workspace_focused_border = 4
|
||||
sway_color_bar_workspace_focused_background = 5
|
||||
sway_color_bar_workspace_focused_text = 6
|
||||
#swaybar: active_workspace
|
||||
sway_color_bar_workspace_active_border = 7
|
||||
sway_color_bar_workspace_active_background = 8
|
||||
sway_color_bar_workspace_active_text = 9
|
||||
#swaybar: inactive_workspace
|
||||
sway_color_bar_workspace_inactive_border = 10
|
||||
sway_color_bar_workspace_inactive_background = 11
|
||||
sway_color_bar_workspace_inactive_text = 12
|
||||
#swaybar: urgent_workspace
|
||||
sway_color_bar_workspace_urgent_border = 13
|
||||
sway_color_bar_workspace_urgent_background = 14
|
||||
sway_color_bar_workspace_urgent_text = 15
|
||||
#swaybar: binding_mode
|
||||
sway_color_bar_mode_indicator_border = 16
|
||||
sway_color_bar_mode_indicator_background = 17
|
||||
sway_color_bar_mode_indicator_text = 18
|
||||
# n when constructing my ColorSchemeSearchConf
|
||||
sway_color_n = 19
|
||||
|
||||
def sway_monitor_image(scheme, output_filename):
|
||||
lwksp = 20
|
||||
gap = 5
|
||||
ltxt = 10
|
||||
|
||||
ls = (lwksp + gap * 3 + ltxt)
|
||||
wksp_gap = (lwksp - ltxt) / 2
|
||||
img = Image.new('RGBA', (5 * lwksp + 6 * gap, 2 * ls))
|
||||
draw = ImageDraw.Draw(img)
|
||||
def draw_section(i, ho):
|
||||
draw.rectangle([(0, i * ls), (5 * lwksp + 6 * gap, i * ls + ls)], fill=conv_color(scheme[ho]))
|
||||
draw.rectangle([(gap, i * ls + 2 * gap + lwksp), (5 * lwksp + 5 * gap, i * ls + 2 * gap + lwksp + ltxt)], fill=conv_color(scheme[ho+1]))
|
||||
def draw_wksp(j, brdr):
|
||||
draw.rectangle([((j+1) * gap + j * lwksp, ls * i + gap), ((j+1) * gap + j * lwksp + lwksp, ls * i + gap + lwksp)], fill=conv_color(scheme[brdr + 1]), outline=conv_color(scheme[brdr]))
|
||||
draw.rectangle([((j+1) * gap + j * lwksp + wksp_gap, ls * i + gap + wksp_gap),
|
||||
((j+1) * gap + j * lwksp + wksp_gap + ltxt, ls * i + gap + wksp_gap + ltxt)],
|
||||
fill=conv_color(scheme[brdr + 2]))
|
||||
draw_wksp(0, sway_color_bar_workspace_focused_border)
|
||||
draw_wksp(1, sway_color_bar_workspace_active_border)
|
||||
draw_wksp(2, sway_color_bar_workspace_inactive_border)
|
||||
draw_wksp(3, sway_color_bar_workspace_urgent_border)
|
||||
draw_wksp(4, sway_color_bar_mode_indicator_border)
|
||||
|
||||
draw_section(0, sway_color_bar_background)
|
||||
draw_section(1, sway_color_bar_focused_background)
|
||||
img.save(output_filename)
|
||||
print(f"Image saved as {output_filename}")
|
||||
|
||||
|
||||
def get_sway_search_conf(image100: np.ndarray) -> ColorSchemeSearchConf:
|
||||
def wild_trio(first_ind):
|
||||
return [
|
||||
ColorDistanceRule(first_ind + 0, first_ind + 1, OmegaPrice(0.2)),
|
||||
ColorDistanceRule(first_ind + 1, first_ind + 2, OmegaPrice(0.6)),
|
||||
ColorDistanceRule(first_ind + 0, first_ind + 2, OmegaPrice(0.5)),
|
||||
]
|
||||
|
||||
rules = [
|
||||
ColorDistanceRule(sway_color_bar_background, sway_color_bar_statusline, OmegaPrice(10)),
|
||||
ColorDistanceRule(sway_color_bar_focused_background, sway_color_bar_focused_statusline, OmegaPrice(10)),
|
||||
ColorDistanceRule(sway_color_bar_background, sway_color_bar_focused_background, OmegaPrice(0.2)),
|
||||
ColorDistanceRule(sway_color_bar_statusline, sway_color_bar_focused_statusline, OmegaPrice(0.2)),
|
||||
|
||||
ColorDistanceRule(sway_color_bar_mode_indicator_background, sway_color_bar_workspace_urgent_background, EtaPrice(0.025)),
|
||||
|
||||
ColorDistanceRule(sway_color_bar_workspace_focused_background, sway_color_bar_workspace_active_background, OmegaPrice(0.4)),
|
||||
ColorDistanceRule(sway_color_bar_workspace_focused_background, sway_color_bar_workspace_inactive_background, OmegaPrice(0.4)),
|
||||
ColorDistanceRule(sway_color_bar_workspace_active_background, sway_color_bar_workspace_inactive_background, OmegaPrice(0.1)),
|
||||
|
||||
ColorDistanceRule(sway_color_bar_background, sway_color_bar_workspace_inactive_background, OmegaPrice(0.07)),
|
||||
ColorDistanceRule(sway_color_bar_focused_background, sway_color_bar_workspace_inactive_background, OmegaPrice(0.07)),
|
||||
ColorDistanceRule(sway_color_bar_background, sway_color_bar_workspace_active_background, OmegaPrice(0.07)),
|
||||
ColorDistanceRule(sway_color_bar_focused_background, sway_color_bar_workspace_active_background, OmegaPrice(0.07)),
|
||||
ColorDistanceRule(sway_color_bar_background, sway_color_bar_workspace_focused_background, OmegaPrice(0.07)),
|
||||
ColorDistanceRule(sway_color_bar_focused_background, sway_color_bar_workspace_focused_background,OmegaPrice(0.07)),
|
||||
]
|
||||
rules += wild_trio(sway_color_bar_workspace_focused_border)
|
||||
rules += wild_trio(sway_color_bar_workspace_active_border)
|
||||
rules += wild_trio(sway_color_bar_workspace_inactive_border)
|
||||
rules += wild_trio(sway_color_bar_workspace_urgent_border)
|
||||
rules += wild_trio(sway_color_bar_mode_indicator_border)
|
||||
return ColorSchemeSearchConf(image100, sway_color_n, rules, AlphaPrice(0.1, 0.5))
|
||||
|
||||
|
||||
def extract_colors_for_sway_complete_io(source_path, dest_colorscheme_path, dest_thumbnail_path):
|
||||
image100 = read_and_compress_image(source_path)
|
||||
print(f"Image shape: {image100.shape}", file=sys.stderr)
|
||||
searcher = get_sway_search_conf(image100)
|
||||
colorscheme = searcher.find_best_colorscheme(20, 1000)
|
||||
|
||||
with open(dest_colorscheme_path, 'a') as f:
|
||||
for i in range(searcher.n):
|
||||
print(color_to_hex(colorscheme[i]), file=f)
|
||||
|
||||
sway_monitor_image(colorscheme, dest_thumbnail_path)
|
||||
|
||||
|
||||
def compile_a_directory(source_dir, target_txt_dir, target_thumbnail_path):
|
||||
image_extensions = {'.jpg', '.jpeg', '.png', '.webp'}
|
||||
source_path = Path(source_dir)
|
||||
target_txt_path = Path(target_txt_dir)
|
||||
target_thumbnail_path = Path(target_thumbnail_path)
|
||||
|
||||
# Walk through all directories and files
|
||||
for root, dirs, files in os.walk(source_dir):
|
||||
root_path = Path(root)
|
||||
|
||||
for file in files:
|
||||
file_path = root_path / file
|
||||
file_ext = file_path.suffix.lower()
|
||||
if file_ext in image_extensions:
|
||||
# Create corresponding target directory and colorscheme
|
||||
relative_path = file_path.relative_to(source_path)
|
||||
target_txt_dir_path = target_txt_path / relative_path.parent
|
||||
target_txt_dir_path.mkdir(parents=True, exist_ok=True)
|
||||
target_txt_file_path = target_txt_dir_path / f"{file_path.stem}.txt"
|
||||
target_thumbnail_dir_path = target_thumbnail_path / relative_path.parent
|
||||
target_thumbnail_dir_path.mkdir(parents=True, exist_ok=True)
|
||||
target_thumbnail_file_path = target_thumbnail_dir_path / f"{file_path.stem}.png"
|
||||
print(str(file_path), "To", str(target_txt_file_path), "And", str(target_thumbnail_file_path))
|
||||
extract_colors_for_sway_complete_io(str(file_path), str(target_txt_file_path), str(target_thumbnail_file_path))
|
||||
|
||||
if __name__ == "__main__":
|
||||
# parser_arg = argparse.ArgumentParser(description="Extract colors from image")
|
||||
# parser_arg.add_argument("input_file", help="Path to the input image file")
|
||||
# parser_arg.add_argument("color_file", help="Path for color blocks output image", nargs='?', default=None)
|
||||
# parser_arg.add_argument("monitor_file", help="Path for monitor thumbnail output image", nargs='?', default=None)
|
||||
# args = parser_arg.parse_args()
|
||||
parser_arg = argparse.ArgumentParser(description="Extract colors from a set of images")
|
||||
parser_arg.add_argument("source_dir", help="Path to directory with input files")
|
||||
parser_arg.add_argument("dest_txt_dir", help="Path to output directory for colorscheme specs")
|
||||
parser_arg.add_argument("dest_thumbnail_dir", help="Path to output directory for thumbnail of sway theme")
|
||||
args = parser_arg.parse_args()
|
||||
compile_a_directory(args.source_dir, args.dest_txt_dir, args.dest_thumbnail_dir)
|
BIN
wallpaper/anime/1.jpg
Normal file
After Width: | Height: | Size: 32 KiB |
BIN
wallpaper/anime/2.jpg
Normal file
After Width: | Height: | Size: 978 KiB |
BIN
wallpaper/anime/3.jpg
Normal file
After Width: | Height: | Size: 29 KiB |
BIN
wallpaper/anime/4.jpg
Normal file
After Width: | Height: | Size: 37 KiB |
BIN
wallpaper/anime/5.jpg
Normal file
After Width: | Height: | Size: 9.6 KiB |
BIN
wallpaper/anime/6.jpg
Normal file
After Width: | Height: | Size: 10 KiB |
BIN
wallpaper/anime/7.jpg
Normal file
After Width: | Height: | Size: 17 KiB |
BIN
wallpaper/anime/8.jpg
Normal file
After Width: | Height: | Size: 35 KiB |
BIN
wallpaper/anime/9.jpg
Normal file
After Width: | Height: | Size: 534 KiB |