Image to Ascii

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:

No alt text provided for this image

Output:

No alt text provided for this image

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:

  1. Get the pixels to convert
  2. Convert each pixel into its corresponding ASCII character
  3. Write each character into the text file

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:

  1. Registers some image formats for decoding.
  2. Creates the path to read the image from.
  3. Trims any whitespace (if any exists).
  4. Tries to open the path and defers closing it to the end of the function.
  5. Creates a path for the output file.
  6. Deletes the output file (if it already exists) and tries create a new output file. Defers closing it to the end of the main function.
  7. Tries to get the ascii characters from the GetPixels() function.
  8. Iterates through the characters and writes them to the output file.
  9. Exists with a status of 0 (success).

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

要查看或添加评论,请登录

Avi Gupta的更多文章

  • I was also cool in the past :)

    I was also cool in the past :)

    You might see some of my more modern projects and think "Wow, those are cool. This guy is very cool and also very epic.

    1 条评论

社区洞察

其他会员也浏览了