【android opencv学习笔记】Day 16: 反向投影实现图像目标精准检测

OpenCV 直方图反向投影实现图像目标精准检测
在 Android 图像开发中,我们经常需要从复杂场景中精准识别特定目标(比如识别图片中的天空、皮肤、特定颜色物体)。
纯 Java/Kotlin 实现图像算法效率低,而借助 OpenCV 的 C++ 底层能力,搭配 Kotlin 做上层业务交互,既能保证性能,又能兼顾开发效率。
本文从零实现:Android Kotlin + C++ 混合开发 + OpenCV 直方图反向投影,完成「图像目标检测」核心功能,彻底解决单通道灰度识别误判问题,用彩色直方图实现高精度检测。
技术方案
1. 为什么选 Kotlin + C++ + OpenCV?
- Kotlin:Android 官方推荐语言,简洁高效,负责 UI 渲染、图片选择、权限申请、结果展示等上层业务;
- C++:底层图像算法执行,OpenCV 原生 C++ 接口性能远超 Java 层,处理大图无卡顿;
- OpenCV 直方图反向投影:无需训练模型,基于「样本颜色特征」快速匹配目标,轻量无依赖,适合移动端。
2. 核心功能
从图片中框选目标样本(ROI) → 提取样本彩色直方图 → 反向投影全图 → 阈值化提取目标,最终输出检测后的二值图像。
核心算法:直方图反向投影原理
1. 基础定义
直方图:统计图像内像素数值分布规律,彩色图像统计RGB三通道像素分布,形成目标独有的颜色特征模板。
反向投影:用提前提取的目标颜色特征模板,遍历整张图像所有像素,计算每个像素与目标特征的匹配相似度,生成相似度热力图。
2. 分步核心原理
(1)ROI样本特征提取原理
手动/固定截取图像中目标区域(ROI感兴趣区域),该区域为标准目标样本,算法仅学习此区域内的颜色分布,以此作为识别依据。
(2)多维颜色直方图构建原理
摒弃单通道灰度直方图(仅亮度特征,易误判),采用BGR三通道三维直方图:
- 将每个颜色通道0~255像素值划分为固定区间(bins)
- 统计样本区域内,每一组三通道颜色组合出现频次
- 最终形成唯一标识目标物体的颜色分布特征矩阵
(3)直方图归一化原理
原始直方图数值差异过大,无法直接用于相似度匹配。通过归一化运算,将所有特征数值统一映射至0~255区间,消除样本大小、明暗差异带来的数值偏差,让特征匹配更通用稳定。
(4)图像反向投影匹配原理
这是算法核心逻辑:
- 遍历整张待测图像每一个像素点
- 对照提前建好的目标颜色直方图,查询当前像素对应的特征匹配分值
- 匹配度越高,像素赋值亮度越高;匹配度越低,像素赋值越暗
- 最终生成一张单通道灰度热力图,直观区分目标与背景区域
(5)阈值分割提纯原理
反向投影生成的热力图存在杂点干扰,通过固定阈值做二值化处理:
- 大于阈值:判定为目标区域,置为纯白255
- 小于阈值:判定为背景干扰,置为纯黑0
最终得到轮廓清晰、无多余噪点的目标检测二值图。
项目实战
Kotlin 负责:图片选择、样本框选、调用 C++ 算法、结果展示。
1. 主页面布局(activity_main.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="16dp">
<!-- 原始图片 -->
<ImageView
android:id="@+id/iv_src"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:scaleType="centerCrop"/>
<!-- 检测结果图片 -->
<ImageView
android:id="@+id/iv_result"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:layout_marginTop="16dp"
android:scaleType="centerCrop"/>
</LinearLayout>
2. Kotlin 核心代码(MainActivity.kt)
package com.nicoli.helloroicalcback
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 {
System.loadLibrary("native-lib")
}
}
// 只调用Native(对齐你的格式)
private external fun detectByBackProjection(src: Bitmap, out: Bitmap)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// 原图
val src = BitmapFactory.decodeResource(resources, R.drawable.puddy)
// 输出图
val result = Bitmap.createBitmap(src.width, src.height, Bitmap.Config.ARGB_8888)
// 调用 C++ 直方图反向投影检测
detectByBackProjection(src, result)
// 显示
findViewById<ImageView>(R.id.iv_src).setImageBitmap(src)
findViewById<ImageView>(R.id.iv_result).setImageBitmap(result)
}
}
2. cpp核心代码
#include <jni.h>
#include <opencv2/opencv.hpp>
#include <android/bitmap.h>
using namespace cv;
using namespace std;
// ==========================
// 工具:Bitmap <-> Mat 转换(和你肤色检测完全一样)
// ==========================
Mat bitmapToMat(JNIEnv *env, jobject bitmap) {
AndroidBitmapInfo info;
void *pixels;
AndroidBitmap_getInfo(env, bitmap, &info);
AndroidBitmap_lockPixels(env, bitmap, &pixels);
Mat mat(info.height, info.width, CV_8UC4, pixels);
Mat bgr;
cvtColor(mat, bgr, COLOR_RGBA2BGR);
AndroidBitmap_unlockPixels(env, bitmap);
return bgr;
}
void matToBitmap(JNIEnv *env, const Mat &mat, jobject bitmap) {
AndroidBitmapInfo info;
void *pixels;
AndroidBitmap_getInfo(env, bitmap, &info);
AndroidBitmap_lockPixels(env, bitmap, &pixels);
Mat rgba;
if (mat.channels() == 1) {
cvtColor(mat, rgba, COLOR_GRAY2RGBA);
} else {
cvtColor(mat, rgba, COLOR_BGR2RGBA);
}
memcpy(pixels, rgba.data, info.height * info.width * 4);
AndroidBitmap_unlockPixels(env, bitmap);
}
// ==========================
// 【核心】直方图反向投影目标检测
// ==========================
void detectByBackProjection(const Mat &src, Mat &result) {
// ========== 1. 选取ROI样本区域(左上角,可自己改) ==========
Rect roi(0, src.rows - src.rows / 8,
src.cols / 8, src.rows / 8);
Mat imageROI = src(roi);
// ========== 2. 计算3D彩色直方图 ==========
int histSize[] = {8, 8, 8};
float range[] = {0, 256};
const float *ranges[] = {range, range, range};
int channels[] = {0, 1, 2};
Mat hist;
calcHist(&imageROI, 1, channels, Mat(), hist, 3, histSize, ranges);
normalize(hist, hist, 0, 255, NORM_MINMAX);
// ========== 3. 反向投影 ==========
Mat backProj;
calcBackProject(&src, 1, channels, hist, backProj, ranges, 1.0);
// ========== 4. 二值化提取目标 ==========
threshold(backProj, backProj, 60, 255, THRESH_BINARY);
// ========== 5. 去噪 ==========
Mat kernel = getStructuringElement(MORPH_RECT, Size(3, 3));
morphologyEx(backProj, backProj, MORPH_OPEN, kernel);
// ========== 6. 输出彩色结果图 ==========
src.copyTo(result, backProj);
}
// ==========================
// JNI 接口给 Kotlin 调用
// ==========================
extern "C" JNIEXPORT void JNICALL
Java_com_nicoli_helloroicalcback_MainActivity_detectByBackProjection(
JNIEnv *env,
jobject thiz,
jobject srcBitmap,
jobject outBitmap
) {
Mat src = bitmapToMat(env, srcBitmap);
Mat result;
// 调用直方图反向投影
detectByBackProjection(src, result);
// 输出到Bitmap
matToBitmap(env, result, outBitmap);
}

核心原理讲解
- ROI 提取:框选目标样本,作为算法的「特征模板」;
- 直方图计算:统计样本的颜色分布,生成「特征指纹」;
- 归一化:将直方图数值缩放到 0-255,变成概率分布;
- 反向投影:遍历全图,给每个像素匹配「特征指纹」,生成概率图;
- 阈值化:过滤低概率像素,保留目标区域,输出二值检测图。
函数详解
// ==========================
// 【核心】直方图反向投影目标检测
// ==========================
void detectByBackProjection(const Mat &src, Mat &result)
解释:
- 输入:
src原图(彩色BGR) - 输出:
result检测后的图(只保留目标区域) - 作用:根据图片左下角一小块区域的颜色,去全图寻找相似颜色,并高亮出来
1. 选取 ROI 样本区域
Rect roi(
0,
src.rows - src.rows / 8,
src.cols / 8,
src.rows / 8
);
Mat imageROI = src(roi);
🧩 逐行解释
-
Rect(x, y, width, height)
创建一个矩形区域,用来框选样本(模板)。- x=0 → 从最左边开始
- y=图片高度 - 1/8高度 → 取图片左下角
- 宽=图片1/8宽
- 高=图片1/8高
-
src(roi)
OpenCV 中,图像(矩形)表示截取该矩形区域。
作用:把左下角一小块图当作要检测的目标模板。
2. 配置 3D 彩色直方图参数
int histSize[] = {8, 8, 8};
float range[] = {0, 256};
const float *ranges[] = {range, range, range};
int channels[] = {0, 1, 2};
🧩 每个参数/API 解释
-
histSize[] = {8,8,8}
每个颜色通道(B、G、R)分成 8个等级。
8×8×8=512个颜色组合,用来描述目标颜色特征。 -
range[] = {0,256}
像素值范围:0~255。 -
ranges[] = {range, range, range}
告诉OpenCV:B、G、R三个通道的范围都是 0~255。 -
channels[] = {0,1,2}
表示使用 B、G、R 三个通道一起计算颜色直方图。
3. 计算 ROI 区域的 3D 直方图
Mat hist;
calcHist(&imageROI, 1, channels, Mat(), hist, 3, histSize, ranges);
normalize(hist, hist, 0, 255, NORM_MINMAX);
🧩 calcHist 详细解释
calcHist = 计算直方图(颜色分布)
calcHist(
&imageROI, // 1. 输入:样本图(ROI)
1, // 2. 输入图片数量=1
channels, // 3. 使用BGR三通道
Mat(), // 4. 不使用掩码
hist, // 5. 输出:计算好的直方图
3, // 6. 3维直方图(BGR三维)
histSize, // 7. 每个维度分8个bin
ranges // 8. 每个维度范围0~255
);
作用:
把样本区域的颜色,统计成一个颜色特征模板。
🧩 normalize 详细解释
normalize(hist, hist, 0, 255, NORM_MINMAX);
- normalize:归一化
- 把直方图数值缩放到 0~255
- 让不同亮度、不同大小的图片都能统一匹配
- NORM_MINMAX:线性缩放模式
4. 直方图反向投影(核心算法)
Mat backProj;
calcBackProject(&src, 1, channels, hist, backProj, ranges, 1.0);
🧩 calcBackProject 逐参数解释
反向投影 = 用样本颜色去全图搜索相似像素
calcBackProject(
&src, // 1. 要检测的整张图
1, // 2. 图片数量=1
channels, // 3. 用BGR三通道
hist, // 4. 样本的直方图(颜色模板)
backProj, // 5. 输出:概率图(越亮=越像样本)
ranges, // 6. 通道范围
1.0 // 7. 比例系数
);
最终效果:backProj 是一张灰度图:
- 亮 = 像素颜色和样本很像
- 暗 = 不像
5. 二值化:把概率图变成黑白掩码
threshold(backProj, backProj, 60, 255, THRESH_BINARY);
🧩 threshold 解释
阈值函数:
threshold(
输入图,
输出图,
60, // 阈值:大于60才算目标
255, // 大于阈值 → 设为白色
THRESH_BINARY // 二值化模式
);
作用:
- 像素值 >60 → 白色(目标)
- 像素值 ≤60 → 黑色(背景)
6. 形态学去噪(去掉小杂点)
Mat kernel = getStructuringElement(MORPH_RECT, Size(3, 3));
morphologyEx(backProj, backProj, MORPH_OPEN, kernel);
🧩 getStructuringElement
创建一个 3×3 的结构元素:
MORPH_RECT= 矩形Size(3,3)= 3x3 小窗口
🧩 morphologyEx
morphologyEx(
输入,
输出,
MORPH_OPEN, // 开运算 = 先腐蚀后膨胀
kernel // 3x3核
);
开运算作用:
去掉小黑点、小白点,让轮廓更干净。
7. 把原图中“目标区域”复制到结果图
src.copyTo(result, backProj);
🧩 copyTo 解释
原图.copyTo(输出图, 掩码图)
规则:
- 掩码中 白色 → 保留原图颜色
- 掩码中 黑色 → 变成黑色
最终输出图 result:
只显示和样本颜色相似的区域,其余全黑。
总结
本文实现了 Android Kotlin + C++ + OpenCV 的完整混合开发方案,基于直方图反向投影实现了轻量、高效的图像目标检测。
这套技术方案不仅适用于风景图目标识别,还能拓展到:皮肤检测、颜色物体跟踪、商品识别等移动端图像场景。
相比深度学习模型,它无需训练、无模型文件、启动速度快,非常适合轻量化图像需求。

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)