UVTC's Blog

Create a Graphics Image from Pixels

2020-01-06

One simple way to draw an image by specifying the colors of pixels is to use the venerable and open Netpbm format. This is a plain text format where you specify each pixel’s color values (red, green, & blue) in order from left to right, top to bottom, and then optionally convert that resulting text file to some other format, such as png, for easy viewing.

If you don’t already have the netpbm software installed, on Debian GNU/Linux you can get it via apt install netpbm.

For example, this foo.ppm file:

P3
# Width and height, in pixels.
10 18
# Max value for any rgb color val is 255.
255
# And now the pixel data (red, green, and blue values of 0 to 255, so,
# three numbers for each pixel). You don't have to line them up in any
# special way. Here's a red line 3 pixels thick that goes across 10 pixels.
255 0 0 255 0 0 255 0 0 255 0 0 255 0 0 255 0 0 255 0 0 255 0 0 255 0 0 255 0 0
255 0 0 255 0 0 255 0 0 255 0 0 255 0 0 255 0 0 255 0 0 255 0 0 255 0 0 255 0 0
255 0 0 255 0 0 255 0 0 255 0 0 255 0 0 255 0 0 255 0 0 255 0 0 255 0 0 255 0 0
# Now a green line,
0 255 0 0 255 0 0 255 0 0 255 0 0 255 0 0 255 0 0 255 0 0 255 0 0 255 0 0 255 0
0 255 0 0 255 0 0 255 0 0 255 0 0 255 0 0 255 0 0 255 0 0 255 0 0 255 0 0 255 0
0 255 0 0 255 0 0 255 0 0 255 0 0 255 0 0 255 0 0 255 0 0 255 0 0 255 0 0 255 0
# and blue,
0 0 255 0 0 255 0 0 255 0 0 255 0 0 255 0 0 255 0 0 255 0 0 255 0 0 255 0 0 255
0 0 255 0 0 255 0 0 255 0 0 255 0 0 255 0 0 255 0 0 255 0 0 255 0 0 255 0 0 255
0 0 255 0 0 255 0 0 255 0 0 255 0 0 255 0 0 255 0 0 255 0 0 255 0 0 255 0 0 255
# and all three once more for a total height of 18 pixels.
255 0 0 255 0 0 255 0 0 255 0 0 255 0 0 255 0 0 255 0 0 255 0 0 255 0 0 255 0 0
255 0 0 255 0 0 255 0 0 255 0 0 255 0 0 255 0 0 255 0 0 255 0 0 255 0 0 255 0 0
255 0 0 255 0 0 255 0 0 255 0 0 255 0 0 255 0 0 255 0 0 255 0 0 255 0 0 255 0 0
0 255 0 0 255 0 0 255 0 0 255 0 0 255 0 0 255 0 0 255 0 0 255 0 0 255 0 0 255 0
0 255 0 0 255 0 0 255 0 0 255 0 0 255 0 0 255 0 0 255 0 0 255 0 0 255 0 0 255 0
0 255 0 0 255 0 0 255 0 0 255 0 0 255 0 0 255 0 0 255 0 0 255 0 0 255 0 0 255 0
0 0 255 0 0 255 0 0 255 0 0 255 0 0 255 0 0 255 0 0 255 0 0 255 0 0 255 0 0 255
0 0 255 0 0 255 0 0 255 0 0 255 0 0 255 0 0 255 0 0 255 0 0 255 0 0 255 0 0 255
0 0 255 0 0 255 0 0 255 0 0 255 0 0 255 0 0 255 0 0 255 0 0 255 0 0 255 0 0 255

converted to png via

pnmtopng foo.ppm > foo.png

yields this 10×18-pixel image:

As another example, this code (written in Haxe) generates a ppm file representing a 100 × 100 image of randomly-colored pixels:

For me, this yields:

If you instead want to color particular pixels based on their position, say, relative to the center of the box, then you need some way to figure out a given pixel’s “x” and “y” coordinates from those “i-j” values in the code above.

In the following code, after calculating x & y from i & j (assume the x-axis goes to the right, and the y-axis goes up.), I also went and computed r (distance out from the center of the box) and θ (angle from the x-axis to r), and then colored the pixels taking r and θ into account.

using StringTools;

class Main {
    static var sz = 400;
    
    public static function main() {
        var lines : Array<String> = [];
        lines.push('P3\n${sz} ${sz}\n255');
        for (i in 0...sz) { // for each line
            var line = "";
            for (j in 0...sz) { // for each pixel in that line
                // x and y each go from -sz/2 to +sz/2.
                var x:Float =  j - sz/2;
                var y:Float = -i + sz/2;
                var r = Math.sqrt(Math.pow(x, 2)
                                + Math.pow(y, 2));
                var theta = compute_theta(x, y);
                
                // Red depends on r: px_r = 255/283 r
                var px_r = Math.round(0.9016 * r);
                if (px_r > 255) { px_r = 255; }
                //px_r = 0;
                
                // Green depends on theta: px_g =  255 / 2*Math.PI theta
                var px_g = Math.round(40.58 * theta);
                if (px_g > 255) { px_g = 255; }
                
                // Blue stays constant...
                var px_b = 100;
                
                // For testing purposes, draws a cirle.
                /*
                if (r > sz/4 && r < sz/4 + sz/40) {
                    //trace('$x\t$y\t$theta');
                    if (theta <= Math.PI/2) {
                        px_r = 255; px_g = 0; px_b = 0;
                    }
                    else if (theta > Math.PI/2 && theta <= Math.PI) {
                        px_r = 0; px_g = 255; px_b = 0;
                    }
                    else if (theta > Math.PI && theta <= 3*Math.PI/2) {
                        px_r = 0; px_g = 0; px_b = 255;
                    }
                    else {
                        px_r = 0; px_g = 0; px_b = 0;
                    }
                }
                */
                
                line += "" + px_r + " " + px_g + " " + px_b + " ";
            }
            line = line.rtrim();
            lines.push(line);
        }
        lines.push("");
        sys.io.File.saveContent("foo.ppm", lines.join("\n"));
    }
    
    public static function compute_theta(x:Float, y:Float) {
        var theta = 0.0;
        var ep = 0.001;
        if (x >= 0 && y >= 0) { // 1st quadrant
            if (x < ep) { theta = Math.PI / 2; }
            else { theta = Math.atan(y/x); }
        }
        else if (x < 0 && y >= 0) { // 2nd quad
            if (Math.abs(x) < ep) { theta = Math.PI / 2; }
            else { theta = Math.PI - Math.atan(y/-x); }
        }
        else if (x < 0 && y < 0) { // 3rd quad
            if (Math.abs(x) < ep) { theta = 3 * Math.PI / 2; }
            else { theta = Math.PI + Math.atan(-y/-x); }
        }
        else { // 4th quad
            if (Math.abs(y) < ep) { theta = 3 * Math.PI / 2; }
            else { theta = 2 * Math.PI - Math.atan(-y/x); }
        }
        return theta;
    }
}

which results in:

Notice how: