Image Scaling and ASCII Conversion

In this example we print out a row of text for each row in the image. However, it seems as if the image is too tall. To address this problem, try to output a single character per block of pixels. In particular, average the grayscale values in a rectangular block that’s twice as tall as it is wide, and print out a single character for this block.

import java.awt.Color;
import java.awt.image.BufferedImage;
import java.awt.Image;
import java.awt.Graphics2D;

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;

import javax.imageio.stream.ImageOutputStream;
import javax.imageio.stream.ImageInputStream;
import javax.imageio.metadata.IIOMetadata;
import javax.imageio.IIOImage;
import javax.imageio.ImageIO;
import javax.imageio.ImageWriteParam;
import javax.imageio.ImageWriter;
import javax.imageio.ImageReader;
import javax.imageio.ImageTypeSpecifier;

public class Pics {
    private final String inDir = "images/"; // location of images
    private final String outDir = "images/tmp/";  // location of created files
    private String inFile;
    private String resizedFile;
    private String asciiFile;
    private String ext;   // extension of file
    private long bytes;
    private int width;
    private int height;

    // Constructor obtains attributes of picture
    public Pics(String name, String ext) {
        this.ext = ext;
        this.inFile = this.inDir + name + "." + ext;
        this.resizedFile = this.outDir + name + "." + ext;
        this.asciiFile = this.outDir + name + ".txt";
        this.setStats();
    }

    
    // An image contains metadata, namely size, width, and height
    public void setStats() {
        BufferedImage img;
        try {
            Path path = Paths.get(this.inFile);
            this.bytes = Files.size(path);
            img = ImageIO.read(new File(this.inFile));
            this.width = img.getWidth();
            this.height = img.getHeight();
        } catch (IOException e) {
        }
    }

    // Console print of data
    public void printStats(String msg) {
        System.out.println(msg + ": " + this.bytes + " " + this.width + "x" + this.height + "  " + this.inFile);
    }

    // Convert scaled image into buffered image
    public static BufferedImage convertToBufferedImage(Image img) {

        // Create a buffered image with transparency
        BufferedImage bi = new BufferedImage(
                img.getWidth(null), img.getHeight(null),
                BufferedImage.TYPE_INT_ARGB);

        // magic?
        Graphics2D graphics2D = bi.createGraphics();
        graphics2D.drawImage(img, 0, 0, null);
        graphics2D.dispose();

        return bi;
    }
    
    // Scale or reduce to "scale" percentage provided
    public void resize(int scale) {
        BufferedImage img = null;
        Image resizedImg = null;  

        int width = (int) (this.width * (scale/100.0) + 0.5);
        int height = (int) (this.height * (scale/100.0) + 0.5);

        try {
            // read an image to BufferedImage for processing
            img = ImageIO.read(new File(this.inFile));  // set buffer of image data
            // create a new BufferedImage for drawing
            resizedImg = img.getScaledInstance(width, height, Image.SCALE_SMOOTH);
        } catch (IOException e) {
            return;
        }

        try {
            ImageIO.write(convertToBufferedImage(resizedImg), this.ext, new File(resizedFile));
        } catch (IOException e) {
            return;
        }
        
        this.inFile = this.resizedFile;  // use scaled file vs original file in Class
        this.setStats();
    }
    
    // convert every pixel to an ascii character (ratio does not seem correct)
    public void convertToAscii() {
        BufferedImage img = null;
        PrintWriter asciiPrt = null;
        FileWriter asciiWrt = null;

        try {
            File file = new File(this.asciiFile);
            Files.deleteIfExists(file.toPath()); 
        } catch (IOException e) {
            System.out.println("Delete File error: " + e);
        }

        try {
            asciiPrt = new PrintWriter(asciiWrt = new FileWriter(this.asciiFile, true));
        } catch (IOException e) {
            System.out.println("ASCII out file create error: " + e);
        }

        try {
            img = ImageIO.read(new File(this.inFile));
        } catch (IOException e) {
        }

        for (int i = 0; i < img.getHeight(); i++) {
            for (int j = 0; j < img.getWidth(); j++) {
                Color col = new Color(img.getRGB(j, i));
                double pixVal = (((col.getRed() * 0.30) + (col.getBlue() * 0.59) + (col
                        .getGreen() * 0.11)));
                try {
                    asciiPrt.print(asciiChar(pixVal));
                    asciiPrt.flush();
                    asciiWrt.flush();
                } catch (Exception ex) {
                }
            }
            try {
                asciiPrt.println("");
                asciiPrt.flush();
                asciiWrt.flush();
            } catch (Exception ex) {
            }
        }
    }

    // conversion table, there may be better out there ie https://www.billmongan.com/Ursinus-CS173-Fall2020/Labs/ASCIIArt
    public String asciiChar(double g) {
        String str = " ";
        if (g >= 240) {
            str = " ";
        } else if (g >= 210) {
            str = ".";
        } else if (g >= 190) {
            str = "*";
        } else if (g >= 170) {
            str = "+";
        } else if (g >= 120) {
            str = "^";
        } else if (g >= 110) {
            str = "&";
        } else if (g >= 80) {
            str = "8";
        } else if (g >= 60) {
            str = "#";
        } else {
            str = "@";
        }
        return str;
    }

    // tester/driver
    public static void main(String[] args) throws IOException {
        Pics monaLisa = new Pics("backendAPI", "png");
        monaLisa.printStats("Original");
        monaLisa.resize(33);
        monaLisa.printStats("Scaled");
        monaLisa.convertToAscii();

        Pics pumpkin = new Pics("pumpkin", "png");
        pumpkin.printStats("Original");
        pumpkin.resize(33);
        pumpkin.printStats("Scaled");
        pumpkin.convertToAscii();
    }
}
Pics.main(null);
Original: 545786 1225x510  images/backendAPI.png

Personal Hacks

For these hacks, we will do the red, green, blue, then gray scale, and finally fix compression.

Template

Some of the code is copied from above; I took the constructor, setStats(), and resize(). Those functions are pretty optimized already, so trying to remake them is pointless. What I do need to optimize with completelly new code is the color scaling and ASCII conversions, which will be later.

import java.awt.Color;
import java.awt.image.BufferedImage;
import java.awt.Image;
import java.awt.Graphics2D;

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;

import javax.imageio.stream.ImageOutputStream;
import javax.imageio.stream.ImageInputStream;
import javax.imageio.metadata.IIOMetadata;
import javax.imageio.IIOImage;
import javax.imageio.ImageIO;
import javax.imageio.ImageWriteParam;
import javax.imageio.ImageWriter;
import javax.imageio.ImageReader;
import javax.imageio.ImageTypeSpecifier;

abstract class ImageBlueprint {
    protected final String inDir = "images/"; // location of images
    protected final String outDir = "images/tmp/";  // location of created files
    protected String inFile;
    protected String resizedFile;
    protected String colorFile;
    protected String ext;   // extension of file
    protected long bytes;
    protected int width;
    protected int height;

    // get attributes of picture
    public ImageBlueprint(String name, String ext) {
        this.ext = ext;
        this.inFile = this.inDir + name + "." + ext;
        this.resizedFile = this.outDir + name + "." + ext;
        this.colorFile = this.outDir + name + "New" + ".png";
        this.setStats();
    }

    public ImageBlueprint(String image) {
        this(image, "png");
    }
   
    // An image contains metadata, namely size, width, and height
    public void setStats() {
        BufferedImage img;
        try {
            Path path = Paths.get(this.inFile);
            this.bytes = Files.size(path);
            img = ImageIO.read(new File(this.inFile));
            this.width = img.getWidth();
            this.height = img.getHeight();
        } catch (IOException e) {
        }
    }

    // Scale or reduce to "scale" percentage provided
    public void resize(int scale) {
        BufferedImage img = null;
        Image resizedImg = null;  

        int width = (int) (this.width * (scale/100.0) + 0.5);
        int height = (int) (this.height * (scale/100.0) + 0.5);

        try {
            // read an image to BufferedImage for processing
            img = ImageIO.read(new File(this.inFile));  // set buffer of image data
            // create a new BufferedImage for drawing
            resizedImg = img.getScaledInstance(width, height, Image.SCALE_SMOOTH);
        } catch (IOException e) {
            return;
        }


        //ImageIO.write(convertToBufferedImage(resizedImg), this.ext, new File(resizedFile));

        
        this.inFile = this.resizedFile;  // use scaled file vs original file in Class
        this.setStats();
    }
    
    // Will be used later
    protected abstract void conversion(); 
}

Seeing Red... Green... Blue... and Gray

Now that I've written the template, it's actually pretty easy to scale everything to a certain color.

For the primary colors, all that's needed is to set the values of the non scaled color to be 00 (for example red-scaling would have green and blue be set to 00).

public class RedConversion extends ImageBlueprint {
    
    public RedConversion(String name, String ext) {
        super(name, ext);
        this.colorFile = this.outDir + name + "Red" + ".png";
    }

    public RedConversion(String name) {
        super(name);
        this.colorFile = this.outDir + name + "Red" + ".png";
    }

    @Override
    protected void conversion() {
        BufferedImage img = null;
        PrintWriter colorPrt = null;
        FileWriter colorWrt = null;
        
        
        // Just deletes file if already exists in tmp
        try {
            File file = new File(this.colorFile);
            Files.deleteIfExists(file.toPath()); 
        } catch (IOException e) {
            System.out.println("Delete File error: " + e);
        }

        // try {
        //     colorPrt = new PrintWriter(colorWrt = new FileWriter(this.colorFile, true));
        // } catch (IOException e) {
        //     // TODO Auto-generated catch block
        //     e.printStackTrace();
        // }

        try {
            img = ImageIO.read(new File(this.inFile));
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }

        for (int i = 0; i < this.height; i++) {
            for (int j = 0; j < this.width; j++) {
                Color col = new Color(img.getRGB(j, i));
                int rgb = new Color(col.getRed(), 0, 0).getRGB();
                img.setRGB(j, i, rgb);

            }
        }
        //System.out.println("for loop done");

        try {
            ImageIO.write(img, "png", new File(this.colorFile) );
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }

        
    }

    public static void main(String[] args) {
        RedConversion nicandrohan = new RedConversion("nicandrohan", "png");
        nicandrohan.conversion();
    }

}

Red

public class GreenConversion extends ImageBlueprint {
    public GreenConversion(String name, String ext) {
        super(name, ext);
        this.colorFile = this.outDir + name + "Green" + ".png";
    }

    public GreenConversion(String name) {
        super(name);
        this.colorFile = this.outDir + name + "Green" + ".png";
    }

    @Override
    protected void conversion() {
        BufferedImage img = null;
        PrintWriter colorPrt = null;
        FileWriter colorWrt = null;
        
        
        // Just deletes file if already exists in tmp
        try {
            File file = new File(this.colorFile);
            Files.deleteIfExists(file.toPath()); 
        } catch (IOException e) {
            System.out.println("Delete File error: " + e);
        }

        // try {
        //     colorPrt = new PrintWriter(colorWrt = new FileWriter(this.colorFile, true));
        // } catch (IOException e) {
        //     // TODO Auto-generated catch block
        //     e.printStackTrace();
        // }

        try {
            img = ImageIO.read(new File(this.inFile));
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }

        for (int i = 0; i < this.height; i++) {
            for (int j = 0; j < this.width; j++) {
                Color col = new Color(img.getRGB(j, i));
                int rgb = new Color(0, col.getGreen(), 0).getRGB();
                img.setRGB(j, i, rgb);

            }
        }
        // System.out.println("for loop done");

        try {
            ImageIO.write(img, "png", new File(this.colorFile) );
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }

        
    }

    public static void main(String[] args) {
        GreenConversion nicandrohan = new GreenConversion("nicandrohan", "png");
        nicandrohan.conversion();
    }

}

Green

public class BlueConversion extends ImageBlueprint {
    public BlueConversion(String name, String ext) {
        super(name, ext);
        this.colorFile = this.outDir + name + "Blue" + ".png";
    }

    public BlueConversion(String name) {
        super(name);
        this.colorFile = this.outDir + name + "Blue" + ".png";
    }

    @Override
    protected void conversion() {
        BufferedImage img = null;
        PrintWriter colorPrt = null;
        FileWriter colorWrt = null;
        
        
        // Just deletes file if already exists in tmp
        try {
            File file = new File(this.colorFile);
            Files.deleteIfExists(file.toPath()); 
        } catch (IOException e) {
            System.out.println("Delete File error: " + e);
        }

        // try {
        //     colorPrt = new PrintWriter(colorWrt = new FileWriter(this.colorFile, true));
        // } catch (IOException e) {
        //     // TODO Auto-generated catch block
        //     e.printStackTrace();
        // }

        try {
            img = ImageIO.read(new File(this.inFile));
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }

        for (int i = 0; i < this.height; i++) {
            for (int j = 0; j < this.width; j++) {
                Color col = new Color(img.getRGB(j, i));
                int rgb = new Color(0, 0, col.getBlue()).getRGB();
                img.setRGB(j, i, rgb);

            }
        }
        // System.out.println("for loop done");

        try {
            ImageIO.write(img, "png", new File(this.colorFile) );
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }

        
    }

    public static void main(String[] args) {
        BlueConversion nicandrohan = new BlueConversion("nicandrohan", "png");
        nicandrohan.conversion();
    }

}

Green

Gray-scaling is a bit more complicated. There are multiple methods that, in theory, should work. According to one website I found, there are two methods: average or weighted. Average is as simple as it seems - just take the average of all three values:

Grayscale = (R + G + B) / 3

However, due to the way that humans perceive light (with different sensitivities for different colors), this isn't the most accurate. This leads to the weighted method, based on the way humans actually weigh different colors of light in their vision:> Grayscale = 0.299R + 0.587G + 0.114B

  • note from andrew: humans see green the best, so that is why green is the highest. The weights make sense.

I got the information on grayscaling from here, so click that link to learn more about it.

Since the weighted method is more efficient, that's what I'll be using here. Actually implementing it was pretty difficult though. Instead of using the numeric RGB values, I had to calculate the luminance in order to make everything work.

  • note from andrew: I found the research for these values, so I assume that calculating luminance requires knowing the gamma, which is assumed to be 2.2 in this case.
public class GrayConversion extends ImageBlueprint {
    public GrayConversion(String name, String ext) {
        super(name, ext);
        this.colorFile = this.outDir + name + "Gray" + ".png";
    }

    public GrayConversion(String name) {
        super(name);
        this.colorFile = this.outDir + name + "Gray" + ".png";
    }

    @Override
    protected void conversion() {
        BufferedImage img = null;
        PrintWriter colorPrt = null;
        FileWriter colorWrt = null;
        
        
        // Just deletes file if already exists in tmp
        try {
            File file = new File(this.colorFile);
            Files.deleteIfExists(file.toPath()); 
        } catch (IOException e) {
            System.out.println("Delete File error: " + e);
        }

        // try {
        //     colorPrt = new PrintWriter(colorWrt = new FileWriter(this.colorFile, true));
        // } catch (IOException e) {
        //     // TODO Auto-generated catch block
        //     e.printStackTrace();
        // }

        try {
            img = ImageIO.read(new File(this.inFile));
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }

        for (int i = 0; i < this.height; i++) {
            for (int j = 0; j < this.width; j++) {
                //Color col = new Color(img.getRGB(j, i));
                //int rgb = new Color((int)(0.299 * col.getRed()), (int)(0.114 * col.getGreen()), (int)(0.587 * col.getBlue())).getRGB();
                int rgb = img.getRGB(j, i);
                int r = (rgb >> 16) & 0xFF;
                int g = (rgb >> 8) & 0xFF;
                int b = (rgb & 0xFF);

                // Normalize and gamma correct:
                float rr = (float) Math.pow(r / 255.0, 2.2);
                float gg = (float) Math.pow(g / 255.0, 2.2);
                float bb = (float) Math.pow(b / 255.0, 2.2);

                // Calculate luminance:
                float lum = (float) (0.2126 * rr + 0.7152 * gg + 0.0722 * bb);

                // Gamma compand and rescale to byte range:
                int grayLevel = (int) (255.0 * Math.pow(lum, 1.0 / 2.2));
                int gray = (grayLevel << 16) + (grayLevel << 8) + grayLevel; 
                img.setRGB(j, i, gray);


            }
        }
        // System.out.println("for loop done");

        try {
            ImageIO.write(img, "png", new File(this.colorFile) );
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }

        
    }

    public static void main(String[] args) {
        GrayConversion nicandrohan = new GrayConversion("nicandrohan", "png");
        nicandrohan.conversion();
    }

}

Green

ASCII Hell

Finally, I'm going to convert images to ASCII characters. The problem with the old method was that it stretched the image out too much and didn't look good at all. I found that the best solution would be to take 4 rows and 2 columns of pixels and convert them into a single ASCII character, so that's what I implemented here.

Also, as a side note, I basically just copied the previous code and edited that. No point in reinventing the wheel once you already get what's happening.

Additionally, (I didn't have to, but) I tried to fix the ASCII characters to be more evenly spaced out. I decided to use the link provided and found a chart of 94 characters organized by those that took the most dark space to those that took the least.

Now here's the fun math part. I wanted to get a mostly divisible number but 94 only factors into 47 and 2, so I rounded to 90 and decided to take 15 of those ASCII characters to use. Then, I took every 6th character in the character list to get my values. Next, I derived an equation to get the thresholds. Since the max value of RGB is 255 and the max percentage of coverage is 23.1%, I got t = (1 - 1/0.23) * 255 to find the thresholds. Finally, I compared each character and plugged its respective coverage percent into the equation to get every threshold.

As for the results, they went a bit differently than how I expected. I'm not going to show them here, but they're a lot more detailed than the original ASCII calculator. Unfortunately, I don't think it looks super good with smaller images, so I decided to keep the old conversion.

public class ASCIIConversion extends ImageBlueprint {
    
    public ASCIIConversion(String name, String ext) {
        super(name, ext);
        this.colorFile = this.outDir + name + "ASCII" + ".txt";
    }

    public ASCIIConversion(String name) {
        super(name);
        this.colorFile = this.outDir + name + "ASCII" + ".txt";
    }

    @Override
    protected void conversion() {
        
        // Controls how big chunks are taken for ASCII characters
        final int XLENGTH = 1;
        final int YLENGTH = 2;

        BufferedImage img = null;
        PrintWriter asciiPrt = null;
        FileWriter asciiWrt = null;
        Color col = null;

        try {
            File file = new File(this.colorFile);
            Files.deleteIfExists(file.toPath()); 
        } catch (IOException e) {
            System.out.println("Delete File error: " + e);
        }

        try {
            asciiPrt = new PrintWriter(asciiWrt = new FileWriter(this.colorFile, true));
        } catch (IOException e) {
            System.out.println("ASCII out file create error: " + e);
        }

        try {
            img = ImageIO.read(new File(this.inFile));
        } catch (IOException e) {
        }

        for (int i = 0; i < img.getHeight(); i += YLENGTH) {
            for (int j = 0; j < img.getWidth(); j += XLENGTH) {

                // colorSum stores total sum of RGB values, counter keeps track of how many are being counted
                // colorSum/counter = average
                // average is taken to get ASCII character
                double colorSum = 0;
                int counter = 0;

                for (int k = 0; k < XLENGTH; k++) {
                    for (int l = 0; l < YLENGTH; l++) {
                        if (k+j < img.getWidth() && l+i < img.getHeight()) {
                            counter++;
                            col = new Color(img.getRGB(j + k, i + l));
                            colorSum += (((col.getRed() * 0.30) + (col.getBlue() * 0.59) + (col
                                .getGreen() * 0.11)));
                        }
                    }
                }

                double average = colorSum / counter;
                try {
                    asciiPrt.print(asciiChar(average));
                    asciiPrt.flush();
                    asciiWrt.flush();
                } catch (Exception ex) {
                }

            }
            try {
                asciiPrt.println("");
                asciiPrt.flush();
                asciiWrt.flush();
            } catch (Exception ex) {
            }
        }
    }

    // conversion table, there may be better out there ie https://www.billmongan.com/Ursinus-CS173-Fall2020/Labs/ASCIIArt
    public String asciiChar(double g) {
        String str = " ";

        // Higher quality, looks better with bigger images
        // if (g >= 224.6) {
        //     str = " ";
        // } else if (g >= 186.6) {
        //     str = "-";
        // } else if (g >= 165.5) {
        //     str = ";";
        // } else if (g >= 143) {
        //     str = "(";
        // } else if (g >= 137.5) {
        //     str = "<";
        // } else if (g >= 127.5) {
        //     str = "L";
        // } else if (g >= 123.1) {
        //     str = "1";
        // } else if (g >= 113.1) {
        //     str = "n";
        // } else if (g >= 102) {
        //     str = "s";
        // } else if (g >= 85.4) {
        //     str = "h";
        // } else if (g >= 75.4) {
        //     str = "4";
        // } else if (g >= 69.8) {
        //     str = "e";
        // } else if (g >= 62.1) {
        //     str = "5";
        // } else if (g >= 51) {
        //     str = "D";
        // } else if (g >= 35.5) {
        //     str = "Q";
        // } else if (g >= 17.8) {
        //     str = "M";
        // } else {
        //     str = "@";
        // }

        if (g >= 240) {
            str = " ";
        } else if (g >= 210) {
            str = ".";
        } else if (g >= 190) {
            str = "*";
        } else if (g >= 170) {
            str = "+";
        } else if (g >= 120) {
            str = "^";
        } else if (g >= 110) {
            str = "&";
        } else if (g >= 80) {
            str = "8";
        } else if (g >= 60) {
            str = "#";
        } else {
            str = "@";
        }

        return str;
    }
    
    public static void main(String[] args) {
        ASCIIConversion nicandrohan = new ASCIIConversion("nicandrohan", "png");
        nicandrohan.conversion();
    }

}

Green

Check bailey's post for results; we worked on this together in class.