Skip to content
Merged
Show file tree
Hide file tree
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
5 changes: 5 additions & 0 deletions .github/workflows/macos.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
5 changes: 5 additions & 0 deletions .github/workflows/ubuntu.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
15 changes: 15 additions & 0 deletions .github/workflows/windows.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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_<module><ver>.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
3 changes: 3 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ cmake_minimum_required(VERSION 3.10)

project(CascadeClassifier)

include(CTest)
enable_testing()

add_subdirectory(traincascade)

add_subdirectory(tools/annotation)
Expand Down
9 changes: 5 additions & 4 deletions traincascade/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -164,15 +168,12 @@ target_compile_options(
$<$<CXX_COMPILER_ID:MSVC>:/w14191 /w14242 /w14254 /w14263 /w14265 /w14266 /w14287>
$<$<CXX_COMPILER_ID:MSVC>:/w14289 /w14296 /w14311 /w14545 /w14546 /w14547 /w14549>
$<$<CXX_COMPILER_ID:MSVC>:/w14555 /w14619 /w14640 /w14826 /w14905 /w14906 /w14928>
$<$<CXX_COMPILER_ID:MSVC>:/w14365>
# $<$<CXX_COMPILER_ID:MSVC>:/w14365> # Triggered by inline definitions in library headers
$<$<CXX_COMPILER_ID:MSVC>:/w14062 /w14061 /w15262 /w14463 /w14146>
$<$<CXX_COMPILER_ID:MSVC>:/w14456 /w14457 /w14458 /w14459>
$<$<CXX_COMPILER_ID:MSVC>:/w14700 /w14701 /w14703 /w14774 /w14777>
$<$<NOT:$<CXX_COMPILER_ID:MSVC>>:-Wall -Wextra -Wpedantic -Werror>
$<$<NOT:$<CXX_COMPILER_ID:MSVC>>:-Wold-style-cast>
$<$<NOT:$<CXX_COMPILER_ID:MSVC>>:-Wcast-qual>
$<$<NOT:$<CXX_COMPILER_ID:MSVC>>:-Wsign-conversion>
$<$<NOT:$<CXX_COMPILER_ID:MSVC>>:-Wconversion>
$<$<NOT:$<CXX_COMPILER_ID:MSVC>>:-Wshadow -Wnull-dereference -Wmisleading-indentation>
$<$<NOT:$<CXX_COMPILER_ID:MSVC>>:-Wimplicit-fallthrough -Wswitch-enum>
$<$<NOT:$<CXX_COMPILER_ID:MSVC>>:-Wshift-negative-value>
Expand Down
236 changes: 236 additions & 0 deletions traincascade/test/test_features.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
#include <doctest/doctest.h>

#include <opencv2/core.hpp>

#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<CvFeatureParams> p = CvFeatureParams::create(CvFeatureParams::HAAR);

// Assert
REQUIRE(p);
// featSize for simple features is 1.
CHECK(p->featSize == 1);
CHECK(dynamic_cast<CvHaarFeatureParams*>(p.get()) != nullptr);
}

TEST_CASE("CvFeatureParams::create: returns an LBP-typed params object with maxCatCount=256") {
// Arrange / Act
cv::Ptr<CvFeatureParams> p = CvFeatureParams::create(CvFeatureParams::LBP);

// Assert
REQUIRE(p);
CHECK(p->maxCatCount == 256);
CHECK(dynamic_cast<CvLBPFeatureParams*>(p.get()) != nullptr);
}

TEST_CASE("CvFeatureParams::create: returns a HOG-typed params object with featSize=N_BINS*N_CELLS") {
// Arrange / Act
cv::Ptr<CvFeatureParams> p = CvFeatureParams::create(CvFeatureParams::HOG);

// Assert
REQUIRE(p);
CHECK(p->maxCatCount == 0);
CHECK(p->featSize == N_BINS * N_CELLS);
CHECK(dynamic_cast<CvHOGFeatureParams*>(p.get()) != nullptr);
}

TEST_CASE("CvFeatureParams::create: returns an empty Ptr for an unknown feature type") {
// Arrange / Act
cv::Ptr<CvFeatureParams> p = CvFeatureParams::create(99);

// Assert
CHECK(!p);
}

TEST_CASE("CvFeatureEvaluator::create: returns concrete evaluators for known feature types") {
// Arrange / Act
cv::Ptr<CvFeatureEvaluator> haar =
CvFeatureEvaluator::create(CvFeatureParams::HAAR);
cv::Ptr<CvFeatureEvaluator> lbp =
CvFeatureEvaluator::create(CvFeatureParams::LBP);
cv::Ptr<CvFeatureEvaluator> hog =
CvFeatureEvaluator::create(CvFeatureParams::HOG);
cv::Ptr<CvFeatureEvaluator> 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(&params, /*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(&params, /*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(&params, /*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(&params, /*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(&params, /*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));
}
31 changes: 31 additions & 0 deletions traincascade/test/test_imagestorage.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
#include <doctest/doctest.h>

#include <opencv2/core.hpp>

#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);
}
Loading