Compare commits
15 Commits
main
...
file-forma
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7588fd232f | ||
|
|
2a75ae8e0a | ||
|
|
cb6e1ffc38 | ||
|
|
398fb99cef | ||
| faff798cfa | |||
|
|
7f65ce824a | ||
|
|
c1486c4c1a | ||
|
|
8f37552ba6 | ||
|
|
cb0fa7b15d | ||
| af68d788b0 | |||
| fccdf56188 | |||
| 2140a5695b | |||
|
|
6fa31720c2 | ||
| 1e3fb6e595 | |||
| 946580320a |
5
.gitignore
vendored
5
.gitignore
vendored
@ -1,2 +1,5 @@
|
||||
*.png
|
||||
*.jpg
|
||||
*.jpg
|
||||
!sample-images/*
|
||||
__pycache__
|
||||
*.gif
|
||||
23
CTIFC.py
Normal file
23
CTIFC.py
Normal file
@ -0,0 +1,23 @@
|
||||
# CTIF CLI
|
||||
|
||||
import argparse
|
||||
|
||||
from PIL import Image
|
||||
|
||||
import ctif
|
||||
|
||||
parser = argparse.ArgumentParser(description='Convert images to CTIF format.')
|
||||
parser.add_argument('image_path', type=str, help='Path to the input image file.')
|
||||
|
||||
def main():
|
||||
args = parser.parse_args()
|
||||
image_path = args.image_path
|
||||
|
||||
image = Image.open(image_path)
|
||||
ctif_image = ctif.CTIF(image)
|
||||
ctif_image.save_image('render.png')
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
||||
|
||||
@ -1,352 +1,220 @@
|
||||
# import scipy.ndimage as nd
|
||||
# import imageio.v3 as iio
|
||||
# import numpy as np
|
||||
import math
|
||||
import os
|
||||
|
||||
from PIL import Image, ImageFilter
|
||||
import pygame as pg
|
||||
|
||||
IMAGE_PATH = 'sample-images/sunflower.jpg'
|
||||
FONT_PATH = 'fonts/scientifica.ttf'
|
||||
|
||||
BLUR_STRENGTH_1 = 1
|
||||
BLUR_STRENGTH_2 = 1.2
|
||||
|
||||
DOG_THRESHOLD = 8
|
||||
|
||||
WHITE = (255,255,255)
|
||||
|
||||
TEXT_WIDTH = 6 # on font size 12 monocraft medium
|
||||
TEXT_HEIGHT = 11
|
||||
|
||||
SCALE_FACTOR = 2
|
||||
RESIZE_FACTOR = 2
|
||||
|
||||
SCALED_WIDTH = math.floor(TEXT_WIDTH / SCALE_FACTOR)
|
||||
SCALED_HEIGHT = math.floor(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',
|
||||
'.',
|
||||
' '
|
||||
]))
|
||||
|
||||
def main():
|
||||
|
||||
image = Image.open(IMAGE_PATH)
|
||||
image = image.resize((image.width * RESIZE_FACTOR, image.height * RESIZE_FACTOR))
|
||||
|
||||
L_image = image.convert('L')
|
||||
|
||||
blur_1 = image.filter(ImageFilter.GaussianBlur(BLUR_STRENGTH_1))
|
||||
blur_2 = image.filter(ImageFilter.GaussianBlur(BLUR_STRENGTH_2))
|
||||
|
||||
|
||||
print(image.size)
|
||||
dog: Image = difference_of_gaussians(blur_1, blur_2)
|
||||
|
||||
dog.save('dog.png')
|
||||
|
||||
sb, gradient = sobel(dog)
|
||||
|
||||
w, h = sb.size
|
||||
|
||||
sb.save('sobel.png')
|
||||
|
||||
text_grid_width = w / SCALED_WIDTH
|
||||
text_grind_height = h / SCALED_HEIGHT
|
||||
|
||||
pg.init()
|
||||
font = pg.font.Font(FONT_PATH, 12)
|
||||
window = pg.display.set_mode((text_grid_width * TEXT_WIDTH, text_grind_height * TEXT_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
|
||||
colors = []
|
||||
|
||||
for y2 in range(SCALED_HEIGHT):
|
||||
for x2 in range(SCALED_WIDTH):
|
||||
|
||||
real_x = x2 + x_offset
|
||||
real_y = y2 + y_offset
|
||||
|
||||
gradient_v = gradient[real_y * w + real_x]
|
||||
|
||||
char = ' '
|
||||
|
||||
if gradient_v:
|
||||
char = match_gradient(gradient_v)
|
||||
else:
|
||||
char = CHARS[round(L_image.getpixel((real_x, real_y))/(255/len(CHARS))) - 1]
|
||||
|
||||
if char in histogram:
|
||||
histogram[char] += 1
|
||||
else:
|
||||
histogram[char] = 1
|
||||
|
||||
colors.append(image.getpixel((real_x, real_y)))
|
||||
|
||||
color_avg = average_colors(colors)
|
||||
|
||||
# get most common
|
||||
most_common = None
|
||||
score = float('-inf')
|
||||
for char in histogram:
|
||||
if histogram[char] > score:
|
||||
score = histogram[char]
|
||||
most_common = char
|
||||
|
||||
rendered_char = font.render(str(most_common), True, color_avg)
|
||||
window.blit(rendered_char, (x * TEXT_WIDTH, y * TEXT_HEIGHT))
|
||||
|
||||
x_offset += SCALED_WIDTH
|
||||
|
||||
y_offset += SCALED_HEIGHT
|
||||
|
||||
pg.display.update()
|
||||
pg.image.save(window, "render.png")
|
||||
pg.quit()
|
||||
|
||||
if DEBUG:
|
||||
sb.save('debug/sobel-dog.png')
|
||||
dog.save('debug/dog.png')
|
||||
|
||||
# Draw only the ASCII edges
|
||||
pg.init()
|
||||
font = pg.font.Font(FONT_PATH, 12)
|
||||
window = pg.display.set_mode((text_grid_width * TEXT_WIDTH, text_grind_height * TEXT_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
|
||||
colors = []
|
||||
|
||||
for y2 in range(SCALED_HEIGHT):
|
||||
for x2 in range(SCALED_WIDTH):
|
||||
|
||||
real_x = x2 + x_offset
|
||||
real_y = y2 + y_offset
|
||||
|
||||
gradient_v = gradient[real_y * w + real_x]
|
||||
|
||||
char = ' '
|
||||
|
||||
if gradient_v:
|
||||
char = match_gradient(gradient_v)
|
||||
else:
|
||||
char = ' '
|
||||
|
||||
if char in histogram:
|
||||
histogram[char] += 1
|
||||
else:
|
||||
histogram[char] = 1
|
||||
|
||||
colors.append(image.getpixel((real_x, real_y)))
|
||||
|
||||
color_avg = average_colors(colors)
|
||||
|
||||
# get most common
|
||||
most_common = None
|
||||
score = float('-inf')
|
||||
for char in histogram:
|
||||
if histogram[char] > score:
|
||||
score = histogram[char]
|
||||
most_common = char
|
||||
|
||||
rendered_char = font.render(most_common, True, color_avg)
|
||||
window.blit(rendered_char, (x * TEXT_WIDTH, y * TEXT_HEIGHT))
|
||||
|
||||
x_offset += SCALED_WIDTH
|
||||
|
||||
y_offset += SCALED_HEIGHT
|
||||
|
||||
pg.display.update()
|
||||
pg.image.save(window, "debug/edges-render.png")
|
||||
pg.quit()
|
||||
|
||||
|
||||
|
||||
|
||||
def subtract_colors(t1: tuple, t2: tuple) -> tuple:
|
||||
|
||||
if type(t1) != tuple:
|
||||
if (t2 - t1) >= DOG_THRESHOLD:
|
||||
return t2 - t1
|
||||
else:
|
||||
return 0
|
||||
|
||||
if len(t1) != len(t2):
|
||||
print('Len of first tuple should equal second tuple probably')
|
||||
exit(1)
|
||||
|
||||
ans = []
|
||||
for i, n in enumerate(t1):
|
||||
ans.append(t2[i] - n)
|
||||
|
||||
return tuple(ans)
|
||||
|
||||
def difference_of_gaussians(blur_1: Image, blur_2: Image) -> Image:
|
||||
|
||||
w, h = blur_1.size
|
||||
|
||||
dog = Image.new(blur_1.mode, blur_1.size)
|
||||
|
||||
for pixel_y in range(0, h):
|
||||
for pixel_x in range(0, w):
|
||||
|
||||
coords = (pixel_x, pixel_y)
|
||||
|
||||
dog.putpixel(coords, subtract_colors(blur_2.getpixel(coords), blur_1.getpixel(coords)))
|
||||
|
||||
return dog
|
||||
|
||||
# Taken from
|
||||
# https://enzoftware.github.io/posts/image-filter-python
|
||||
def sobel(img: Image) -> tuple[Image.Image, list]:
|
||||
if img.mode == 'L':
|
||||
# return sobel_L(img)
|
||||
pass
|
||||
img = img.convert('RGB')
|
||||
|
||||
width, height = img.size
|
||||
|
||||
newimg = Image.new("RGB", (width, height), "white")
|
||||
gradient = [None for x in range(0, width * height)]
|
||||
for x in range(1, width - 1): # ignore the edge pixels for simplicity (1 to width-1)
|
||||
for y in range(1, height - 1): # ignore edge pixels for simplicity (1 to height-1)
|
||||
|
||||
# initialise Gx to 0 and Gy to 0 for every pixel
|
||||
Gx = 0
|
||||
Gy = 0
|
||||
|
||||
# top left pixel
|
||||
r, g, b = img.getpixel((x - 1, y - 1))
|
||||
|
||||
# intensity ranges from 0 to 765 (255 * 3)
|
||||
intensity = r + g + b
|
||||
|
||||
# accumulate the value into Gx, and Gy
|
||||
Gx += -intensity
|
||||
Gy += -intensity
|
||||
|
||||
# remaining left column
|
||||
r, g, b = img.getpixel((x-1, y))
|
||||
|
||||
Gx += -2 * (r + g + b)
|
||||
|
||||
r, g, b = img.getpixel((x-1, y+1))
|
||||
|
||||
Gx += -(r + g + b)
|
||||
Gy += (r + g + b)
|
||||
|
||||
# middle pixels
|
||||
r, g, b = img.getpixel((x, y-1))
|
||||
|
||||
Gy += -2 * (r + g + b)
|
||||
|
||||
r, g, b = img.getpixel((x, y+1))
|
||||
|
||||
Gy += 2 * (r + g + b)
|
||||
|
||||
# right column
|
||||
r, g, b = img.getpixel((x+1, y-1))
|
||||
|
||||
Gx += (r + g + b)
|
||||
Gy += -(r + g + b)
|
||||
|
||||
r, g, b = img.getpixel((x+1, y))
|
||||
|
||||
Gx += 2 * (r + g + b)
|
||||
|
||||
r, g, b = img.getpixel((x+1, y+1))
|
||||
|
||||
Gx += (r + g + b)
|
||||
Gy += (r + g + b)
|
||||
|
||||
# calculate the length of the gradient (Pythagorean theorem)
|
||||
length = math.sqrt((Gx * Gx) + (Gy * Gy))
|
||||
|
||||
# normalise the length of gradient to the range 0 to 255
|
||||
length = length / 4328 * 255
|
||||
|
||||
length = int(length)
|
||||
gradient_v = math.atan2(Gy, Gx)
|
||||
# print(gradient_v)
|
||||
|
||||
# draw the length in the edge image
|
||||
#newpixel = img.putpixel((length,length,length))
|
||||
newimg.putpixel((x,y),(length,length,length))
|
||||
if length < 20:
|
||||
gradient[y * width + x] = gradient_v
|
||||
|
||||
return (newimg, gradient)
|
||||
|
||||
def sobel_L(img: Image) -> tuple[Image.Image, list]:
|
||||
pass
|
||||
|
||||
# TODO @0x01FE : refactor plz increment by 30 & 60
|
||||
def match_gradient(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 average_colors(colors: list):
|
||||
avg = [0 for x in range(len(colors[0]))]
|
||||
|
||||
for color in colors:
|
||||
for i, num in enumerate(color):
|
||||
avg[i] += num
|
||||
|
||||
for i, n in enumerate(avg):
|
||||
avg[i] /= len(colors)
|
||||
|
||||
return avg
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
||||
import logging
|
||||
import math
|
||||
import os
|
||||
|
||||
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',
|
||||
'.',
|
||||
' '
|
||||
]))
|
||||
|
||||
|
||||
def asciify(image: Image.Image) -> str:
|
||||
|
||||
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('dog.png')
|
||||
|
||||
sb, gradient = filters.sobel(dog)
|
||||
|
||||
w, h = sb.size
|
||||
|
||||
sb.save('sobel.png')
|
||||
|
||||
text_grid_width = w / SCALED_WIDTH
|
||||
text_grind_height = h / SCALED_HEIGHT
|
||||
|
||||
pg.init()
|
||||
font = pg.font.Font(FONT_PATH, 12)
|
||||
window = pg.display.set_mode((text_grid_width * TEXT_WIDTH, text_grind_height * TEXT_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
|
||||
colors = []
|
||||
|
||||
for y2 in range(SCALED_HEIGHT):
|
||||
for x2 in range(SCALED_WIDTH):
|
||||
|
||||
real_x = x2 + x_offset
|
||||
real_y = y2 + y_offset
|
||||
|
||||
gradient_v = gradient[real_y * w + real_x]
|
||||
|
||||
char = ' '
|
||||
|
||||
if gradient_v:
|
||||
char = match_gradient(gradient_v)
|
||||
else:
|
||||
char = CHARS[round(L_image.getpixel((real_x, real_y))/(255/len(CHARS))) - 1]
|
||||
|
||||
if char in histogram:
|
||||
histogram[char] += 1
|
||||
else:
|
||||
histogram[char] = 1
|
||||
|
||||
colors.append(image.getpixel((real_x, real_y)))
|
||||
|
||||
color_avg = average_colors(colors)
|
||||
|
||||
# get most common
|
||||
most_common = None
|
||||
score = float('-inf')
|
||||
for char in histogram:
|
||||
if histogram[char] > score:
|
||||
score = histogram[char]
|
||||
most_common = char
|
||||
|
||||
rendered_char = font.render(str(most_common), True, color_avg)
|
||||
window.blit(rendered_char, (x * TEXT_WIDTH, y * TEXT_HEIGHT))
|
||||
|
||||
x_offset += SCALED_WIDTH
|
||||
|
||||
y_offset += SCALED_HEIGHT
|
||||
|
||||
pg.display.update()
|
||||
pg.image.save(window, "render.png")
|
||||
pg.quit()
|
||||
|
||||
if DEBUG:
|
||||
sb.save('debug/sobel-dog.png')
|
||||
dog.save('debug/dog.png')
|
||||
|
||||
# Draw only the ASCII edges
|
||||
pg.init()
|
||||
font = pg.font.Font(FONT_PATH, 12)
|
||||
window = pg.display.set_mode((text_grid_width * TEXT_WIDTH, text_grind_height * TEXT_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
|
||||
colors = []
|
||||
|
||||
for y2 in range(SCALED_HEIGHT):
|
||||
for x2 in range(SCALED_WIDTH):
|
||||
|
||||
real_x = x2 + x_offset
|
||||
real_y = y2 + y_offset
|
||||
|
||||
gradient_v = gradient[real_y * w + real_x]
|
||||
|
||||
char = ' '
|
||||
|
||||
if gradient_v:
|
||||
char = match_gradient(gradient_v)
|
||||
else:
|
||||
char = ' '
|
||||
|
||||
if char in histogram:
|
||||
histogram[char] += 1
|
||||
else:
|
||||
histogram[char] = 1
|
||||
|
||||
colors.append(image.getpixel((real_x, real_y)))
|
||||
|
||||
color_avg = average_colors(colors)
|
||||
|
||||
# get most common
|
||||
most_common = None
|
||||
score = float('-inf')
|
||||
for char in histogram:
|
||||
if histogram[char] > score:
|
||||
score = histogram[char]
|
||||
most_common = char
|
||||
|
||||
rendered_char = font.render(most_common, True, color_avg)
|
||||
window.blit(rendered_char, (x * TEXT_WIDTH, y * TEXT_HEIGHT))
|
||||
|
||||
x_offset += SCALED_WIDTH
|
||||
|
||||
y_offset += SCALED_HEIGHT
|
||||
|
||||
pg.display.update()
|
||||
pg.image.save(window, "debug/edges-render.png")
|
||||
pg.quit()
|
||||
|
||||
# TODO @0x01FE : refactor plz increment by 30 & 60
|
||||
def match_gradient(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 average_colors(colors: list):
|
||||
avg = [0 for x in range(len(colors[0]))]
|
||||
|
||||
for color in colors:
|
||||
for i, num in enumerate(color):
|
||||
avg[i] += num
|
||||
|
||||
for i, n in enumerate(avg):
|
||||
avg[i] /= len(colors)
|
||||
|
||||
return avg
|
||||
59
brightness.py
Normal file
59
brightness.py
Normal file
@ -0,0 +1,59 @@
|
||||
import string
|
||||
|
||||
from PIL import Image
|
||||
|
||||
import pygame as pg
|
||||
|
||||
|
||||
|
||||
|
||||
FONT_PATH = 'fonts/scientifica.ttf'
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
CHARS = (string.ascii_letters + string.punctuation).replace('\\', '').replace('/', '').replace('|', '').replace('-', '')
|
||||
sorted_chars = {}
|
||||
|
||||
for c in CHARS:
|
||||
pg.init()
|
||||
font = pg.font.Font(FONT_PATH, 12)
|
||||
|
||||
font_w, font_h = font.size(c)
|
||||
|
||||
window = pg.display.set_mode((font_w, font_h))
|
||||
|
||||
rendered_char = font.render(c, False, (255, 255, 255))
|
||||
|
||||
window.blit(rendered_char, (0, 0))
|
||||
|
||||
pg.display.update()
|
||||
pg.image.save(window, 'temp.png')
|
||||
|
||||
pg.quit()
|
||||
|
||||
image = Image.open('temp.png')
|
||||
|
||||
w, h = image.size
|
||||
|
||||
white = 0
|
||||
for pixel_y in range(h):
|
||||
for pixel_x in range(w):
|
||||
pixel = image.getpixel((pixel_x, pixel_y))
|
||||
|
||||
if pixel == (255, 255, 255):
|
||||
white += 1
|
||||
|
||||
sorted_chars[c] = white/(w * h)
|
||||
|
||||
sorted_chars = dict(sorted(sorted_chars.items(), key=lambda item: item[1]))
|
||||
|
||||
chr_str = ''
|
||||
for k, v in sorted_chars.items():
|
||||
chr_str += k
|
||||
|
||||
print(chr_str)
|
||||
|
||||
|
||||
359
ctif.py
Normal file
359
ctif.py
Normal file
@ -0,0 +1,359 @@
|
||||
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 = 1
|
||||
RESIZE_FACTOR = 0.75
|
||||
|
||||
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((int(image.width * RESIZE_FACTOR), int(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, blur_strength_2=1)
|
||||
# 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 not in EDGE_CHARS:
|
||||
v = (NEW_CHARS.find(char) + 1) * (255/len(NEW_CHARS))/255
|
||||
|
||||
# logging.debug(f'HSV Color: {h, s, v}')
|
||||
|
||||
# Make the bright colors brighter and the darks darker
|
||||
# if v <= 0.4:
|
||||
# v *= 1.5
|
||||
|
||||
|
||||
r, g, b = colorsys.hsv_to_rgb(h, s, v)
|
||||
|
||||
|
||||
# logging.debug(f'RGB Color Before: {r, g, b}')
|
||||
r *= 255
|
||||
g *= 255
|
||||
b *= 255
|
||||
|
||||
r = min(r, 255)
|
||||
g = min(g, 255)
|
||||
b = min(b, 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 = min(int(h * 255), 255)
|
||||
bin += h.to_bytes(1, 'big')
|
||||
|
||||
s = min(int(s * 255), 255)
|
||||
bin += s.to_bytes(1, 'big')
|
||||
|
||||
# for n in (h, s):
|
||||
# i = int(n * 255)
|
||||
|
||||
# bin += i.to_bytes(1, 'big')
|
||||
|
||||
char_index = (NEW_CHARS + EDGE_CHARS).index(char)
|
||||
|
||||
bin += char_index.to_bytes(1, 'big')
|
||||
if char in EDGE_CHARS:
|
||||
v = min(int(v * 255), 255)
|
||||
bin += v.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:]
|
||||
|
||||
if len(data) % 3 != 0:
|
||||
logging.error(f"File is bad size. {len(data)}")
|
||||
|
||||
# Read colors and characters
|
||||
i = 0
|
||||
for n in range(1, (int(len(data)/3)) + 1):
|
||||
|
||||
d = data[i:i + 3]
|
||||
|
||||
# print(f'index: {i}')
|
||||
# print(f'index end: {i + 3}')
|
||||
# print(f'data: {d}')
|
||||
|
||||
h = d[0:1]
|
||||
h = int.from_bytes(h, 'big')
|
||||
|
||||
s = d[1:2]
|
||||
s = int.from_bytes(s, 'big')
|
||||
|
||||
|
||||
char_n = d[2:3]
|
||||
char_n = int.from_bytes(char_n, 'big')
|
||||
# print(char_n)
|
||||
char = (NEW_CHARS + EDGE_CHARS)[char_n]
|
||||
|
||||
logging.debug(f'H: {h}, S: {s}, Char: {char}')
|
||||
self.text += char
|
||||
|
||||
if char in EDGE_CHARS:
|
||||
v = d[i+3:i+4]
|
||||
|
||||
v = int.from_bytes(v, 'big')
|
||||
|
||||
self.colors.append((h/255, s/255, v/255))
|
||||
|
||||
i += 4
|
||||
|
||||
else:
|
||||
self.colors.append((h/255, s/255, 255))
|
||||
i += 3
|
||||
BIN
engine.citf
Normal file
BIN
engine.citf
Normal file
Binary file not shown.
90
filters.py
Normal file
90
filters.py
Normal file
@ -0,0 +1,90 @@
|
||||
import logging
|
||||
|
||||
from PIL import Image, ImageFilter
|
||||
import scipy.signal
|
||||
import numpy as np
|
||||
|
||||
logging.getLogger(__name__)
|
||||
|
||||
sobel_x_kernel = np.array((
|
||||
(1, 0, -1),
|
||||
(2, 0, -2),
|
||||
(1, 0, -1)
|
||||
))
|
||||
|
||||
sobel_y_kernel = np.transpose(sobel_x_kernel)
|
||||
|
||||
def difference_of_gaussians(img: Image.Image, blur_strength_1: float | None = 1.0, blur_strength_2: float | None = 1.2, threshold: int | None = 128*3) -> Image.Image:
|
||||
blur_1 = img.filter(ImageFilter.GaussianBlur(blur_strength_1))
|
||||
blur_2 = img.filter(ImageFilter.GaussianBlur(blur_strength_2))
|
||||
|
||||
w, h = img.size
|
||||
|
||||
dog = Image.new(img.mode, img.size)
|
||||
|
||||
for pixel_y in range(h):
|
||||
for pixel_x in range(w):
|
||||
|
||||
coords = (pixel_x, pixel_y)
|
||||
|
||||
p1 = blur_1.getpixel(coords)
|
||||
p2 = blur_2.getpixel(coords)
|
||||
|
||||
p1 = np.array(p1)
|
||||
p2 = np.array(p2)
|
||||
new_pixel = p2 - p1
|
||||
|
||||
# if new_pixel.sum() < threshold:
|
||||
# new_pixel = np.zeros(3).astype(np.uint8)
|
||||
|
||||
dog.putpixel(coords, tuple(new_pixel.tolist()))
|
||||
|
||||
return dog
|
||||
|
||||
def sobel(img: Image.Image, gradient_threshold: int | None = 20) -> tuple[Image.Image, np.ndarray]:
|
||||
logging.debug(img.size)
|
||||
|
||||
img = img.convert('L')
|
||||
img.save('debug/test-L.png')
|
||||
|
||||
logging.debug('Sobel Operator')
|
||||
|
||||
logging.debug(img.size)
|
||||
|
||||
pixels = np.array(img)
|
||||
logging.debug(f'pixels size: {pixels.size}')
|
||||
|
||||
dx = scipy.signal.correlate(pixels, sobel_x_kernel, mode='same')
|
||||
dy = scipy.signal.correlate(pixels, sobel_y_kernel, mode='same')
|
||||
logging.debug(f'dx size: {dx.size}')
|
||||
|
||||
edges = np.sqrt(np.square(dx) + np.square(dy))
|
||||
edges[edges>255] = 255
|
||||
|
||||
logging.debug(f'edges size: {edges.size}')
|
||||
|
||||
gradient = np.arctan2(dy.flatten(), dx.flatten())
|
||||
gradient = np.absolute(gradient)
|
||||
print(gradient)
|
||||
print(np.average(gradient))
|
||||
gradient[gradient > gradient_threshold] = 0
|
||||
|
||||
pil_edges = edges.astype(np.uint8)
|
||||
newimg = Image.fromarray(pil_edges, mode='L')
|
||||
logging.debug(f'New Img Size: {newimg.size}')
|
||||
|
||||
return (newimg, gradient)
|
||||
|
||||
def average_colors(colors: list) -> tuple:
|
||||
colors = np.array(colors)
|
||||
|
||||
np.average(colors)
|
||||
|
||||
sum = np.zeros(len(colors[0]))
|
||||
for c in colors:
|
||||
sum += c
|
||||
|
||||
sum /= len(colors)
|
||||
|
||||
return tuple(sum.astype(np.uint8).tolist())
|
||||
|
||||
25
main.py
Normal file
25
main.py
Normal file
@ -0,0 +1,25 @@
|
||||
import logging
|
||||
|
||||
from PIL import Image
|
||||
import ctif
|
||||
|
||||
|
||||
FORMAT = "%(levelname)s %(filename)s - %(message)s"
|
||||
logging.basicConfig(level=logging.DEBUG, format=FORMAT)
|
||||
|
||||
IMAGE_PATH = 'game_screenshots/deadwell-banner.jpg'
|
||||
|
||||
def main():
|
||||
|
||||
image = Image.open(IMAGE_PATH)
|
||||
i = ctif.CTIF(image)
|
||||
# i.save_citf('engine')
|
||||
# i.read_citf('engine.citf')
|
||||
i.save_image('C:/Users/Jacks/Documents/code/My-Website/app/static/render.png')
|
||||
i.save_edges('debug/edges-render.png')
|
||||
# i.save_image('render.png')
|
||||
# i.save_citf('engine')
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user