GestureRecognitionToolkit  Version: 0.2.5
The Gesture Recognition Toolkit (GRT) is a cross-platform, open-source, c++ machine learning library for real-time gesture recognition.
FiniteStateMachine.cpp
1 /*
2 GRT MIT License
3 Copyright (c) <2012> <Nicholas Gillian, Media Lab, MIT>
4 
5 Permission is hereby granted, free of charge, to any person obtaining a copy of this software
6 and associated documentation files (the "Software"), to deal in the Software without restriction,
7 including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
8 and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so,
9 subject to the following conditions:
10 
11 The above copyright notice and this permission notice shall be included in all copies or substantial
12 portions of the Software.
13 
14 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
15 LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
16 IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
17 WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
18 SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
19 */
20 
21 #define GRT_DLL_EXPORTS
22 #include "FiniteStateMachine.h"
23 
24 GRT_BEGIN_NAMESPACE
25 
26 //Define the string that will be used to identify the object
27 const std::string FiniteStateMachine::id = "FiniteStateMachine";
28 std::string FiniteStateMachine::getId() { return FiniteStateMachine::id; }
29 
30 //Register the FiniteStateMachine module with the Classifier base class
31 RegisterClassifierModule< FiniteStateMachine > FiniteStateMachine::registerModule( getId() );
32 
33 FiniteStateMachine::FiniteStateMachine(const UINT numParticles,const UINT numClustersPerState,const Float stateTransitionSmoothingCoeff,const Float measurementNoise) : Classifier( FiniteStateMachine::getId() )
34 {
35  this->numParticles = numParticles;
36  this->numClustersPerState = numClustersPerState;
37  this->stateTransitionSmoothingCoeff = stateTransitionSmoothingCoeff;
38  this->measurementNoise = measurementNoise;
39  classifierMode = TIMESERIES_CLASSIFIER_MODE;
40 
41  //Set the learning settings that will be used to build the KMeans model
42  minChange = 1.0e-5;
43  minNumEpochs = 0;
44  maxNumEpochs = 1000;
45 }
46 
48 {
49  classifierMode = STANDARD_CLASSIFIER_MODE;
50  *this = rhs;
51 }
52 
54 {
55 }
56 
58  if( this != &rhs ){
59 
60  this->clear();
61 
62  //Classifier variables
63  copyBaseVariables( (Classifier*)&rhs );
64 
65  //Copy the FiniteStateMachine variable
66  this->numParticles = rhs.numParticles;
67  this->numClustersPerState = rhs.numClustersPerState;
68  this->stateTransitionSmoothingCoeff = rhs.stateTransitionSmoothingCoeff;
69  this->measurementNoise = rhs.measurementNoise;
70  this->particles = rhs.particles;
71  this->stateTransitions = rhs.stateTransitions;
72  this->stateEmissions = rhs.stateEmissions;
73 
74  if( rhs.trained ){
75  this->initParticles();
76  }
77  }
78  return *this;
79 }
80 
82 
83  if( classifier == NULL ) return false;
84 
85  if( this->getClassifierType() == classifier->getClassifierType() ){
86 
87  FiniteStateMachine *ptr = (FiniteStateMachine*)classifier;
88 
89  this->clear();
90 
91  //Clone the classifier variables
92  if( !copyBaseVariables( classifier ) ){
93  errorLog << "deepCopyFrom(const Classifier *classifier) - Failed to deep copy classifier base class!" << std::endl;
94  return false;
95  }
96 
97  this->numParticles = ptr->numParticles;
98  this->numClustersPerState = ptr->numClustersPerState;
99  this->stateTransitionSmoothingCoeff = ptr->stateTransitionSmoothingCoeff;
100  this->measurementNoise = ptr->measurementNoise;
101  this->particles = ptr->particles;
102  this->stateTransitions = ptr->stateTransitions;
103  this->stateEmissions = ptr->stateEmissions;
104 
105  if( ptr->trained ){
106  this->initParticles();
107  }
108 
109  return true;
110  }
111  return false;
112 }
113 
115 
116  const unsigned int M = trainingData.getNumSamples();
117  const unsigned int N = trainingData.getNumDimensions();
118 
119  if( M == 0 ){
120  errorLog << "train_(ClassificationData &trainingData) - Training data has zero samples!" << std::endl;
121  clear();
122  return false;
123  }
124 
125  //Convert the classification data into a continuous time stream
127  timeseries.setNumDimensions( N );
128 
129  for(unsigned int i=0; i<M; i++){
130  timeseries.addSample(trainingData[i].getClassLabel(), trainingData[i].getSample());
131  }
132 
133  //Train the particle filter
134  if( !train_( timeseries ) ){
135  clear();
136  errorLog << "train_(ClassificationData &trainingData) - Failed to train particle filter!" << std::endl;
137  return false;
138  }
139 
140  return true;
141 }
142 
144 
145  const unsigned int M = trainingData.getNumSamples();
146  const unsigned int N = trainingData.getNumDimensions();
147 
148  if( M == 0 ){
149  errorLog << "train_(TimeSeriesClassificationData &trainingData) - Training data has zero samples!" << std::endl;
150  clear();
151  return false;
152  }
153 
154  //Convert the timeseries classification data into a continuous time stream
156  timeseries.setNumDimensions( N );
157 
158  for(unsigned int i=0; i<M; i++){
159  for(unsigned int j=0; j<trainingData[i].getLength(); j++){
160  timeseries.addSample(trainingData[i].getClassLabel(), trainingData[i].getData().getRow(j));
161  }
162  }
163 
164  //Train the particle filter
165  if( !train_( timeseries ) ){
166  clear();
167  errorLog << __GRT_LOG__ << " Failed to train particle filter!" << std::endl;
168  return false;
169  }
170 
171  return true;
172 }
173 
175 
176  //Clear any previous model
177  clear();
178 
179  const UINT M = data.getNumSamples();
180  const UINT N = data.getNumDimensions();
181  const UINT K = data.getNumClasses();
182 
183  if( M == 0 ){
184  return false;
185  }
186 
187  numInputDimensions = N;
188  numClasses = K;
189  ranges = data.getRanges();
190 
191  //Scale the training data if needed
192  if( useScaling ){
193  //Scale the training data between 0 and 1
194  data.scale(0, 1);
195  }
196 
197  stateTransitions.resize(K, K);
198  stateTransitions.setAllValues(0);
199 
200  //Get a copy of the class labels, each class label is a state
201  classLabels = data.getClassLabels();
202 
203  //Count how many times we transitiion from one state to another
204  UINT lastStateIndex = getClassLabelIndexValue( data[0].getClassLabel() );
205  UINT currentStateIndex = 0;
206  for(UINT i=1; i<data.getNumSamples(); i++){
207  currentStateIndex = getClassLabelIndexValue( data[i].getClassLabel() );
208  stateTransitions[ lastStateIndex ][ currentStateIndex ]++;
209  lastStateIndex = currentStateIndex;
210  }
211 
212  //Normalize the state transitions
213  Float sum = 0;
214  for(UINT i=0; i<K; i++){
215  sum = 0;
216  for(UINT j=0; j<K; j++){
217  sum += stateTransitions[i][j] + stateTransitionSmoothingCoeff;
218  }
219  for(UINT j=0; j<K; j++){
220  stateTransitions[i][j] /= sum;
221  }
222  }
223 
224  //Build the state emissions model for each state
225  for(UINT k=0; k<K; k++){
226 
227  //Get the data that belongs to the current state
228  MatrixFloat classData;
229  for(UINT i=0; i<M; i++){
230  if( data[i].getClassLabel() == classLabels[k] ){
231  classData.push_back( data[i].getSample() );
232  }
233  }
234 
235  //Make sure there are enough training samples to support the numClustersPerState
236  if( classData.getNumRows() < numClustersPerState ){
237  errorLog << __GRT_LOG__ << " There are not enough samples in state " << classLabels[k] << "! You should reduce the numClustersPerState to: " << classData.getNumRows() << std::endl;
238  clear();
239  return false;
240  }
241 
242  //Use KMeans to find a clusters within the state data
243  KMeans kmeans;
244  kmeans.setNumClusters( numClustersPerState );
245  kmeans.setMinChange( minChange );
246  kmeans.setMinNumEpochs( minNumEpochs );
247  kmeans.setMaxNumEpochs( maxNumEpochs );
248 
249  if( !kmeans.train_( classData ) ){
250  errorLog << __GRT_LOG__ << " Failed to train kmeans cluster for class k: " << classLabels[k] << std::endl;
251  clear();
252  return false;
253  }
254 
255  //Add the clusters for this state to the stateEmissions vector
256  stateEmissions.push_back( kmeans.getClusters() );
257  }
258 
259  //Flag the model is trained
260  trained = true;
261  converged = true;
262 
263  //Init the particles
264  initParticles();
265 
266  //Reset the particles to random starting states
267  reset();
268 
269  print();
270 
271  return true;
272 }
273 
275 
276  if( !trained ){
277  errorLog << __GRT_LOG__ << " Model Not Trained!" << std::endl;
278  return false;
279  }
280 
281  predictedClassLabel = 0;
282  maxLikelihood = -10000;
283 
284  if( inputVector.getSize() != numInputDimensions ){
285  errorLog << __GRT_LOG__ << " The size of the input vector (" << inputVector.getSize() << ") does not match the num features in the model (" << numInputDimensions << std::endl;
286  return false;
287  }
288 
289  if( useScaling ){
290  for(UINT n=0; n<numInputDimensions; n++){
291  inputVector[n] = scale(inputVector[n], ranges[n].minValue, ranges[n].maxValue, 0, 1);
292  }
293  }
294 
295  if( classLikelihoods.getSize() != numClasses ) classLikelihoods.resize(numClasses,0);
296  if( classDistances.getSize() != numClasses ) classDistances.resize(numClasses,0);
297 
298  std::fill(classLikelihoods.begin(),classLikelihoods.end(),0);
299  std::fill(classDistances.begin(),classDistances.end(),0);
300 
301  //Update the particle filter
302  particles.filter( inputVector );
303 
304  //Compute the state estimation
305  Float sum = 0;
306  for(UINT i=0; i<numParticles; i++){
307  sum += particles[i].w;
308  classLikelihoods[ particles[i].currentState ] += particles[i].w;
309  classDistances[ particles[i].currentState ] += particles[i].w;
310  }
311 
312  //Normalize the class likelihoods
313  maxLikelihood = 0;
314  predictedClassLabel = 0;
315  for(UINT i=0; i<numClasses; i++){
316  classLikelihoods[ i ] /= sum;
317 
318  if( classLikelihoods[i] > maxLikelihood ){
319  maxLikelihood = classLikelihoods[i];
320  predictedClassLabel = classLabels[i];
321  }
322  }
323 
324  return true;
325 }
326 
328 
329  if( trained ){
330  //Randomize the particles starting states
331  for(UINT i=0; i<numParticles; i++){
332  particles[i].currentState = random.getRandomNumberInt(0, numClasses);
333  particles[i].w = 0;
334  }
335  }
336 
337  return true;
338 }
339 
341 
342  //Clear the Classifier variables
344 
345  //Clear the model
346  stateTransitions.clear();
347  stateEmissions.clear();
348  pt.clear();
349  pe.clear();
350  particles.clear();
351 
352  return true;
353 }
354 
356 
357  infoLog << "FiniteStateMachineModel" << std::endl;
358  infoLog << "NumParticles: " << numParticles << std::endl;
359  infoLog << "NumClustersPerState: " << numClustersPerState << std::endl;
360  infoLog << "StateTransitionSmoothingCoeff: " << stateTransitionSmoothingCoeff << std::endl;
361 
362  if( trained ){
363  infoLog << "StateTransitions: " << std::endl;
364  for(unsigned int i=0; i<stateTransitions.getNumRows(); i++){
365  for(unsigned int j=0; j<stateTransitions.getNumCols(); j++){
366  infoLog << stateTransitions[i][j] << " ";
367  }
368  infoLog << std::endl;
369  }
370 
371  infoLog << "StateEmissions: " << std::endl;
372  for(unsigned int k=0; k<stateEmissions.size(); k++){
373  for(unsigned int i=0; i<stateEmissions[k].getNumRows(); i++){
374  for(unsigned int j=0; j<stateEmissions[k].getNumCols(); j++){
375  infoLog << stateEmissions[k][i][j] << " ";
376  }
377  infoLog << std::endl;
378  }
379  }
380  }
381 
382  return true;
383 }
384 
385 bool FiniteStateMachine::save( std::fstream &file ) const{
386 
387  if(!file.is_open())
388  {
389  errorLog << __GRT_LOG__ << " The file is not open!" << std::endl;
390  return false;
391  }
392 
393  //Write the header info
394  file << "GRT_FSM_MODEL_FILE_V1.0\n";
395 
396  //Write the classifier settings to the file
398  errorLog << __GRT_LOG__ << " Failed to save classifier base settings to file!" << std::endl;
399  return false;
400  }
401 
402  file << "NumParticles: " << numParticles << std::endl;
403  file << "NumClustersPerState: " << numClustersPerState << std::endl;
404  file << "StateTransitionSmoothingCoeff: " << stateTransitionSmoothingCoeff << std::endl;
405 
406  if( trained ){
407  file << "StateTransitions:" << std::endl;
408  for(unsigned int i=0; i<stateTransitions.getNumRows(); i++){
409  for(unsigned int j=0; j<stateTransitions.getNumCols(); j++){
410  file << stateTransitions[i][j] << " ";
411  }
412  file << std::endl;
413  }
414 
415  file << "StateEmissions:" << std::endl;
416  for(unsigned int k=0; k<numClasses; k++){
417  for(unsigned int i=0; i<stateEmissions[k].getNumRows(); i++){
418  for(unsigned int j=0; j<stateEmissions[k].getNumCols(); j++){
419  file << stateEmissions[k][i][j] << " ";
420  }
421  file << std::endl;
422  }
423  }
424 
425  if( !useScaling ){
426  file << "Ranges: " << std::endl;
427  for(UINT i=0; i<ranges.getSize(); i++){
428  file << ranges[i].minValue << "\t" << ranges[i].maxValue << std::endl;
429  }
430  }
431  }
432 
433  return true;
434 }
435 
436 bool FiniteStateMachine::load( std::fstream &file ){
437 
438  //Clear any previous model
439  clear();
440 
441  if(!file.is_open())
442  {
443  errorLog << __GRT_LOG__ << " Could not open file to load model" << std::endl;
444  return false;
445  }
446 
447  std::string word;
448 
449  //Find the file type header
450  file >> word;
451  if( word != "GRT_FSM_MODEL_FILE_V1.0" ){
452  errorLog << __GRT_LOG__ << " Could not find Model File Header" << std::endl;
453  return false;
454  }
455 
456  //Load the base settings from the file
458  errorLog << __GRT_LOG__ << " Failed to load base settings from file!" << std::endl;
459  return false;
460  }
461 
462  //Find the NumParticles header
463  file >> word;
464  if( word != "NumParticles:" ){
465  errorLog << __GRT_LOG__ << " Could not find NumParticles Header" << std::endl;
466  return false;
467  }
468  file >> numParticles;
469 
470  //Find the NumClustersPerState header
471  file >> word;
472  if( word != "NumClustersPerState:" ){
473  errorLog << __GRT_LOG__ << " Could not find NumClustersPerState Header" << std::endl;
474  return false;
475  }
476  file >> numClustersPerState;
477 
478  //Find the StateTransitionSmoothingCoeff header
479  file >> word;
480  if( word != "StateTransitionSmoothingCoeff:" ){
481  errorLog << __GRT_LOG__ << " Could not find stateTransitionSmoothingCoeff Header" << std::endl;
482  return false;
483  }
484  file >> stateTransitionSmoothingCoeff;
485 
486  if( trained ){
487 
488  //Find the StateTransitions header
489  file >> word;
490  if( word != "StateTransitions:" ){
491  errorLog << __GRT_LOG__ << " Could not find StateTransitions Header" << std::endl;
492  return false;
493  }
494  stateTransitions.resize(numClasses, numClasses);
495 
496  for(unsigned int i=0; i<stateTransitions.getNumRows(); i++){
497  for(unsigned int j=0; j<stateTransitions.getNumCols(); j++){
498  file >> stateTransitions[i][j];
499  }
500  }
501 
502  //Find the StateEmissions header
503  file >> word;
504  if( word != "StateEmissions:" ){
505  errorLog << __GRT_LOG__ << " Could not find StateEmissions Header" << std::endl;
506  return false;
507  }
508  stateEmissions.resize( numClasses );
509 
510  for(unsigned int k=0; k<numClasses; k++){
511  stateEmissions[k].resize( numClustersPerState, numInputDimensions );
512  for(unsigned int i=0; i<stateEmissions[k].getNumRows(); i++){
513  for(unsigned int j=0; j<stateEmissions[k].getNumCols(); j++){
514  file >> stateEmissions[k][i][j];
515  }
516  }
517  }
518 
519  if( !useScaling ){
520  //Load if the Ranges
521  file >> word;
522  if( word != "Ranges:" ){
523  errorLog << __GRT_LOG__ << " Failed to read Ranges header!" << std::endl;
524  clear();
525  return false;
526  }
527  ranges.resize(numInputDimensions);
528 
529  for(UINT i=0; i<ranges.size(); i++){
530  file >> ranges[i].minValue;
531  file >> ranges[i].maxValue;
532  }
533  }
534 
535  initParticles();
536  }
537 
538  return true;
539 }
540 
541 bool FiniteStateMachine::recomputePT(){
542 
543  if( !trained ){
544  warningLog << __GRT_LOG__ << " Failed to init particles, the model has not been trained!" << std::endl;
545  return false;
546  }
547 
548  pt.clear();
549 
550  //Build pt, this is simply the state transitions formated as indexed Floats to make the prediction stage more efficient
551  const UINT K = stateTransitions.getNumRows();
552  for(UINT i=0; i<K; i++){
553  Vector< IndexedDouble > model(K);
554  for(UINT j=0; j<K; j++){
555  model[j].index = j;
556  model[j].value = stateTransitions[i][j];
557  }
558  pt.push_back( model );
559  }
560 
561  return true;
562 }
563 
564 bool FiniteStateMachine::recomputePE(){
565 
566  if( !trained ){
567  warningLog << __GRT_LOG__ << " Failed to init particles, the model has not been trained!" << std::endl;
568  return false;
569  }
570 
571  pe.clear();
572 
573  const UINT K = stateEmissions.getSize();
574 
575  //Run over each state (k)
576  for(UINT k=0; k<K; k++){
577 
578  //For each state, convert the Matrix of emissions data to a vector of vectors (this format is more efficient for the particle filter)
579  Vector< VectorFloat > model;
580  model.reserve( numClustersPerState );
581  for(UINT i=0; i<stateEmissions[k].getNumRows(); i++){
582  model.push_back( stateEmissions[k].getRow(i) );
583  }
584 
585  pe.push_back( model );
586  }
587 
588  return true;
589 }
590 
591 bool FiniteStateMachine::initParticles(){
592 
593  if( !trained ){
594  warningLog << __GRT_LOG__ << " Failed to init particles, the model has not been trained!" << std::endl;
595  return false;
596  }
597 
598  //Init the particles
599  Vector< VectorFloat > initModel( numInputDimensions, VectorFloat(2,0) );
600  VectorFloat initProcessNoise( numInputDimensions, 0 ); //Process noise is ignored for the FSM particle filter
601  VectorFloat initMeasurementNoise( numInputDimensions, 0 );
602 
603  //Setup the init model
604  for(unsigned int i=0; i<numInputDimensions; i++){
605  initModel[i][0] = useScaling ? 0 : ranges[i].minValue;
606  initModel[i][1] = useScaling ? 1 : ranges[i].maxValue;
607  }
608 
609  //Set the measurement noise
610  for(unsigned int i=0; i<numInputDimensions; i++){
611  initMeasurementNoise[i] = measurementNoise;
612  }
613 
614  particles.init(numParticles, initModel, initProcessNoise, initMeasurementNoise);
615 
616  recomputePT();
617  recomputePE();
618 
619  //Set the lookup table references
620  particles.setLookupTables( pt, pe );
621 
622  //Reset the particles
623  reset();
624 
625  return true;
626 }
627 
628 bool FiniteStateMachine::setNumParticles(const UINT numParticles){
629 
630  clear();
631 
632  this->numParticles = numParticles;
633 
634  return true;
635 }
636 
637 bool FiniteStateMachine::setNumClustersPerState(const UINT numClustersPerState){
638 
639  clear();
640 
641  this->numClustersPerState = numClustersPerState;
642 
643  return true;
644 }
645 
646 bool FiniteStateMachine::setStateTransitionSmoothingCoeff(const Float stateTransitionSmoothingCoeff){
647 
648  clear();
649 
650  this->stateTransitionSmoothingCoeff = stateTransitionSmoothingCoeff;
651 
652  return true;
653 }
654 
655 bool FiniteStateMachine::setMeasurementNoise(const Float measurementNoise){
656 
657  clear();
658 
659  this->measurementNoise = measurementNoise;
660 
661  return true;
662 }
663 
664 GRT_END_NAMESPACE
bool saveBaseSettingsToFile(std::fstream &file) const
Definition: Classifier.cpp:274
virtual bool filter(SENSOR_DATA &data)
std::string getClassifierType() const
Definition: Classifier.cpp:175
FiniteStateMachine(const UINT numParticles=200, const UINT numClustersPerState=20, const Float stateTransitionSmoothingCoeff=0.0, const Float measurementNoise=10.0)
virtual bool resize(const unsigned int size)
Definition: Vector.h:133
virtual bool train_(MatrixFloat &data)
Definition: KMeans.cpp:153
virtual bool print() const
UINT getSize() const
Definition: Vector.h:201
bool scale(const Float minTarget, const Float maxTarget)
bool setMinChange(const Float minChange)
Definition: MLBase.cpp:344
Vector< UINT > getClassLabels() const
bool clear()
Definition: Matrix.h:553
bool setAllValues(const T &value)
Definition: Matrix.h:366
UINT getNumSamples() const
virtual ~FiniteStateMachine(void)
virtual bool deepCopyFrom(const Classifier *classifier)
bool copyBaseVariables(const Classifier *classifier)
Definition: Classifier.cpp:101
Vector< Vector< IndexedDouble > > pt
This stores the stateTransitions matrix in a format more efficient for the particle filter...
bool loadBaseSettingsFromFile(std::fstream &file)
Definition: Classifier.cpp:321
virtual bool load(std::fstream &file)
unsigned int getNumRows() const
Definition: Matrix.h:574
UINT getNumDimensions() const
virtual bool clear()
unsigned int getNumCols() const
Definition: Matrix.h:581
bool addSample(const UINT classLabel, const VectorFloat &sample)
bool setMinNumEpochs(const UINT minNumEpochs)
Definition: MLBase.cpp:329
virtual bool train_(ClassificationData &trainingData)
UINT getClassLabelIndexValue(const UINT classLabel) const
Definition: Classifier.cpp:213
virtual bool save(std::fstream &file) const
bool setNumDimensions(const UINT numDimensions)
int getRandomNumberInt(int minRange, int maxRange)
Definition: Random.cpp:59
Definition: KMeans.h:41
virtual bool predict_(VectorFloat &inputVector)
virtual bool resize(const unsigned int r, const unsigned int c)
Definition: Matrix.h:245
Vector< MinMax > getRanges() const
Vector< Vector< VectorFloat > > pe
This stores the stateEmissions model in a format more efficient for the particle filter.
virtual bool init(const unsigned int numParticles, const Vector< VectorFloat > &initModel, const VectorFloat &processNoise, const VectorFloat &measurementNoise)
bool setMaxNumEpochs(const UINT maxNumEpochs)
Definition: MLBase.cpp:320
bool push_back(const Vector< T > &sample)
Definition: Matrix.h:431
virtual bool clear()
Definition: Classifier.cpp:151
This is the main base class that all GRT Classification algorithms should inherit from...
Definition: Classifier.h:41
FiniteStateMachine & operator=(const FiniteStateMachine &rhs)
bool setNumClusters(const UINT numClusters)
Definition: Clusterer.cpp:262
static std::string getId()
Float scale(const Float &x, const Float &minSource, const Float &maxSource, const Float &minTarget, const Float &maxTarget, const bool constrain=false)
Definition: GRTBase.h:184