由于从头开始实现一个完整、高效、可生产使用的 Haar + AdaBoost 系统是一个非常庞大且复杂的工程(涉及到图像金字塔、积分图、级联分类器等),我们将分步进行,重点讲解核心算法的原理和 C 语言实现。

本教程将分为以下几个部分:
- 核心概念回顾:快速回顾 Haar 特征和 AdaBoost 的原理。
- C 语言实现步骤:分模块讲解代码实现。
- 步骤 1:Haar 特征的计算(使用积分图)
- 步骤 2:AdaBoost 训练算法
- 步骤 3:将 AdaBoost 组合成一个强分类器(级联的第一步)
- 完整示例代码:提供可编译运行的 C 语言代码,并附上详细注释。
- 如何扩展与优化:指出通往生产级实现的路径。
核心概念回顾
Haar 特征
Haar 特征是一种简单的矩形特征,用于快速描述图像中的局部模式。
- 两矩形特征:可以检测边缘、线条。
- 三矩形特征:可以检测中心亮/暗的边缘。
- 四矩形特征:可以检测角点或中心亮/暗的块。
直接计算所有可能的 Haar 特征在图像的所有位置和所有尺度上计算量巨大,为了加速,我们使用 积分图,积分图可以在 O(1) 时间内计算出任意矩形区域的像素和,从而极大地加速 Haar 特征的计算。
AdaBoost (Adaptive Boosting)
AdaBoost 是一种集成学习算法,它的核心思想是:

- 训练一系列“弱分类器”(Weak Learners),在 Haar+AdaBoost 中,一个弱分类器通常是一个简单的“决策桩”(Decision Stump),即基于 一个 Haar 特征 和一个 阈值 来进行判断。
if (feature_value >= threshold) return class_A; else return class_B;
- 每次训练后,那些被错误分类的样本会获得更高的权重。
- 下一个弱分类器会更专注于那些被前一个分类器分错的“困难样本”。
- 将所有训练好的弱分类器加权组合起来,形成一个强大的强分类器。
级联分类器
一个强分类器仍然需要计算很多 Haar 特征,速度不够快,OpenCV 等实际应用使用 级联分类器。
- 它由多个“阶段”(Stage)组成,每个阶段都是一个强分类器。
- 每个阶段内部又包含多个弱分类器。
- 工作流程:将待检测的窗口依次通过每个阶段。
- 如果任何一个阶段判定为“非人脸”(负样本),则立即丢弃该窗口,不再计算后续特征,极大地提高了速度。
- 只有通过所有阶段的窗口,才被最终判定为“人脸”。
本教程将专注于实现 单个强分类器 的训练,这是理解级联分类器的基础。
C 语言实现步骤
我们将实现一个简化版的训练流程。
准备工作:数据格式
为了简化,我们不使用图像文件,而是使用预处理好的数据文件,假设我们有两个文件:

positives.dat: 包含所有正样本(人脸)的积分图数据。negatives.dat: 包含所有负样本(非人脸)的积分图数据。
每个样本的数据格式可以这样定义:
typedef struct {
int width;
int height;
unsigned long long *integral_image; // 指向积分图数据的指针
} Sample;
integral_image 是一个一维数组,大小为 width * height,存储了计算好的积分图值。
步骤 1:Haar 特征的计算(使用积分图)
我们需要定义 Haar 特征,一个特征由它的类型、位置和大小决定。
// Haar 特征类型
typedef enum {
TWO_RECT_H, // 水平两矩形
TWO_RECT_V, // 垂直两矩形
THREE_RECT_H,// 水平三矩形
FOUR_RECT // 四矩形
} HaarFeatureType;
typedef struct {
HaarFeatureType type;
int x; // 特征在样本窗口中的左上角 x 坐标
int y; // 特征在样本窗口中的左上角 y 坐标
int width; // 特征的宽度
int height; // 特征的高度
} HaarFeature;
我们实现一个函数,根据给定的 Haar 特征和积分图,计算其特征值。
// 计算单个 Haar 特征的值
// ii: 积分图指针, w: 样本宽度, h: 样本高度
float calculate_feature_value(const HaarFeature* feature, const unsigned long long* ii, int w, int h) {
int x = feature->x;
int y = feature->y;
int width = feature->width;
int height = feature->height;
// 使用积分图公式计算矩形和
// get_rect_sum(ii, x, y, w, h) = ii[y+w*x] + ii[(y+h)+w*(x+width)] - ii[y+h+w*x] - ii[y+w*(x+width)]
auto get_rect_sum = [](const unsigned long long* ii, int x, int y, int w, int h, int img_w) {
return ii[y + img_w * x] + ii[(y + h) + img_w * (x + w)] - ii[(y + h) + img_w * x] - ii[y + img_w * (x + w)];
};
int img_w = w; // 假设积分图的宽度和样本宽度一致
float sum = 0;
switch (feature->type) {
case TWO_RECT_H: {
int half_w = width / 2;
int sum1 = get_rect_sum(ii, x, y, half_w, height, img_w);
int sum2 = get_rect_sum(ii, x + half_w, y, width - half_w, height, img_w);
sum = sum1 - sum2; // 左 - 右
break;
}
case TWO_RECT_V: {
int half_h = height / 2;
int sum1 = get_rect_sum(ii, x, y, width, half_h, img_w);
int sum2 = get_rect_sum(ii, x, y + half_h, width, height - half_h, img_w);
sum = sum1 - sum2; // 上 - 下
break;
}
// ... 其他特征类型的计算
// 为了简洁,这里只实现两种
}
return (float)sum;
}
注意:get_rect_sum 是一个 lambda 函数,如果您的 C 编译器不支持 C11,可以将其定义为一个普通函数。
步骤 2:AdaBoost 训练算法
AdaBoost 的核心循环,我们将实现一个函数,为给定的权重分布,找到“最佳”的弱分类器。
// 定义弱分类器结构
typedef struct {
HaarFeature feature;
float threshold;
int polarity; // 1 或 -1,决定特征值大于阈值时是正类还是负类
float error;
float alpha; // 该弱分类器的权重
} WeakClassifier;
// 找到最佳弱分类器
WeakClassifier find_best_weak_classifier(Sample* samples, int* labels, float* weights, int num_samples,
HaarFeature* features, int num_features, int sample_width, int sample_height) {
WeakClassifier best_wc;
best_wc.error = FLT_MAX; // 初始化为一个很大的值
// 遍历所有 Haar 特征
for (int f = 0; f < num_features; f++) {
HaarFeature current_feature = features[f];
// 1. 根据当前特征,计算所有样本的特征值
float* feature_values = (float*)malloc(num_samples * sizeof(float));
for (int i = 0; i < num_samples; i++) {
feature_values[i] = calculate_feature_value(¤t_feature, samples[i].integral_image, sample_width, sample_height);
}
// 2. 对特征值进行排序,以便快速找到最佳阈值
// ... (排序代码,可以使用 qsort)
// 3. 遍历所有可能的阈值(通常取排序后相邻特征值的中间值)
for (int i = 0; i < num_samples - 1; i++) {
float threshold = (feature_values[i] + feature_values[i+1]) / 2.0f;
// 尝试两种极性 (polarity)
for (int p = 1; p >= -1; p -= 2) {
float error = 0.0f;
for (int j = 0; j < num_samples; j++) {
int prediction = (feature_values[j] * p >= threshold * p) ? 1 : -1;
if (prediction != labels[j]) {
error += weights[j];
}
}
// 4. 如果当前分类器的错误率更低,则更新最佳分类器
if (error < best_wc.error) {
best_wc.feature = current_feature;
best_wc.threshold = threshold;
best_wc.polarity = p;
best_wc.error = error;
}
}
}
free(feature_values);
}
// 5. 计算该弱分类器的 alpha
best_wc.alpha = 0.5f * logf((1.0f - best_wc.error) / best_wc.error);
return best_wc;
}
注意:上面的 find_best_weak_classifier 是一个高度简化的版本,实际实现中:
- 特征值的排序和阈值查找需要更高效,避免对每个特征都重新排序所有样本。
- 错误率
error应该是一个很小的数(如1e-6),以避免除零错误。
步骤 3:将 AdaBoost 组合成一个强分类器
我们使用 AdaBoost 循环来训练多个弱分类器,并将它们组合起来。
// 定义强分类器
typedef struct {
WeakClassifier* classifiers;
int num_classifiers;
} StrongClassifier;
// 训练强分类器
StrongClassifier train_adaboost(Sample* pos_samples, Sample* neg_samples, int num_pos, int num_neg,
int T, int sample_width, int sample_height) {
int total_samples = num_pos + num_neg;
Sample* all_samples = (Sample*)malloc(total_samples * sizeof(Sample));
int* labels = (int*)malloc(total_samples * sizeof(int));
float* weights = (float*)malloc(total_samples * sizeof(float));
// 1. 初始化样本、标签和权重
for (int i = 0; i < num_pos; i++) {
all_samples[i] = pos_samples[i];
labels[i] = 1;
weights[i] = 1.0f / (2.0f * num_pos); // 正样本初始权重
}
for (int i = 0; i < num_neg; i++) {
all_samples[num_pos + i] = neg_samples[i];
labels[num_pos + i] = -1;
weights[num_pos + i] = 1.0f / (2.0f * num_neg); // 负样本初始权重
}
// 2. 生成大量的 Haar 特征 (这部分非常耗时)
// 在实际应用中,特征数量是固定的(160,000+)
int num_features = 100; // 为了演示,我们只生成100个
HaarFeature* features = generate_haar_features(sample_width, sample_height, num_features);
// 3. AdaBoost 主循环
StrongClassifier strong_classifier;
strong_classifier.classifiers = (WeakClassifier*)malloc(T * sizeof(WeakClassifier));
strong_classifier.num_classifiers = 0;
for (int t = 0; t < T; t++) {
printf("Training weak classifier %d/%d...\n", t + 1, T);
// a. 找到当前权重下最好的弱分类器
WeakClassifier wc = find_best_weak_classifier(all_samples, labels, weights, total_samples, features, num_features, sample_width, sample_height);
// b. 更新样本权重
float normalization_factor = 0.0f;
for (int i = 0; i < total_samples; i++) {
int prediction = (calculate_feature_value(&wc.feature, all_samples[i].integral_image, sample_width, sample_height) * wc.polarity >= wc.threshold * wc.polarity) ? 1 : -1;
if (prediction == labels[i]) {
weights[i] *= expf(-wc.alpha);
} else {
weights[i] *= expf(wc.alpha);
}
normalization_factor += weights[i];
}
// 归一化权重
for (int i = 0; i < total_samples; i++) {
weights[i] /= normalization_factor;
}
// c. 保存弱分类器
strong_classifier.classifiers[strong_classifier.num_classifiers++] = wc;
printf(" - Weak Classifier %d: Error=%.4f, Alpha=%.4f\n", t+1, wc.error, wc.alpha);
}
free(all_samples);
free(labels);
free(weights);
free(features); // 注意:在实际应用中,特征池可能会被重复使用,不应在此释放
return strong_classifier;
}
完整示例代码
下面是一个将上述逻辑串联起来的完整、可编译的 C 语言示例。这是一个教学示例,为了代码的简洁性和可读性,做了大量简化,并且不包含完整的 Haar 特征生成和 I/O 处理。
您需要创建两个数据文件 positives.dat 和 negatives.dat,由于格式定义复杂,这里我们用代码模拟生成一些假的样本数据。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <float.h>
#include <math.h>
// --- 1. 数据结构定义 ---
typedef struct {
int width;
int height;
unsigned long long *integral_image;
} Sample;
typedef enum {
TWO_RECT_H, // 水平两矩形
TWO_RECT_V // 垂直两矩形
} HaarFeatureType;
typedef struct {
HaarFeatureType type;
int x;
int y;
int width;
int height;
} HaarFeature;
typedef struct {
HaarFeature feature;
float threshold;
int polarity; // 1 或 -1
float error;
float alpha;
} WeakClassifier;
typedef struct {
WeakClassifier* classifiers;
int num_classifiers;
} StrongClassifier;
// --- 2. 工具函数 ---
// 模拟生成积分图 (仅用于演示)
unsigned long long* create_dummy_integral_image(int w, int h) {
unsigned long long* ii = (unsigned long long*)malloc(w * h * sizeof(unsigned long long));
for (int i = 0; i < h; i++) {
for (int j = 0; j < w; j++) {
// 模拟一个简单的梯度图像
unsigned long long val = (i * w + j) % 256;
if (j > 0) val += ii[i * w + j - 1];
if (i > 0) val += ii[(i-1) * w + j];
if (i > 0 && j > 0) val -= ii[(i-1) * w + j-1];
ii[i * w + j] = val;
}
}
return ii;
}
// 模拟加载样本
Sample* load_dummy_samples(const char* filename, int count, int w, int h) {
Sample* samples = (Sample*)malloc(count * sizeof(Sample));
for (int i = 0; i < count; i++) {
samples[i].width = w;
samples[i].height = h;
samples[i].integral_image = create_dummy_integral_image(w, h);
}
printf("Loaded %d dummy samples from %s\n", count, filename);
return samples;
}
// 释放样本内存
void free_samples(Sample* samples, int count) {
for (int i = 0; i < count; i++) {
free(samples[i].integral_image);
}
free(samples);
}
// --- 3. Haar 特征计算 ---
float calculate_feature_value(const HaarFeature* feature, const unsigned long long* ii, int w, int h) {
int x = feature->x;
int y = feature->y;
int fw = feature->width;
int fh = feature->height;
auto get_rect_sum = [](const unsigned long long* ii, int x, int y, int w, int h, int img_w) {
if (x + w > img_w || y + h > (int)(sizeof(ii)/sizeof(ii[0]))/img_w) return 0; // 简单边界检查
return ii[y * img_w + x] + ii[(y + h) * img_w + (x + w)] - ii[(y + h) * img_w + x] - ii[y * img_w + (x + w)];
};
int img_w = w;
float sum = 0;
switch (feature->type) {
case TWO_RECT_H: {
int half_w = fw / 2;
int sum1 = get_rect_sum(ii, x, y, half_w, fh, img_w);
int sum2 = get_rect_sum(ii, x + half_w, y, fw - half_w, fh, img_w);
sum = sum1 - sum2;
break;
}
case TWO_RECT_V: {
int half_h = fh / 2;
int sum1 = get_rect_sum(ii, x, y, fw, half_h, img_w);
int sum2 = get_rect_sum(ii, x, y + half_h, fw, fh - half_h, img_w);
sum = sum1 - sum2;
break;
}
}
return sum;
}
// 模拟生成 Haar 特征池
HaarFeature* generate_haar_features(int sample_w, int sample_h, int num_features) {
HaarFeature* features = (HaarFeature*)malloc(num_features * sizeof(HaarFeature));
for (int i = 0; i < num_features; i++) {
features[i].type = (i % 2 == 0) ? TWO_RECT_H : TWO_RECT_V;
features[i].x = rand() % (sample_w / 2);
features[i].y = rand() % (sample_h / 2);
features[i].width = (rand() % (sample_w / 2)) + 10;
features[i].height = (rand() % (sample_h / 2)) + 10;
// 确保特征不超出边界
if (features[i].x + features[i].width > sample_w) features[i].width = sample_w - features[i].x;
if (features[i].y + features[i].height > sample_h) features[i].height = sample_h - features[i].y;
}
printf("Generated %d Haar features.\n", num_features);
return features;
}
// --- 4. AdaBoost 训练 ---
// 简化的 find_best_weak_classifier
WeakClassifier find_best_weak_classifier(Sample* samples, int* labels, float* weights, int num_samples,
HaarFeature* features, int num_features, int sample_width, int sample_height) {
WeakClassifier best_wc;
best_wc.error = FLT_MAX;
// 为了极大简化,我们只对前10个特征进行搜索
int search_features = (num_features > 10) ? 10 : num_features;
for (int f = 0; f < search_features; f++) {
HaarFeature current_feature = features[f];
float min_val = FLT_MAX, max_val = -FLT_MAX;
// 找到当前特征在所有样本中的最大最小值,以确定阈值范围
for (int i = 0; i < num_samples; i++) {
float val = calculate_feature_value(¤t_feature, samples[i].integral_image, sample_width, sample_height);
if (val < min_val) min_val = val;
if (val > max_val) max_val = val;
}
// 简单地取中间值作为阈值
float threshold = (min_val + max_val) / 2.0f;
// 尝试两种极性
for (int p = 1; p >= -1; p -= 2) {
float error = 0.0f;
for (int j = 0; j < num_samples; j++) {
float val = calculate_feature_value(¤t_feature, samples[j].integral_image, sample_width, sample_height);
int prediction = (val * p >= threshold * p) ? 1 : -1;
if (prediction != labels[j]) {
error += weights[j];
}
}
if (error < best_wc.error && error > 1e-6) { // 避免完美分类器
best_wc.feature = current_feature;
best_wc.threshold = threshold;
best_wc.polarity = p;
best_wc.error = error;
}
}
}
if (best_wc.error == FLT_MAX) {
// 如果没找到好的,创建一个随机分类器
best_wc.error = 0.5;
best_wc.alpha = 0;
printf("Warning: Could not find a good weak classifier.\n");
} else {
best_wc.alpha = 0.5f * logf((1.0f - best_wc.error) / best_wc.error);
}
return best_wc;
}
StrongClassifier train_adaboost(Sample* pos_samples, Sample* neg_samples, int num_pos, int num_neg,
int T, int sample_width, int sample_height) {
int total_samples = num_pos + num_neg;
Sample* all_samples = (Sample*)malloc(total_samples * sizeof(Sample));
int* labels = (int*)malloc(total_samples * sizeof(int));
float* weights = (float*)malloc(total_samples * sizeof(float));
for (int i = 0; i < num_pos; i++) {
all_samples[i] = pos_samples[i];
labels[i] = 1;
weights[i] = 1.0f / (2.0f * num_pos);
}
for (int i = 0; i < num_neg; i++) {
all_samples[num_pos + i] = neg_samples[i];
labels[num_pos + i] = -1;
weights[num_pos + i] = 1.0f / (2.0f * num_neg);
}
int num_features = 100; // 简化:只生成100个特征
HaarFeature* features = generate_haar_features(sample_width, sample_height, num_features);
StrongClassifier strong_classifier;
strong_classifier.classifiers = (WeakClassifier*)malloc(T * sizeof(WeakClassifier));
strong_classifier.num_classifiers = 0;
for (int t = 0; t < T; t++) {
printf("\n--- Iteration %d ---\n", t + 1);
WeakClassifier wc = find_best_weak_classifier(all_samples, labels, weights, total_samples, features, num_features, sample_width, sample_height);
if (wc.error >= 0.5) { // 如果错误率太高,提前终止
printf("Error rate too high. Stopping training.\n");
break;
}
float normalization_factor = 0.0f;
for (int i = 0; i < total_samples; i++) {
float val = calculate_feature_value(&wc.feature, all_samples[i].integral_image, sample_width, sample_height);
int prediction = (val * wc.polarity >= wc.threshold * wc.polarity) ? 1 : -1;
if (prediction == labels[i]) {
weights[i] *= expf(-wc.alpha);
} else {
weights[i] *= expf(wc.alpha);
}
normalization_factor += weights[i];
}
for (int i = 0; i < total_samples; i++) {
weights[i] /= normalization_factor;
}
strong_classifier.classifiers[strong_classifier.num_classifiers++] = wc;
printf("Weak Classifier %d: Error=%.4f, Alpha=%.4f\n", t+1, wc.error, wc.alpha);
}
free(all_samples);
free(labels);
free(weights);
free(features);
return strong_classifier;
}
// --- 5. 使用强分类器进行预测 ---
int predict_strong_classifier(const StrongClassifier* sc, const Sample* sample) {
float sum = 0.0f;
for (int i = 0; i < sc->num_classifiers; i++) {
const WeakClassifier* wc = &sc->classifiers[i];
float val = calculate_feature_value(&wc->feature, sample->integral_image, sample->width, sample->height);
float vote = wc->alpha * ( (val * wc->polarity >= wc->threshold * wc->polarity) ? 1.0f : -1.0f );
sum += vote;
}
return (sum > 0) ? 1 : -1;
}
void free_strong_classifier(StrongClassifier* sc) {
free(sc->classifiers);
sc->classifiers = NULL;
sc->num_classifiers = 0;
}
// --- 6. 主函数 ---
int main() {
const int NUM_POS_SAMPLES = 100;
const int NUM_NEG_SAMPLES = 100;
const int T = 5; // 训练5个弱分类器
const int SAMPLE_W = 24;
const int SAMPLE_H = 24;
// 1. 加载样本数据
Sample* pos_samples = load_dummy_samples("positives.dat", NUM_POS_SAMPLES, SAMPLE_W, SAMPLE_H);
Sample* neg_samples = load_dummy_samples("negatives.dat", NUM_NEG_SAMPLES, SAMPLE_W, SAMPLE_H);
// 2. 训练 AdaBoost 模型
printf("\nStarting AdaBoost training for T=%d...\n", T);
StrongClassifier classifier = train_adaboost(pos_samples, neg_samples, NUM_POS_SAMPLES, NUM_NEG_SAMPLES, T, SAMPLE_W, SAMPLE_H);
printf("\nTraining complete. Strong classifier has %d weak classifiers.\n", classifier.num_classifiers);
// 3. 在训练样本上测试
printf("\n--- Testing on training data ---\n");
int correct = 0;
for (int i = 0; i < NUM_POS_SAMPLES; i++) {
if (predict_strong_classifier(&classifier, &pos_samples[i]) == 1) correct++;
}
printf("Positive accuracy: %.2f%% (%d/%d)\n", (float)correct / NUM_POS_SAMPLES * 100, correct, NUM_POS_SAMPLES);
correct = 0;
for (int i = 0; i < NUM_NEG_SAMPLES; i++) {
if (predict_strong_classifier(&classifier, &neg_samples[i]) == -1) correct++;
}
printf("Negative accuracy: %.2f%% (%d/%d)\n", (float)correct / NUM_NEG_SAMPLES * 100, correct, NUM_NEG_SAMPLES);
// 4. 释放内存
free_samples(pos_samples, NUM_POS_SAMPLES);
free_samples(neg_samples, NUM_NEG_SAMPLES);
free_strong_classifier(&classifier);
return 0;
}
如何编译和运行
- 将上述代码保存为
haar_adaboost.c。 - 使用支持 C11 的 C 编译器进行编译,GCC:
gcc haar_adaboost.c -o haar_adaboost -lm
(
-lm是为了链接数学库libm)。 - 运行程序:
./haar_adaboost
如何扩展与优化
这个示例只是一个起点,要实现一个像 OpenCV 那样可用的系统,还需要进行大量的优化和扩展:
-
完整的 Haar 特征生成:
- 实现所有类型的 Haar 特征(三矩形、四矩形)。
- 生成所有可能的尺度(size)和位置(x, y)的特征,这是一个巨大的特征空间(24x24 的图像有超过 160,000 个特征)。
-
高效的训练算法:
- 特征值排序:在
find_best_weak_classifier中,对每个特征计算其所有样本的特征值,然后排序,这样可以在 O(N log N) 时间内找到最佳阈值,而不是 O(N^2)。 - 提前终止:如果某个弱分类器的错误率高于 0.5(即比随机猜测还差),则应该丢弃它并重新选择特征,而不是继续训练。
- 特征值排序:在
-
级联分类器:
- 将训练过程修改为训练多个阶段,每个阶段的目标是达到一个很高的检测率(99.9%)和可接受的误报率(50%)。
- 每个阶段的训练数据是上一阶段分类错误的负样本。
-
图像金字塔和滑动窗口:
- 检测:为了检测不同大小的人脸,需要将输入图像缩放成不同尺度的图像金字塔。
- 在金字塔的每一层,使用一个固定大小的窗口(如 24x24)进行滑动,将窗口内的图像区域提取出来,转换成样本格式,然后用训练好的级联分类器进行判断。
-
数据 I/O:
- 实现真实的文件 I/O,可以读取
.dat文件或 OpenCV 的opencv_createsamples工具生成的样本文件。 - 积分图也需要从原始图像计算,而不是模拟生成。
- 实现真实的文件 I/O,可以读取
-
性能优化:
- 使用 SIMD 指令(如 SSE, AVX)来并行计算多个 Haar 特征。
- 使用多线程来加速特征计算和分类过程。
这个 C 语言实现为你提供了一个理解 Haar + AdaBoost 算法底层逻辑的绝佳机会,虽然它离一个完整的应用还有很长的路,但它清晰地展示了算法的每一个关键步骤。
