main.cpp 46.9 KB
Newer Older
André Anjos's avatar
André Anjos committed
1 2 3 4
/**
 * @author Andre Anjos <andre.anjos@idiap.ch>
 * @date Fri 25 Oct 16:54:55 2013
 *
André Anjos's avatar
André Anjos committed
5
 * @brief Bindings to bob::measure
André Anjos's avatar
André Anjos committed
6 7 8 9 10
 */

#ifdef NO_IMPORT_ARRAY
#undef NO_IMPORT_ARRAY
#endif
André Anjos's avatar
André Anjos committed
11
#include <bob.blitz/cleanup.h>
Amir Mohammadi's avatar
lint  
Amir Mohammadi committed
12
#include <bob.blitz/cppapi.h>
13
#include <bob.core/api.h>
14
#include <bob.extension/documentation.h>
Amir Mohammadi's avatar
lint  
Amir Mohammadi committed
15
#include <bob.io.base/api.h>
16

17
#include "cpp/error.h"
André Anjos's avatar
André Anjos committed
18

Amir Mohammadi's avatar
lint  
Amir Mohammadi committed
19 20 21
static int double1d_converter(PyObject *o, PyBlitzArrayObject **a) {
  if (PyBlitzArray_Converter(o, a) == 0)
    return 0;
22 23
  // in this case, *a is set to a new reference
  if ((*a)->type_num != NPY_FLOAT64 || (*a)->ndim != 1) {
Amir Mohammadi's avatar
lint  
Amir Mohammadi committed
24 25 26 27
    PyErr_Format(PyExc_TypeError,
                 "cannot convert blitz::Array<%s,%" PY_FORMAT_SIZE_T
                 "d> to a blitz::Array<double,1>",
                 PyBlitzArray_TypenumAsString((*a)->type_num), (*a)->ndim);
28
    return 0;
29
  }
30
  return 1;
31
}
André Anjos's avatar
André Anjos committed
32

Amir Mohammadi's avatar
lint  
Amir Mohammadi committed
33 34 35
static int double2d_converter(PyObject *o, PyBlitzArrayObject **a) {
  if (PyBlitzArray_Converter(o, a) == 0)
    return 0;
36 37
  // in this case, *a is set to a new reference
  if ((*a)->type_num != NPY_FLOAT64 || (*a)->ndim != 2) {
Amir Mohammadi's avatar
lint  
Amir Mohammadi committed
38 39 40 41
    PyErr_Format(PyExc_TypeError,
                 "cannot convert blitz::Array<%s,%" PY_FORMAT_SIZE_T
                 "d> to a blitz::Array<double,2>",
                 PyBlitzArray_TypenumAsString((*a)->type_num), (*a)->ndim);
42 43 44 45
    return 0;
  }
  return 1;
}
André Anjos's avatar
André Anjos committed
46

Amir Mohammadi's avatar
lint  
Amir Mohammadi committed
47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103
static auto epc_doc =
    bob::extension::FunctionDoc(
        "epc", "Calculates points of an Expected Performance Curve (EPC)",
        "Calculates the EPC curve given a set of positive and negative scores "
        "and a desired number of points. "
        "Returns a two-dimensional :py:class:`numpy.ndarray` of type float "
        "with the "
        "shape of ``(2, points)`` or ``(3, points)`` depending on the "
        "``thresholds`` argument. "
        "The rows correspond to the X (cost), Y (weighted error rate on the "
        "test set given the min. threshold on the development set), and the "
        "thresholds which were used to calculate the error (if the "
        "``thresholds`` argument was set to ``True``), respectively. "
        "Please note that, in order to calculate the EPC curve, one needs two "
        "sets of data comprising a development set and a test set. "
        "The minimum weighted error is calculated on the development set and "
        "then applied to the test set to evaluate the weighted error rate at "
        "that position.\n\n"
        "The EPC curve plots the HTER on the test set for various values of "
        "'cost'. "
        "For each value of 'cost', a threshold is found that provides the "
        "minimum weighted error (see "
        ":py:func:`bob.measure.min_weighted_error_rate_threshold`) on the "
        "development set. "
        "Each threshold is consecutively applied to the test set and the "
        "resulting weighted error values are plotted in the EPC.\n\n"
        "The cost points in which the EPC curve are calculated are distributed "
        "uniformly in the range :math:`[0.0, 1.0]`.\n\n"
        ".. note:: It is more memory efficient, when sorted arrays of scores "
        "are provided and the ``is_sorted`` parameter is set to ``True``.")
        .add_prototype("dev_negatives, dev_positives, test_negatives, "
                       "test_positives, n_points, [is_sorted], [thresholds]",
                       "curve")
        .add_parameter(
            "dev_negatives, dev_positives, test_negatives, test_positives",
            "array_like(1D, float)", "The scores for negatives and positives "
                                     "of the development and test set")
        .add_parameter(
            "n_points", "int",
            "The number of weights for which the EPC curve should be computed")
        .add_parameter("is_sorted", "bool",
                       "[Default: ``False``] Set this to ``True`` if the "
                       "scores are already sorted. If ``False``, scores will "
                       "be sorted internally, which will require more memory")
        .add_parameter("thresholds", "bool",
                       "[Default: ``False``] If ``True`` the function returns "
                       "an array with the shape of ``(3, points)`` where the "
                       "third row contains the thresholds that were calculated "
                       "on the development set.")
        .add_return("curve", "array_like(2D, float)",
                    "The EPC curve, with the first row containing the weights "
                    "and the second row containing the weighted errors on the "
                    "test set. If ``thresholds`` is ``True``, there is also a "
                    "third row which contains the thresholds that were "
                    "calculated on the development set.");
static PyObject *epc(PyObject *, PyObject *args, PyObject *kwds) {
  BOB_TRY
104
  /* Parses input arguments in a single shot */
Amir Mohammadi's avatar
lint  
Amir Mohammadi committed
105
  char **kwlist = epc_doc.kwlist();
106

Amir Mohammadi's avatar
lint  
Amir Mohammadi committed
107 108 109 110
  PyBlitzArrayObject *dev_neg;
  PyBlitzArrayObject *dev_pos;
  PyBlitzArrayObject *test_neg;
  PyBlitzArrayObject *test_pos;
111
  Py_ssize_t n_points;
Amir Mohammadi's avatar
lint  
Amir Mohammadi committed
112 113 114 115 116 117 118 119 120 121
  PyObject *is_sorted = Py_False;
  PyObject *thresholds = Py_False;

  if (!PyArg_ParseTupleAndKeywords(
          args, kwds, "O&O&O&O&n|OO", kwlist, &double1d_converter, &dev_neg,
          &double1d_converter, &dev_pos, &double1d_converter, &test_neg,
          &double1d_converter, &test_pos, &n_points, &is_sorted, &thresholds))
    return 0;

  // protects acquired resources through this scope
122 123 124 125 126
  auto dev_neg_ = make_safe(dev_neg);
  auto dev_pos_ = make_safe(dev_pos);
  auto test_neg_ = make_safe(test_neg);
  auto test_pos_ = make_safe(test_pos);

Amir Mohammadi's avatar
lint  
Amir Mohammadi committed
127 128 129 130 131 132
  auto result = bob::measure::epc(*PyBlitzArrayCxx_AsBlitz<double, 1>(dev_neg),
                                  *PyBlitzArrayCxx_AsBlitz<double, 1>(dev_pos),
                                  *PyBlitzArrayCxx_AsBlitz<double, 1>(test_neg),
                                  *PyBlitzArrayCxx_AsBlitz<double, 1>(test_pos),
                                  n_points, PyObject_IsTrue(is_sorted),
                                  PyObject_IsTrue(thresholds));
André Anjos's avatar
André Anjos committed
133

134
  return PyBlitzArrayCxx_AsNumpy(result);
Amir Mohammadi's avatar
lint  
Amir Mohammadi committed
135
  BOB_CATCH_FUNCTION("epc", 0)
136
}
André Anjos's avatar
André Anjos committed
137

Amir Mohammadi's avatar
lint  
Amir Mohammadi committed
138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169
static auto det_doc =
    bob::extension::FunctionDoc(
        "det", "Calculates points of an Detection Error-Tradeoff (DET) curve",
        "Calculates the DET curve given a set of negative and positive scores "
        "and a desired number of points. Returns a two-dimensional array of "
        "doubles that express on its rows:\n\n"
        "[0]  X axis values in the normal deviate scale for the "
        "false-accepts\n\n"
        "[1]  Y axis values in the normal deviate scale for the "
        "false-rejections\n\n"
        "You can plot the results using your preferred tool to first create a "
        "plot using rows 0 and 1 from the returned value and then replace the "
        "X/Y axis annotation using a pre-determined set of tickmarks as "
        "recommended by NIST. "
        "The derivative scales are computed with the "
        ":py:func:`bob.measure.ppndf` function.")
        .add_prototype("negatives, positives, n_points", "curve")
        .add_parameter(
            "negatives, positives", "array_like(1D, float)",
            "The list of negative and positive scores to compute the DET for")
        .add_parameter("n_points", "int", "The number of points on the DET "
                                          "curve, for which the DET should be "
                                          "evaluated")
        .add_return("curve", "array_like(2D, float)",
                    "The DET curve, with the FAR in the first and the FRR in "
                    "the second row");
static PyObject *det(PyObject *, PyObject *args, PyObject *kwds) {
  BOB_TRY
  char **kwlist = det_doc.kwlist();

  PyBlitzArrayObject *neg;
  PyBlitzArrayObject *pos;
170
  Py_ssize_t n_points;
André Anjos's avatar
André Anjos committed
171

Amir Mohammadi's avatar
lint  
Amir Mohammadi committed
172 173 174 175
  if (!PyArg_ParseTupleAndKeywords(args, kwds, "O&O&n", kwlist,
                                   &double1d_converter, &neg,
                                   &double1d_converter, &pos, &n_points))
    return 0;
André Anjos's avatar
André Anjos committed
176

Amir Mohammadi's avatar
lint  
Amir Mohammadi committed
177
  // protects acquired resources through this scope
178 179 180
  auto neg_ = make_safe(neg);
  auto pos_ = make_safe(pos);

Amir Mohammadi's avatar
lint  
Amir Mohammadi committed
181 182 183
  auto result =
      bob::measure::det(*PyBlitzArrayCxx_AsBlitz<double, 1>(neg),
                        *PyBlitzArrayCxx_AsBlitz<double, 1>(pos), n_points);
André Anjos's avatar
André Anjos committed
184

185
  return PyBlitzArrayCxx_AsNumpy(result);
Amir Mohammadi's avatar
lint  
Amir Mohammadi committed
186
  BOB_CATCH_FUNCTION("det", 0)
André Anjos's avatar
André Anjos committed
187 188
}

Amir Mohammadi's avatar
lint  
Amir Mohammadi committed
189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206
static auto ppndf_doc =
    bob::extension::FunctionDoc(
        "ppndf", "Returns the Deviate Scale equivalent of a false "
                 "rejection/acceptance ratio",
        "The algorithm that calculates the deviate scale is based on function "
        "ppndf() from the NIST package DETware version 2.1, freely available "
        "on the internet. "
        "Please consult it for more details. "
        "By 20.04.2011, you could find such package `here "
        "<http://www.itl.nist.gov/iad/mig/tools/>`_.")
        .add_prototype("value", "ppndf")
        .add_parameter("value", "float", "The value (usually FAR or FRR) for "
                                         "which the ppndf should be calculated")
        .add_return("ppndf", "float",
                    "The derivative scale of the given value");
static PyObject *ppndf(PyObject *, PyObject *args, PyObject *kwds) {
  BOB_TRY
  char **kwlist = ppndf_doc.kwlist();
207
  double v;
Amir Mohammadi's avatar
lint  
Amir Mohammadi committed
208 209
  if (!PyArg_ParseTupleAndKeywords(args, kwds, "d", kwlist, &v))
    return 0;
André Anjos's avatar
André Anjos committed
210

211
  return Py_BuildValue("d", bob::measure::ppndf(v));
Amir Mohammadi's avatar
lint  
Amir Mohammadi committed
212
  BOB_CATCH_FUNCTION("ppndf", 0)
André Anjos's avatar
André Anjos committed
213 214
}

Amir Mohammadi's avatar
lint  
Amir Mohammadi committed
215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238
static auto roc_doc =
    bob::extension::FunctionDoc(
        "roc",
        "Calculates points of an Receiver Operating Characteristic (ROC)",
        "Calculates the ROC curve given a set of negative and positive scores "
        "and a desired number of points. ")
        .add_prototype("negatives, positives, n_points", "curve")
        .add_parameter("negatives, positives", "array_like(1D, float)",
                       "The negative and positive scores, for which the ROC "
                       "curve should be calculated")
        .add_parameter("n_points", "int", "The number of points, in which the "
                                          "ROC curve are calculated, which are "
                                          "distributed uniformly in the range "
                                          "``[min(negatives, positives), "
                                          "max(negatives, positives)]``")
        .add_return("curve", "array_like(2D, float)",
                    "A two-dimensional array of doubles that express the X "
                    "(FAR) and Y (FRR) coordinates in this order");
static PyObject *roc(PyObject *, PyObject *args, PyObject *kwds) {
  BOB_TRY
  static char **kwlist = roc_doc.kwlist();

  PyBlitzArrayObject *neg;
  PyBlitzArrayObject *pos;
239
  Py_ssize_t n_points;
André Anjos's avatar
André Anjos committed
240

Amir Mohammadi's avatar
lint  
Amir Mohammadi committed
241 242 243 244
  if (!PyArg_ParseTupleAndKeywords(args, kwds, "O&O&n", kwlist,
                                   &double1d_converter, &neg,
                                   &double1d_converter, &pos, &n_points))
    return 0;
André Anjos's avatar
André Anjos committed
245

Amir Mohammadi's avatar
lint  
Amir Mohammadi committed
246
  // protects acquired resources through this scope
247 248 249
  auto neg_ = make_safe(neg);
  auto pos_ = make_safe(pos);

Amir Mohammadi's avatar
lint  
Amir Mohammadi committed
250 251 252
  auto result =
      bob::measure::roc(*PyBlitzArrayCxx_AsBlitz<double, 1>(neg),
                        *PyBlitzArrayCxx_AsBlitz<double, 1>(pos), n_points);
André Anjos's avatar
André Anjos committed
253

254
  return PyBlitzArrayCxx_AsNumpy(result);
Amir Mohammadi's avatar
lint  
Amir Mohammadi committed
255
  BOB_CATCH_FUNCTION("roc", 0)
André Anjos's avatar
André Anjos committed
256 257
}

Amir Mohammadi's avatar
lint  
Amir Mohammadi committed
258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317
static auto farfrr_doc =
    bob::extension::FunctionDoc(
        "farfrr", "Calculates the false-acceptance (FA) ratio and the "
                  "false-rejection (FR) ratio for the given positive and "
                  "negative scores and a score threshold",
        "``positives`` holds the score information for samples that are "
        "labeled to belong to a certain class (a.k.a., 'signal' or 'client'). "
        "``negatives`` holds the score information for samples that are "
        "labeled **not** to belong to the class (a.k.a., 'noise' or "
        "'impostor'). "
        "It is expected that 'positive' scores are, at least by design, "
        "greater than 'negative' scores. "
        "So, every 'positive' value that falls bellow the threshold is "
        "considered a false-rejection (FR). "
        "`negative` samples that fall above the threshold are considered a "
        "false-accept (FA).\n\n"
        "Positives that fall on the threshold (exactly) are considered "
        "correctly classified. "
        "Negatives that fall on the threshold (exactly) are considered "
        "**incorrectly** classified. "
        "This equivalent to setting the comparison like this pseudo-code:\n\n"
        "  ``foreach (positive as K) if K < threshold: falseRejectionCount += "
        "1``\n\n"
        "  ``foreach (negative as K) if K >= threshold: falseAcceptCount += "
        "1``\n\n"
        "The output is in form of a tuple of two double-precision real "
        "numbers. "
        "The numbers range from 0 to 1. "
        "The first element of the pair is the false-accept ratio (FAR), the "
        "second element the false-rejection ratio (FRR).\n\n"
        "The ``threshold`` value does not necessarily have to fall in the "
        "range covered by the input scores (negatives and positives "
        "altogether), but if it does not, the output will be either (1.0, 0.0) "
        "or (0.0, 1.0), depending on the side the threshold falls.\n\n"
        "It is possible that scores are inverted in the negative/positive "
        "sense. "
        "In some setups the designer may have setup the system so 'positive' "
        "samples have a smaller score than the 'negative' ones. "
        "In this case, make sure you normalize the scores so positive samples "
        "have greater scores before feeding them into this method.")
        .add_prototype("negatives, positives, threshold", "far, frr")
        .add_parameter(
            "negatives", "array_like(1D, float)",
            "The scores for comparisons of objects of different classes")
        .add_parameter(
            "positives", "array_like(1D, float)",
            "The scores for comparisons of objects of the same class")
        .add_parameter("threshold", "float", "The threshold to separate "
                                             "correctly and incorrectly "
                                             "classified scores")
        .add_return("far", "float",
                    "The False Accept Rate (FAR) for the given threshold")
        .add_return("frr", "float",
                    "The False Reject Rate (FRR) for the given threshold");
static PyObject *farfrr(PyObject *, PyObject *args, PyObject *kwds) {
  BOB_TRY
  char **kwlist = farfrr_doc.kwlist();

  PyBlitzArrayObject *neg;
  PyBlitzArrayObject *pos;
318
  double threshold;
André Anjos's avatar
André Anjos committed
319

Amir Mohammadi's avatar
lint  
Amir Mohammadi committed
320 321 322 323
  if (!PyArg_ParseTupleAndKeywords(args, kwds, "O&O&d", kwlist,
                                   &double1d_converter, &neg,
                                   &double1d_converter, &pos, &threshold))
    return 0;
André Anjos's avatar
André Anjos committed
324

Amir Mohammadi's avatar
lint  
Amir Mohammadi committed
325
  // protects acquired resources through this scope
326 327 328
  auto neg_ = make_safe(neg);
  auto pos_ = make_safe(pos);

Amir Mohammadi's avatar
lint  
Amir Mohammadi committed
329 330 331
  auto result =
      bob::measure::farfrr(*PyBlitzArrayCxx_AsBlitz<double, 1>(neg),
                           *PyBlitzArrayCxx_AsBlitz<double, 1>(pos), threshold);
André Anjos's avatar
André Anjos committed
332

333
  return Py_BuildValue("dd", result.first, result.second);
Amir Mohammadi's avatar
lint  
Amir Mohammadi committed
334
  BOB_CATCH_FUNCTION("farfrr", 0)
André Anjos's avatar
André Anjos committed
335 336
}

Amir Mohammadi's avatar
lint  
Amir Mohammadi committed
337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374
static auto eer_threshold_doc =
    bob::extension::FunctionDoc(
        "eer_threshold", "Calculates the threshold that is as close as "
                         "possible to the equal-error-rate (EER) for the given "
                         "input data",
        "The EER should be the point where the FAR equals the FRR. "
        "Graphically, this would be equivalent to the intersection between the "
        "ROC (or DET) curves and the identity.\n\n"
        ".. note::\n\n"
        "   The scores will be sorted internally, requiring the scores to be "
        "copied.\n"
        "   To avoid this copy, you can sort both sets of scores externally in "
        "ascendant order, and set the ``is_sorted`` parameter to ``True``")
        .add_prototype("negatives, positives, [is_sorted]", "threshold")
        .add_parameter(
            "negatives, positives", "array_like(1D, float)",
            "The set of negative and positive scores to compute the threshold")
        .add_parameter("is_sorted", "bool", "[Default: ``False``] Are both "
                                            "sets of scores already in "
                                            "ascendantly sorted order?")
        .add_return("threshold", "float", "The threshold (i.e., as used in "
                                          ":py:func:`bob.measure.farfrr`) "
                                          "where FAR and FRR are as close as "
                                          "possible");
static PyObject *eer_threshold(PyObject *, PyObject *args, PyObject *kwds) {
  BOB_TRY
  char **kwlist = eer_threshold_doc.kwlist();

  PyBlitzArrayObject *neg;
  PyBlitzArrayObject *pos;
  PyObject *is_sorted = Py_False;

  if (!PyArg_ParseTupleAndKeywords(args, kwds, "O&O&|O", kwlist,
                                   &double1d_converter, &neg,
                                   &double1d_converter, &pos, &is_sorted))
    return 0;

  // protects acquired resources through this scope
375 376 377
  auto neg_ = make_safe(neg);
  auto pos_ = make_safe(pos);

André Anjos's avatar
André Anjos committed
378
  double result = bob::measure::eerThreshold(
Amir Mohammadi's avatar
lint  
Amir Mohammadi committed
379 380
      *PyBlitzArrayCxx_AsBlitz<double, 1>(neg),
      *PyBlitzArrayCxx_AsBlitz<double, 1>(pos), PyObject_IsTrue(is_sorted));
André Anjos's avatar
André Anjos committed
381

382
  return Py_BuildValue("d", result);
Amir Mohammadi's avatar
lint  
Amir Mohammadi committed
383
  BOB_CATCH_FUNCTION("eer_threshold", 0)
André Anjos's avatar
André Anjos committed
384 385
}

Amir Mohammadi's avatar
lint  
Amir Mohammadi committed
386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423
static auto min_weighted_error_rate_threshold_doc =
    bob::extension::FunctionDoc(
        "min_weighted_error_rate_threshold", "Calculates the threshold that "
                                             "minimizes the error rate for the "
                                             "given input data",
        "The ``cost`` parameter determines the relative importance between "
        "false-accepts and false-rejections. "
        "This number should be between 0 and 1 and will be clipped to those "
        "extremes. "
        "The value to minimize becomes: :math:`ER_{cost} = cost * FAR + "
        "(1-cost) * FRR`. "
        "The higher the cost, the higher the importance given to **not** "
        "making mistakes classifying negatives/noise/impostors.\n\n"
        ".. note:: "
        "The scores will be sorted internally, requiring the scores to be "
        "copied. "
        "To avoid this copy, you can sort both sets of scores externally in "
        "ascendant order, and set the ``is_sorted`` parameter to ``True``")
        .add_prototype("negatives, positives, cost, [is_sorted]", "threshold")
        .add_parameter(
            "negatives, positives", "array_like(1D, float)",
            "The set of negative and positive scores to compute the threshold")
        .add_parameter("cost", "float", "The relative cost over FAR with "
                                        "respect to FRR in the threshold "
                                        "calculation")
        .add_parameter("is_sorted", "bool", "[Default: ``False``] Are both "
                                            "sets of scores already in "
                                            "ascendantly sorted order?")
        .add_return(
            "threshold", "float",
            "The threshold for which the weighted error rate is minimal");
static PyObject *min_weighted_error_rate_threshold(PyObject *, PyObject *args,
                                                   PyObject *kwds) {
  BOB_TRY
  char **kwlist = min_weighted_error_rate_threshold_doc.kwlist();

  PyBlitzArrayObject *neg;
  PyBlitzArrayObject *pos;
424
  double cost;
Amir Mohammadi's avatar
lint  
Amir Mohammadi committed
425
  PyObject *is_sorted = Py_False;
André Anjos's avatar
André Anjos committed
426

Amir Mohammadi's avatar
lint  
Amir Mohammadi committed
427 428 429 430
  if (!PyArg_ParseTupleAndKeywords(
          args, kwds, "O&O&d|O", kwlist, &double1d_converter, &neg,
          &double1d_converter, &pos, &cost, &is_sorted))
    return 0;
André Anjos's avatar
André Anjos committed
431

Amir Mohammadi's avatar
lint  
Amir Mohammadi committed
432
  // protects acquired resources through this scope
433 434 435
  auto neg_ = make_safe(neg);
  auto pos_ = make_safe(pos);

André Anjos's avatar
André Anjos committed
436
  double result = bob::measure::minWeightedErrorRateThreshold(
Amir Mohammadi's avatar
lint  
Amir Mohammadi committed
437 438
      *PyBlitzArrayCxx_AsBlitz<double, 1>(neg),
      *PyBlitzArrayCxx_AsBlitz<double, 1>(pos), cost,
439
      PyObject_IsTrue(is_sorted));
André Anjos's avatar
André Anjos committed
440

441
  return Py_BuildValue("d", result);
Amir Mohammadi's avatar
lint  
Amir Mohammadi committed
442
  BOB_CATCH_FUNCTION("min_weighted_error_rate_threshold", 0)
André Anjos's avatar
André Anjos committed
443 444
}

Amir Mohammadi's avatar
lint  
Amir Mohammadi committed
445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474
static auto min_hter_threshold_doc =
    bob::extension::FunctionDoc("min_hter_threshold",
                                "Calculates the "
                                ":py:func:`bob.measure.min_weighted_error_rate_"
                                "threshold` with ``cost=0.5``")
        .add_prototype("negatives, positives, [is_sorted]", "threshold")
        .add_parameter(
            "negatives, positives", "array_like(1D, float)",
            "The set of negative and positive scores to compute the threshold")
        .add_parameter("is_sorted", "bool", "[Default: ``False``] Are both "
                                            "sets of scores already in "
                                            "ascendantly sorted order?")
        .add_return(
            "threshold", "float",
            "The threshold for which the weighted error rate is minimal");
static PyObject *min_hter_threshold(PyObject *, PyObject *args,
                                    PyObject *kwds) {
  BOB_TRY
  char **kwlist = min_hter_threshold_doc.kwlist();

  PyBlitzArrayObject *neg;
  PyBlitzArrayObject *pos;
  PyObject *is_sorted = Py_False;

  if (!PyArg_ParseTupleAndKeywords(args, kwds, "O&O&|O", kwlist,
                                   &double1d_converter, &neg,
                                   &double1d_converter, &pos, &is_sorted))
    return 0;

  // protects acquired resources through this scope
475 476 477
  auto neg_ = make_safe(neg);
  auto pos_ = make_safe(pos);

André Anjos's avatar
André Anjos committed
478
  double result = bob::measure::minHterThreshold(
Amir Mohammadi's avatar
lint  
Amir Mohammadi committed
479 480
      *PyBlitzArrayCxx_AsBlitz<double, 1>(neg),
      *PyBlitzArrayCxx_AsBlitz<double, 1>(pos), PyObject_IsTrue(is_sorted));
André Anjos's avatar
André Anjos committed
481

482
  return Py_BuildValue("d", result);
Amir Mohammadi's avatar
lint  
Amir Mohammadi committed
483
  BOB_CATCH_FUNCTION("min_hter_threshold", 0)
André Anjos's avatar
André Anjos committed
484 485
}

Amir Mohammadi's avatar
lint  
Amir Mohammadi committed
486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519
static auto precision_recall_doc =
    bob::extension::FunctionDoc(
        "precision_recall", "Calculates the precision and recall "
                            "(sensitiveness) values given negative and "
                            "positive scores and a threshold",
        "Precision and recall are computed as:\n\n"
        ".. math::\n\n"
        "   \\mathrm{precision} = \\frac{tp}{tp + fp}\n\n"
        "   \\mathrm{recall} = \\frac{tp}{tp + fn}\n\n"
        "where :math:`tp` are the true positives, :math:`fp` are the false "
        "positives and :math:`fn` are the false negatives.\n\n"
        "``positives`` holds the score information for samples that are "
        "labeled to belong to a certain class (a.k.a., 'signal' or 'client'). "
        "``negatives`` holds the score information for samples that are "
        "labeled **not** to belong to the class (a.k.a., 'noise' or "
        "'impostor'). "
        "For more precise details about how the method considers error rates, "
        "see :py:func:`bob.measure.farfrr`.")
        .add_prototype("negatives, positives, threshold", "precision, recall")
        .add_parameter("negatives, positives", "array_like(1D, float)",
                       "The set of negative and positive scores to compute the "
                       "measurements")
        .add_parameter("threshold", "float",
                       "The threshold to compute the measures for")
        .add_return("precision", "float",
                    "The precision value for the given negatives and positives")
        .add_return("recall", "float",
                    "The recall value for the given negatives and positives");
static PyObject *precision_recall(PyObject *, PyObject *args, PyObject *kwds) {
  BOB_TRY
  static char **kwlist = precision_recall_doc.kwlist();

  PyBlitzArrayObject *neg;
  PyBlitzArrayObject *pos;
520
  double threshold;
André Anjos's avatar
André Anjos committed
521

Amir Mohammadi's avatar
lint  
Amir Mohammadi committed
522 523 524 525
  if (!PyArg_ParseTupleAndKeywords(args, kwds, "O&O&d", kwlist,
                                   &double1d_converter, &neg,
                                   &double1d_converter, &pos, &threshold))
    return 0;
André Anjos's avatar
André Anjos committed
526

Amir Mohammadi's avatar
lint  
Amir Mohammadi committed
527
  // protects acquired resources through this scope
528 529 530
  auto neg_ = make_safe(neg);
  auto pos_ = make_safe(pos);

André Anjos's avatar
André Anjos committed
531
  auto result = bob::measure::precision_recall(
Amir Mohammadi's avatar
lint  
Amir Mohammadi committed
532 533
      *PyBlitzArrayCxx_AsBlitz<double, 1>(neg),
      *PyBlitzArrayCxx_AsBlitz<double, 1>(pos), threshold);
André Anjos's avatar
André Anjos committed
534

535
  return Py_BuildValue("dd", result.first, result.second);
Amir Mohammadi's avatar
lint  
Amir Mohammadi committed
536
  BOB_CATCH_FUNCTION("precision_recall", 0)
André Anjos's avatar
André Anjos committed
537 538
}

Amir Mohammadi's avatar
lint  
Amir Mohammadi committed
539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570
static auto f_score_doc =
    bob::extension::FunctionDoc(
        "f_score", "This method computes the F-score of the accuracy of the "
                   "classification",
        "The F-score is a weighted mean of precision and recall measurements, "
        "see :py:func:`bob.measure.precision_recall`. "
        "It is computed as:\n\n"
        ".. math::\n\n"
        "    \\mathrm{f-score} = (1 + "
        "w^2)\\frac{\\mathrm{precision}\\cdot{}\\mathrm{recall}}{w^2\\cdot{}"
        "\\mathrm{precision} + \\mathrm{recall}}\n\n"
        "The weight :math:`w` needs to be non-negative real value. "
        "In case the weight parameter is 1 (the default), the F-score is "
        "called F1 score and is a harmonic mean between precision and recall "
        "values.")
        .add_prototype("negatives, positives, threshold, [weight]", "f_score")
        .add_parameter("negatives, positives", "array_like(1D, float)",
                       "The set of negative and positive scores to compute the "
                       "precision and recall")
        .add_parameter("threshold", "float",
                       "The threshold to compute the precision and recall for")
        .add_parameter("weight", "float", "[Default: ``1``] The weight "
                                          ":math:`w` between precision and "
                                          "recall")
        .add_return("f_score", "float", "The computed f-score for the given "
                                        "scores and the given threshold");
static PyObject *f_score(PyObject *, PyObject *args, PyObject *kwds) {
  BOB_TRY
  static char **kwlist = f_score_doc.kwlist();

  PyBlitzArrayObject *neg;
  PyBlitzArrayObject *pos;
571
  double threshold;
André Anjos's avatar
André Anjos committed
572 573
  double weight = 1.0;

Amir Mohammadi's avatar
lint  
Amir Mohammadi committed
574 575 576 577
  if (!PyArg_ParseTupleAndKeywords(
          args, kwds, "O&O&d|d", kwlist, &double1d_converter, &neg,
          &double1d_converter, &pos, &threshold, &weight))
    return 0;
André Anjos's avatar
André Anjos committed
578

Amir Mohammadi's avatar
lint  
Amir Mohammadi committed
579
  // protects acquired resources through this scope
580 581 582
  auto neg_ = make_safe(neg);
  auto pos_ = make_safe(pos);

Amir Mohammadi's avatar
lint  
Amir Mohammadi committed
583 584 585
  auto result = bob::measure::f_score(*PyBlitzArrayCxx_AsBlitz<double, 1>(neg),
                                      *PyBlitzArrayCxx_AsBlitz<double, 1>(pos),
                                      threshold, weight);
André Anjos's avatar
André Anjos committed
586

Amir Mohammadi's avatar
lint  
Amir Mohammadi committed
587 588
  return Py_BuildValue("d", result);
  BOB_CATCH_FUNCTION("f_score", 0)
André Anjos's avatar
André Anjos committed
589 590
}

Amir Mohammadi's avatar
lint  
Amir Mohammadi committed
591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613
static auto correctly_classified_negatives_doc =
    bob::extension::FunctionDoc(
        "correctly_classified_negatives",
        "This method returns an array composed of booleans that pin-point, "
        "which negatives where correctly classified for the given threshold",
        "The pseudo-code for this function is:\n\n"
        "  ``foreach (k in negatives) if negatives[k] < threshold: "
        "classified[k] = true else: classified[k] = false``")
        .add_prototype("negatives, threshold", "classified")
        .add_parameter(
            "negatives", "array_like(1D, float)",
            "The scores generated by comparing objects of different classes")
        .add_parameter("threshold", "float", "The threshold, for which scores "
                                             "should be considered to be "
                                             "correctly classified")
        .add_return("classified", "array_like(1D, bool)",
                    "The decision for each of the ``negatives``");
static PyObject *correctly_classified_negatives(PyObject *, PyObject *args,
                                                PyObject *kwds) {
  BOB_TRY
  static char **kwlist = correctly_classified_negatives_doc.kwlist();

  PyBlitzArrayObject *neg;
614
  double threshold;
André Anjos's avatar
André Anjos committed
615

Amir Mohammadi's avatar
lint  
Amir Mohammadi committed
616 617 618
  if (!PyArg_ParseTupleAndKeywords(args, kwds, "O&d", kwlist,
                                   &double1d_converter, &neg, &threshold))
    return 0;
André Anjos's avatar
André Anjos committed
619

Amir Mohammadi's avatar
lint  
Amir Mohammadi committed
620
  // protects acquired resources through this scope
621 622
  auto neg_ = make_safe(neg);

André Anjos's avatar
André Anjos committed
623
  auto result = bob::measure::correctlyClassifiedNegatives(
Amir Mohammadi's avatar
lint  
Amir Mohammadi committed
624
      *PyBlitzArrayCxx_AsBlitz<double, 1>(neg), threshold);
André Anjos's avatar
André Anjos committed
625

626
  return PyBlitzArrayCxx_AsNumpy(result);
Amir Mohammadi's avatar
lint  
Amir Mohammadi committed
627
  BOB_CATCH_FUNCTION("correctly_classified_negatives", 0)
André Anjos's avatar
André Anjos committed
628 629
}

Amir Mohammadi's avatar
lint  
Amir Mohammadi committed
630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652
static auto correctly_classified_positives_doc =
    bob::extension::FunctionDoc(
        "correctly_classified_positives",
        "This method returns an array composed of booleans that pin-point, "
        "which positives where correctly classified for the given threshold",
        "The pseudo-code for this function is:\n\n"
        "  ``foreach (k in positives) if positives[k] >= threshold: "
        "classified[k] = true else: classified[k] = false``")
        .add_prototype("positives, threshold", "classified")
        .add_parameter(
            "positives", "array_like(1D, float)",
            "The scores generated by comparing objects of the same classes")
        .add_parameter("threshold", "float", "The threshold, for which scores "
                                             "should be considered to be "
                                             "correctly classified")
        .add_return("classified", "array_like(1D, bool)",
                    "The decision for each of the ``positives``");
static PyObject *correctly_classified_positives(PyObject *, PyObject *args,
                                                PyObject *kwds) {
  BOB_TRY
  static char **kwlist = correctly_classified_positives_doc.kwlist();

  PyBlitzArrayObject *pos;
653
  double threshold;
André Anjos's avatar
André Anjos committed
654

Amir Mohammadi's avatar
lint  
Amir Mohammadi committed
655 656 657
  if (!PyArg_ParseTupleAndKeywords(args, kwds, "O&d", kwlist,
                                   &double1d_converter, &pos, &threshold))
    return 0;
André Anjos's avatar
André Anjos committed
658

Amir Mohammadi's avatar
lint  
Amir Mohammadi committed
659
  // protects acquired resources through this scope
660 661
  auto pos_ = make_safe(pos);

André Anjos's avatar
André Anjos committed
662
  auto result = bob::measure::correctlyClassifiedPositives(
Amir Mohammadi's avatar
lint  
Amir Mohammadi committed
663
      *PyBlitzArrayCxx_AsBlitz<double, 1>(pos), threshold);
André Anjos's avatar
André Anjos committed
664

665
  return PyBlitzArrayCxx_AsNumpy(result);
Amir Mohammadi's avatar
lint  
Amir Mohammadi committed
666
  BOB_CATCH_FUNCTION("correctly_classified_positives", 0)
André Anjos's avatar
André Anjos committed
667 668
}

Amir Mohammadi's avatar
lint  
Amir Mohammadi committed
669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693
static auto precision_recall_curve_doc =
    bob::extension::FunctionDoc(
        "precision_recall_curve", "Calculates the precision-recall curve given "
                                  "a set of positive and negative scores and a "
                                  "number of desired points",
        "The points in which the curve is calculated are distributed uniformly "
        "in the range ``[min(negatives, positives), max(negatives, "
        "positives)]``")
        .add_prototype("negatives, positives, n_points", "curve")
        .add_parameter("negatives, positives", "array_like(1D, float)",
                       "The set of negative and positive scores to compute the "
                       "measurements")
        .add_parameter("n_points", "int", "The number of thresholds for which "
                                          "precision and recall should be "
                                          "evaluated")
        .add_return("curve", "array_like(2D, float)",
                    "2D array of floats that express the X (precision) and Y "
                    "(recall) coordinates");
static PyObject *precision_recall_curve(PyObject *, PyObject *args,
                                        PyObject *kwds) {
  BOB_TRY
  char **kwlist = precision_recall_curve_doc.kwlist();

  PyBlitzArrayObject *neg;
  PyBlitzArrayObject *pos;
694
  Py_ssize_t n_points;
André Anjos's avatar
André Anjos committed
695

Amir Mohammadi's avatar
lint  
Amir Mohammadi committed
696 697 698 699
  if (!PyArg_ParseTupleAndKeywords(args, kwds, "O&O&n", kwlist,
                                   &double1d_converter, &neg,
                                   &double1d_converter, &pos, &n_points))
    return 0;
André Anjos's avatar
André Anjos committed
700

Amir Mohammadi's avatar
lint  
Amir Mohammadi committed
701
  // protects acquired resources through this scope
702 703 704
  auto neg_ = make_safe(neg);
  auto pos_ = make_safe(pos);

André Anjos's avatar
André Anjos committed
705
  auto result = bob::measure::precision_recall_curve(
Amir Mohammadi's avatar
lint  
Amir Mohammadi committed
706 707
      *PyBlitzArrayCxx_AsBlitz<double, 1>(neg),
      *PyBlitzArrayCxx_AsBlitz<double, 1>(pos), n_points);
André Anjos's avatar
André Anjos committed
708

709
  return PyBlitzArrayCxx_AsNumpy(result);
Amir Mohammadi's avatar
lint  
Amir Mohammadi committed
710
  BOB_CATCH_FUNCTION("precision_recall_curve", 0)
André Anjos's avatar
André Anjos committed
711 712
}

Amir Mohammadi's avatar
lint  
Amir Mohammadi committed
713 714 715
static auto far_threshold_doc =
    bob::extension::FunctionDoc(
        "far_threshold", "Computes the threshold such that the real FAR is "
716 717 718 719 720
                         "**at least** the requested ``far_value`` if possible",
        "If no such threshold can be computed, ``NaN`` is returned. It is "
        "impossible to compute the threshold, when too few non-identical "
        "highest scores exist, so that the desired ``far_value`` cannot be "
        "reached by any threshold.\n\n"
Amir Mohammadi's avatar
lint  
Amir Mohammadi committed
721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751
        ".. note::\n\n"
        "   The scores will be sorted internally, requiring the scores to be "
        "copied.\n"
        "   To avoid this copy, you can sort the ``negatives`` scores "
        "externally in ascendant order, and set the ``is_sorted`` parameter to "
        "``True``")
        .add_prototype("negatives, positives, [far_value], [is_sorted]",
                       "threshold")
        .add_parameter(
            "negatives", "array_like(1D, float)",
            "The set of negative scores to compute the FAR threshold")
        .add_parameter(
            "positives", "array_like(1D, float)",
            "Ignored, but needs to be specified -- may be given as ``[]``")
        .add_parameter("far_value", "float", "[Default: ``0.001``] The FAR "
                                             "value, for which the threshold "
                                             "should be computed")
        .add_parameter("is_sorted", "bool",
                       "[Default: ``False``] Set this to ``True`` if the "
                       "``negatives`` are already sorted in ascending order. "
                       "If ``False``, scores will be sorted internally, which "
                       "will require more memory")
        .add_return(
            "threshold", "float",
            "The threshold such that the real FAR is at least ``far_value``");
static PyObject *far_threshold(PyObject *, PyObject *args, PyObject *kwds) {
  BOB_TRY
  static char **kwlist = far_threshold_doc.kwlist();

  PyBlitzArrayObject *neg;
  PyBlitzArrayObject *pos;
André Anjos's avatar
André Anjos committed
752
  double far_value = 0.001;
Amir Mohammadi's avatar
lint  
Amir Mohammadi committed
753
  PyObject *is_sorted = Py_False;
André Anjos's avatar
André Anjos committed
754

Amir Mohammadi's avatar
lint  
Amir Mohammadi committed
755 756 757 758
  if (!PyArg_ParseTupleAndKeywords(
          args, kwds, "O&O&|dO", kwlist, &double1d_converter, &neg,
          &double1d_converter, &pos, &far_value, is_sorted))
    return 0;
André Anjos's avatar
André Anjos committed
759

Amir Mohammadi's avatar
lint  
Amir Mohammadi committed
760
  // protects acquired resources through this scope
761 762 763
  auto neg_ = make_safe(neg);
  auto pos_ = make_safe(pos);

Amir Mohammadi's avatar
lint  
Amir Mohammadi committed
764 765 766 767
  auto result =
      bob::measure::farThreshold(*PyBlitzArrayCxx_AsBlitz<double, 1>(neg),
                                 *PyBlitzArrayCxx_AsBlitz<double, 1>(pos),
                                 far_value, PyObject_IsTrue(is_sorted));
André Anjos's avatar
André Anjos committed
768

769
  return Py_BuildValue("d", result);
Amir Mohammadi's avatar
lint  
Amir Mohammadi committed
770
  BOB_CATCH_FUNCTION("far_threshold", 0)
André Anjos's avatar
André Anjos committed
771 772
}

Amir Mohammadi's avatar
lint  
Amir Mohammadi committed
773 774 775
static auto frr_threshold_doc =
    bob::extension::FunctionDoc(
        "frr_threshold", "Computes the threshold such that the real FRR is "
776 777 778 779 780
                         "**at least** the requested ``frr_value`` if possible",
        "If no such threshold can be computed, ``NaN`` is returned. It is "
        "impossible to compute the threshold, when too few non-identical "
        "lowest scores exist, so that the desired ``frr_value`` cannot be "
        "reached by any threshold.\n\n"
Amir Mohammadi's avatar
lint  
Amir Mohammadi committed
781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811
        ".. note::\n\n"
        "   The scores will be sorted internally, requiring the scores to be "
        "copied.\n"
        "   To avoid this copy, you can sort the ``positives`` scores "
        "externally in ascendant order, and set the ``is_sorted`` parameter to "
        "``True``")
        .add_prototype("negatives, positives, [frr_value], [is_sorted]",
                       "threshold")
        .add_parameter(
            "negatives", "array_like(1D, float)",
            "Ignored, but needs to be specified -- may be given as ``[]``")
        .add_parameter(
            "positives", "array_like(1D, float)",
            "The set of positive scores to compute the FRR threshold")
        .add_parameter("frr_value", "float", "[Default: ``0.001``] The FRR "
                                             "value, for which the threshold "
                                             "should be computed")
        .add_parameter("is_sorted", "bool",
                       "[Default: ``False``] Set this to ``True`` if the "
                       "``positives`` are already sorted in ascendant order. "
                       "If ``False``, scores will be sorted internally, which "
                       "will require more memory")
        .add_return(
            "threshold", "float",
            "The threshold such that the real FRR is at least ``frr_value``");
static PyObject *frr_threshold(PyObject *, PyObject *args, PyObject *kwds) {
  BOB_TRY
  char **kwlist = frr_threshold_doc.kwlist();

  PyBlitzArrayObject *neg;
  PyBlitzArrayObject *pos;
André Anjos's avatar
André Anjos committed
812
  double frr_value = 0.001;
Amir Mohammadi's avatar
lint  
Amir Mohammadi committed
813
  PyObject *is_sorted = Py_False;
André Anjos's avatar
André Anjos committed
814

Amir Mohammadi's avatar
lint  
Amir Mohammadi committed
815 816 817 818
  if (!PyArg_ParseTupleAndKeywords(
          args, kwds, "O&O&|dO", kwlist, &double1d_converter, &neg,
          &double1d_converter, &pos, &frr_value, &is_sorted))
    return 0;
André Anjos's avatar
André Anjos committed
819

Amir Mohammadi's avatar
lint  
Amir Mohammadi committed
820
  // protects acquired resources through this scope
821 822 823
  auto neg_ = make_safe(neg);
  auto pos_ = make_safe(pos);

Amir Mohammadi's avatar
lint  
Amir Mohammadi committed
824 825 826 827
  auto result =
      bob::measure::frrThreshold(*PyBlitzArrayCxx_AsBlitz<double, 1>(neg),
                                 *PyBlitzArrayCxx_AsBlitz<double, 1>(pos),
                                 frr_value, PyObject_IsTrue(is_sorted));
André Anjos's avatar
André Anjos committed
828

829
  return Py_BuildValue("d", result);
Amir Mohammadi's avatar
lint  
Amir Mohammadi committed
830
  BOB_CATCH_FUNCTION("frr_threshold", 0)
André Anjos's avatar
André Anjos committed
831 832
}

Amir Mohammadi's avatar
lint  
Amir Mohammadi committed
833 834 835 836 837 838 839 840 841 842 843 844 845 846
static auto eer_rocch_doc =
    bob::extension::FunctionDoc(
        "eer_rocch", "Calculates the equal-error-rate (EER) given the input "
                     "data, on the ROC Convex Hull (ROCCH)",
        "It replicates the EER calculation from the Bosaris toolkit "
        "(https://sites.google.com/site/bosaristoolkit/).")
        .add_prototype("negatives, positives", "threshold")
        .add_parameter(
            "negatives, positives", "array_like(1D, float)",
            "The set of negative and positive scores to compute the threshold")
        .add_return("threshold", "float",
                    "The threshold for the equal error rate");
static PyObject *eer_rocch(PyObject *, PyObject *args, PyObject *kwds) {
  BOB_TRY
André Anjos's avatar
André Anjos committed
847
  /* Parses input arguments in a single shot */
Amir Mohammadi's avatar
lint  
Amir Mohammadi committed
848
  char **kwlist = eer_rocch_doc.kwlist();
André Anjos's avatar
André Anjos committed
849

Amir Mohammadi's avatar
lint  
Amir Mohammadi committed
850 851
  PyBlitzArrayObject *neg;
  PyBlitzArrayObject *pos;
André Anjos's avatar
André Anjos committed
852

Amir Mohammadi's avatar
lint  
Amir Mohammadi committed
853 854 855 856
  if (!PyArg_ParseTupleAndKeywords(args, kwds, "O&O&", kwlist,
                                   &double1d_converter, &neg,
                                   &double1d_converter, &pos))
    return 0;
André Anjos's avatar
André Anjos committed
857

Amir Mohammadi's avatar
lint  
Amir Mohammadi committed
858
  // protects acquired resources through this scope
859 860 861
  auto neg_ = make_safe(neg);
  auto pos_ = make_safe(pos);

Amir Mohammadi's avatar
lint  
Amir Mohammadi committed
862 863 864
  auto result =
      bob::measure::eerRocch(*PyBlitzArrayCxx_AsBlitz<double, 1>(neg),
                             *PyBlitzArrayCxx_AsBlitz<double, 1>(pos));
André Anjos's avatar
André Anjos committed
865

866
  return Py_BuildValue("d", result);
Amir Mohammadi's avatar
lint  
Amir Mohammadi committed
867
  BOB_CATCH_FUNCTION("eer_rocch", 0)
André Anjos's avatar
André Anjos committed
868 869
}

Amir Mohammadi's avatar
lint  
Amir Mohammadi committed
870 871 872 873 874 875 876 877 878 879 880 881 882
static auto rocch_doc =
    bob::extension::FunctionDoc("rocch", "Calculates the ROC Convex Hull "
                                         "(ROCCH) curve given a set of "
                                         "positive and negative scores")
        .add_prototype("negatives, positives", "curve")
        .add_parameter(
            "negatives, positives", "array_like(1D, float)",
            "The set of negative and positive scores to compute the curve")
        .add_return("curve", "array_like(2D, float)",
                    "The ROC curve, with the first row containing the FAR, and "
                    "the second row containing the FRR");
static PyObject *rocch(PyObject *, PyObject *args, PyObject *kwds) {
  BOB_TRY
André Anjos's avatar
André Anjos committed
883
  /* Parses input arguments in a single shot */
Amir Mohammadi's avatar
lint  
Amir Mohammadi committed
884
  char **kwlist = rocch_doc.kwlist();
André Anjos's avatar
André Anjos committed
885

Amir Mohammadi's avatar
lint  
Amir Mohammadi committed
886 887
  PyBlitzArrayObject *neg;
  PyBlitzArrayObject *pos;
André Anjos's avatar
André Anjos committed
888

Amir Mohammadi's avatar
lint  
Amir Mohammadi committed
889 890 891 892
  if (!PyArg_ParseTupleAndKeywords(args, kwds, "O&O&", kwlist,
                                   &double1d_converter, &neg,
                                   &double1d_converter, &pos))
    return 0;
André Anjos's avatar
André Anjos committed
893

Amir Mohammadi's avatar
lint  
Amir Mohammadi committed
894
  // protects acquired resources through this scope
895 896 897
  auto neg_ = make_safe(neg);
  auto pos_ = make_safe(pos);

Amir Mohammadi's avatar
lint  
Amir Mohammadi committed
898 899
  auto result = bob::measure::rocch(*PyBlitzArrayCxx_AsBlitz<double, 1>(neg),
                                    *PyBlitzArrayCxx_AsBlitz<double, 1>(pos));
André Anjos's avatar
André Anjos committed
900

901
  return PyBlitzArrayCxx_AsNumpy(result);
Amir Mohammadi's avatar
lint  
Amir Mohammadi committed
902
  BOB_CATCH_FUNCTION("rocch", 0)
André Anjos's avatar
André Anjos committed
903 904
}

Amir Mohammadi's avatar
lint  
Amir Mohammadi committed
905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923
static auto rocch2eer_doc =
    bob::extension::FunctionDoc(
        "rocch2eer", "Calculates the threshold that is as close as possible to "
                     "the equal-error-rate (EER) given the input data")
        .add_prototype("pmiss_pfa", "threshold")
        // I don't know, what the pmiss_pfa parameter is, so I leave out its
        // documentation (a .. todo:: will be generated automatically)
        //.add_parameter("pmiss_pfa", "array_like(2D, float)", "???")
        .add_return("threshold", "float",
                    "The computed threshold, at which the EER can be obtained");
static PyObject *rocch2eer(PyObject *, PyObject *args, PyObject *kwds) {
  BOB_TRY
  static char **kwlist = rocch2eer_doc.kwlist();

  PyBlitzArrayObject *p;

  if (!PyArg_ParseTupleAndKeywords(args, kwds, "O&", kwlist,
                                   &double2d_converter, &p))
    return 0;
André Anjos's avatar
André Anjos committed
924

925
  auto p_ = make_safe(p);
André Anjos's avatar
André Anjos committed
926

Amir Mohammadi's avatar
lint  
Amir Mohammadi committed
927
  auto result = bob::measure::rocch2eer(*PyBlitzArrayCxx_AsBlitz<double, 2>(p));
André Anjos's avatar
André Anjos committed
928

929
  return Py_BuildValue("d", result);
Amir Mohammadi's avatar
lint  
Amir Mohammadi committed
930
  BOB_CATCH_FUNCTION("rocch2eer", 0)
André Anjos's avatar
André Anjos committed
931 932
}

Amir Mohammadi's avatar
lint  
Amir Mohammadi committed
933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959
static auto roc_for_far_doc =
    bob::extension::FunctionDoc(
        "roc_for_far", "Calculates the ROC curve for a given set of positive "
                       "and negative scores and the FAR values, for which the "
                       "FRR should be computed",
        ".. note::\n\n"
        "   The scores will be sorted internally, requiring the scores to be "
        "copied.\n"
        "   To avoid this copy, you can sort both sets of scores externally in "
        "ascendant order, and set the ``is_sorted`` parameter to ``True``")
        .add_prototype("negatives, positives, far_list, [is_sorted]", "curve")
        .add_parameter(
            "negatives, positives", "array_like(1D, float)",
            "The set of negative and positive scores to compute the curve")
        .add_parameter(
            "far_list", "array_like(1D, float)",
            "A list of FAR values, for which the FRR values should be computed")
        .add_parameter("is_sorted", "bool",
                       "[Default: ``False``] Set this to ``True`` if both sets "
                       "of scores are already sorted in ascending order. If "
                       "``False``, scores will be sorted internally, which "
                       "will require more memory")
        .add_return("curve", "array_like(2D, float)",
                    "The ROC curve, which holds a copy of the given FAR values "
                    "in row 0, and the corresponding FRR values in row 1");
static PyObject *roc_for_far(PyObject *, PyObject *args, PyObject *kwds) {
  BOB_TRY
960
  /* Parses input arguments in a single shot */
Amir Mohammadi's avatar
lint  
Amir Mohammadi committed
961 962 963 964 965 966 967 968 969 970 971 972 973
  char **kwlist = roc_for_far_doc.kwlist();

  PyBlitzArrayObject *neg;
  PyBlitzArrayObject *pos;
  PyBlitzArrayObject *far;
  PyObject *is_sorted = Py_False;

  if (!PyArg_ParseTupleAndKeywords(
          args, kwds, "O&O&O&|O", kwlist, &double1d_converter, &neg,
          &double1d_converter, &pos, &double1d_converter, &far, &is_sorted))
    return 0;

  // protects acquired resources through this scope
974 975
  auto neg_ = make_safe(neg);
  auto pos_ = make_safe(pos);
976
  auto far_ = make_safe(far);
977

978
  auto result = bob::measure::roc_for_far(
Amir Mohammadi's avatar
lint  
Amir Mohammadi committed
979 980 981
      *PyBlitzArrayCxx_AsBlitz<double, 1>(neg),
      *PyBlitzArrayCxx_AsBlitz<double, 1>(pos),
      *PyBlitzArrayCxx_AsBlitz<double, 1>(far), PyObject_IsTrue(is_sorted));
982

983
  return PyBlitzArrayCxx_AsNumpy(result);
Amir Mohammadi's avatar
lint  
Amir Mohammadi committed
984
  BOB_CATCH_FUNCTION("roc_for_far", 0)
André Anjos's avatar
André Anjos committed
985 986
}

André Anjos's avatar
André Anjos committed
987
static PyMethodDef module_methods[] = {
Amir Mohammadi's avatar
lint  
Amir Mohammadi committed
988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029
    {epc_doc.name(), (PyCFunction)epc, METH_VARARGS | METH_KEYWORDS,
     epc_doc.doc()},
    {det_doc.name(), (PyCFunction)det, METH_VARARGS | METH_KEYWORDS,
     det_doc.doc()},
    {ppndf_doc.name(), (PyCFunction)ppndf, METH_VARARGS | METH_KEYWORDS,
     ppndf_doc.doc()},
    {roc_doc.name(), (PyCFunction)roc, METH_VARARGS | METH_KEYWORDS,
     roc_doc.doc()},
    {farfrr_doc.name(), (PyCFunction)farfrr, METH_VARARGS | METH_KEYWORDS,
     farfrr_doc.doc()},
    {eer_threshold_doc.name(), (PyCFunction)eer_threshold,
     METH_VARARGS | METH_KEYWORDS, eer_threshold_doc.doc()},
    {min_weighted_error_rate_threshold_doc.name(),
     (PyCFunction)min_weighted_error_rate_threshold,
     METH_VARARGS | METH_KEYWORDS, min_weighted_error_rate_threshold_doc.doc()},
    {min_hter_threshold_doc.name(), (PyCFunction)min_hter_threshold,
     METH_VARARGS | METH_KEYWORDS, min_hter_threshold_doc.doc()},
    {precision_recall_doc.name(), (PyCFunction)precision_recall,
     METH_VARARGS | METH_KEYWORDS, precision_recall_doc.doc()},
    {f_score_doc.name(), (PyCFunction)f_score, METH_VARARGS | METH_KEYWORDS,
     f_score_doc.doc()},
    {correctly_classified_negatives_doc.name(),
     (PyCFunction)correctly_classified_negatives, METH_VARARGS | METH_KEYWORDS,
     correctly_classified_negatives_doc.doc()},
    {correctly_classified_positives_doc.name(),
     (PyCFunction)correctly_classified_positives, METH_VARARGS | METH_KEYWORDS,
     correctly_classified_positives_doc.doc()},
    {precision_recall_curve_doc.name(), (PyCFunction)precision_recall_curve,
     METH_VARARGS | METH_KEYWORDS, precision_recall_curve_doc.doc()},
    {far_threshold_doc.name(), (PyCFunction)far_threshold,
     METH_VARARGS | METH_KEYWORDS, far_threshold_doc.doc()},
    {frr_threshold_doc.name(), (PyCFunction)frr_threshold,
     METH_VARARGS | METH_KEYWORDS, frr_threshold_doc.doc()},
    {eer_rocch_doc.name(), (PyCFunction)eer_rocch, METH_VARARGS | METH_KEYWORDS,
     eer_rocch_doc.doc()},
    {rocch_doc.name(), (PyCFunction)rocch, METH_VARARGS | METH_KEYWORDS,
     rocch_doc.doc()},
    {rocch2eer_doc.name(), (PyCFunction)rocch2eer, METH_VARARGS | METH_KEYWORDS,
     rocch2eer_doc.doc()},
    {roc_for_far_doc.name(), (PyCFunction)roc_for_far,
     METH_VARARGS | METH_KEYWORDS, roc_for_far_doc.doc()},
    {0} /* Sentinel */
1030
};
André Anjos's avatar
André Anjos committed
1031

André Anjos's avatar
André Anjos committed
1032 1033 1034
PyDoc_STRVAR(module_docstr, "Bob metrics and performance figures");

#if PY_VERSION_HEX >= 0x03000000
Amir Mohammadi's avatar
lint  
Amir Mohammadi committed
1035 1036 1037 1038 1039 1040 1041 1042 1043
static PyModuleDef module_definition = {PyModuleDef_HEAD_INIT,
                                        BOB_EXT_MODULE_NAME,
                                        module_docstr,
                                        -1,
                                        module_methods,
                                        0,
                                        0,
                                        0,
                                        0};
André Anjos's avatar
André Anjos committed
1044 1045
#endif

Amir Mohammadi's avatar
lint  
Amir Mohammadi committed
1046
static PyObject *create_module(void) {
André Anjos's avatar
André Anjos committed
1047

Amir Mohammadi's avatar
lint  
Amir Mohammadi committed
1048 1049
#if PY_VERSION_HEX >= 0x03000000
  PyObject *m = PyModule_Create(&module_definition);
1050
  auto m_ = make_xsafe(m);
Amir Mohammadi's avatar
lint  
Amir Mohammadi committed
1051 1052 1053 1054 1055 1056 1057 1058
  const char *ret = "O";
#else
  PyObject *m =
      Py_InitModule3(BOB_EXT_MODULE_NAME, module_methods, module_docstr);
  const char *ret = "N";
#endif
  if (!m)
    return 0;
André Anjos's avatar
André Anjos committed
1059

André Anjos's avatar
André Anjos committed
1060
  /* imports bob.blitz C-API + dependencies */
Amir Mohammadi's avatar
lint  
Amir Mohammadi committed
1061 1062 1063 1064 1065 1066
  if (import_bob_blitz() < 0)
    return 0;
  if (import_bob_core_logging() < 0)
    return 0;
  if (import_bob_io_base() < 0)
    return 0;
André Anjos's avatar
André Anjos committed
1067

1068
  return Py_BuildValue(ret, m);
André Anjos's avatar
André Anjos committed
1069 1070
}

Amir Mohammadi's avatar
lint  
Amir Mohammadi committed
1071 1072
PyMODINIT_FUNC BOB_EXT_ENTRY_NAME(void) {
#if PY_VERSION_HEX >= 0x03000000
André Anjos's avatar
André Anjos committed
1073
  return
Amir Mohammadi's avatar
lint  
Amir Mohammadi committed
1074 1075
#endif
      create_module();
André Anjos's avatar
André Anjos committed
1076
}