import logging import math import os from rich.console import Console from PIL import Image import pygame as pg import filters logging.getLogger(__name__) FONT_PATH = 'fonts/scientifica.ttf' TEXT_WIDTH = 6 # on font size 12 monocraft medium TEXT_HEIGHT = 11 SCALE_FACTOR = 2 RESIZE_FACTOR = 2 SCALED_WIDTH = TEXT_WIDTH // SCALE_FACTOR SCALED_HEIGHT = TEXT_HEIGHT // SCALE_FACTOR DEBUG = True if not os.path.exists('./debug/') and DEBUG: os.makedirs('./debug') # '█@?OPoci. ' CHARS = list(reversed([ u'█', '@', '?', 'O', 'P', 'o', 'c', 'i', '.', ' ' ])) NEW_CHARS = '.:\',`;_<^~%>"?!ir=clsxzL}ovCS(+eknuFJfj{amwPZtIT)*hbdpqyEGKOVXYgU&ABDHQR[]MNW@$#' ALL_CHARS = [ u'█', '@', '?', 'O', 'P', 'o', 'c', 'i', '.', ' ', '|', '/', '-', '\\' ] # Character Text Image Format class CTIF: colors: list text: str width: int height: int def __init__(self, image: Image.Image | None = None): self.colors = [] self.text = "" self.width = 0 self.height = 0 if image: self.convert(image) def convert(self, image: Image.Image) -> None: image = image.resize((image.width * RESIZE_FACTOR, image.height * RESIZE_FACTOR)) logging.debug(f'Image Size: {image.size}') L_image = image.convert('L') logging.debug(f'L_image Size: {L_image.size}') dog = filters.difference_of_gaussians(image) logging.debug(f'Dog Size: {dog.size}') dog.save('debug/dog.png') sb, gradient = filters.sobel(dog) image = image.convert('HSV') w, h = sb.size sb.save('debug/sobel.png') text_grid_width = w // SCALED_WIDTH self.width = text_grid_width text_grind_height = h // SCALED_HEIGHT self.height = text_grind_height y_offset = 0 for y in range(math.floor(h / SCALED_HEIGHT)): x_offset = 0 for x in range(math.floor(w / SCALED_WIDTH)): histogram = {} # Collect the most common char for a group of pixels color = [] for y2 in range(SCALED_HEIGHT): for x2 in range(SCALED_WIDTH): real_x = x2 + x_offset real_y = y2 + y_offset h, s, v = image.getpixel((real_x, real_y)) color.append((h, s, v)) print(h, s, v) gradient_v = gradient[real_y * w + real_x] char = ' ' if gradient_v: char = self._match_gradient(gradient_v) else: char = NEW_CHARS[round(v/(255/len(NEW_CHARS))) - 1] if char in histogram: histogram[char] += 1 else: histogram[char] = 1 color_avg = filters.average_colors(color) self.colors.append(color_avg) # get most common most_common = None score = float('-inf') for char in histogram: if histogram[char] > score: score = histogram[char] most_common = char self.text += str(most_common) x_offset += SCALED_WIDTH y_offset += SCALED_HEIGHT # TODO @0x01FE : refactor plz increment by 30 & 60 def _match_gradient(self, n: float) -> str: n = abs(n) if n < math.radians(30): return '|' elif n < math.radians(60): return '/' elif n < math.radians(120): return '-' elif n < math.radians(150): return '\\' elif n < math.radians(210): return '|' elif n < math.radians(240): return '/' elif n < math.radians(300): return '-' elif n < math.radians(330): return '\\' else: return '|' def render(self) -> None: console = Console() for i, (color, char) in enumerate(zip(self.colors, self.text)): hex_color = '#%x%x%x' % color console.print(f'[{hex_color}]{char}', end='') if i + 1 % self.width == 0: print() def save_image(self, path: str) -> None: pg.init() font = pg.font.Font(FONT_PATH, 12) window = pg.display.set_mode((self.width * TEXT_WIDTH, self.height * TEXT_HEIGHT)) x, y = 0, 0 logging.debug(f'Rendering CITF Image with Dim: ({self.width}, {self.height})') for (color, char) in zip(self.colors, self.text): rendered_char = font.render(char, True, color) window.blit(rendered_char, (x * TEXT_WIDTH, y * TEXT_HEIGHT)) x += 1 if x >= self.width: x = 0 y += 1 pg.display.update() pg.image.save(window, path) pg.quit() def save_citf(self, filename: str) -> None: # 3 byte for color, byte for char b = bytes() for (color, char) in zip(self.colors, self.text): for n in color: b += n.to_bytes(1, 'big') b += ALL_CHARS.index(char).to_bytes(1, 'big') with open(f'{filename}.citf', 'wb+') as file: file.write(b)