Skip to content
This repository has been archived by the owner on Sep 26, 2023. It is now read-only.

Commit

Permalink
refactor training and collection scripts
Browse files Browse the repository at this point in the history
  • Loading branch information
harthur committed Oct 25, 2012
1 parent e1723d2 commit bf37854
Show file tree
Hide file tree
Showing 18 changed files with 593 additions and 698 deletions.
2 changes: 1 addition & 1 deletion kittydar.js
Expand Up @@ -6,7 +6,7 @@ if (process.arch) { // in node
var Canvas = (require)('canvas');
}

var network = require("./network.json");
var network = require("./network.js");
var net = new brain.NeuralNetwork().fromJSON(network);

var params = {
Expand Down
1 change: 0 additions & 1 deletion nms.js
Expand Up @@ -18,7 +18,6 @@ exports.combineOverlaps = combineOverlaps;
* to be included in the final returned set.
*/
function combineOverlaps(rects, minRatio, minOverlaps) {
console.log(minRatio, minOverlaps);
minRatio = minRatio || 0.5;
minOverlaps = minOverlaps || 1;

Expand Down
31 changes: 17 additions & 14 deletions package.json
@@ -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"
}
60 changes: 60 additions & 0 deletions training/README.md
@@ -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
```

107 changes: 107 additions & 0 deletions training/collect.js
@@ -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;
}
30 changes: 30 additions & 0 deletions training/collection/README.md
@@ -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).



Expand Up @@ -5,34 +5,31 @@
FlickRaw.api_key="0cc11cffc8a238efef4dfa6dca255a44"
FlickRaw.shared_secret="5f76a97053f99673"

$count = 0

$fetched = Hash.new

$dir = ARGV[0]

def getPage(page)
list = flickr.photos.getRecent :per_page => 500, :page => page

list.each do |photo|
url = "http://farm#{photo.farm}.staticflickr.com/#{photo.server}/#{photo.id}_#{photo.secret}_c.jpg"
url = "http://farm#{photo.farm}.staticflickr.com/#{photo.server}/#{photo.id}_#{photo.secret}.jpg"

if $fetched[url] != 1
$fetched[url] = 1

name = rand(100000000000)

file = "NEGS_FLICKR/#{name}.jpg"

puts file
file = "#{$dir}/#{name}.jpg"

open(file, 'wb') do |file|
file << open(url).read
end
puts "saved to #{file}"
$count+=1
end
end
end

# gets 120 x 500 = 60,000 images
120.times do |i|
getPage(i)
end
75 changes: 75 additions & 0 deletions training/collection/make-negatives.js
@@ -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;
}

0 comments on commit bf37854

Please sign in to comment.