From 6b5858522b4229f7b2490286bd9becfcb3d612ee Mon Sep 17 00:00:00 2001
From: Manuel Guenther <manuel.guenther@idiap.ch>
Date: Wed, 6 May 2015 11:41:12 +0200
Subject: [PATCH] Added LDA algorithm and tests

---
 bob/bio/base/algorithm/LDA.py             | 183 ++++++++++++++++++++++
 bob/bio/base/algorithm/PCA.py             |   6 +-
 bob/bio/base/algorithm/__init__.py        |   1 +
 bob/bio/base/config/algorithm/lda.py      |  10 ++
 bob/bio/base/config/algorithm/pca_lda.py  |  11 ++
 bob/bio/base/test/data/lda_model.hdf5     | Bin 0 -> 2344 bytes
 bob/bio/base/test/data/lda_projected.hdf5 | Bin 0 -> 2184 bytes
 bob/bio/base/test/data/lda_projector.hdf5 | Bin 0 -> 16712 bytes
 bob/bio/base/test/test_algorithms.py      | 147 +++++++++--------
 setup.py                                  |   2 +
 10 files changed, 288 insertions(+), 72 deletions(-)
 create mode 100644 bob/bio/base/algorithm/LDA.py
 create mode 100644 bob/bio/base/config/algorithm/lda.py
 create mode 100644 bob/bio/base/config/algorithm/pca_lda.py
 create mode 100644 bob/bio/base/test/data/lda_model.hdf5
 create mode 100644 bob/bio/base/test/data/lda_projected.hdf5
 create mode 100644 bob/bio/base/test/data/lda_projector.hdf5

diff --git a/bob/bio/base/algorithm/LDA.py b/bob/bio/base/algorithm/LDA.py
new file mode 100644
index 00000000..bd3940f4
--- /dev/null
+++ b/bob/bio/base/algorithm/LDA.py
@@ -0,0 +1,183 @@
+#!/usr/bin/env python
+# vim: set fileencoding=utf-8 :
+# Manuel Guenther <Manuel.Guenther@idiap.ch>
+
+import bob.io.base
+import bob.learn.linear
+
+import numpy
+import scipy.spatial
+
+from .Algorithm import Algorithm
+
+import logging
+logger = logging.getLogger("bob.bio.base")
+
+class LDA (Algorithm):
+  """Tool for computing linear discriminant analysis (so-called Fisher faces)"""
+
+  def __init__(
+      self,
+      lda_subspace_dimension = 0, # if set, the LDA subspace will be truncated to the given number of dimensions; by default it is limited to the number of classes in the training set
+      pca_subspace_dimension = None, # if set, a PCA subspace truncation is performed before applying LDA; might be integral or float
+      distance_function = scipy.spatial.distance.euclidean,
+      is_distance_function = True,
+      uses_variances = False,
+      **kwargs  # parameters directly sent to the base class
+  ):
+    """Initializes the LDA tool with the given configuration"""
+
+    # call base class constructor and register that the LDA tool performs projection and need the training features split by client
+    Algorithm.__init__(
+        self,
+        performs_projection = True,
+        split_training_features_by_client = True,
+
+        lda_subspace_dimension = lda_subspace_dimension,
+        pca_subspace_dimension = pca_subspace_dimension,
+        distance_function = str(distance_function),
+        is_distance_function = is_distance_function,
+        uses_variances = uses_variances,
+
+        **kwargs
+    )
+
+    # copy information
+    self.pca_subspace = pca_subspace_dimension
+    self.lda_subspace = lda_subspace_dimension
+    if self.pca_subspace and isinstance(self.pca_subspace, int) and self.lda_subspace and self.pca_subspace < self.lda_subspace:
+      raise ValueError("The LDA subspace is larger than the PCA subspace size. This won't work properly. Please check your setup!")
+
+    self.machine = None
+    self.distance_function = distance_function
+    self.factor = -1 if is_distance_function else 1.
+    self.uses_variances = uses_variances
+
+
+  def _check_feature(self, feature):
+    """Checks that the features are appropriate"""
+    if not isinstance(feature, numpy.ndarray) or len(feature.shape) != 1 or feature.dtype != numpy.float64:
+      raise ValueError("The given feature is not appropriate")
+
+
+  def _arrange_data(self, training_files):
+    """Arranges the data to train the LDA projection matrix"""
+    data = []
+    for client_files in training_files:
+      # at least two files per client are required!
+      if len(client_files) < 2:
+        logger.warn("Skipping one client since the number of client files is only %d", len(client_files))
+        continue
+      data.append(numpy.vstack([feature.flatten() for feature in client_files]))
+
+    # Returns the list of lists of arrays
+    return data
+
+
+  def _train_pca(self, training_set):
+    """Trains and returns a LinearMachine that is trained using PCA"""
+    data_list = [feature for client in training_set for feature in client]
+    data = numpy.vstack(data_list)
+
+    logger.info("  -> Training Linear Machine using PCA")
+    t = bob.learn.linear.PCATrainer()
+    machine, eigen_values = t.train(data)
+
+    if isinstance(self.pca_subspace, float):
+      cummulated = numpy.cumsum(eigen_values) / numpy.sum(eigen_values)
+      for index in range(len(cummulated)):
+        if cummulated[index] > self.pca_subspace:
+          self.pca_subspace = index
+          break
+      self.pca_subspace = index
+
+    if self.lda_subspace and self.pca_subspace <= self.lda_subspace:
+      logger.warn("  ... Extending the PCA subspace dimension from %d to %d", self.pca_subspace, self.lda_subspace + 1)
+      self.pca_subspace = self.lda_subspace + 1
+    else:
+      logger.info("  ... Limiting PCA subspace to %d dimensions", self.pca_subspace)
+
+    # limit number of pcs
+    machine.resize(machine.shape[0], self.pca_subspace)
+    return machine
+
+
+  def _perform_pca(self, machine, training_set):
+    """Perform PCA on data of the training set"""
+    return [numpy.vstack([machine(feature) for feature in client_features]) for client_features in training_set]
+
+
+  def train_projector(self, training_features, projector_file):
+    """Generates the LDA projection matrix from the given features (that are sorted by identity)"""
+    # check data
+    [self._check_feature(feature) for client_features in training_features for feature in client_features]
+
+    # arrange LDA training data
+    data = self._arrange_data(training_features)
+
+    # train PCA of wanted
+    if self.pca_subspace:
+      # train on all training features
+      pca_machine = self._train_pca(training_features)
+      # project only the features that are used for training
+      logger.info("  -> Projecting training data to PCA subspace")
+      data = self._perform_pca(pca_machine, data)
+
+    logger.info("  -> Training Linear Machine using LDA")
+    trainer = bob.learn.linear.FisherLDATrainer(strip_to_rank = (self.lda_subspace == 0))
+    self.machine, self.variances = trainer.train(data)
+    if self.lda_subspace:
+      self.machine.resize(self.machine.shape[0], self.lda_subspace)
+      self.variances = self.variances.copy()
+      self.variances.resize(self.lda_subspace)
+
+    if self.pca_subspace:
+      # compute combined PCA/LDA projection matrix
+      combined_matrix = numpy.dot(pca_machine.weights, self.machine.weights)
+      # set new weight matrix (and new mean vector) of novel machine
+      self.machine = bob.learn.linear.Machine(combined_matrix)
+      self.machine.input_subtract = pca_machine.input_subtract
+
+    hdf5 = bob.io.base.HDF5File(projector_file, "w")
+    hdf5.set("Eigenvalues", self.variances)
+    hdf5.create_group("/Machine")
+    hdf5.cd("/Machine")
+    self.machine.save(hdf5)
+
+
+  def load_projector(self, projector_file):
+    """Reads the LDA projection matrix from file"""
+    # read LDA projector
+    hdf5 = bob.io.base.HDF5File(projector_file)
+    self.variances = hdf5.read("Eigenvalues")
+    hdf5.cd("/Machine")
+    self.machine = bob.learn.linear.Machine(hdf5)
+
+
+  def project(self, feature):
+    """Projects the data using the stored covariance matrix"""
+    self._check_feature(feature)
+    # Projects the data
+    return self.machine(feature)
+
+
+  def enroll(self, enroll_features):
+    """Enrolls the model by storing all given input vectors"""
+    assert len(enroll_features)
+    [self._check_feature(feature) for feature in enroll_features]
+    # just store all the features
+    return numpy.vstack(enroll_features)
+
+
+  def score(self, model, probe):
+    """Computes the distance of the model to the probe using the distance function"""
+    # return the negative distance (as a similarity measure)
+    if len(model.shape) == 2:
+      # we have multiple models, so we use the multiple model scoring
+      return self.score_for_multiple_models(model, probe)
+    elif self.uses_variances:
+      # single model, single probe (multiple probes have already been handled)
+      return self.factor * self.distance_function(model, probe, self.variances)
+    else:
+      # single model, single probe (multiple probes have already been handled)
+      return self.factor * self.distance_function(model, probe)
diff --git a/bob/bio/base/algorithm/PCA.py b/bob/bio/base/algorithm/PCA.py
index f9141a17..0866f319 100644
--- a/bob/bio/base/algorithm/PCA.py
+++ b/bob/bio/base/algorithm/PCA.py
@@ -47,8 +47,8 @@ class PCA (Algorithm):
 
 
   def _check_feature(self, feature):
-    """Checks that the features are apropriate"""
-    if not isinstance(feature, numpy.ndarray) or len(feature.shape) != 1:
+    """Checks that the features are appropriate"""
+    if not isinstance(feature, numpy.ndarray) or len(feature.shape) != 1 or feature.dtype != numpy.float64:
       raise ValueError("The given feature is not appropriate")
 
 
@@ -103,8 +103,8 @@ class PCA (Algorithm):
 
   def enroll(self, enroll_features):
     """Enrolls the model by storing all given input vectors"""
-    [self._check_feature(feature) for feature in enroll_features]
     assert len(enroll_features)
+    [self._check_feature(feature) for feature in enroll_features]
     # just store all the features
     return numpy.vstack(enroll_features)
 
diff --git a/bob/bio/base/algorithm/__init__.py b/bob/bio/base/algorithm/__init__.py
index e5890597..0866bd1e 100644
--- a/bob/bio/base/algorithm/__init__.py
+++ b/bob/bio/base/algorithm/__init__.py
@@ -1,2 +1,3 @@
 from .Algorithm import Algorithm
 from .PCA import PCA
+from .LDA import LDA
diff --git a/bob/bio/base/config/algorithm/lda.py b/bob/bio/base/config/algorithm/lda.py
new file mode 100644
index 00000000..58dff6ed
--- /dev/null
+++ b/bob/bio/base/config/algorithm/lda.py
@@ -0,0 +1,10 @@
+#!/usr/bin/env python
+
+import bob.bio.base
+import scipy.spatial
+
+algorithm = bob.bio.base.algorithm.LDA(
+    subspace_dimension = 50,
+    distance_function = scipy.spatial.distance.euclidean,
+    is_distance_function = True
+)
diff --git a/bob/bio/base/config/algorithm/pca_lda.py b/bob/bio/base/config/algorithm/pca_lda.py
new file mode 100644
index 00000000..8bf7c4a3
--- /dev/null
+++ b/bob/bio/base/config/algorithm/pca_lda.py
@@ -0,0 +1,11 @@
+#!/usr/bin/env python
+
+import bob.bio.base
+import scipy.spatial
+
+algorithm = bob.bio.base.algorithm.LDA(
+    subspace_dimension = 50,
+    pca_subspace_dimension = 100,
+    distance_function = scipy.spatial.distance.euclidean,
+    is_distance_function = True
+)
diff --git a/bob/bio/base/test/data/lda_model.hdf5 b/bob/bio/base/test/data/lda_model.hdf5
new file mode 100644
index 0000000000000000000000000000000000000000..55b34d75582aac2d9b51898ca1dd5fc7c705a4c1
GIT binary patch
literal 2344
zcmeD5aB<`1lHy_j0S*oZ76t(@6Gr@p0u4@x2#gPtPk=HQp>zk7Ucm%mFfxE31A_!q
zTo7tLy1I}cS62q0N|^aD8mf)KfCa+hfC-G!BPs+uTpa^I9*%(e8kR~=K+_p4FcOQ3
z5-WimSbFq;Nsvi1GO$6+f*Q!kpaC|CkqIKe3N;rO%?wQWAeDj&_6(4;>%d^b&0zAM
z8KRLDC<>BiVuBdR0aee;m;g=cC!jV!C?TjRAUy#$Jwq87GEn4c%?bANcLC*SsN)zE
zp#Gq>y`$!jhQMeD480I|En8yUdQ-#U_=834u6aQY5AP}55&fO!aM0jNdOnYsLuKQ+
zwSkUa4uz)n*S<W_bda&^J0<KI?7*Vh&(oxt>TqeTrf2SwV28yzVppYRB{^I$f7m{K
zuc3okXM`*3vTTQ$hi;et_~YPEsmZPLcdMJj`x==)6X(V|I7j6%oodf^*tM=Z;I~1B
zL+aVa*&6Tj9CFJQSH3XLa7a7)@X6P@2nV65i>f{Nq8t?59n)C%ra4^tBe^2PG0<U(
TxW}F<%WQ``bJ`fQrE?tsjSY$#

literal 0
HcmV?d00001

diff --git a/bob/bio/base/test/data/lda_projected.hdf5 b/bob/bio/base/test/data/lda_projected.hdf5
new file mode 100644
index 0000000000000000000000000000000000000000..c5a800eb1fa3f2c8d0aa9abef1b30a86c6edc343
GIT binary patch
literal 2184
zcmeD5aB<`1lHy_j0S*oZ76t(@6Gr@pf({Od2#gPtPk=HQp>zk7Ucm%mFfxE31A_!q
zTo7tLy1I}cS62q0N|^aD8mf)KfCa*WIs+y=N{^5b@Njhu0C_b6>R(tYJpoN;uwY0m
zDoU&ba$xDv113Qx&B(w8F$-!SBQzy5GC~Acq2|J9W}rMsMo__?0g_f77)-bsO#U-N
z6tV(ELGnyYOke{zpz4_!6QD_318O3K5`vlnQXFv8Gn9d$0!5zQoM1nH7f_CdI+j5J
z>Mwe`W7NLU5Eu=C!5;#*_~R~l=eavbrO!C^$|ukveyjD0)QB*L&d){{Kb=uOV3Iid
JfVzvh0|4mnM3Ddh

literal 0
HcmV?d00001

diff --git a/bob/bio/base/test/data/lda_projector.hdf5 b/bob/bio/base/test/data/lda_projector.hdf5
new file mode 100644
index 0000000000000000000000000000000000000000..f3ad8115a6a505d8f2375c5b7d140ca4d38a813c
GIT binary patch
literal 16712
zcmeHu2{@JC*7z}{$dI8)g9=SZLPV`MQid{w=FT~$k|czXN<?Hx=6N{g`Iu+RoCX<E
zsU$>El2Z67`A)yM-|xHkx&M2g`=7q&yTA85&%5^CYwi8+we}kJ+UI?})b{M<;#kf>
z`gzTtPhutU{$8BB&CFhEyMMLJ)#vck*#grn^qhsM%)bguq@O<$Y3XeHqS^8Pnoj?q
zsw#;=IXC_sob5)EV*OQ`8)vTlPx$X40X5a#IzKr*GRv;;ENq(9v)L+1^~`BgOM62L
zJJSolHytoEHalZ!`nwMPY>=NL{3jt3Y2L5CvkJ~MtI14EzZ%$P$C-mH4E#^bB4HrS
z{~bq=WH?DOGc3Od*%+-q@yyK3zYj2fmY#))`iq>j;TPfToo{xOpTs)P)D1`^hyQ~A
ze_hej(%!>FVxFB9i6lJB*Wa7RoV_~C3g~yH8N_k3^=kIN*YkgNI}?fJcRiSs5AUpB
zq|Ulu>MZ;J$S<}o`bGF(wdQ}e?Ed8LKPB+jeo<`})$><&b;i=l&X#h)&S)0@t8Q|}
zeztzm^vr29+Y2P4Glmy_x=ur5+cWltwr9>;{vw#O+qvDr-;3)#JAcuuE&c^lF0he8
z-&KH91&6ZtfpSQfdAr0sAs=?+`fAvx=YV9~cxD`?fzN5Nf=k_GV&3Cjx)E>4#L(XG
z(5Ku~VoD;tWWy+h;6I?9*RYF9T*#iVJIO*NB$qIsrI%2N4@QTWw>eP>k*yDnw8qGU
zG?T5{{Q@#E98T`%E20pGv_{uHKTRPvoh3!8m63_ItBz`UvScFnW6`Kx3YFNAxNLCp
zB!zG*w$U5lV6^MrT(L};N>~z`G-l3Ih(WT+!gN(CVLnk98NZoI9AES)j!2;ptHmjy
zq3<X}*)*F(!Eq{aH7CUEaWk1ne57B<F+wI5S1or7bfgfCLh<e|8S_0cO;PpVOC|Om
zP`E_+l8KdPT*ULAQwXn~j={G=6yp5rSMmC5DFi8?s7%R}!KYxLb_=85CckZ~cIr?G
zcRF?c(+<Ww-zgNvB$5ddX-R!SAcZ((Wz?dqMJ1%VSvt4&Qwct;Cw2n)6oNO{^ce@i
z80WEu=?5Dsq4eq&<^3rNv1+B@#m-v{I=bUjjs=C-{>AqA13wCpx#C^&%pocvcAVSv
zP76a$2|>Frgi;6=7hYLcWh$X{$lF1IA=h!OTQwHjsDuND#=38Ns6^e#&Yk|psD%E`
zy)2eFWTMPHsp570N#a<y&$H$6Wa2}Q<oCdP40&B6g$zzoh$oeOqJxLY#G@|`yh_fD
zb#z->!MBl2ytedQ(r8H~u5jF3%f&$@wxN0Ab4L3bKLgfqHY(wK^V{WeCMq$pDb&Yn
zib5=4(o<Q@Od+zr_?{_Jp%N@7#q-q|{Y5)8()(_b3612KySWt%K6gnf9%0lcJI|5a
z$Q0r*2kELJLr(^>H=K)6XFUHSlela(h46h<%U1q?LhNV_%GH=46a16_`}y7!qGp|u
zo8|{H@w792udE$~SXgkV@!<%CSad7#s@`^n{)xFYsPAORPmnxOCQBt&tT(J(X+$NW
zjcgR#K2nGT=LO}W!xX|q`Dym#O)Bw;M`FW04Ju*$_MO|{D+&QAZ~Q7EsYFZ5*NsM}
zsf6p-?sQguDsigHV1jLsOmsEqa<!bI662@c`8>xd#7ec%*T+LBgn;6KqzBd%qHa;s
zQ%^oB!NX)MRU}O%D*VkSjxtk;EDOn#bU`Z7-@lBhkCQ^AwqM;!x1$o=CcYcjWiZBl
z^Wf;GGZf;?n8wDeVKVV!U;0McJt|?}>r}OP5tUfk-M6^tI)$iwKan`mP9dVEYq*qq
zD1>;_NYJfTDsfEe$QPN_R6=7%t|f7eLJ*=?Cmm`i#MWzZQAUFlV)t{=Kp6ol;i=V;
zdRd-I$lmaHvzTF5Ygk5%`h2LwBy;<=I!7{r+1xIiLS*9MJ?<@P40-Wu^TsatN+II3
zWrD8#AQK~@^_#ExQ;2aRMd5jMWa3^qd+cMzIAfi=53UlU5bL%Y?|7ffup?8|Vr5B&
zzZk332qjSn%HkzXHo{~=Wi#it=3WXB$nownjWHh{q4qC<+bBfK8X3MlSE<DC{ZR`O
zhTIoF*(W!mNhW;vk2pn1FxFvz`xa?VhW(tJxH&OKAw<fp(gfB~iTLfTMJwYeM1*2;
zXSfuVI8d3M|Gt7kNVBdg+H!<SY<QsmFkXpDtm9#wx8gIyzK(7#by-Fw)N;1#OEBtd
z@>o}WW~~1{9V_z_jCD%;Q0Pn3p%6n+BLnjuGx+IeRcB$?w_$gyu5k#JVC5N>H@r+C
zE<V!vrtqFZw4V^Yd~+$4XlEUpU@K<miKw(dDnn17+I8hgGoE|s(w4ze7lvNBjBHuN
z%kU?G?rl6=47$5!cXb(dAmOugz=NUxlVZpHL>T_$v|4$?@)uNsv}K|3%j=AFeIEWb
z=mUlDXw%V)GGpj>9+%OsE;4cZqr~^ggA{`Ofb`IEKL($c-0^-4{pcU><F%b86MNNX
zO#Fw)M8ibq3Fb};!LjVkHI)ri!h2y^c83U+$RANA_u5hku6++(19_+f8@W%U^%I%!
z(^KdXQY8~n-{ZG^3ZfF?-)i5kSx6;%CuY9xV%U3vJ-@U_FqJ4Oeri8&4a3hD9U78b
zOC?_1XSSq8QV2ay{<kqd=l@S$M(2$rEldq<EG7Rl!b$G_OR$qvH!-!eJ!9+mPY(Ad
z**}oLT)zL;`~8I^;`g3Rj7(x7BL}iKwYl(fv%$;Af_|m?!oTpddDh(i<8R^rIzMar
zeH0RD4*u);nI~grKQCh5FZk>J-!ngB`%9iHKRfF=x&8J2Olp=HdBHz&^|$4@JY^~W
z-{St*GW$4E_1s4BkH|liH_Vnd2>hY(zftiY<^4^wbDe|#x?WLdMf=}dul`mZzgCdx
z*D=Ol>}&2EWKJ$~^?zMHajSlbnppiu_4r%6Gy5a@|9bgv<?(A7|IeQbHF5u9=9j$Z
z82elP9p+H~U*KQop>yVMHk${|P3nxvuhN`9Qkz5ki|`+<!`yS`APXbj{4D=;V?QPO
z_s`=-|4F%f{);pJcj`6@zqtJq{war_68KXO{?vm%_25rC_{V;rJ!8vue36dUp38T3
zY%0R(PfGM2_8JrnoA>rxdKz>s2wt)*sT|uPUhJ*;tpL8O><q!s5(qHa_+m$Y8iv>f
z*NU{4g0tk>5_Q1}SfzN#V*RTE6!NuMEhv5)9NA3wsZIpKW6s|;mNb@v$$QPh1tuk!
zx|HUlrJsf#$M{}W3DU68W6}8Hv;s^SDas#DEr7~%F2_=xN};_yI$*b6EKt7toOKH=
zg3o$w{V9p{Xw_P{*ups;c5|PQHQJgDrbVm|AKVWHwH^ESq;dsgGi#GUYETSZQ+K*_
zax@<m6uC>Lq$0tHTwR+;^TFeyRU<##{PDugDV0KwXy~GCU}g8tg;U%x-n;n%vJ0;X
zZ|F+K^yj-;SjuAYTGXJ;&}1(37-e2`5lw>E1)b(w6%$d1`><&6Ogw)6-nEugcNGid
zqMqdEUc-YDe5|8fv8Y^AYTb9M5vX52OEWLgMfz7In`LGNut?-<nUEeLWvA3Nk=HTk
zrLE_`M56+>nysIg?Nx}ad-zt_a%Mnf<>*VFx*9ajnin3ao`nZ)nJTQf=8yO8$Dh1>
zEDX$p-E~{HS3;7N{~fUlr5NWIz0q-lB~D0rQuOI*sI)`a=Y|79oFQMtrI0+_xuIuY
z*q371I4HMTmfD0;<?N$$-eSy7KIZ+~Z$Wr@+x!JmKZ>yZ;nBCZ4y8do&vNI2wkW`P
zvFbG)G0?8n81&J<07JhCKJT5E2<v+Fg<}s#g9tb4%kS@U(ZA#_>w_6@u&+$Zx~u1n
z=eCrb`koq!)vu*?-tW8$V!=9MoF><iwy%8j^!+sW_8=f*`dKunT@@#;%qs@I2EJXX
zuj3$&?Ch9%g^o&m;m3vB^PuLtmR12<0#qs4e&$-5kHun2J954h0DIuFlwPkIR6?fb
zrJ~soL?u0VFPQ`n0~Xb1DrMr*u)DG!qb;Fblv7DjmIm7`v$}d-M`6(IuIbYg#c(Q?
zqlCwx3<Ld=@;7f!ftb``Y4+ekRM$<^36Ssu&&Ar$Z*j(hkod`yN`dh(QhGg4@M$V4
zdT$P?c~S#Daxp=7oC3gD)}t!MIujG`Q0sU(?J&D{y(gP+GI$BT^qilNfI`6ho96@#
z6l5m-(~RS>@uXuy0OR@l@3!}KD&(SLX3nTkT0UyH=q@+w3I<(YZU>cTx!`-~!8dtT
zZ+O~3G}g)o<M#2-j%>?ZvFBOwT{*2%pc(YoRc9r^&ET*j>8s+vj)z#scbA5OZ9HZp
zjQ&S_RZG35GqG^<C#7e57<FD<58RNCr~55Fna9xK3IFApV9i9(etP`l0sBhedUv)i
zdTA!EtITN~70v~JkBccjeWg%g<8WZfo-)vpVE^DPSc}y=-b-za494`z!A>^sM7-75
z`*fzJ93@MWzpbnZgc%{j%hL}cpkFknfV8>-V>ejn@EYYJImzCu`FlQ|IH_sJTv`mw
z7Wrd`P9}m0)sMnq?FD9z1&vj?S3!Q+jwD<8G;n{IaQLcWKK^iGN{qi*1g#?@Ya`fF
z;Mt9#U7lX?Ae~$HhApldO=s%A?!D&*%66(ruIxd;Hmdo`XKgIB=a~sByuXbz17cgt
zs>;zbKuCO7Lo80QOS8Uv8vt@YR`R!xSA%^=-o<aCW#Ib6jPKq13T!{~Om(Sn7+$gc
zu!)%x2Ej`MIqYrHz+54=f?2=}x9zpWWWgZ(d}NwiZht!TZfKppNMzxPgdftJN(mrR
zno`X@o{8l-57XBhWa51H(NCt|!|=V@lC!S@{GnNvTm8t!aCkGIU~McNh2f3`F6QR9
z@aQ3(0UypdND}KCJ6{tCRJV<aE<W*imS{>9@%Mw^NiIKiUPrXr=j(K%>?Xc*=aPBT
z69n%34TX;rvSE|bI$q(T1Z*+4ANLXTK)zHJH(A~gbZJ-<rc2L3!Nbh``eRv`OJsc)
zv!ugN-Mw!gHXA@7?Sy-QQ!WaZ&vZ%%+yV9}=>=i3*?3o*<;tD`I<8Nc&NecR1i#<z
zX)LrWL~kQLwGJW)?%m)2TspW0Tn;^_dZ`$~o3M2}Z(at#Uh{3DAxAU8rBQIigcc3o
zCEnO9QA)w#p{yLPYcv?V&wWSqLL$zLCMhJDBKisref;1U2&@)1Ix~DZkgAyw_fRht
zcaFcQB>eB-QoD(Lw0psjacu7k?(8y<kPlkMF<Fc3;rG{g+a<z!rnkEnXj<VCUF#WH
z-U77APu%bFs2mg;t-C^s%c0w|ccsagC%CR;l~~4{f%7VFuQhcH#60gH!?Bu3tYyCy
z^K@@AG#9^{U#U#TZjI76dv7HJdpCK)<*XNo510C-cSi%W^|lw8_i~`G%&pHZsThiu
zPiJ%P@J0m_{g1+lMR05b2Pd1DEzVczi)$UEg1+k1KyykW_Ir_^Ifg`|_v%~=;SW(@
zp0eKVb8{xzuZTRp?zk_oPDHw&;wR%~D<#7{oL7<Drnhy)JYU>T5#4j*d<N9+x6De<
zjziCt-mkBSU4dO+V0o=k3_dxY-bZ;CigF)pR}5UpMc!^xWj=ljcp>S;wf%c2%s35(
z-|Wpp7N6sb9C~ZP=z`mRK3_Umz<~w(Pgg*!+rD?E#%Zvv$THe<105FENbMfqSBL^;
zD_*rT{FVcEPVia}AD}aB>>F8|f~D_$7Ah7bpuK^xb&s<@I9zSd?G&^It|+oEy}A^_
z4=>@4enE$@)Hg@)W-c%Xet9Azl!+Q+r3qTSq0sz1;H`UU3Z6-qfB#hpft{sV^*uQN
zSoPlsB{ZgEh1a{KcH2uaa#P6Y_sMuD_pP&vZEwJ!#6#4D`T1aLcmUK|-BC<${R3_$
zgl&%EPv5U9g^nc^yG|U404v)R;oe<TBx{q^kCDT`=}gt()kTqD+vWab{1pujh<U_5
zUqZ)2uT`Erjm*Gr{D!BFnbFZ}FtlZ7Q!dn;;#=U#m4pV}E*ufP!Qj4s#33l|COo8B
zXXo?fLzur2m}nPaNrpoCWI;F(UU#Qnu0H_|#>4Z!983e#Iv0g4(z!rXoGBglIgjQm
zRL|{9ZNRouEju3f$HL|JnJPLFrAVq8scPPO8U4gHZ!SKcf{$G|t1+<}WaVzj6et#=
z==w{fFFqB>RjQ%SdN=}9BOPnwTQXp&wddpB>KJ&iU+gZgVFdIX8nZHPErj{M6)d;$
zsm3qXo0R9phC#Wi-r2pvxyXFwoMDG04H`O)fcC`)=Eqb{OU^IE4SDVRjz*XQ-(^yN
z+U_ebc5`H)X+<P3RXTocS4c;1i6H5nddcuiLt%baQ#hRI*6#XFPsOYYtJY60D@DCE
z9f5QkAFvkL5boN~h#y?dW4pfO;`>smU`lr`T6?T+oo5q=A-j&tZ;Oq?h&vbOJ#tF}
zHeZ&viZVg4E#pPOHRn3qt~;>zrB)s&r?LqgP_KhwU-OpTJ}E%nOjS6}mJjms-mF7>
zZeY8*@?-mwa-bgNIyto^0F!dZqBJ|>K+NEh1l6J%)MzssJwG);tKp-{GT|f;Rvk9V
zV6nh3Df4$R@%b1K&asm9TnV&#Jd*S~6M*kqfAk+$ufeS0`UjF4xe%vTvz~8>G0bxu
zO*I*d#PQvs5dzWPkXrn)_QIJGyr{~4%Bj{1on#A7#j951{im&9wa*@In_XXjZW9d~
zE{zHZG{$03zJ_Sn=PGzSuYUKvgE<)WMKgX(GYuRYqM4aWX;84B^oHiP1iXWbeFpD_
z!U2J?y-)TfVY8F)!X2-Zz;Dgn!Df#z*rWypfli*N$#kfx<Dorn(M?@z`8WjCoew{4
z)XxBOuPXE5gc2yfq|m^1IR&!wL=Rg}#$jqMr|K!05d5^R__rdzO6(lsu`XiRv*c9$
zytn2lc(=zZTU;m$j{EeCwC^s2<`Z?g3RPLS^++nGT!lYGa5{6Ix)Ojd-womNPkGok
z>f7}&j}9VrnQXSt!*K6fiGFp>c<c&ck`#AN!mVCT-5sa=kz0MdPw`q1R4$0RN!eD5
z^-q2a(=(-E0P{1+O63?JQX1#KI1+`r3v3<RMDL+_CKuN?!7S8Qvb&P16#>^@+V4ta
zEk>!<y-hb46k?B9)1goA*TSkKQ<)cUBQUO<tBKR?9GcYoNa(So;+bO2*OxBd0_W>C
z+rCuBVA)E6D4nosSmO1lf$0Du>8j<bO6l#u8{hoU^_4Xo^CM*)SQv-pA5=4Pn(5fT
zdivpUy8v8NJGQ=Xc`oX>m{uv|)Ptejmd=FwJa{TiEe!HTTqiZX?eSSUWNf)FIWbfQ
znN>uNj~E?pKMbDVU|j>|5oRkA#&4teOnpk7Ya?7SuL#sG@CW(M^5dJ?D<SB;uVp@a
zDlDI2V+qzMhc!oLXv@A=p~{b~={!?5P^9L5-yk6#>P0kNOQMR<mAOUP-lrbg#s|$T
zWAZUeW9;&>TV<%29`fT0DFe%#-@Y{I2!#?8_Vcc#>6pUyu)*zqFn*@{IEfsMhGzd8
zr*bu|arvEtf&RmhnEX^?*~X%1Tw3b1^lfeq$c&U&iEKKHSuU3*7H>=hd){+5o~Y%)
zu{D#d@<CY;nHjxmAS)H_$nRKj!?ywKxLpj>q=I4W{fENo?kKeDIwE9yA`>fncD;~w
zON0nhStrly@z7cyu~0ET0Of}G)pg5@;6~%>&pbM}ux}vk-98IOoa<9s@3$`i7eA)y
z?tYpKvX=dVEpi2Tx@1pn$jTD1w*P!?zGNy0e_wyzA)yM^^$$hoC&fYL?adzp`8?2~
zVA$#U{sOd!qwH_;Pl5BN=5?gEA|Bcp^i1bkI1FpN^17N^1=|`Qywhu_0@V+B#xiy^
z7+&+pd~r=WtlzG`?8l}|)Lbc~ZIWI9jSW-cI|{2&tZB>3%0>D3{(gmU(3x~(Tcn}s
z)Ef@=3(wukY$<{r;X#@isfmy|&mdlIZy@f^VR;gt7X*@tPSd?i$>`#qe#0S&4R`Jg
z>`$!8gI6+cW1b7o!NZ|(!9kG*tl7>|crDZiGp;{ZwDd&0QfBU8l*Z8W$k33^^R<wi
zYnL)#u?!U2zf9)~WrOueRhPTmiJ-2iHhx(*8A&pko?q1w8d9DG)tmaG|1CaRqZSRS
zvG`<UfHY2q9%ClltWmp{^ICFUF`Bu?vrIiN!>;8Y9?F}>VybennuaC9L8r*uyrc0z
zT^i`z>sN~F-uYMuzb=B#Pq%e9+q=M0uioBto?@JlcV1i66%VH@M0QB$rQvSA{4%-x
z5;XBy6&v&<3vD;OPP@M{7Ph#b@xQNDg42_EaS{@ZXn#SvN=rKe&GvI0KOtFx99>bH
zGur8xyif96GgB<;Y_O$tN+x6L+Nte<s##zY@I<h|BLL2ro?!j{yd0k7mz>&M8i&38
zJ7pUp(y=?J=V5zS7R=L-SgPWa0Yl78OWBPRFy=b*#e;U$Ah1?oYT#fp>W^6Y8{Vcd
z?0w`8Q*bS+7}d>l<<A4{;EAC79JzQ@dciYg3M1YLyy%h+&xcpE;I3WF6)=_<xh6>~
z8IzP!x$gWZgzm$l`+qo;LfZ5zHr_+`aQ;dE6MA0xz`kqhX>H#P)FN?R?(=s;*6Nna
z7Nkt{2XlYkj4UiW7N4#;oC~$D1>F6DFF--UR)3zhB0Md2{*VnrzD_Jz5^AoAP`8L&
ztddWM<a)N@l4GS{Ch_U&^Lz_%D|cHc5JbaAIzgBGT<O^647?d<LxFjiCe_>$i1~L1
zO*rSrz?}sfE-qKT4K8LSj<Mo#@S-py^vLc8Jn&=VQJvfZtiC`b^)c*l@Qw4W@9Rr&
z+Yd$an>8iy{er?dakgrVkSeN6xKRzPs+)yoC^68)y#8KgtTRqHY#cP{XXGEBj*Yz@
z^FZ(KhQm+X!a&QswVHWD0<3!Un(E>b0bAwx*pwh03EK6n@tetbl`Fs^Gba-*>8Y*l
zZ!7UiuX>mE{W~z&62$c7s|_R%oe~mo%ER98idx@a6l1{~eP(KL3RZ`Ym&nEDqFc9#
z$f%AFwozSod0(qTPW6dCJ@ZnK)XlJbbGQcdlRgi;K3N3WN?vT%-L>#c%GSzxXFM23
zW-3}3RiTM;_|)@{=P<wV)H>BI>1cd^LfKrs5O{yA$<J4-L0_l%V<g=`kX%xCf6ehY
z{M3J1Z02Gd8mzw2kZKSJ$v1KjE>o(86=nw3`{f_N7xP8j!bvs2HcopGQ&@<XUbcI@
z)h@(k22pR1yv@gChbI%;o0E{_XCE=i^SbJaGEZ_tU>1nH59P>IvBdgo``fm*rs92J
z1-pCN8PFlD+@SlY9ES`(<cPPFpw!l>=W3?j7^PZY$&z*&-kizd>HhA7qEBzG3%Ad~
zW#PMSxgsO(9{MQqw%#AkXL+34<r@n&xos;{Si>-0Ykks%j0iN@V0QShe<4<1{!&8Q
z<b{vs?d+wCG(bxAQU@=;v!EhBc_uYH3oW)PUpgp&pvvBLrOdVl9u|3Cfb9{$JUXp>
z>?~uyuub=#sa*mne3RAN{vidoS4%PPt;xe_A%AzFySLG|?~W0TwGO2+a~&Q>7UHO@
z#<8JbebnA5&}r<Di&~sdI|h%Yp&Fmc*U_FT<o~v%dTe0{M4m8vqGnZ%OEdjxJnYGs
zvgm{ohcXQ>>^4kzpv$nsCiUE6xdarhc%7%>>W}7z2MT-C^RPHBIWY8OI`rorq@~J*
z1N~yfq`!C=l33kt#SCSj{}4D@k?JA!_DIA4!@u)=I^el;108E<qRX|j^H4)SRLuWO
zGz!s{M&3GCg0EXfKj>;lL8HThxVK3qu-s=;`1K=UaLk!wt#j;cl-Fc3xz5OI{mJqw
zkC(fHgS#W+9ZX+P_gFI(evXD;L#Lc>FwScNqCShMT`$A~3kP#UFV>>7zh=SXbNLXw
z;dXnc*9|mLX>PgoEDF0HT7@fR)Zq2a*Bkc_7QlT8i8m7BjPsUrJ}${`;(;tdS>A3?
z4jc_H8#m2l;B=>nUd|0VYD(xbMP-G-*F=Yo3(iF-vn>s#c=FL!{8Y~3>|ktC>3R`S
z%{cdyZi!p#?~7K?Z#9?p(jip&(|7wrk#M*sAaO7~4;*hiI($q#77pGfCpalx2bVUk
z$id&rfPLq^rU1@V^xS_kLQCHddgZpXKWhlW)TW$=BCqo>)hpS)+aMYn?`o*Iq|kwL
zf?bJ4gb_#l7A$9J%>^m0mE%8JMDfTZ(+MssNBFkrxqiSv9ysqk&B0qmM+$qki(hRb
zG){kUJG?L-46mN=dNn^5f8@EU+;n8*xvlCO3z({)v!pY&#W@z^blL)5DJi3Kg%TU9
zt~-94XxM+vp%4qm5gqT{^5Es!?!2#tG@KZw-ksPLh$X8&^ZYoYihK_a&+Kt1!%^nm
zlEV90U}RyV5=-~S9m}@7@!sHxdSe_UR^fDfez3i6@24Q_yPFs5Q|SpiD1^Lbhd+=X
z_Z)xCuq&e<&sJ2Fr2uKS=n64QX)s6%oeDis0OIpCb53v|=p;H&+nT%~Z;7$>di7+u
zF*T26)Pn}*)oXG~j~1b>{>SI3b>Uzp6wgGjr=iTsnI~@ad)TrwXX}t-0bEl`_g%L!
z1yt1bKkT|%f~VD(&dF=ILAu`pvQD}URzI3NwS2@MXb#O8!&2cmRN2!OCl-J<?>fqZ
zPtmZ)U;F4LSuKqA4OnxwEfL*BE#F(-EJPQ-(%voCWANI7An&LhUQnHun#IM47vp2=
z7gh47VbC`1ZO<e<L2l6e?3U%F5Vy-moxR5&eD9=HE<Rn1%y~-^UZtg?Mcdw82hBV&
zSX#y{)-xL~6>UisTbY1Urpo2K&PkBr9^d(KG!=)|MbKPpOL1j`$S|9EA-cZ4bf1Sm
z1wtBbUsU{235~&~P4<j98=)e!$weX_1S$eF4tJ;E0L@>|-8LV?N}Twqk+;$6q~h&|
zy;ni&S){x*R~|_9d@8pZtA?hA5ZQ?D`oQiH{wQL98FF_XjTxh+L(DOOol?p*z}&dG
zyD-xUqZf65JrNNC^}U6CSEN(nv|K;miuho7E_=*;^0F@~W*TmEo36$rhl}-ED%n84
zBW%bTtO^l}+>7m7Ye7a{b*1=~WT?t{GU@D|ht*D&Et?#3P<LGXU87tOC>d*QKOaO#
z_Ew7FaCQyI&A;7yWN8?zZ691AQ=5Y|lP(?td5nFS*ry!^Pp%{V5S#0@-Hh|j>wZn)
z8zUjkMBQL`r~nh$hBlvLDg;ulb*w|J4q9o%(VYx}QEaK}@f|4%NQC)(P`h3NtJ+&G
z4R4JEm7|eIXOxT3_#M}9k!d6zO;hz8=&OW<V{6WSRjGn8zDaRrIvt(LLu-GWFU6pk
z&wS$ZT+si<l^WifeW;tGakB4N1@4nsX|`)k3>NQ+=J$ITf-Cp}KMn+VU_o20KEIAX
z_^(d<OpbO1{(Xy2M6~(i%p)r+L8~elu1Vi<<w`#Cy&SJKjnBp3>@4yo$x+bLW_zGo
zG6BU-PilFx7vWR|zd^_2ELbq%$TM@Djyo1a?%-ITk9Al47cG>d;c*s`E`6dHTGCQS
zn#60dSdi7=vu7EYnO`&#W}(50Z-LVtn~T9~=@7S0C>@tCurv<1egWKA9ylJ(&jEX;
zWm!2%m%!j4UuwT$66&n1G4WWHjJB_9wzRp2!^t$`+Y3^%V7|(<Sb97SPs^OzYRAYM
z9WsTIJ&G9rXFPc*EMQZJSDIY)y*j*s2+Ec#GuZ(|tC0CQAv(rBuQb$YzKhYSgAd+z
zN2A6{yWxs^IcR5lWBtnHG!%ZkR#-5p8pM~epV%i7fkNlMs8)QbN1rS0YuDP_pk}MB
zquWd-iv2cG6UjJ76F2M97KreLQ|ID39{FTJLuDZ8P<9%c`*upTKC3|U&UniMm<(Iu
zd-n)MhC|&(ZJBQa$@n%`eZ?|b1#S)CSj^6rh4R@8ZyzfS0rB9nPg|Ppz*uAV8qtF>
z7;{_q>k*d}kQP+k$-XZiR}W+kZ~9gW{oz)JuJJj8{duJ#`Lse9U9iiAUnLwZ7p&k)
z&8vbRqDKXmt|);_?c7U7{WoB3u)I@V77a7cuTFE$$${=}XR7YTYUoPh8G0L$26-*r
za<(yP00kwFLSiCNq%-@2qlYcNh3VqY480Q>IAYu+ngQ=DZjTtPVVoa+7Pecqu@O+d
z&i&@<9GK!SuROqh3kptE)+E?mMmY_arZ@ay*fV(dNdFNzkj&kwU%WaQ`3DBJ=6Bq|
zKAVSiU)9rL;JC#Y-^eZCJA@o<>uX?<^rkrLFXf<=`I%28Di4yV9#v}Hx$uL$-%eSV
zj=nURD#6-#$UM=;aZsiJzcG&=&9%!1o{h~0Ve|+v_Vi8Zba26u8>e=<_+?{#WBx+#
z#aTFim`UV`Y$@oBey%wont+^JK%&*Q6kJJ4zfqlTqL)FKRrSGA+#n|YXiAEXij$jb
zwHDUnk*jGJ6Nf{fHfnXL<Q^MLYListC}PB^#?%i>_a`&<&8DkouGZj~ewo(Mo_M6a
zaP&N4pMf1YJEz4M=aEW^w%lIPc_=@re~=WN1dbyiMY5t5Sh+GoElM;CXh+T}nW)im
zptjGXS*R4;&K8eyJTJu#!JfDF9w{)jC+>!QYB)Z=+&`t~RF13Jj&Ym0RANTMvawf1
zchGr(gUiAS8&GUHH+1D%DJUIWc8ULE7PzNU{GTs~fuP&VcTp%8aZnXxmqn(*juvB+
zTFXf66n<kbqsWL)kG_a2olb&oUdv5kn{GnCg?_%eYZ2Bz9nnAEk%-%mHV-Cs6k!8#
z`)$kl5LDJ)^KvUsEUcH9ilRm&pvu*azdgE>hCJp6a@bbnfxYN!Bf%31@M-sD`<)6l
zApAZ=p>|Ulnm@Yav};izDhcNCvM9Tt6yHtLXA=j2;|o8#V_ZCB$qXoK97@C9`0B!s
zTTAij15IsN(<FRw`SFT9wv7K3ZdKb~RR}_x_O%73nt4cn$I0wM+ypD>hPC0+c^K8v
z8_Ml&gQTL1Yr1zE0CQEs`^%<2V9sHj>L-2;o*Y!t-V~DxU)Q~>cpwl0mhVqwR;#&!
zGx?daP`(jL`Ou%O&Ci1oyY)ei$I8&*`f!7qLLN|_wY{j<OTqzBw-qKPc{m!_{BeAB
zIkb~|%-GN7<4`$C+tDH)#y>7wGXH!6{G;y&XJ5nq%lDkW-b?xO{_i4zU*Gfod<*~A
RRsC1*YyP{);eVIU{{Xm(fsX(H

literal 0
HcmV?d00001

diff --git a/bob/bio/base/test/test_algorithms.py b/bob/bio/base/test/test_algorithms.py
index 609c332e..b9525276 100644
--- a/bob/bio/base/test/test_algorithms.py
+++ b/bob/bio/base/test/test_algorithms.py
@@ -33,6 +33,8 @@ import sys
 _mac_os = sys.platform == 'darwin'
 
 
+import scipy.spatial
+
 import bob.io.base
 import bob.learn.linear
 import bob.io.base.test_utils
@@ -101,7 +103,7 @@ def test_pca():
       assert numpy.allclose(pca1.machine.weights[:,i], pca2.machine.weights[:,i], atol=1e-5) or numpy.allclose(pca1.machine.weights[:,i], - pca2.machine.weights[:,i], atol=1e-5)
 
   finally:
-    os.remove(temp_file)
+    if os.path.exists(temp_file): os.remove(temp_file)
 
   # generate and project random feature
   feature = utils.random_array(200, 0., 255., seed=84)
@@ -120,7 +122,6 @@ def test_pca():
   assert abs(pca1.score(model, probe) - reference_score) < 1e-5, "The scores differ: %3.8f, %3.8f" % (pca1.score(model, probe), reference_score)
   assert abs(pca1.score_for_multiple_probes(model, [probe, probe]) - reference_score) < 1e-5
 
-
   # test the calculation of the subspace dimension based on percentage of variance
   pca3 = bob.bio.base.algorithm.PCA(.9)
   try:
@@ -131,7 +132,81 @@ def test_pca():
     pca3.load_projector(temp_file)
     assert pca3.machine.shape[1] == 140
   finally:
-    os.remove(temp_file)
+    if os.path.exists(temp_file): os.remove(temp_file)
+
+
+def test_lda():
+  temp_file = bob.io.base.test_utils.temporary_filename()
+  # assure that the configurations are loadable
+  lda1 = bob.bio.base.load_resource("lda", "algorithm")
+  assert isinstance(lda1, bob.bio.base.algorithm.LDA)
+  assert isinstance(lda1, bob.bio.base.algorithm.Algorithm)
+  lda2 = bob.bio.base.load_resource("pca+lda", "algorithm")
+  assert isinstance(lda2, bob.bio.base.algorithm.LDA)
+  assert isinstance(lda2, bob.bio.base.algorithm.Algorithm)
+
+  assert lda1.performs_projection
+  assert lda1.requires_projector_training
+  assert lda1.use_projected_features_for_enrollment
+  assert lda1.split_training_features_by_client
+  assert not lda1.requires_enroller_training
+
+  # generate a smaller PCA subspcae
+  lda3 = bob.bio.base.algorithm.LDA(5, 10, scipy.spatial.distance.seuclidean, True, True)
+
+  # create random training set
+  train_set = utils.random_training_set_by_id(200, count=20, minimum=0., maximum=255.)
+  # train the projector
+  reference_file = pkg_resources.resource_filename('bob.bio.base.test', 'data/lda_projector.hdf5')
+  try:
+    # train projector
+    lda3.train_projector(train_set, temp_file)
+    assert os.path.exists(temp_file)
+
+    if regenerate_refs: shutil.copy(temp_file, reference_file)
+
+    # check projection matrix
+    lda1.load_projector(reference_file)
+    lda3.load_projector(temp_file)
+
+    assert numpy.allclose(lda1.variances, lda3.variances, atol=1e-5)
+    assert lda3.machine.shape == (200, 5)
+    assert lda1.machine.shape == lda3.machine.shape
+    # ... rotation direction might change, hence either the sum or the difference should be 0
+    for i in range(5):
+      assert numpy.allclose(lda1.machine.weights[:,i], lda3.machine.weights[:,i], atol=1e-5) or numpy.allclose(lda1.machine.weights[:,i], - lda3.machine.weights[:,i], atol=1e-5)
+
+  finally:
+    if os.path.exists(temp_file): os.remove(temp_file)
+
+  # generate and project random feature
+  feature = utils.random_array(200, 0., 255., seed=84)
+  projected = lda1.project(feature)
+  assert projected.shape == (5,)
+  _compare(projected, pkg_resources.resource_filename('bob.bio.base.test', 'data/lda_projected.hdf5'), lda1.write_feature, lda1.read_feature)
+
+  # enroll model from random features
+  enroll = utils.random_training_set(5, 5, 0., 255., seed=21)
+  model = lda1.enroll(enroll)
+  _compare(model, pkg_resources.resource_filename('bob.bio.base.test', 'data/lda_model.hdf5'), lda1.write_model, lda1.read_model)
+
+  # compare model with probe
+  probe = lda1.read_probe(pkg_resources.resource_filename('bob.bio.base.test', 'data/lda_projected.hdf5'))
+  reference_score = -233.30450012
+  assert abs(lda1.score(model, probe) - reference_score) < 1e-5, "The scores differ: %3.8f, %3.8f" % (lda1.score(model, probe), reference_score)
+  assert abs(lda1.score_for_multiple_probes(model, [probe, probe]) - reference_score) < 1e-5
+
+  # test the calculation of the subspace dimension based on percentage of variance
+  lda4 = bob.bio.base.algorithm.LDA(pca_subspace_dimension=.9)
+  try:
+    # train projector
+    lda4.train_projector(train_set, temp_file)
+    assert os.path.exists(temp_file)
+    assert lda4.pca_subspace == 132
+    lda4.load_projector(temp_file)
+    assert lda4.machine.shape[1] == 19
+  finally:
+    if os.path.exists(temp_file): os.remove(temp_file)
 
 
 """
@@ -200,72 +275,6 @@ def test_pca():
 
 
 
-  def test04_lda(self):
-    # read input
-    feature = facereclib.utils.load(self.input_dir('linearize.hdf5'))
-    # assure that the config file is loadable
-    tool = self.config('lda')
-    self.assertTrue(isinstance(tool, facereclib.tools.LDA))
-    # assure that the config file is loadable
-    tool = self.config('pca+lda')
-    self.assertTrue(isinstance(tool, facereclib.tools.LDA))
-
-    # here we use a reduced tool, using the scaled Euclidean distance (mahalanobis) from scipy
-    import scipy.spatial
-    tool = facereclib.tools.LDA(5, 10, scipy.spatial.distance.seuclidean, True, True)
-    self.assertTrue(tool.performs_projection)
-    self.assertTrue(tool.requires_projector_training)
-    self.assertTrue(tool.use_projected_features_for_enrollment)
-    self.assertTrue(tool.split_training_features_by_client)
-
-    # train the projector
-    t = tempfile.mkstemp('pca+lda.hdf5', prefix='frltest_')[1]
-    tool.train_projector(facereclib.utils.tests.random_training_set_by_id(feature.shape, count=20, minimum=0., maximum=255.), t)
-    if regenerate_refs:
-      import shutil
-      shutil.copy2(t, self.reference_dir('pca+lda_projector.hdf5'))
-
-    # load the projector file
-    tool.load_projector(self.reference_dir('pca+lda_projector.hdf5'))
-    # compare the resulting machines
-    f = bob.io.base.HDF5File(t)
-    new_variances = f.read("Eigenvalues")
-    f.cd("/Machine")
-    new_machine = bob.learn.linear.Machine(f)
-    del f
-    self.assertEqual(tool.m_machine.shape, new_machine.shape)
-    self.assertTrue(numpy.abs(tool.m_variances - new_variances < 1e-5).all())
-    # ... rotation direction might change, hence either the sum or the difference should be 0
-    for i in range(5):
-      self.assertTrue(numpy.abs(tool.m_machine.weights[:,i] - new_machine.weights[:,i] < 1e-5).all() or numpy.abs(tool.m_machine.weights[:,i] + new_machine.weights[:,i] < 1e-5).all())
-    os.remove(t)
-
-    # project feature
-    projected = tool.project(feature)
-    self.compare(projected, 'pca+lda_feature.hdf5')
-    self.assertTrue(len(projected.shape) == 1)
-
-    # enroll model
-    model = tool.enroll([projected])
-    self.compare(model, 'pca+lda_model.hdf5')
-    self.assertTrue(model.shape == (1,5))
-
-    # score
-    sim = tool.score(model, projected)
-    self.assertAlmostEqual(sim, 0.)
-
-    # test the calculation of the subspace dimension based on percentage of variance,
-    # and the usage of a different way to compute the final score in case of multiple features per model
-    tool = facereclib.tools.LDA(5, .9, multiple_model_scoring = 'median')
-    tool.train_projector(facereclib.utils.tests.random_training_set_by_id(feature.shape, count=20, minimum=0., maximum=255.), t)
-    self.assertEqual(tool.m_pca_subspace, 334)
-    tool.load_projector(t)
-    os.remove(t)
-    projected = tool.project(feature)
-    model = tool.enroll([projected, projected])
-    self.assertTrue(model.shape == (2,5))
-    self.assertAlmostEqual(tool.score(model, projected), 0.)
-    self.assertAlmostEqual(tool.score_for_multiple_probes(model, [projected, projected]), 0.)
 
 
   def test05_bic(self):
diff --git a/setup.py b/setup.py
index 90f3419b..6a0ee5e7 100644
--- a/setup.py
+++ b/setup.py
@@ -121,6 +121,8 @@ setup(
       'bob.bio.algorithm': [
         'dummy             = bob.bio.base.test.dummy.algorithm:algorithm', # for test purposes only
         'pca               = bob.bio.base.config.algorithm.pca:algorithm',
+        'lda               = bob.bio.base.config.algorithm.lda:algorithm',
+        'pca+lda           = bob.bio.base.config.algorithm.lda:algorithm',
       ],
    },
 
-- 
GitLab