diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml index e323ccb..a7584fa 100644 --- a/.github/workflows/macos.yml +++ b/.github/workflows/macos.yml @@ -45,3 +45,8 @@ jobs: working-directory: ${{github.workspace}}/build shell: bash run: cmake --build . --config $BUILD_TYPE -- -j 3 + + - name: Test + working-directory: ${{github.workspace}}/build + shell: bash + run: ctest --output-on-failure -C $BUILD_TYPE diff --git a/.github/workflows/ubuntu.yml b/.github/workflows/ubuntu.yml index 4640f17..23ff58d 100644 --- a/.github/workflows/ubuntu.yml +++ b/.github/workflows/ubuntu.yml @@ -36,3 +36,8 @@ jobs: working-directory: ${{github.workspace}}/build shell: bash run: cmake --build . --config $BUILD_TYPE + + - name: Test + working-directory: ${{github.workspace}}/build + shell: bash + run: ctest --output-on-failure -C $BUILD_TYPE diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index 06d2c38..5b4e9b0 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -45,3 +45,18 @@ jobs: working-directory: ${{github.workspace}}/build shell: bash run: cmake --build . --config $BUILD_TYPE + + - name: Test + working-directory: ${{github.workspace}}/build + shell: bash + run: | + # OpenCV is built with BUILD_LIST (no world module) so DLLs are named + # opencv_.dll. Add every directory containing OpenCV DLLs + # to PATH so ctest can launch the test executable. + DLL_DIRS=$(find "${{github.workspace}}/build/external" -iname 'opencv_*.dll' -printf '%h\n' | sort -u) + echo "OpenCV DLL directories:" + echo "$DLL_DIRS" + for d in $DLL_DIRS; do + export PATH="$(cygpath -u "$d"):$PATH" + done + ctest --output-on-failure -C $BUILD_TYPE diff --git a/CMakeLists.txt b/CMakeLists.txt index 2289fa8..92f6353 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,6 +2,9 @@ cmake_minimum_required(VERSION 3.10) project(CascadeClassifier) +include(CTest) +enable_testing() + add_subdirectory(traincascade) add_subdirectory(tools/annotation) diff --git a/traincascade/CMakeLists.txt b/traincascade/CMakeLists.txt index 479514a..975973d 100644 --- a/traincascade/CMakeLists.txt +++ b/traincascade/CMakeLists.txt @@ -146,6 +146,10 @@ include(CTest) add_executable( test_traincascade test/main.cpp + test/test_o_utils.cpp + test/test_params.cpp + test/test_features.cpp + test/test_imagestorage.cpp ) set_target_properties( @@ -164,15 +168,12 @@ target_compile_options( $<$:/w14191 /w14242 /w14254 /w14263 /w14265 /w14266 /w14287> $<$:/w14289 /w14296 /w14311 /w14545 /w14546 /w14547 /w14549> $<$:/w14555 /w14619 /w14640 /w14826 /w14905 /w14906 /w14928> - $<$:/w14365> + # $<$:/w14365> # Triggered by inline definitions in library headers $<$:/w14062 /w14061 /w15262 /w14463 /w14146> $<$:/w14456 /w14457 /w14458 /w14459> $<$:/w14700 /w14701 /w14703 /w14774 /w14777> $<$>:-Wall -Wextra -Wpedantic -Werror> - $<$>:-Wold-style-cast> $<$>:-Wcast-qual> - $<$>:-Wsign-conversion> - $<$>:-Wconversion> $<$>:-Wshadow -Wnull-dereference -Wmisleading-indentation> $<$>:-Wimplicit-fallthrough -Wswitch-enum> $<$>:-Wshift-negative-value> diff --git a/traincascade/test/test_features.cpp b/traincascade/test/test_features.cpp new file mode 100644 index 0000000..118c35b --- /dev/null +++ b/traincascade/test/test_features.cpp @@ -0,0 +1,236 @@ +#include + +#include + +#include "traincascade_features.h" +#include "haarfeatures.h" +#include "lbpfeatures.h" +#include "HOGfeatures.h" + +// --------------------------------------------------------------------------- +// CvFeatureParams factory + base behavior +// --------------------------------------------------------------------------- + +TEST_CASE("CvFeatureParams::create: returns a HAAR-typed params object") { + // Arrange / Act + cv::Ptr p = CvFeatureParams::create(CvFeatureParams::HAAR); + + // Assert + REQUIRE(p); + // featSize for simple features is 1. + CHECK(p->featSize == 1); + CHECK(dynamic_cast(p.get()) != nullptr); +} + +TEST_CASE("CvFeatureParams::create: returns an LBP-typed params object with maxCatCount=256") { + // Arrange / Act + cv::Ptr p = CvFeatureParams::create(CvFeatureParams::LBP); + + // Assert + REQUIRE(p); + CHECK(p->maxCatCount == 256); + CHECK(dynamic_cast(p.get()) != nullptr); +} + +TEST_CASE("CvFeatureParams::create: returns a HOG-typed params object with featSize=N_BINS*N_CELLS") { + // Arrange / Act + cv::Ptr p = CvFeatureParams::create(CvFeatureParams::HOG); + + // Assert + REQUIRE(p); + CHECK(p->maxCatCount == 0); + CHECK(p->featSize == N_BINS * N_CELLS); + CHECK(dynamic_cast(p.get()) != nullptr); +} + +TEST_CASE("CvFeatureParams::create: returns an empty Ptr for an unknown feature type") { + // Arrange / Act + cv::Ptr p = CvFeatureParams::create(99); + + // Assert + CHECK(!p); +} + +TEST_CASE("CvFeatureEvaluator::create: returns concrete evaluators for known feature types") { + // Arrange / Act + cv::Ptr haar = + CvFeatureEvaluator::create(CvFeatureParams::HAAR); + cv::Ptr lbp = + CvFeatureEvaluator::create(CvFeatureParams::LBP); + cv::Ptr hog = + CvFeatureEvaluator::create(CvFeatureParams::HOG); + cv::Ptr bad = CvFeatureEvaluator::create(123); + + // Assert + CHECK(haar); + CHECK(lbp); + CHECK(hog); + CHECK(!bad); +} + +// --------------------------------------------------------------------------- +// CvHaarFeatureParams +// --------------------------------------------------------------------------- + +TEST_CASE("CvHaarFeatureParams: default constructor selects BASIC mode") { + // Arrange / Act + CvHaarFeatureParams p; + + // Assert + CHECK(p.mode == CvHaarFeatureParams::BASIC); +} + +TEST_CASE("CvHaarFeatureParams: explicit constructor stores requested mode") { + // Arrange / Act + CvHaarFeatureParams basic(CvHaarFeatureParams::BASIC); + CvHaarFeatureParams core(CvHaarFeatureParams::CORE); + CvHaarFeatureParams all(CvHaarFeatureParams::ALL); + + // Assert + CHECK(basic.mode == CvHaarFeatureParams::BASIC); + CHECK(core.mode == CvHaarFeatureParams::CORE); + CHECK(all.mode == CvHaarFeatureParams::ALL); +} + +TEST_CASE("CvHaarFeatureParams::scanAttr: rejects an unknown mode value") { + // Arrange + CvHaarFeatureParams p; + + // Act + const bool result = p.scanAttr("-mode", "GARBAGE"); + + // Assert + CHECK_FALSE(result); + CHECK(p.mode == -1); +} + +TEST_CASE("CvHaarFeatureParams::init: copies fields from another instance") { + // Arrange + CvHaarFeatureParams src(CvHaarFeatureParams::ALL); + src.maxCatCount = 7; + src.featSize = 3; + CvHaarFeatureParams dst; + + // Act + dst.init(src); + + // Assert + CHECK(dst.mode == CvHaarFeatureParams::ALL); + CHECK(dst.maxCatCount == 7); + CHECK(dst.featSize == 3); +} + +// --------------------------------------------------------------------------- +// CvLBPFeatureParams / CvHOGFeatureParams +// --------------------------------------------------------------------------- + +TEST_CASE("CvLBPFeatureParams: default constructor sets maxCatCount=256") { + // Arrange / Act + CvLBPFeatureParams p; + + // Assert + CHECK(p.maxCatCount == 256); + CHECK(p.featSize == 1); +} + +TEST_CASE("CvHOGFeatureParams: default constructor sets featSize=N_BINS*N_CELLS") { + // Arrange / Act + CvHOGFeatureParams p; + + // Assert + CHECK(p.maxCatCount == 0); + CHECK(p.featSize == N_BINS * N_CELLS); +} + +// --------------------------------------------------------------------------- +// Evaluator init() and generateFeatures() +// --------------------------------------------------------------------------- + +TEST_CASE("CvHaarEvaluator::init: generates a non-empty feature set for a 24x24 window") { + // Arrange + CvHaarFeatureParams params(CvHaarFeatureParams::BASIC); + params.maxCatCount = 0; + params.featSize = 1; + CvHaarEvaluator evaluator; + + // Act + evaluator.init(¶ms, /*maxSampleCount=*/4, cv::Size(24, 24)); + + // Assert + CHECK(evaluator.getNumFeatures() > 0); + CHECK(evaluator.getFeatureSize() == 1); + CHECK(evaluator.getMaxCatCount() == 0); + CHECK(evaluator.getCls().rows == 4); + CHECK(evaluator.getCls().cols == 1); +} + +TEST_CASE("CvHaarEvaluator: BASIC and ALL produce different feature counts") { + // Arrange + CvHaarFeatureParams basic(CvHaarFeatureParams::BASIC); + basic.maxCatCount = 0; + basic.featSize = 1; + CvHaarFeatureParams all(CvHaarFeatureParams::ALL); + all.maxCatCount = 0; + all.featSize = 1; + CvHaarEvaluator basicEval; + CvHaarEvaluator allEval; + + // Act + basicEval.init(&basic, 1, cv::Size(24, 24)); + allEval.init(&all, 1, cv::Size(24, 24)); + + // Assert: ALL mode includes tilted features and is strictly larger. + CHECK(allEval.getNumFeatures() > basicEval.getNumFeatures()); +} + +TEST_CASE("CvLBPEvaluator::init: generates a non-empty feature set for a 24x24 window") { + // Arrange + CvLBPFeatureParams params; + CvLBPEvaluator evaluator; + + // Act + evaluator.init(¶ms, /*maxSampleCount=*/2, cv::Size(24, 24)); + + // Assert + CHECK(evaluator.getNumFeatures() > 0); + CHECK(evaluator.getMaxCatCount() == 256); +} + +TEST_CASE("CvHOGEvaluator::init: generates a non-empty feature set for a 32x32 window") { + // Arrange + CvHOGFeatureParams params; + CvHOGEvaluator evaluator; + + // Act + evaluator.init(¶ms, /*maxSampleCount=*/2, cv::Size(32, 32)); + + // Assert: HOG features only generated when winSize/2 >= 8. + CHECK(evaluator.getNumFeatures() > 0); + CHECK(evaluator.getFeatureSize() == N_BINS * N_CELLS); +} + +TEST_CASE("CvHOGEvaluator::init: generates no features when window is too small") { + // Arrange: HOG inner loop requires winSize.width/2 >= 8. + CvHOGFeatureParams params; + CvHOGEvaluator evaluator; + + // Act + evaluator.init(¶ms, /*maxSampleCount=*/1, cv::Size(8, 8)); + + // Assert + CHECK(evaluator.getNumFeatures() == 0); +} + +TEST_CASE("CvFeatureEvaluator::setImage: stores class label at the given sample index") { + // Arrange + CvLBPFeatureParams params; + CvLBPEvaluator evaluator; + evaluator.init(¶ms, /*maxSampleCount=*/3, cv::Size(24, 24)); + cv::Mat img(24, 24, CV_8UC1, cv::Scalar(127)); + + // Act + evaluator.setImage(img, /*clsLabel=*/1, /*idx=*/2); + + // Assert + CHECK(evaluator.getCls(2) == doctest::Approx(1.0f)); +} diff --git a/traincascade/test/test_imagestorage.cpp b/traincascade/test/test_imagestorage.cpp new file mode 100644 index 0000000..26c90b6 --- /dev/null +++ b/traincascade/test/test_imagestorage.cpp @@ -0,0 +1,31 @@ +#include + +#include + +#include "imagestorage.h" + +TEST_CASE("CvCascadeImageReader::create: returns false when positive vec file is missing") { + // Arrange + CvCascadeImageReader reader; + + // Act + const bool ok = reader.create("/no/such/file.vec", + "/no/such/bg.txt", + cv::Size(24, 24)); + + // Assert + CHECK_FALSE(ok); +} + +TEST_CASE("CvCascadeImageReader::create: returns false even with a single missing file") { + // Arrange + CvCascadeImageReader reader; + + // Act: same nonexistent path on both arguments — both readers must fail. + const bool ok = reader.create("", + "", + cv::Size(24, 24)); + + // Assert + CHECK_FALSE(ok); +} diff --git a/traincascade/test/test_o_utils.cpp b/traincascade/test/test_o_utils.cpp new file mode 100644 index 0000000..e256310 --- /dev/null +++ b/traincascade/test/test_o_utils.cpp @@ -0,0 +1,366 @@ +#include + +#include +#include +#include + +#include +#include + +#include "o_utils.h" +#include "o_blockedrange.h" +#include "o_cvdtreenode.h" +#include "o_cvdtreesplit.h" + +// --------------------------------------------------------------------------- +// icvCmpIntegers +// --------------------------------------------------------------------------- + +TEST_CASE("icvCmpIntegers: compares two integers via void pointers") { + // Arrange + const int smaller = 1; + const int larger = 5; + const int equal_a = 7; + const int equal_b = 7; + + // Act + const int neg = icvCmpIntegers(&smaller, &larger); + const int pos = icvCmpIntegers(&larger, &smaller); + const int zero = icvCmpIntegers(&equal_a, &equal_b); + + // Assert + CHECK(neg < 0); + CHECK(pos > 0); + CHECK(zero == 0); +} + +TEST_CASE("icvCmpIntegers: works as comparator for qsort") { + // Arrange + std::vector values = {5, 2, 9, 1, 7, 3}; + + // Act + std::qsort(values.data(), values.size(), sizeof(int), icvCmpIntegers); + + // Assert + CHECK(std::is_sorted(values.begin(), values.end())); +} + +// --------------------------------------------------------------------------- +// cvAlign +// --------------------------------------------------------------------------- + +TEST_CASE("cvAlign: aligns to power-of-two boundaries") { + // Arrange / Act / Assert + CHECK(cvAlign(0, 8) == 0); + CHECK(cvAlign(1, 8) == 8); + CHECK(cvAlign(7, 8) == 8); + CHECK(cvAlign(8, 8) == 8); + CHECK(cvAlign(9, 8) == 16); + CHECK(cvAlign(15, 16) == 16); + CHECK(cvAlign(16, 16) == 16); + CHECK(cvAlign(17, 16) == 32); +} + +TEST_CASE("cvAlign: align == 1 acts as identity") { + // Arrange / Act / Assert + CHECK(cvAlign(0, 1) == 0); + CHECK(cvAlign(1, 1) == 1); + CHECK(cvAlign(42, 1) == 42); + CHECK(cvAlign(123, 1) == 123); +} + +// --------------------------------------------------------------------------- +// CV_DTREE_CAT_DIR +// --------------------------------------------------------------------------- + +TEST_CASE("CV_DTREE_CAT_DIR: returns +1 for unset bits and -1 for set bits") { + // Arrange: a 64-bit subset (two 32-bit words) with a known bit pattern. + // Bit 0 set, bit 1 unset, bit 5 set, bit 32 set, bit 33 unset. + int subset[2] = {0, 0}; + subset[0] = (1 << 0) | (1 << 5); + subset[1] = (1 << 0); // bit 32 of the 64-bit "view" + + // Act / Assert + CHECK(CV_DTREE_CAT_DIR(0, subset) == -1); + CHECK(CV_DTREE_CAT_DIR(1, subset) == 1); + CHECK(CV_DTREE_CAT_DIR(2, subset) == 1); + CHECK(CV_DTREE_CAT_DIR(5, subset) == -1); + CHECK(CV_DTREE_CAT_DIR(31, subset) == 1); + CHECK(CV_DTREE_CAT_DIR(32, subset) == -1); + CHECK(CV_DTREE_CAT_DIR(33, subset) == 1); +} + +// --------------------------------------------------------------------------- +// LessThanIdx / LessThanPtr +// --------------------------------------------------------------------------- + +TEST_CASE("LessThanIdx: orders indices by referenced array values") { + // Arrange + const float arr[] = {3.0f, 1.0f, 4.0f, 1.5f, 2.0f}; + std::vector idx = {0, 1, 2, 3, 4}; + + // Act + std::sort(idx.begin(), idx.end(), LessThanIdx(arr)); + + // Assert: indices sorted such that arr[idx[i]] is non-decreasing + for (std::size_t i = 1; i < idx.size(); ++i) { + CHECK(arr[idx[i - 1]] <= arr[idx[i]]); + } +} + +TEST_CASE("LessThanIdx: handles single element and equal values") { + // Arrange + const int singletonArr[] = {42}; + std::vector singletonIdx = {0}; + const int equalArr[] = {7, 7, 7}; + std::vector equalIdx = {0, 1, 2}; + + // Act + std::sort(singletonIdx.begin(), singletonIdx.end(), + LessThanIdx(singletonArr)); + std::sort(equalIdx.begin(), equalIdx.end(), + LessThanIdx(equalArr)); + + // Assert + CHECK(singletonIdx.size() == 1); + CHECK(singletonIdx[0] == 0); + CHECK(equalIdx.size() == 3); // stable order not required, just a no-throw run +} + +TEST_CASE("LessThanPtr: orders pointers by their dereferenced values") { + // Arrange + int a = 10; + int b = 5; + int c = 7; + std::vector ptrs = {&a, &b, &c}; + + // Act + std::sort(ptrs.begin(), ptrs.end(), LessThanPtr()); + + // Assert + CHECK(*ptrs[0] == 5); + CHECK(*ptrs[1] == 7); + CHECK(*ptrs[2] == 10); +} + +// --------------------------------------------------------------------------- +// cvPreprocessIndexArray +// --------------------------------------------------------------------------- + +TEST_CASE("cvPreprocessIndexArray: copies a sorted CV_32SC1 index array") { + // Arrange + int data[] = {0, 2, 4}; + CvMat m = cvMat(1, 3, CV_32SC1, data); + + // Act + CvMat* out = cvPreprocessIndexArray(&m, /*data_arr_size=*/5); + + // Assert + REQUIRE(out != nullptr); + CHECK(CV_MAT_TYPE(out->type) == CV_32SC1); + CHECK(out->cols == 3); + CHECK(out->data.i[0] == 0); + CHECK(out->data.i[1] == 2); + CHECK(out->data.i[2] == 4); + + cvReleaseMat(&out); +} + +TEST_CASE("cvPreprocessIndexArray: expands an 8U mask into selected indices") { + // Arrange: mask with three set entries out of five. + unsigned char mask[] = {1, 0, 1, 0, 1}; + CvMat m = cvMat(1, 5, CV_8UC1, mask); + + // Act + CvMat* out = cvPreprocessIndexArray(&m, /*data_arr_size=*/5); + + // Assert + REQUIRE(out != nullptr); + CHECK(out->cols == 3); + CHECK(out->data.i[0] == 0); + CHECK(out->data.i[1] == 2); + CHECK(out->data.i[2] == 4); + + cvReleaseMat(&out); +} + +TEST_CASE("cvPreprocessIndexArray: throws when no element is selected by mask") { + // Arrange + unsigned char mask[] = {0, 0, 0}; + CvMat m = cvMat(1, 3, CV_8UC1, mask); + + // Act / Assert + CHECK_THROWS_AS(cvPreprocessIndexArray(&m, 3), cv::Exception); +} + +TEST_CASE("cvPreprocessIndexArray: throws on out-of-range integer index") { + // Arrange + int data[] = {0, 1, 99}; + CvMat m = cvMat(1, 3, CV_32SC1, data); + + // Act / Assert + CHECK_THROWS_AS(cvPreprocessIndexArray(&m, /*data_arr_size=*/5), + cv::Exception); +} + +TEST_CASE("cvPreprocessIndexArray: throws on duplicates when checking enabled") { + // Arrange + int data[] = {1, 2, 2, 3}; + CvMat m = cvMat(1, 4, CV_32SC1, data); + + // Act / Assert + CHECK_THROWS_AS(cvPreprocessIndexArray(&m, /*data_arr_size=*/10, + /*check_for_duplicates=*/true), + cv::Exception); +} + +TEST_CASE("cvPreprocessIndexArray: throws when index array is multi-dimensional") { + // Arrange + int data[] = {0, 1, 2, 3}; + CvMat m = cvMat(2, 2, CV_32SC1, data); + + // Act / Assert + CHECK_THROWS_AS(cvPreprocessIndexArray(&m, 4), cv::Exception); +} + +TEST_CASE("cvPreprocessIndexArray: throws on unsupported data type") { + // Arrange + float data[] = {0.0f, 1.0f, 2.0f}; + CvMat m = cvMat(1, 3, CV_32FC1, data); + + // Act / Assert + CHECK_THROWS_AS(cvPreprocessIndexArray(&m, 5), cv::Exception); +} + +// --------------------------------------------------------------------------- +// BlockedRange / parallel_for / parallel_reduce +// --------------------------------------------------------------------------- + +TEST_CASE("BlockedRange: default-constructed range is empty") { + // Arrange / Act + BlockedRange r; + + // Assert + CHECK(r.begin() == 0); + CHECK(r.end() == 0); + CHECK(r.grainsize() == 0); +} + +TEST_CASE("BlockedRange: explicit construction stores bounds and grainsize") { + // Arrange / Act + BlockedRange r(5, 17, 4); + + // Assert + CHECK(r.begin() == 5); + CHECK(r.end() == 17); + CHECK(r.grainsize() == 4); +} + +TEST_CASE("BlockedRange: default grainsize is 1") { + // Arrange / Act + BlockedRange r(0, 10); + + // Assert + CHECK(r.grainsize() == 1); +} + +namespace { +struct SumBody { + mutable long long sum = 0; + void operator()(const BlockedRange& r) const { + for (int i = r.begin(); i < r.end(); ++i) { + sum += i; + } + } +}; +} // namespace + +TEST_CASE("parallel_for: invokes body once over the whole range") { + // Arrange + SumBody body; + BlockedRange r(1, 11); + + // Act + parallel_for(r, body); + + // Assert: 1 + 2 + ... + 10 == 55 + CHECK(body.sum == 55); +} + +TEST_CASE("parallel_for: empty range does not invoke the loop body") { + // Arrange + SumBody body; + BlockedRange r(7, 7); + + // Act + parallel_for(r, body); + + // Assert + CHECK(body.sum == 0); +} + +TEST_CASE("parallel_reduce: forwards range to body and mutates it") { + // Arrange + SumBody body; + BlockedRange r(0, 5); + + // Act + parallel_reduce(r, body); + + // Assert: 0 + 1 + 2 + 3 + 4 == 10 + CHECK(body.sum == 10); +} + +// --------------------------------------------------------------------------- +// CvDTreeNode helpers +// --------------------------------------------------------------------------- + +TEST_CASE("CvDTreeNode::get_num_valid: falls back to sample_count when null") { + // Arrange + CvDTreeNode node{}; + node.sample_count = 42; + node.num_valid = nullptr; + + // Act / Assert + CHECK(node.get_num_valid(0) == 42); + CHECK(node.get_num_valid(99) == 42); +} + +TEST_CASE("CvDTreeNode::get_num_valid: reads from num_valid array when present") { + // Arrange + int valid[] = {3, 7, 11}; + CvDTreeNode node{}; + node.sample_count = 0; + node.num_valid = valid; + + // Act / Assert + CHECK(node.get_num_valid(0) == 3); + CHECK(node.get_num_valid(1) == 7); + CHECK(node.get_num_valid(2) == 11); +} + +TEST_CASE("CvDTreeNode::set_num_valid: writes to array when allocated") { + // Arrange + int valid[] = {0, 0, 0}; + CvDTreeNode node{}; + node.num_valid = valid; + + // Act + node.set_num_valid(1, 99); + + // Assert + CHECK(valid[0] == 0); + CHECK(valid[1] == 99); + CHECK(valid[2] == 0); +} + +TEST_CASE("CvDTreeNode::set_num_valid: is a no-op when num_valid is null") { + // Arrange + CvDTreeNode node{}; + node.num_valid = nullptr; + node.sample_count = 5; + + // Act / Assert (must not crash) + node.set_num_valid(0, 123); + CHECK(node.get_num_valid(0) == 5); +} diff --git a/traincascade/test/test_params.cpp b/traincascade/test/test_params.cpp new file mode 100644 index 0000000..434a66c --- /dev/null +++ b/traincascade/test/test_params.cpp @@ -0,0 +1,234 @@ +#include + +#include + +#include + +#include "o_cvdtreeparams.h" +#include "o_cvboostparams.h" +#include "o_cvboost.h" +#include "boost.h" +#include "cascadeclassifier.h" + +// --------------------------------------------------------------------------- +// CvDTreeParams +// --------------------------------------------------------------------------- + +TEST_CASE("CvDTreeParams: default constructor sets documented defaults") { + // Arrange / Act + CvDTreeParams p; + + // Assert + CHECK(p.max_categories == 10); + CHECK(p.max_depth == INT_MAX); + CHECK(p.min_sample_count == 10); + CHECK(p.cv_folds == 10); + CHECK(p.use_surrogates == true); + CHECK(p.use_1se_rule == true); + CHECK(p.truncate_pruned_tree == true); + CHECK(p.regression_accuracy == doctest::Approx(0.01f)); + CHECK(p.priors == nullptr); +} + +TEST_CASE("CvDTreeParams: parameterized constructor stores all arguments") { + // Arrange + const float priors[] = {0.5f, 0.5f}; + + // Act + CvDTreeParams p(/*max_depth=*/5, + /*min_sample_count=*/20, + /*regression_accuracy=*/0.05f, + /*use_surrogates=*/false, + /*max_categories=*/15, + /*cv_folds=*/3, + /*use_1se_rule=*/false, + /*truncate_pruned_tree=*/false, + priors); + + // Assert + CHECK(p.max_depth == 5); + CHECK(p.min_sample_count == 20); + CHECK(p.regression_accuracy == doctest::Approx(0.05f)); + CHECK(p.use_surrogates == false); + CHECK(p.max_categories == 15); + CHECK(p.cv_folds == 3); + CHECK(p.use_1se_rule == false); + CHECK(p.truncate_pruned_tree == false); + CHECK(p.priors == priors); +} + +// --------------------------------------------------------------------------- +// CvBoostParams +// --------------------------------------------------------------------------- + +TEST_CASE("CvBoostParams: default constructor selects REAL boost & one-level trees") { + // Arrange / Act + CvBoostParams p; + + // Assert + CHECK(p.boost_type == cv::ml::Boost::REAL); + CHECK(p.weak_count == 100); + CHECK(p.weight_trim_rate == doctest::Approx(0.95)); + CHECK(p.cv_folds == 0); + CHECK(p.max_depth == 1); + CHECK(p.split_criteria == CvBoost::DEFAULT); +} + +TEST_CASE("CvBoostParams: parameterized constructor copies arguments") { + // Arrange + const float priors[] = {0.3f, 0.7f}; + + // Act + CvBoostParams p(cv::ml::Boost::GENTLE, + /*weak_count=*/42, + /*weight_trim_rate=*/0.5, + /*max_depth=*/3, + /*use_surrogates=*/true, + priors); + + // Assert + CHECK(p.boost_type == cv::ml::Boost::GENTLE); + CHECK(p.weak_count == 42); + CHECK(p.weight_trim_rate == doctest::Approx(0.5)); + CHECK(p.max_depth == 3); + CHECK(p.use_surrogates == true); + CHECK(p.priors == priors); + CHECK(p.split_criteria == CvBoost::DEFAULT); + CHECK(p.cv_folds == 0); +} + +// --------------------------------------------------------------------------- +// CvCascadeBoostParams +// --------------------------------------------------------------------------- + +TEST_CASE("CvCascadeBoostParams: default constructor uses GENTLE boost & cascade defaults") { + // Arrange / Act + CvCascadeBoostParams p; + + // Assert + CHECK(p.boost_type == cv::ml::Boost::GENTLE); + CHECK(p.minHitRate == doctest::Approx(0.995f)); + CHECK(p.maxFalseAlarm == doctest::Approx(0.5f)); + CHECK(p.use_surrogates == false); + CHECK(p.use_1se_rule == false); + CHECK(p.truncate_pruned_tree == false); +} + +TEST_CASE("CvCascadeBoostParams: parameterized constructor stores cascade-specific values") { + // Arrange / Act + CvCascadeBoostParams p(cv::ml::Boost::DISCRETE, + /*minHitRate=*/0.99f, + /*maxFalseAlarm=*/0.4f, + /*weightTrimRate=*/0.95, + /*maxDepth=*/2, + /*maxWeakCount=*/50); + + // Assert: boost_type is forced to GENTLE in the cascade ctor regardless of input. + CHECK(p.boost_type == cv::ml::Boost::GENTLE); + CHECK(p.minHitRate == doctest::Approx(0.99f)); + CHECK(p.maxFalseAlarm == doctest::Approx(0.4f)); + CHECK(p.weight_trim_rate == doctest::Approx(0.95)); + CHECK(p.max_depth == 2); + CHECK(p.weak_count == 50); + CHECK(p.use_surrogates == false); +} + +TEST_CASE("CvCascadeBoostParams::scanAttr: parses each named attribute") { + // Arrange + CvCascadeBoostParams p; + + // Act / Assert + CHECK(p.scanAttr("-bt", "DAB")); + CHECK(p.boost_type == cv::ml::Boost::DISCRETE); + + CHECK(p.scanAttr("-bt", "RAB")); + CHECK(p.boost_type == cv::ml::Boost::REAL); + + CHECK(p.scanAttr("-bt", "LB")); + CHECK(p.boost_type == cv::ml::Boost::LOGIT); + + CHECK(p.scanAttr("-bt", "GAB")); + CHECK(p.boost_type == cv::ml::Boost::GENTLE); + + CHECK(p.scanAttr("-minHitRate", "0.9")); + CHECK(p.minHitRate == doctest::Approx(0.9f)); + + CHECK(p.scanAttr("-maxFalseAlarmRate", "0.25")); + CHECK(p.maxFalseAlarm == doctest::Approx(0.25f)); + + CHECK(p.scanAttr("-weightTrimRate", "0.8")); + CHECK(p.weight_trim_rate == doctest::Approx(0.8)); + + CHECK(p.scanAttr("-maxDepth", "7")); + CHECK(p.max_depth == 7); + + CHECK(p.scanAttr("-maxWeakCount", "12")); + CHECK(p.weak_count == 12); +} + +TEST_CASE("CvCascadeBoostParams::scanAttr: rejects unknown args and bad boost type") { + // Arrange + CvCascadeBoostParams p; + + // Act / Assert + CHECK_FALSE(p.scanAttr("-unknown", "value")); + CHECK_FALSE(p.scanAttr("-bt", "NOPE")); +} + +// --------------------------------------------------------------------------- +// CvCascadeParams +// --------------------------------------------------------------------------- + +TEST_CASE("CvCascadeParams: default constructor uses BOOST stages, HAAR features, 24x24") { + // Arrange / Act + CvCascadeParams p; + + // Assert + CHECK(p.stageType == CvCascadeParams::BOOST); + CHECK(p.featureType == CvFeatureParams::HAAR); + CHECK(p.winSize.width == 24); + CHECK(p.winSize.height == 24); +} + +TEST_CASE("CvCascadeParams: parameterized constructor stores stage and feature types") { + // Arrange / Act + CvCascadeParams p(CvCascadeParams::BOOST, CvFeatureParams::LBP); + + // Assert + CHECK(p.stageType == CvCascadeParams::BOOST); + CHECK(p.featureType == CvFeatureParams::LBP); + CHECK(p.winSize.width == 24); + CHECK(p.winSize.height == 24); +} + +TEST_CASE("CvCascadeParams::scanAttr: parses width, height and feature type") { + // Arrange + CvCascadeParams p; + + // Act / Assert + CHECK(p.scanAttr("-w", "32")); + CHECK(p.winSize.width == 32); + + CHECK(p.scanAttr("-h", "48")); + CHECK(p.winSize.height == 48); + + CHECK(p.scanAttr("-featureType", "LBP")); + CHECK(p.featureType == CvFeatureParams::LBP); + + CHECK(p.scanAttr("-featureType", "HOG")); + CHECK(p.featureType == CvFeatureParams::HOG); + + CHECK(p.scanAttr("-stageType", "BOOST")); + CHECK(p.stageType == CvCascadeParams::BOOST); +} + +TEST_CASE("CvCascadeParams::scanAttr: returns false for unknown attribute") { + // Arrange + CvCascadeParams p; + + // Act + const bool result = p.scanAttr("-bogus", "0"); + + // Assert + CHECK_FALSE(result); +}