Oliver's Notes

Common Mistakes with ml5.js DoodleNet + p5.js

A guide to frequent beginner errors when using ml5.js’s DoodleNet model with p5.js, their symptoms, and how to fix them.

1. ml5.js Version and Application Programming Interface Mismatch

Many tutorials, including Coding Train videos, were written for ml5.js v0.x. Version 1.0, released in August 2024, changed the application programming interface (API) significantly. Code copied from older tutorials will silently fail or throw errors.

Key API differences

ConceptOld API (v0.x)New API (v1.0+)
Continuous classificationCall classify() recursively inside the callbackclassifyStart(canvas, gotResults), which handles the loop internally
Stop classifying(no built-in way)classifyStop()
Result shaperesults[i].label, results[i].confidenceSame, but returned as { label, confidence }
Constructorml5.imageClassifier('DoodleNet', callback)Same signature, but also supports await

Symptoms

Check your version

Look at your <script> tag:

<!-- v0.x (old) -->
<script src="https://unpkg.com/[email protected]/dist/ml5.min.js"></script>

<!-- v1.0+ (new) -->
<script src="https://unpkg.com/ml5@1/dist/ml5.min.js"></script>

Fix: Match your code to the version you’re loading. If following an older tutorial, either pin to the old version or translate the API calls.


2. Wrong Canvas Background Color

DoodleNet was trained on Google’s QuickDraw dataset, which consists of black strokes on a white background. The model expects this contrast.

Symptoms

Mistake

function setup() {
  createCanvas(280, 280);
  // The default background is gray, which is bad for DoodleNet.
}

Fix

function setup() {
  createCanvas(280, 280);
  background(255);  // White matches the training data.
}

3. Wrong strokeWeight

The QuickDraw training data uses thick, bold strokes, approximately 16 pixels wide. The default p5.js strokeWeight is 1, which produces lines far too thin for the model to reliably recognize.

Symptoms

Mistake

function setup() {
  createCanvas(280, 280);
  background(255);
  // The default strokeWeight of 1 is too thin.
}

Fix

function setup() {
  createCanvas(280, 280);
  background(255);
  strokeWeight(16);  // thick lines matching training data
  stroke(0);         // black ink
}

The exact value doesn’t need to be 16, but anything under ~8 will noticeably degrade accuracy.


4. Not Waiting for Model to Load

DoodleNet’s model weights are downloaded over the network when ml5.imageClassifier('DoodleNet') is called. This takes time. Attempting to classify before the model is ready causes errors.

Symptoms

Mistake

let classifier;

function setup() {
  createCanvas(280, 280);
  classifier = ml5.imageClassifier('DoodleNet');
  // model is still loading!
  classifier.classify(canvas, gotResults); // ERROR
}

Fix option A: callback

function setup() {
  createCanvas(280, 280);
  classifier = ml5.imageClassifier('DoodleNet', modelReady);
}

function modelReady() {
  console.log('DoodleNet loaded!');
  classifier.classify(canvas, gotResults);
}

Fix option B: preload() (p5.js 1.x only)

let classifier;

function preload() {
  // p5.js waits for preload() to finish before calling setup()
  classifier = ml5.imageClassifier('DoodleNet');
}

function setup() {
  createCanvas(280, 280);
  // classifier is guaranteed to be ready here
  classifier.classify(canvas, gotResults);
}

Fix option C: async/await (ml5 v1.0 without p5.js)

const classifier = await ml5.imageClassifier('DoodleNet');
// now safe to classify

Note: If you’re not using p5.js, constructors in ml5.js v1.0 require await. With p5.js, the preload() approach still works.


5. Recursive classify() Loop Done Wrong (v0.x)

In ml5.js v0.x, continuous classification requires you to manually create a loop by calling classify() again inside the results callback. Forgetting this step is extremely common.

Symptoms

Mistake

function gotResults(error, results) {
  if (error) {
    console.error(error);
    return;
  }
  label = results[0].label;
  confidence = results[0].confidence;
  // WRONG: classification happens once and stops
}

Fix (v0.x)

function gotResults(error, results) {
  if (error) {
    console.error(error);
    return;
  }
  label = results[0].label;
  confidence = results[0].confidence;
  // call classify again to keep the loop going
  classifier.classify(canvas, gotResults);
}

Fix (v1.0+)

Use classifyStart(), which handles the loop internally:

function modelReady() {
  classifier.classifyStart(canvas, gotResults);
}

function gotResults(results) {
  // note: v1.0 does not pass error as the first argument
  label = results[0].label;
  confidence = results[0].confidence;
  // classifyStart manages the loop, so do not call classify again.
}

6. Not Clearing Canvas Properly

Symptoms

Mistake: Using clear() instead of background(255)

function clearCanvas() {
  clear(); // makes the canvas transparent, not white!
}

Fix

function clearCanvas() {
  background(255); // reset to white, matching training data
}

Also: If you don’t provide a “clear” button or mechanism, users draw over previous doodles. The overlapping shapes confuse the classifier.


7. Passing the Wrong Element to classify()

The classify() method accepts several input types, but mixing them up causes errors.

Symptoms

Mistake: Passing a DOM element instead of a p5 canvas

// WRONG: document.getElementById returns a DOM element.
classifier.classify(document.getElementById('defaultCanvas0'), gotResults);

Mistake: Passing nothing

// WRONG
classifier.classify(gotResults); // missing the image/canvas argument

Fix for canvas drawing classification

In p5.js, the built-in canvas variable (available after createCanvas()) is the correct thing to pass:

classifier.classify(canvas, gotResults);

Fix for video/webcam classification

Pass the video element when constructing the classifier:

let video = createCapture(VIDEO);
classifier = ml5.imageClassifier('DoodleNet', video, modelReady);

DoodleNet on a webcam feed rarely makes sense. See mistake #8.


8. Using DoodleNet on Photos or Webcam

DoodleNet was trained exclusively on hand-drawn doodles (simple black line drawings on white backgrounds from Google QuickDraw). It is not a general-purpose image classifier.

Symptoms

Mistake

let video = createCapture(VIDEO);
// DoodleNet cannot meaningfully classify camera images
classifier = ml5.imageClassifier('DoodleNet', video, modelReady);

Fix

Use the right model for the job:

TaskModel
Classify photos or webcamMobileNet
Classify hand-drawn doodlesDoodleNet
Custom categoriesTrain your own with Teachable Machine

9. Not Handling Errors in the Callback

Mistake (v0.x)

function gotResults(error, results) {
  // WRONG: if error is not null, results is undefined
  label = results[0].label;
}

Fix (v0.x)

function gotResults(error, results) {
  if (error) {
    console.error(error);
    return;
  }
  label = results[0].label;
  confidence = results[0].confidence;
  classifier.classify(canvas, gotResults);
}

Symptoms

Note for v1.0

In ml5.js v1.0, the callback signature changed. Errors are no longer passed as the first argument. Instead, errors are thrown as exceptions or logged to the console. The callback receives only results:

function gotResults(results) {
  label = results[0].label;
  confidence = results[0].confidence;
}

Mixing up the callback signatures between versions is itself a common error: if you write function gotResults(error, results) with v1.0, what you think is error is actually results, and results is undefined.


10. TensorFlow.js Version Conflicts

ml5.js bundles its own compatible version of TensorFlow.js internally. Loading a separate TensorFlow.js <script> tag can cause version conflicts.

Symptoms

Mistake

<!-- DON'T load TensorFlow.js separately when using ml5.js -->
<script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs"></script>
<script src="https://unpkg.com/ml5@1/dist/ml5.min.js"></script>

Fix

Only load ml5.js because it includes TensorFlow.js:

<script src="https://unpkg.com/ml5@1/dist/ml5.min.js"></script>
<!-- that's all you need -->

11. Canvas Size Issues

DoodleNet internally resizes the input to a fixed size (28x28 or similar). But extreme canvas dimensions can cause unexpected behavior.

Symptoms

Mistake: Very large or very small canvas

createCanvas(50, 50);    // Very small canvases make recognizable shapes hard to draw.
createCanvas(1920, 1080); // Very large canvases hurt performance and make strokes look thin.

Fix

Use a moderate canvas size (200-400px). The official examples use 280x280:

createCanvas(280, 280);

If you need a larger drawing area, consider drawing on an off-screen graphics buffer at 280x280 and displaying a scaled-up version.


12. Drawing with mousePressed Instead of mouseDragged

Symptoms

Mistake

function mousePressed() {
  // This draws a single point per click instead of a continuous line.
  point(mouseX, mouseY);
}

Fix

function mouseDragged() {
  strokeWeight(16);
  line(pmouseX, pmouseY, mouseX, mouseY);
}

Using line(pmouseX, pmouseY, mouseX, mouseY) connects the previous mouse position to the current one, creating smooth strokes even when the mouse moves fast.


13. Calling classify() in draw() Without Throttling

Symptoms

Mistake

function draw() {
  // WRONG: classify runs 60 times per second and hammers the model.
  classifier.classify(canvas, gotResults);
}

Fix

Either use the callback-based loop (v0.x) or classifyStart() (v1.0), which internally manages timing. Don’t drive classification from draw():

// v1.0
function modelReady() {
  classifier.classifyStart(canvas, gotResults);
}

// In v0.x, the recursive callback pattern throttles itself because
// the next classify() only runs after the previous one finishes.

14. Forgetting noFill() or Using fill()

If fill() is active (it is by default in p5.js), shapes you draw will have a filled interior, which can interfere with doodle recognition.

Symptoms

Mistake

function mouseDragged() {
  // Ellipses have a white fill by default.
  // creates filled circles, not strokes
  ellipse(mouseX, mouseY, 16, 16);
}

Fix

function setup() {
  createCanvas(280, 280);
  background(255);
  stroke(0);
  strokeWeight(16);
  noFill();  // prevent filled shapes from interfering
}

15. CDN and Script Loading Order

Symptoms

Mistake: Loading ml5.js before p5.js

<!-- WRONG order -->
<script src="https://unpkg.com/ml5@1/dist/ml5.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/p5"></script>
<script src="sketch.js"></script>

Mistake: Typo or wrong CDN URL

<!-- WRONG: 'ml5js' is not the correct package name -->
<script src="https://unpkg.com/ml5js@1/dist/ml5.min.js"></script>

Fix

Load p5.js first, then ml5.js, then your sketch:

<script src="https://cdn.jsdelivr.net/npm/p5@1/lib/p5.min.js"></script>
<script src="https://unpkg.com/ml5@1/dist/ml5.min.js"></script>
<script src="sketch.js"></script>

Always check the browser’s Network tab to confirm all scripts loaded with status 200.


Quick Diagnostic Table

SymptomLikely Cause
Model never loads / page hangsWrong CDN URL, network issue, or script loading order
classify is not a functionModel not loaded yet, or v0.x/v1.0 API mismatch
Everything classified the sameWrong background color, wrong strokeWeight, or DoodleNet on photos
Low confidence on all drawingsThin strokes, wrong background, canvas too small or too large
Works once then stopsMissing recursive classify() call (v0.x), or need classifyStart() (v1.0)
results is undefined in callbackUsing v0.x callback signature (error, results) with v1.0 library
Tensor errors in consoleTensorFlow.js version conflict (extra <script> tag)
Browser is sluggishCalling classify() inside draw() without throttling
Drawings are dots, not linesUsing mousePressed instead of mouseDragged, or point() instead of line()
Canvas looks transparent after clearUsing clear() instead of background(255)
ml5 is not definedScript failed to load (404), wrong URL, or wrong loading order

Minimal Working Example

A complete, working sketch for ml5.js v1.0 with DoodleNet:

<!DOCTYPE html>
<html>
<head>
  <script src="https://cdn.jsdelivr.net/npm/p5@1/lib/p5.min.js"></script>
  <script src="https://unpkg.com/ml5@1/dist/ml5.min.js"></script>
</head>
<body>
<script>
let classifier;
let label = "Loading model...";
let confidence = 0;

function preload() {
  classifier = ml5.imageClassifier('DoodleNet');
}

function setup() {
  let cnv = createCanvas(280, 280);
  background(255);
  stroke(0);
  strokeWeight(16);
  noFill();

  // start continuous classification
  classifier.classifyStart(cnv, gotResults);

  // clear button
  let btn = createButton('Clear');
  btn.mousePressed(() => background(255));
}

function draw() {
  // display the current classification
  fill(0);
  noStroke();
  textSize(16);
  textAlign(CENTER);
  text(label + ' (' + nf(confidence * 100, 2, 1) + '%)', width / 2, height - 10);

  // restore drawing settings
  noFill();
  stroke(0);
  strokeWeight(16);
}

function mouseDragged() {
  line(pmouseX, pmouseY, mouseX, mouseY);
}

function gotResults(results) {
  label = results[0].label;
  confidence = results[0].confidence;
}
</script>
</body>
</html>

Sources