commit b17c82bf2a5e34b431e46b0270ceebd0b5d74391 Author: Andreew Gregory Date: Fri Sep 19 14:47:35 2025 +0300 extracting color themes (very poorly) diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..141b8f1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +.idea +.venv +__pycache__ +answer.png +thumbnails +themes +monitor.png + diff --git a/apply_colorscheme.py b/apply_colorscheme.py new file mode 100644 index 0000000..99b2969 --- /dev/null +++ b/apply_colorscheme.py @@ -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 + diff --git a/extract_colors.py b/extract_colors.py new file mode 100644 index 0000000..3361acc --- /dev/null +++ b/extract_colors.py @@ -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) \ No newline at end of file diff --git a/wallpaper/anime/1.jpg b/wallpaper/anime/1.jpg new file mode 100644 index 0000000..0dd3730 Binary files /dev/null and b/wallpaper/anime/1.jpg differ diff --git a/wallpaper/anime/2.jpg b/wallpaper/anime/2.jpg new file mode 100644 index 0000000..f2e5a4e Binary files /dev/null and b/wallpaper/anime/2.jpg differ diff --git a/wallpaper/anime/3.jpg b/wallpaper/anime/3.jpg new file mode 100644 index 0000000..a854476 Binary files /dev/null and b/wallpaper/anime/3.jpg differ diff --git a/wallpaper/anime/4.jpg b/wallpaper/anime/4.jpg new file mode 100644 index 0000000..e39f283 Binary files /dev/null and b/wallpaper/anime/4.jpg differ diff --git a/wallpaper/anime/5.jpg b/wallpaper/anime/5.jpg new file mode 100644 index 0000000..571fba3 Binary files /dev/null and b/wallpaper/anime/5.jpg differ diff --git a/wallpaper/anime/6.jpg b/wallpaper/anime/6.jpg new file mode 100644 index 0000000..c3fa850 Binary files /dev/null and b/wallpaper/anime/6.jpg differ diff --git a/wallpaper/anime/7.jpg b/wallpaper/anime/7.jpg new file mode 100644 index 0000000..75a15bc Binary files /dev/null and b/wallpaper/anime/7.jpg differ diff --git a/wallpaper/anime/8.jpg b/wallpaper/anime/8.jpg new file mode 100644 index 0000000..bc9ccd3 Binary files /dev/null and b/wallpaper/anime/8.jpg differ diff --git a/wallpaper/anime/9.jpg b/wallpaper/anime/9.jpg new file mode 100644 index 0000000..586657b Binary files /dev/null and b/wallpaper/anime/9.jpg differ