ASCII-Filter/ctif.py
2024-09-12 13:30:54 -05:00

229 lines
5.4 KiB
Python

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)