Skip to content

Commit b22e11f

Browse files
Merge pull request #1916 from aryanrai2001/Dynamic-Resolution-In-Photo-To-ASCII
Added dynamic resolution to Photo-to-ASCII
2 parents 1c1dbaa + b584547 commit b22e11f

File tree

1 file changed

+175
-78
lines changed

1 file changed

+175
-78
lines changed

Photo To Ascii/photo_to_ascii.py

Lines changed: 175 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,58 @@
1+
"""
2+
Script that converts any Image into its ASCII representation.
3+
"""
4+
5+
from sys import exit as sysexit
16
from PIL import Image, ImageEnhance
27

3-
# ASCII characters used to build the output text
4-
char_ramp = []
58

6-
# Choose character sequence for mapping
7-
def set_char_ramp():
8-
global char_ramp
9+
def pixels_to_ascii(image, char_ramp):
10+
"""
11+
Function that takes in an image and a character sequence.
12+
And returns a string containing the ASCII representation of
13+
the image based on the character sequence provided.
14+
"""
15+
16+
pixels = image.convert("L").getdata()
17+
characters = " ".join(
18+
[char_ramp[int((pixel / 256) * len(char_ramp))] for pixel in pixels]
19+
)
20+
pixel_count = len(characters)
21+
scanline_width = image.width * 2
22+
23+
return "\n".join(
24+
[
25+
characters[index: (index + scanline_width)]
26+
for index in range(0, pixel_count, scanline_width)
27+
]
28+
)
29+
30+
31+
def input_image():
32+
"""
33+
Function that asks user for a path to an image file
34+
and checks for validity. Then it loads and returns the
35+
image and the path as a tuple(image, path).
36+
"""
37+
38+
path = input("Enter a valid pathname to an image:\n")
39+
40+
try:
41+
image = Image.open(path)
42+
except IOError:
43+
print("Invalid Path!")
44+
sysexit()
45+
46+
return image, path
47+
48+
49+
def input_ramp():
50+
"""
51+
Function that asks the user to choose from a set
52+
of character sequences or to specify their own
53+
custom sequence and then returns that as a string.
54+
"""
55+
956
print("Choose a Character Sequence!")
1057
print("1 - Basic")
1158
print("2 - Standard (Short)")
@@ -17,90 +64,140 @@ def set_char_ramp():
1764

1865
try:
1966
choice = int(choice)
20-
except Exception:
67+
except ValueError:
2168
print("Invalid Input!")
22-
exit()
69+
sysexit()
2370

2471
if choice == 1:
25-
char_ramp = list("00011111...")
26-
elif choice == 2:
27-
char_ramp = list("@%#*+=-:. ")
28-
elif choice == 3:
29-
char_ramp = list("$@B%8&WM#*oahkbdpqwmZO0QLCJUYXzcvunxrjft/\\|()1}{[]?-_+~<>i!lI;:,\"^`'. ")
30-
elif choice == 4:
31-
char_ramp = ["█", "▉", "▊", "▋", "▌", "▍", "▎", "▏"]
32-
elif choice == 5:
33-
char_ramp = ["█", "▓", "▒", "░", " "]
34-
elif choice == 6:
35-
custom_ramp = input("Enter your Character Sequence from High density to Low: ")
72+
return list("00011111...")
73+
if choice == 2:
74+
return list("@%#*+=-:. ")
75+
if choice == 3:
76+
return list(
77+
"$@B%8&WM#*oahkbdpqwmZO0QLCJUYXzcvunxrjft"
78+
+ "/\\|()1}{[]?-_+~<>i!lI;:,\"^`'. "
79+
)
80+
if choice == 4:
81+
return ["█", "▉", "▊", "▋", "▌", "▍", "▎", "▏"]
82+
if choice == 5:
83+
return ["█", "▓", "▒", "░", " "]
84+
if choice == 6:
85+
while True:
86+
custom_ramp = input("Enter character sequence ['?' for info]: ")
87+
if custom_ramp == "?":
88+
print(
89+
"The character sequence must start with characters",
90+
"that represent high pixel density and end with",
91+
"characters that represent low pixel density.",
92+
)
93+
else:
94+
break
3695
if len(custom_ramp) == 0:
3796
print("Invalid Input!")
38-
exit()
39-
char_ramp = list(custom_ramp)
40-
else:
41-
print("Invalid Input!")
42-
exit()
97+
sysexit()
98+
return list(custom_ramp)
4399

100+
print("Invalid Input!")
101+
sysexit()
44102

45-
# Convert pixels to a string of ASCII characters
46-
def pixels_to_ascii(image, contrast_factor):
47-
image = image.convert("L")
48-
enhancer = ImageEnhance.Contrast(image)
49-
image = enhancer.enhance(contrast_factor)
50-
pixels = image.getdata()
51-
characters = " ".join([char_ramp[int((pixel/256)*len(char_ramp))] for pixel in pixels])
52-
return(characters)
53103

54-
# Driver function
55-
def photoascii():
56-
# Attempt to open image file from user-input
57-
path = input("Enter a valid pathname to an image:\n")
58-
try:
59-
image = Image.open(path)
60-
except Exception:
61-
print("Invalid Path!")
62-
exit()
63-
64-
contrast_factor = input("Enter contrast factor (1 = Original Contrast) [Note: Enter negative value to invert output] : ")
104+
def input_contrast():
105+
"""
106+
Function that asks user for the contrast factor that is to be applied
107+
on the image before conversion. And returns it.
108+
"""
109+
110+
while True:
111+
contrast_factor = input("Enter contrast factor ['?' for info] : ")
112+
if contrast_factor == "?":
113+
print(
114+
"Contrast factor is a value that is used to controle",
115+
"the contrast of the output. Default value of the contrast",
116+
"factor is 1. Negative value will invert the output.",
117+
)
118+
else:
119+
break
120+
65121
try:
66122
contrast_factor = float(contrast_factor)
67-
except Exception:
123+
except ValueError:
68124
print("Invalid Input!")
69-
exit()
70-
71-
set_char_ramp()
72-
125+
sysexit()
126+
127+
return contrast_factor
128+
129+
130+
def resize_image(image):
131+
"""
132+
Function that takes in an image and asks the user for a sample size.
133+
Then returns a resized image with each pixel representing the sample grid.
134+
"""
135+
136+
while True:
137+
sample_size = input("Enter the sample size ['?' for info] : ")
138+
if sample_size == "?":
139+
print(
140+
"Sample size refers to the number of pixels",
141+
"that will be sampled for one character.",
142+
"Default value of sample size is 4.",
143+
"Its value must be greater than or equal to 1.",
144+
)
145+
else:
146+
break
147+
148+
try:
149+
sample_size = int(sample_size)
150+
except ValueError:
151+
print("Invalid Input!")
152+
sysexit()
153+
154+
if sample_size <= 0:
155+
print("Invalid Input!")
156+
sysexit()
157+
158+
width, height = image.size
159+
new_width = width // sample_size
160+
new_height = (new_width * height) // width
161+
162+
return image.resize((new_width, new_height))
163+
164+
165+
def get_output_path(path):
166+
"""
167+
Function that takes in the path of the input image file and returns the
168+
path of a text file that the ouput will be saved to.
169+
"""
170+
171+
dot_index = path.rfind(".")
172+
slash_index = path.rfind("\\")
173+
174+
if slash_index == -1:
175+
slash_index = path.rfind("/")
176+
177+
image_name = path[slash_index + 1: dot_index] + "_" + path[dot_index + 1:]
178+
179+
return path[:slash_index] + f"/{image_name}_ASCII.txt"
180+
181+
182+
def main():
183+
"""
184+
The main function.
185+
"""
186+
187+
image, path = input_image()
188+
char_ramp = input_ramp()
189+
190+
contrast_factor = input_contrast()
73191
if contrast_factor < 0:
74-
global char_ramp
75192
char_ramp.reverse()
76193
contrast_factor = -contrast_factor
77-
78-
# Fetch the name of the image file
79-
dot_index = path.rfind('.')
80-
slash_index = path.rfind('\\')
81-
if slash_index == -1:
82-
slash_index = path.rfind('/')
83-
image_name = path[slash_index+1:dot_index] + "_" + path[dot_index+1:]
84-
85-
# Resize image
86-
new_width = 100
87-
width, height = image.size
88-
ratio = height/width
89-
new_height = int(new_width * ratio)
90-
resized_image = image.resize((new_width, new_height))
91-
92-
# Convert image to ASCII
93-
new_image_data = pixels_to_ascii(resized_image, contrast_factor)
94-
pixel_count = len(new_image_data)
95-
scanline_width = new_width * 2;
96-
ascii_image = "\n".join([new_image_data[index:(index+scanline_width)]
97-
for index in range(0, pixel_count, scanline_width)])
98-
99-
# Save result to text file
100-
with open(path[:slash_index] + "/{}_ASCII.txt".format(image_name), "w", encoding='utf8') as f:
101-
f.write(ascii_image)
102-
103-
104-
# Run Program
105-
if __name__ == '__main__':
106-
photoascii()
194+
195+
image = resize_image(ImageEnhance.Contrast(image).enhance(contrast_factor))
196+
ascii_image = pixels_to_ascii(image, char_ramp)
197+
198+
with open(get_output_path(path), "w", encoding="utf8") as file:
199+
file.write(ascii_image)
200+
201+
202+
if __name__ == "__main__":
203+
main()

0 commit comments

Comments
 (0)