在这里插入图片描述

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)图像反向投影匹配原理

这是算法核心逻辑:

  1. 遍历整张待测图像每一个像素点
  2. 对照提前建好的目标颜色直方图,查询当前像素对应的特征匹配分值
  3. 匹配度越高,像素赋值亮度越高;匹配度越低,像素赋值越暗
  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);
}

在这里插入图片描述

核心原理讲解

  1. ROI 提取:框选目标样本,作为算法的「特征模板」;
  2. 直方图计算:统计样本的颜色分布,生成「特征指纹」;
  3. 归一化:将直方图数值缩放到 0-255,变成概率分布;
  4. 反向投影:遍历全图,给每个像素匹配「特征指纹」,生成概率图;
  5. 阈值化:过滤低概率像素,保留目标区域,输出二值检测图。

函数详解

// ==========================
// 【核心】直方图反向投影目标检测
// ==========================
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 的完整混合开发方案,基于直方图反向投影实现了轻量、高效的图像目标检测。

这套技术方案不仅适用于风景图目标识别,还能拓展到:皮肤检测、颜色物体跟踪、商品识别等移动端图像场景。

相比深度学习模型,它无需训练、无模型文件、启动速度快,非常适合轻量化图像需求。


在这里插入图片描述

Logo

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

更多推荐