Image to Ascii
I recently wrote some code in Go that converts an Image into an ASCII text file.
The Code
package main
import (
"bufio"
"fmt"
"image"
"image/png"
"image/jpeg"
"io"
"os"
"strings"
)
var (
jump int = 10
)
func ClampJump(size int, cols int) int {
n := size/cols
if n < 1 {
n = 1
}
return n
}
const (
localPath = "<path relative from the folder your go file is in to the folder your images are in>"
shader = "$@B%8&WM#*oahkbdpqwmZO0QLCJUYXzcvunxrjft/\\|()1{}[]?-_+~<>i!lI;:,\"^`'. "
desiredDetail = 300
)
func GetCharFromShader(s float64, darkbg bool) rune {
index := int(s / 255.0 * float64(len(shader)))
if darkbg {
index = len(shader) - index
}
if index >= len(shader) {
index = len(shader) - 1
}
return rune(shader[index])
}
func GetPixels(file io.Reader) ([][]rune, error) {
img, _, err := image.Decode(file)
if err != nil {
return nil, err
}
bounds := img.Bounds()
width, height := bounds.Max.X, bounds.Max.Y
jump = ClampJump(width, desiredDetail)
fmt.Printf("dimensions of image: %d x %d", width, height)
var pixels [][]rune
for y := 0; y < height/(jump * 2); y++ {
var row []rune
for x := 0; x < width/jump; x++ {
r, g, b, _ := img.At(x * jump, y * jump * 2).RGBA()
r /= 255
g /= 255
b /= 255
s := 0.3*float64(r) + 0.59*float64(g) + 0.11*float64(b)
//fmt.Printf("s = %v\n", s)
char := GetCharFromShader(s, true)
row = append(row, char)
}
pixels = append(pixels, row)
}
return pixels, nil
}
func Check(e error) {
if e != nil {
panic(e)
}
}
func main() {
// get path
image.RegisterFormat("jpeg", "jpeg", jpeg.Decode, jpeg.DecodeConfig)
image.RegisterFormat("png", "png", png.Decode, png.DecodeConfig)
path := localPath + "<filename>"
path = strings.TrimSuffix(path, "\n")
file, err := os.Open(path)
Check(err)
defer file.Close()
outputPath := localPath + "1.txt"
_ = os.Remove(outputPath)
output, err := os.Create(outputPath)
Check(err)
defer output.Close()
pixels, err := GetPixels(file)
Check(err)
writer := bufio.NewWriter(output)
for _, row := range pixels {
for _, c := range row {
writer.WriteRune(c)
}
writer.WriteRune('\n')
}
fmt.Println()
os.Exit(0)
}
Example
Input:
Output:
How the code works
First, the imports:
import (
"bufio" // input-output buffer for writing to the file
"fmt" // formatting strings, mainly used for debugging. You don't really need this one.
"image" // for images, of course
"image/png" // support for png images
"image/jpeg" // support for jpg/jpeg images
"io" // input-output
"os" // direct calls to OS
"strings" // stuff or appending strings, useful when writting to the file
)
Next up, some constant and variable definitions:
const (
localPath = "<relative path from folder of go file to folder of image file>"
shader = "$@B%8&WM#*oahkbdpqwmZO0QLCJUYXzcvunxrjft/\\|()1{}[]?-_+~<>i!lI;:,\"^`'. " // the ascii characters used for shading the image
desiredDetail = 250 // the amount of columns you want in the image
)
var (
jump int = 10
)
Now, the actual code. In order to get the ascii values, we have to:
Getting the Pixels and converting them is done in one function.
// returns the amount of pixels you need to "jump" in order to get the desired detail.
func ClampJump(size int, cols int) int {
n := size/cols
if n < 1 { // int division can sometimes return 0. So we gotta avoid this.
n = 1
}
return n
}
// gets the character from the shader string, with s as the greyscale value. darkbg considers printing on dark backgrounds.
func GetCharFromShader(s float64, darkbg bool) rune {
index := int(s / 255.0 * float64(len(shader))) // maps the greyscale value (0-255) to the corresponding shader index.
if darkbg {
index = len(shader) - index // "inverts" the shader if darkbg is true.
}
if index >= len(shader) { // this might happen, so just be sure.
index = len(shader) - 1
}
return rune(shader[index]) // returns the rune (character).
}
func GetPixels(file io.Reader) ([][]rune, error) {
img, _, err := image.Decode(file)
if err != nil {
return nil, err
}
bounds := img.Bounds()
// gets the width and height of the image.
width, height := bounds.Max.X, bounds.Max.Y
jump = ClampJump(width, desiredDetail) // gets the jump.
// you don't need this line, but it's useful for debugging.
fmt.Printf("dimensions of image: %d x %d", width, height)
// this array of pixels is what will be written.
var pixels [][]rune
for y := 0; y < height/(jump * 2); y++ {
var row []rune
for x := 0; x < width/jump; x++ {
r, g, b, _ := img.At(x * jump, y * jump * 2).RGBA()
r /= 255
g /= 255
b /= 255
s := 0.3*float64(r) + 0.59*float64(g) + 0.11*float64(b)
//fmt.Printf("s = %v\n", s)
char := GetCharFromShader(s, true)
row = append(row, char)
}
pixels = append(pixels, row)
}
return pixels, nil
}
领英推荐
Getting the pixels
The GetPixels function first gets the pixels, and defines how much we will have to "jump". "Jumping" is skipping over x amount of pixels. So, a jump of 20 will read the first pixel, then the 21st pixel, then the 41st pixel, and then so on. The ClampJump function makes the jump amount the amount required to have the amount of x jumps equaled to the desired detail.
The amount of pixels jumped along the x coordinate is half that jumped along the y coordinate. This is because most monospace fonts are taller than they are wider, so having both the x and y jump be the same can make the image appear stretched out.
The pixels are then read through a nested for loop.
var pixels [][]rune
for y := 0; y < height/(jump * 2); y++ {
var row []rune
for x := 0; x < width/jump; x++ {
...
}
pixels = append(pixels, row)
}
Converting the Pixels
Each pixel read has an RGBA (red green blue alpha) value, with R, G, B, and A each ranging from 0 to 65025 (for some reason). We're going to ignore the alpha value, and only focus on the RGB values. In order to get the RGB values to the ones we want, simply divide each one by 255.
From there, we get our greyscale value, marked by s.
...
r, g, b, _ := img.At(x * jump, y * jump * 2).RGBA()
r /= 255
g /= 255
b /= 255
s := 0.3*float64(r) + 0.59*float64(g) + 0.11*float64(b)
...
After that, we use our GetCharFromShader function in order to get the character that relates to that greyscale value. We then append that to the row.
char := GetCharFromShader(s, true)
row = append(row, char)
And then we return pixels.
Writing the characters to the text file
This is done in the main function. it may seem difficult to understand, but it basically does this:
func main() {
// get path
image.RegisterFormat("jpeg", "jp?g", jpeg.Decode, jpeg.DecodeConfig)
image.RegisterFormat("png", "png", png.Decode, png.DecodeConfig)
path := localPath + "letters.jpeg"
path = strings.TrimSuffix(path, "\n")
file, err := os.Open(path)
Check(err)
defer file.Close()
outputPath := localPath + "1.txt"
_ = os.Remove(outputPath)
output, err := os.Create(outputPath)
Check(err)
defer output.Close()
pixels, err := GetPixels(file)
Check(err)
writer := bufio.NewWriter(output)
for _, row := range pixels {
for _, c := range row {
writer.WriteRune(c)
}
writer.WriteRune('\n')
}
fmt.Println()
os.Exit(0)
}
And that's basically it!
Thanks for reading this! You can check out some of my other works/projects on my GitHub. Also, be sure to check out xinf.dev, a small JSON service I made using Go as the backend.
>> Avi