1559 lines
56 KiB
C++
1559 lines
56 KiB
C++
/*M///////////////////////////////////////////////////////////////////////////////////////
|
|
//
|
|
// IMPORTANT: READ BEFORE DOWNLOADING, COPYING, INSTALLING OR USING.
|
|
//
|
|
// By downloading, copying, installing or using the software you agree to this license.
|
|
// If you do not agree to this license, do not download, install,
|
|
// copy or use the software.
|
|
//
|
|
//
|
|
// Intel License Agreement
|
|
// For Open Source Computer Vision Library
|
|
//
|
|
// Copyright (C) 2000, Intel Corporation, all rights reserved.
|
|
// Third party copyrights are property of their respective owners.
|
|
//
|
|
// Redistribution and use in source and binary forms, with or without modification,
|
|
// are permitted provided that the following conditions are met:
|
|
//
|
|
// * Redistribution's of source code must retain the above copyright notice,
|
|
// this list of conditions and the following disclaimer.
|
|
//
|
|
// * Redistribution's in binary form must reproduce the above copyright notice,
|
|
// this list of conditions and the following disclaimer in the documentation
|
|
// and/or other materials provided with the distribution.
|
|
//
|
|
// * The name of Intel Corporation may not be used to endorse or promote products
|
|
// derived from this software without specific prior written permission.
|
|
//
|
|
// This software is provided by the copyright holders and contributors "as is" and
|
|
// any express or implied warranties, including, but not limited to, the implied
|
|
// warranties of merchantability and fitness for a particular purpose are disclaimed.
|
|
// In no event shall the Intel Corporation or contributors be liable for any direct,
|
|
// indirect, incidental, special, exemplary, or consequential damages
|
|
// (including, but not limited to, procurement of substitute goods or services;
|
|
// loss of use, data, or profits; or business interruption) however caused
|
|
// and on any theory of liability, whether in contract, strict liability,
|
|
// or tort (including negligence or otherwise) arising in any way out of
|
|
// the use of this software, even if advised of the possibility of such damage.
|
|
//
|
|
//M*/
|
|
|
|
#include "cvtest.h"
|
|
#include <limits>
|
|
#include <cstdio>
|
|
#include <iostream>
|
|
#include <fstream>
|
|
|
|
using namespace std;
|
|
using namespace cv;
|
|
|
|
/****************************************************************************************\
|
|
* Functions to evaluate affine covariant detectors and descriptors. *
|
|
\****************************************************************************************/
|
|
inline Point2f applyHomography( const Mat_<double>& H, const Point2f& pt )
|
|
{
|
|
double z = H(2,0)*pt.x + H(2,1)*pt.y + H(2,2);
|
|
if( z )
|
|
{
|
|
double w = 1./z;
|
|
return Point2f( (H(0,0)*pt.x + H(0,1)*pt.y + H(0,2))*w, (H(1,0)*pt.x + H(1,1)*pt.y + H(1,2))*w );
|
|
}
|
|
return Point2f( numeric_limits<double>::max(), numeric_limits<double>::max() );
|
|
}
|
|
|
|
inline void linearizeHomographyAt( const Mat_<double>& H, const Point2f& pt, Mat_<double>& A )
|
|
{
|
|
A.create(2,2);
|
|
double p1 = H(0,0)*pt.x + H(0,1)*pt.y + H(0,2),
|
|
p2 = H(1,0)*pt.x + H(1,1)*pt.y + H(1,2),
|
|
p3 = H(2,0)*pt.x + H(2,1)*pt.y + H(2,2),
|
|
p3_2 = p3*p3;
|
|
if( p3 )
|
|
{
|
|
A(0,0) = H(0,0)/p3 - p1*H(2,0)/p3_2; // fxdx
|
|
A(0,1) = H(0,1)/p3 - p1*H(2,1)/p3_2; // fxdy
|
|
|
|
A(1,0) = H(1,0)/p3 - p2*H(2,0)/p3_2; // fydx
|
|
A(1,1) = H(1,1)/p3 - p2*H(2,1)/p3_2; // fydx
|
|
}
|
|
else
|
|
A.setTo(Scalar::all(numeric_limits<double>::max()));
|
|
}
|
|
|
|
class EllipticKeyPoint
|
|
{
|
|
public:
|
|
EllipticKeyPoint();
|
|
EllipticKeyPoint( const Point2f& _center, const Scalar& _ellipse );
|
|
|
|
static Mat_<double> getSecondMomentsMatrix( const Scalar& _ellipse );
|
|
Mat_<double> getSecondMomentsMatrix() const;
|
|
|
|
void calcProjection( const Mat_<double>& H, EllipticKeyPoint& projection ) const;
|
|
|
|
Point2f center;
|
|
Scalar ellipse; // 3 elements a, b, c: ax^2+2bxy+cy^2=1
|
|
Size_<float> axes; // half lenght of elipse axes
|
|
Size_<float> boundingBox; // half sizes of bounding box
|
|
};
|
|
|
|
EllipticKeyPoint::EllipticKeyPoint()
|
|
{
|
|
*this = EllipticKeyPoint(Point2f(0,0), Scalar(1, 0, 1) );
|
|
}
|
|
|
|
EllipticKeyPoint::EllipticKeyPoint( const Point2f& _center, const Scalar& _ellipse )
|
|
{
|
|
center = _center;
|
|
ellipse = _ellipse;
|
|
|
|
Mat_<double> M = getSecondMomentsMatrix(_ellipse), eval;
|
|
eigen( M, eval );
|
|
assert( eval.rows == 2 && eval.cols == 1 );
|
|
axes.width = 1.f / sqrt(eval(0,0));
|
|
axes.height = 1.f / sqrt(eval(1,0));
|
|
|
|
float ac_b2 = ellipse[0]*ellipse[2] - ellipse[1]*ellipse[1];
|
|
boundingBox.width = sqrt(ellipse[2]/ac_b2);
|
|
boundingBox.height = sqrt(ellipse[0]/ac_b2);
|
|
}
|
|
|
|
Mat_<double> EllipticKeyPoint::getSecondMomentsMatrix( const Scalar& _ellipse )
|
|
{
|
|
Mat_<double> M(2, 2);
|
|
M(0,0) = _ellipse[0];
|
|
M(1,0) = M(0,1) = _ellipse[1];
|
|
M(1,1) = _ellipse[2];
|
|
return M;
|
|
}
|
|
|
|
Mat_<double> EllipticKeyPoint::getSecondMomentsMatrix() const
|
|
{
|
|
return getSecondMomentsMatrix(ellipse);
|
|
}
|
|
|
|
void EllipticKeyPoint::calcProjection( const Mat_<double>& H, EllipticKeyPoint& projection ) const
|
|
{
|
|
Point2f dstCenter = applyHomography(H, center);
|
|
|
|
Mat_<double> invM; invert(getSecondMomentsMatrix(), invM);
|
|
Mat_<double> Aff; linearizeHomographyAt(H, center, Aff);
|
|
Mat_<double> dstM; invert(Aff*invM*Aff.t(), dstM);
|
|
|
|
projection = EllipticKeyPoint( dstCenter, Scalar(dstM(0,0), dstM(0,1), dstM(1,1)) );
|
|
}
|
|
|
|
void calcEllipticKeyPointProjections( const vector<EllipticKeyPoint>& src, const Mat_<double>& H, vector<EllipticKeyPoint>& dst )
|
|
{
|
|
if( !src.empty() )
|
|
{
|
|
assert( !H.empty() && H.cols == 3 && H.rows == 3);
|
|
dst.resize(src.size());
|
|
vector<EllipticKeyPoint>::const_iterator srcIt = src.begin();
|
|
vector<EllipticKeyPoint>::iterator dstIt = dst.begin();
|
|
for( ; srcIt != src.end(); ++srcIt, ++dstIt )
|
|
srcIt->calcProjection(H, *dstIt);
|
|
}
|
|
}
|
|
|
|
void transformToEllipticKeyPoints( const vector<KeyPoint>& src, vector<EllipticKeyPoint>& dst )
|
|
{
|
|
if( !src.empty() )
|
|
{
|
|
dst.resize(src.size());
|
|
for( size_t i = 0; i < src.size(); i++ )
|
|
{
|
|
float rad = src[i].size/2;
|
|
assert( rad );
|
|
float fac = 1.f/(rad*rad);
|
|
dst[i] = EllipticKeyPoint( src[i].pt, Scalar(fac, 0, fac) );
|
|
}
|
|
}
|
|
}
|
|
|
|
void transformToKeyPoints( const vector<EllipticKeyPoint>& src, vector<KeyPoint>& dst )
|
|
{
|
|
if( !src.empty() )
|
|
{
|
|
dst.resize(src.size());
|
|
for( size_t i = 0; i < src.size(); i++ )
|
|
{
|
|
Size_<float> axes = src[i].axes;
|
|
float rad = sqrt(axes.height*axes.width);
|
|
dst[i] = KeyPoint(src[i].center, 2*rad );
|
|
}
|
|
}
|
|
}
|
|
|
|
void calcKeyPointProjections( const vector<KeyPoint>& src, const Mat_<double>& H, vector<KeyPoint>& dst )
|
|
{
|
|
if( !src.empty() )
|
|
{
|
|
assert( !H.empty() && H.cols == 3 && H.rows == 3);
|
|
dst.resize(src.size());
|
|
vector<KeyPoint>::const_iterator srcIt = src.begin();
|
|
vector<KeyPoint>::iterator dstIt = dst.begin();
|
|
for( ; srcIt != src.end(); ++srcIt, ++dstIt )
|
|
{
|
|
Point2f dstPt = applyHomography(H, srcIt->pt);
|
|
|
|
float srcSize2 = srcIt->size * srcIt->size;
|
|
Mat_<double> invM; invert(EllipticKeyPoint::getSecondMomentsMatrix( Scalar(1./srcSize2, 0., 1./srcSize2)), invM);
|
|
Mat_<double> Aff; linearizeHomographyAt(H, srcIt->pt, Aff);
|
|
Mat_<double> dstM; invert(Aff*invM*Aff.t(), dstM);
|
|
Mat_<double> eval; eigen( dstM, eval );
|
|
assert( eval(0,0) && eval(1,0) );
|
|
float dstSize = pow(1./(eval(0,0)*eval(1,0)), 0.25);
|
|
|
|
// TODO: check angle projection
|
|
float srcAngleRad = srcIt->angle*CV_PI/180;
|
|
Point2f vec1(cos(srcAngleRad), sin(srcAngleRad)), vec2;
|
|
vec2.x = Aff(0,0)*vec1.x + Aff(0,1)*vec1.y;
|
|
vec2.y = Aff(1,0)*vec1.x + Aff(0,1)*vec1.y;
|
|
float dstAngleGrad = fastAtan2(vec2.y, vec2.x);
|
|
|
|
*dstIt = KeyPoint( dstPt, dstSize, dstAngleGrad, srcIt->response, srcIt->octave, srcIt->class_id );
|
|
}
|
|
}
|
|
}
|
|
|
|
void filterKeyPointsByImageSize( vector<KeyPoint>& keypoints, const Size& imgSize )
|
|
{
|
|
if( !keypoints.empty() )
|
|
{
|
|
vector<KeyPoint> filtered;
|
|
filtered.reserve(keypoints.size());
|
|
Rect r(0, 0, imgSize.width, imgSize.height);
|
|
vector<KeyPoint>::const_iterator it = keypoints.begin();
|
|
for( int i = 0; it != keypoints.end(); ++it, i++ )
|
|
if( r.contains(it->pt) )
|
|
filtered.push_back(*it);
|
|
keypoints.assign(filtered.begin(), filtered.end());
|
|
}
|
|
}
|
|
|
|
/*
|
|
* calulate ovelap errors
|
|
*/
|
|
void overlap( const vector<EllipticKeyPoint>& keypoints1, const vector<EllipticKeyPoint>& keypoints2t, bool commonPart,
|
|
SparseMat_<float>& overlaps )
|
|
{
|
|
overlaps.clear();
|
|
if( keypoints1.empty() || keypoints2t.empty() )
|
|
return;
|
|
|
|
int size[] = { keypoints1.size(), keypoints2t.size() };
|
|
overlaps.create( 2, size );
|
|
|
|
for( size_t i1 = 0; i1 < keypoints1.size(); i1++ )
|
|
{
|
|
EllipticKeyPoint kp1 = keypoints1[i1];
|
|
float maxDist = sqrt(kp1.axes.width*kp1.axes.height),
|
|
fac = 30.f/maxDist;
|
|
if( !commonPart)
|
|
fac=3;
|
|
|
|
maxDist = maxDist*4;
|
|
fac = 1.0/(fac*fac);
|
|
|
|
EllipticKeyPoint keypoint1a = EllipticKeyPoint( kp1.center, Scalar(fac*kp1.ellipse[0], fac*kp1.ellipse[1], fac*kp1.ellipse[2]) );
|
|
|
|
for( size_t i2 = 0; i2 < keypoints2t.size(); i2++ )
|
|
{
|
|
EllipticKeyPoint kp2 = keypoints2t[i2];
|
|
Point2f diff = kp2.center - kp1.center;
|
|
|
|
if( norm(diff) < maxDist )
|
|
{
|
|
EllipticKeyPoint keypoint2a = EllipticKeyPoint( kp2.center, Scalar(fac*kp2.ellipse[0], fac*kp2.ellipse[1], fac*kp2.ellipse[2]) );
|
|
//find the largest eigenvalue
|
|
float maxx = ceil(( keypoint1a.boundingBox.width > (diff.x+keypoint2a.boundingBox.width)) ?
|
|
keypoint1a.boundingBox.width : (diff.x+keypoint2a.boundingBox.width));
|
|
float minx = floor((-keypoint1a.boundingBox.width < (diff.x-keypoint2a.boundingBox.width)) ?
|
|
-keypoint1a.boundingBox.width : (diff.x-keypoint2a.boundingBox.width));
|
|
|
|
float maxy = ceil(( keypoint1a.boundingBox.height > (diff.y+keypoint2a.boundingBox.height)) ?
|
|
keypoint1a.boundingBox.height : (diff.y+keypoint2a.boundingBox.height));
|
|
float miny = floor((-keypoint1a.boundingBox.height < (diff.y-keypoint2a.boundingBox.height)) ?
|
|
-keypoint1a.boundingBox.height : (diff.y-keypoint2a.boundingBox.height));
|
|
float mina = (maxx-minx) < (maxy-miny) ? (maxx-minx) : (maxy-miny) ;
|
|
float dr = mina/50.0;
|
|
float bua = 0, bna = 0;
|
|
//compute the area
|
|
for( float rx1 = minx; rx1 <= maxx; rx1+=dr )
|
|
{
|
|
float rx2 = rx1-diff.x;
|
|
for( float ry1=miny; ry1<=maxy; ry1+=dr )
|
|
{
|
|
float ry2=ry1-diff.y;
|
|
//compute the distance from the ellipse center
|
|
float e1 = keypoint1a.ellipse[0]*rx1*rx1+2*keypoint1a.ellipse[1]*rx1*ry1+keypoint1a.ellipse[2]*ry1*ry1;
|
|
float e2 = keypoint2a.ellipse[0]*rx2*rx2+2*keypoint2a.ellipse[1]*rx2*ry2+keypoint2a.ellipse[2]*ry2*ry2;
|
|
//compute the area
|
|
if( e1<1 && e2<1 ) bna++;
|
|
if( e1<1 || e2<1 ) bua++;
|
|
}
|
|
}
|
|
if( bna > 0)
|
|
overlaps.ref(i1,i2) = 100.0*bna/bua;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void filterEllipticKeyPointsByImageSize( vector<EllipticKeyPoint>& keypoints, const Size& imgSize )
|
|
{
|
|
if( !keypoints.empty() )
|
|
{
|
|
vector<EllipticKeyPoint> filtered;
|
|
filtered.reserve(keypoints.size());
|
|
vector<EllipticKeyPoint>::const_iterator it = keypoints.begin();
|
|
for( int i = 0; it != keypoints.end(); ++it, i++ )
|
|
{
|
|
if( it->center.x + it->boundingBox.width < imgSize.width &&
|
|
it->center.x - it->boundingBox.width > 0 &&
|
|
it->center.y + it->boundingBox.height < imgSize.height &&
|
|
it->center.y - it->boundingBox.height > 0 )
|
|
filtered.push_back(*it);
|
|
}
|
|
keypoints.assign(filtered.begin(), filtered.end());
|
|
}
|
|
}
|
|
|
|
void getEllipticKeyPointsInCommonPart( vector<EllipticKeyPoint>& keypoints1, vector<EllipticKeyPoint>& keypoints2,
|
|
vector<EllipticKeyPoint>& keypoints1t, vector<EllipticKeyPoint>& keypoints2t,
|
|
Size& imgSize1, const Size& imgSize2 )
|
|
{
|
|
filterEllipticKeyPointsByImageSize( keypoints1, imgSize1 );
|
|
filterEllipticKeyPointsByImageSize( keypoints1t, imgSize2 );
|
|
filterEllipticKeyPointsByImageSize( keypoints2, imgSize2 );
|
|
filterEllipticKeyPointsByImageSize( keypoints2t, imgSize1 );
|
|
}
|
|
|
|
void calculateRepeatability( const vector<EllipticKeyPoint>& _keypoints1, const vector<EllipticKeyPoint>& _keypoints2,
|
|
const Mat& img1, const Mat& img2, const Mat& H1to2,
|
|
float& repeatability, int& correspondencesCount,
|
|
SparseMat_<uchar>* thresholdedOverlapMask=0 )
|
|
{
|
|
vector<EllipticKeyPoint> keypoints1( _keypoints1.begin(), _keypoints1.end() ),
|
|
keypoints2( _keypoints2.begin(), _keypoints2.end() ),
|
|
keypoints1t( keypoints1.size() ),
|
|
keypoints2t( keypoints2.size() );
|
|
|
|
// calculate projections of key points
|
|
calcEllipticKeyPointProjections( keypoints1, H1to2, keypoints1t );
|
|
Mat H2to1; invert(H1to2, H2to1);
|
|
calcEllipticKeyPointProjections( keypoints2, H2to1, keypoints2t );
|
|
|
|
bool ifEvaluateDetectors = !thresholdedOverlapMask; // == commonPart
|
|
float overlapThreshold;
|
|
if( ifEvaluateDetectors )
|
|
{
|
|
overlapThreshold = 100.f - 40.f;
|
|
|
|
// remove key points from outside of the common image part
|
|
Size sz1 = img1.size(), sz2 = img2.size();
|
|
getEllipticKeyPointsInCommonPart( keypoints1, keypoints2, keypoints1t, keypoints2t, sz1, sz2 );
|
|
}
|
|
else
|
|
{
|
|
overlapThreshold = 100.f - 50.f;
|
|
}
|
|
int minCount = min( keypoints1.size(), keypoints2t.size() );
|
|
|
|
// calculate overlap errors
|
|
SparseMat_<float> overlaps;
|
|
overlap( keypoints1, keypoints2t, ifEvaluateDetectors, overlaps );
|
|
|
|
correspondencesCount = -1;
|
|
repeatability = -1.f;
|
|
const int* size = overlaps.size();
|
|
if( !size || overlaps.nzcount() == 0 )
|
|
return;
|
|
|
|
// threshold the overlaps
|
|
for( int y = 0; y < size[0]; y++ )
|
|
{
|
|
for( int x = 0; x < size[1]; x++ )
|
|
{
|
|
if ( overlaps(y,x) < overlapThreshold )
|
|
overlaps.erase(y,x);
|
|
}
|
|
}
|
|
if( ifEvaluateDetectors )
|
|
{
|
|
// regions one-to-one matching
|
|
correspondencesCount = 0;
|
|
while( overlaps.nzcount() > 0 )
|
|
{
|
|
double maxOverlap = 0;
|
|
int maxIdx[2];
|
|
minMaxLoc( overlaps, 0, &maxOverlap, 0, maxIdx );
|
|
for( size_t i1 = 0; i1 < keypoints1.size(); i1++ )
|
|
overlaps.erase(i1, maxIdx[1]);
|
|
for( size_t i2 = 0; i2 < keypoints2t.size(); i2++ )
|
|
overlaps.erase(maxIdx[0], i2);
|
|
correspondencesCount++;
|
|
}
|
|
repeatability = minCount ? (float)(correspondencesCount*100)/minCount : -1;
|
|
}
|
|
else
|
|
{
|
|
overlaps.copyTo(*thresholdedOverlapMask);
|
|
}
|
|
}
|
|
|
|
|
|
void evaluateDetectors( const vector<EllipticKeyPoint>& keypoints1, const vector<EllipticKeyPoint>& keypoints2,
|
|
const Mat& img1, const Mat& img2, const Mat& H1to2,
|
|
float& repeatability, int& correspCount )
|
|
{
|
|
calculateRepeatability( keypoints1, keypoints2,
|
|
img1, img2, H1to2,
|
|
repeatability, correspCount );
|
|
}
|
|
|
|
inline float recall( int correctMatchCount, int correspondenceCount )
|
|
{
|
|
return correspondenceCount ? (float)correctMatchCount / (float)correspondenceCount : -1;
|
|
}
|
|
|
|
inline float precision( int correctMatchCount, int falseMatchCount )
|
|
{
|
|
return correctMatchCount + falseMatchCount ? (float)correctMatchCount / (float)(correctMatchCount + falseMatchCount) : -1;
|
|
}
|
|
|
|
void evaluateDescriptors( const vector<EllipticKeyPoint>& keypoints1, const vector<EllipticKeyPoint>& keypoints2,
|
|
vector< pair<DMatch, int> >& matches1to2,
|
|
const Mat& img1, const Mat& img2, const Mat& H1to2,
|
|
int &correctMatchCount, int &falseMatchCount, vector<int> &matchStatuses, int& correspondenceCount )
|
|
{
|
|
assert( !keypoints1.empty() && !keypoints2.empty() && !matches1to2.empty() );
|
|
assert( keypoints1.size() == matches1to2.size() );
|
|
|
|
float repeatability;
|
|
int correspCount;
|
|
SparseMat_<uchar> thresholdedOverlapMask; // thresholded allOverlapErrors
|
|
calculateRepeatability( keypoints1, keypoints2,
|
|
img1, img2, H1to2,
|
|
repeatability, correspCount,
|
|
&thresholdedOverlapMask );
|
|
correspondenceCount = thresholdedOverlapMask.nzcount();
|
|
|
|
matchStatuses.resize( matches1to2.size() );
|
|
correctMatchCount = 0;
|
|
falseMatchCount = 0;
|
|
|
|
//the nearest descriptors should be examined first
|
|
std::sort( matches1to2.begin(), matches1to2.end() );
|
|
|
|
for( size_t i1 = 0; i1 < matches1to2.size(); i1++ )
|
|
{
|
|
int i2 = matches1to2[i1].first.index;
|
|
if( i2 > 0 )
|
|
{
|
|
matchStatuses[i2] = thresholdedOverlapMask(matches1to2[i1].second, i2);
|
|
if( matchStatuses[i2] )
|
|
correctMatchCount++;
|
|
else
|
|
falseMatchCount++;
|
|
}
|
|
else
|
|
matchStatuses[i2] = -1;
|
|
}
|
|
}
|
|
|
|
/****************************************************************************************\
|
|
* Detectors evaluation *
|
|
\****************************************************************************************/
|
|
const int DATASETS_COUNT = 8;
|
|
const int TEST_CASE_COUNT = 5;
|
|
|
|
const string IMAGE_DATASETS_DIR = "detectors_descriptors_evaluation/images_datasets/";
|
|
const string DETECTORS_DIR = "detectors_descriptors_evaluation/detectors/";
|
|
const string DESCRIPTORS_DIR = "detectors_descriptors_evaluation/descriptors/";
|
|
const string KEYPOINTS_DIR = "detectors_descriptors_evaluation/keypoints_datasets/";
|
|
|
|
const string PARAMS_POSTFIX = "_params.xml";
|
|
const string RES_POSTFIX = "_res.xml";
|
|
|
|
const string REPEAT = "repeatability";
|
|
const string CORRESP_COUNT = "correspondence_count";
|
|
|
|
string DATASET_NAMES[DATASETS_COUNT] = { "bark", "bikes", "boat", "graf", "leuven", "trees", "ubc", "wall"};
|
|
|
|
string DEFAULT_PARAMS = "default";
|
|
|
|
string IS_ACTIVE_PARAMS = "isActiveParams";
|
|
string IS_SAVE_KEYPOINTS = "isSaveKeypoints";
|
|
|
|
|
|
class BaseQualityTest : public CvTest
|
|
{
|
|
public:
|
|
BaseQualityTest( const char* _algName, const char* _testName, const char* _testFuncs ) :
|
|
CvTest( _testName, _testFuncs ), algName(_algName)
|
|
{
|
|
//TODO: change this
|
|
isWriteGraphicsData = true;
|
|
}
|
|
|
|
protected:
|
|
virtual string getRunParamsFilename() const = 0;
|
|
virtual string getResultsFilename() const = 0;
|
|
virtual string getPlotPath() const = 0;
|
|
|
|
virtual void validQualityClear( int datasetIdx ) = 0;
|
|
virtual void calcQualityClear( int datasetIdx ) = 0;
|
|
virtual void validQualityCreate( int datasetIdx ) = 0;
|
|
virtual bool isValidQualityEmpty( int datasetIdx ) const = 0;
|
|
virtual bool isCalcQualityEmpty( int datasetIdx ) const = 0;
|
|
|
|
void readAllDatasetsRunParams();
|
|
virtual void readDatasetRunParams( FileNode& fn, int datasetIdx ) = 0;
|
|
void writeAllDatasetsRunParams() const;
|
|
virtual void writeDatasetRunParams( FileStorage& fs, int datasetIdx ) const = 0;
|
|
void setDefaultAllDatasetsRunParams();
|
|
virtual void setDefaultDatasetRunParams( int datasetIdx ) = 0;
|
|
virtual void readDefaultRunParams( FileNode &fn ) {};
|
|
virtual void writeDefaultRunParams( FileStorage &fs ) const {};
|
|
|
|
virtual void readResults();
|
|
virtual void readResults( FileNode& fn, int datasetIdx, int caseIdx ) = 0;
|
|
void writeResults() const;
|
|
virtual void writeResults( FileStorage& fs, int datasetIdx, int caseIdx ) const = 0;
|
|
|
|
bool readDataset( const string& datasetName, vector<Mat>& Hs, vector<Mat>& imgs );
|
|
|
|
virtual void readAlgorithm( ) {};
|
|
virtual void processRunParamsFile () {};
|
|
virtual void runDatasetTest( const vector<Mat> &imgs, const vector<Mat> &Hs, int di, int &progress ) {};
|
|
void run( int );
|
|
|
|
virtual void processResults();
|
|
virtual int processResults( int datasetIdx, int caseIdx ) = 0;
|
|
void writeAllPlotData() const;
|
|
virtual void writePlotData( int datasetIdx ) const {};
|
|
virtual void writeAveragePlotData() const {};
|
|
|
|
string algName;
|
|
bool isWriteParams, isWriteResults, isWriteGraphicsData;
|
|
};
|
|
|
|
void BaseQualityTest::readAllDatasetsRunParams()
|
|
{
|
|
string filename = getRunParamsFilename();
|
|
FileStorage fs( filename, FileStorage::READ );
|
|
if( !fs.isOpened() )
|
|
{
|
|
isWriteParams = true;
|
|
setDefaultAllDatasetsRunParams();
|
|
ts->printf(CvTS::LOG, "all runParams are default\n");
|
|
}
|
|
else
|
|
{
|
|
isWriteParams = false;
|
|
FileNode topfn = fs.getFirstTopLevelNode();
|
|
|
|
FileNode fn = topfn[DEFAULT_PARAMS];
|
|
readDefaultRunParams(fn);
|
|
|
|
for( int i = 0; i < DATASETS_COUNT; i++ )
|
|
{
|
|
FileNode fn = topfn[DATASET_NAMES[i]];
|
|
if( fn.empty() )
|
|
{
|
|
ts->printf( CvTS::LOG, "%d-runParams is default\n", i);
|
|
setDefaultDatasetRunParams(i);
|
|
}
|
|
else
|
|
readDatasetRunParams(fn, i);
|
|
}
|
|
}
|
|
}
|
|
|
|
void BaseQualityTest::writeAllDatasetsRunParams() const
|
|
{
|
|
string filename = getRunParamsFilename();
|
|
FileStorage fs( filename, FileStorage::WRITE );
|
|
if( fs.isOpened() )
|
|
{
|
|
fs << "run_params" << "{"; // top file node
|
|
fs << DEFAULT_PARAMS << "{";
|
|
writeDefaultRunParams(fs);
|
|
fs << "}";
|
|
for( int i = 0; i < DATASETS_COUNT; i++ )
|
|
{
|
|
fs << DATASET_NAMES[i] << "{";
|
|
writeDatasetRunParams(fs, i);
|
|
fs << "}";
|
|
}
|
|
fs << "}";
|
|
}
|
|
else
|
|
ts->printf(CvTS::LOG, "file %s for writing run params can not be opened\n", filename.c_str() );
|
|
}
|
|
|
|
void BaseQualityTest::setDefaultAllDatasetsRunParams()
|
|
{
|
|
for( int i = 0; i < DATASETS_COUNT; i++ )
|
|
setDefaultDatasetRunParams(i);
|
|
}
|
|
|
|
bool BaseQualityTest::readDataset( const string& datasetName, vector<Mat>& Hs, vector<Mat>& imgs )
|
|
{
|
|
Hs.resize( TEST_CASE_COUNT );
|
|
imgs.resize( TEST_CASE_COUNT+1 );
|
|
string dirname = string(ts->get_data_path()) + IMAGE_DATASETS_DIR + datasetName + "/";
|
|
|
|
for( int i = 0; i < (int)Hs.size(); i++ )
|
|
{
|
|
stringstream filename; filename << "H1to" << i+2 << "p.xml";
|
|
FileStorage fs( dirname + filename.str(), FileStorage::READ );
|
|
if( !fs.isOpened() )
|
|
return false;
|
|
fs.getFirstTopLevelNode() >> Hs[i];
|
|
}
|
|
|
|
for( int i = 0; i < (int)imgs.size(); i++ )
|
|
{
|
|
stringstream filename; filename << "img" << i+1 << ".png";
|
|
imgs[i] = imread( dirname + filename.str(), 0 );
|
|
if( imgs[i].empty() )
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void BaseQualityTest::readResults()
|
|
{
|
|
string filename = getResultsFilename();
|
|
FileStorage fs( filename, FileStorage::READ );
|
|
if( fs.isOpened() )
|
|
{
|
|
isWriteResults = false;
|
|
FileNode topfn = fs.getFirstTopLevelNode();
|
|
for( int di = 0; di < DATASETS_COUNT; di++ )
|
|
{
|
|
FileNode datafn = topfn[DATASET_NAMES[di]];
|
|
if( datafn.empty() )
|
|
{
|
|
validQualityClear(di);
|
|
ts->printf( CvTS::LOG, "results for %s dataset were not read\n",
|
|
DATASET_NAMES[di].c_str() );
|
|
}
|
|
else
|
|
{
|
|
validQualityCreate(di);
|
|
for( int ci = 0; ci < TEST_CASE_COUNT; ci++ )
|
|
{
|
|
stringstream ss; ss << "case" << ci;
|
|
FileNode casefn = datafn[ss.str()];
|
|
CV_Assert( !casefn.empty() );
|
|
readResults( casefn , di, ci );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else
|
|
isWriteResults = true;
|
|
}
|
|
|
|
void BaseQualityTest::writeResults() const
|
|
{
|
|
string filename = getResultsFilename();;
|
|
FileStorage fs( filename, FileStorage::WRITE );
|
|
if( fs.isOpened() )
|
|
{
|
|
fs << "results" << "{";
|
|
for( int di = 0; di < DATASETS_COUNT; di++ )
|
|
{
|
|
if( isCalcQualityEmpty(di) )
|
|
{
|
|
ts->printf(CvTS::LOG, "results on %s dataset were not write because of empty\n",
|
|
DATASET_NAMES[di].c_str());
|
|
}
|
|
else
|
|
{
|
|
fs << DATASET_NAMES[di] << "{";
|
|
for( int ci = 0; ci < TEST_CASE_COUNT; ci++ )
|
|
{
|
|
stringstream ss; ss << "case" << ci;
|
|
fs << ss.str() << "{";
|
|
writeResults( fs, di, ci );
|
|
fs << "}"; //ss.str()
|
|
}
|
|
fs << "}"; //DATASET_NAMES[di]
|
|
}
|
|
}
|
|
fs << "}"; //results
|
|
}
|
|
else
|
|
ts->printf(CvTS::LOG, "results were not written because file %s can not be opened\n", filename.c_str() );
|
|
}
|
|
|
|
void BaseQualityTest::processResults()
|
|
{
|
|
if( isWriteParams )
|
|
writeAllDatasetsRunParams();
|
|
if( isWriteGraphicsData )
|
|
writeAllPlotData();
|
|
|
|
int res = CvTS::OK;
|
|
if( isWriteResults )
|
|
writeResults();
|
|
else
|
|
{
|
|
for( int di = 0; di < DATASETS_COUNT; di++ )
|
|
{
|
|
if( isValidQualityEmpty(di) || isCalcQualityEmpty(di) )
|
|
continue;
|
|
|
|
ts->printf(CvTS::LOG, "\nDataset: %s\n", DATASET_NAMES[di].c_str() );
|
|
|
|
for( int ci = 0; ci < TEST_CASE_COUNT; ci++ )
|
|
{
|
|
ts->printf(CvTS::LOG, "case%d\n", ci);
|
|
int currRes = processResults( di, ci );
|
|
res = currRes == CvTS::OK ? res : currRes;
|
|
}
|
|
}
|
|
}
|
|
|
|
if( res != CvTS::OK )
|
|
ts->printf(CvTS::LOG, "BAD ACCURACY\n");
|
|
ts->set_failed_test_info( res );
|
|
}
|
|
|
|
void BaseQualityTest::writeAllPlotData() const
|
|
{
|
|
for( int di = 0; di < DATASETS_COUNT; di++ )
|
|
{
|
|
writePlotData( di );
|
|
}
|
|
writeAveragePlotData();
|
|
}
|
|
|
|
void BaseQualityTest::run ( int )
|
|
{
|
|
readAlgorithm ();
|
|
processRunParamsFile ();
|
|
readResults();
|
|
|
|
int notReadDatasets = 0;
|
|
int progress = 0;
|
|
|
|
FileStorage runParamsFS( getRunParamsFilename(), FileStorage::READ );
|
|
isWriteParams = (! runParamsFS.isOpened());
|
|
FileNode topfn = runParamsFS.getFirstTopLevelNode();
|
|
FileNode defaultParams = topfn[DEFAULT_PARAMS];
|
|
readDefaultRunParams (defaultParams);
|
|
|
|
for(int di = 0; di < DATASETS_COUNT; di++ )
|
|
{
|
|
vector<Mat> imgs, Hs;
|
|
if( !readDataset( DATASET_NAMES[di], Hs, imgs ) )
|
|
{
|
|
calcQualityClear (di);
|
|
ts->printf( CvTS::LOG, "images or homography matrices of dataset named %s can not be read\n",
|
|
DATASET_NAMES[di].c_str());
|
|
notReadDatasets++;
|
|
continue;
|
|
}
|
|
|
|
FileNode fn = topfn[DATASET_NAMES[di]];
|
|
readDatasetRunParams(fn, di);
|
|
|
|
runDatasetTest (imgs, Hs, di, progress);
|
|
}
|
|
if( notReadDatasets == DATASETS_COUNT )
|
|
{
|
|
ts->printf(CvTS::LOG, "All datasets were not be read\n");
|
|
ts->set_failed_test_info( CvTS::FAIL_INVALID_TEST_DATA );
|
|
}
|
|
else
|
|
processResults();
|
|
runParamsFS.release();
|
|
}
|
|
|
|
|
|
|
|
class DetectorQualityTest : public BaseQualityTest
|
|
{
|
|
public:
|
|
DetectorQualityTest( const char* _detectorName, const char* _testName ) :
|
|
BaseQualityTest( _detectorName, _testName, "quality-of-detector" )
|
|
{
|
|
validQuality.resize(DATASETS_COUNT);
|
|
calcQuality.resize(DATASETS_COUNT);
|
|
isSaveKeypoints.resize(DATASETS_COUNT);
|
|
isActiveParams.resize(DATASETS_COUNT);
|
|
|
|
isSaveKeypointsDefault = false;
|
|
isActiveParamsDefault = false;
|
|
}
|
|
|
|
protected:
|
|
using BaseQualityTest::readResults;
|
|
using BaseQualityTest::writeResults;
|
|
using BaseQualityTest::processResults;
|
|
|
|
virtual string getRunParamsFilename() const;
|
|
virtual string getResultsFilename() const;
|
|
virtual string getPlotPath() const;
|
|
|
|
virtual void validQualityClear( int datasetIdx );
|
|
virtual void calcQualityClear( int datasetIdx );
|
|
virtual void validQualityCreate( int datasetIdx );
|
|
virtual bool isValidQualityEmpty( int datasetIdx ) const;
|
|
virtual bool isCalcQualityEmpty( int datasetIdx ) const;
|
|
|
|
virtual void readResults( FileNode& fn, int datasetIdx, int caseIdx );
|
|
virtual void writeResults( FileStorage& fs, int datasetIdx, int caseIdx ) const;
|
|
|
|
virtual void readDatasetRunParams( FileNode& fn, int datasetIdx );
|
|
virtual void writeDatasetRunParams( FileStorage& fs, int datasetIdx ) const;
|
|
virtual void setDefaultDatasetRunParams( int datasetIdx );
|
|
virtual void readDefaultRunParams( FileNode &fn );
|
|
virtual void writeDefaultRunParams( FileStorage &fs ) const;
|
|
|
|
virtual void writePlotData( int di ) const;
|
|
virtual void writeAveragePlotData() const;
|
|
|
|
void openToWriteKeypointsFile( FileStorage& fs, int datasetIdx );
|
|
|
|
virtual void readAlgorithm( );
|
|
virtual void processRunParamsFile () {};
|
|
virtual void runDatasetTest( const vector<Mat> &imgs, const vector<Mat> &Hs, int di, int &progress );
|
|
virtual int processResults( int datasetIdx, int caseIdx );
|
|
|
|
Ptr<FeatureDetector> specificDetector;
|
|
Ptr<FeatureDetector> defaultDetector;
|
|
|
|
struct Quality
|
|
{
|
|
float repeatability;
|
|
int correspondenceCount;
|
|
};
|
|
vector<vector<Quality> > validQuality;
|
|
vector<vector<Quality> > calcQuality;
|
|
|
|
vector<bool> isSaveKeypoints;
|
|
vector<bool> isActiveParams;
|
|
|
|
bool isSaveKeypointsDefault;
|
|
bool isActiveParamsDefault;
|
|
};
|
|
|
|
string DetectorQualityTest::getRunParamsFilename() const
|
|
{
|
|
return string(ts->get_data_path()) + DETECTORS_DIR + algName + PARAMS_POSTFIX;
|
|
}
|
|
|
|
string DetectorQualityTest::getResultsFilename() const
|
|
{
|
|
return string(ts->get_data_path()) + DETECTORS_DIR + algName + RES_POSTFIX;
|
|
}
|
|
|
|
string DetectorQualityTest::getPlotPath() const
|
|
{
|
|
return string(ts->get_data_path()) + DETECTORS_DIR + "plots/";
|
|
}
|
|
|
|
void DetectorQualityTest::validQualityClear( int datasetIdx )
|
|
{
|
|
validQuality[datasetIdx].clear();
|
|
}
|
|
|
|
void DetectorQualityTest::calcQualityClear( int datasetIdx )
|
|
{
|
|
calcQuality[datasetIdx].clear();
|
|
}
|
|
|
|
void DetectorQualityTest::validQualityCreate( int datasetIdx )
|
|
{
|
|
validQuality[datasetIdx].resize(TEST_CASE_COUNT);
|
|
}
|
|
|
|
bool DetectorQualityTest::isValidQualityEmpty( int datasetIdx ) const
|
|
{
|
|
return validQuality[datasetIdx].empty();
|
|
}
|
|
|
|
bool DetectorQualityTest::isCalcQualityEmpty( int datasetIdx ) const
|
|
{
|
|
return calcQuality[datasetIdx].empty();
|
|
}
|
|
|
|
void DetectorQualityTest::readResults( FileNode& fn, int datasetIdx, int caseIdx )
|
|
{
|
|
validQuality[datasetIdx][caseIdx].repeatability = fn[REPEAT];
|
|
validQuality[datasetIdx][caseIdx].correspondenceCount = fn[CORRESP_COUNT];
|
|
}
|
|
|
|
void DetectorQualityTest::writeResults( FileStorage& fs, int datasetIdx, int caseIdx ) const
|
|
{
|
|
fs << REPEAT << calcQuality[datasetIdx][caseIdx].repeatability;
|
|
fs << CORRESP_COUNT << calcQuality[datasetIdx][caseIdx].correspondenceCount;
|
|
}
|
|
|
|
void DetectorQualityTest::readDefaultRunParams (FileNode &fn)
|
|
{
|
|
if (! fn.empty() )
|
|
{
|
|
isSaveKeypointsDefault = (int)fn[IS_SAVE_KEYPOINTS] != 0;
|
|
defaultDetector->read (fn);
|
|
}
|
|
}
|
|
|
|
void DetectorQualityTest::writeDefaultRunParams (FileStorage &fs) const
|
|
{
|
|
fs << IS_SAVE_KEYPOINTS << isSaveKeypointsDefault;
|
|
defaultDetector->write (fs);
|
|
}
|
|
|
|
void DetectorQualityTest::readDatasetRunParams( FileNode& fn, int datasetIdx )
|
|
{
|
|
isActiveParams[datasetIdx] = (int)fn[IS_ACTIVE_PARAMS] != 0;
|
|
if (isActiveParams[datasetIdx])
|
|
{
|
|
isSaveKeypoints[datasetIdx] = (int)fn[IS_SAVE_KEYPOINTS] != 0;
|
|
specificDetector->read (fn);
|
|
}
|
|
else
|
|
{
|
|
setDefaultDatasetRunParams(datasetIdx);
|
|
}
|
|
}
|
|
|
|
void DetectorQualityTest::writeDatasetRunParams( FileStorage& fs, int datasetIdx ) const
|
|
{
|
|
fs << IS_ACTIVE_PARAMS << isActiveParams[datasetIdx];
|
|
fs << IS_SAVE_KEYPOINTS << isSaveKeypoints[datasetIdx];
|
|
defaultDetector->write (fs);
|
|
}
|
|
|
|
void DetectorQualityTest::setDefaultDatasetRunParams( int datasetIdx )
|
|
{
|
|
isSaveKeypoints[datasetIdx] = isSaveKeypointsDefault;
|
|
isActiveParams[datasetIdx] = isActiveParamsDefault;
|
|
}
|
|
|
|
void DetectorQualityTest::writePlotData(int di ) const
|
|
{
|
|
int imgXVals[] = { 2, 3, 4, 5, 6 }; // if scale, blur or light changes
|
|
int viewpointXVals[] = { 20, 30, 40, 50, 60 }; // if viewpoint changes
|
|
int jpegXVals[] = { 60, 80, 90, 95, 98 }; // if jpeg compression
|
|
|
|
int* xVals = 0;
|
|
if( !DATASET_NAMES[di].compare("ubc") )
|
|
{
|
|
xVals = jpegXVals;
|
|
}
|
|
else if( !DATASET_NAMES[di].compare("graf") || !DATASET_NAMES[di].compare("wall") )
|
|
{
|
|
xVals = viewpointXVals;
|
|
}
|
|
else
|
|
xVals = imgXVals;
|
|
|
|
stringstream rFilename, cFilename;
|
|
rFilename << getPlotPath() << algName << "_" << DATASET_NAMES[di] << "_repeatability.csv";
|
|
cFilename << getPlotPath() << algName << "_" << DATASET_NAMES[di] << "_correspondenceCount.csv";
|
|
ofstream rfile(rFilename.str().c_str()), cfile(cFilename.str().c_str());
|
|
for( int ci = 0; ci < TEST_CASE_COUNT; ci++ )
|
|
{
|
|
rfile << xVals[ci] << ", " << calcQuality[di][ci].repeatability << endl;
|
|
cfile << xVals[ci] << ", " << calcQuality[di][ci].correspondenceCount << endl;
|
|
}
|
|
}
|
|
|
|
void DetectorQualityTest::writeAveragePlotData() const
|
|
{
|
|
stringstream rFilename, cFilename;
|
|
rFilename << getPlotPath() << algName << "_average_repeatability.csv";
|
|
cFilename << getPlotPath() << algName << "_average_correspondenceCount.csv";
|
|
ofstream rfile(rFilename.str().c_str()), cfile(cFilename.str().c_str());
|
|
float avRep = 0, avCorCount = 0;
|
|
for( int di = 0; di < DATASETS_COUNT; di++ )
|
|
{
|
|
for( int ci = 0; ci < TEST_CASE_COUNT; ci++ )
|
|
{
|
|
avRep += calcQuality[di][ci].repeatability;
|
|
avCorCount += calcQuality[di][ci].correspondenceCount;
|
|
}
|
|
}
|
|
avRep /= DATASETS_COUNT*TEST_CASE_COUNT;
|
|
avCorCount /= DATASETS_COUNT*TEST_CASE_COUNT;
|
|
rfile << algName << ", " << avRep << endl;
|
|
cfile << algName << ", " << cvRound(avCorCount) << endl;
|
|
}
|
|
|
|
void DetectorQualityTest::openToWriteKeypointsFile( FileStorage& fs, int datasetIdx )
|
|
{
|
|
string filename = string(ts->get_data_path()) + KEYPOINTS_DIR + algName + "_"+
|
|
DATASET_NAMES[datasetIdx] + ".xml.gz" ;
|
|
|
|
fs.open(filename, FileStorage::WRITE);
|
|
if( !fs.isOpened() )
|
|
ts->printf( CvTS::LOG, "keypoints can not be written in file %s because this file can not be opened\n",
|
|
filename.c_str());
|
|
}
|
|
|
|
inline void writeKeypoints( FileStorage& fs, const vector<KeyPoint>& keypoints, int imgIdx )
|
|
{
|
|
if( fs.isOpened() )
|
|
{
|
|
stringstream imgName; imgName << "img" << imgIdx;
|
|
write( fs, imgName.str(), keypoints );
|
|
}
|
|
}
|
|
|
|
inline void readKeypoints( FileStorage& fs, vector<KeyPoint>& keypoints, int imgIdx )
|
|
{
|
|
assert( fs.isOpened() );
|
|
stringstream imgName; imgName << "img" << imgIdx;
|
|
read( fs[imgName.str()], keypoints);
|
|
}
|
|
|
|
void DetectorQualityTest::readAlgorithm ()
|
|
{
|
|
//TODO: use Factory Register when it will be implemented
|
|
if (! algName.compare ("fast"))
|
|
{
|
|
defaultDetector = new FastFeatureDetector(50, true);
|
|
specificDetector = new FastFeatureDetector();
|
|
}
|
|
else if (! algName.compare ("mser"))
|
|
{
|
|
defaultDetector = new MserFeatureDetector();
|
|
specificDetector = new MserFeatureDetector();
|
|
}
|
|
else if (! algName.compare ("star"))
|
|
{
|
|
defaultDetector = new StarFeatureDetector();
|
|
specificDetector = new StarFeatureDetector();
|
|
}
|
|
else if (! algName.compare ("sift"))
|
|
{
|
|
defaultDetector = new SiftFeatureDetector(SIFT::DetectorParams::GET_DEFAULT_THRESHOLD(), 3);
|
|
specificDetector = new SiftFeatureDetector();
|
|
}
|
|
else if (! algName.compare ("surf"))
|
|
{
|
|
defaultDetector = new SurfFeatureDetector(1500);
|
|
specificDetector = new SurfFeatureDetector();
|
|
}
|
|
else
|
|
{
|
|
int maxCorners = 1500;
|
|
double qualityLevel = 0.01;
|
|
double minDistance = 2.0;
|
|
int blockSize=3;
|
|
|
|
if (! algName.compare ("gftt"))
|
|
{
|
|
bool useHarrisDetector = false;
|
|
defaultDetector = new GoodFeaturesToTrackDetector (maxCorners, qualityLevel, minDistance, blockSize, useHarrisDetector);
|
|
specificDetector = new GoodFeaturesToTrackDetector (maxCorners, qualityLevel, minDistance, blockSize, useHarrisDetector);
|
|
}
|
|
else if (! algName.compare ("harris"))
|
|
{
|
|
bool useHarrisDetector = true;
|
|
defaultDetector = new GoodFeaturesToTrackDetector (maxCorners, qualityLevel, minDistance, blockSize, useHarrisDetector);
|
|
specificDetector = new GoodFeaturesToTrackDetector (maxCorners, qualityLevel, minDistance, blockSize, useHarrisDetector);
|
|
}
|
|
else
|
|
{
|
|
ts->printf(CvTS::LOG, "Algorithm can not be read\n");
|
|
ts->set_failed_test_info( CvTS::FAIL_GENERIC);
|
|
}
|
|
}
|
|
}
|
|
|
|
void DetectorQualityTest::runDatasetTest (const vector<Mat> &imgs, const vector<Mat> &Hs, int di, int &progress)
|
|
{
|
|
Ptr<FeatureDetector> detector = isActiveParams[di] ? specificDetector : defaultDetector;
|
|
FileStorage keypontsFS;
|
|
if( isSaveKeypoints[di] )
|
|
openToWriteKeypointsFile( keypontsFS, di );
|
|
|
|
calcQuality[di].resize(TEST_CASE_COUNT);
|
|
|
|
vector<KeyPoint> keypoints1; vector<EllipticKeyPoint> ekeypoints1;
|
|
|
|
detector->detect( imgs[0], keypoints1 );
|
|
writeKeypoints( keypontsFS, keypoints1, 0);
|
|
transformToEllipticKeyPoints( keypoints1, ekeypoints1 );
|
|
int progressCount = DATASETS_COUNT*TEST_CASE_COUNT;
|
|
for( int ci = 0; ci < TEST_CASE_COUNT; ci++ )
|
|
{
|
|
progress = update_progress( progress, di*TEST_CASE_COUNT + ci, progressCount, 0 );
|
|
vector<KeyPoint> keypoints2;
|
|
detector->detect( imgs[ci+1], keypoints2 );
|
|
writeKeypoints( keypontsFS, keypoints2, ci+1);
|
|
vector<EllipticKeyPoint> ekeypoints2;
|
|
transformToEllipticKeyPoints( keypoints2, ekeypoints2 );
|
|
evaluateDetectors( ekeypoints1, ekeypoints2, imgs[0], imgs[ci], Hs[ci],
|
|
calcQuality[di][ci].repeatability, calcQuality[di][ci].correspondenceCount );
|
|
}
|
|
}
|
|
|
|
void testLog( CvTS* ts, bool isBadAccuracy )
|
|
{
|
|
if( isBadAccuracy )
|
|
ts->printf(CvTS::LOG, " bad accuracy\n");
|
|
else
|
|
ts->printf(CvTS::LOG, "\n");
|
|
}
|
|
|
|
int DetectorQualityTest::processResults( int datasetIdx, int caseIdx )
|
|
{
|
|
int res = CvTS::OK;
|
|
|
|
Quality valid = validQuality[datasetIdx][caseIdx], calc = calcQuality[datasetIdx][caseIdx];
|
|
|
|
bool isBadAccuracy;
|
|
int countEps = 1;
|
|
const float rltvEps = 0.001;
|
|
ts->printf(CvTS::LOG, "%s: calc=%f, valid=%f", REPEAT.c_str(), calc.repeatability, valid.repeatability );
|
|
isBadAccuracy = valid.repeatability - calc.repeatability > rltvEps;
|
|
testLog( ts, isBadAccuracy );
|
|
res = isBadAccuracy ? CvTS::FAIL_BAD_ACCURACY : res;
|
|
|
|
ts->printf(CvTS::LOG, "%s: calc=%d, valid=%d", CORRESP_COUNT.c_str(), calc.correspondenceCount, valid.correspondenceCount );
|
|
isBadAccuracy = valid.correspondenceCount - calc.correspondenceCount > countEps;
|
|
testLog( ts, isBadAccuracy );
|
|
res = isBadAccuracy ? CvTS::FAIL_BAD_ACCURACY : res;
|
|
return res;
|
|
}
|
|
|
|
DetectorQualityTest fastDetectorQuality = DetectorQualityTest( "fast", "quality-detector-fast" );
|
|
DetectorQualityTest gfttDetectorQuality = DetectorQualityTest( "gftt", "quality-detector-gftt" );
|
|
DetectorQualityTest harrisDetectorQuality = DetectorQualityTest( "harris", "quality-detector-harris" );
|
|
DetectorQualityTest mserDetectorQuality = DetectorQualityTest( "mser", "quality-detector-mser" );
|
|
DetectorQualityTest starDetectorQuality = DetectorQualityTest( "star", "quality-detector-star" );
|
|
DetectorQualityTest siftDetectorQuality = DetectorQualityTest( "sift", "quality-detector-sift" );
|
|
DetectorQualityTest surfDetectorQuality = DetectorQualityTest( "surf", "quality-detector-surf" );
|
|
|
|
/****************************************************************************************\
|
|
* Descriptors evaluation *
|
|
\****************************************************************************************/
|
|
|
|
const string RECALL = "recall";
|
|
const string PRECISION = "precision";
|
|
|
|
const string KEYPOINTS_FILENAME = "keypointsFilename";
|
|
const string PROJECT_KEYPOINTS_FROM_1IMAGE = "projectKeypointsFrom1Image";
|
|
const string MATCH_FILTER = "matchFilter";
|
|
const string RUN_PARAMS_IS_IDENTICAL = "runParamsIsIdentical";
|
|
|
|
const string ONE_WAY_TRAIN_DIR = "detectors_descriptors_evaluation/one_way_train_images/";
|
|
const string ONE_WAY_IMAGES_LIST = "one_way_train_images.txt";
|
|
|
|
class DescriptorQualityTest : public BaseQualityTest
|
|
{
|
|
public:
|
|
enum{ NO_MATCH_FILTER = 0 };
|
|
DescriptorQualityTest( const char* _descriptorName, const char* _testName ) :
|
|
BaseQualityTest( _descriptorName, _testName, "quality-of-descriptor" )
|
|
{
|
|
validQuality.resize(DATASETS_COUNT);
|
|
calcQuality.resize(DATASETS_COUNT);
|
|
calcDatasetQuality.resize(DATASETS_COUNT);
|
|
commRunParams.resize(DATASETS_COUNT);
|
|
|
|
commRunParamsDefault.projectKeypointsFrom1Image = true;
|
|
commRunParamsDefault.matchFilter = NO_MATCH_FILTER;
|
|
commRunParamsDefault.isActiveParams = false;
|
|
}
|
|
|
|
protected:
|
|
using BaseQualityTest::readResults;
|
|
using BaseQualityTest::writeResults;
|
|
using BaseQualityTest::processResults;
|
|
|
|
virtual string getRunParamsFilename() const;
|
|
virtual string getResultsFilename() const;
|
|
virtual string getPlotPath() const;
|
|
|
|
virtual void validQualityClear( int datasetIdx );
|
|
virtual void calcQualityClear( int datasetIdx );
|
|
virtual void validQualityCreate( int datasetIdx );
|
|
virtual bool isValidQualityEmpty( int datasetIdx ) const;
|
|
virtual bool isCalcQualityEmpty( int datasetIdx ) const;
|
|
|
|
virtual void readResults( FileNode& fn, int datasetIdx, int caseIdx );
|
|
virtual void writeResults( FileStorage& fs, int datasetIdx, int caseIdx ) const;
|
|
|
|
virtual void readDatasetRunParams( FileNode& fn, int datasetIdx ); //
|
|
virtual void writeDatasetRunParams( FileStorage& fs, int datasetIdx ) const;
|
|
virtual void setDefaultDatasetRunParams( int datasetIdx );
|
|
virtual void readDefaultRunParams( FileNode &fn );
|
|
virtual void writeDefaultRunParams( FileStorage &fs ) const;
|
|
|
|
virtual void readAlgorithm( );
|
|
virtual void processRunParamsFile () {};
|
|
virtual void runDatasetTest( const vector<Mat> &imgs, const vector<Mat> &Hs, int di, int &progress );
|
|
|
|
virtual int processResults( int datasetIdx, int caseIdx );
|
|
|
|
virtual void writePlotData( int di ) const;
|
|
|
|
struct Quality
|
|
{
|
|
float recall;
|
|
float precision;
|
|
};
|
|
vector<vector<Quality> > validQuality;
|
|
vector<vector<Quality> > calcQuality;
|
|
vector<vector<Quality> > calcDatasetQuality;
|
|
|
|
struct CommonRunParams
|
|
{
|
|
string keypontsFilename;
|
|
bool projectKeypointsFrom1Image;
|
|
int matchFilter; // not used now
|
|
bool isActiveParams;
|
|
};
|
|
vector<CommonRunParams> commRunParams;
|
|
|
|
Ptr<GenericDescriptorMatch> specificDescMatch;
|
|
Ptr<GenericDescriptorMatch> defaultDescMatch;
|
|
|
|
CommonRunParams commRunParamsDefault;
|
|
};
|
|
|
|
string DescriptorQualityTest::getRunParamsFilename() const
|
|
{
|
|
return string(ts->get_data_path()) + DESCRIPTORS_DIR + algName + PARAMS_POSTFIX;
|
|
}
|
|
|
|
string DescriptorQualityTest::getResultsFilename() const
|
|
{
|
|
return string(ts->get_data_path()) + DESCRIPTORS_DIR + algName + RES_POSTFIX;
|
|
}
|
|
|
|
string DescriptorQualityTest::getPlotPath() const
|
|
{
|
|
return string(ts->get_data_path()) + DESCRIPTORS_DIR + "plots/";
|
|
}
|
|
|
|
void DescriptorQualityTest::validQualityClear( int datasetIdx )
|
|
{
|
|
validQuality[datasetIdx].clear();
|
|
}
|
|
|
|
void DescriptorQualityTest::calcQualityClear( int datasetIdx )
|
|
{
|
|
calcQuality[datasetIdx].clear();
|
|
}
|
|
|
|
void DescriptorQualityTest::validQualityCreate( int datasetIdx )
|
|
{
|
|
validQuality[datasetIdx].resize(TEST_CASE_COUNT);
|
|
}
|
|
|
|
bool DescriptorQualityTest::isValidQualityEmpty( int datasetIdx ) const
|
|
{
|
|
return validQuality[datasetIdx].empty();
|
|
}
|
|
|
|
bool DescriptorQualityTest::isCalcQualityEmpty( int datasetIdx ) const
|
|
{
|
|
return calcQuality[datasetIdx].empty();
|
|
}
|
|
|
|
void DescriptorQualityTest::readResults( FileNode& fn, int datasetIdx, int caseIdx )
|
|
{
|
|
validQuality[datasetIdx][caseIdx].recall = fn[RECALL];
|
|
validQuality[datasetIdx][caseIdx].precision = fn[PRECISION];
|
|
}
|
|
|
|
void DescriptorQualityTest::writeResults( FileStorage& fs, int datasetIdx, int caseIdx ) const
|
|
{
|
|
fs << RECALL << calcQuality[datasetIdx][caseIdx].recall;
|
|
fs << PRECISION << calcQuality[datasetIdx][caseIdx].precision;
|
|
}
|
|
|
|
void DescriptorQualityTest::readDefaultRunParams (FileNode &fn)
|
|
{
|
|
if (! fn.empty() )
|
|
{
|
|
commRunParamsDefault.projectKeypointsFrom1Image = (int)fn[PROJECT_KEYPOINTS_FROM_1IMAGE] != 0;
|
|
commRunParamsDefault.matchFilter = (int)fn[MATCH_FILTER];
|
|
defaultDescMatch->read (fn);
|
|
}
|
|
}
|
|
|
|
void DescriptorQualityTest::writeDefaultRunParams (FileStorage &fs) const
|
|
{
|
|
fs << PROJECT_KEYPOINTS_FROM_1IMAGE << commRunParamsDefault.projectKeypointsFrom1Image;
|
|
fs << MATCH_FILTER << commRunParamsDefault.matchFilter;
|
|
defaultDescMatch->write (fs);
|
|
}
|
|
|
|
void DescriptorQualityTest::readDatasetRunParams( FileNode& fn, int datasetIdx )
|
|
{
|
|
commRunParams[datasetIdx].isActiveParams = (int)fn[IS_ACTIVE_PARAMS];
|
|
if (commRunParams[datasetIdx].isActiveParams)
|
|
{
|
|
commRunParams[datasetIdx].keypontsFilename = (string)fn[KEYPOINTS_FILENAME];
|
|
commRunParams[datasetIdx].projectKeypointsFrom1Image = (int)fn[PROJECT_KEYPOINTS_FROM_1IMAGE] != 0;
|
|
commRunParams[datasetIdx].matchFilter = (int)fn[MATCH_FILTER];
|
|
specificDescMatch->read (fn);
|
|
}
|
|
else
|
|
{
|
|
setDefaultDatasetRunParams(datasetIdx);
|
|
}
|
|
}
|
|
|
|
void DescriptorQualityTest::writeDatasetRunParams( FileStorage& fs, int datasetIdx ) const
|
|
{
|
|
fs << IS_ACTIVE_PARAMS << commRunParams[datasetIdx].isActiveParams;
|
|
fs << KEYPOINTS_FILENAME << commRunParams[datasetIdx].keypontsFilename;
|
|
fs << PROJECT_KEYPOINTS_FROM_1IMAGE << commRunParams[datasetIdx].projectKeypointsFrom1Image;
|
|
fs << MATCH_FILTER << commRunParams[datasetIdx].matchFilter;
|
|
|
|
defaultDescMatch->write (fs);
|
|
}
|
|
|
|
void DescriptorQualityTest::setDefaultDatasetRunParams( int datasetIdx )
|
|
{
|
|
commRunParams[datasetIdx] = commRunParamsDefault;
|
|
commRunParams[datasetIdx].keypontsFilename = "surf_" + DATASET_NAMES[datasetIdx] + ".xml.gz";
|
|
}
|
|
|
|
void DescriptorQualityTest::writePlotData( int di ) const
|
|
{
|
|
stringstream filename;
|
|
filename << getPlotPath() << algName << "_" << DATASET_NAMES[di] << ".csv";
|
|
FILE *file = fopen (filename.str().c_str(), "w");
|
|
size_t size = calcDatasetQuality[di].size();
|
|
for (size_t i=0;i<size;i++)
|
|
{
|
|
fprintf( file, "%f, %f\n", 1 - calcDatasetQuality[di][i].precision, calcDatasetQuality[di][i].recall);
|
|
}
|
|
fclose( file );
|
|
}
|
|
|
|
void DescriptorQualityTest::readAlgorithm( )
|
|
{
|
|
//TODO: use Factory Register when it will be implemented
|
|
if (! algName.compare ("sift"))
|
|
{
|
|
SiftDescriptorExtractor extractor;
|
|
BruteForceMatcher<L2<float> > matcher;
|
|
defaultDescMatch = new VectorDescriptorMatch<SiftDescriptorExtractor, BruteForceMatcher<L2<float> > >(extractor, matcher);
|
|
specificDescMatch = new VectorDescriptorMatch<SiftDescriptorExtractor, BruteForceMatcher<L2<float> > >(extractor, matcher);
|
|
}
|
|
else if (! algName.compare ("surf"))
|
|
{
|
|
SurfDescriptorExtractor extractor;
|
|
BruteForceMatcher<L2<float> > matcher;
|
|
defaultDescMatch = new VectorDescriptorMatch<SurfDescriptorExtractor, BruteForceMatcher<L2<float> > >(extractor, matcher);
|
|
specificDescMatch = new VectorDescriptorMatch<SurfDescriptorExtractor, BruteForceMatcher<L2<float> > >(extractor, matcher);
|
|
}
|
|
else if (! algName.compare ("one_way"))
|
|
{
|
|
defaultDescMatch = new OneWayDescriptorMatch ();
|
|
specificDescMatch = new OneWayDescriptorMatch ();
|
|
}
|
|
else if (! algName.compare ("fern"))
|
|
{
|
|
FernDescriptorMatch::Params params;
|
|
params.nviews = 100;
|
|
params.signatureSize = INT_MAX;
|
|
params.nstructs = 50;
|
|
defaultDescMatch = new FernDescriptorMatch (params);
|
|
specificDescMatch = new FernDescriptorMatch ();
|
|
}
|
|
else if (! algName.compare ("calonder"))
|
|
{
|
|
CalonderDescriptorMatch::Params params;
|
|
params.numTrees = 20;
|
|
params.depth = 7;
|
|
params.views = 100;
|
|
params.reducedNumDim = 100;
|
|
params.patchSize = 20;
|
|
|
|
defaultDescMatch = new CalonderDescriptorMatch (params);
|
|
specificDescMatch = new CalonderDescriptorMatch ();
|
|
}
|
|
else
|
|
{
|
|
ts->printf(CvTS::LOG, "Algorithm can not be read\n");
|
|
ts->set_failed_test_info( CvTS::FAIL_GENERIC);
|
|
}
|
|
}
|
|
|
|
void DescriptorQualityTest::runDatasetTest (const vector<Mat> &imgs, const vector<Mat> &Hs, int di, int &progress)
|
|
{
|
|
FileStorage keypontsFS( string(ts->get_data_path()) + KEYPOINTS_DIR + commRunParams[di].keypontsFilename,
|
|
FileStorage::READ );
|
|
if( !keypontsFS.isOpened())
|
|
{
|
|
calcQuality[di].clear();
|
|
ts->printf( CvTS::LOG, "keypoints from file %s can not be read\n", commRunParams[di].keypontsFilename.c_str() );
|
|
return;
|
|
}
|
|
|
|
Ptr<GenericDescriptorMatch> descMatch = commRunParams[di].isActiveParams ? specificDescMatch : defaultDescMatch;
|
|
calcQuality[di].resize(TEST_CASE_COUNT);
|
|
|
|
vector<KeyPoint> keypoints1; vector<EllipticKeyPoint> ekeypoints1;
|
|
readKeypoints( keypontsFS, keypoints1, 0);
|
|
transformToEllipticKeyPoints( keypoints1, ekeypoints1 );
|
|
|
|
int progressCount = DATASETS_COUNT*TEST_CASE_COUNT;
|
|
vector< pair<DMatch, int> > allMatchings;
|
|
vector<int> allMatchStatuses;
|
|
size_t matchingIndex = 0;
|
|
int allCorrespCount = 0;
|
|
for( int ci = 0; ci < TEST_CASE_COUNT; ci++ )
|
|
{
|
|
progress = update_progress( progress, di*TEST_CASE_COUNT + ci, progressCount, 0 );
|
|
|
|
vector<KeyPoint> keypoints2;
|
|
vector<EllipticKeyPoint> ekeypoints2;
|
|
if( commRunParams[di].projectKeypointsFrom1Image )
|
|
{
|
|
// TODO need to test function calcKeyPointProjections
|
|
calcKeyPointProjections( keypoints1, Hs[ci], keypoints2 );
|
|
filterKeyPointsByImageSize( keypoints2, imgs[ci+1].size() );
|
|
}
|
|
else
|
|
readKeypoints( keypontsFS, keypoints2, ci+1 );
|
|
transformToEllipticKeyPoints( keypoints2, ekeypoints2 );
|
|
descMatch->add( imgs[ci+1], keypoints2 );
|
|
vector<DMatch> matchings1to2;
|
|
descMatch->match( imgs[0], keypoints1, matchings1to2 );
|
|
vector< pair<DMatch, int> > matchings (matchings1to2.size());
|
|
for( size_t i=0;i<matchings1to2.size();i++ )
|
|
matchings[i] = pair<DMatch, int>( matchings1to2[i], i);
|
|
|
|
// TODO if( commRunParams[di].matchFilter )
|
|
int correspCount;
|
|
int correctMatchCount = 0, falseMatchCount = 0;
|
|
vector<int> matchStatuses;
|
|
evaluateDescriptors( ekeypoints1, ekeypoints2, matchings, imgs[0], imgs[ci+1], Hs[ci],
|
|
correctMatchCount, falseMatchCount, matchStatuses, correspCount );
|
|
for( size_t i=0;i<matchings.size();i++ )
|
|
matchings[i].second += matchingIndex;
|
|
matchingIndex += matchings.size();
|
|
|
|
|
|
allCorrespCount += correspCount;
|
|
|
|
//TODO: use merge
|
|
std::copy( matchings.begin(), matchings.end(), std::back_inserter( allMatchings ) );
|
|
std::copy( matchStatuses.begin(), matchStatuses.end(), std::back_inserter( allMatchStatuses ) );
|
|
|
|
printf ("%d %d %d \n", correctMatchCount, falseMatchCount, correspCount );
|
|
|
|
calcQuality[di][ci].recall = recall( correctMatchCount, correspCount );
|
|
calcQuality[di][ci].precision = precision( correctMatchCount, falseMatchCount );
|
|
descMatch->clear ();
|
|
}
|
|
|
|
std::sort( allMatchings.begin(), allMatchings.end() );
|
|
|
|
calcDatasetQuality[di].resize( allMatchings.size() );
|
|
int correctMatchCount = 0, falseMatchCount = 0;
|
|
for( size_t i=0;i<allMatchings.size();i++)
|
|
{
|
|
if( allMatchStatuses[ allMatchings[i].second ] )
|
|
correctMatchCount++;
|
|
else
|
|
falseMatchCount++;
|
|
|
|
calcDatasetQuality[di][i].recall = recall( correctMatchCount, allCorrespCount );
|
|
calcDatasetQuality[di][i].precision = precision( correctMatchCount, falseMatchCount );
|
|
}
|
|
}
|
|
|
|
int DescriptorQualityTest::processResults( int datasetIdx, int caseIdx )
|
|
{
|
|
int res = CvTS::OK;
|
|
Quality valid = validQuality[datasetIdx][caseIdx], calc = calcQuality[datasetIdx][caseIdx];
|
|
|
|
bool isBadAccuracy;
|
|
const float rltvEps = 0.001;
|
|
ts->printf(CvTS::LOG, "%s: calc=%f, valid=%f", RECALL.c_str(), calc.recall, valid.recall );
|
|
isBadAccuracy = valid.recall - calc.recall > rltvEps;
|
|
testLog( ts, isBadAccuracy );
|
|
res = isBadAccuracy ? CvTS::FAIL_BAD_ACCURACY : res;
|
|
|
|
ts->printf(CvTS::LOG, "%s: calc=%f, valid=%f", PRECISION.c_str(), calc.precision, valid.precision );
|
|
isBadAccuracy = valid.precision - calc.precision > rltvEps;
|
|
testLog( ts, isBadAccuracy );
|
|
res = isBadAccuracy ? CvTS::FAIL_BAD_ACCURACY : res;
|
|
|
|
return res;
|
|
}
|
|
|
|
DescriptorQualityTest siftDescriptorQuality = DescriptorQualityTest( "sift", "quality-descriptor-sift" );
|
|
DescriptorQualityTest surfDescriptorQuality = DescriptorQualityTest( "surf", "quality-descriptor-surf" );
|
|
|
|
//--------------------------------- One Way descriptor test --------------------------------------------
|
|
class OneWayDescriptorQualityTest : public DescriptorQualityTest
|
|
{
|
|
public:
|
|
OneWayDescriptorQualityTest() :
|
|
DescriptorQualityTest("one_way", "quality-descriptor-one-way")
|
|
{
|
|
}
|
|
protected:
|
|
virtual void processRunParamsFile ();
|
|
virtual void writeDatasetRunParams( FileStorage& fs, int datasetIdx ) const;
|
|
};
|
|
|
|
void OneWayDescriptorQualityTest::processRunParamsFile ()
|
|
{
|
|
string filename = getRunParamsFilename();
|
|
FileStorage fs = FileStorage (filename, FileStorage::READ);
|
|
FileNode fn = fs.getFirstTopLevelNode();
|
|
fn = fn[DEFAULT_PARAMS];
|
|
|
|
string pcaFilename = string(ts->get_data_path()) + (string)fn["pcaFilename"];
|
|
string trainPath = string(ts->get_data_path()) + (string)fn["trainPath"];
|
|
string trainImagesList = (string)fn["trainImagesList"];
|
|
int patch_width = fn["patchWidth"];
|
|
int patch_height = fn["patchHeight"];
|
|
Size patchSize = cvSize (patch_width, patch_height);
|
|
int poseCount = fn["poseCount"];
|
|
|
|
if (trainImagesList.length () == 0 )
|
|
{
|
|
return;
|
|
fs.release ();
|
|
}
|
|
fs.release ();
|
|
|
|
readAllDatasetsRunParams();
|
|
|
|
OneWayDescriptorBase *base = new OneWayDescriptorBase(patchSize, poseCount, pcaFilename,
|
|
trainPath, trainImagesList);
|
|
|
|
OneWayDescriptorMatch *match = new OneWayDescriptorMatch ();
|
|
match->initialize( OneWayDescriptorMatch::Params (), base );
|
|
defaultDescMatch = match;
|
|
writeAllDatasetsRunParams();
|
|
}
|
|
|
|
void OneWayDescriptorQualityTest::writeDatasetRunParams( FileStorage& fs, int datasetIdx ) const
|
|
{
|
|
fs << IS_ACTIVE_PARAMS << commRunParams[datasetIdx].isActiveParams;
|
|
fs << KEYPOINTS_FILENAME << commRunParams[datasetIdx].keypontsFilename;
|
|
fs << PROJECT_KEYPOINTS_FROM_1IMAGE << commRunParams[datasetIdx].projectKeypointsFrom1Image;
|
|
fs << MATCH_FILTER << commRunParams[datasetIdx].matchFilter;
|
|
}
|
|
|
|
|
|
OneWayDescriptorQualityTest oneWayDescriptorQuality;
|
|
|
|
DescriptorQualityTest fernDescriptorQualityTest( "fern", "quality-descriptor-fern");
|
|
DescriptorQualityTest calonderDescriptorQualityTest( "calonder", "quality-descriptor-calonder");
|