diff --git a/openbr/core/eval.cpp b/openbr/core/eval.cpp index ee87e72..4ac3e5b 100644 --- a/openbr/core/eval.cpp +++ b/openbr/core/eval.cpp @@ -411,35 +411,82 @@ static QStringList computeDetectionResults(const QList &detec QString getDetectKey(const TemplateList &templates) { const File &f = templates.first().file; - foreach (const QString &key, f.localKeys()) + foreach (const QString &key, f.localKeys()) { + // first check for single detections if (!f.get(key, QRectF()).isNull()) return key; + } + // and then multiple + if (!f.rects().empty()) + return "Rects"; return ""; } -float EvalDetection(const QString &predictedGallery, const QString &truthGallery, const QString &csv) +bool detectKeyIsList(QString key, const TemplateList &templates) { - qDebug("Evaluating detection of %s against %s", qPrintable(predictedGallery), qPrintable(truthGallery)); - const TemplateList predicted(TemplateList::fromGallery(predictedGallery)); - const TemplateList truth(TemplateList::fromGallery(truthGallery)); + return templates.first().file.get(key, QRectF()).isNull(); +} + +// return a list of detections whether the template holds +// multiple detections or a single detection +QList getDetections(QString key, const Template &t, bool isList, bool isTruth) +{ + File f = t.file; + QList dets; + if (isList) { + QList rects = f.rects(); + QList confidences = f.getList("Confidences", QList()); + if (!isTruth && rects.size() != confidences.size()) + qFatal("You don't have enough confidence. I mean, your detections don't all have confidence measures."); + for (int i=0; i(key))); + } else { + dets.append(Detection(f.get(key), f.get("Confidence", -1))); + } + } + return dets; +} +QMap getDetections(const TemplateList &predicted, const TemplateList &truth) +{ // Figure out which metadata field contains a bounding box QString truthDetectKey = getDetectKey(truth); if (truthDetectKey.isEmpty()) qFatal("No suitable ground truth metadata key found."); - QString predictedDetectKey = truthDetectKey; - if (predicted.first().file.get(predictedDetectKey, QRectF()).isNull()) - predictedDetectKey = getDetectKey(predicted); + QString predictedDetectKey = getDetectKey(predicted); if (predictedDetectKey.isEmpty()) qFatal("No suitable predicted metadata key found."); - qDebug("Using metadata key: %s%s", qPrintable(predictedDetectKey), qPrintable(predictedDetectKey == truthDetectKey ? QString() : "/"+truthDetectKey)); - QMap allDetections; // Organized by file, QMap used to preserve order - foreach (const Template &t, predicted) - allDetections[t.file.baseName()].predicted.append(Detection(t.file.get(predictedDetectKey), t.file.get("Confidence", -1))); - foreach (const Template &t, truth) - allDetections[t.file.baseName()].truth.append(Detection(t.file.get(truthDetectKey))); + QMap allDetections; + bool predKeyIsList = detectKeyIsList(predictedDetectKey, predicted); + bool truthKeyIsList = detectKeyIsList(truthDetectKey, truth); + foreach (const Template &t, predicted) { + QList dets = getDetections(predictedDetectKey, t, predKeyIsList, false); + allDetections[t.file.baseName()].predicted.append(dets); + } + foreach (const Template &t, truth) { + QList dets = getDetections(truthDetectKey, t, truthKeyIsList, true); + allDetections[t.file.baseName()].truth.append(dets); + } + return allDetections; +} + +float EvalDetection(const QString &predictedGallery, const QString &truthGallery, const QString &csv) +{ + qDebug("Evaluating detection of %s against %s", qPrintable(predictedGallery), qPrintable(truthGallery)); + const TemplateList predicted(TemplateList::fromGallery(predictedGallery)); + const TemplateList truth(TemplateList::fromGallery(truthGallery)); + + // Organized by file, QMap used to preserve order + QMap allDetections = getDetections(predicted, truth); QList resolvedDetections, falseNegativeDetections; foreach (Detections detections, allDetections.values()) { diff --git a/openbr/core/plot.cpp b/openbr/core/plot.cpp index fde8469..1f926ec 100644 --- a/openbr/core/plot.cpp +++ b/openbr/core/plot.cpp @@ -343,10 +343,10 @@ bool PlotDetection(const QStringList &files, const File &destination, bool show) QString(" + theme(aspect.ratio=1)\n\n"))); p.file.write(qPrintable(QString("ggplot(AverageOverlap, aes(x=%1, y=%2, label=round(X,3)), main=\"Average Overlap\") + geom_text() + theme_minimal()").arg(p.minor.size > 1 ? p.minor.header : "'X'", p.major.size > 1 ? p.major.header : "'Y'") + - QString("%1%2\n\n").arg(p.minor.size > 1 ? "" : " + xlab(NULL)", p.major.size > 1 ? "" : "ylab(NULL)"))); + QString("%1%2\n\n").arg(p.minor.size > 1 ? "" : " + xlab(NULL)", p.major.size > 1 ? "" : " + ylab(NULL)"))); p.file.write(qPrintable(QString("ggplot(AverageOverlap, aes(x=%1, y=%2, fill=X)) + geom_tile() + scale_fill_continuous(\"Average Overlap\") + theme_minimal()").arg(p.minor.size > 1 ? p.minor.header : "'X'", p.major.size > 1 ? p.major.header : "'Y'") + - QString("%1%2\n\n").arg(p.minor.size > 1 ? "" : " + xlab(NULL)", p.major.size > 1 ? "" : "ylab(NULL)"))); + QString("%1%2\n\n").arg(p.minor.size > 1 ? "" : " + xlab(NULL)", p.major.size > 1 ? "" : " + ylab(NULL)"))); return p.finalize(show); } diff --git a/openbr/core/qtutils.cpp b/openbr/core/qtutils.cpp index a153853..aaccf0a 100644 --- a/openbr/core/qtutils.cpp +++ b/openbr/core/qtutils.cpp @@ -399,13 +399,33 @@ void showFile(const QString &file) QString toString(const QVariant &variant) { - if (variant.canConvert(QVariant::String)) return variant.toString(); - else if(variant.canConvert(QVariant::PointF)) return QString("(%1,%2)").arg(QString::number(qvariant_cast(variant).x()), - QString::number(qvariant_cast(variant).y())); - else if (variant.canConvert(QVariant::RectF)) return QString("(%1,%2,%3,%4)").arg(QString::number(qvariant_cast(variant).x()), - QString::number(qvariant_cast(variant).y()), - QString::number(qvariant_cast(variant).width()), - QString::number(qvariant_cast(variant).height())); + if (variant.canConvert(QVariant::String)) + return variant.toString(); + else if(variant.canConvert(QVariant::PointF)) { + QPointF pt = qvariant_cast(variant); + return QString("(%1,%2)").arg(QString::number(pt.x()), + QString::number(pt.y())); + } + else if (variant.canConvert(QVariant::RectF)) { + QRectF rect = qvariant_cast(variant); + return QString("(%1,%2,%3,%4)").arg(QString::number(rect.x()), + QString::number(rect.y()), + QString::number(rect.width()), + QString::number(rect.height())); + } + else if (variant.canConvert(QVariant::List)) { + QString ret = QString("["); + bool first = true; + foreach (const QVariant &i, variant.toList()) { + if (!first) + ret += ","; + else + first = false; + ret += toString(i); + } + ret += "]"; + return ret; + } return QString(); } diff --git a/openbr/openbr_plugin.cpp b/openbr/openbr_plugin.cpp index ccfae5c..4f61df9 100644 --- a/openbr/openbr_plugin.cpp +++ b/openbr/openbr_plugin.cpp @@ -19,6 +19,7 @@ #include #include #include +#include #include #include #include @@ -40,6 +41,12 @@ using namespace br; using namespace cv; +// Some globals used to transfer data to Context::messageHandler so that +// we can restart the process if we try and fail to create a QApplication. +static bool creating_qapp = false; +static int * argc_ptr = NULL; +static char ** argv_ptr = NULL; + /* File - public methods */ // Note that the convention for displaying metadata is as follows: // [] for lists in which argument order does not matter (e.g. [FTO=false, Index=0]), @@ -886,14 +893,28 @@ void br::Context::initialize(int &argc, char *argv[], QString sdkPath, bool use_ break; } } + + qInstallMessageHandler(messageHandler); + // We take in argc as a reference due to: // https://bugreports.qt-project.org/browse/QTBUG-5637 // QApplication should be initialized before anything else. // Since we can't ensure that it gets deleted last, we never delete it. if (QCoreApplication::instance() == NULL) { #ifndef BR_EMBEDDED - if (use_gui) application = new QApplication(argc, argv); - else application = new QCoreApplication(argc, argv); + if (use_gui) { + // Set up variables to be used in the message handler if this fails. + // Just so you know, we + creating_qapp = true; + argc_ptr = &argc; + argv_ptr = argv; + + application = new QApplication(argc, argv); + creating_qapp = false; + } + else { + application = new QCoreApplication(argc, argv); + } #else application = new QCoreApplication(argc, argv); #endif @@ -919,7 +940,6 @@ void br::Context::initialize(int &argc, char *argv[], QString sdkPath, bool use_ Globals->init(File()); Globals->useGui = use_gui; - qInstallMessageHandler(messageHandler); Common::seedRNG(); @@ -988,6 +1008,26 @@ void br::Context::messageHandler(QtMsgType type, const QMessageLogContext &conte static QMutex generalLock; QMutexLocker locker(&generalLock); + // If we are trying to create a QApplication, and get a fatal, then restart the process + // with useGui set to 0. + if (creating_qapp && type == QtFatalMsg) + { + // re-launch process with useGui = 0 + std::cout << "Failed to initialize gui, restarting with -useGui 0" << std::endl; + QStringList arguments; + arguments.append("-useGui"); + arguments.append("0"); + for (int i=1; i < *argc_ptr; i++) + { + arguments.append(argv_ptr[i]); + } + // QProcess::execute blocks until the other process completes. + QProcess::execute(argv_ptr[0], arguments); + // have to unlock this for some reason + locker.unlock(); + std::exit(0); + } + QString txt; if (type == QtDebugMsg) { if (Globals->quiet) return; diff --git a/openbr/openbr_plugin.h b/openbr/openbr_plugin.h index d382184..d551bb0 100644 --- a/openbr/openbr_plugin.h +++ b/openbr/openbr_plugin.h @@ -41,6 +41,8 @@ #include #include #include +#include +#include /*! * \defgroup cpp_plugin_sdk C++ Plugin SDK @@ -215,6 +217,14 @@ struct BR_EXPORT File static QVariant parse(const QString &value); /*!< \brief Try to convert the QString to a QPointF or QRectF if possible. */ inline void set(const QString &key, const QVariant &value) { m_metadata.insert(key, value); } /*!< \brief Insert or overwrite the metadata key with the specified value. */ void set(const QString &key, const QString &value); /*!< \brief Insert or overwrite the metadata key with the specified value. */ + + /*!< \brief Specialization for list type. Insert or overwrite the metadata key with the specified value. */ + template + void setList(const QString &key, const QList &value) + { + set(key, QtUtils::toVariantList(value)); + } + inline void remove(const QString &key) { m_metadata.remove(key); } /*!< \brief Remove the metadata key. */ /*!< \brief Returns a value for the key, throwing an error if the key does not exist. */ @@ -253,6 +263,19 @@ struct BR_EXPORT File return list; } + /*!< \brief Specialization for list type. Returns a list of type T for the key, returning \em defaultValue if the key does not exist or can't be converted. */ + template + QList getList(const QString &key, const QList defaultValue) const + { + if (!contains(key)) return defaultValue; + QList list; + foreach (const QVariant &item, m_metadata[key].toList()) { + if (item.canConvert()) list.append(item.value()); + else return defaultValue; + } + return list; + } + /*!< \brief Returns the value for the specified key for every file in the list. */ template static QList values(const QList &fileList, const QString &key) @@ -292,9 +315,12 @@ struct BR_EXPORT File QList namedRects() const; /*!< \brief Returns rects convertible from metadata values. */ QList rects() const; /*!< \brief Returns the file's rects list. */ void appendRect(const QRectF &rect); /*!< \brief Adds a rect to the file's rect list. */ + void appendRect(const cv::Rect &rect) { appendRect(OpenCVUtils::fromRect(rect)); } /*!< \brief Adds a rect to the file's rect list. */ void appendRects(const QList &rects); /*!< \brief Adds rects to the file's rect list. */ + void appendRects(const QList &rects) { appendRects(OpenCVUtils::fromRects(rects)); } /*!< \brief Adds rects to the file's rect list. */ inline void clearRects() { m_metadata["Rects"] = QList(); } /*!< \brief Clears the file's rect list. */ inline void setRects(const QList &rects) { clearRects(); appendRects(rects); } /*!< \brief Overwrites the file's rect list. */ + inline void setRects(const QList &rects) { clearRects(); appendRects(rects); } /*!< \brief Overwrites the file's rect list. */ private: QMap m_metadata; diff --git a/openbr/plugins/gallery.cpp b/openbr/plugins/gallery.cpp index 43a52b2..1549bcd 100644 --- a/openbr/plugins/gallery.cpp +++ b/openbr/plugins/gallery.cpp @@ -857,18 +857,21 @@ class FDDBGallery : public Gallery for (int i=0; i faceList; //to be consistent with slidingWindow if (detect.size() == 5) { //rectangle - t.file.set("Face", QRectF(detect[0].toFloat(), detect[1].toFloat(), detect[2].toFloat(), detect[3].toFloat())); + faceList.append(QRectF(detect[0].toFloat(), detect[1].toFloat(), detect[2].toFloat(), detect[3].toFloat())); t.file.set("Confidence", detect[4].toFloat()); } else if (detect.size() == 6) { //ellipse float x = detect[3].toFloat(), y = detect[4].toFloat(), radius = detect[1].toFloat(); - t.file.set("Face", QRectF(x - radius,y - radius,radius * 2.0, radius * 2.0)); + faceList.append(QRectF(x - radius,y - radius,radius * 2.0, radius * 2.0)); t.file.set("Confidence", detect[5].toFloat()); } else { qFatal("Unknown FDDB annotation format."); } + t.file.set("Face", faceList); + t.file.set("Label",QString("face")); templates.append(t); } } diff --git a/openbr/plugins/landmarks.cpp b/openbr/plugins/landmarks.cpp index a19439a..0773541 100644 --- a/openbr/plugins/landmarks.cpp +++ b/openbr/plugins/landmarks.cpp @@ -113,7 +113,7 @@ class ProcrustesTransform : public Transform // R(0,0), R(1,0), R(1,1), R(0,1), mean_x, mean_y, norm QList procrustesStats; procrustesStats << R(0,0) << R(1,0) << R(1,1) << R(0,1) << mean[0] << mean[1] << norm; - dst.file.set("ProcrustesStats",QtUtils::toVariantList(procrustesStats)); + dst.file.setList("ProcrustesStats",procrustesStats); if (warp) { Eigen::MatrixXf dstMat = srcMat*R; @@ -273,7 +273,7 @@ class DelaunayTransform : public UntrainableTransform dst.file.setRects(QList() << OpenCVUtils::fromRect(boundingBox)); } else dst = src; - dst.file.set("DelaunayTriangles", QtUtils::toVariantList(validTriangles)); + dst.file.setList("DelaunayTriangles", validTriangles); } }; diff --git a/openbr/plugins/regions.cpp b/openbr/plugins/regions.cpp index bd850aa..317eb54 100644 --- a/openbr/plugins/regions.cpp +++ b/openbr/plugins/regions.cpp @@ -53,7 +53,6 @@ class RectRegionsTransform : public UntrainableTransform dst += m(Rect(x, y, width, height)); } }; - BR_REGISTER(Transform, RectRegionsTransform) /*! diff --git a/openbr/plugins/slidingwindow.cpp b/openbr/plugins/slidingwindow.cpp index 27a37a2..84c93a2 100644 --- a/openbr/plugins/slidingwindow.cpp +++ b/openbr/plugins/slidingwindow.cpp @@ -1,9 +1,16 @@ #include "openbr_internal.h" #include "openbr/core/opencvutils.h" #include "openbr/core/common.h" +#include "openbr/core/qtutils.h" +#include +#include +#include using namespace cv; +// Because MSVC doesn't provide a round() function in math.h +static int round(float x) { return (floor(x + 0.5)); } + namespace br { @@ -19,31 +26,65 @@ class SlidingWindowTransform : public Transform Q_PROPERTY(br::Transform *transform READ get_transform WRITE set_transform RESET reset_transform STORED false) Q_PROPERTY(int minSize READ get_minSize WRITE set_minSize RESET reset_minSize STORED false) Q_PROPERTY(double scaleFactor READ get_scaleFactor WRITE set_scaleFactor RESET reset_scaleFactor STORED false) - Q_PROPERTY(double stepSize READ get_stepSize WRITE set_stepSize RESET reset_stepSize STORED false) + Q_PROPERTY(int stepSize READ get_stepSize WRITE set_stepSize RESET reset_stepSize STORED false) Q_PROPERTY(bool takeLargestScale READ get_takeLargestScale WRITE set_takeLargestScale RESET reset_takeLargestScale STORED false) Q_PROPERTY(bool negSamples READ get_negSamples WRITE set_negSamples RESET reset_negSamples STORED false) Q_PROPERTY(int negToPosRatio READ get_negToPosRatio WRITE set_negToPosRatio RESET reset_negToPosRatio STORED false) Q_PROPERTY(double maxOverlap READ get_maxOverlap WRITE set_maxOverlap RESET reset_maxOverlap STORED false) + Q_PROPERTY(float aspectRatio READ get_aspectRatio WRITE set_aspectRatio RESET reset_aspectRatio STORED true) + Q_PROPERTY(int windowWidth READ get_windowWidth WRITE set_windowWidth RESET reset_windowWidth STORED false) BR_PROPERTY(br::Transform *, transform, NULL) BR_PROPERTY(int, minSize, 8) BR_PROPERTY(double, scaleFactor, 0.75) - BR_PROPERTY(double, stepSize, 1) + BR_PROPERTY(int, stepSize, 1) BR_PROPERTY(bool, takeLargestScale, true) BR_PROPERTY(bool, negSamples, true) BR_PROPERTY(int, negToPosRatio, 1) BR_PROPERTY(double, maxOverlap, 0) + BR_PROPERTY(float, aspectRatio, 1) + BR_PROPERTY(int, windowWidth, 24) public: SlidingWindowTransform() : Transform(false, true) {} - private: + void train(const TemplateList &data) { if (transform->trainable) { + double tempRatio = 0; + int ratioCnt = 0; TemplateList full; + + //First find avg aspect ratio + foreach (const Template &tmpl, data) { + QList posRects = OpenCVUtils::toRects(tmpl.file.rects()); + foreach (const Rect &posRect, posRects) { + if (posRect.x + posRect.width >= tmpl.m().cols || posRect.y + posRect.height >= tmpl.m().rows || posRect.x < 0 || posRect.y < 0) { + continue; + } + tempRatio += (float)posRect.width / (float)posRect.height; + ratioCnt += 1; + } + } + aspectRatio = tempRatio / (double)ratioCnt; + foreach (const Template &tmpl, data) { - foreach (const Rect &rect, OpenCVUtils::toRects(tmpl.file.rects())) { - Template pos(tmpl.file, Mat(tmpl, rect)); + QList posRects = OpenCVUtils::toRects(tmpl.file.rects()); + QList negRects; + foreach (Rect posRect, posRects) { + + //Adjust for training samples that have different aspect ratios + int diff = posRect.width - (int)((float) posRect.height * aspectRatio); + posRect.x += diff / 2; + posRect.width += diff; + + if (posRect.x + posRect.width >= tmpl.m().cols || posRect.y + posRect.height >= tmpl.m().rows || posRect.x < 0 || posRect.y < 0) { + continue; + } + + Mat scaledImg; + resize(Mat(tmpl, posRect), scaledImg, Size(windowWidth,round(windowWidth / aspectRatio))); + Template pos(tmpl.file, scaledImg); full += pos; // add random negative samples @@ -57,9 +98,11 @@ private: int maxSize = std::min(maxWidth, maxHeight); int size = (maxSize <= minSize ? maxSize : Common::RandSample(1, maxSize, minSize)[0]); Rect negRect(x, y, size, size); - Rect intersect = negRect & rect; - if (intersect.area() > maxOverlap*rect.area()) + // the negative samples cannot overlap the positive at all + // but they may overlap with other negatives + if (overlaps(posRects, negRect, 0) || overlaps(negRects, negRect, maxOverlap)) continue; + negRects.append(negRect); Template neg(tmpl.file, Mat(tmpl, negRect)); neg.file.set("Label", QString("neg")); full += neg; @@ -72,6 +115,16 @@ private: } } + bool overlaps(QList posRects, Rect negRect, double overlap) + { + foreach (const Rect posRect, posRects) { + Rect intersect = negRect & posRect; + if (intersect.area() > overlap*posRect.area()) + return true; + } + return false; + } + void project(const Template &src, Template &dst) const { dst = src; @@ -79,17 +132,33 @@ private: if (src.file.getBool("Train", false)) return; dst.file.clearRects(); - int rows = src.m().rows, cols = src.m().cols; - for (double size=std::min(rows, cols); size>=minSize; size*=scaleFactor) { - for (double y=0; y+size aspectRatio) + startScale = round((float) rows / (float) windowHeight); + else + startScale = round((float) cols / (float) windowWidth); + + for (float scale = startScale; scale >= 1.0; scale -= (1.0 - scaleFactor)) { + Mat scaleImg; + resize(src, scaleImg, Size(round(cols / scale), round(rows / scale))); + + for (double y = 0; y + windowHeight < scaleImg.rows; y += stepSize) { + for (double x = 0; x + windowWidth < scaleImg.cols; x += stepSize) { +qDebug() << "x=" << x << "\ty=" << y; + Rect window(x, y, windowWidth, windowHeight); + Template windowMat(src.file, Mat(scaleImg, window)); Template detect; transform->project(windowMat, detect); // the result will be in the Label - if (detect.file.get(QString("Label")) == "pos") { - dst.file.appendRect(OpenCVUtils::fromRect(window)); + if (detect.file.get("Label") == "pos") { + dst.file.appendRect(QRectF((float) x * scale, (float) y * scale, (float) windowWidth * scale, (float) windowHeight * scale)); + float confidence = detect.file.get("Dist"); + QList confidences = dst.file.getList("Confidences", QList()); + confidences.append(confidence); + dst.file.setList("Confidences", confidences); if (takeLargestScale) return; } } @@ -100,6 +169,36 @@ private: BR_REGISTER(Transform, SlidingWindowTransform) +/*! + * \ingroup transforms + * \brief Detects objects with OpenCV's built-in HOG detection. + * \author Austin Blanton \cite imaus10 + */ +class HOGDetectTransform : public UntrainableTransform +{ + Q_OBJECT + + HOGDescriptor hog; + + void init() + { + hog.setSVMDetector(HOGDescriptor::getDefaultPeopleDetector()); + } + + void project(const Template &src, Template &dst) const + { + dst = src; + std::vector objLocs; + QList rects; + hog.detectMultiScale(src, objLocs); + foreach (const Rect &obj, objLocs) + rects.append(obj); + dst.file.setRects(rects); + } +}; + +BR_REGISTER(Transform, HOGDetectTransform) + } // namespace br #include "slidingwindow.moc" diff --git a/openbr/plugins/svm.cpp b/openbr/plugins/svm.cpp index d4e2aad..a30fe9b 100644 --- a/openbr/plugins/svm.cpp +++ b/openbr/plugins/svm.cpp @@ -103,6 +103,7 @@ class SVMTransform : public Transform Q_PROPERTY(float gamma READ get_gamma WRITE set_gamma RESET reset_gamma STORED false) Q_PROPERTY(QString inputVariable READ get_inputVariable WRITE set_inputVariable RESET reset_inputVariable STORED false) Q_PROPERTY(QString outputVariable READ get_outputVariable WRITE set_outputVariable RESET reset_outputVariable STORED false) + Q_PROPERTY(bool returnDFVal READ get_returnDFVal WRITE set_returnDFVal RESET reset_returnDFVal STORED false) public: enum Kernel { Linear = CvSVM::LINEAR, @@ -123,6 +124,7 @@ private: BR_PROPERTY(float, gamma, -1) BR_PROPERTY(QString, inputVariable, "") BR_PROPERTY(QString, outputVariable, "") + BR_PROPERTY(bool, returnDFVal, false) SVM svm; @@ -149,8 +151,17 @@ private: void project(const Template &src, Template &dst) const { + if (returnDFVal && reverseLookup.size() > 2) + qFatal("Decision function for multiclass classification not implemented."); + dst = src; - float prediction = svm.predict(src.m().reshape(1, 1)); + float prediction = svm.predict(src.m().reshape(1, 1), returnDFVal); + if (returnDFVal) { + dst.file.set("Dist", prediction); + // positive values ==> first class + // negative values ==> second class + prediction = prediction > 0 ? 0 : 1; + } if (type == EPS_SVR || type == NU_SVR) dst.file.set(outputVariable, prediction); else