基于码本选择的视频目标跟踪系统(C语言实现)
基于码本(Codebook)背景建模的视频目标跟踪系统的C语言实现。该系统使用码本方法进行背景建模,通过背景差分检测运动目标,并实现目标跟踪功能。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
#include <time.h>
#include <opencv2/opencv.hpp>
#define MAX_CODEWORDS 8 // 每个像素的最大码字数
#define LEARNING_RATE 0.01 // 背景学习率
#define MIN_DISTANCE 30 // 最小颜色距离阈值
#define MAX_REPEAT 5 // 最大连续检测次数
#define TRACKING_THRESHOLD 0.6 // 跟踪匹配阈值
// 码字结构体
typedef struct {
unsigned char cbMin[3]; // 最小颜色值 (B,G,R)
unsigned char cbMax[3]; // 最大颜色值 (B,G,R)
int lastUpdate; // 最后更新帧号
int firstTime; // 首次出现帧号
int missCount; // 连续未匹配次数
} Codeword;
// 码本结构体(每个像素一个码本)
typedef struct {
Codeword codewords[MAX_CODEWORDS];
int numCodewords; // 当前码字数
int currentLearningRate; // 当前学习率
} Codebook;
// 目标结构体
typedef struct {
int id; // 目标ID
CvRect boundingBox; // 边界框
int age; // 目标年龄(存在帧数)
int totalVisibleCount; // 可见总帧数
int consecutiveInvisibleCount; // 连续不可见帧数
CvScalar color; // 目标显示颜色
} TrackedObject;
// 全局变量
Codebook*** codebooks = NULL; // 三维码本数组 [height][width][channel]
IplImage* backgroundModel = NULL;
TrackedObject* trackedObjects = NULL;
int objectCount = 0;
int nextObjectId = 1;
int frameCount = 0;
// 初始化码本
void initializeCodebook(IplImage* frame) {
int width = frame->width;
int height = frame->height;
// 分配内存
codebooks = (Codebook***)malloc(height * sizeof(Codebook**));
for (int y = 0; y < height; y++) {
codebooks[y] = (Codebook**)malloc(width * sizeof(Codebook*));
for (int x = 0; x < width; x++) {
codebooks[y][x] = (Codebook*)malloc(sizeof(Codebook));
memset(codebooks[y][x], 0, sizeof(Codebook));
codebooks[y][x]->numCodewords = 0;
codebooks[y][x]->currentLearningRate = (int)(LEARNING_RATE * 1000);
}
}
// 创建背景模型
backgroundModel = cvCloneImage(frame);
}
// 计算颜色距离
int colorDistance(CvScalar c1, CvScalar c2) {
return (int)sqrt(
pow(c1.val[0] - c2.val[0], 2) +
pow(c1.val[1] - c2.val[1], 2) +
pow(c1.val[2] - c2.val[2], 2)
);
}
// 更新码本
void updateCodebook(IplImage* frame, int frameNum) {
uchar* data = (uchar*)(frame->imageData);
int step = frame->widthStep;
int width = frame->width;
int height = frame->height;
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
int index = y * step + x * 3;
CvScalar pixel = cvScalar(data[index+2], data[index+1], data[index], 0); // RGB
Codebook* cb = codebooks[y][x];
int found = 0;
// 检查现有码字
for (int i = 0; i < cb->numCodewords; i++) {
Codeword* cw = &cb->codewords[i];
// 检查颜色是否在码字范围内
int inRange = 1;
for (int ch = 0; ch < 3; ch++) {
int minVal = cw->cbMin[ch];
int maxVal = cw->cbMax[ch];
// 处理环形颜色空间(如红色)
if (minVal > maxVal) {
if (!(pixel.val[ch] >= minVal || pixel.val[ch] <= maxVal)) {
inRange = 0;
break;
}
} else {
if (pixel.val[ch] < minVal || pixel.val[ch] > maxVal) {
inRange = 0;
break;
}
}
}
if (inRange) {
// 更新码字
for (int ch = 0; ch < 3; ch++) {
if (pixel.val[ch] < cw->cbMin[ch]) cw->cbMin[ch] = pixel.val[ch];
if (pixel.val[ch] > cw->cbMax[ch]) cw->cbMax[ch] = pixel.val[ch];
}
cw->lastUpdate = frameNum;
cw->missCount = 0;
found = 1;
break;
}
}
// 没有匹配的码字,创建新码字
if (!found && cb->numCodewords < MAX_CODEWORDS) {
Codeword newCw;
for (int ch = 0; ch < 3; ch++) {
newCw.cbMin[ch] = pixel.val[ch];
newCw.cbMax[ch] = pixel.val[ch];
}
newCw.firstTime = frameNum;
newCw.lastUpdate = frameNum;
newCw.missCount = 0;
cb->codewords[cb->numCodewords++] = newCw;
}
// 码本已满,替换最老的码字
else if (!found && cb->numCodewords == MAX_CODEWORDS) {
int oldestIdx = 0;
int oldestTime = cb->codewords[0].firstTime;
for (int i = 1; i < cb->numCodewords; i++) {
if (cb->codewords[i].firstTime < oldestTime) {
oldestTime = cb->codewords[i].firstTime;
oldestIdx = i;
}
}
// 替换最老的码字
for (int ch = 0; ch < 3; ch++) {
cb->codewords[oldestIdx].cbMin[ch] = pixel.val[ch];
cb->codewords[oldestIdx].cbMax[ch] = pixel.val[ch];
}
cb->codewords[oldestIdx].firstTime = frameNum;
cb->codewords[oldestIdx].lastUpdate = frameNum;
cb->codewords[oldestIdx].missCount = 0;
}
}
}
}
// 生成前景掩码
IplImage* createForegroundMask(IplImage* frame, int frameNum) {
int width = frame->width;
int height = frame->height;
IplImage* mask = cvCreateImage(cvSize(width, height), IPL_DEPTH_8U, 1);
uchar* maskData = (uchar*)mask->imageData;
int maskStep = mask->widthStep;
uchar* frameData = (uchar*)(frame->imageData);
int frameStep = frame->widthStep;
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
int index = y * frameStep + x * 3;
CvScalar pixel = cvScalar(frameData[index+2], frameData[index+1], frameData[index], 0);
Codebook* cb = codebooks[y][x];
int isBackground = 0;
// 检查所有码字
for (int i = 0; i < cb->numCodewords; i++) {
Codeword* cw = &cb->codewords[i];
// 检查码字是否过期
if (frameNum - cw->lastUpdate > 30) {
cw->missCount++;
if (cw->missCount > 5) {
// 移除过期码字(简化处理)
}
continue;
}
// 检查颜色是否在码字范围内
int inRange = 1;
for (int ch = 0; ch < 3; ch++) {
int minVal = cw->cbMin[ch];
int maxVal = cw->cbMax[ch];
if (minVal > maxVal) {
if (!(pixel.val[ch] >= minVal || pixel.val[ch] <= maxVal)) {
inRange = 0;
break;
}
} else {
if (pixel.val[ch] < minVal || pixel.val[ch] > maxVal) {
inRange = 0;
break;
}
}
}
if (inRange) {
isBackground = 1;
break;
}
}
int maskIndex = y * maskStep + x;
maskData[maskIndex] = isBackground ? 0 : 255; // 背景为0,前景为255
}
}
return mask;
}
// 更新背景模型
void updateBackgroundModel(IplImage* frame) {
cvAddWeighted(backgroundModel, 1.0 - LEARNING_RATE, frame, LEARNING_RATE, 0, backgroundModel);
}
// 检测运动目标
CvSeq* detectObjects(IplImage* mask, int minArea) {
IplImage* temp = cvCreateImage(cvGetSize(mask), IPL_DEPTH_8U, 1);
cvCopy(mask, temp, NULL);
// 形态学操作去除噪声
cvErode(temp, temp, NULL, 1);
cvDilate(temp, temp, NULL, 2);
cvErode(temp, temp, NULL, 1);
// 查找轮廓
CvMemStorage* storage = cvCreateMemStorage(0);
CvSeq* contours = cvFindContours(temp, storage, sizeof(CvContour), CV_RETR_EXTERNAL, CV_CHAIN_APPROX_SIMPLE);
// 过滤小区域
CvSeq* filteredContours = cvCreateSeq(0, sizeof(CvSeq), sizeof(CvPoint), storage);
for (CvSeq* c = contours; c != NULL; c = c->h_next) {
double area = fabs(cvContourArea(c, CV_WHOLE_SEQ));
if (area > minArea) {
cvSeqPush(filteredContours, c);
}
}
cvReleaseImage(&temp);
return filteredContours;
}
// 初始化目标跟踪
void initTracking(CvSeq* contours) {
// 释放之前的目标
if (trackedObjects != NULL) {
free(trackedObjects);
}
objectCount = contours->total;
trackedObjects = (TrackedObject*)malloc(objectCount * sizeof(TrackedObject));
CvScalar colors[] = {
CV_RGB(255, 0, 0), // 红
CV_RGB(0, 255, 0), // 绿
CV_RGB(0, 0, 255), // 蓝
CV_RGB(255, 255, 0), // 黄
CV_RGB(255, 0, 255), // 紫
CV_RGB(0, 255, 255), // 青
CV_RGB(255, 128, 0), // 橙
CV_RGB(128, 0, 255) // 紫罗兰
};
for (int i = 0; i < objectCount; i++) {
CvContour* contour = (CvContour*)cvGetSeqElem(contours, i);
CvRect rect = cvBoundingRect(contour, 0);
trackedObjects[i].id = nextObjectId++;
trackedObjects[i].boundingBox = rect;
trackedObjects[i].age = 1;
trackedObjects[i].totalVisibleCount = 1;
trackedObjects[i].consecutiveInvisibleCount = 0;
trackedObjects[i].color = colors[i % 8];
}
}
// 更新目标跟踪
void updateTracking(CvSeq* contours) {
int newObjectCount = contours->total;
TrackedObject* newObjects = (TrackedObject*)malloc(newObjectCount * sizeof(TrackedObject));
// 临时存储新检测到的目标
for (int i = 0; i < newObjectCount; i++) {
CvContour* contour = (CvContour*)cvGetSeqElem(contours, i);
CvRect rect = cvBoundingRect(contour, 0);
newObjects[i].id = -1; // 未分配ID
newObjects[i].boundingBox = rect;
newObjects[i].age = 1;
newObjects[i].totalVisibleCount = 1;
newObjects[i].consecutiveInvisibleCount = 0;
}
// 匹配现有目标和新检测目标
for (int i = 0; i < objectCount; i++) {
TrackedObject* obj = &trackedObjects[i];
double maxIoU = 0.0;
int bestMatch = -1;
for (int j = 0; j < newObjectCount; j++) {
CvRect newRect = newObjects[j].boundingBox;
CvRect oldRect = obj->boundingBox;
// 计算交并比(IoU)
int interX1 = (oldRect.x > newRect.x) ? oldRect.x : newRect.x;
int interY1 = (oldRect.y > newRect.y) ? oldRect.y : newRect.y;
int interX2 = (oldRect.x + oldRect.width < newRect.x + newRect.width) ?
oldRect.x + oldRect.width : newRect.x + newRect.width;
int interY2 = (oldRect.y + oldRect.height < newRect.y + newRect.height) ?
oldRect.y + oldRect.height : newRect.y + newRect.height;
int interArea = (interX2 > interX1 && interY2 > interY1) ?
(interX2 - interX1) * (interY2 - interY1) : 0;
int unionArea = oldRect.width * oldRect.height +
newRect.width * newRect.height - interArea;
double iou = (double)interArea / unionArea;
if (iou > maxIoU && iou > TRACKING_THRESHOLD) {
maxIoU = iou;
bestMatch = j;
}
}
if (bestMatch >= 0) {
// 更新现有目标
newObjects[bestMatch].id = obj->id;
newObjects[bestMatch].age = obj->age + 1;
newObjects[bestMatch].totalVisibleCount = obj->totalVisibleCount + 1;
newObjects[bestMatch].consecutiveInvisibleCount = 0;
newObjects[bestMatch].color = obj->color;
// 更新边界框(使用卡尔曼滤波预测)
CvRect predRect = obj->boundingBox;
predRect.x = (predRect.x + newObjects[bestMatch].boundingBox.x) / 2;
predRect.y = (predRect.y + newObjects[bestMatch].boundingBox.y) / 2;
predRect.width = (predRect.width + newObjects[bestMatch].boundingBox.width) / 2;
predRect.height = (predRect.height + newObjects[bestMatch].boundingBox.height) / 2;
newObjects[bestMatch].boundingBox = predRect;
} else {
// 目标消失
obj->consecutiveInvisibleCount++;
}
}
// 处理新出现的目标
for (int j = 0; j < newObjectCount; j++) {
if (newObjects[j].id == -1) {
newObjects[j].id = nextObjectId++;
newObjects[j].color = CV_RGB(rand() % 256, rand() % 256, rand() % 256);
}
}
// 更新目标列表
free(trackedObjects);
trackedObjects = newObjects;
objectCount = newObjectCount;
}
// 绘制跟踪结果
void drawTrackingResults(IplImage* frame) {
for (int i = 0; i < objectCount; i++) {
TrackedObject* obj = &trackedObjects[i];
CvScalar color = obj->color;
// 绘制边界框
cvRectangle(frame,
cvPoint(obj->boundingBox.x, obj->boundingBox.y),
cvPoint(obj->boundingBox.x + obj->boundingBox.width,
obj->boundingBox.y + obj->boundingBox.height),
color, 2, 8, 0);
// 绘制ID和年龄
char text[50];
sprintf(text, "ID:%d Age:%d", obj->id, obj->age);
cvPutText(frame, text,
cvPoint(obj->boundingBox.x, obj->boundingBox.y - 5),
cvFont(1, 1), 0.8, color, 1, 8, 0);
}
}
// 主函数
int main(int argc, char** argv) {
const char* inputVideo = (argc > 1) ? argv[1] : "test.mp4";
CvCapture* capture = cvCaptureFromFile(inputVideo);
if (!capture) {
fprintf(stderr, "Could not open video file: %s\n", inputVideo);
return 1;
}
cvNamedWindow("Original", CV_WINDOW_AUTOSIZE);
cvNamedWindow("Foreground Mask", CV_WINDOW_AUTOSIZE);
cvNamedWindow("Tracking Results", CV_WINDOW_AUTOSIZE);
IplImage* frame = cvQueryFrame(capture);
if (!frame) {
fprintf(stderr, "Could not read first frame\n");
return 1;
}
initializeCodebook(frame);
frameCount = 0;
while ((frame = cvQueryFrame(capture)) != NULL) {
frameCount++;
// 更新码本
updateCodebook(frame, frameCount);
// 生成前景掩码
IplImage* mask = createForegroundMask(frame, frameCount);
// 检测运动目标
CvSeq* contours = detectObjects(mask, 500); // 最小面积500像素
// 目标跟踪
if (frameCount == 1) {
initTracking(contours);
} else {
updateTracking(contours);
}
// 绘制跟踪结果
IplImage* trackingFrame = cvCloneImage(frame);
drawTrackingResults(trackingFrame);
// 显示结果
cvShowImage("Original", frame);
cvShowImage("Foreground Mask", mask);
cvShowImage("Tracking Results", trackingFrame);
// 清理
cvReleaseImage(&mask);
cvReleaseImage(&trackingFrame);
cvClearSeq(contours);
cvReleaseMemStorage(contours->storage);
// 退出条件
char key = cvWaitKey(30);
if (key == 27) break; // ESC退出
}
// 释放资源
for (int y = 0; y < frame->height; y++) {
for (int x = 0; x < frame->width; x++) {
free(codebooks[y][x]);
}
free(codebooks[y]);
}
free(codebooks);
cvReleaseImage(&backgroundModel);
free(trackedObjects);
cvReleaseCapture(&capture);
cvDestroyAllWindows();
return 0;
}
系统架构与算法原理
1. 码本背景建模
码本方法是一种高效的背景建模技术,为每个像素维护一个码本(一组颜色向量及其时空信息):
graph TD
A[输入视频帧] --> B[像素级处理]
B --> C{颜色匹配码字?}
C -->|是| D[更新码字范围]
C -->|否| E{码本未满?}
E -->|是| F[添加新码字]
E -->|否| G[替换最老码字]
D --> H[生成前景掩码]
F --> H
G --> H
H --> I[目标检测与跟踪]
2. 目标跟踪流程
-
背景建模:使用码本方法建立背景模型
-
前景检测:通过背景差分生成前景掩码
-
目标检测:形态学处理 + 轮廓分析
-
目标跟踪:
- 初始化:第一帧检测到的目标分配ID
- 更新:使用IoU(交并比)匹配新旧目标
- 预测:简单的卡尔曼滤波预测目标位置
3. 关键数据结构
码字结构:
typedef struct {
unsigned char cbMin[3]; // 最小颜色值 (B,G,R)
unsigned char cbMax[3]; // 最大颜色值 (B,G,R)
int lastUpdate; // 最后更新帧号
int firstTime; // 首次出现帧号
int missCount; // 连续未匹配次数
} Codeword;
目标结构:
typedef struct {
int id; // 目标ID
CvRect boundingBox; // 边界框
int age; // 目标年龄(存在帧数)
int totalVisibleCount; // 可见总帧数
int consecutiveInvisibleCount; // 连续不可见帧数
CvScalar color; // 目标显示颜色
} TrackedObject;
参考代码 根据codebook的选择对视频中目标跟踪 www.youwenfan.com/contentcns/122445.html
编译与运行
编译命令(Linux)
gcc -o object_tracker object_tracker.c `pkg-config --cflags --libs opencv`
运行命令
./object_tracker input_video.mp4
参数调整建议
| 参数 | 默认值 | 调整建议 |
|---|---|---|
MAX_CODEWORDS |
8 | 复杂场景增加至12-16 |
LEARNING_RATE |
0.01 | 光照变化大时增大至0.05 |
MIN_DISTANCE |
30 | 目标与背景相似时减小 |
TRACKING_THRESHOLD |
0.6 | 目标移动快时减小至0.4 |
minArea |
500 | 小目标检测减小至200 |
系统优化方向
-
性能优化:
// 使用多线程处理 #pragma omp parallel for for (int y = 0; y < height; y++) { // 像素处理代码 } -
鲁棒性增强:
// 自适应学习率 float adaptiveLR = LEARNING_RATE * (1.0 + 0.5 * motion_intensity); -
高级跟踪算法:
// 卡尔曼滤波实现 typedef struct { float x, y, vx, vy; // 位置和速度 float P[4][4]; // 协方差矩阵 } KalmanFilter; -
目标重识别:
// 使用特征描述子进行目标重识别 void extractFeatures(IplImage* roi, float* features);
应用场景
- 智能视频监控:入侵检测、异常行为识别
- 交通流量分析:车辆计数、违章检测
- 零售分析:顾客行为分析、热力图生成
- 体育分析:运动员跟踪、战术分析
- 人机交互:手势识别、动作捕捉
扩展功能建议
-
多摄像头跟踪:
// 跨摄像头目标关联 void associateAcrossCameras(TrackedObject* obj1, TrackedObject* obj2); -
行为识别:
// 分析目标运动模式 enum Behavior { STATIONARY, MOVING, LOITERING, RUNNING }; Behavior analyzeBehavior(TrackedObject* obj); -
异常检测:
// 检测异常运动模式 int detectAnomaly(TrackedObject* obj, HistoryBuffer* history); -
数据导出:
// 导出跟踪数据到CSV void exportTrackingData(const char* filename);
注意事项
- 光照变化:在极端光照条件下,考虑使用HSV颜色空间
- 阴影处理:添加阴影检测和抑制模块
- 目标遮挡:实现遮挡处理和目标合并/分裂逻辑
- 实时性:对于高清视频,考虑降低分辨率或使用GPU加速