Skip to content

Instantly share code, notes, and snippets.

@joshuajnoble
Created March 10, 2013 21:11
Show Gist options
  • Save joshuajnoble/5130437 to your computer and use it in GitHub Desktop.
Save joshuajnoble/5130437 to your computer and use it in GitHub Desktop.
Cinder based Histogram of Oriented Gradients creation using SVMLight (svmlight.joachims.org) requires lots of images to work properly
#include "cinder/app/AppBasic.h"
#include "cinder/gl/gl.h"
#include "cinder/gl/Texture.h"
#include "cinder/Capture.h"
#include <boost/filesystem/operations.hpp>
#include <boost/filesystem/fstream.hpp>
#include <boost/foreach.hpp>
#include <stdio.h>
#include <dirent.h>
#include <ios>
#include <fstream>
#include <stdexcept>
#include "CinderOpenCV.h"
#include "svmlight/svmlight.h"
using namespace cv;
using namespace ci;
using namespace ci::app;
using namespace std;
class HOGTrainApp : public AppBasic {
public:
HOGTrainApp();
void setup();
void mouseDown( MouseEvent event );
void update();
void draw();
// generate the descriptor
int createHOG();
// write out data
void saveDescriptorVectorToFile(vector<float>& descriptorVector, vector<unsigned int>& _vectorIndices, string fileName);
// utility
void getFilesInDirectory(const string& dirName, vector<string>& fileNames, const vector<string>& validExtensions);
// pass in the descriptors in case we want to make multiple
void calculateFeaturesFromInput(const string& imageFilename, vector<float>& featureVector, shared_ptr<HOGDescriptor> hog);
// test the detection
void detectTest(Mat& imageData, vector<cv::Rect>& found);
shared_ptr<HOGDescriptor> hogDesc;
// Directory containing positive sample images
string posSamplesDirectory;
// Directory containing negative sample images
string negSamplesDirectory;
// Set the file to write the features to
string featuresFile;
// Set the file to write the SVM model to
string svmModelFile;
// Set the file to write the resulting detecting descriptor vector to
string descriptorVectorFile;
const cv::Size trainingPadding;
const cv::Size winStride;
Capture cap;
bool doDetection;
gl::Texture captureTexture;
vector<ci::Rectf> detectedLocations;
};
HOGTrainApp::HOGTrainApp():trainingPadding(0, 0), winStride(8,8)
{
posSamplesDirectory = "pos/";
negSamplesDirectory = "neg/";
featuresFile = "genfiles/features.dat";
svmModelFile = "genfiles/svmlightmodel.dat";
descriptorVectorFile = "genfiles/descriptorvector.dat";
}
void HOGTrainApp::setup()
{
createHOG(); // make a HOG descriptor based on our pos & neg samples
captureTexture = gl::Texture(640, 480);
doDetection = false;
cap = Capture(640, 480);
cap.start();
}
void HOGTrainApp::mouseDown( MouseEvent event )
{
doDetection = true;
}
void HOGTrainApp::update()
{
if(cap.checkNewFrame()) {
captureTexture = gl::Texture(cap.getSurface());
if(doDetection)
{
cv::Mat m = toOcv(cap.getSurface());
vector<cv::Rect> found;
detectTest(m, found);
doDetection = false;
detectedLocations.clear();
for ( int i = 0; i < found.size(); i++) {
detectedLocations.push_back(fromOcv(found[i]));
}
}
}
}
void HOGTrainApp::draw()
{
// clear out the window with black
gl::clear( Color( 0, 0, 0 ) );
gl::color(1, 1, 1);
gl::draw(captureTexture);
gl::color(0, 1, 0);
for ( int i = 0; i < detectedLocations.size(); i++) {
gl::drawStrokedRect(detectedLocations[i]);
}
}
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// HOG training
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
void HOGTrainApp::saveDescriptorVectorToFile(vector<float>& descriptorVector, vector<unsigned int>& _vectorIndices, string fileName) {
console() << "Saving descriptor vector to file " << fileName << endl;
string separator = " "; // Use blank as default separator between single features
fstream File;
float percent;
File.open(fileName.c_str(), ios::out);
if (File.good() && File.is_open()) {
for (int feature = 0; feature < descriptorVector.size(); ++feature) {
if ((feature % 10 == 0) || (feature == (descriptorVector.size()-1)) ) {
percent = ((1 + feature) * 100 / descriptorVector.size());
}
File << descriptorVector.at(feature) << separator;
}
File << endl;
File.flush();
File.close();
}
}
void HOGTrainApp::getFilesInDirectory(const string& dirName, vector<string>& fileNames, const vector<string>& validExtensions) {
fs::path targetDir(dirName);
fs::directory_iterator it(targetDir), eod;
BOOST_FOREACH(fs::path const &p, std::make_pair(it, eod))
{
if(is_regular_file(p))
{
string filename = p.string();
// is it a hidden file?
if(filename.find(".") == 0)
continue;
int extensionLocation = string(filename).find_last_of(".");
string tempExt = filename.substr(extensionLocation + 1);
std::transform(tempExt.begin(), tempExt.end(), tempExt.begin(), ::tolower);
if (find(validExtensions.begin(), validExtensions.end(), tempExt) != validExtensions.end()) {
fileNames.push_back(filename);
}
}
}
}
void HOGTrainApp::calculateFeaturesFromInput(const string& imageFilename, vector<float>& featureVector, shared_ptr<HOGDescriptor> hog) {
Mat imageData = imread(imageFilename, 0);
if (imageData.empty()) {
featureVector.clear();
console() << "Error: imge is empty " << imageFilename << endl;
return;
}
// all dimensions need to match
if (imageData.cols != hog->winSize.width || imageData.rows != hog->winSize.height) {
featureVector.clear();
console() << "image dimesions need to match HOG window size " << endl;
return;
}
vector<cv::Point> locations;
hog->compute(imageData, featureVector, winStride, trainingPadding, locations);
imageData.release(); // Release the image again after features are extracted
}
void HOGTrainApp::detectTest(Mat& imageData, vector<cv::Rect>& found) {
//vector<cv::Rect> found;
int groupThreshold = 0;
cv::Size padding(cv::Size(0,0));
cv::Size winStride(cv::Size(8, 8));
double hitThreshold = 0; // tolerance
hogDesc->detectMultiScale(imageData, found, hitThreshold, winStride, padding, 1.05, groupThreshold);
}
int HOGTrainApp::createHOG()
{
cv::Size win(96,160);
cv::Size blockSize(16, 16);
cv::Size blockStride(8,8);
cv::Size cellSize(8,8);
int nbins = 9;
hogDesc.reset( new HOGDescriptor(win, blockSize, blockStride, cellSize, nbins) ); // Use standard parameters here
// Get the files to train from somewhere
static vector<string> positiveTrainingImages;
static vector<string> negativeTrainingImages;
static vector<string> validExtensions;
validExtensions.push_back("jpg");
validExtensions.push_back("jpeg");
validExtensions.push_back("png");
getFilesInDirectory(posSamplesDirectory, positiveTrainingImages, validExtensions);
getFilesInDirectory(negSamplesDirectory, negativeTrainingImages, validExtensions);
/// Retrieve the descriptor vectors from the samples
unsigned long overallSamples = positiveTrainingImages.size() + negativeTrainingImages.size();
// Make sure there are actually samples to train
if (overallSamples == 0) {
console() << "No training sample files found, nothing to do! " << endl;;
return 1;
}
setlocale(LC_ALL, "C"); // Do not use the system locale
setlocale(LC_NUMERIC,"C");
setlocale(LC_ALL, "POSIX");
console() << "Reading files, generating HOG features nd saving them. Go enjoy some coffee or tea, this is gonna take a while" << endl;
float percent;
fstream File;
File.open(featuresFile.c_str(), ios::out);
if (File.good() && File.is_open()) {
File << "# Use this file to train, e.g. SVMlight by issuing $ svm_learn -i 1 -a weights.txt " << featuresFile.c_str() << endl; // Remove this line for libsvm which does not support comments
// Iterate over sample images
for (unsigned long currentFile = 0; currentFile < overallSamples; ++currentFile) {
vector<float> featureVector;
// Get positive or negative sample image file path
const string currentImageFile = (currentFile < positiveTrainingImages.size() ? positiveTrainingImages.at(currentFile) : negativeTrainingImages.at(currentFile - positiveTrainingImages.size()));
// Output progress
if ( (currentFile+1) % 10 == 0 || (currentFile+1) == overallSamples ) {
percent = ((currentFile+1) * 100 / overallSamples);
console() << currentFile+1 << " " << percent << "% " << currentImageFile << endl;
}
// Calculate feature vector from current image file
calculateFeaturesFromInput(currentImageFile, featureVector, hogDesc);
if (!featureVector.empty()) {
// positive class to +1 and negative class to -1 for SVMlight processing
File << ((currentFile < positiveTrainingImages.size()) ? "+1" : "-1");
// Save feature vector components
for (unsigned int feature = 0; feature < featureVector.size(); ++feature) {
File << " " << (feature + 1) << ":" << featureVector.at(feature);
}
File << endl;
}
}
File.flush();
File.close();
} else {
console() << "Error opening file " << featuresFile << endl;
return 0;
}
// </editor-fold>
/// Read in and train the calculated feature vectors
console() << "Calling SVMlight " << endl;
SVMlight::getInstance()->read_problem(const_cast<char*> (featuresFile.c_str()));
SVMlight::getInstance()->train(); // Call the core libsvm training procedure
console() <<"Training done, saving model " << endl;
SVMlight::getInstance()->saveModelToFile(svmModelFile);
//Generate single detecting feature vector from calculated SVM support vectors and SVM model
console() << "Generating HOG feature vector w/ svmlight" << endl;
vector<float> descriptorVector;
vector<unsigned int> descriptorVectorIndices;
// Generate a single detecting feature vector (v1 | b) from the trained support vectors, for use e.g. with the HOG algorithm
SVMlight::getInstance()->getSingleDetectingVector(descriptorVector, descriptorVectorIndices);
// save to file
saveDescriptorVectorToFile(descriptorVector, descriptorVectorIndices, descriptorVectorFile);
console() <<"Testing custom detection using camera " << endl;
hogDesc->setSVMDetector(descriptorVector); // Set our custom detecting vector
return 1;
}
CINDER_APP_BASIC( HOGTrainApp, RendererGl )
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment