diff --git a/openbr/plugins/cascade.cpp b/openbr/plugins/cascade.cpp index cc928b5..02d63ba 100644 --- a/openbr/plugins/cascade.cpp +++ b/openbr/plugins/cascade.cpp @@ -19,11 +19,164 @@ #include "openbr_internal.h" #include "openbr/core/opencvutils.h" #include "openbr/core/resource.h" +#include +#include + +#ifndef _win32 + const QString foldersep = "/"; +#else + const QString foldersep = "\\"; +#endif using namespace cv; namespace br { + + struct trainParams{ + public: + QString data; // REQUIRED: Filepath to store trained classifier + QString vec; // REQUIRED: Filepath to store vector of positive samples, default "vector" + QString img; // Filepath to source object image. Either this or info is REQUIRED + QString info; // Description file of source images. Either this or img is REQUIRED + QString bg; // REQUIRED: Filepath to background list file + int num; // Number of samples to generate + int bgcolor; // Background color supplied image (via img) + int bgthresh; // Threshold to determine bgcolor match + bool inv; // Invert colors + bool randinv; // Randomly invert colors + int maxidev; // Max intensity deviation of foreground pixels + double maxxangle; // Maximum rotation angle (X) + double maxyangle; // Maximum rotation angle (Y) + double maxzangle; // Maximum rotation angle (Z) + bool show; // Show generated samples + int w; // REQUIRED: Sample width + int h; // REQUIRED: Sample height + int numPos; // Number of positive samples + int numNeg; // Number of negative samples + int numStages; // Number of stages + int precalcValBufSize; // Precalculated val buffer size in Mb + int precalcIdxBufSize; // Precalculated index buffer size in Mb + bool baseFormatSave; // Save in old format + QString stageType; // Stage type (BOOST) + QString featureType; // Feature type (HAAR, LBP) + QString bt; // Boosted classifier type (DAB, RAB, LB, GAB) + double minHitRate; // Minimal hit rate per stage + double maxFalseAlarmRate; // Max false alarm rate per stage + double weightTrimRate; // Weight for trimming + int maxDepth; // Max weak tree depth + int maxWeakCount; // Max weak tree count per stage + QString mode; // Haar feature mode (BASIC, CORE, ALL) + + trainParams(){ + num = -1; + maxidev = -1; + maxxangle = -1; + maxyangle = -1; + maxzangle = -1; + w = -1; + h = -1; + numPos = -1; + numNeg = -1; + numStages = -1; + precalcValBufSize = -1; + precalcIdxBufSize = -1; + minHitRate = -1; + maxFalseAlarmRate = -1; + weightTrimRate = -1; + maxDepth = -1; + maxWeakCount = -1; + inv = false; + randinv = false; + show = false; + baseFormatSave = false; + vec = "vector.vec"; + bgcolor = -1; + bgthresh = -1; + } + }; + + QString buildTrainingArgs(trainParams params){ + QString args = ""; + if (params.data != "") args += "-data " + params.data + " "; + else return ""; + if (params.vec != "") args += "-vec " + params.vec + " "; + else return ""; + if (params.bg != "") args += "-bg " + params.bg + " "; + else return ""; + if (params.numPos >= 0) args += "-numPos " + QString::number(params.numPos) + " "; + if (params.numNeg >= 0) args += "-numNeg " + QString::number(params.numNeg) + " "; + if (params.numStages >= 0) args += "-numStages " + QString::number(params.numStages) + " "; + if (params.precalcValBufSize >= 0) args += "-precalcValBufSize " + QString::number(params.precalcValBufSize) + " "; + if (params.precalcIdxBufSize >= 0) args += "-precalcIdxBufSize " + QString::number(params.precalcIdxBufSize) + " "; + if (params.baseFormatSave) args += "-baseFormatSave "; + if (params.stageType != "") args += "-stageType " + params.stageType + " "; + if (params.featureType != "") args += "-featureType " + params.featureType + " "; + if (params.w >= 0) args += "-w " + QString::number(params.w) + " "; + else return ""; + if (params.h >= 0) args += "-h " + QString::number(params.h) + " "; + else return ""; + if (params.bt != "") args += "-bt " + params.bt + " "; + if (params.minHitRate >= 0) args += "-minHitRate " + QString::number(params.minHitRate) + " "; + if (params.maxFalseAlarmRate >= 0) args += "-maxFalseAlarmRate " + QString::number(params.maxFalseAlarmRate) + " "; + if (params.weightTrimRate >= 0) args += "-weightTrimRate " + QString::number(params.weightTrimRate) + " "; + if (params.maxDepth >= 0) args += "-maxDepth " + QString::number(params.maxDepth) + " "; + if (params.maxWeakCount >= 0) args += "-maxWeakCount " + QString::number(params.maxWeakCount) + " "; + if (params.mode != "") args += "-mode " + params.mode + " "; + return args; + } + + + QString buildSampleArgs(trainParams params){ + QString args = ""; + if (params.vec != "") args += "-vec "+params.vec+" "; + else return ""; + if (params.img != "") args += "-img " + params.img + " "; + else if (params.info != "") args += "-info " + params.info + " "; + else return ""; + if (params.bg != "") args += "-bg " + params.bg + " "; + if (params.num > 0) args += "-num " + QString::number(params.num) + " "; + if (params.bgcolor >=0 ) args += "-bgcolor " + QString::number(params.bgcolor) + " "; + if (params.bgthresh >= 0) args += "-bgthresh " + QString::number(params.bgthresh) + " "; + if (params.maxidev >= 0) args += "-maxidev " + QString::number(params.maxidev) + " "; + if (params.maxxangle >= 0) args += "-maxxangle " + QString::number(params.maxxangle) + " "; + if (params.maxyangle >= 0) args += "-maxyangle " + QString::number(params.maxyangle) + " "; + if (params.maxzangle >= 0) args += "-maxzangle " + QString::number(params.maxzangle) + " "; + if (params.w >= 0) args += "-w " + QString::number(params.w) + " "; + if (params.h >= 0) args += "-h " + QString::number(params.h) + " "; + if (params.show) args += "-show "; + if (params.inv) args += "-inv "; + if (params.randinv) args += "-randinv "; + return args; + } + + void execCommand(QString cmd, QString args){ + #ifdef _WIN32 + cmd += ".exe"; + #endif + cmd += " " + args; + system(cmd.toLocal8Bit().data()); + } + + void genSamples(trainParams params, QString argStr = ""){ + QString cmdArgs = buildSampleArgs(params); + if (argStr != "") cmdArgs += " " + argStr; + execCommand("opencv_createsamples",cmdArgs); + } + + + void trainCascade(trainParams params,QString argStr = ""){ + QString cmdArgs = buildTrainingArgs(params); + if (argStr != "") cmdArgs += " " + argStr; + + execCommand("opencv_traincascade", cmdArgs); + } + +QString rectToString(QRectF r){ + QString out = " " + QString::number(r.x()) + " " + QString::number(r.y()) + " " + QString::number(r.width()) + " "+ QString::number(r.height()); + return out; +} + class CascadeResourceMaker : public ResourceMaker { @@ -32,13 +185,26 @@ class CascadeResourceMaker : public ResourceMaker public: CascadeResourceMaker(const QString &model) { - file = Globals->sdkPath + "/share/openbr/models/"; + file = Globals->sdkPath + "/share/openbr/models/"; if (model == "Ear") file += "haarcascades/haarcascade_ear.xml"; else if (model == "Eye") file += "haarcascades/haarcascade_eye_tree_eyeglasses.xml"; else if (model == "FrontalFace") file += "haarcascades/haarcascade_frontalface_alt2.xml"; else if (model == "ProfileFace") file += "haarcascades/haarcascade_profileface.xml"; - else qFatal("Invalid model."); - } + else{ + // Create temp folder if does not exist + file = model+foldersep+"cascade.xml"; + QDir dir(model); + if (!dir.exists()) + if (!QDir::current().mkdir(model)) qFatal("Cannot create model."); + + // Make sure file can be created + QFile pathTest(file); + if (pathTest.exists()) pathTest.remove(); + + if (!pathTest.open(QIODevice::WriteOnly | QIODevice::Text)) qFatal("Cannot create model."); + pathTest.remove(); + } + } private: CascadeClassifier *make() const @@ -50,20 +216,66 @@ private: } }; + /*! * \ingroup transforms * \brief Wraps OpenCV cascade classifier * \author Josh Klontz \cite jklontz */ -class CascadeTransform : public UntrainableMetaTransform +class CascadeTransform : public MetaTransform { Q_OBJECT Q_PROPERTY(QString model READ get_model WRITE set_model RESET reset_model STORED false) Q_PROPERTY(int minSize READ get_minSize WRITE set_minSize RESET reset_minSize STORED false) Q_PROPERTY(bool ROCMode READ get_ROCMode WRITE set_ROCMode RESET reset_ROCMode STORED false) + + // Training parameters + Q_PROPERTY(int numStages READ get_numStages WRITE set_numStages RESET reset_numStages STORED false) + Q_PROPERTY(int w READ get_w WRITE set_w RESET reset_w STORED false) + Q_PROPERTY(int h READ get_h WRITE set_h RESET reset_h STORED false) + Q_PROPERTY(int numPos READ get_numPos WRITE set_numPos RESET reset_numPos STORED false) + Q_PROPERTY(int numNeg READ get_numNeg WRITE set_numNeg RESET reset_numNeg STORED false) + Q_PROPERTY(int precalcValBufSize READ get_precalcValBufSize WRITE set_precalcValBufSize RESET reset_precalcValBufSize STORED false) + Q_PROPERTY(int precalcIdxBufSize READ get_precalcIdxBufSize WRITE set_precalcIdxBufSize RESET reset_precalcIdxBufSize STORED false) + Q_PROPERTY(double minHitRate READ get_minHitRate WRITE set_minHitRate RESET reset_minHitRate STORED false) + Q_PROPERTY(double maxFalseAlarmRate READ get_maxFalseAlarmRate WRITE set_maxFalseAlarmRate RESET reset_maxFalseAlarmRate STORED false) + Q_PROPERTY(double weightTrimRate READ get_weightTrimRate WRITE set_weightTrimRate RESET reset_weightTrimRate STORED false) + Q_PROPERTY(int maxDepth READ get_maxDepth WRITE set_maxDepth RESET reset_maxDepth STORED false) + Q_PROPERTY(int maxWeakCount READ get_maxWeakCount WRITE set_maxWeakCount RESET reset_maxWeakCount STORED false) + Q_PROPERTY(QString stageType READ get_stageType WRITE set_stageType RESET reset_stageType STORED false) + Q_PROPERTY(QString featureType READ get_featureType WRITE set_featureType RESET reset_featureType STORED false) + Q_PROPERTY(QString bt READ get_bt WRITE set_bt RESET reset_bt STORED false) + Q_PROPERTY(QString mode READ get_mode WRITE set_mode RESET reset_mode STORED false) + Q_PROPERTY(bool show READ get_show WRITE set_show RESET reset_show STORED false) + Q_PROPERTY(bool baseFormatSave READ get_baseFormatSave WRITE set_baseFormatSave RESET reset_baseFormatSave STORED false) + Q_PROPERTY(bool overwrite READ get_overwrite WRITE set_overwrite RESET reset_overwrite STORED false) + + BR_PROPERTY(QString, model, "FrontalFace") BR_PROPERTY(int, minSize, 64) BR_PROPERTY(bool, ROCMode, false) + + // Training parameters - Default values provided trigger OpenCV defaults + BR_PROPERTY(int, numStages, -1) + BR_PROPERTY(int,w,-1) + BR_PROPERTY(int,h,-1) + BR_PROPERTY(int,numPos,-1) + BR_PROPERTY(int,numNeg,-1) + BR_PROPERTY(int,precalcValBufSize,-1) + BR_PROPERTY(int,precalcIdxBufSize,-1) + BR_PROPERTY(double,minHitRate,-1) + BR_PROPERTY(double,maxFalseAlarmRate,-1) + BR_PROPERTY(double,weightTrimRate,-1) + BR_PROPERTY(int,maxDepth,-1) + BR_PROPERTY(int,maxWeakCount,-1) + BR_PROPERTY(QString,stageType,"") + BR_PROPERTY(QString,featureType,"") + BR_PROPERTY(QString,bt,"") + BR_PROPERTY(QString,mode,"") + BR_PROPERTY(bool,show,false) + BR_PROPERTY(bool,baseFormatSave,false) + BR_PROPERTY(bool,overwrite,false) + Resource cascadeResource; @@ -71,6 +283,144 @@ class CascadeTransform : public UntrainableMetaTransform { cascadeResource.setResourceMaker(new CascadeResourceMaker(model)); } + + // Train transform + void train(const TemplateList& data){ + if (overwrite){ + QDir dataDir(model); + if (dataDir.exists()){ + dataDir.removeRecursively(); + QDir::current().mkdir(model); + } + } + + FileList files = data.files(); + + + // Open positive and negative list files + QString posFName = "pos.txt"; + QString negFName = "neg.txt"; + QFile posFile(posFName); + QFile negFile(negFName); + posFile.open(QIODevice::WriteOnly | QIODevice::Text); + negFile.open(QIODevice::WriteOnly | QIODevice::Text); + QTextStream posStream(&posFile); + QTextStream negStream(&negFile); + + + const QString endln = "\r\n"; + + int posCount = 0; + int negCount = 0; + + bool buildPos = false; // If true, build positive vector from single image + + QList vectors; + + trainParams params; + + // Fill in from params (param defaults are same as struct defaults, so no checks are needed) + params.numStages = numStages; + params.w = w; + params.h = h; + params.numPos = numPos; + params.numNeg = numNeg; + params.precalcValBufSize = precalcValBufSize; + params.precalcIdxBufSize = precalcIdxBufSize; + params.minHitRate = minHitRate; + params.maxFalseAlarmRate = maxFalseAlarmRate; + params.weightTrimRate = weightTrimRate; + params.maxDepth = maxDepth; + params.maxWeakCount = maxWeakCount; + params.stageType = stageType; + params.featureType = featureType; + params.bt = bt; + params.mode = mode; + params.show = show; + params.baseFormatSave = baseFormatSave; + if (params.w < 0) params.w = minSize; + if (params.h < 0) params.h = minSize; + + for (int i = 0; i < files.length(); i++){ + File f = files[i]; + if (f.localKeys().contains("training-set")){ + QString tset = f.localMetadata()["training-set"].toString().toLower(); + + // Negative samples + if (tset == "neg"){ + if (negCount > 0) negStream< 0) posStream< 0) + posStream << f.path() << foldersep << f.fileName() << " " << f.rects().length() << " " << rects; + + // Single positive sample for background removal and overlay on negatives + }else if (tset == "pos-base"){ + + buildPos = true; + params.img = f.path() + foldersep + f.fileName(); + + // Parse settings (unique to this one tag) + if (f.localKeys().contains("num")) params.num = f.localMetadata()["num"].toInt(); + if (f.localKeys().contains("bgcolor")) params.bgcolor = f.localMetadata()["bgcolor"].toInt(); + if (f.localKeys().contains("bgthresh")) params.bgthresh = f.localMetadata()["bgthresh"].toInt(); + if (f.localKeys().contains("inv")) params.inv = f.localMetadata()["inv"].toBool(); + if (f.localKeys().contains("randinv")) params.randinv = f.localMetadata()["randinv"].toBool(); + if (f.localKeys().contains("maxidev")) params.maxidev = f.localMetadata()["maxidev"].toInt(); + if (f.localKeys().contains("maxxangle")) params.maxxangle = f.localMetadata()["maxxangle"].toDouble(); + if (f.localKeys().contains("maxyangle")) params.maxyangle = f.localMetadata()["maxyangle"].toDouble(); + if (f.localKeys().contains("maxzangle")) params.maxzangle = f.localMetadata()["maxzangle"].toDouble(); + + }else if (tset == "vector"){ + vectors.append(f.path() + foldersep + f.fileName()); + } + } + } + + + // Fill in remaining params conditionally + posFile.close(); + negFile.close(); + if (buildPos){ + if (params.numPos < 0){ + if (params.num > 0) params.numPos = (int)(params.num*.95); + else params.numPos = 950; + posFile.remove(); + } + }else{ + params.info = posFName; + if (params.numPos < 0){ + params.numPos = (int)(posCount*.95); + } + } + params.bg = negFName; + params.data = model; + if (params.num < 0){ + params.num = posCount; + } + if (params.numNeg < 0){ + params.numNeg = negCount*10; + } + + + genSamples(params); + trainCascade(params); + if (posFile.exists()) posFile.remove(); + negFile.remove(); + } + void project(const Template &src, Template &dst) const {