-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathasciimage.py
281 lines (226 loc) · 11.5 KB
/
asciimage.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
import sys, os
import argparse
import numpy as np
from PIL import Image, ImageDraw, ImageFont
_symbols = "$@B%8&WM#*oahkbdpqwmZ_QLCJUYX123456789zcvunxrjft/\\|()1[]?-_+~<>i!lI;:,\"^`'. "
# _symbols = "░▀█▀▒██▀░▀▄▀░▀█▀░░▒█▀▒▄▀▄░█▄░█░▄▀▀░▀▄▀░▒█▒░█▄▄░█▒█░▒█▒▒░░█▀░█▀█░█▒▀█░▀▄▄░▒█▒"
# _symbols = "&'()*+,-./"
# Character size (empirical value)
char_width = 10
char_height = 10
def calculate_char_brightness(symbols):
"""
Calculates the average brightness of each ASCII character in the symbol list.
"""
brightness_values = []
for char in symbols:
canvas = Image.new('L', (50, 50), color=255) # Luminance
draw = ImageDraw.Draw(canvas)
font = ImageFont.load_default()
draw.text((char_width, char_height), char, font=font, fill=0)
brightness = np.mean(np.array(canvas))
brightness_values.append(brightness)
return brightness_values
def convert_image_to_ascii(image, ascii_cols, scale, symbols):
"""
Converts an image into ASCII art, ensuring the entire image is covered and not cropped.
"""
image_width, image_height = image.size
# Adjust the number of columns to fit the image and the scale
ascii_width = image_width / ascii_cols # Width of each block
ascii_height = ascii_width / scale # Height of each block based on the scale
# Calculate the number of rows (doesn't crop the image, rounding up)
ascii_rows = int(np.ceil(image_height / ascii_height)) # Rounding up to ensure the whole image is covered
brightness_values = calculate_char_brightness(symbols)
sorted_symbols = [symbols[idx] for idx in np.argsort(brightness_values)]
ascii_img = []
for j in range(ascii_rows):
y1 = int(j * ascii_height)
y2 = int((j + 1) * ascii_height)
# Adjust the last row so it isn't cropped
if y2 > image_height:
y2 = image_height
ascii_row = ""
for i in range(ascii_cols):
x1 = int(i * ascii_width)
x2 = int((i + 1) * ascii_width)
# Crop the image into blocks and calculate the average brightness
img_tile = image.crop((x1, y1, x2, y2))
avg_brightness = int(np.mean(np.array(img_tile)))
char_index = int((avg_brightness * (len(sorted_symbols) - 1)) / 255)
ascii_row += sorted_symbols[char_index]
ascii_img.append(ascii_row)
return ascii_img
def save_ascii_as_image(ascii_art, output_file, font_color='black', font_type='default'):
"""
Saves the ASCII art into a PNG image with the same dimensions as the generated text.
"""
try:
if font_type == 'default':
font = ImageFont.load_default()
else:
font = ImageFont.truetype(f"./fonts/{font_type}.ttf", size=char_height)
except IOError as e:
print(f"Error loading font '{font_type}': {e}. Using default font.")
font = ImageFont.load_default()
if font_color == 'black':
text_color = 0
bg_color = 255
elif font_color == 'white':
text_color = 255
bg_color = 0
else:
text_color = font_color
bg_color = 255
# Calculate the dimensions of the final image based on the text
ascii_img_width = len(ascii_art[0]) * char_width # Image width in pixels
ascii_img_height = len(ascii_art) * char_height # Image height in pixels
print(f"ASCII art dimensions (text): {len(ascii_art[0])} columns x {len(ascii_art)} rows")
print(f"Resulting image dimensions: {ascii_img_width} x {ascii_img_height} pixels")
print(f"Character size: {char_width} x {char_height} pixels")
# Create an image with the appropriate size
img = Image.new('L', (ascii_img_width, ascii_img_height), color=bg_color)
draw = ImageDraw.Draw(img)
# Place the characters in the image
y_offset = 0
for line in ascii_art:
x_offset = 0
for char in line:
# Draw each character at its position
draw.text((x_offset, y_offset), char, fill=text_color, font=font)
x_offset += char_width # Move to the next character
y_offset += char_height # Move to the next row
# Crop any empty border on the image to remove any unnecessary white space
img = img.crop((0, 0, ascii_img_width, ascii_img_height))
img.save(output_file)
def save_ascii_text(ascii_art, text_file):
"""
Saves the ASCII art into a text file.
"""
with open(text_file, 'w') as f:
for row in ascii_art:
f.write(row + '\n')
def calculate_one_char_brightness(symbol):
"""
Calculates the average brightness of a single ASCII character.
This assumes that darker characters have a lower brightness.
"""
# Canvas size for the character (empirical value)
char_width = 10
char_height = 10
canvas = Image.new('L', (50, 50), color=255) # Luminance mode
draw = ImageDraw.Draw(canvas)
font = ImageFont.load_default() # Use default font
# Position to draw the symbol
draw.text((char_width, char_height), symbol, font=font, fill=0)
# Calculate the average brightness of the character
brightness = np.mean(np.array(canvas))
return brightness
def convert_ascii_to_grayscale_image(ascii_art, pixel_ar):
"""
Converts ASCII art to a grayscale image by using the brightness of each symbol in the ascii_art
to generate corresponding grayscale pixel values.
Each character in the ascii_art is a patch of pixels of size `pixel_size`.
"""
# Dictionary to store the brightness of each unique character in the ascii_art
char_to_brightness = {}
for row in ascii_art:
for char in row:
if char not in char_to_brightness:
char_to_brightness[char] = calculate_one_char_brightness(char)
# Min-max normalize between 0 and 255
brightness_values = list(char_to_brightness.values())
min_brightness = min(brightness_values)
max_brightness = max(brightness_values)
for char, brightness in char_to_brightness.items():
normalized_brightness = (brightness - min_brightness) / (max_brightness - min_brightness)
char_to_brightness[char] = int(normalized_brightness * 255)
patch_width = int(pixel_ar * char_width)
patch_height = char_height
# Create the output image with the required size
num_rows = len(ascii_art)
num_cols = len(ascii_art[0])
image_width = num_cols * patch_width
image_height = num_rows * patch_height
output_image = Image.new('L', (image_width, image_height), color=255) # 'L' mode for grayscale
pixels = output_image.load() # To obtein access to pixel data through PIL
# Process each character in the ASCII art and fill the image with corresponding brightness
for row_idx, row in enumerate(ascii_art):
for col_idx, char in enumerate(row):
avg_brightness = char_to_brightness.get(char, 255) # Default to white if symbol not found
pixel_value = int(avg_brightness)
# Fill the patch corresponding to this character
for y in range(patch_height):
for x in range(patch_width):
pixels[col_idx * patch_width + x, row_idx * patch_height + y] = pixel_value
return output_image
def print_custom_help():
"""
Prints a custom help message.
"""
help_text = """
asciimage.py
A Python program to convert images into ASCII representations, and vice versa.
Usage:
python asciimage.py --mode <mode> --file <image_path> [--pixel_size <int>] [--scale <float>] [--ascii_cols <int>] [--out <output_type>] [--font_color=<font>] [--font_type=<font>] [--symbols <string>]
Options:
--mode Mode of operation: 'to_ascii' converts image to ASCII, 'from_ascii' converts ASCII to image (required).
--file Path to the input image file (required).
--scale Scale factor for aspect ratio (default: 0.43, a good value to visually maintain the ratio of ASCII characters).
--ascii_cols Number of ASCII columns (default: 100).
--out Output path (default: ./results).
--font_color Color of the image characters; the background will be the opposite: (white, black)
--font_type Font type for output image: (default, arial, dirtydoz, fudd, times or times_new_roman).
--symbols Custom ASCII characters to use (optional, e.g. --symbols dfsg256B%8).
--pixel_ar A character represents a squared superpixel of this aspect ratio (W/H) (only in 'from_ascii' mode) (default: 1) (min: 0.5, max: 2)
Author:
agarnung
"""
print(help_text)
def main():
global _symbols
if '--help' in sys.argv:
print_custom_help()
sys.exit(0)
parser = argparse.ArgumentParser()
parser.add_argument('--file', dest='imgFile', required=True)
parser.add_argument('--scale', dest='scale', required=False, default=0.43)
parser.add_argument('--ascii_cols', dest='ascii_cols', required=False)
parser.add_argument('--out', dest='out', required=False)
parser.add_argument('--font_color', dest='font_color', required=False, default='black', choices=['white', 'black'])
parser.add_argument('--font_type', dest='font_type', required=False, default='default', choices=['default', 'arial', 'dirtydoz', 'fudd', 'times', 'times_new_roman'])
parser.add_argument('--symbols', dest='symbols', required=False)
parser.add_argument('--mode', dest='mode', required=True, choices=['to_ascii', 'from_ascii'])
parser.add_argument('--pixel_ar', dest='pixel_ar', required=False, default=1.0)
args = parser.parse_args()
output_dir = str(args.out) if args.out else './results'
if not os.path.exists(output_dir):
os.makedirs(output_dir)
if args.mode == 'to_ascii':
image = Image.open(args.imgFile).convert('L')
ascii_cols = int(args.ascii_cols) if args.ascii_cols else 100
scale = float(args.scale) if args.scale else 0.43
_symbols = str(args.symbols) if args.symbols else _symbols
print(f"Input image dimensions: {image.size[0]} x {image.size[1]}")
print("Generating ASCII art...")
ascii_art = convert_image_to_ascii(image, ascii_cols, scale, _symbols)
text_file = os.path.join(output_dir, f"{os.path.splitext(os.path.basename(args.imgFile))[0]}_ascii.txt")
save_ascii_text(ascii_art, text_file)
output_image = os.path.join(output_dir, f"{os.path.splitext(os.path.basename(args.imgFile))[0]}_ascii.png")
save_ascii_as_image(convert_image_to_ascii(image, ascii_cols, 1, _symbols), output_image, args.font_color, args.font_type)
print(f"Results saved in {output_dir}")
elif args.mode == 'from_ascii':
pixel_ar = float(args.pixel_ar) if args.pixel_ar else 1.0
if not (0.2 < pixel_ar < 5):
print("pixel_ar set to 1")
pixel_ar = 1
with open(args.imgFile, 'r') as f:
ascii_art = [line.rstrip('\n') for line in f.readlines()]
output_image_path = os.path.join(output_dir, f"{os.path.splitext(os.path.basename(args.imgFile))[0]}_reconstructed.png")
print(f"Input dimensions: ")
print("Generating image from ASCII...")
output_image = convert_ascii_to_grayscale_image(ascii_art, pixel_ar)
output_image.save(output_image_path)
print(f"Results saved in {output_dir}")
if __name__ == '__main__':
main()