
均值漂移(MeanShift)目标追踪实现
在计算机视觉中,目标追踪 是核心技术之一,而 MeanShift(均值漂移) 是无需训练模型、轻量高效的经典追踪算法。
它结合直方图反向投影,能在连续帧/不同图片中自动锁定目标位置,广泛应用于移动端人脸、物体、肤色追踪场景。
核心技术原理
1. 算法整体流程
MeanShift 目标追踪分为两大阶段 ,全程依赖HSV 色调直方图(抗光照干扰,比 BGR 更稳定):
- 训练阶段 :在参考图中框选目标(ROI)→ 提取目标的色调(H)直方图作为特征模板;
- 追踪阶段:对新图像做直方图反向投影 → 生成目标概率图 → MeanShift 迭代搜索 → 锁定目标位置。
2. 关键原理详解
(1)为什么用 HSV 色彩空间?
- BGR 空间受亮度影响极大,光线变化会直接导致追踪失效;
- HSV 空间将颜色(色调 H)、饱和度(S)、亮度(V)分离 ,仅用色调 H 描述目标颜色,彻底屏蔽亮度干扰,追踪更稳定。
(2)色调直方图(特征模板)
只提取目标的色调通道直方图,并过滤低饱和度像素(避免灰色/白色干扰),生成唯一的目标颜色特征。
(3)直方图反向投影
用训练好的色调直方图,对新图像逐像素计算匹配概率:
- 像素越接近目标颜色 → 概率越高(图像越亮);
- 像素与目标颜色差异大 → 概率越低(图像越暗)。
(4)MeanShift 均值漂移(核心)
这是算法的灵魂:
- 从初始窗口位置开始;
- 计算窗口内概率图的重心(颜色密度最高的点);
- 将窗口中心移动到重心位置;
- 重复迭代,直到窗口位置不再变化(收敛);
- 最终窗口位置 = 目标真实位置。
简单理解:让窗口自动"飘向"目标颜色最密集的区域。
项目完整实现
1. Kotlin 上层代码(MainActivity.kt)
负责图片加载、调用 C++ 算法、结果展示,格式与你的肤色检测代码完全对齐。
kotlin
package com.nicoli.helloroidetectskin
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.os.Bundle
import android.widget.ImageView
import androidx.appcompat.app.AppCompatActivity
class MainActivity : AppCompatActivity() {
companion object {
init {
// 加载 C++ 原生库
System.loadLibrary("native-lib")
}
}
// 1. 训练:传入人脸样本图,学习肤色特征
private external fun trainSkinFeature(sampleBitmap: Bitmap)
// 2. 追踪:传入全身图,输出带人脸框的结果
private external fun trackHumanFace(srcBitmap: Bitmap, outBitmap: Bitmap)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// ========== 1. 加载两张图片 ==========
// 样本图(整张都是人脸)
val faceSample = BitmapFactory.decodeResource(resources, R.drawable.face_sample)
// 全身待测图
val fullBody = BitmapFactory.decodeResource(resources, R.drawable.full_body)
// ========== 2. 创建输出位图 ==========
val resultBitmap = Bitmap.createBitmap(
fullBody.width,
fullBody.height,
Bitmap.Config.ARGB_8888
)
// ========== 3. 执行算法流程 ==========
trainSkinFeature(faceSample) // 学习肤色
trackHumanFace(fullBody, resultBitmap) // 追踪人脸
// ========== 4. 显示结果 ==========
findViewById<ImageView>(R.id.iv_sample_face).setImageBitmap(faceSample)
findViewById<ImageView>(R.id.iv_full_body).setImageBitmap(fullBody)
findViewById<ImageView>(R.id.iv_result).setImageBitmap(resultBitmap)
}
}
2. C++ 算法核心代码(native-lib.cpp)
包含格式转换、色调直方图提取、反向投影、MeanShift 追踪,所有 API 均适配 Android。
cpp
// JNI 通信头文件
#include <jni.h>
// OpenCV 核心库
#include <opencv2/opencv.hpp>
// Android Bitmap 操作
#include <android/bitmap.h>
// 通道拆分容器
#include <vector>
using namespace cv;
using namespace std;
// 全局变量:保存训练好的肤色直方图
Mat g_skinHist;
// ==========================
// 工具函数1:Android Bitmap → OpenCV Mat
// 作用:将安卓图像格式转为OpenCV可处理的矩阵
// ==========================
Mat bitmapToMat(JNIEnv *env, jobject bitmap) {
AndroidBitmapInfo info;
void* pixels;
// 获取图片信息
AndroidBitmap_getInfo(env, bitmap, &info);
// 锁定像素内存
AndroidBitmap_lockPixels(env, bitmap, &pixels);
// 构建 RGBA 格式矩阵
Mat rgba(info.height, info.width, CV_8UC4, pixels);
Mat bgr;
// RGBA → BGR(OpenCV 默认格式)
cvtColor(rgba, bgr, COLOR_RGBA2BGR);
// 解锁内存
AndroidBitmap_unlockPixels(env, bitmap);
return bgr;
}
// ==========================
// 工具函数2:OpenCV Mat → Android Bitmap
// 作用:将处理后的图像转回安卓可显示格式
// ==========================
void matToBitmap(JNIEnv *env, const Mat& srcMat, jobject dstBitmap) {
AndroidBitmapInfo info;
void* pixels;
AndroidBitmap_getInfo(env, dstBitmap, &info);
AndroidBitmap_lockPixels(env, dstBitmap, &pixels);
Mat rgba;
// 灰度图 / 彩色图分别转 RGBA
if(srcMat.channels() == 1)
cvtColor(srcMat, rgba, COLOR_GRAY2RGBA);
else
cvtColor(srcMat, rgba, COLOR_BGR2RGBA);
// 复制像素数据
memcpy(pixels, rgba.data, info.width * info.height * 4);
AndroidBitmap_unlockPixels(env, dstBitmap);
}
// ==========================
// 【核心】从整张人脸图提取肤色直方图
// ==========================
Mat getSkinHueHist(const Mat& src)
{
Mat hsv;
// BGR 转 HSV(肤色检测必须用)
cvtColor(src, hsv, COLOR_BGR2HSV);
// ✅ 整张图都是人脸,直接作为样本
Mat faceSrc = hsv;
// 拆分 H/S/V 三个通道
vector<Mat> chs;
split(faceSrc,chs);
Mat mask;
// 过滤低饱和度杂色(灰色/白色干扰)
threshold(chs[1],mask,35,255,THRESH_BINARY);
// 直方图参数
int histSize = 18;
float hueRange[] = {0,180};
const float* ranges[] = {hueRange};
int channels[] = {0}; // 只使用 H 通道
Mat hist;
// 计算一维色调直方图
calcHist(&faceSrc,1,channels,mask,hist,1,&histSize,ranges);
// 归一化到 0~255
normalize(hist,hist,0,255,NORM_MINMAX);
return hist;
}
// ==========================
// JNI 接口1:训练肤色特征
// ==========================
extern "C" JNIEXPORT void JNICALL
Java_com_nicoli_helloroidetectskin_MainActivity_trainSkinFeature
(JNIEnv *env,jobject thiz,jobject sampleBitmap)
{
Mat sampleMat = bitmapToMat(env,sampleBitmap);
// 训练并保存肤色直方图
g_skinHist = getSkinHueHist(sampleMat);
}
// ==========================
// JNI 接口2:MeanShift 人脸追踪
// ==========================
extern "C" JNIEXPORT void JNICALL
Java_com_nicoli_helloroidetectskin_MainActivity_trackHumanFace
(JNIEnv *env,jobject thiz,jobject srcBitmap,jobject outBitmap)
{
Mat src = bitmapToMat(env,srcBitmap);
Mat hsv;
cvtColor(src,hsv,COLOR_BGR2HSV);
// 反向投影:生成肤色概率图
float hueRange[]={0,180};
const float* ranges[]={hueRange};
int ch[]={0};
Mat backProj;
calcBackProject(&hsv,1,ch,g_skinHist,backProj,ranges,1.0);
// 初始搜索窗口(画面上半部分)
Rect searchRect(
src.cols * 0.3f,
src.rows * 0.05f,
src.cols * 0.4f,
src.rows * 0.35f
);
// MeanShift 停止条件
TermCriteria criteria(TermCriteria::EPS | TermCriteria::MAX_ITER,15,2);
// 执行追踪
meanShift(backProj,searchRect,criteria);
// 复制原图并绘制绿色框
Mat result = src.clone();
rectangle(result,searchRect,Scalar(0,255,0),3);
// 输出结果
matToBitmap(env,result,outBitmap);
}
3. 布局文件(activity_main.xml)
展示训练图、追踪图、结果图:
xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="8dp">
<!-- 1. 人脸样本图:整张都是人脸 -->
<ImageView
android:id="@+id/iv_sample_face"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:scaleType="fitCenter"
android:adjustViewBounds="true"/>
<!-- 2. 全身原图 -->
<ImageView
android:id="@+id/iv_full_body"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:layout_marginTop="4dp"
android:scaleType="fitCenter"
android:adjustViewBounds="true"/>
<!-- 3. 追踪结果:带绿色人脸框 -->
<ImageView
android:id="@+id/iv_result"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:layout_marginTop="4dp"
android:scaleType="fitCenter"
android:adjustViewBounds="true"/>
</LinearLayout>

项目优势与拓展方向
1. 核心优势
- 轻量无模型:无需深度学习,无权重文件,包体极小;
- 抗光照干扰:基于 HSV 色调,亮度变化不影响效果;
- 高性能:C++ 底层运算,适配 Android 移动端;
- 易集成:与你的肤色检测代码结构统一,直接合并项目。
2. 拓展方向
- 实时相机追踪:改为相机预览流,实现实时目标追踪;
- CamShift 升级:支持目标缩放/旋转,适配更复杂场景;
- 多目标追踪:同时追踪多个物体;
- 手动框选 ROI:支持手指拖拽选择追踪目标。
总结
本文完整实现了 OpenCV MeanShift 目标追踪 在 Android 平台的落地,从原理讲解 到全项目代码全覆盖,且完全对齐你的项目结构。
MeanShift 作为轻量化追踪算法,非常适合移动端无模型、高性能的图像需求,无论是物体追踪、人脸锁定,还是肤色跟踪,都能快速适配使用。
