This repository has been archived by the owner on Sep 26, 2023. It is now read-only.
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
refactor training and collection scripts
- Loading branch information
Showing
18 changed files
with
593 additions
and
698 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,16 +1,19 @@ | ||
{ | ||
"name": "kittydar", | ||
"description": "Cat detection", | ||
"version": "0.1.2", | ||
"author": "Heather Arthur <fayearthur@gmail.com>", | ||
"repository": { | ||
"type": "git", | ||
"url": "http://github.com/harthur/kittydar.git" | ||
}, | ||
"dependencies" : { | ||
"canvas" : "~0.10.0", | ||
"brain" : "~0.6.0", | ||
"hog-descriptor" : "~0.4.0" | ||
}, | ||
"main": "./kittydar" | ||
"name": "kittydar", | ||
"description": "Cat detection", | ||
"version": "0.1.2", | ||
"author": "Heather Arthur <fayearthur@gmail.com>", | ||
"repository": { | ||
"type": "git", | ||
"url": "http://github.com/harthur/kittydar.git" | ||
}, | ||
"dependencies" : { | ||
"canvas" : "~0.13.1", | ||
"brain" : "~0.6.0", | ||
"hog-descriptor" : "~0.4.0" | ||
}, | ||
"devDependencies" : { | ||
"nomnom" : "~1.5.2" | ||
}, | ||
"main": "./kittydar" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
# Training | ||
|
||
The goal of training is to create a classifier (in this case a neural network) that can be used to classify cat head images. | ||
|
||
After a final round of training you should have the JSON state of a neural network in the file "network.json", which can be imported and used by kittydar. | ||
|
||
## collection | ||
|
||
First you need to collect positive and negative images to train the network with. See the `collection` directory for more information. | ||
|
||
## train the classifier | ||
|
||
You can train a network with: | ||
|
||
``` | ||
node train-network.js POSITIVES NEGATIVES | ||
``` | ||
|
||
where POSITIVES is the directory of positive images (cat head crops), and NEGATIVES is a directory of samples from non-cat images. | ||
|
||
This will write the network to "network.json". | ||
|
||
## test the classifier | ||
|
||
After training the network you can test the network on a set of test positive and negative images (different from the ones that trained it): | ||
|
||
``` | ||
node test-network.js POSITIVES_TEST NEGATIVES_TEST --network ./network.json | ||
``` | ||
|
||
This will report the neural network error, as well as binary classification statistics like precision and recall. | ||
|
||
## optional: finding optimal parameters | ||
|
||
Find the best parameters for the feature extraction and classifier with cross-validation. Edit the `combos` object to add a combination and run with: | ||
|
||
``` | ||
node cross-validate.js POSITIVES NEGATIVES | ||
``` | ||
|
||
This will cross-validate on each combination of parameteres and report statistics on each combination, including the precision, recall, accuracy, and error of the test set. | ||
|
||
## optional: mining hard negatives | ||
|
||
After you've trained a classifier, you can test the classifier on a different set of negative images and save any false positives as "hard negatives". You can take the hard negatives and the positives and train a new (more precise) classifier. | ||
|
||
``` | ||
node mine-negatives.js NEGATIVES_EXTRA HARD --samples 1 --network ./network.json | ||
``` | ||
|
||
where `HARD` is a new directory to hold the mined negatives. The `threshold` param determines when a negative is classified as hard. It's a number from 0.5 to 1.0 (from "leaning positive" to very false positive). | ||
|
||
`samples` is the number of times to sample each negative image. It can take a lot of images to find a few hard negatives if you're classifier is good enough, so specifying a higher value will mine more hard negatives in the end. | ||
|
||
You can then train a new classifier with: | ||
|
||
``` | ||
node train-network.js POSITIVES HARD | ||
``` | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,107 @@ | ||
var fs = require("fs"), | ||
path = require("path"), | ||
Canvas = require("canvas"), | ||
utils = require("../utils") | ||
features = require("../features"); | ||
|
||
exports.collectData = collectData; | ||
exports.getDir = getDir; | ||
exports.extractSamples = extractSamples; | ||
|
||
/* | ||
* Collect the canvas representations of the images in the positive and | ||
* negative directories and return | ||
* an array of objects that look like: | ||
* { | ||
* input: <Array of floats> from image features | ||
* output: [0,1] (depending if it's a cat or not) | ||
* file: 'test.jpg' | ||
* } | ||
*/ | ||
function collectData(posDir, negDir, samples, limit, params) { | ||
// number of samples to extract from each negative, 0 for whole image | ||
samples = samples || 0; | ||
params = params || {}; | ||
|
||
var pos = getDir(posDir, true, 0, limit, params); | ||
var neg = getDir(negDir, false, samples, limit, params); | ||
|
||
var data = pos.concat(neg); | ||
|
||
// randomize so neural network doesn't get biased toward one set | ||
data.sort(function() { | ||
return 1 - 2 * Math.round(Math.random()); | ||
}); | ||
return data; | ||
} | ||
|
||
function getDir(dir, isCat, samples, limit, params) { | ||
var files = fs.readdirSync(dir); | ||
|
||
var images = files.filter(function(file) { | ||
return (path.extname(file) == ".png" | ||
|| path.extname(file) == ".jpg"); | ||
}); | ||
|
||
images = images.slice(0, limit); | ||
|
||
var data = []; | ||
for (var i = 0; i < images.length; i++) { | ||
var file = dir + "/" + images[i]; | ||
try { | ||
var canvas = utils.drawImgToCanvasSync(file); | ||
} | ||
catch(e) { | ||
console.log(e, file); | ||
continue; | ||
} | ||
|
||
var canvases = extractSamples(canvas, samples); | ||
|
||
for (var j = 0; j < canvases.length; j++) { | ||
var fts; | ||
try { | ||
fts = features.extractFeatures(canvases[j], params.HOG); | ||
} catch(e) { | ||
console.log("error extracting features", e, file); | ||
continue; | ||
} | ||
data.push({ | ||
input: fts, | ||
output: [isCat ? 1 : 0], | ||
file: file, | ||
}); | ||
} | ||
} | ||
|
||
return data; | ||
} | ||
|
||
|
||
function extractSamples(canvas, num) { | ||
if (num == 0) { | ||
// 0 means "don't sample" | ||
return [canvas]; | ||
} | ||
|
||
var min = 48; | ||
var max = Math.min(canvas.width, canvas.height); | ||
|
||
var canvases = []; | ||
for (var i = 0; i < num; i++) { | ||
var length = Math.max(min, Math.ceil(Math.random() * max)); | ||
|
||
var x = Math.floor(Math.random() * (max - length)); | ||
var y = Math.floor(Math.random() * (max - length)); | ||
|
||
canvases.push(cropCanvas(canvas, x, y, length, length)); | ||
} | ||
return canvases; | ||
} | ||
|
||
function cropCanvas(canvas, x, y, width, height) { | ||
var cropCanvas = new Canvas(width, height); | ||
var context = cropCanvas.getContext("2d"); | ||
context.drawImage(canvas, x, y, width, height, 0, 0, width, height); | ||
return cropCanvas; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
## collection | ||
|
||
the goal of collection is to get a folder of positive (cat head) images and a folder of negative (non-cat) images to train the classifier with. | ||
|
||
### creating the positives | ||
|
||
To get the positives, first download this [dataset of cat pictures](http://137.189.35.203/WebUI/CatDatabase/catData.html). There should be folders called CAT_00, CAT_01, etc. Take the images from all of these and combine into one directory. Also remove the file "00000003_019.jpg.cat" and add [00000003_015.jpg.cat](http://137.189.35.203/WebUI/CatDatabase/Data/00000003_015.jpg.cat). | ||
|
||
Run the script to rotate and the crop out the cat head from each image. If you put the cat dataset in a folder called "CATS" and you want to put the cropped images in a folder called "POSITIVES": | ||
|
||
`node make-positives.js CATS POSITIVES` | ||
|
||
### creating the negatives | ||
|
||
If you don't already have a bunch of non-cat pictures you can fetch recent images from Flickr and save them in a folder called "FLICKR" by running: | ||
|
||
`ruby fetch-negatives.rb NEGATIVES` | ||
|
||
You'll need at least 10,000 images. | ||
|
||
To turn the full-sized images into negatives that can be used directly for training or testing, sample them with: | ||
|
||
`node make-negatives NEGATIVES NEGATIVES_SAMPLED` | ||
|
||
Where `NEGATIVES_SAMPLED` is the directory to contain the sampled images. | ||
|
||
If you're getting images from Flickr, some will contain cats for sure, so you'll need to weed those out by taking a close look at your hard negatives (see `training` directory above). | ||
|
||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
var fs = require("fs"), | ||
path = require("path"), | ||
nomnom = require("nomnom"), | ||
Canvas = require("canvas"), | ||
utils = require("../../utils"); | ||
|
||
var opts = nomnom.options({ | ||
indir: { | ||
position: 0, | ||
default: __dirname + "/FLICKR/", | ||
help: "Directory of full-sizes negative images" | ||
}, | ||
outdir: { | ||
position: 1, | ||
default: __dirname + "/NEGATIVES/", | ||
help: "Directory to save cropped image sections" | ||
}, | ||
samples: { | ||
default: 1, | ||
help: "How many times to sub-sample each image" | ||
} | ||
}).colors().parse(); | ||
|
||
|
||
fs.readdir(opts.indir, function(err, files) { | ||
if (err) throw err; | ||
|
||
var images = files.filter(function(file) { | ||
return path.extname(file) == ".jpg"; | ||
}); | ||
|
||
console.log(images.length, "images to process"); | ||
|
||
images.forEach(function(image) { | ||
var file = opts.indir + "/" + image; | ||
try { | ||
var canvas = utils.drawImgToCanvasSync(file); | ||
} | ||
catch(e) { | ||
console.log(e, file); | ||
return; | ||
} | ||
var canvases = extractSamples(canvas, opts.samples); | ||
|
||
canvases.forEach(function(canvas) { | ||
var name = Math.floor(Math.random() * 10000000000); | ||
var file = opts.outdir + "/" + name + ".jpg"; | ||
|
||
utils.writeCanvasToFileSync(canvas, file); | ||
}); | ||
}); | ||
}) | ||
|
||
function extractSamples(canvas, num) { | ||
var min = 48; | ||
var max = Math.min(canvas.width, canvas.height); | ||
|
||
var canvases = []; | ||
for (var i = 0; i < num; i++) { | ||
var length = Math.max(48, Math.ceil(Math.random() * max)); | ||
|
||
var x = Math.floor(Math.random() * (max - length)); | ||
var y = Math.floor(Math.random() * (max - length)); | ||
|
||
canvases.push(cropCanvas(canvas, x, y, length, length)); | ||
} | ||
return canvases; | ||
} | ||
|
||
function cropCanvas(canvas, x, y, width, height) { | ||
var cropCanvas = new Canvas(width, height); | ||
var context = cropCanvas.getContext("2d"); | ||
context.drawImage(canvas, x, y, width, height, 0, 0, width, height); | ||
return cropCanvas; | ||
} |
Oops, something went wrong.