Tw1ddle/geometrize-haxe

View on GitHub
geometrize/Core.hx

Summary

Maintainability
Test Coverage
package geometrize;

import geometrize.bitmap.Bitmap;
import geometrize.bitmap.Rgba;
import geometrize.rasterizer.Rasterizer;
import geometrize.rasterizer.Scanline;
import geometrize.shape.Shape;
import geometrize.shape.ShapeFactory;
import geometrize.shape.ShapeType;

/**
 * The core functions in Geometrize Haxe.
 * @author Sam Twidale (https://samcodes.co.uk/)
 */
class Core {
    /**
     * Calculates the color of the scanlines.
     * @param    target    The target image.
     * @param    current    The current image.
     * @param    lines    The scanlines.
     * @param    alpha    The alpha of the scanline.
     * @return    The color of the scanlines.
     */
    public static function computeColor(target:Bitmap, current:Bitmap, lines:Array<Scanline>, alpha:Int):Rgba {
        Sure.sure(target != null);
        Sure.sure(current != null);
        Sure.sure(lines != null);
        Sure.sure(alpha >= 0);
        
        var totalRed:Int = 0;
        var totalGreen:Int = 0;
        var totalBlue:Int = 0;
        var count:Int = 0;
        
        var f:Float = 257 * 255 / alpha;
        var a:Int = Std.int(f);
        
        // For each scanline
        for (line in lines) {
            var y:Int = line.y;
            for (x in line.x1...line.x2 + 1) {
                // Get the overlapping target and current colors
                var t:Rgba = target.getPixel(x, y);
                var c:Rgba = current.getPixel(x, y);
                
                // Mix the red, green and blue components, blending by the given alpha value
                totalRed += (t.r - c.r) * a + c.r * 257;
                totalGreen += (t.g - c.g) * a + c.g * 257;
                totalBlue += (t.b - c.b) * a + c.b * 257;
                
                count++;
            }
        }
        
        if (count == 0) { // Early out to avoid integer divide by 0
            return 0;
        }
        
        // Scale totals down to 0-255 range to get average blended color
        var r:Int = Util.clamp(Std.int(totalRed / count) >> 8, 0, 255);
        var g:Int = Util.clamp(Std.int(totalGreen / count) >> 8, 0, 255);
        var b:Int = Util.clamp(Std.int(totalBlue / count) >> 8, 0, 255);
        
        return Rgba.create(r, g, b, alpha);
    }
    
    /**
     * Calculates the root-mean-square error between two bitmaps.
     * @param    first    The first bitmap.
     * @param    second    The second bitmap.
     * @return    The difference/error measure between the two bitmaps.
     */
    public static function differenceFull(first:Bitmap, second:Bitmap):Float {
        Sure.sure(first != null);
        Sure.sure(second != null);
        Sure.sure(first.width != 0);
        Sure.sure(first.height != 0);
        Sure.sure(second.width != 0);
        Sure.sure(second.height != 0);
        Sure.sure(first.width == second.width);
        Sure.sure(first.height == second.height);
        
        var total:Float = 0;
        var width:Int = first.width;
        var height:Int = first.height;
        for (y in 0...height) {
            for (x in 0...width) {
                var f:Rgba = first.getPixel(x, y);
                var s:Rgba = second.getPixel(x, y);
                
                var dr:Int = f.r - s.r;
                var dg:Int = f.g - s.g;
                var db:Int = f.b - s.b;
                var da:Int = f.a - s.a;
                
                total += (dr * dr + dg * dg + db * db + da * da);
            }
        }
        var result:Float = Math.sqrt(total / (width * height * 4.0)) / 255;
        Sure.sure(Math.isFinite(result));
        return result;
    }
    
    /**
     * Calculates the root-mean-square error between the parts of the two bitmaps within the scanline mask.
     * This is for optimization purposes, it lets us calculate new error values only for parts of the image we know have changed.
     * @param    target    The target bitmap.
     * @param    before    The bitmap before the change.
     * @param    after    The bitmap after the change.
     * @param    score    The score.
     * @param    lines    The scanlines.
     * @return    The difference/error between the two bitmaps, masked by the scanlines.
     */
    public static function differencePartial(target:Bitmap, before:Bitmap, after:Bitmap, score:Float, lines:Array<Scanline>):Float {
        Sure.sure(target != null);
        Sure.sure(before != null);
        Sure.sure(after != null);
        Sure.sure(lines != null);
        Sure.sure(lines.length != 0);
        
        var width:Int = target.width;
        var height:Int = target.height;
        var rgbaCount:Int = width * height * 4;
        var total:Float = Math.pow(score * 255, 2) * rgbaCount;
        for (line in lines) {
            var y = line.y;
            for (x in line.x1...line.x2 + 1) {
                var t:Rgba = target.getPixel(x, y);
                var b:Rgba = before.getPixel(x, y);
                var a:Rgba = after.getPixel(x, y);
                
                var dtbr:Int = t.r - b.r;
                var dtbg:Int = t.g - b.g;
                var dtbb:Int = t.b - b.b;
                var dtba:Int = t.a - b.a;
                
                var dtar:Int = t.r - a.r;
                var dtag:Int = t.g - a.g;
                var dtab:Int = t.b - a.b;
                var dtaa:Int = t.a - a.a;
                
                total -= (dtbr * dtbr + dtbg * dtbg + dtbb * dtbb + dtba * dtba);
                total += (dtar * dtar + dtag * dtag + dtab * dtab + dtaa * dtaa);
            }
        }
        var result:Float = Math.sqrt(total / rgbaCount) / 255;
        Sure.sure(Math.isFinite(result));
        return result;
    }
    
    /**
     * Gets the best state using a random algorithm.
     * @param    shapes    The types of shape to use.
     * @param    alpha    The opacity of the shape.
     * @param    n    The number of states to try.
     * @param    target    The target bitmap.
     * @param    current    The current bitmap.
     * @param    buffer    The buffer bitmap.
     * @param    lastScore    The last score recorded by the model.
     * @return    The best random state i.e. the one with the lowest energy.
     */
    public static function bestRandomState(shapes:Array<ShapeType>, alpha:Int, n:Int, target:Bitmap, current:Bitmap, buffer:Bitmap, lastScore:Float):State {
        var bestEnergy:Float = 0;
        var bestState:State = null;
        
        for (i in 0...n) {
            var state:State = new State(ShapeFactory.randomShapeOf(shapes, current.width, current.height), alpha, target, current, buffer);
            var energy:Float = state.energy(lastScore);
            if (i == 0 || energy < bestEnergy) {
                bestEnergy = energy;
                bestState = state;
            }
        }
        return bestState;
    }
    
    /**
     * Gets the best state using a hill climbing algorithm.
     * @param    shapes    The types of shape to use.
     * @param    alpha    The opacity of the shape.
     * @param    n    The number of random states to generate.
     * @param    age    The number of hillclimbing steps.
     * @param    target    The target bitmap.
     * @param    current    The current bitmap.
     * @param    buffer    The buffer bitmap.
     * @param    lastScore    The last score recorded by the model.
     * @return    The best state acquired from hill climbing i.e. the one with the lowest energy.
     */
    public static function bestHillClimbState(shapes:Array<ShapeType>, alpha:Int, n:Int, age:Int, target:Bitmap, current:Bitmap, buffer:Bitmap, lastScore:Float):State {
        var state:State = bestRandomState(shapes, alpha, n, target, current, buffer, lastScore);
        state = hillClimb(state, age, lastScore);
        return state;
    }
    
    /**
     * Hill climbing optimization algorithm, attempts to minimize energy (the error/difference).
     * @param    state    The state to optimize.
     * @param    maxAge    The maximum age.
     * @param    lastScore    The last score recorded by the model.
     * @return    The best state found from hillclimbing.
     */
    public static function hillClimb(state:State, maxAge:Int, lastScore:Float):State {
        Sure.sure(state != null);
        Sure.sure(maxAge >= 0);
        
        var state:State = state.clone();
        var bestState:State = state.clone();
        var bestEnergy:Float = state.energy(lastScore);
        
        var age:Int = 0;
        while (age < maxAge) {
            var undo:State = state.mutate();
            var energy:Float = state.energy(lastScore);
            if (energy >= bestEnergy) {
                state = undo;
            } else {
                bestEnergy = energy;
                bestState = state.clone();
                age = -1;
            }
            age++;
        }
        
        return bestState;
    }
    
    /**
     * Calculates a measure of the improvement adding the shape provides - lower energy is better.
     * @param    shape    The shape to check.
     * @param    alpha    The alpha of the shape.
     * @param    target    The target bitmap.
     * @param    current    The current bitmap.
     * @param    buffer    The buffer bitmap.
     * @param    score    The score.
     * @return    The energy measure.
     */
    public static function energy(shape:Shape, alpha:Int, target:Bitmap, current:Bitmap, buffer:Bitmap, score:Float):Float {
        Sure.sure(shape != null);
        Sure.sure(target != null);
        Sure.sure(current != null);
        Sure.sure(buffer != null);
        
        var lines:Array<Scanline> = shape.rasterize(); // Gets the set of scanlines that describe the pixels covered by the shape
        Sure.sure(lines != null);
        Sure.sure(lines.length != 0);
        
        var color:Rgba = computeColor(target, current, lines, alpha); // Calculate best color for areas covered by the scanlines
        Rasterizer.copyLines(buffer, current, lines); // Copy area covered by scanlines to buffer bitmap
        Rasterizer.drawLines(buffer, color, lines); // Blend scanlines into the buffer using the color calculated earlier
        return differencePartial(target, current, buffer, score, lines); // Get error measure between areas of current and modified buffers covered by scanlines
    }
}