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
| Concept | Old API (v0.x) | New API (v1.0+) |
|---|---|---|
| Continuous classification | Call classify() recursively inside the callback | classifyStart(canvas, gotResults), which handles the loop internally |
| Stop classifying | (no built-in way) | classifyStop() |
| Result shape | results[i].label, results[i].confidence | Same, but returned as { label, confidence } |
| Constructor | ml5.imageClassifier('DoodleNet', callback) | Same signature, but also supports await |
Symptoms
classifier.classifyStart is not a function: you’re using v0.x code with a v1.0 library (or vice versa).- Classification happens once and stops: you’re using the v0.x recursive
pattern but forgot to recurse, or you’re loading v1.0 and should use
classifyStart()instead. ml5.imageClassifier is not a function: the library failed to load entirely, or you’re loading a CDN URL that doesn’t match the API you’re calling.
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
- Classification confidence is uniformly low (< 30%) even for clear drawings.
- Every drawing is classified as the same category regardless of what you drew.
- Results seem random or nonsensical.
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
- Model returns results but with very low confidence.
- Drawings of clearly recognizable objects get misclassified.
- Adding more detail to a drawing doesn’t improve recognition.
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
TypeError: Cannot read properties of undefined (reading 'classify')Error: Model not loaded yet- Nothing happens: no errors and no results.
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
- The label updates once (or only when you reload the page) and never changes again no matter what you draw.
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
- After “clearing” and drawing again, the model still sees the old drawing (if there’s content behind the canvas in the HTML).
- The transparent background confuses the model and produces erratic results.
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
Error: No input image providedTypeError: Cannot read properties of null- The model runs but always returns the same result (it may be classifying a blank or uninitialized buffer).
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
- Every frame is classified as the same category with similar confidence.
- Results have no correlation with what the camera sees.
- Confidence values cluster around the same low range for all inputs.
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:
| Task | Model |
|---|---|
| Classify photos or webcam | MobileNet |
| Classify hand-drawn doodles | DoodleNet |
| Custom categories | Train 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
TypeError: Cannot read properties of undefined (reading '0'): appears intermittently, especially on slow connections or when the model is still warming up.
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
Error: Number of splits must evenly divide the axis(a known DoodleNet issue with certain TF.js versions; see GitHub #558).Error: Argument 'x' passed to 'conv2d' must be a Tensor- Random tensor shape errors during classification.
- Model loads but crashes on first
classify()call.
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
- On tiny canvases: strokes fill the entire space, everything looks like a blob.
- On very large canvases: even with
strokeWeight(16), strokes look thin relative to the canvas area. The model sees mostly white.
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
- Drawing produces dots instead of smooth strokes.
- DoodleNet can’t recognize disconnected dots as meaningful shapes.
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
- Browser becomes sluggish or unresponsive.
- Console fills with results faster than you can read them.
- GPU memory usage spikes.
- Results flicker rapidly as each frame triggers a new classification.
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
- Drawings look different from what the model expects (filled shapes vs. line drawings).
- Classification is unreliable even though the drawing looks correct to you.
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
ml5 is not definedcreateCanvas is not defined- Page loads but nothing happens: no canvas and no errors (the script failed to load silently with a 404).
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
| Symptom | Likely Cause |
|---|---|
| Model never loads / page hangs | Wrong CDN URL, network issue, or script loading order |
classify is not a function | Model not loaded yet, or v0.x/v1.0 API mismatch |
| Everything classified the same | Wrong background color, wrong strokeWeight, or DoodleNet on photos |
| Low confidence on all drawings | Thin strokes, wrong background, canvas too small or too large |
| Works once then stops | Missing recursive classify() call (v0.x), or need classifyStart() (v1.0) |
results is undefined in callback | Using v0.x callback signature (error, results) with v1.0 library |
| Tensor errors in console | TensorFlow.js version conflict (extra <script> tag) |
| Browser is sluggish | Calling classify() inside draw() without throttling |
| Drawings are dots, not lines | Using mousePressed instead of mouseDragged, or point() instead of line() |
| Canvas looks transparent after clear | Using clear() instead of background(255) |
ml5 is not defined | Script 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
- The Coding Train: Classifying Drawings with DoodleNet
- ml5.js Official DoodleNet Canvas Example
- ml5.js 1.0 Release Blog Post
- ml5.js + p5.js 2.0 Async Model Constructors
- DoodleNet TF.js Error: GitHub Issue #558
- ml5.js ImageClassifier Source (next-gen)
- Canvas Image Classification DoodleNet Example (ml5 examples)
- DoodleNet Model Repository
- Original DoodleNet by Yining Shi