From 8c4497ffc9ca765a3aaf084d7c096239c89554f0 Mon Sep 17 00:00:00 2001 From: shiffman <daniel@shiffman.net> Date: Tue, 22 Oct 2019 16:12:27 -0400 Subject: [PATCH 1/3] only lowercase if not a URL --- src/ImageClassifier/index.js | 78 ++++++++++++++++++------------------ 1 file changed, 39 insertions(+), 39 deletions(-) diff --git a/src/ImageClassifier/index.js b/src/ImageClassifier/index.js index 1359ca68b..8cf509b74 100644 --- a/src/ImageClassifier/index.js +++ b/src/ImageClassifier/index.js @@ -76,46 +76,46 @@ class ImageClassifier { */ async loadModel(modelUrl) { if (modelUrl) this.model = await this.loadModelFrom(modelUrl); - else this.model = await this.modelToUse.load({version: this.version, alpha: this.alpha}); + else this.model = await this.modelToUse.load({ version: this.version, alpha: this.alpha }); return this; } async loadModelFrom(path = null) { fetch(path) - .then(r => r.json()) - .then((r) => { - if (r.ml5Specs) { - this.mapStringToIndex = r.ml5Specs.mapStringToIndex; - } - }) - // When loading model generated by Teachable Machine 2.0, the r.ml5Specs is missing, - // which is causing imageClassifier failing to display lables. - // In this case, labels are stored in path/./metadata.json - // Therefore, I'm fetching the metadata and feeding the labels into this.mapStringToIndex - // by Yang Yang, yy2473@nyu.edu, Oct 2, 2019 - .then(()=>{ - if (this.mapStringToIndex.length === 0) { - const split = path.split("/"); - const prefix = split.slice(0, split.length - 1).join("/"); - const metadataUrl = `${prefix}/metadata.json`; - fetch(metadataUrl) - .then((res) => { - if (!res.ok) { - console.log("Tried to fetch metadata.json, but it seems to be missing."); - throw Error(res.statusText); - } - return res; - }) - .then(metadataJson => metadataJson.json()) - .then((metadataJson)=> { - if (metadataJson.labels){ - this.mapStringToIndex = metadataJson.labels; - } - }) - .catch(() => console.log("Error when loading metadata.json")); - } - }); + .then(r => r.json()) + .then((r) => { + if (r.ml5Specs) { + this.mapStringToIndex = r.ml5Specs.mapStringToIndex; + } + }) + // When loading model generated by Teachable Machine 2.0, the r.ml5Specs is missing, + // which is causing imageClassifier failing to display lables. + // In this case, labels are stored in path/./metadata.json + // Therefore, I'm fetching the metadata and feeding the labels into this.mapStringToIndex + // by Yang Yang, yy2473@nyu.edu, Oct 2, 2019 + .then(() => { + if (this.mapStringToIndex.length === 0) { + const split = path.split("/"); + const prefix = split.slice(0, split.length - 1).join("/"); + const metadataUrl = `${prefix}/metadata.json`; + fetch(metadataUrl) + .then((res) => { + if (!res.ok) { + console.log("Tried to fetch metadata.json, but it seems to be missing."); + throw Error(res.statusText); + } + return res; + }) + .then(metadataJson => metadataJson.json()) + .then((metadataJson) => { + if (metadataJson.labels) { + this.mapStringToIndex = metadataJson.labels; + } + }) + .catch(() => console.log("Error when loading metadata.json")); + } + }); // end of the Oct 2, 2019 fix this.model = await tf.loadLayersModel(path); return this.model; @@ -206,7 +206,7 @@ class ImageClassifier { || inputNumOrCallback.elt instanceof HTMLCanvasElement || inputNumOrCallback.elt instanceof ImageData) ) { - imgToPredict = inputNumOrCallback.elt; // Handle p5.js image + imgToPredict = inputNumOrCallback.elt; // Handle p5.js image } else if (typeof inputNumOrCallback === 'object' && inputNumOrCallback.canvas instanceof HTMLCanvasElement) { imgToPredict = inputNumOrCallback.canvas; // Handle p5.js image } else if (!(this.video instanceof HTMLVideoElement)) { @@ -242,15 +242,15 @@ class ImageClassifier { } const imageClassifier = (modelName, videoOrOptionsOrCallback, optionsOrCallback, cb) => { - let model; let video; let options = {}; let callback = cb; - if (typeof modelName === 'string') { - model = modelName.toLowerCase(); - } else { + let model = modelName; + if (typeof model !== 'string') { throw new Error('Please specify a model to use. E.g: "MobileNet"'); + } else if (model.indexOf('http') === -1) { + model = modelName.toLowerCase(); } if (videoOrOptionsOrCallback instanceof HTMLVideoElement) { From b9535e78868f5c58cc3c8d6deca02e758dc9e868 Mon Sep 17 00:00:00 2001 From: shiffman <daniel@shiffman.net> Date: Tue, 22 Oct 2019 16:57:38 -0400 Subject: [PATCH 2/3] same fix for sound classifier --- src/SoundClassifier/index.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/SoundClassifier/index.js b/src/SoundClassifier/index.js index e9a0a5334..b42449f34 100644 --- a/src/SoundClassifier/index.js +++ b/src/SoundClassifier/index.js @@ -59,7 +59,7 @@ class SoundClassifier { /** * Classifies the audio from microphone and takes a callback to handle the results - * @param {function | number} numOrCallback - + * @param {function | number} numOrCallback - * takes any of the following params * @param {function} cb - a callback function that handles the results of the function. * @return {function} a promise or the results of a given callback, cb. @@ -82,14 +82,14 @@ class SoundClassifier { } const soundClassifier = (modelName, optionsOrCallback, cb) => { - let model; let options = {}; let callback = cb; - if (typeof modelName === 'string') { - model = modelName.toLowerCase(); - } else { + let model = modelName; + if (typeof model !== 'string') { throw new Error('Please specify a model to use. E.g: "SpeechCommands18w"'); + } else if (model.indexOf('http') === -1) { + model = modelName.toLowerCase(); } if (typeof optionsOrCallback === 'object') { From 36c0c428706c037da7ddd9721cbe59a53e32121a Mon Sep 17 00:00:00 2001 From: joeyklee <jsphknglee@gmail.com> Date: Wed, 23 Oct 2019 17:38:25 -0400 Subject: [PATCH 3/3] only call modelInstantiation with beforeAll() rather than beforeEach() --- CONTRIBUTING.md | 2 +- src/BodyPix/index_test.js | 2 +- src/CharRNN/index_test.js | 16 ++-- src/FaceApi/index_test.js | 13 +-- src/ImageClassifier/index_test.js | 150 +++++++++++++++++++----------- src/KMeans/index_test.js | 10 +- src/NeuralNetwork/index_test.js | 34 +++---- 7 files changed, 134 insertions(+), 93 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index df592d8d5..ea8fd1b3c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -249,7 +249,7 @@ We’re still rolling out all of our unit tests, but if you want to contribute t ```npm run test:single``` - To run a test on a single model - ```npm run test -- --model:YourModelNameHere``` + ```npm run test -- --model=YourModelNameHere``` This last one is case sensitive! diff --git a/src/BodyPix/index_test.js b/src/BodyPix/index_test.js index b1c767a31..bc4f6e29f 100644 --- a/src/BodyPix/index_test.js +++ b/src/BodyPix/index_test.js @@ -47,7 +47,7 @@ describe('bodyPix', () => { return img; } - beforeEach(async () => { + beforeAll(async () => { jasmine.DEFAULT_TIMEOUT_INTERVAL = 5000; bp = await bodyPix(); }); diff --git a/src/CharRNN/index_test.js b/src/CharRNN/index_test.js index 573c64e2c..5f9dde22e 100644 --- a/src/CharRNN/index_test.js +++ b/src/CharRNN/index_test.js @@ -33,20 +33,20 @@ describe('charRnn', () => { rnn = await charRNN(RNN_MODEL_URL, undefined); }); - it('instantiates an rnn with all the defaults', async () => { - expect(rnn.ready).toBeTruthy(); - expect(rnn.defaults.seed).toBe(RNN_DEFAULTS.seed); - expect(rnn.defaults.length).toBe(RNN_DEFAULTS.length); - expect(rnn.defaults.temperature).toBe(RNN_DEFAULTS.temperature); - expect(rnn.defaults.stateful).toBe(RNN_DEFAULTS.stateful); - }); - // it('loads the model with all the defaults', async () => { // expect(rnn.cellsAmount).toBe(RNN_MODEL_DEFAULTS.cellsAmount); // expect(rnn.vocabSize).toBe(RNN_MODEL_DEFAULTS.vocabSize); // }); describe('generate', () => { + it('instantiates an rnn with all the defaults', async () => { + expect(rnn.ready).toBeTruthy(); + expect(rnn.defaults.seed).toBe(RNN_DEFAULTS.seed); + expect(rnn.defaults.length).toBe(RNN_DEFAULTS.length); + expect(rnn.defaults.temperature).toBe(RNN_DEFAULTS.temperature); + expect(rnn.defaults.stateful).toBe(RNN_DEFAULTS.stateful); + }); + it('Should generate content that follows default options if given an empty object', async() => { const result = await rnn.generate({}); expect(result.sample.length).toBe(20); diff --git a/src/FaceApi/index_test.js b/src/FaceApi/index_test.js index 35b965733..5dfa6fe25 100644 --- a/src/FaceApi/index_test.js +++ b/src/FaceApi/index_test.js @@ -41,17 +41,18 @@ describe('faceApi', () => { // return canvas; // } - beforeEach(async () => { + beforeAll(async () => { jasmine.DEFAULT_TIMEOUT_INTERVAL = 15000; faceapi = await faceApi(); }); - - it('Should create faceApi with all the defaults', async () => { - expect(faceapi.config.withLandmarks).toBe(FACEAPI_DEFAULTS.withLandmarks); - expect(faceapi.config.withDescriptors).toBe(FACEAPI_DEFAULTS.withDescriptors); - }); describe('landmarks', () => { + + it('Should create faceApi with all the defaults', async () => { + expect(faceapi.config.withLandmarks).toBe(FACEAPI_DEFAULTS.withLandmarks); + expect(faceapi.config.withDescriptors).toBe(FACEAPI_DEFAULTS.withDescriptors); + }); + it('Should get landmarks for Frida', async () => { const img = await getImage(); await faceapi.detectSingle(img) diff --git a/src/ImageClassifier/index_test.js b/src/ImageClassifier/index_test.js index 31b0b4bdb..617f6478e 100644 --- a/src/ImageClassifier/index_test.js +++ b/src/ImageClassifier/index_test.js @@ -3,7 +3,11 @@ // This software is released under the MIT License. // https://opensource.org/licenses/MIT -const { imageClassifier } = ml5; +const { + imageClassifier +} = ml5; + +const TM_URL = 'https://storage.googleapis.com/tm-models/WfgKPytY/model.json'; const DEFAULTS = { learningRate: 0.0001, @@ -16,67 +20,109 @@ const DEFAULTS = { version: 2, }; +async function getImage() { + const img = new Image(); + img.crossOrigin = true; + img.src = 'https://cdn.jsdelivr.net/gh/ml5js/ml5-library@development/assets/bird.jpg'; + await new Promise((resolve) => { + img.onload = resolve; + }); + return img; +} + +async function getCanvas() { + const img = await getImage(); + const canvas = document.createElement('canvas'); + canvas.width = img.width; + canvas.height = img.height; + canvas.getContext('2d').drawImage(img, 0, 0); + return canvas; +} + describe('imageClassifier', () => { let classifier; - async function getImage() { - const img = new Image(); - img.crossOrigin = true; - img.src = 'https://cdn.jsdelivr.net/gh/ml5js/ml5-library@development/assets/bird.jpg'; - await new Promise((resolve) => { img.onload = resolve; }); - return img; - } - - async function getCanvas() { - const img = await getImage(); - const canvas = document.createElement('canvas'); - canvas.width = img.width; - canvas.height = img.height; - canvas.getContext('2d').drawImage(img, 0, 0); - return canvas; - } - - beforeEach(async () => { - jasmine.DEFAULT_TIMEOUT_INTERVAL = 15000; - classifier = await imageClassifier('MobileNet', undefined, {}); - }); + /** + * Test imageClassifier with teachable machine + */ + // Teachable machine model + describe('with Teachable Machine model', () => { + + beforeAll(async () => { + jasmine.DEFAULT_TIMEOUT_INTERVAL = 15000; + classifier = await imageClassifier(TM_URL, undefined, {}); + }); + + describe('instantiate', () => { + it('Should create a classifier with all the defaults', async () => { + console.log(classifier) + expect(classifier.modelUrl).toBe(TM_URL); + }); + }); - it('Should create a classifier with all the defaults', async () => { - expect(classifier.version).toBe(DEFAULTS.version); - expect(classifier.alpha).toBe(DEFAULTS.alpha); - expect(classifier.topk).toBe(DEFAULTS.topk); - expect(classifier.ready).toBeTruthy(); }); - describe('classify', () => { - it('Should classify an image of a Robin', async () => { - const img = await getImage(); - await classifier.classify(img) - .then(results => expect(results[0].label).toBe('robin, American robin, Turdus migratorius')); - }); - it('Should support p5 elements with an image on .elt', async () => { - const img = await getImage(); - await classifier.classify({ elt: img }) - .then(results => expect(results[0].label).toBe('robin, American robin, Turdus migratorius')); - }); - it('Should support HTMLCanvasElement', async () => { - const canvas = await getCanvas(); - await classifier.classify(canvas) - .then(results => expect(results[0].label).toBe('robin, American robin, Turdus migratorius')); - }); + /** + * Test imageClassifier with Mobilenet + */ + describe('imageClassifier with Mobilenet', () => { - it('Should support p5 elements with canvas on .canvas', async () => { - const canvas = await getCanvas(); - await classifier.classify({ canvas }) - .then(results => expect(results[0].label).toBe('robin, American robin, Turdus migratorius')); + beforeAll(async () => { + jasmine.DEFAULT_TIMEOUT_INTERVAL = 15000; + classifier = await imageClassifier('MobileNet', undefined, {}); }); - it('Should support p5 elements with canvas on .elt', async () => { - const canvas = await getCanvas(); - await classifier.classify({ elt: canvas }) - .then(results => expect(results[0].label).toBe('robin, American robin, Turdus migratorius')); + describe('instantiate', () => { + + it('Should create a classifier with all the defaults', async () => { + expect(classifier.version).toBe(DEFAULTS.version); + expect(classifier.alpha).toBe(DEFAULTS.alpha); + expect(classifier.topk).toBe(DEFAULTS.topk); + expect(classifier.ready).toBeTruthy(); + }); + }) + + describe('classify', () => { + + it('Should classify an image of a Robin', async () => { + const img = await getImage(); + await classifier.classify(img) + .then(results => expect(results[0].label).toBe('robin, American robin, Turdus migratorius')); + }); + + it('Should support p5 elements with an image on .elt', async () => { + const img = await getImage(); + await classifier.classify({ + elt: img + }) + .then(results => expect(results[0].label).toBe('robin, American robin, Turdus migratorius')); + }); + + it('Should support HTMLCanvasElement', async () => { + const canvas = await getCanvas(); + await classifier.classify(canvas) + .then(results => expect(results[0].label).toBe('robin, American robin, Turdus migratorius')); + }); + + it('Should support p5 elements with canvas on .canvas', async () => { + const canvas = await getCanvas(); + await classifier.classify({ + canvas + }) + .then(results => expect(results[0].label).toBe('robin, American robin, Turdus migratorius')); + }); + + it('Should support p5 elements with canvas on .elt', async () => { + const canvas = await getCanvas(); + await classifier.classify({ + elt: canvas + }) + .then(results => expect(results[0].label).toBe('robin, American robin, Turdus migratorius')); + }); }); + }); -}); + +}) \ No newline at end of file diff --git a/src/KMeans/index_test.js b/src/KMeans/index_test.js index c90cb960c..a14bdecac 100644 --- a/src/KMeans/index_test.js +++ b/src/KMeans/index_test.js @@ -16,7 +16,8 @@ const KMEANS_DEFAULTS = { describe('kMeans', () => { let kmeansModel; const dataurl = 'https://raw.githubusercontent.com/ml5js/ml5-examples/development/d3/KMeans/KMeans_GaussianClusterDemo/data/gaussian2d_2clusters.csv' - beforeEach(async () => { + + beforeAll(async () => { jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000; kmeansModel = await kmeans(dataurl, KMEANS_DEFAULTS, (err, result) => { return; @@ -43,11 +44,4 @@ describe('kMeans', () => { expect(unique).toBe(2); }); - - - - - - - }); \ No newline at end of file diff --git a/src/NeuralNetwork/index_test.js b/src/NeuralNetwork/index_test.js index fdb079270..ca09f47ce 100644 --- a/src/NeuralNetwork/index_test.js +++ b/src/NeuralNetwork/index_test.js @@ -9,27 +9,27 @@ const { } = ml5; const NN_DEFAULTS = { - task: 'regression', - activationHidden: 'sigmoid', - activationOutput: 'sigmoid', - debug: false, - learningRate: 0.25, - inputs: 2, - outputs: 1, - noVal: null, - hiddenUnits: 16, - modelMetrics: ['accuracy'], - modelLoss: 'meanSquaredError', - modelOptimizer: null, - batchSize: 64, - epochs: 32, + task: 'regression', + activationHidden: 'sigmoid', + activationOutput: 'sigmoid', + debug: false, + learningRate: 0.25, + inputs: 2, + outputs: 1, + noVal: null, + hiddenUnits: 16, + modelMetrics: ['accuracy'], + modelLoss: 'meanSquaredError', + modelOptimizer: null, + batchSize: 64, + epochs: 32, } describe('neuralNetwork', () => { let nn; - beforeEach(async () => { + beforeAll(async () => { jasmine.DEFAULT_TIMEOUT_INTERVAL = 15000; nn = await neuralNetwork(); }); @@ -50,7 +50,7 @@ describe('neuralNetwork', () => { // expect(nn.config.training.modelMetrics).toBe(NN_DEFAULTS.modelMetrics); expect(nn.config.training.modelLoss).toBe(NN_DEFAULTS.modelLoss); // expect(nn.config.training.modelOptimizer).toBe(); - + // data defaults // expect(nn.config.dataOptions.dataUrl).toBe(); // expect(nn.config.dataOptions.inputs).toBe(NN_DEFAULTS.inputs); @@ -60,4 +60,4 @@ describe('neuralNetwork', () => { }); -}); +}); \ No newline at end of file