main.cpp 46.4 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
716
717
718
719
720
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
static auto far_threshold_doc =
    bob::extension::FunctionDoc(
        "far_threshold", "Computes the threshold such that the real FAR is "
                         "**at least** the requested ``far_value``",
        ".. 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
748
  double far_value = 0.001;
Amir Mohammadi's avatar
lint    
Amir Mohammadi committed
749
  PyObject *is_sorted = Py_False;
André Anjos's avatar
André Anjos committed
750

Amir Mohammadi's avatar
lint    
Amir Mohammadi committed
751
752
753
754
  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
755

Amir Mohammadi's avatar
lint    
Amir Mohammadi committed
756
  // protects acquired resources through this scope
757
758
759
  auto neg_ = make_safe(neg);
  auto pos_ = make_safe(pos);

Amir Mohammadi's avatar
lint    
Amir Mohammadi committed
760
761
762
763
  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
764

765
  return Py_BuildValue("d", result);
Amir Mohammadi's avatar
lint    
Amir Mohammadi committed
766
  BOB_CATCH_FUNCTION("far_threshold", 0)
André Anjos's avatar
André Anjos committed
767
768
}

Amir Mohammadi's avatar
lint    
Amir Mohammadi committed
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
static auto frr_threshold_doc =
    bob::extension::FunctionDoc(
        "frr_threshold", "Computes the threshold such that the real FRR is "
                         "**at least** the requested ``frr_value``",
        ".. 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
804
  double frr_value = 0.001;
Amir Mohammadi's avatar
lint    
Amir Mohammadi committed
805
  PyObject *is_sorted = Py_False;
André Anjos's avatar
André Anjos committed
806

Amir Mohammadi's avatar
lint    
Amir Mohammadi committed
807
808
809
810
  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
811

Amir Mohammadi's avatar
lint    
Amir Mohammadi committed
812
  // protects acquired resources through this scope
813
814
815
  auto neg_ = make_safe(neg);
  auto pos_ = make_safe(pos);

Amir Mohammadi's avatar
lint    
Amir Mohammadi committed
816
817
818
819
  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
820

821
  return Py_BuildValue("d", result);
Amir Mohammadi's avatar
lint    
Amir Mohammadi committed
822
  BOB_CATCH_FUNCTION("frr_threshold", 0)
André Anjos's avatar
André Anjos committed
823
824
}

Amir Mohammadi's avatar
lint    
Amir Mohammadi committed
825
826
827
828
829
830
831
832
833
834
835
836
837
838
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
839
  /* Parses input arguments in a single shot */
Amir Mohammadi's avatar
lint    
Amir Mohammadi committed
840
  char **kwlist = eer_rocch_doc.kwlist();
André Anjos's avatar
André Anjos committed
841

Amir Mohammadi's avatar
lint    
Amir Mohammadi committed
842
843
  PyBlitzArrayObject *neg;
  PyBlitzArrayObject *pos;
André Anjos's avatar
André Anjos committed
844

Amir Mohammadi's avatar
lint    
Amir Mohammadi committed
845
846
847
848
  if (!PyArg_ParseTupleAndKeywords(args, kwds, "O&O&", kwlist,
                                   &double1d_converter, &neg,
                                   &double1d_converter, &pos))
    return 0;
André Anjos's avatar
André Anjos committed
849

Amir Mohammadi's avatar
lint    
Amir Mohammadi committed
850
  // protects acquired resources through this scope
851
852
853
  auto neg_ = make_safe(neg);
  auto pos_ = make_safe(pos);

Amir Mohammadi's avatar
lint    
Amir Mohammadi committed
854
855
856
  auto result =
      bob::measure::eerRocch(*PyBlitzArrayCxx_AsBlitz<double, 1>(neg),
                             *PyBlitzArrayCxx_AsBlitz<double, 1>(pos));
André Anjos's avatar
André Anjos committed
857

858
  return Py_BuildValue("d", result);
Amir Mohammadi's avatar
lint    
Amir Mohammadi committed
859
  BOB_CATCH_FUNCTION("eer_rocch", 0)
André Anjos's avatar
André Anjos committed
860
861
}

Amir Mohammadi's avatar
lint    
Amir Mohammadi committed
862
863
864
865
866
867
868
869
870
871
872
873
874
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
875
  /* Parses input arguments in a single shot */
Amir Mohammadi's avatar
lint    
Amir Mohammadi committed
876
  char **kwlist = rocch_doc.kwlist();
André Anjos's avatar
André Anjos committed
877

Amir Mohammadi's avatar
lint    
Amir Mohammadi committed
878
879
  PyBlitzArrayObject *neg;
  PyBlitzArrayObject *pos;
André Anjos's avatar
André Anjos committed
880

Amir Mohammadi's avatar
lint    
Amir Mohammadi committed
881
882
883
884
  if (!PyArg_ParseTupleAndKeywords(args, kwds, "O&O&", kwlist,
                                   &double1d_converter, &neg,
                                   &double1d_converter, &pos))
    return 0;
André Anjos's avatar
André Anjos committed
885

Amir Mohammadi's avatar
lint    
Amir Mohammadi committed
886
  // protects acquired resources through this scope
887
888
889
  auto neg_ = make_safe(neg);
  auto pos_ = make_safe(pos);

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

893
  return PyBlitzArrayCxx_AsNumpy(result);
Amir Mohammadi's avatar
lint    
Amir Mohammadi committed
894
  BOB_CATCH_FUNCTION("rocch", 0)
André Anjos's avatar
André Anjos committed
895
896
}

Amir Mohammadi's avatar
lint    
Amir Mohammadi committed
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
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
916

917
  auto p_ = make_safe(p);
André Anjos's avatar
André Anjos committed
918

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

921
  return Py_BuildValue("d", result);
Amir Mohammadi's avatar
lint    
Amir Mohammadi committed
922
  BOB_CATCH_FUNCTION("rocch2eer", 0)
André Anjos's avatar
André Anjos committed
923
924
}

Amir Mohammadi's avatar
lint    
Amir Mohammadi committed
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
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
952
  /* Parses input arguments in a single shot */
Amir Mohammadi's avatar
lint    
Amir Mohammadi committed
953
954
955
956
957
958
959
960
961
962
963
964
965
  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
966
967
  auto neg_ = make_safe(neg);
  auto pos_ = make_safe(pos);
968
  auto far_ = make_safe(far);
969

970
  auto result = bob::measure::roc_for_far(
Amir Mohammadi's avatar
lint    
Amir Mohammadi committed
971
972
973
      *PyBlitzArrayCxx_AsBlitz<double, 1>(neg),
      *PyBlitzArrayCxx_AsBlitz<double, 1>(pos),
      *PyBlitzArrayCxx_AsBlitz<double, 1>(far), PyObject_IsTrue(is_sorted));
974

975
  return PyBlitzArrayCxx_AsNumpy(result);
Amir Mohammadi's avatar
lint    
Amir Mohammadi committed
976
  BOB_CATCH_FUNCTION("roc_for_far", 0)
André Anjos's avatar
André Anjos committed
977
978
}

André Anjos's avatar
André Anjos committed
979
static PyMethodDef module_methods[] = {
Amir Mohammadi's avatar
lint    
Amir Mohammadi committed
980
981
982
983
984
985
986
987
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
    {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 */
1022
};
André Anjos's avatar
André Anjos committed
1023

André Anjos's avatar
André Anjos committed
1024
1025
1026
PyDoc_STRVAR(module_docstr, "Bob metrics and performance figures");

#if PY_VERSION_HEX >= 0x03000000
Amir Mohammadi's avatar
lint    
Amir Mohammadi committed
1027
1028
1029
1030
1031
1032
1033
1034
1035
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
1036
1037
#endif

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

Amir Mohammadi's avatar
lint    
Amir Mohammadi committed
1040
1041
#if PY_VERSION_HEX >= 0x03000000
  PyObject *m = PyModule_Create(&module_definition);
1042
  auto m_ = make_xsafe(m);
Amir Mohammadi's avatar
lint    
Amir Mohammadi committed
1043
1044
1045
1046
1047
1048
1049
1050
  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
1051

André Anjos's avatar
André Anjos committed
1052
  /* imports bob.blitz C-API + dependencies */
Amir Mohammadi's avatar
lint    
Amir Mohammadi committed
1053
1054
1055
1056
1057
1058
  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
1059

1060
  return Py_BuildValue(ret, m);
André Anjos's avatar
André Anjos committed
1061
1062
}

Amir Mohammadi's avatar
lint    
Amir Mohammadi committed
1063
1064
PyMODINIT_FUNC BOB_EXT_ENTRY_NAME(void) {
#if PY_VERSION_HEX >= 0x03000000
André Anjos's avatar
André Anjos committed
1065
  return
Amir Mohammadi's avatar
lint    
Amir Mohammadi committed
1066
1067
#endif
      create_module();
André Anjos's avatar
André Anjos committed
1068
}