Experiments with ruby-processing (processing-2.2.1) and JRubyArt for processing-3.0

Tuesday 29 April 2014

Implementing my ruby Vecmath library in java (as a JRuby extension)

Well finally I've done it, no compromises, no dodgy fixes, I've 100% implemented my ruby-library in java, as a jruby extension, so it behaves exactly as the original library. The last thing I implemented was the attr_accessors (well just the setters, the getters are obvious) and methods with an optional block. The setters was the easiest it was as simple as @JRubyMethod(name = "x=") instead of @JRubyMethod(name = "set_x") in the method annotation.
Here is the Vec3D version of set_mag that takes an optional block
Using the Vec3D name was a simple as Vec3D = Processing::Vec2::Vec2
/**
* Call yield if block given, do nothing if yield == false
* else set_mag to given scalar
* @param context
* @param scalar double value to set
* @param block should return a boolean (optional)
* @return
*/
@JRubyMethod(name = "set_mag")
public IRubyObject set_mag(ThreadContext context, IRubyObject scalar, Block block) {
 if (block.isGiven()) {
  if (!(boolean) block.yield(context, scalar).toJava(Boolean.class)) {
   return this;
  }
 }
 double new_mag = (Double) scalar.toJava(Double.class);
 double current = FastMath.sqrt(FastMath.pow(jx, 2) + FastMath.pow(jy, 2) + FastMath.pow(jz, 2));
 jx *= new_mag / current;
 jy *= new_mag / current;
 jz *= new_mag / current;
 return this;
}

See the full code here.

Friday 25 April 2014

Experiments with JRuby Integration (for ruby-processing)

Here is the Vec2 class (I wanted to have Vec2D but that doesn't play too well with JRuby, well at least not for me), very loosely based on yokolets Fraction example.
package processing.vec2;

import org.apache.commons.math3.util.FastMath;
import org.jruby.Ruby;
import org.jruby.RubyBoolean;
import org.jruby.RubyClass;
import org.jruby.RubyFloat;
import org.jruby.RubyObject;
import org.jruby.anno.JRubyClass;
import org.jruby.anno.JRubyMethod;
import org.jruby.runtime.Arity;
import org.jruby.runtime.ThreadContext;
import org.jruby.runtime.builtin.IRubyObject;

/**
 *
 * @author Martin Prout
 */
@JRubyClass(name = "Processing::Vec2")
public class Vec2 extends RubyObject {

    static final double EPSILON = 1.0e-04; // matches processing.org EPSILON
    double jx = 0;
    double jy = 0;

    /**
     *
     * @param context
     * @param klazz
     * @param args optional (no args jx = 0, jy = 0)
     * @return new Vec2 object (ruby)
     */
    @JRubyMethod(name = "new", meta = true, rest = true)
    public static IRubyObject rbNew(ThreadContext context, IRubyObject klazz, IRubyObject[] args) {
        Vec2 vec2 = (Vec2) ((RubyClass) klazz).allocate();
        vec2.init(context, args);
        return vec2;
    }

    /**
     *
     * @param runtime
     * @param klass
     */
    public Vec2(Ruby runtime, RubyClass klass) {
        super(runtime, klass);
    }

    void init(ThreadContext context, IRubyObject[] args) {
        if (Arity.checkArgumentCount(context.getRuntime(), args, Arity.OPTIONAL.getValue(), 2) == 2) {
            jx = (Double) args[0].toJava(Double.class);
            jy = (Double) args[1].toJava(Double.class);
        }
    }

    /**
     *
     * @param context
     * @return
     */
    @JRubyMethod(name = "x")
    public RubyFloat getX(ThreadContext context) {
        return context.getRuntime().newFloat(jx);
    }

    /**
     *
     * @param context
     * @return
     */
    @JRubyMethod(name = "y")
    public RubyFloat getY(ThreadContext context) {
        return context.getRuntime().newFloat(jy);
    }

    /**
     *
     * @param context
     * @param other
     * @return
     */
    @JRubyMethod(name = "set_x")
    public RubyFloat setX(ThreadContext context, IRubyObject other) {
        double scalar = (Double) other.toJava(Double.class);
        jx = scalar;
        return context.getRuntime().newFloat(jx);
    }

    /**
     *
     * @param context
     * @param other
     * @return
     */
    @JRubyMethod(name = "set_y")
    public RubyFloat setY(ThreadContext context, IRubyObject other) {
        double scalar = (Double) other.toJava(Double.class);
        jy = scalar;
        return context.getRuntime().newFloat(jy);
    }

    /**
     *
     * @param context
     * @param other
     * @return
     */
    @JRubyMethod(name = "dist")
    public IRubyObject dist(ThreadContext context, IRubyObject other) {
        Vec2 b = (Vec2) other.toJava(Vec2.class);
        double result = FastMath.sqrt((jx - b.jx) * (jx - b.jx) + (jy - b.jy) * (jy - b.jy));
        return context.getRuntime().newFloat(result);
    }

    /**
     *
     * @param context
     * @param other
     * @return
     */
    @JRubyMethod(name = "dist_squared")
    public IRubyObject dist_squared(ThreadContext context, IRubyObject other) {
        Vec2 b = (Vec2) other.toJava(Vec2.class);
        double result = (jx - b.jx) * (jx - b.jx) + (jy - b.jy) * (jy - b.jy);
        return context.getRuntime().newFloat(result);
    }

    /**
     *
     * @param context
     * @param other
     * @return
     */
    @JRubyMethod(name = "cross")
    public IRubyObject cross(ThreadContext context, IRubyObject other) {
        Vec2 b = (Vec2) other.toJava(Vec2.class);
        return context.getRuntime().newFloat(jx * b.jy - jy * b.jx);
    }

    /**
     *
     * @param context
     * @param other
     * @return
     */
    @JRubyMethod(name = "dot")
    public IRubyObject dot(ThreadContext context, IRubyObject other) {
        Vec2 b = (Vec2) other.toJava(Vec2.class);
        return context.getRuntime().newFloat(jx * b.jx + jy * b.jy);
    }

    /**
     *
     * @param context
     * @param other
     * @return new Vec2 object (ruby)
     */
    @JRubyMethod(name = "add")
    public IRubyObject add(ThreadContext context, IRubyObject other) {
        Vec2 b = (Vec2) other.toJava(Vec2.class);
        RubyFloat[] input = {context.getRuntime().newFloat(jx + b.jx),
            context.getRuntime().newFloat(jy + b.jy)};
        return Vec2.rbNew(context, other.getMetaClass(), input);
    }

    /**
     *
     * @param context
     * @param other
     * @return new Vec2 object (ruby)
     */
    @JRubyMethod(name = "sub")
    public IRubyObject sub(ThreadContext context, IRubyObject other) {
        Vec2 b = (Vec2) other.toJava(Vec2.class);
        RubyFloat[] input = {context.getRuntime().newFloat(jx - b.jx),
            context.getRuntime().newFloat(jy - b.jy)};
        return Vec2.rbNew(context, other.getMetaClass(), input);
    }

    /**
     *
     * @param context
     * @param other
     * @return new Vec2 object (ruby)
     */
    @JRubyMethod(name = "mult")
    public IRubyObject mult(ThreadContext context, IRubyObject other) {
        double scalar = (Double) other.toJava(Double.class);
        RubyFloat[] input = {context.getRuntime().newFloat(jx * scalar),
            context.getRuntime().newFloat(jy * scalar)};
        return Vec2.rbNew(context, this.getMetaClass(), input);
    }

    /**
     *
     * @param context
     * @param other
     * @return new Vec2 object (ruby)
     */
    @JRubyMethod(name = "div")
    public IRubyObject div(ThreadContext context, IRubyObject other) {
        double scalar = (Double) other.toJava(Double.class);
        if (FastMath.abs(scalar) < Vec2.EPSILON) {
            return this;
        }
        RubyFloat[] input = {context.getRuntime().newFloat(jx / scalar),
            context.getRuntime().newFloat(jy / scalar)};
        return Vec2.rbNew(context, this.getMetaClass(), input);
    }

    /**
     *
     * @param context
     * @return
     */
    @JRubyMethod(name = "heading")
    public IRubyObject heading(ThreadContext context) {
        return context.getRuntime().newFloat(FastMath.atan2(-jy, jx) * -1.0);
    }

    /**
     *
     * @param context
     * @return
     */
    @JRubyMethod(name = "mag_squared")
    public IRubyObject mag_squared(ThreadContext context) {
        return context.getRuntime().newFloat(jx * jx + jy * jy);
    }

    /**
     *
     * @param context
     * @return
     */
    @JRubyMethod(name = "mag")
    public IRubyObject mag(ThreadContext context) {
        return context.getRuntime().newFloat(FastMath.hypot(jx, jy));
    }

    /**
     *
     * @param context
     * @param scalar
     * @return
     * @todo have optional conditional block (evaluate to boolean)
     */
    @JRubyMethod(name = "set_mag")
    public IRubyObject set_mag(ThreadContext context, IRubyObject scalar) {
        double new_mag = (Double) scalar.toJava(Double.class);
        double current = FastMath.sqrt(FastMath.pow(jx, 2) + FastMath.pow(jy, 2));
        jx *= new_mag / current;
        jy *= new_mag / current;
        return this;
    }

    /**
     *
     * @param context
     * @return this as a ruby object
     */
    @JRubyMethod(name = "normalize!")
    public IRubyObject normalize_bang(ThreadContext context) {
        double mag = FastMath.sqrt(FastMath.pow(jx, 2) + FastMath.pow(jy, 2));
        jx /= mag;
        jy /= mag;
        return this;
    }

    /**
     *
     * @param context
     * @return new Vec2 object (ruby)
     */
    @JRubyMethod(name = "normalize")
    public IRubyObject normalize(ThreadContext context) {
        double mag = FastMath.sqrt(FastMath.pow(jx, 2) + FastMath.pow(jy, 2));
        RubyFloat[] input = {context.getRuntime().newFloat(jx / mag),
            context.getRuntime().newFloat(jy / mag)};
        return Vec2.rbNew(context, this.getMetaClass(), input);
    }

    /**
     * Example of a regular ruby class method
     *
     * @param context
     * @param klazz the klazz of object we are creating
     * @param other input angle in radians
     * @return new Vec2 object (ruby)
     */
    @JRubyMethod(name = "from_angle", meta = true)
    public static IRubyObject from_angle(ThreadContext context, IRubyObject klazz, IRubyObject other) {
        final double scalar = (Double) other.toJava(Double.class);
        RubyFloat[] args = {context.getRuntime().newFloat(FastMath.cos(scalar)),
            context.getRuntime().newFloat(FastMath.sin(scalar))};
        Vec2 vec2 = (Vec2) ((RubyClass) klazz).allocate();
        vec2.init(context, args);
        return vec2;
    }

    /**
     *
     * @param context
     * @param other
     * @return this Vec2 object rotated
     */
    @JRubyMethod(name = "rotate!")
    public IRubyObject rotate_bang(ThreadContext context, IRubyObject other) {
        double theta = (Double) other.toJava(Double.class);
        double x = (jx * FastMath.cos(theta) - jy * FastMath.sin(theta));
        double y = (jx * FastMath.sin(theta) + jy * FastMath.cos(theta));
        jx = x;
        jy = y;
        return this;
    }

    /**
     *
     * @param context
     * @param other
     * @return a new Vec2 object rotated
     */
    @JRubyMethod(name = "rotate")
    public IRubyObject rotate(ThreadContext context, IRubyObject other) {
        double theta = (Double) other.toJava(Double.class);
        double x = (jx * FastMath.cos(theta) - jy * FastMath.sin(theta));
        double y = (jx * FastMath.sin(theta) + jy * FastMath.cos(theta));
        RubyFloat[] input = {context.getRuntime().newFloat(x),
            context.getRuntime().newFloat(y)};
        return Vec2.rbNew(context, this.getMetaClass(), input);
    }

    /**
     *
     * @param context
     * @param args
     * @return as a new Vec2 object (ruby)
     */
    @JRubyMethod(name = "lerp", rest = true)
    public IRubyObject lerp(ThreadContext context, IRubyObject[] args) {
        Arity.checkArgumentCount(context.getRuntime(), args, 2, 2);
        Vec2 vec = (Vec2) args[0].toJava(Vec2.class);
        double scalar = (Double) args[1].toJava(Double.class);
        assert (scalar >= 0 && scalar < 1.0) :
                "Lerp value " + scalar + " out of range 0 .. 1.0";
        double x0 = jx + (vec.jx - jx) * scalar;
        double y0 = jx + (vec.jx - jx) * scalar;
        RubyFloat[] input = {context.getRuntime().newFloat(x0),
            context.getRuntime().newFloat(y0)};
        return Vec2.rbNew(context, this.getMetaClass(), input);
    }

    /**
     *
     * @param context
     * @param args
     * @return this
     */
    @JRubyMethod(name = "lerp!", rest = true)
    public IRubyObject lerp_bang(ThreadContext context, IRubyObject[] args) {
        Arity.checkArgumentCount(context.getRuntime(), args, 2, 2);
        Vec2 vec = (Vec2) args[0].toJava(Vec2.class);
        double scalar = (Double) args[1].toJava(Double.class);
        assert (scalar >= 0 && scalar < 1.0) :
                "Lerp value " + scalar + " out of range 0 .. 1.0";
        double x0 = jx + (vec.jx - jx) * scalar;
        double y0 = jx + (vec.jx - jx) * scalar;
        jx = x0;
        jy = y0;
        return this;
    }

    /**
     *
     * @param context
     * @return
     */
    @JRubyMethod(name = "copy")
    public IRubyObject copy(ThreadContext context) {
        double x0 = jx;
        double y0 = jy;
        RubyFloat[] input = {context.getRuntime().newFloat(x0),
            context.getRuntime().newFloat(y0)};
        return Vec2.rbNew(context, this.getMetaClass(), input);
    }

    /**
     *
     * @param context
     * @return
     */
    @JRubyMethod(name = "to_a")
    public IRubyObject toArray(ThreadContext context) {
        RubyFloat[] output = {context.getRuntime().newFloat(jx), context.getRuntime().newFloat(jy)};
        return context.getRuntime().newArray(output);
    }

    /**
     *
     * @param context
     * @return
     */
    @JRubyMethod(name = "to_s")
    public IRubyObject to_s(ThreadContext context) {
        return context.getRuntime().newString(String.format("Vec2(x = %4.4f, y = %4.4f)", jx, jy));
    }

    /**
     *
     * @return
     */
    @Override
    public int hashCode() {
        int hash = 5;
        hash = 53 * hash + (int) (Double.doubleToLongBits(this.jx) ^ (Double.doubleToLongBits(this.jx) >>> 32));
        hash = 53 * hash + (int) (Double.doubleToLongBits(this.jy) ^ (Double.doubleToLongBits(this.jy) >>> 32));
        return hash;
    }

    /**
     *
     * @param obj
     * @return
     */
    @Override
    public boolean equals(Object obj) {
        if (obj == null) {
            return false;
        }
        if (getClass() != obj.getClass()) {
            return false;
        }
        final Vec2 other = (Vec2) obj;
        if (Double.doubleToLongBits(this.jx) != Double.doubleToLongBits(other.jx)) {
            return false;
        }
        return (Double.doubleToLongBits(this.jy) == Double.doubleToLongBits(other.jy));
    }

    /**
     *
     * @param context
     * @param other
     * @return
     */
    @JRubyMethod(name = "almost_eql?")
    public IRubyObject almost_eql_p(ThreadContext context, IRubyObject other) {
        Vec2 v = (other instanceof Vec2) ? (Vec2) other.toJava(Vec2.class) : null;
        IRubyObject result = (v == null) ? RubyBoolean.newBoolean(context.getRuntime(), false)
                : (FastMath.abs(jx - v.jx) > Vec2.EPSILON)
                ? RubyBoolean.newBoolean(context.getRuntime(), false)
                : (FastMath.abs(jy - v.jy) > Vec2.EPSILON)
                ? RubyBoolean.newBoolean(context.getRuntime(), false)
                : RubyBoolean.newBoolean(context.getRuntime(), true);
        return result; // return false as default unless not null && values equal
    }

}

Here is the Vec2Service class
package processing.vec2;
import java.io.IOException;

import org.jruby.Ruby;
import org.jruby.RubyClass;
import org.jruby.RubyModule;
import org.jruby.runtime.ObjectAllocator;
import org.jruby.runtime.builtin.IRubyObject;
import org.jruby.runtime.load.BasicLibraryService;


public class Vec2Service implements BasicLibraryService {
    @Override
    public boolean basicLoad(final Ruby runtime) throws IOException {
        RubyModule processing = runtime.defineModule("Processing");
        RubyModule vec2Module = processing.defineModuleUnder("Vec2");
        RubyClass vec2 = vec2Module.defineClassUnder(Vec2.class.getSimpleName(), runtime.getObject(), VEC_ALLOCATOR);
        vec2.defineAnnotatedMethods(Vec2.class);
        return true;
    }

    private static final ObjectAllocator VEC_ALLOCATOR = new ObjectAllocator() {
        @Override
        public IRubyObject allocate(Ruby runtime, RubyClass klazz) {
            return new Vec2(runtime, klazz);
        }
    };
}

Here is the Rakefile jruby -S rake compile
# -*- ruby -*-
require 'java'
require 'rake/javaextensiontask'

Rake::JavaExtensionTask.new('processing/vec2') do |ext|
  jars = FileList['lib/*.jar']
  ext.classpath = jars.map {|x| File.expand_path x}.join ':'
  ext.name = 'processing/vecmath'
  ext.debug=true
  ext.source_version='1.7'
  ext.target_version='1.7'
end

Here is vec2.rb
require_relative './vecmath'
require 'processing/vec2/vec2'

module Processing
  module Vec2
    class Vec2
      alias_method :*, :mult
      alias_method :/, :div
      alias_method :+, :add
      alias_method :-, :sub
      alias_method :==, :eql?
    end
  end
end

Here are some sample usages:-
require 'java'
require_relative '../lib/processing_vecmath'
include Processing::Vec2
vec = Vec2.new(0, 0)
vec1 = Vec2.new(0, 0)
vec2 = Vec2.new(3.0, 4.0)
vec3 =Vec2.new(1.00000000000, 1.000000000000)
puts vec3.normalize
puts "mult #{vec3}"
puts "add #{vec2 + vec3}"
puts "original #{vec2}"
puts vec == vec1
puts vec2.almost_eql? Vec2.new(3.0, 4.0)
puts vec.almost_eql? Vec2.new(0, 0)
puts vec.dist(vec2)
puts vec2.mag

Here is the output:-
Vec2(x = 0.7071, y = 0.7071)
mult Vec2(x = 1.0000, y = 1.0000)
add Vec2(x = 4.0000, y = 5.0000)
original Vec2(x = 3.0000, y = 4.0000)
true
true
true
5.0
5.0
Interesting working in Netbeans the rake build script was unecessary, but size of compiled jar was much smaller with the script (however I had linked jruby-9000 in Netbeans). See repository on github for Vec3 and spec tests.

Thursday 24 April 2014

Netbeans-8 rubyplugin now available (run ruby files from the ide)

Well this will no doubt be useful for ruby-processing development:-

Saturday 19 April 2014

Testing the water for more jruby integration

For some time now I've been thinking about implementing stuff in jruby java for ruby-processing. To that end I have forked yokolets Fraction class here. This could be a sort of half-way house where the guts could be implemented in pure java, but with a compiled jruby java interface, but I am sure I would do better with 100% jruby implementation of say my vecmath library (which currently is pure ruby). For performance, it is possible (but by no means a certainty to me) that SinCos lut tables would improve ruby-processing performance, but they will definetly need jruby java implementation to work. Charles nutter recently cited this as possible model for writing a jruby extension other people point to jrubies RubyEnumerable implementation.

Wednesday 16 April 2014

Latest ruby-processing release 2.4.4

The latest release ruby-processing-2.4.4 has been updated to use the just released jruby-1.7.12 (which is mainly bug-fixes). Coincidentally a new version of vanilla processing (version 2.1.2) has also just been released. I have been testing this processing-2.1.2 during its development on my linux box, and the most significant change for ruby-processing users is that we are now able to use watch mode with P2D and P3D, but there have been other changes that might make this release work better generally on MacOSX. I am therefore recommending that all ruby-processing users update processing to the latest version. One caveat is that I expect application export to be broken on the Mac, it should not be too difficult to fix but I haven't got a Mac, so I'm hoping someone supplies a fix.

Tuesday 8 April 2014

Ruby-Processing Architecture

Recently I watched a presentation by Hisaro Asari on JRuby which inspired me to produce this for ruby-processing:-

The interesting from this talk was the future direction of JRuby including replacing the current the Lexical Analysis parsing to an AST with a Semantic Analysis to an Intermediate Representation. This will allow for optimization, before passing to compiler (via Control Flow Graph, and Data Flow Graph analysis?). In the further future there may be foreign function interface support at the jvm level (Charlie has submitted an ffi proposal, 2016 at earliest), and there is the experimental truffle and graaal. What is clear is there will be no further support for ruby-2.0.0 (experimental in JRuby 1.7) with JRuby-9000 aiming at ruby-2.1.0 support.


Seems we may not need to wait too long to test the new Intermediate Representation runtime (and then we might also be able to start using ruby-2.1.0 syntax?).

Monday 7 April 2014

Funked up drawolver

Now draw multiple shapes, use 'c' key to clear.
# Composite Drawolver: draw 2D & revolve 3D

# Example to show how to use the VecMath library.
# Also features the use each_cons, possibly a rare 
# use for this ruby Enumerable method?
# From an original by fjenett (funked up by monkstone)
# now uses 'zip' and 'each', in place of a custom Array object
# with a 'one_of_each' method

load_library :vecmath

attr_reader :drawing_mode, :points, :rot_x, :rot_y, :vertices

def setup
  size 1024, 768, P3D
  frame_rate 30
  reset_scene
end

def draw
  background 0
  if (!drawing_mode)
    translate(width/2, height/2)
    rotate_x rot_x
    rotate_y rot_y
    @rot_x += 0.01
    @rot_y += 0.02
    translate(-width/2, -height/2)
  end
  no_fill
  stroke 255
  points.each_cons(2) { |ps, pe| line ps.x, ps.y, pe.x, pe.y}

  if (!drawing_mode)
    stroke 125
    fill 120
    lights
    ambient_light 120, 120, 120
    vertices.each_cons(2) do |r1, r2|
      begin_shape(TRIANGLE_STRIP)
      r1.zip(r2).each do |v1, v2|
        vertex v1.x, v1.y, v1.z
        vertex v2.x, v2.y, v2.z
      end
      end_shape
    end
  end
end

def reset_scene
  @drawing_mode = true
  @points = []
  @rot_x = 0.0
  @rot_y = 0.0
end

def mouse_pressed
  reset_scene
  points << Vec3D.new(mouse_x, mouse_y)
end

def mouse_dragged
  points << Vec3D.new(mouse_x, mouse_y)
end

def mouse_released
  points << Vec3D.new(mouse_x, mouse_y)
  recalculate_shape
end

def key_pressed
  case key
  when 'c'
    @vertices.clear
    reset_scene
  end
end

def recalculate_shape
  @vertices ||= []
  points.each_cons(2) do |ps, pe|
    b = points.last - points.first
    b.normalize!
    a = ps - points.first
    dot = a.dot b
    b = b * dot
    normal = points.first + b
    c = ps - normal
    vertices << []
    ecos = ->(n, c, a){n +c * cos(a)}
    esin = ->(c, a){c.mag * sin(a)}
    (0..TAU).step(PI/15) do |ang|
      e = ecos.call(normal, c, ang)
      e.z = esin.call(c, ang)
      vertices.last << e
    end
  end
  @drawing_mode = false
end

Thursday 3 April 2014

Enhance ruby-processing functionality with the rpbundle gem

One of the annoyances of ruby-processing has been the need to use an installed jruby to use rubygems (well in itself it is not an annoyance), because this bars us from using rubygems with sketches that require jruby-complete to run (mainly load_image but also shader sketches, and this has been the annoyance). Thanks to the recently released rpbundle gem this is no longer a problem since Emil Soman has figured a way to use bundler with jruby-complete (and gems installed by rpbundle are accessible to ruby-processing using the vendored jruby-complete). Why is this exciting well for the first time I will be able to experiment with ai4r gem with loaded images, but also mdarray (similar to numpy) so scope for image transformations. Installing rpbundle is very straightforward if you already use bundler and/or rvm user, and not much harder if you eschew both of them, you need only add the .rpbundle/bin to your path.
You need to create a Gemfile in your working folder eg:-
source 'https://rubygems.org'
gem "ai4r", "~> 1.13"

# if using jruby it may pay to specify engine


Here is the working sketch which after rpbundle install to install the ai4r gem you run with rpbundle exec run/watch sketch.rb

#####################################################################
# Using the ai4r gem in ruby-processing.
# A simple example that demonstrates using
# a backpropagation neural network. Use the drop box menu to
# select a prebuilt shape. To draw a test shape tick drawing checkbox,
# release the mouse when drawing a discontinous shape eg cross.
# Clear the sketch with clear button. 
# Press evaluate and result is printed to the console....
####################################################################

require 'ai4r'
require 'json'

load_library :vecmath, :control_panel

attr_reader  :img, :img_pixels, :ci_input, :cr_input, :tr_input, :sq_input, :net, :points, :panel, :hide, :drawing, :source_string

def setup
  size(320, 320)
  control_panel do |c|
    c.title = "control"
    c.look_feel "Nimbus"
    c.checkbox :drawing
    c.button :clear
    c.button :evaluate
    c.menu :shape, ['CIRCLE', 'CROSS', 'CROSS_WITH_NOISE', 'SQUARE', 'SQUARE_WITH_NOISE', 'TRIANGLE', 'DEFAULT']
    @panel = c
  end
  @hide = false
  @source_string = open("data/data.json", "r"){ |file| file.read }
  triangle = JSON.parse(source_string)["TRIANGLE"]
  square = JSON.parse(source_string)["SQUARE"]
  cross = JSON.parse(source_string)["CROSS"]
  circle = JSON.parse(source_string)["CIRCLE"]
  @points = []
  srand 1
  @net = Ai4r::NeuralNetwork::Backpropagation.new([256, 3])
  @tr_input = triangle.flatten.collect { |input| input.to_f / 127.0}
  @sq_input = square.flatten.collect { |input| input.to_f / 127.0}
  @cr_input = cross.flatten.collect { |input| input.to_f / 127.0}
  @ci_input = circle.flatten.collect { |input| input.to_f / 127.0}
  train
  background 255
end


def draw
  # only make control_panel visible once, or again when hide is false
  unless hide
    @hide = true
    panel.set_visible(hide)
  end
  if drawing
    stroke_weight 32
    stroke 127
    points.each_cons(2) { |ps, pe| line ps.x, ps.y, pe.x, pe.y}
  else
    no_fill
    stroke_weight(32)
    stroke(127)
    case @shape
    when 'CIRCLE'
      background(255)
      img = load_image('circle.png')
      image(img, 0, 0)
      @shape = 'DEFAULT'
    when 'CROSS'
      img = load_image('cross.png')
      image(img, 0, 0)
      @shape = 'DEFAULT'
    when 'CROSS_WITH_NOISE','SQUARE_WITH_NOISE'
      background(255)
      draw_shape @shape
      @shape = 'DEFAULT'
    when 'SQUARE'
      img = load_image('square.png')
      image(img, 0, 0)
      background(255)
      @shape = 'DEFAULT'
    when 'TRIANGLE'
      img = load_image('triangle.png')
      image(img, 0, 0)
      @shape = 'DEFAULT'
    end
  end
end

def draw_shape shp
  shape = JSON.parse(source_string)[shp]
  background(255)
  no_stroke
  (0  ... width / 20).each do |i|
    (0  ... height / 20).each do |j|
      col = 255 - shape[i][j]
      fill(col)
      rect(i * 20, j * 20,  20,  20)
    end
  end
end

def train
  puts "Training Network Please Wait"
  101.times do |i|
    error = net.train(tr_input, [1.0, 0, 0])
    error = net.train(sq_input, [0, 1.0, 0])
    error = net.train(cr_input, [0, 0, 1.0])
    error = net.train(ci_input, [0, 1.0, 1.0])
    puts "Error after iteration #{i}:\t#{format("%.5f", error)}" if i%20 == 0
  end
end

def result_label(result)
  if result.inject(0, :+).between?(1.9, 2.1)
    if result[0] < 0.01 && result[1].between?(0.99, 1.0) && result[2].between?(0.99, 1.0)
      return "CIRCLE"
    else
      return "UNKNOWN"
    end
  elsif result.inject(0, :+).between?(0.95, 1.1)
    if result[0].between?(0.95, 1.0) && (result[1] + result[2]) < 0.01
      return "TRIANGLE"
    elsif result[1].between?(0.95, 1.0) && (result[0] + result[2]) < 0.01
      return "SQUARE"
    elsif result[2].between?(0.95, 1.0) && (result[1] + result[0]) < 0.01
      return "CROSS"
    else
      return "UNKNOWN"
    end
  end
  return "UNKNOWN"
end

def mouse_dragged
  points << Vec2D.new(mouse_x, mouse_y)
end

def mouse_released
  points.clear
end

def draw_circle
  ellipse(width / 2, height / 2, 320 - 32, 320 - 32)
end

def draw_square
  rect(16, 16, 320 - 32, 320 - 32)
end

def draw_cross
  line(width / 2, 0, width / 2, 320)
  line(0, height / 2,  320 , height / 2)
end

def draw_triangle
  triangle(width / 2, 32, 24, height - 16,  width - 24, height - 16)
end

def clear
  background 255
end

def evaluate
  load_pixels
  img_pixels = []
  (0...height).step(20) do |y|
    row = []
    (0...width).step(20) do |x|
      row << 255 - brightness(pixels[(y + 10) * width + x + 10])
    end
    img_pixels << row
  end
  puts "#{net.eval(img_pixels.flatten).inspect} => #{result_label(net.eval(img_pixels.flatten))}"
end

Followers

Blog Archive

About Me

My photo
I have developed JRubyArt and propane new versions of ruby-processing for JRuby-9.1.5.0 and processing-3.2.2