Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
204 changes: 204 additions & 0 deletions traincascade/test/test_features.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -234,3 +234,207 @@ TEST_CASE("CvFeatureEvaluator::setImage: stores class label at the given sample
// Assert
CHECK(evaluator.getCls(2) == doctest::Approx(1.0f));
}

// ---------------------------------------------------------------------------
// setImage / operator() — numerical tests on synthetic images
//
// These tests exercise the feature-evaluation code path end-to-end:
// 1. evaluator.init(...) — generates feature descriptors
// 2. evaluator.setImage(img, ..) — computes integral images / histograms
// 3. evaluator(featureIdx, idx) — evaluates a feature at a sample
//
// Each evaluator has a property that holds for any uniform (constant)
// image, which lets us assert exact numerical values without depending on
// which feature index corresponds to which geometric layout.
// ---------------------------------------------------------------------------

TEST_CASE("CvHaarEvaluator::operator(): returns 0 for every feature on a constant image") {
// Arrange: a constant image has zero variance, so calcNormFactor() is 0
// and CvHaarEvaluator::operator() short-circuits to 0.0f.
CvHaarFeatureParams params(CvHaarFeatureParams::BASIC);
params.maxCatCount = 0;
params.featSize = 1;
CvHaarEvaluator evaluator;
evaluator.init(&params, /*maxSampleCount=*/1, cv::Size(24, 24));
cv::Mat constImg(24, 24, CV_8UC1, cv::Scalar(128));

// Act
evaluator.setImage(constImg, /*clsLabel=*/1, /*idx=*/0);

// Assert: every Haar feature evaluates to exactly 0 on a flat patch.
bool allZero = true;
for (int fi = 0; fi < evaluator.getNumFeatures(); ++fi) {
if (evaluator(fi, 0) != 0.0f) {
allZero = false;
break;
}
}
CHECK(allZero);
CHECK(evaluator.getNumFeatures() > 0);
}

TEST_CASE("CvHaarEvaluator::operator(): returns at least one non-zero value on a textured image") {
// Arrange: a vertical step edge has non-zero variance and breaks the
// Haar feature symmetry — at least one feature must produce a non-zero
// response, otherwise something is wrong with setImage / operator().
CvHaarFeatureParams params(CvHaarFeatureParams::BASIC);
CvHaarEvaluator evaluator;
evaluator.init(&params, /*maxSampleCount=*/1, cv::Size(24, 24));
cv::Mat img(24, 24, CV_8UC1, cv::Scalar(0));
img(cv::Rect(12, 0, 12, 24)).setTo(cv::Scalar(255)); // vertical step edge

// Act
evaluator.setImage(img, /*clsLabel=*/1, /*idx=*/0);

// Assert
bool foundNonZero = false;
for (int fi = 0; fi < evaluator.getNumFeatures() && !foundNonZero; ++fi) {
if (evaluator(fi, 0) != 0.0f) {
foundNonZero = true;
}
}
CHECK(foundNonZero);
}

TEST_CASE("CvHaarEvaluator::setImage: ALL mode also computes the tilted integral") {
// Arrange: ALL mode adds tilted features; the evaluator must still return
// 0 on a constant image because the tilted integral is also flat.
CvHaarFeatureParams params(CvHaarFeatureParams::ALL);
CvHaarEvaluator evaluator;
evaluator.init(&params, /*maxSampleCount=*/1, cv::Size(24, 24));
cv::Mat constImg(24, 24, CV_8UC1, cv::Scalar(64));

// Act
evaluator.setImage(constImg, /*clsLabel=*/0, /*idx=*/0);

// Assert: pick a couple of feature indices spanning the full range.
REQUIRE(evaluator.getNumFeatures() > 1);
CHECK(evaluator(0, 0) == doctest::Approx(0.0f));
CHECK(evaluator(evaluator.getNumFeatures() - 1, 0) == doctest::Approx(0.0f));
// And the class label was stored.
CHECK(evaluator.getCls(0) == doctest::Approx(0.0f));
}

TEST_CASE("CvLBPEvaluator::operator(): returns 255 for every feature on a constant image") {
// Arrange: on a uniform image every 3x3 block sum equals cval, so every
// one of the 8 LBP comparisons (`>= cval`) is true. Result: 0xFF == 255.
CvLBPFeatureParams params;
CvLBPEvaluator evaluator;
evaluator.init(&params, /*maxSampleCount=*/1, cv::Size(24, 24));
cv::Mat constImg(24, 24, CV_8UC1, cv::Scalar(50));

// Act
evaluator.setImage(constImg, /*clsLabel=*/1, /*idx=*/0);

// Assert
REQUIRE(evaluator.getNumFeatures() > 0);
bool allMax = true;
for (int fi = 0; fi < evaluator.getNumFeatures(); ++fi) {
if (evaluator(fi, 0) != 255.0f) {
allMax = false;
break;
}
}
CHECK(allMax);
}

TEST_CASE("CvLBPEvaluator::operator(): produces values < 255 on a non-constant image") {
// Arrange: a horizontal step edge breaks the >= cval invariant for at
// least one comparison in many features.
CvLBPFeatureParams params;
CvLBPEvaluator evaluator;
evaluator.init(&params, /*maxSampleCount=*/1, cv::Size(24, 24));
cv::Mat img(24, 24, CV_8UC1, cv::Scalar(0));
img(cv::Rect(0, 12, 24, 12)).setTo(cv::Scalar(200));

// Act
evaluator.setImage(img, /*clsLabel=*/1, /*idx=*/0);

// Assert: at least one feature must encode a bit pattern other than 0xFF.
bool foundNonMax = false;
for (int fi = 0; fi < evaluator.getNumFeatures() && !foundNonMax; ++fi) {
if (evaluator(fi, 0) < 255.0f) {
foundNonMax = true;
}
}
CHECK(foundNonMax);
}

TEST_CASE("CvLBPEvaluator: setImage isolates samples by index") {
// Arrange: write two different images at indices 0 and 1, then verify
// each sample's evaluation reflects the image stored at that index.
CvLBPFeatureParams params;
CvLBPEvaluator evaluator;
evaluator.init(&params, /*maxSampleCount=*/2, cv::Size(24, 24));
cv::Mat constImg(24, 24, CV_8UC1, cv::Scalar(80));
cv::Mat textImg(24, 24, CV_8UC1, cv::Scalar(0));
textImg(cv::Rect(0, 12, 24, 12)).setTo(cv::Scalar(200));

// Act
evaluator.setImage(constImg, /*clsLabel=*/0, /*idx=*/0);
evaluator.setImage(textImg, /*clsLabel=*/1, /*idx=*/1);

// Assert: sample 0 (constant) -> all features == 255; sample 1 (textured)
// -> at least one feature differs from sample 0.
REQUIRE(evaluator.getNumFeatures() > 0);
CHECK(evaluator(0, 0) == doctest::Approx(255.0f));
bool sample1HasDifferentValue = false;
for (int fi = 0; fi < evaluator.getNumFeatures(); ++fi) {
if (evaluator(fi, 1) != evaluator(fi, 0)) {
sample1HasDifferentValue = true;
break;
}
}
CHECK(sample1HasDifferentValue);
CHECK(evaluator.getCls(0) == doctest::Approx(0.0f));
CHECK(evaluator.getCls(1) == doctest::Approx(1.0f));
}

TEST_CASE("CvHOGEvaluator::operator(): returns 0 for every component on a constant image") {
// Arrange: a constant image has zero gradients, so every HOG bin is 0
// and the implementation's `res > 0.001f` guard returns 0.0f.
CvHOGFeatureParams params;
CvHOGEvaluator evaluator;
evaluator.init(&params, /*maxSampleCount=*/1, cv::Size(32, 32));
cv::Mat constImg(32, 32, CV_8UC1, cv::Scalar(100));

// Act
evaluator.setImage(constImg, /*clsLabel=*/1, /*idx=*/0);

// Assert: getNumFeatures() returns the number of feature blocks, while
// operator() is indexed by varIdx in [0, numFeatures * N_BINS * N_CELLS).
REQUIRE(evaluator.getNumFeatures() > 0);
const int totalVars = evaluator.getNumFeatures() * N_BINS * N_CELLS;
bool allZero = true;
for (int v = 0; v < totalVars; ++v) {
if (evaluator(v, 0) != 0.0f) {
allZero = false;
break;
}
}
CHECK(allZero);
}

TEST_CASE("CvHOGEvaluator::operator(): produces at least one non-zero on a textured image") {
// Arrange
CvHOGFeatureParams params;
CvHOGEvaluator evaluator;
evaluator.init(&params, /*maxSampleCount=*/1, cv::Size(32, 32));
cv::Mat img(32, 32, CV_8UC1, cv::Scalar(0));
img(cv::Rect(16, 0, 16, 32)).setTo(cv::Scalar(255)); // strong vertical edge

// Act
evaluator.setImage(img, /*clsLabel=*/1, /*idx=*/0);

// Assert
REQUIRE(evaluator.getNumFeatures() > 0);
const int totalVars = evaluator.getNumFeatures() * N_BINS * N_CELLS;
bool foundNonZero = false;
for (int v = 0; v < totalVars && !foundNonZero; ++v) {
if (evaluator(v, 0) > 0.0f) {
foundNonZero = true;
}
}
CHECK(foundNonZero);
}