├── DejaVuSansMono.ttf ├── Test_Images ├── Calvin.jpg ├── Charlie.jpg └── PythonLogo.jpg ├── LICENSE.txt ├── README.md └── Image2ASCII.py /DejaVuSansMono.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/controversial/Image2ASCII/HEAD/DejaVuSansMono.ttf -------------------------------------------------------------------------------- /Test_Images/Calvin.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/controversial/Image2ASCII/HEAD/Test_Images/Calvin.jpg -------------------------------------------------------------------------------- /Test_Images/Charlie.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/controversial/Image2ASCII/HEAD/Test_Images/Charlie.jpg -------------------------------------------------------------------------------- /Test_Images/PythonLogo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/controversial/Image2ASCII/HEAD/Test_Images/PythonLogo.jpg -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Luke Deen Taylor 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Image2ASCII 2 | Python application to convert images to ASCII art. 3 | 4 | This is a python module that can convert any arbitrary image to ASCII art. It contains a module, `Image2ASCII.py`, that contains two functions. 5 | 6 | ## Functions 7 | 8 | #### image2ASCII 9 | The first function contained in `Image2ASCII.py` is `image2ASCII`. It can be used like `image2ASCII(im)` where im is a PIL image. For any image you provide, it does a few things. 10 | * Crop the image: the first real thing the function does is crop the image to be the largest possible square, which it centers in the top left. 11 | * Convert the image to black and white: for our use, we don't need the image to have multiple values with color. We only need it to have areas of brightness. We convert it to type 'L' for luminosity, that only has one value to work with. 12 | * Compress the image: We need the image to be a size so that one pixel corresponds to one character. We don't want it to be too big! By default, the image is 200 pixels wide 13 | * Strech the image: Fonts are typically taller than they are wide. When we print out ASCII art this way, it appears to be streched vertically. By squishing it down beforehand, it appears normal in text. 14 | * Actually generate the art: We iterate through the image, pixel by pixel, and decide on a character that would have appropriate 'visual weight' 15 | Then, the art is returned to you as a string with line breaks (`\n`) 16 | 17 | The function can also be used to show the starting image beforehand or generate art of a different size. In this case, you can use `image2ASCII(im, scale, showimage)`, where scale is an integer and showimage is a boolean value. 18 | 19 | ##### Example 20 | The image ![Circle](https://www.easycalculation.com/area/images/circle.gif), at a size of 20x20 looks like this: 21 | ``` 22 | 23 | ,iv||/- 24 | ./i|=~_cvivc, 25 | /v/|c||c~v!~vi\ 26 | c~==~\/cc|/_civ~/ 27 | vv!vv|c=!v~~i|/_\ 28 | |!|~icvc|/~==_v_! 29 | /_\|/v/iv~~ii!! 30 | ,~i\c__|~c~!, 31 | -=|\vi. 32 | ``` 33 | #### RenderASCII 34 | The second function in `Image2ASCII.py` is `RenderASCII`. It can be used like `RenderASCII(text)`, where text is a string. The function is designed to render text from `image2ASCII` onto an image for easy viewing, without copy/pasting from the console. Rendering the text from the ball looks pretty much the same as the ball text pasted above. This same rendering method works quickly with big images. 35 | ##### Example 36 | ![Imgur](http://i.imgur.com/WWw63u4.jpg) 37 | 38 | ## Example Script 39 | ``` 40 | from Image2ASCII import * 41 | image = Image.open("Test_Images/Calvin.jpg") #Open the image 42 | ascii = image2ASCII(image) #Convert the image to ASCII 43 | outputimage = RenderASCII(ascii) #Render the ASCII 44 | outputimage.show() #Show the final product 45 | ``` 46 | This shows a rendered ASCII art image of the provided image of Calvin. 47 | -------------------------------------------------------------------------------- /Image2ASCII.py: -------------------------------------------------------------------------------- 1 | import random 2 | import time 3 | import warnings 4 | import string 5 | 6 | from PIL import Image, ImageFont, ImageDraw, ImageStat, ImageEnhance 7 | 8 | 9 | def _getfont(fontsize): 10 | '''Return the ImageFont for the font I'm using''' 11 | try: 12 | return ImageFont.truetype("DejaVuSansMono", fontsize * 4) 13 | except IOError: 14 | import _font_cache 15 | return ImageFont.truetype(_font_cache.get_font_path('DejaVuSansMono')) 16 | 17 | 18 | def visual_weight(char): 19 | '''Return the (approximate) visual weight for a character''' 20 | font = _getfont(10) 21 | # The size of the letter 22 | width, height = font.getsize(char) 23 | # Render the letter in question onto an image 24 | im = Image.new("RGB", (width, height), (255, 255, 255)) 25 | dr = ImageDraw.Draw(im) 26 | dr.text((0, 0), char, (0, 0, 0), font=font) 27 | # Mean of image is visual weight 28 | stat = ImageStat.Stat(im) 29 | lightness = stat.mean[0] 30 | # Project the lightness from a scale of 100 to a scale of 255 31 | lightness = (255 - lightness) / 100.0 * 255 32 | return lightness 33 | 34 | 35 | def gen_charmap(chars=string.printable): 36 | '''Generate a character map for all input characters, mapping each 37 | character to its visual weight.''' 38 | # Use the translate method with only the second `deletechars` param. 39 | chars = chars.replace("\n", "").replace("\r", "").replace("\t", "") 40 | charmap = {} 41 | for c in chars: 42 | weight = visual_weight(c) 43 | if weight not in charmap: 44 | charmap[weight] = '' 45 | charmap[weight] += c 46 | return charmap 47 | 48 | 49 | def resize(im, base=200): 50 | # Resize so the smaller image dimension is always 200 51 | if im.size[0] > im.size[1]: 52 | x = im.size[1] 53 | y = im.size[0] 54 | a = False 55 | else: 56 | x = im.size[0] 57 | y = im.size[1] 58 | a = True 59 | 60 | percent = (base / float(x)) 61 | size = int((float(y) * float(percent))) 62 | if a: 63 | im = im.resize((base, int(size * 0.5)), Image.ANTIALIAS) 64 | else: 65 | im = im.resize((size, int(base * 0.5)), Image.ANTIALIAS) 66 | return im 67 | 68 | 69 | def image2ASCII(im, scale=200, showimage=False, charmap=gen_charmap()): 70 | thresholds = list(charmap.keys()) 71 | grayscale = list(charmap.values()) 72 | 73 | if showimage: 74 | im.show() 75 | # Make sure an image is selected 76 | if im is None: 77 | raise ValueError("No Image Selected") 78 | 79 | # Make sure the output size is not too big 80 | if scale > 500: 81 | warnings.warn("Image cannot be more than 500 characters wide") 82 | scale = 500 83 | 84 | # Resize the image and convert to grayscale 85 | im = resize(im, scale).convert("L") 86 | # Optimize the image by increasing contrast. 87 | enhancer = ImageEnhance.Contrast(im) 88 | im = enhancer.enhance(1.5) 89 | 90 | # Begin with an empty string that will be added on to 91 | output = '' 92 | 93 | # Create the ASCII string by assigning a character 94 | # of appropriate weight to each pixel 95 | for y in range(im.size[1]): 96 | for x in range(im.size[0]): 97 | luminosity = 255 - im.getpixel((x, y)) 98 | # Closest match for luminosity 99 | closestLum = min(thresholds, key=lambda x: abs(x - luminosity)) 100 | row = thresholds.index(closestLum) 101 | possiblechars = grayscale[row] 102 | output += possiblechars[random.randint(0, len(possiblechars) - 1)] 103 | output += '\n' 104 | 105 | # return the final string 106 | return output 107 | 108 | 109 | def RenderASCII(text, fontsize=5, bgcolor='#EDEDED'): 110 | '''Create an image of ASCII text''' 111 | linelist = text.split('\n') 112 | font = _getfont(fontsize) 113 | width, height = font.getsize(linelist[1]) 114 | 115 | image = Image.new("RGB", (width, height * len(linelist)), bgcolor) 116 | draw = ImageDraw.Draw(image) 117 | 118 | for x in range(len(linelist)): 119 | line = linelist[x] 120 | draw.text((0, x * height), line, (0, 0, 0), font=font) 121 | return image 122 | 123 | 124 | def stitchImages(im1, im2): 125 | '''Takes 2 PIL Images and returns a new image that 126 | appends the two images side-by-side.''' 127 | 128 | im2 = im2.resize( 129 | (int(im2.size[0] / 2), int(im2.size[1] / 2)), Image.ANTIALIAS 130 | ) 131 | im1 = im1.resize(im2.size, Image.ANTIALIAS) 132 | 133 | # store the dimensions of each variable 134 | w1, h1 = im1.size 135 | w2, h2 = im2.size 136 | 137 | # Take the combined width of both images, and the greater height 138 | width = w1 + w2 139 | height = max(h1, h2) 140 | 141 | im = Image.new("RGB", (width, height), "white") 142 | im.paste(im1, (0, 0)) 143 | im.paste(im2, (w1, 0)) 144 | 145 | return im 146 | 147 | 148 | def PythonistaTest(): 149 | '''A test of the module for iOS devices running Pythonista''' 150 | import console 151 | import photos 152 | # Ask the user to either take a photo or choose an existing one 153 | capture = console.alert("Image2ASCII", button1="Take Photo", 154 | button2="Pick Photo") 155 | 156 | if capture == 1: 157 | im = photos.capture_image() 158 | elif capture == 2: 159 | im = photos.pick_image(original=False) 160 | 161 | photos.save_image(im) 162 | 163 | console.show_activity() 164 | out = image2ASCII(im, 200) 165 | outim = RenderASCII(out, bgcolor='#ededed') 166 | stitchImages(im, outim).show() 167 | console.hide_activity() 168 | 169 | outim.save('image.png') 170 | console.quicklook('image.png') 171 | mode = console.alert("Image2ASCII", "You can either:", "Share Text", 172 | "Share Image") 173 | 174 | if mode == 1: 175 | with open('output.txt', 'w') as f: 176 | f.write(out) 177 | console.open_in('output.txt') 178 | elif mode == 2: 179 | console.open_in('image.png') 180 | time.sleep(5) 181 | console.clear() 182 | 183 | 184 | if __name__ == "__main__": 185 | import sys 186 | # We're on iOS, and therefore Pythonista 187 | if sys.platform in ['iphoneos', 'ios']: 188 | PythonistaTest() 189 | else: # We're on desktop. Use `python Image2ASCII.py my-image.png` 190 | im = Image.open(sys.argv[1]) 191 | out = image2ASCII(im, 200) 192 | outim = RenderASCII(out, bgcolor='#ededed') 193 | final = stitchImages(im, outim) 194 | final.show() 195 | final.save("final.png") 196 | outim.save("ascii.png") 197 | --------------------------------------------------------------------------------