基于码本选择的视频目标跟踪系统(C语言实现)

基于码本选择的视频目标跟踪系统(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. 目标跟踪流程

  1. 背景建模:使用码本方法建立背景模型

  2. 前景检测:通过背景差分生成前景掩码

  3. 目标检测:形态学处理 + 轮廓分析

  4. 目标跟踪

    • 初始化:第一帧检测到的目标分配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

系统优化方向

  1. 性能优化

    // 使用多线程处理
    #pragma omp parallel for
    for (int y = 0; y < height; y++) {
        // 像素处理代码
    }
    
  2. 鲁棒性增强

    // 自适应学习率
    float adaptiveLR = LEARNING_RATE * (1.0 + 0.5 * motion_intensity);
    
  3. 高级跟踪算法

    // 卡尔曼滤波实现
    typedef struct {
        float x, y, vx, vy; // 位置和速度
        float P[4][4];      // 协方差矩阵
    } KalmanFilter;
    
  4. 目标重识别

    // 使用特征描述子进行目标重识别
    void extractFeatures(IplImage* roi, float* features);
    

应用场景

  1. 智能视频监控:入侵检测、异常行为识别
  2. 交通流量分析:车辆计数、违章检测
  3. 零售分析:顾客行为分析、热力图生成
  4. 体育分析:运动员跟踪、战术分析
  5. 人机交互:手势识别、动作捕捉

扩展功能建议

  1. 多摄像头跟踪

    // 跨摄像头目标关联
    void associateAcrossCameras(TrackedObject* obj1, TrackedObject* obj2);
    
  2. 行为识别

    // 分析目标运动模式
    enum Behavior { STATIONARY, MOVING, LOITERING, RUNNING };
    Behavior analyzeBehavior(TrackedObject* obj);
    
  3. 异常检测

    // 检测异常运动模式
    int detectAnomaly(TrackedObject* obj, HistoryBuffer* history);
    
  4. 数据导出

    // 导出跟踪数据到CSV
    void exportTrackingData(const char* filename);
    

注意事项

  1. 光照变化:在极端光照条件下,考虑使用HSV颜色空间
  2. 阴影处理:添加阴影检测和抑制模块
  3. 目标遮挡:实现遮挡处理和目标合并/分裂逻辑
  4. 实时性:对于高清视频,考虑降低分辨率或使用GPU加速

 

专注于matlab/simulink,电子电路,编程