级联分类器检测类CascadeClassifier,在2.4.5版本中使用Adaboost的方法+LBP、HOG、HAAR进行目标检测,加载的是使用traincascade进行训练的分类器
class CV_EXPORTS_W CascadeClassifier
{
public:
CV_WRAP CascadeClassifier(); // 无参数构造函数,new自动调用该函数分配初试内存
CV_WRAP CascadeClassifier( const string& filename ); // 带参数构造函数,参数为XML的绝对名称
virtual ~CascadeClassifier(); // 析构函数,无需关心
CV_WRAP virtual bool empty() const; // 是否导入参数,只创建了该对象而没有加载或者加载失败时都是空的
CV_WRAP bool load( const string& filename ); // 加载分类器,参数为XML的绝对名称,函数内部调用read读取新格式的分类器,读取成功后直接返回,读取失败后调用cvLoad读取旧格式的分类器,读取成功返回true,否则返回false
virtual bool read( const FileNode& node ); // load内部调用read解析XML中的内容,也可以自己创建节点然后调用Read即可,但是该函数只能读取新格式的分类器,不能读取旧格式的分类器
// 多尺度检测函数
CV_WRAP virtual void detectMultiScale( const Mat& image, // 图像,cvarrtoMat实现IplImage转换为Mat,必须为8位,内部可自行转换为灰度图像
CV_OUT vector& objects, // 输出矩形,注意vector不是线程安全的
double scaleFactor=1.1, // 缩放比例,必须大于1
int minNeighbors=3, // 合并窗口时最小neighbor,每个候选矩阵至少包含的附近元素个数
int flags=0, // 检测标记,只对旧格式的分类器有效,与
cvHaarDetectObjects的参数flags相同,
默认为0,可能的取值为CV_HAAR_DO_CANNY_PRUNING(CANNY边缘检测)、CV_HAAR_SCALE_IMAGE(缩放图像)、CV_HAAR_FIND_BIGGEST_OBJECT(寻找最大的目标)、
CV_HAAR_DO_ROUGH_SEARCH(做粗略搜索);如果寻找最大的目标就不能缩放图像,也不能CANNY边缘检测
Size minSize=Size(), // 最小检测目标
Size maxSize=Size() ); // 最大检测目标
// 最好不要在这里设置最大最小,可能会影响合并的效果,因此可以在检测完毕后自行判断结果是否满足要求
CV_WRAP virtual void detectMultiScale( const Mat& image,
CV_OUT vector& objects,
vector& rejectLevels,
vector& levelWeights,
double scaleFactor=1.1,
int minNeighbors=3, int flags=0,
Size minSize=Size(),
Size maxSize=Size(),
bool outputRejectLevels=false );
// 上述参数多了rejectLevels和levelWeights以及
outputRejectLevels参数,只有在
outputRejectLevels为true的时候才可能输出前两个参数
// 还有就是在使用旧分类器的时候必须设置flags为
CV_HAAR_SCALE_IMAGE,可以通过haarcascade_frontalface_alt.xml检测人脸尝试
bool isOldFormatCascade() const; // 是否是旧格式的分类器
virtual Size getOriginalWindowSize() const; // 初始检测窗口大小,也就是训练的窗口
int getFeatureType() const; // 获取特征类型
bool setImage( const Mat& ); // 设置图像,计算图像的积分图
virtual int runAt( Ptr& feval, Point pt, double& weight ); // 计算某检测窗口是否为目标
// 保存强分类器数据
class Data
{
public:
struct CV_EXPORTS DTreeNode // 节点
{
int featureIdx; // 对应的特征编号
float threshold; // for ordered features only 节点阈值
int left; // 左子树
int right; // 右子树
};
struct CV_EXPORTS DTree // 弱分类器
{
int nodeCount; // 弱分类器中节点个数
};
struct CV_EXPORTS Stage // 强分类器
{
int first; // 在classifier中的起始位置
int ntrees; // 该强分类器中的弱分类器数
float threshold; // 强分类器阈值
};
bool read(const FileNode &node); // 读取强分类器
bool isStumpBased; // 是否只有树桩
int stageType; // BOOST,boostType:GAB、RAB等
int featureType; // HAAR、HOG、LBP
int ncategories; // maxCatCount,LBP为256,其余为0
Size origWinSize;
vector stages;
vector classifiers;
vector nodes;
vector leaves;
vector subsets;
};
Data data;
Ptr featureEvaluator;
Ptr oldCascade;
// 关于mask这块参考《OpenCV目标检测之MaskGenerator》
public:
class CV_EXPORTS MaskGenerator
{
public:
virtual ~MaskGenerator() {}
virtual cv::Mat generateMask(const cv::Mat& src)=0;
virtual void initializeMask(const cv::Mat& /*src*/) {};
};
void setMaskGenerator(Ptr maskGenerator);
Ptr getMaskGenerator();
void setFaceDetectionMaskGenerator();
protected:
Ptr maskGenerator;
}
注意:当在不同的分类器之间切换的时候,需要手动释放,因为read内部没有释放上一次读取的分类器数据!
关于新旧格式的分类器参考《OpenCV存储解读之Adaboost分类器》
使用CascadeClassifier检测目标的过程
1) load分类器并调用empty函数检测是否load成功
// 读取stages
bool CascadeClassifier::Data::read(const FileNode &root)
{
static const float THRESHOLD_EPS = 1e-5f;
// load stage params
string stageTypeStr = (string)root[CC_STAGE_TYPE];
if( stageTypeStr == CC_BOOST )
stageType = BOOST;
else
return false;
printf("stageType: %s\n", stageTypeStr.c_str());
string featureTypeStr = (string)root[CC_FEATURE_TYPE];
if( featureTypeStr == CC_HAAR )
featureType = FeatureEvaluator::HAAR;
else if( featureTypeStr == CC_LBP )
featureType = FeatureEvaluator::LBP;
else if( featureTypeStr == CC_HOG )
featureType = FeatureEvaluator::HOG;
else
return false;
printf("featureType: %s\n", featureTypeStr.c_str());
origWinSize.width = (int)root[CC_WIDTH];
origWinSize.height = (int)root[CC_HEIGHT];
CV_Assert( origWinSize.height > 0 && origWinSize.width > 0 );
isStumpBased = (int)(root[CC_STAGE_PARAMS][CC_MAX_DEPTH]) == 1 ? true : false;
printf("stumpBased: %d\n", isStumpBased);
// load feature params
FileNode fn = root[CC_FEATURE_PARAMS];
if( fn.empty() )
return false;
// LBP的maxCatCount=256,其余特征都等于0
ncategories = fn[CC_MAX_CAT_COUNT]; // ncategories=256/0
int subsetSize = (ncategories + 31)/32,// subsetSize=8/0 // 强制类型转换取整,不是四舍五入
nodeStep = 3 + ( ncategories>0 ? subsetSize : 1 ); //每组数值个数,nodeStep=11/4
printf("subsetSize: %d, nodeStep: %d\n", subsetSize, nodeStep);
// load stages
fn = root[CC_STAGES];
if( fn.empty() )
return false;
stages.reserve(fn.size());
classifiers.clear();
nodes.clear();
FileNodeIterator it = fn.begin(), it_end = fn.end();
for( int si = 0; it != it_end; si++, ++it )
{
FileNode fns = *it;
Stage stage;
stage.threshold = (float)fns[CC_STAGE_THRESHOLD] - THRESHOLD_EPS;
fns = fns[CC_WEAK_CLASSIFIERS];
if(fns.empty())
return false;
stage.ntrees = (int)fns.size();
stage.first = (int)classifiers.size();
printf("stage %d: ntrees: %d, first: %d\n", si, stage.ntrees, stage.first);
stages.push_back(stage);
classifiers.reserve(stages[si].first + stages[si].ntrees);
FileNodeIterator it1 = fns.begin(), it1_end = fns.end();
for( ; it1 != it1_end; ++it1 ) // weak trees
{
FileNode fnw = *it1;
FileNode internalNodes = fnw[CC_INTERNAL_NODES];
FileNode leafValues = fnw[CC_LEAF_VALUES];
if( internalNodes.empty() || leafValues.empty() )
return false;
// 弱分类器中的节点
DTree tree;
tree.nodeCount = (int)internalNodes.size()/nodeStep;
classifiers.push_back(tree);
nodes.reserve(nodes.size() + tree.nodeCount);
leaves.reserve(leaves.size() + leafValues.size());
if( subsetSize > 0 ) // 针对LBP
subsets.reserve(subsets.size() + tree.nodeCount*subsetSize);
FileNodeIterator internalNodesIter = internalNodes.begin(), internalNodesEnd = internalNodes.end();
// 保存每一个node
for( ; internalNodesIter != internalNodesEnd; ) // nodes
{
DTreeNode node;
node.left = (int)*internalNodesIter; ++internalNodesIter;
node.right = (int)*internalNodesIter; ++internalNodesIter;
node.featureIdx = (int)*internalNodesIter; ++internalNodesIter;
// 针对LBP,获取8个数值
if( subsetSize > 0 )
{
for( int j = 0; j < subsetSize; j++, ++internalNodesIter )
subsets.push_back((int)*internalNodesIter);
node.threshold = 0.f;
}
else
{
node.threshold = (float)*internalNodesIter; ++internalNodesIter;
}
nodes.push_back(node);
}
// 保存叶子节点
internalNodesIter = leafValues.begin(), internalNodesEnd = leafValues.end();
for( ; internalNodesIter != internalNodesEnd; ++internalNodesIter ) // leaves
leaves.push_back((float)*internalNodesIter);
}
}
return true;
}
// 读取stages与features
bool CascadeClassifier::read(const FileNode& root)
{
// load stages
if( !data.read(root) )
return false;
// load features,参考
《图像特征->XXX特征之OpenCV-估计》
featureEvaluator = FeatureEvaluator::create(data.featureType);
FileNode fn = root[CC_FEATURES];
if( fn.empty() )
return false;
return featureEvaluator->read(fn);
}
// 外部调用的函数
bool CascadeClassifier::load(const string& filename)
{
oldCascade.release();
data = Data();
featureEvaluator.release();
// 读取新格式的分类器
FileStorage fs(filename, FileStorage::READ);
if( !fs.isOpened() )
return false;
if( read(fs.getFirstTopLevelNode()) )
return true;
fs.release();
// 读取新格式失败则读取旧格式的分类器
oldCascade = Ptr((CvHaarClassifierCascade*)cvLoad(filename.c_str(), 0, 0, 0));
return !oldCascade.empty();
}
2) 调用detectMultiScale函数进行多尺度检测,该函数可以使用老分类器进行检测也可以使用新分类器进行检测
2.1 如果load的为旧格式的分类器则使用cvHaarDetectObjectsForROC进行检测,flags参数只对旧格式的分类器有效,参考《OpenCV函数解读之cvHaarDetectObjects》
if( isOldFormatCascade() )
{
MemStorage storage(cvCreateMemStorage(0));
CvMat _image = image;
CvSeq* _objects = cvHaarDetectObjectsForROC( &_image, oldCascade, storage, rejectLevels, levelWeights, scaleFactor,
minNeighbors, flags, minObjectSize, maxObjectSize, outputRejectLevels );
vector vecAvgComp;
Seq(_objects).copyTo(vecAvgComp);
objects.resize(vecAvgComp.size());
std::transform(vecAvgComp.begin(), vecAvgComp.end(), objects.begin(), getRect());
return;
}
2.2 新格式分类器多尺度检测
for( double factor = 1; ; factor *= scaleFactor )
{
Size originalWindowSize = getOriginalWindowSize();
Size windowSize( cvRound(originalWindowSize.width*factor), cvRound(originalWindowSize.height*factor) );
Size scaledImageSize( cvRound( grayImage.cols/factor ), cvRound( grayImage.rows/factor ) );
Size processingRectSize( scaledImageSize.width-originalWindowSize.width + 1, scaledImageSize.height-originalWindowSize.height + 1 );
if( processingRectSize.width <= 0 || processingRectSize.height <= 0 )
break;
if( windowSize.width > maxObjectSize.width || windowSize.height > maxObjectSize.height )
break;
if( windowSize.width < minObjectSize.width || windowSize.height < minObjectSize.height )
continue;
// 缩放图像
Mat scaledImage( scaledImageSize, CV_8U, imageBuffer.data );
resize( grayImage, scaledImage, scaledImageSize, 0, 0, CV_INTER_LINEAR );
// 计算步长
int yStep;
if( getFeatureType() == cv::FeatureEvaluator::HOG )
{
yStep = 4;
}
else
{
yStep = factor > 2. ? 1 : 2;
}
// 并行个数以及大小,按照列进行并行处理
int stripCount, stripSize;
// 是否采用TBB进行优化
#ifdef HAVE_TBB
const int PTS_PER_THREAD = 1000;
stripCount = ((processingRectSize.width/yStep)*(processingRectSize.height + yStep-1)/yStep + PTS_PER_THREAD/2)/PTS_PER_THREAD;
stripCount = std::min(std::max(stripCount, 1), 100);
stripSize = (((processingRectSize.height + stripCount - 1)/stripCount + yStep-1)/yStep)*yStep;
#else
stripCount = 1;
stripSize = processingRectSize.height;
#endif
// 调用单尺度检测函数进行检测
if( !detectSingleScale( scaledImage, stripCount, processingRectSize, stripSize, yStep, factor, candidates,
rejectLevels, levelWeights, outputRejectLevels ) )
break;
}
2.3 合并检测结果
objects.resize(candidates.size());
std::copy(candidates.begin(), candidates.end(), objects.begin());
if( outputRejectLevels )
{
groupRectangles( objects, rejectLevels, levelWeights, minNeighbors, GROUP_EPS );
}
else
{
groupRectangles( objects, minNeighbors, GROUP_EPS );
}
单尺度检测函数流程
2.2.1 根据所载入的特征计算积分图、积分直方图等
// 计算当前图像的积分图,参考《图像特征->XXX特征之OpenCV-估计》
if( !featureEvaluator->setImage( image, data.origWinSize ) )
return false;
2.2.2 根据是否输出检测级数并行目标检测
vector candidatesVector;
vector rejectLevels;
vector levelWeights;
Mutex mtx;
if( outputRejectLevels )
{
parallel_for_(Range(0, stripCount), CascadeClassifierInvoker( *this, processingRectSize, stripSize, yStep, factor,
candidatesVector, rejectLevels, levelWeights, true, currentMask, &mtx));
levels.insert( levels.end(), rejectLevels.begin(), rejectLevels.end() );
weights.insert( weights.end(), levelWeights.begin(), levelWeights.end() );
}
else
{
parallel_for_(Range(0, stripCount), CascadeClassifierInvoker( *this, processingRectSize, stripSize, yStep, factor,
candidatesVector, rejectLevels, levelWeights, false, currentMask, &mtx));
}
candidates.insert( candidates.end(), candidatesVector.begin(), candidatesVector.end() );
CascadeClassifierInvoker函数的operator()实现具体的检测过程
// 对于没有并行时range.start=0,range.end=1
void operator()(const Range& range) const
{
Ptr evaluator = classifier->featureEvaluator->clone();
Size winSize(cvRound(classifier->data.origWinSize.width * scalingFactor),
cvRound(classifier->data.origWinSize.height * scalingFactor));
// strip=processingRectSize.height
int y1 = range.start * stripSize; // 0
int y2 = min(range.end * stripSize, processingRectSize.height); // processSizeRect.height也就是可以处理的高度,已经减去窗口高度
for( int y = y1; y < y2; y += yStep )
{
for( int x = 0; x < processingRectSize.width; x += yStep )
{
if ( (!mask.empty()) && (mask.at(Point(x,y))==0)) {
continue;
}
// result=1表示通过了所有的分类器 <=0表示失败的级数
// gypWeight表示返回的阈值
double gypWeight;
int result = classifier->runAt(evaluator, Point(x, y), gypWeight);
// 输出LOG
#if defined (LOG_CASCADE_STATISTIC)
logger.setPoint(Point(x, y), result);
#endif
// 当返回级数的时候可以最后三个分类器不通过
if( rejectLevels )
{
if( result == 1 )
result = -(int)classifier->data.stages.size();
// 可以最后三个分类器不通过
if( classifier->data.stages.size() + result < 4 )
{
mtx->lock();
rectangles->push_back(Rect(cvRound(x*scalingFactor), cvRound(y*scalingFactor), winSize.width, winSize.height));
rejectLevels->push_back(-result);
levelWeights->push_back(gypWeight);
mtx->unlock();
}
}
// 不返回级数的时候通过所有的分类器才保存起来
else if( result > 0 )
{
mtx->lock();
rectangles->push_back(Rect(cvRound(x*scalingFactor), cvRound(y*scalingFactor),
winSize.width, winSize.height));
mtx->unlock();
}
// 如果一级都没有通过那么加大搜索步长
if( result == 0 )
x += yStep;
}
}
}
runAt函数实现某一检测窗口的检测
int CascadeClassifier::runAt( Ptr& evaluator, Point pt, double& weight )
{
CV_Assert( oldCascade.empty() );
assert( data.featureType == FeatureEvaluator::HAAR ||
data.featureType == FeatureEvaluator::LBP ||
data.featureType == FeatureEvaluator::HOG );
// 设置某一点处的特征,参考
《图像特征->XXX特征之OpenCV-估计》
if( !evaluator->setWindow(pt) )
return -1;
// 如果为树桩,没有树枝
if( data.isStumpBased )
{
if( data.featureType == FeatureEvaluator::HAAR )
return predictOrderedStump( *this, evaluator, weight );
else if( data.featureType == FeatureEvaluator::LBP )
return predictCategoricalStump( *this, evaluator, weight );
else if( data.featureType == FeatureEvaluator::HOG )
return predictOrderedStump( *this, evaluator, weight );
else
return -2;
}
// 每个弱分类器不止一个node
else
{
if( data.featureType == FeatureEvaluator::HAAR )
return predictOrdered( *this, evaluator, weight );
else if( data.featureType == FeatureEvaluator::LBP )
return predictCategorical( *this, evaluator, weight );
else if( data.featureType == FeatureEvaluator::HOG )
return predictOrdered( *this, evaluator, weight );
else
return -2;
}
}
predictOrdered*函数实现判断当前检测窗口的判断
// HAAR与HOG特征的多node检测
template
inline int predictOrdered( CascadeClassifier& cascade, Ptr &_featureEvaluator, double& sum )
{
int nstages = (int)cascade.data.stages.size();
int nodeOfs = 0, leafOfs = 0;
FEval& featureEvaluator = (FEval&)*_featureEvaluator;
float* cascadeLeaves = &cascade.data.leaves[0];
CascadeClassifier::Data::DTreeNode* cascadeNodes = &cascade.data.nodes[0];
CascadeClassifier::Data::DTree* cascadeWeaks = &cascade.data.classifiers[0];
CascadeClassifier::Data::Stage* cascadeStages = &cascade.data.stages[0];
// 遍历每个强分类器
for( int si = 0; si < nstages; si++ )
{
CascadeClassifier::Data::Stage& stage = cascadeStages[si];
int wi, ntrees = stage.ntrees;
sum = 0;
// 遍历每个弱分类器
for( wi = 0; wi < ntrees; wi++ )
{
CascadeClassifier::Data::DTree& weak = cascadeWeaks[stage.first + wi];
int idx = 0, root = nodeOfs;
// 遍历每个节点
do
{
// 选择一个node:root和idx初始化为0,即第一个node
CascadeClassifier::Data::DTreeNode& node = cascadeNodes[root + idx];
// 计算当前node特征池编号下的特征值
double val = featureEvaluator(node.featureIdx);
// 如果val小于node阈值则选择左子树,否则选择右子树
idx = val < node.threshold ? node.left : node.right;
}
while( idx > 0 );
// 累加最终的叶子节点
sum += cascadeLeaves[leafOfs - idx];
nodeOfs += weak.nodeCount;
leafOfs += weak.nodeCount + 1;
}
// 判断所有叶子节点累加和是否小于强分类器阈值,小于强分类器阈值则失败
if( sum < stage.threshold )
return -si;
}
// 通过了所有的强分类器返回1,否则返回失败的分类器
return 1;
}
// LBP特征的多node检测
template
inline int predictCategorical( CascadeClassifier& cascade, Ptr &_featureEvaluator, double& sum )
{
int nstages = (int)cascade.data.stages.size();
int nodeOfs = 0, leafOfs = 0;
FEval& featureEvaluator = (FEval&)*_featureEvaluator;
size_t subsetSize = (cascade.data.ncategories + 31)/32;
int* cascadeSubsets = &cascade.data.subsets[0];
float* cascadeLeaves = &cascade.data.leaves[0];
CascadeClassifier::Data::DTreeNode* cascadeNodes = &cascade.data.nodes[0];
CascadeClassifier::Data::DTree* cascadeWeaks = &cascade.data.classifiers[0];
CascadeClassifier::Data::Stage* cascadeStages = &cascade.data.stages[0];
for(int si = 0; si < nstages; si++ )
{
CascadeClassifier::Data::Stage& stage = cascadeStages[si];
int wi, ntrees = stage.ntrees;
sum = 0;
for( wi = 0; wi < ntrees; wi++ )
{
CascadeClassifier::Data::DTree& weak = cascadeWeaks[stage.first + wi];
int idx = 0, root = nodeOfs;
do
{
CascadeClassifier::Data::DTreeNode& node = cascadeNodes[root + idx];
// c为0-255之间的数
int c = featureEvaluator(node.featureIdx);
// 获取当前node的subset头位置
const int* subset = &cascadeSubsets[(root + idx)*subsetSize]; // LBP:subsetSize=8
// 判断选择左子树还是右子树
idx = (subset[c>>5] & (1 << (c & 31))) ? node.left : node.right;
// c>>5表示将c右移5位,选择高3位,0-7之间
// c&31表示低5位,1<<(c&31)选择低5位后左移1位
// 将上面的数按位与,如果最后结果不为0表示选择左子树,否则选择右子树
}
while( idx > 0 );
sum += cascadeLeaves[leafOfs - idx];
nodeOfs += weak.nodeCount;
leafOfs += weak.nodeCount + 1;
}
if( sum < stage.threshold )
return -si;
}
return 1;
}
// HAAR与HOG特征的单node检测
template
inline int predictOrderedStump( CascadeClassifier& cascade, Ptr &_featureEvaluator, double& sum )
{
int nodeOfs = 0, leafOfs = 0;
FEval& featureEvaluator = (FEval&)*_featureEvaluator;
float* cascadeLeaves = &cascade.data.leaves[0];
CascadeClassifier::Data::DTreeNode* cascadeNodes = &cascade.data.nodes[0];
CascadeClassifier::Data::Stage* cascadeStages = &cascade.data.stages[0];
int nstages = (int)cascade.data.stages.size();
// 遍历每个强分类器
for( int stageIdx = 0; stageIdx < nstages; stageIdx++ )
{
CascadeClassifier::Data::Stage& stage = cascadeStages[stageIdx];
sum = 0.0;
int ntrees = stage.ntrees;
// 遍历每个弱分类器
for( int i = 0; i < ntrees; i++, nodeOfs++, leafOfs+= 2 )
{
CascadeClassifier::Data::DTreeNode& node = cascadeNodes[nodeOfs];
double value = featureEvaluator(node.featureIdx);
sum += cascadeLeaves[ value < node.threshold ? leafOfs : leafOfs + 1 ];
}
if( sum < stage.threshold )
return -stageIdx;
}
return 1;
}
// LBP特征的单node检测
template
inline int predictCategoricalStump( CascadeClassifier& cascade, Ptr &_featureEvaluator, double& sum )
{
int nstages = (int)cascade.data.stages.size();
int nodeOfs = 0, leafOfs = 0;
FEval& featureEvaluator = (FEval&)*_featureEvaluator;
size_t subsetSize = (cascade.data.ncategories + 31)/32;
int* cascadeSubsets = &cascade.data.subsets[0];
float* cascadeLeaves = &cascade.data.leaves[0];
CascadeClassifier::Data::DTreeNode* cascadeNodes = &cascade.data.nodes[0];
CascadeClassifier::Data::Stage* cascadeStages = &cascade.data.stages[0];
#ifdef HAVE_TEGRA_OPTIMIZATION
float tmp = 0; // float accumulator -- float operations are quicker
#endif
for( int si = 0; si < nstages; si++ )
{
CascadeClassifier::Data::Stage& stage = cascadeStages[si];
int wi, ntrees = stage.ntrees;
#ifdef HAVE_TEGRA_OPTIMIZATION
tmp = 0;
#else
sum = 0;
#endif
for( wi = 0; wi < ntrees; wi++ )
{
CascadeClassifier::Data::DTreeNode& node = cascadeNodes[nodeOfs];
int c = featureEvaluator(node.featureIdx);
const int* subset = &cascadeSubsets[nodeOfs*subsetSize];
#ifdef HAVE_TEGRA_OPTIMIZATION
tmp += cascadeLeaves[ subset[c>>5] & (1 << (c & 31)) ? leafOfs : leafOfs+1];
#else
sum += cascadeLeaves[ subset[c>>5] & (1 << (c & 31)) ? leafOfs : leafOfs+1];
#endif
nodeOfs++;
leafOfs += 2;
}
#ifdef HAVE_TEGRA_OPTIMIZATION
if( tmp < stage.threshold ) {
sum = (double)tmp;
return -si;
}
#else
if( sum < stage.threshold )
return -si;
#endif
}
#ifdef HAVE_TEGRA_OPTIMIZATION
sum = (double)tmp;
#endif
return 1;
}
}