326 lines
8.4 KiB
Python
326 lines
8.4 KiB
Python
import colorsys
|
|
import logging
|
|
import math
|
|
import os
|
|
|
|
from rich.console import Console
|
|
from PIL import Image, ImageFilter
|
|
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
|
|
ANTI_ALIASING = False
|
|
|
|
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@$#'
|
|
|
|
EDGE_CHARS = '-\\|/'
|
|
|
|
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:
|
|
if image.mode != 'RGB':
|
|
image = image.convert('RGB')
|
|
|
|
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
|
|
|
|
r, g, b = image.getpixel((real_x, real_y))
|
|
color.append((r, g, b))
|
|
h, s, v = colorsys.rgb_to_hsv(r, g, b)
|
|
# 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
|
|
|
|
r, g, b = filters.average_colors(color)
|
|
color_avg_hsv = colorsys.rgb_to_hsv(r/255, g/255, b/255)
|
|
self.colors.append(color_avg_hsv)
|
|
|
|
# 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:
|
|
logging.debug('Rendering and Saving image...')
|
|
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):
|
|
h, s, v = color
|
|
if char in EDGE_CHARS:
|
|
v = 255
|
|
else:
|
|
v = (NEW_CHARS.find(char) + 1) * (255/len(NEW_CHARS))
|
|
|
|
logging.debug(f'HSV Color: {h, s, v/255}')
|
|
r, g, b = colorsys.hsv_to_rgb(h, s, v/255)
|
|
|
|
|
|
logging.debug(f'RGB Color Before: {r, g, b}')
|
|
r *= 255
|
|
g *= 255
|
|
|
|
r = min(r, 255)
|
|
g = min(g, 255)
|
|
logging.debug(f'RGB Color: {r, g, b}')
|
|
rendered_char = font.render(char, ANTI_ALIASING, (int(r), int(g), int(b)))
|
|
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_edges(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):
|
|
|
|
if char in EDGE_CHARS:
|
|
|
|
h, s, v = color
|
|
new_color = colorsys.hsv_to_rgb(h, s, 255)
|
|
|
|
# print(color)
|
|
rendered_char = font.render(char, ANTI_ALIASING, new_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:
|
|
# 1 byte for h, 1 byte for s, 1 byte for character which will be interperated as v when rendering
|
|
|
|
bin = bytes()
|
|
|
|
# Save Width and Height
|
|
bin += self.width.to_bytes(2, 'big')
|
|
bin += self.height.to_bytes(2, 'big')
|
|
|
|
for (color, char) in zip(self.colors, self.text):
|
|
|
|
h, s, v = color
|
|
|
|
h = int((h * 360) / (360/255))
|
|
bin += h.to_bytes(1, 'big')
|
|
|
|
s = int((s*100) / (100/255))
|
|
bin += s.to_bytes(1, 'big')
|
|
|
|
# for n in (h, s):
|
|
# i = int(n * 255)
|
|
|
|
# bin += i.to_bytes(1, 'big')
|
|
|
|
bin += (NEW_CHARS + EDGE_CHARS).index(char).to_bytes(1, 'big')
|
|
|
|
with open(f'{filename}.citf', 'wb+') as file:
|
|
file.write(bin)
|
|
|
|
def read_citf(self, filepath: str) -> None:
|
|
self.text = ''
|
|
self.colors = []
|
|
|
|
with open(filepath, 'rb') as file:
|
|
data = file.read()
|
|
|
|
# Get Width and Height
|
|
self.width = int.from_bytes(data[:2], 'big')
|
|
self.height = int.from_bytes(data[2:4], 'big')
|
|
|
|
data = data[4:]
|
|
|
|
# Read colors and characters
|
|
char = None
|
|
h, s = None, None
|
|
for n in data:
|
|
if not h:
|
|
h = n
|
|
elif not s:
|
|
s = n
|
|
else:
|
|
print(n)
|
|
char = (NEW_CHARS + EDGE_CHARS)[n]
|
|
|
|
if h and s and char:
|
|
logging.debug(f'H: {h}, S: {s}, Char: {char}')
|
|
self.text += char
|
|
self.colors.append((h/255, s/255, 255))
|
|
h, s, char = None, None, None
|