Code viewer for World: Practical 2: Character rec...
// Cloned by Prathmesh Lad on 5 Dec 2021 from World "Character recognition neural network" by "Coding Train" project 
// Please leave this clone trail here.
 

// Port of Character recognition neural network from here:
// https://github.com/CodingTrain/Toy-Neural-Network-JS/tree/master/examples/mnist
// with many modifications 

// --- defined by MNIST - do not change these ---------------------------------------

const PIXELS = 28;                              // images in data set are tiny 
const PIXELSSQUARED = PIXELS * PIXELS;

// number of training and test exemplars in the data set:
const NOTRAIN = 60000;
const NOTEST  = 10000;

//myCodeBegins
const PIXELS_CENTER_SQ_LEN = 20;
const PIXELS_PADDING_LEN = 4;
//myCodeEnds

//--- can modify all these --------------------------------------------------

// no of nodes in network 
const noinput = PIXELSSQUARED;
const nohidden = 64;
const nooutput = 10;

const learningrate = 0.1;

// should we train every timestep or not 
let do_training = true;

// how many to train and test per timestep
const TRAINPERSTEP = 30;
const TESTPERSTEP = 5;

// multiply it by this to magnify for display
const ZOOMFACTOR = 7;
const ZOOMPIXELS    = ZOOMFACTOR * PIXELS;
// 3 rows of
// large image + 50 gap + small image    
// 50 gap between rows 

const canvaswidth = ( PIXELS + ZOOMPIXELS ) + 50;
const canvasheight = ( ZOOMPIXELS * 3 ) + 100;

const DOODLE_THICK = 11;    // thickness of doodle lines changed to 11
const DOODLE_BLUR = 5;      // blur factor applied to doodles changed to 5

let mnist;      
// all data is loaded into this 
// mnist.train_images
// mnist.train_labels
// mnist.test_images
// mnist.test_labels

let nn;
let trainrun = 1;
let train_index = 0;

let testrun = 1;
let test_index = 0;
let total_tests = 0;
let total_correct = 0;

// images in LHS:
let doodle, demo;
let doodle_exists = false;
let demo_exists = false;

let mousedrag = false;      // are we in the middle of a mouse drag drawing?  


// save inputs to global var to inspect
// type these names in console 
var train_inputs, test_inputs, demo_inputs, doodle_inputs;

//myCodeBegin
 var cnn_model;
 var cnn_trainer;
 var cnn_train_time = 0;
 var cnn_total_correct = 0;
 var cnn_test_time = 0;
 var train_time = 0;
 var test_time = 0;
 //myCodeEnd

function randomWeight() {
    return AB.randomFloatAtoB(-0.5, 0.5);
    // Coding Train default is -1 to 1
}

// make run header bigger
AB.headerCSS({ "max-height": "95vh" });




//--- start of AB.msgs structure: ---------------------------------------------------------
// We output a serious of AB.msgs to put data at various places in the run header 
var thehtml;

// 1 Doodle header 
thehtml = "<hr> <h1> 1. Doodle </h1> Top row: Doodle (left) and shrunk (right). <br>  Draw your doodle in top LHS. <button onclick='wipeDoodle();' class='normbutton' >Clear doodle</button> <br> ";
AB.msg ( thehtml, 1);
// 2 Doodle variable data (guess)
  
// 3 Training header
thehtml = "<hr> <h1> 2. Training </h1> Middle row: Training image magnified (left) and original (right). <br> <button onclick='do_training = false;' class='normbutton' >Stop training</button> <br> ";
AB.msg ( thehtml, 3 );

// 4 variable training data 
  
// 5 Testing header
thehtml = "<h3> Hidden tests </h3> ";
AB.msg(thehtml, 5);

// 6 variable testing data 
  
// 7 Demo header 
thehtml = "<hr> <h1> 3. Demo </h1> Bottom row: Test image magnified (left) and  original (right). <br> The network is <i>not</i> trained on any of these images. <br>  <button onclick='makeDemo();' class='normbutton' >Demo test image</button> <br> ";
AB.msg(thehtml, 7);

// 8 Demo variable data (random demo ID)
// 9 Demo variable data (changing guess)

const greenspan = "<span style='font-weight:bold; font-size:x-large; color:darkgreen'> ";






//--- end of AB.msgs structure: ---------------------------------------------------------




function setup() {
    
    createCanvas(canvaswidth, canvasheight);
    doodle = createGraphics ( ZOOMPIXELS, ZOOMPIXELS );     // doodle on larger canvas 
    doodle.pixelDensity(1);

    // JS load other JS 
    // maybe have a loading screen while loading the JS and the data set

    AB.loadingScreen();
    
    $.getScript("/uploads/codingtrain/matrix.js", function() {
        
        $.getScript("/uploads/codingtrain/nn.js", function() {
            
            $.getScript("/uploads/codingtrain/mnist.js", function() {
                
                console.log("All JS loaded");
                nn = new NeuralNetwork ( noinput, nohidden, nooutput );
                nn.setLearningRate ( learningrate );
                loadData();
            });
        });
    });

    //myCodeBegins
    $.getScript("/uploads/prathmeshlad/convnet-min.js", function() {        //setting the prototype of my CNN
        
        layer_defs = [];
        layer_defs.push({
            type: "input",          // the first input layer
            out_sx: 28,
            out_sy: 28,
            out_depth: 1
        });
        layer_defs.push({
            type: "conv",           // the first convolution layer
            sx: 5,
            filters: 8,
            stride: 1,              //stride set to 1 to cover every cell
            pad: 2,
            activation: "relu"      //activation done with the help of ReLU
        });
        layer_defs.push({
            type: "pool",           //pooling done to keep the data size in check
            sx: 2,
            stride: 2
        });
        layer_defs.push({
            type: "conv",           //the second convolution layer
            sx: 5,
            filters: 16,
            stride: 1,
            pad: 2,
            activation: "relu"      //activation done with the help of ReLU
        });
        layer_defs.push({
            type: "pool",           //final pooling with a stride of 3
            sx: 3,
            stride: 3
        });
        layer_defs.push({
            type: "softmax",
            num_classes: 10
        });
        AB.restoreData ( function(e) {
            if(typeof e == 'undefined'){
                cnn_model.fromJSON(e);
            }
        });
        cnn_model = new convnetjs.Net;
        cnn_model.makeLayers(layer_defs);
        cnn_trainer = new convnetjs.SGDTrainer(cnn_model, {
            method: "adadelta",
            batch_size: 20,
            l2_decay: .001
        });
    });
    //myCodeEnds
}

// load data set from local file (on this server)
function loadData() {
    loadMNIST ( function(data) {
        mnist = data;
        console.log ( "All data loaded into mnist object:" );
        console.log(mnist);
        AB.removeLoading();     // if no loading screen exists, this does nothing 
    });
}

function getImage(img) {        // make a P5 image object from a raw data array   
    
    let theimage = createImage(PIXELS, PIXELS);     // make blank image, then populate it 
    theimage.loadPixels();
    
    for (let i = 0; i < PIXELSSQUARED; i++) {
        let bright = img[i];
        let index = i * 4;
        theimage.pixels[index + 0] = bright;
        theimage.pixels[index + 1] = bright;
        theimage.pixels[index + 2] = bright;
        theimage.pixels[index + 3] = 255;
    }

    theimage.updatePixels();
    return theimage;
}

function getInputs(img) {
    
    let inputs = [];
    for (let i = 0; i < PIXELSSQUARED; i++) {
        
        let bright = img[i];
        inputs[i] = bright / 255;
    }
    return inputs;
}

//myCodeBegins
function getCnnInputs(cnnInputs) {
    for (var train_data = new convnetjs.Vol(PIXELS, PIXELS, 1, 0), n = 0; n < PIXELSSQUARED; n++) {
        train_data.w[n] = cnnInputs[n];
    }
    return train_data;
}
//myCodeEnds

function trainit(show) {                           // train the network with a single exemplar, from global var "train_index", show visual on or off 
    let img = mnist.train_images[train_index],
        label = mnist.train_labels[train_index];
    
        // optional - show visual of the image
    if (show) {
        var theImage = getImage( img );     //get image from data array
        image ( theImage, 0, ZOOMPIXELS + 50, ZOOMPIXELS, ZOOMPIXELS);      //magnified
        image ( theImage, ZOOMPIXELS + 50, ZOOMPIXELS + 50, PIXELS, PIXELS);    //original
    }

    // set up the inputs
    let inputs = getInputs ( img );       // get inputs from data array 
       
    let sample = getCnnInputs(inputs);       //myCode: passing the flattened image array to my CNN 
    
    // set up the outputs
    let targets = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
    targets[label] = 1;       // change one output location to 1, the rest stay at 0

    // console.log(train_index);
    // console.log(inputs);
    // console.log(targets);
    
    train_inputs = inputs;      //can inspect in console

    let timeBeforeTrain = (new Date).getTime();       //myCode: recording the systemtime before training begins
    
    nn.train(inputs, targets);
    
    //myCodeBegins
    train_time += (new Date).getTime() - timeBeforeTrain;       // calculating the time difference to show time taken for normal training
    let cnntimeBeforeTrain = (new Date).getTime();              //recording the time before CNN training begins
    cnn_trainer.train(sample, label);               // training the model with the sample using CNN
    cnn_train_time += (new Date).getTime() - cnntimeBeforeTrain;    // calculating the time difference to show time taken for training with CNN
    //myCodeEnds
    
    thehtml = " trainrun: " + trainrun + "<br> no: " + train_index;
    AB.msg (thehtml, 4);
    
    
    train_index++;
    if(train_index == NOTRAIN){
        train_index = 0;
        console.log("finished trainrun: " + trainrun);

        AB.saveData(cnn_model.toJSON());        // Saving the trained model to the server.
        
        trainrun++;
    }
}

function testit() {     // test the network with a single exemplar, from global var "test_index"
    let img = mnist.test_images[test_index];
    let label = mnist.test_labels[test_index];
    
    // set up the inputs
    let inputs = getInputs(img);

    test_inputs = inputs;       // can inspect in console 

    let timeBeforeTest = (new Date).getTime();       // Recording time before testing begins for existing neural net

    let prediction = nn.predict(inputs);        // array of outputs 
    test_time += (new Date).getTime() - timeBeforeTest;      //calculating existing test time

    let guess = findMax(prediction);        // the top output

    //myCodeBegins
    let sample = getCnnInputs(inputs);      //passing the flattened image array to my trained CNN model
    let cnnTimeBeforeTest = (new Date).getTime();         //recording the time before CNN testing begins
    let cnnPrediction = cnn_model.forward(sample);      //passing the sample data through every layer
    cnn_test_time += (new Date).getTime() - cnnTimeBeforeTest;  //calculating the time taken for CNN testing
    let cnnGuess = findMax(cnnPrediction.w);            //the top output in CNN prediction
    //myCodeEnds

    total_tests++;
    if(guess == label){
        total_correct++;        //incrementing existing neural network prediction score
    }

    //myCodeBegins
    if(cnnGuess == label){
        cnn_total_correct++;            //incrementing the CNN prediction score
    }
    //myCodeEnds

    let percent = total_correct / total_tests * 100;

    let cnnPercent = cnn_total_correct / total_tests * 100;     //calculating total score

    thehtml = " Testrun: " + testrun + "    No: " + total_tests + " <br>  Existing Correct: " + total_correct + "     CNN Correct: " + cnn_total_correct + "<br> Existing Train Time(ms): " + train_time + "  CNN Train Time(ms): " + cnn_train_time + "<br> Existing Prediction Time(ms): " + test_time + "      CNN Prediction Time(ms): " + cnn_test_time + "<br> Existing Score(%): " + greenspan + percent.toFixed(2) + " </span>  CNN Score(%): " + greenspan + cnnPercent.toFixed(2) + "</span>";
    AB.msg(thehtml, 6);
    
    test_index++;
    if(test_index == NOTEST ){
        console.log("finished testrun: " + testrun + " score: " + percent.toFixed(2) );
        testrun++;
        test_index = 0;
        total_tests = 0;
        total_correct = 0;
        cnn_total_correct = 0;
    }
}


//--- find no.1 (and maybe no.2) output nodes ---------------------------------------
// (restriction) assumes array values start at 0 (which is true for output nodes) 

function find12(a) {        // return array showing indexes of no.1 and no.2 values in array 
    let no1 = 0;
    let no2 = 0;
    let no1value = 0;
    let no2value = 0;
    
    for (let i = 0; i < a.length; i++){

        if( a[i] > no1value){       // new no1
            
            // old no1 becomes no2
            no2 = no1;
            no2value = no1value;
            // now put in the new no1
            no1 = i;
            no1value = a[i];
         }
         else if( a[i] > no2value){     // new no2
             no2 = i;
             no2value = a[i];
         }
    }

    var b = [ no1, no2 ];
    return b;
}

// just get the maximum - separate function for speed - done many times 
// find our guess - the max of the output nodes array

function findMax(a) {
    let no1 = 0;
    let no1value = 0;

    for (let i = 0; i < a.length; i++){

        if(a[i] > no1value){

            no1 = i;
            no1value = a[i];
        }
    }

    return no1;
}


// --- the draw function -------------------------------------------------------------
// every step:

function draw() {
    
    // check if libraries and data loaded yet:
    if ( typeof mnist == 'undefined' ) return;
    
    // how can we get white doodle on black background on yellow canvas?
    // background('#ffffcc');    doodle.background('black');

    background ('black');

    if ( do_training ){
        // do some training per step 
        for (let i = 0; i < TRAINPERSTEP; i++) {
            if (i == 0)
                trainit(true);    // show only one per step - still flashes by
            else 
                trainit(false);
        }
        
        // do some testing per step 
        for (let i = 0; i < TESTPERSTEP; i++) 
            testit();
    }
    
    // keep drawing demo and doodle images 
    // and keep guessing - we will update our guess as time goes on 
    if ( demo_exists ){
        drawDemo();
        guessDemo();
    }
    
    if ( doodle_exists ){
        drawDoodle();
        guessDoodle();
    }
    
    // detect doodle drawing 
    // (restriction) the following assumes doodle starts at 0,0 
    if ( mouseIsPressed ){         // gets called when we click buttons, as well as if in doodle corner
        // console.log ( mouseX + " " + mouseY + " " + pmouseX + " " + pmouseY );
        var MAX = ZOOMPIXELS + 20;     // can draw up to this pixels in corner 
        if ( (mouseX < MAX) && (mouseY < MAX) && (pmouseX < MAX) && (pmouseY < MAX) ){
            mousedrag = true;       // start a mouse drag 
            doodle_exists = true;
            doodle.stroke('white');
            doodle.strokeWeight( DOODLE_THICK );
            doodle.line(mouseX, mouseY, pmouseX, pmouseY);      
        }
    }
    else{
        // are we exiting a drawing
        if ( mousedrag ){
            mousedrag = false;
            // console.log ("Exiting draw. Now blurring.");
            doodle.filter (BLUR, DOODLE_BLUR);    // just blur once 
            //   console.log (doodle);
        }
    }
}


//--- demo -------------------------------------------------------------
// demo some test image and predict it
// get it from test set so have not used it in training


function makeDemo()
{
    demo_exists = true;
    var  i = AB.randomIntAtoB ( 0, NOTEST - 1 );  
    
    demo        = mnist.test_images[i];     
    var label   = mnist.test_labels[i];
    
   thehtml =  "Test image no: " + i + "<br>" + 
            "Classification: " + label + "<br>" ;
   AB.msg ( thehtml, 8 );
   
   // type "demo" in console to see raw data 
}

function drawDemo()
{
    var theimage = getImage ( demo );
     //  console.log (theimage);
     
    image ( theimage,   0,                canvasheight - ZOOMPIXELS,    ZOOMPIXELS,     ZOOMPIXELS  );      // magnified 
    image ( theimage,   ZOOMPIXELS+50,    canvasheight - ZOOMPIXELS,    PIXELS,         PIXELS      );      // original
}

function guessDemo() {
    let inputs = getInputs(demo);

    let cnnInputs = getCnnInputs(inputs);

    demo_inputs = inputs;  // can inspect in console

    let prediction = nn.predict(inputs);
    let guess = findMax(prediction);

    let cnnPrediction = cnn_model.forward(cnnInputs).w;
    let cnnGuess = findMax(cnnPrediction);

    thehtml = " Existing Nerul net classify it as: " + greenspan + guess + "</span> <br> CNN classify it as: " + greenspan + cnnGuess + "</span> <br>";
    AB.msg(thehtml, 9);
}

//--- doodle -------------------------------------------------------------

function drawDoodle()
{
    // doodle is createGraphics not createImage
    let theimage = doodle.get();
    // console.log (theimage);
    
    image ( theimage,   0,                0,    ZOOMPIXELS,     ZOOMPIXELS  );      // original 
    image ( theimage,   ZOOMPIXELS+50,    0,    PIXELS,         PIXELS      );      // shrunk
}


function printImg(e) {
    let t = [];
    for (let n = 0; n < e.width * e.height; n++) t[n] = e.pixels[4 * n];
    showInputs(PIXELS, inputs.width)
}

function preProcessDoodleImage(e) {
    let t = [];
    for (let n = 0; n < PIXELSSQUARED; n++) t[n] = e.pixels[4 * n];
    let n = Number.MAX_VALUE,
        o = Number.MIN_VALUE;
    for (let e = 0; e < PIXELS; e++) {
        let i = Number.MAX_VALUE,
            s = Number.MIN_VALUE;
        for (let n = 0; n < PIXELS; n++) t[e * PIXELS + n] > 0 && (s = n, i == Number.MAX_VALUE && (i = n));
        n = Math.min(n, i), o = Math.max(o, s)
    }
    let i = Number.MAX_VALUE,
        s = Number.MIN_VALUE;
    for (let e = 0; e < PIXELS; e++) {
        let n = Number.MAX_VALUE,
            o = Number.MIN_VALUE;
        for (let i = 0; i < PIXELS; i++) t[i * PIXELS + e] > 0 && (o = i, n == Number.MAX_VALUE && (n = i));
        i = Math.min(i, n), s = Math.max(s, o)
    }
    let r = o - n + 1,
        a = s - i + 1,
        l = createImage(r, a);
    l.loadPixels();
    for (let e = 0; e < a; e++)
        for (let o = 0; o < r; o++) {
            let s = t[(e + i) * PIXELS + (o + n)];
            l.set(o, e, color(s, s, s))
        }
    l.updatePixels(), r >= a ? l.resize(PIXELS_CENTER_SQ_LEN, 0) : l.resize(0, PIXELS_CENTER_SQ_LEN), l.loadPixels();
    let d = 0,
        m = 0,
        c = 0;
    for (let e = 0; e < l.height; e++)
        for (let t = 0; t < l.width; t++) {
            let n = l.pixels[4 * (e * l.width + t)];
            d += n, m += (t + 1) * n, c += (e + 1) * n
        }
    let _ = 0 == d ? 0 : Math.round(m / d),
        u = 0 == d ? 0 : Math.round(c / d),
        g = PIXELS_PADDING_LEN + (PIXELS_CENTER_SQ_LEN / 2 - u),
        E = PIXELS_PADDING_LEN + (PIXELS_CENTER_SQ_LEN / 2 - _);
    shiftImg = createImage(PIXELS, PIXELS), shiftImg.loadPixels();
    for (let e = 0; e < PIXELS; e++)
        for (let t = 0; t < PIXELS; t++)
            if (e >= PIXELS_PADDING_LEN && t >= PIXELS_PADDING_LEN && e < PIXELS - PIXELS_PADDING_LEN && t < PIXELS - PIXELS_PADDING_LEN)
                if (t >= E && e >= g && t <= E + l.width - 1 && e <= g + l.height - 1) {
                    let n = l.pixels[4 * (l.width * (e - g) + (t - E))];
                    shiftImg.set(t, e, color(n, n, n))
                } else shiftImg.set(t, e, color(0, 0, 0));
    else shiftImg.set(t, e, color(0, 0, 0));
    return shiftImg.updatePixels(), shiftImg
}

function guessDoodle() {
    
    // doodle is createGraphics not createImage
    let img = doodle.get();

    img.resize(PIXELS, PIXELS);
    img.loadPixels();

    let sample = preProcessDoodleImage(img);
    
    // set up inputs
    let inputs = [];
    for (let i = 0; i < PIXELSSQUARED; i++){
        inputs[i] = sample.pixels[4 * i] / 255;
    }
    doodle_inputs = inputs;     // can inspect in console 
    let o = getCnnInputs(inputs);
    let prediction = find12(nn.predict(inputs));
    let cnnPrediction = find12(cnn_model.forward(o).w);
    thehtml = " Existing Neural net prediction: " + greenspan + prediction[0] + "</span>  No.2 guess is: " + greenspan + prediction[1] + "</span> <br> CNN prediction: " + greenspan + cnnPrediction[0] + "</span>  Second guess: " + greenspan + cnnPrediction[1] + "</span>";
    AB.msg(thehtml, 2);
}

function wipeDoodle()    
{
    doodle_exists = false;
    doodle.background('black');
}


// --- debugging --------------------------------------------------
// in console
// showInputs(demo_inputs);
// showInputs(doodle_inputs);

function showInputs(inputs, t) {
    // display inputs row by row, corresponding to square of pixels
    var str = "";
    for (let i = 0; i < inputs.length; i++) {
        if(i % t == 0){
            str += "\n";
            var value = inputs[i];
            str = str + " " + value.toFixed(2);
        }
    }
    console.log(str);
}