Code viewer for World: Preprocessing to improve c...

// Cloned by Philip on 4 Dec 2020 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;



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

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

const learningrate = 0.1;   // default 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 
const DOODLE_BLUR = 3;      // blur factor applied to doodles 


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;


// Matrix.randomize() is changed to point to this. Must be defined by user of Matrix. 

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



// CSS trick 
// make run header bigger 
$("#runheaderbox").css({ "max-height": "95vh" });



let htmlPositions = [0, 1, 3, 4, 5, 6, 7, 8, 9, 10, 2, 11, 12, 13]; //Philip : A little bit of an awkward hack but lets easy rearrangement



//--- 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> " +
    validateDoodleHtml();

AB.msg(thehtml, htmlPositions[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, htmlPositions[3]);

// 4 variable training data 

// 5 Testing header
thehtml = "<h3> Hidden tests </h3> ";
AB.msg(thehtml, htmlPositions[5]);

// 6 variable testing data 

// 7 Demo header 
thehtml = "<hr> <h1> 3. Demo </h1>The Demos begin to run automatically at 80% accuracy unless turned off! 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, htmlPositions[7]);

thehtml = "<hr> <h1> 4. Test against Stored Doodles </h1> <br>" +
    " <button onclick='testSavedDoodles();' class='normbutton' >This became pointless as I began to modify different things such as the filters and how the images were preprocessed</button> <br>";
AB.msg(thehtml, htmlPositions[12]);

// 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() {
    startTime = Math.round((new Date().getTime() / 1000));
    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);
                //Why doesn't this work? Is everything negative on the otherside and being wiped out instantly
                let rectifierFunction = new ActivationFunction(
                    x => x > 0 ? x : 0, 
                    y => y > 0 ? 1 : 0
                    );
              //  nn.setActivationFunction(rectifierFunction);
                nn.setLearningRate(learningrate);
                
                loadData();
            });
        });
    });
}


// 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] = bright; //Why is this 255? Set it to bright instead
    }

    theimage.updatePixels();
    return theimage;
}


function getInputs(img)      // convert img array into normalised input array 
{
    let inputs = [];
    for (let i = 0; i < PIXELSSQUARED; i++) {
        let bright = img[i];
        inputs[i] = bright / 255;       // normalise to 0 to 1 

        //Philip : Test 1: trim anything below a certain brightness
        //if(bright < 250){
        //     inputs[i] = 0;
        //  }
        // else {
        //     inputs[i] = 1;
        // }
    }
    return (inputs);
}



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];
    let label = mnist.train_labels[train_index];

    // optional - show visual of the image 
    if (show) {
        var theimage = getImage(img);    // get image from data array 
       // var theimage = getImage(arrayManipulations(img, false, true));  //Philip: This shows what actually gets drawn after the manipulations but as some of these are extremely unoptimised time can be saved by turning it off
        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 

    // 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 
    nn.train(arrayManipulations(inputs, false, true), targets);

    thehtml = " trainrun: " + trainrun + "<br> no: " + train_index + "<br> Time Passed: " + runTime() + " seconds";
    AB.msg(thehtml, htmlPositions[4]);

    train_index++;
    if (train_index == NOTRAIN) {
        train_index = 0;
        console.log("finished trainrun: " + trainrun);
        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 prediction = nn.predict(arrayManipulations(inputs, false, true));       // array of outputs 
    let guess = findMax(prediction);      // the top output 

    total_tests++;
    if (guess == label) total_correct++;

    let percent = (total_correct / total_tests) * 100;
    currentPercent = percent;


    calculateLandmarkTimes(percent);
    thehtml = " testrun: " + testrun + "<br> no: " + total_tests + " <br> " +
        " correct: " + total_correct + "<br>" +
        "  score: " + greenspan + percent.toFixed(2) + "</span>" + " <br> " +
        " Time to 10%: " + timeToArray[0] + " <br> " +
        " Time to 20%: " + timeToArray[1] + " <br> " +
        " Time to 30%: " + timeToArray[2] + " <br> " +
        " Time to 40%: " + timeToArray[3] + " <br> " +
        " Time to 50%: " + timeToArray[4] + " <br> " +
        " Time to 60%: " + timeToArray[5] + " <br> " +
        " Time to 70%: " + timeToArray[6] + " <br> " +
        " Time to 80%: " + timeToArray[7] + " <br> " +
        " Time to 85%: " + timeToArray[8] + " <br> " +
        " Time to 90%: " + timeToArray[9] + " <br> " +
        " Time to 100%: " + timeToArray[10];

    AB.msg(thehtml, htmlPositions[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;
    }
}




//--- 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) {
            no1 = i;
            no1value = a[i];
        }
        else if (a[i] > no2value) {
            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() {
    reportDoodleAccuracy();

    // 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 

    //Philip: Going to change it so the demo starts running automatically. Again as another metric of how good/bad any changes made are.
    //85% was chosen as the start time for this as after this the improvements slow down significantly

    if (demo_exists) {
        drawDemo();
        guessDemo();
    }
    if (currentPercent > demoThreshold) {

        makeDemoBlind();
    }
    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) {
            rawDoodle = getRawDoodleInputs(doodle); // Philip@ the raw doodle will get saved now instead of processed one so all doodles are shareable across the different filters for quick testing
            mousedrag = false;
            // console.log ("Exiting draw. Now blurring.");
            applyDoodleFilters();
            //   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];
    demoLabel = mnist.test_labels[i];

    thehtml = "Test image no: " + i + "<br>" +
        "Classification: " + label + "<br>";
    AB.msg(thehtml, htmlPositions[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);

    demo_inputs = inputs;  // can inspect in console 

    let prediction = nn.predict(inputs);       // array of outputs 
    let guess = findMax(prediction);      // the top output 

    thehtml = " We classify it as: " + greenspan + guess + "</span>";
    AB.msg(thehtml, htmlPositions[9]);
}




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

function drawDoodle() {
    // doodle is createGraphics not createImage
    let theimage = doodle.get();
    theimage.loadPixels();
    // console.log (theimage);
    drawFromImage(theimage);
}

function drawFromImage(theimage){
        // doodle is createGraphics not createImage
    // console.log (theimage);

    image(theimage, 0, 0, ZOOMPIXELS, ZOOMPIXELS);      // original 
    image(theimage, ZOOMPIXELS + 50, 0, PIXELS, PIXELS);      // shrunk
   
}



function guessDoodle() {
    // doodle is createGraphics not createImage
    getDoodleInputs(doodle)

    // feed forward to make prediction 
    let prediction = nn.predict(arrayManipulations(doodle_inputs, true, false));       // array of outputs 
    let b = find12(prediction);       // get no.1 and no.2 guesses  
    doodleGuess = b[0];

    thehtml = " We classify it as: " + greenspan + b[0] + "</span> <br>" +
        " No.2 guess is: " + greenspan + b[1] + "</span>";

    AB.msg(thehtml, htmlPositions[2]);
}


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




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


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


//|********************************************|//
//|****************New stuff*****************|//
//|********************************************|//

//Philip: New Variables
let startTime = 0;
//Some performance metrics, want to figure out how long it took to train to a percent. Will give a quick indicator if training is broken
let timeToArray = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0,0]
let doodleAccuracy = 0;
let numberOfDoodles = 0;
let correctDoodles = 0;
let incorrectDoodles = 0;
let currentPercent = 0;
let doodleGuess = null;
let demoLabel = null;
let numberOfDemos = 0;
let numberOfCorrectDemoGuesses = 0;
let demoAccuracy = 0;
let demoThreshold = 80;
let rawDoodle = [];//Modify this to change the percent that demos kick in at
//Html positions so I can add new stuff more easily without breaking everything

//These need to be changed as the drawing changes as the doodles won't representative
let localStoragePrefix = "doodle_"; //For the base program with no drawing changes

function runTime() {
    return Math.round((new Date().getTime() / 1000)) - startTime;
}

function calculateLandmarkTimes(percent) {
    updateTimeArray(0, 10, percent);
    updateTimeArray(1, 20, percent);
    updateTimeArray(2, 30, percent);
    updateTimeArray(3, 40, percent);
    updateTimeArray(4, 50, percent);
    updateTimeArray(5, 60, percent);
    updateTimeArray(6, 70, percent);
    updateTimeArray(7, 80, percent);
    updateTimeArray(8, 85, percent);
    updateTimeArray(9, 90, percent);
    updateTimeArray(10, 100, percent);
}

function updateTimeArray(i, landmark, percent) {
    if (timeToArray[i] == 0 && percent > landmark) {
        timeToArray[i] = runTime();
    }
}

//Just use some lazy metric gathering, its just to make polling easier and save some effort rather than be accurate

//Rework this a little to capture the doodle before it has filters applied
function validateDoodle(i) {
    numberOfDoodles = numberOfDoodles + 1;

    if (i == doodleGuess)
        correctDoodles = correctDoodles + 1;
    else
        incorrectDoodles = incorrectDoodles + 1;
    getDoodleInputs(doodle)
    
  //  addDoodleToHistory( {"classification" :  i, "doodle": rawDoodle});
    //save the doodle to localStorage The doodle is a bit too big to store at normal size so we'll reduce it down first, but no filters have been applied
    //Doesn't really work
   // localStorage.setItem(localStoragePrefix + (new Date().getTime()), JSON.stringify({ "classification": i, "doodle": convertRawDoodleToPixelsSquared(rawDoodle) })); //this will be a bit messed up for 2 strokes so try to avoid them
    wipeDoodle();
}

function calculateDoodleAccuracy() {
    doodleAccuracy = (correctDoodles / numberOfDoodles) * 100;
}

function calculateDemoAccuracy(guess) {
    if (numberOfDemos > 0) {
        if (demoLabel == guess)
            numberOfCorrectDemoGuesses = numberOfCorrectDemoGuesses + 1;

        demoAccuracy = (numberOfCorrectDemoGuesses / numberOfDemos) * 100;
    }
    var html = "<br> Score :  " + demoAccuracy + "<br>";

    AB.msg(html, htmlPositions[11]);
}


function reportDoodleAccuracy() {
    if (numberOfDoodles > 0) {
        calculateDoodleAccuracy();
        var html = "<br> Score :  " + doodleAccuracy + "<br>";
        AB.msg(html, htmlPositions[10]);
    }
}

function validateDoodleHtml() {
    return " Validate: <br> <button onclick='validateDoodle(0);' class='normbutton' >0</button>  <br> " +
        "<button onclick='validateDoodle(1);' class='normbutton' >1</button>  <br> " +
        "<button onclick='validateDoodle(2);' class='normbutton' >2</button>  <br> " +
        "<button onclick='validateDoodle(3);' class='normbutton' >3</button>  <br> " +
        "<button onclick='validateDoodle(4);' class='normbutton' >4</button>  <br> " +
        "<button onclick='validateDoodle(5);' class='normbutton' >5</button>  <br> " +
        "<button onclick='validateDoodle(6);' class='normbutton' >6</button>  <br> " +
        "<button onclick='validateDoodle(7);' class='normbutton' >7</button>  <br> " +
        "<button onclick='validateDoodle(8);' class='normbutton' >8</button>  <br> " +
        "<button onclick='validateDoodle(9);' class='normbutton' >9</button>  <br> ";
}

function makeDemoBlind() {
    numberOfDemos = numberOfDemos + 1;
    var i = AB.randomIntAtoB(0, NOTEST - 1);

    demo = mnist.test_images[i];
    demoLabel = mnist.test_labels[i];

    let inputs = getInputs(demo);

    demo_inputs = inputs;  // can inspect in console 

    let prediction = nn.predict(arrayManipulations(inputs, false, true));       // array of outputs 
    let guess = findMax(prediction);      // the top output 
    calculateDemoAccuracy(guess);
}


function getDoodleInputs(dood) {
    let img = dood.get();

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

    // set up inputs   
    let inputs = [];
    for (let i = 0; i < PIXELSSQUARED; i++) {
        inputs[i] = img.pixels[i * 4] / 255;
    }

    doodle_inputs = inputs;     // can inspect in console 

    return inputs;
}


//Im not trusting this anymore, the initial attempt of just an array worked but I need to pass it through filters and I've had no success with that so for now I'm ignoring it and will rely on the counter.
function testSavedDoodles() {
    var savedDoodles = 0;
    var savedDoodlesCorrectGuesses = 0;
    AB.restoreData ( function ( data ) {
    for (var i = 0; i < data.length; i++) {
            var currentDoodle = data[i];

            savedDoodles = savedDoodles + 1;

            var doodleAsPixelsSquared = currentDoodle.doodle;;
            //Test 1
            //Going to remove everything above a certain brightness
            var asImage = getDoodleAsImage(doodleAsPixelsSquared);
           // drawFromImage(asImage)
            applyFilters(asImage);
            asImage.resize(PIXELS,PIXELS);
            asImage.loadPixels();
            
            var maxMinInput = [];
            for (var j = 0; j < asImage.pixels.length; j=j+4) {
                maxMinInput.push(asImage.pixels[j]/255)
                //if (doodleAsPixelsSquared[j] > 128)
                  //  maxMinInput.push(1);
                //else
                  //  maxMinInput.push(0);
            }


            let prediction = nn.predict(maxMinInput);       //Test 1 
            //  let prediction    = nn.predict(currentDoodle.doodle);       // array of outputs 
            let b = find12(prediction);       // get no.1 and no.2 guesses  

            if (b[0] == currentDoodle.classification) {
                savedDoodlesCorrectGuesses = savedDoodlesCorrectGuesses + 1;
            }
        
    }
    var html = " <br> Score : " + ((savedDoodlesCorrectGuesses / savedDoodles) * 100).toFixed(2) +
        " <br> Saved Doodles : " + savedDoodles
    " <br> Correct Guesses : " + savedDoodlesCorrectGuesses;

    AB.msg(html, htmlPositions[13]);
    });
};

//Doodle is a larger canvas, and their are actually 4 times more pixels than it says so in the pixel array because it has the other channels
function getRawDoodleInputs(dood) {
    let img = dood.get();
    img.loadPixels();
    
    let inputs = [];
    for (let i = 0; i < ZOOMPIXELS * ZOOMPIXELS; i++) {
        inputs[i] = img.pixels[i * 4];
    }

    return inputs;
}



function convertRawDoodleToPixelsSquared(rawInputs) {
    
    // set up inputs   
    let inputs = [];
    for (let i = 0; i < (ZOOMPIXELS * ZOOMPIXELS); i++) {
        inputs[i] = rawInputs[i * 4]; // lets not save the normalised data, we cna do that later
    }
    return inputs;
}


function getDoodleAsImage(doodleArr)
{
    let theimage = createImage(ZOOMPIXELS, ZOOMPIXELS);    // make blank image, then populate it 
    theimage.loadPixels();
    
    for(var i = 0; i< doodleArr.length; i++){
        theimage.pixels[i] = doodleArr[i];
        theimage.pixels[i+1] = doodleArr[i];
        theimage.pixels[i+2] = doodleArr[i];
        theimage.pixels[i+3] = doodleArr[i];
    }
    theimage.updatePixels();
    return theimage;
}


function addDoodleToHistory(obj){
    AB.runloggedin;
    let previousData;
    AB.queryDataExists (function ( exists )                // asynchronous - need callback function 
    {
                                if ( exists ) {
                                    AB.restoreData ( function ( data ) { 
                                        data.push(obj);
                                        AB.saveData ( data );  
                                    } ); 
                                } else {
                                    AB.saveData([obj]);
                                }
    });
}

//Lets make a funciton that will remove any "floating pixels", one where all the neighbours are 0
//I got a 20% improvement for doodle recognition by using this on the trianing images before passing them to the network
function removeRandomPixels(arr){
    
    for(var i=0; i < arr.length; i++){
        if(i - 1 > 0)
        if(arr[i- 1] != 0) //To left
            continue;
        
        if(i + 1 < arr.length) //To right
        if(arr[i + 1] != 0)
        continue;
        
        if(i + 28 < arr.length) //below
        if(arr[i + 28] != 0)
        continue;
        
        if(i - 28 > 0) //above
        if(arr[i- 28] != 0)
        continue;
        
        arr[i] = 0;
    }
    return arr;
}

function fillHoles(arr){
    
    for(var i=0; i < arr.length; i++){
        if(i - 1 > 0)
        if(arr[i- 1] != 0) //To left
            continue;
        
        if(i + 1 < arr.length) //To right
        if(arr[i + 1] != 0)
        continue;
        
        if(i + 28 < arr.length) //below
        if(arr[i + 28] != 0)
        continue;
        
        if(i - 28 > 0) //above
        if(arr[i- 28] != 0)
        continue;
        
        arr[i] = (arr[i-1] + arr[i+1] + arr[i+28] + arr[i-28])/4;
    }
    return arr;
}

function thickenLines(arr){
    
    for(var i=0; i < arr.length; i++){
        if(!arr[i]) //dont add values around is that have value
            continue;
        
        if(i - 1 > 0 && i + 1 < arr.length) //check if its inbounds
            if(arr[i - 1] == 0 && arr[i + 1] == 0) //To left
                arr[i-1]= arr[i+1] = arr[i];
        
       
        
        if(i + 28 < arr.length && i - 28 > 0) //below
            if(arr[i + 28] == 0 && arr[i- 28] == 0)
              arr[i-28]= arr[i+28] = arr[i];
    }
    
    return arr;
}

function shiftToTopLeft(arr){
    //If this works, leave it be, if it doesn't map to an array of arrays while moving it, it'll be easier to visual and debug.
    //Again, into 28s, looking for the first non-zero value, the row this on then needs to be moved up by 28 until this value is < 28
    //Easy, Find first value, subtract 28x where x is the number of 28s into it
    var lastValuedIndex = 0;
    for(var i = 0; i < arr.length; i++){
        if(arr[i] !=0){
            firstValuedIndex = i;
            break;
        }
    }
    
    var indexesToShift = Math.floor(firstValuedIndex/28);
    if(indexesToShift > 0){
    arr = arr.slice(indexesToShift * 28, arr.length);
    for(var i = 0; i < indexesToShift * 28; i++) {
    arr.push(0);
    }
    }
    //Now shifting to the left
    //Actually need to look at every 28th cell and find the first one
    var firstLeftestValuedIndex = 0;
    var firstValuedVerticalIndex = 999;
    for(var i = 0; i < PIXELS; i++){
        for(var j = 0; j < PIXELS; j++){
            if(arr[i + j * PIXELS] !=0 && i < firstValuedVerticalIndex) {//most leftest spot
            firstLeftestValuedIndex = (i + j * PIXELS);
            firstValuedVerticalIndex = i;
            }
        }
    }
    
    if(firstLeftestValuedIndex > 0 && firstValuedVerticalIndex < 999){
       for(var i = 0; i < PIXELS; i++){
        for(var j = 0; j < PIXELS; j++){
          if(j + firstValuedVerticalIndex > PIXELS-1) {//End of column, don't start shifting from next one
             arr[(i*PIXELS) + j - firstValuedVerticalIndex] = arr[(i*PIXELS) + j];
             arr[(i*PIXELS) + j] = 0;
          } else 
            if(j - firstValuedVerticalIndex >= 0)
             arr[(i*PIXELS) + j - firstValuedVerticalIndex] = arr[(i*PIXELS) + j];
            }
        }
    }
    
    return arr;
}

function addNoise(arr){
    
    for(var i = 0; i < arr.length; i++){
        if(arr[i] ==0){
            arr[i] = ( AB.randomPick ( 0,		Math.round(Math.random() * 255) 	));
        }
    }
    
    return arr;
}


//This isn't very good, it has a HUGE HUGE HUGE impact on performance, I know this could be done better but don't have time to optimise it and couldn't find something that does it effectively
function rotateImage(arr, angle) { 
    var rotatedArr = [];    
   var firstIndex = 999;
    for(var i = 0; i < PIXELS; i++){
        for (var j = 0; j< PIXELS; j++){
           ir = i*cos(angle) - j*sin(angle)
            jr = i*sin(angle) + j*cos(angle)
            //still want nice round numbers
            ir = Math.round(ir) + 100; //arbritrarily making the rotated array bigger to avoid negative indexes
            jr = Math.round(jr) + 100;
            
            if(ir < firstIndex)
                firstIndex = ir;
                
            if(rotatedArr[ir])
                rotatedArr[ir][jr] = arr[(i* PIXELS)+j] + (rotatedArr[ir][jr] ? rotatedArr[ir][jr] : 0);
            else {
                rotatedArr[ir] = [];
                rotatedArr[ir][jr] = arr[i* PIXELS+j] +(0);
            }
        }
    }
    
    //Shift the array back to 0th index
    var rotateLength = rotatedArr.length - firstIndex;
    
    for(var i = 0; i < rotateLength; i++){
        rotatedArr[i] = rotatedArr[firstIndex + i];
    }
    
    rotatedArr = rotatedArr.slice(0, rotateLength);
    
    
    //Now I need to solve the problem of the shortest line, or rather lef most and right most points
    var longestLine = 0;
    var shortestLine = 999;
    for(var i = 0; i < rotatedArr.length; i++){
        if(rotatedArr[i].length > longestLine)
            longestLine = rotatedArr[i].length;
    } 
    
    for(var i = 0; i < rotatedArr.length; i++){
        for(var j = longestLine-1; j > 0; j--){
          if(rotatedArr[i][j] !== undefined && j < shortestLine)
             shortestLine = j;   
        }
    }
    
    //Now I can fill in the empty space with 0s
    for(var i = 0; i < rotatedArr.length; i++){
        for(var j = shortestLine; j < longestLine; j++){
            if(!rotatedArr[i][j])
                rotatedArr[i][j] = 0;
        }
    }
    
    //Lets now shift each line to 0
    var width = longestLine -shortestLine;
    var widthDiff = width - PIXELS;
    for(var i =0; i < rotatedArr.length; i++){
        for(var j = 0; j < width; j++){
            rotatedArr[i][j] = rotatedArr[i][j+shortestLine];
        }
        rotatedArr[i] = rotatedArr[i].slice(Math.floor(widthDiff/2), longestLine - Math.floor(widthDiff/2)); 
        if(rotatedArr[i].length > PIXELS){
            rotatedArr[i] = rotatedArr[i].slice(0, PIXELS); 
        }
        else if(rotatedArr[i].length < PIXELS){
            for(var j = PIXELS-1; j > 0; j++){
             if(!rotatedArr[i][j])
                rotatedArr[i][j] = 0;
            }
        }
    }
    
    var heightDiff = rotatedArr.length - PIXELS;
    rotatedArr = rotatedArr.slice(heightDiff/2, rotatedArr.length - heightDiff/2);
     if(rotatedArr.length < PIXELS){
            for(var i = 28; i > 0; j++){
             if(rotatedArr[i])
                break;
                
            var line =[
                0,0,0,0,0,0,0,
                0,0,0,0,0,0,0,
                0,0,0,0,0,0,0,
                0,0,0,0,0,0,0,
            ]; 
            
             rotatedArr[i] = line;
            }
        }
        
    //We should now have a matrix thats fully populated that is bigger than 28 * 28 unless the angle was a x* 90
    rotatedArr = rotatedArr.flat();
    //lets fill in the holes
    //simply if the next 3 pixels are blank, I do nothing if not I fill the gap hopefully this won't close intentional holes
    for(var i = 0; i< PIXELS; i++){
        for(var j=3; j < PIXELS - 3; j++)
        {
            if(rotatedArr[(i*PIXELS) + j] != 0){
            if(
               (rotatedArr[(i*PIXELS) + j+1] == 0 && rotatedArr[(i*PIXELS) + j + 2] ==0 && rotatedArr[(i*PIXELS) + j + 3] !=0)
            || (rotatedArr[(i*PIXELS) + j+1] == 0 && rotatedArr[(i*PIXELS) + j + 2] !=0)
            ){
                rotatedArr[(i*PIXELS) + j+1] = rotatedArr[(i*PIXELS) + j + 2] = (rotatedArr[(i*PIXELS) + j + 3] + rotatedArr[(i*PIXELS) +j])/2;
            }
            
              if(
               (rotatedArr[(i*PIXELS) + j-1] == 0 && rotatedArr[(i*PIXELS) + j - 2] ==0 && rotatedArr[(i*PIXELS) + j - 3] !=0)
            || (rotatedArr[(i*PIXELS) + j-1] == 0 && rotatedArr[(i*PIXELS) + j - 2] !=0)
            ){
                rotatedArr[(i*PIXELS) + j+1] = rotatedArr[(i*PIXELS) + j + 2] = (rotatedArr[(i*PIXELS) + j + 3] + rotatedArr[(i*PIXELS) +j])/2;
            }
            }
        }
    }
    //I've missed something as a white artificat is appearing down the side, is a single line not getting rotated? Maybe the middle line?
    return rotatedArr;
}

function applyDoodleFilters() {
  applyFilters(doodle);
}

function applyFilters(inputDoodle){
   // doodle.filter(INVERT)
    doodle.filter(BLUR, 3);    // just blur once 
   // doodle.filter(INVERT)
    //doodle.filter(ERODE);    // just blur once 
    //doodle.filter(THRESHOLD);    // just blur once 
   // doodle.filter(BLUR, 2);    // just blur once 
   // doodle.resize(PIXELS, PIXELS);
    //doodle.resize(ZOOMPIXELS, ZOOMPIXELS);
   // doodle.filter(POSTERIZE, 6);    // just blur once 
}

//Anywhere predict is called the array is first passed into this. Modify these methods to effect what happens to all of them
function arrayManipulations(arr, doodle, training){
    arr = Array.from(arr)
    
    if(training){
    //arr = rotateImage(arr, AB.randomPick(1,-1)*Math.random()/Math.PI); //I've noticed after this the image has loads of holes, so lets do the inverse of removeRandomPixels

    }
    
    arr = removeRandomPixels(arr);
    arr = shiftToTopLeft(arr);
    
    if(doodle){ //I'll put anything in here that I don't want on a certain set, like for example adding noise to the doodles
      // arr =  addNoise(arr)
    }
    
    if(training){
    //    thickenLines(arr);
    }
    
    return arr;
}