【android opencv学习笔记】Day 12: HSV 色彩空间

HSV 色彩空间

在图像处理中,HSV(Hue 色调、Saturation 饱和度、Value 亮度) 色彩空间是比 RGB/BGR 更符合人类直觉的颜色表示方式。

它将颜色的主色、鲜艳度和明暗度分离,让颜色调整、目标检测变得更加直观高效。

本文将从 HSV 的基础原理出发,结合 OpenCV 代码,实现色彩通道分离、颜色特效制作,并最终完成一个基于 HSV 的肤色检测功能,为你展示 HSV 在实际项目中的强大能力。


为什么选择 HSV 色彩空间?

1. RGB/BGR 的痛点

RGB 色彩空间的三个通道(红、绿、蓝)同时影响颜色的主色、鲜艳度和亮度,无法独立调整。例如,改变亮度时,会不可避免地影响颜色本身,这给颜色检测和调整带来了不便。

2. HSV 的核心优势

HSV 将颜色分解为三个独立的分量,完美契合人类对颜色的感知方式:

  • Hue(色调):表示颜色的"主色",如红、绿、蓝等,范围 0°~360°。在 OpenCV 的 8 位图像中,为了适配 0~255 的存储范围,将其映射为 0~180。
  • Saturation(饱和度):表示颜色的鲜艳程度,范围 0~1(或 0~255)。数值越低,颜色越接近灰度;数值越高,颜色越鲜艳。
  • Value(亮度):表示颜色的明亮程度,范围 0~1(或 0~255)。数值越高,颜色越亮。

这种分离特性让我们可以:

  • 只修改亮度而不改变颜色
  • 只修改饱和度而不改变色调
  • 仅通过色调和饱和度,就能快速识别特定颜色的物体,如肤色、交通标志等。

OpenCV 中 HSV 的基础操作

1. 图像色彩空间转换

使用 cv::cvtColor 函数,可以轻松实现 BGR 与 HSV 的相互转换。

cpp 复制代码
#include <opencv2/opencv.hpp>

int main() {
    cv::Mat image = cv::imread("input.jpg");
    cv::Mat hsv;

    // BGR → HSV
    cv::cvtColor(image, hsv, cv::COLOR_BGR2HSV);

    // HSV → BGR
    cv::Mat bgrOut;
    cv::cvtColor(hsv, bgrOut, cv::COLOR_HSV2BGR);

    cv::imshow("Original", image);
    cv::imshow("HSV", hsv);
    cv::waitKey(0);
    return 0;
}

2. 分离 HSV 三个通道

通过 cv::split 函数,可以将 HSV 图像的三个通道分离为独立的灰度图像,方便观察和处理。

cpp 复制代码
std::vector<cv::Mat> channels;
cv::split(hsv, channels);

// channels[0] = 色调(Hue)
// channels[1] = 饱和度(Saturation)
// channels[2] = 亮度(Value)

cv::imshow("Hue", channels[0]);
cv::imshow("Saturation", channels[1]);
cv::imshow("Value", channels[2]);

3. 制作"高饱和度绘画"特效

我们可以修改亮度通道,将其设置为最大值 255,再合并回 HSV 图像,实现一种高饱和度的绘画效果。

cpp 复制代码
// 转换为 HSV
cv::Mat hsv;
cv::cvtColor(image, hsv, cv::COLOR_BGR2HSV);

// 分离通道
std::vector<cv::Mat> channels;
cv::split(hsv, channels);

// 将亮度通道设置为最大值
channels[2] = 255;

// 合并通道
cv::merge(channels, hsv);

// 转换回 BGR
cv::Mat result;
cv::cvtColor(hsv, result, cv::COLOR_HSV2BGR);

cv::imshow("Painting Effect", result);

实战一

C++ 核心代码(native-lib.cpp)

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);
}

// ==========================
// 【核心】拆分 HSV 三通道
// ==========================
extern "C" JNIEXPORT void JNICALL
Java_com_example_hsvdemo_MainActivity_splitHsvChannels(
        JNIEnv *env,
        jobject thiz,
        jobject srcBitmap,
        jobject outBrightness,  // 亮度 V
        jobject outSaturation,  // 饱和度 S
        jobject outHue           // 色调 H
) {
    // 1. 转成 OpenCV Mat
    Mat src = bitmapToMat(env, srcBitmap);

    // 2. 转 HSV(C++ OpenCV 处理)
    Mat hsv;
    cvtColor(src, hsv, COLOR_BGR2HSV);

    // 3. 拆分通道
    vector<Mat> hsvChannels;
    split(hsv, hsvChannels);

    // H:0, S:1, V:2
    Mat H = hsvChannels[0];
    Mat S = hsvChannels[1];
    Mat V = hsvChannels[2];

    // 4. 输出三个通道图像
    matToBitmap(env, V, outBrightness);
    matToBitmap(env, S, outSaturation);
    matToBitmap(env, H, outHue);
}

Kotlin 界面代码(MainActivity.kt)

Kotlin 只负责显示,不处理任何图像逻辑

kotlin 复制代码
package com.example.hsvdemo

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 splitHsvChannels(
        src: Bitmap,
        brightness: Bitmap,
        saturation: Bitmap,
        hue: Bitmap
    )

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // 原图
        val src = BitmapFactory.decodeResource(resources, R.drawable.test)

        // 创建输出位图
        val bmpBrightness = Bitmap.createBitmap(src.width, src.height, Bitmap.Config.ARGB_8888)
        val bmpSaturation = Bitmap.createBitmap(src.width, src.height, Bitmap.Config.ARGB_8888)
        val bmpHue = Bitmap.createBitmap(src.width, src.height, Bitmap.Config.ARGB_8888)

        // 调用 C++/OpenCV 处理
        splitHsvChannels(src, bmpBrightness, bmpSaturation, bmpHue)

        // 显示
        findViewById<ImageView>(R.id.iv_src).setImageBitmap(src)
        findViewById<ImageView>(R.id.iv_brightness).setImageBitmap(bmpBrightness)
        findViewById<ImageView>(R.id.iv_saturation).setImageBitmap(bmpSaturation)
        findViewById<ImageView>(R.id.iv_hue).setImageBitmap(bmpHue)
    }
}

布局文件(activity_main.xml)

2×2 网格布局:原图、亮度、饱和度、色调

xml 复制代码
<?xml version="1.0" encoding="utf-8"?>
<GridLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:columnCount="2"
    android:rowCount="2">

    <!-- 原图 -->
    <ImageView
        android:id="@+id/iv_src"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:layout_column="0"
        android:layout_row="0"
        android:layout_columnWeight="1"
        android:layout_rowWeight="1"
        android:scaleType="centerCrop"/>

    <!-- 亮度 V -->
    <ImageView
        android:id="@+id/iv_brightness"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:layout_column="1"
        android:layout_row="0"
        android:layout_columnWeight="1"
        android:layout_rowWeight="1"
        android:scaleType="centerCrop"/>

    <!-- 饱和度 S -->
    <ImageView
        android:id="@+id/iv_saturation"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:layout_column="0"
        android:layout_row="1"
        android:layout_columnWeight="1"
        android:layout_rowWeight="1"
        android:scaleType="centerCrop"/>

    <!-- 色调 H -->
    <ImageView
        android:id="@+id/iv_hue"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:layout_column="1"
        android:layout_row="1"
        android:layout_columnWeight="1"
        android:layout_rowWeight="1"
        android:scaleType="centerCrop"/>

</GridLayout>

项目二:基于 HSV 的肤色检测

肤色检测是手势识别、人脸检测等应用的基础。

大量研究表明,不同人种的肤色在 HSV 空间中,都集中在特定的色调和饱和度区间内。

因此,我们可以通过定义 HSV 区间来分割出图像中的肤色区域。

C++ 完整代码(native-lib.cpp)

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 detectSkin(const Mat &src, Mat &result) {
    Mat hsv;
    cvtColor(src, hsv, COLOR_BGR2HSV);

    // 标准肤色范围(H: 0-20, S: 40-255, V: 80-255)
    Scalar lower = Scalar(0, 40, 80);
    Scalar upper = Scalar(20, 255, 255);

    Mat mask;
    inRange(hsv, lower, upper, mask);

    // 形态学开运算,去噪点
    Mat kernel = getStructuringElement(MORPH_RECT, Size(3, 3));
    morphologyEx(mask, mask, MORPH_OPEN, kernel);

    // 提取肤色(彩色)
    src.copyTo(result, mask);
}

// ==========================
// JNI 接口给 Kotlin 调用
// ==========================
extern "C" JNIEXPORT void JNICALL
Java_com_example_skindemo_MainActivity_detectSkin(
        JNIEnv *env,
        jobject thiz,
        jobject srcBitmap,
        jobject outBitmap
) {
    Mat src = bitmapToMat(env, srcBitmap);
    Mat result;

    // 调用肤色检测
    detectSkin(src, result);

    // 输出到Bitmap
    matToBitmap(env, result, outBitmap);
}

Kotlin 界面(MainActivity.kt)

kotlin 复制代码
package com.example.skindemo

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 detectSkin(src: Bitmap, out: Bitmap)

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // 原图
        val src = BitmapFactory.decodeResource(resources, R.drawable.test)

        // 输出图
        val result = Bitmap.createBitmap(src.width, src.height, Bitmap.Config.ARGB_8888)

        // 调用 C++ 肤色检测
        detectSkin(src, result)

        // 显示
        findViewById<ImageView>(R.id.iv_src).setImageBitmap(src)
        findViewById<ImageView>(R.id.iv_result).setImageBitmap(result)
    }
}

布局(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="horizontal"
    android:weightSum="2">

    <!-- 原图 -->
    <ImageView
        android:id="@+id/iv_src"
        android:layout_width="0dp"
        android:layout_height="match_parent"
        android:layout_weight="1"
        android:scaleType="centerCrop"/>

    <!-- 肤色检测结果 -->
    <ImageView
        android:id="@+id/iv_result"
        android:layout_width="0dp"
        android:layout_height="match_parent"
        android:layout_weight="1"
        android:scaleType="centerCrop"/>

</LinearLayout>

肤色检测原理

基于 HSV 色彩空间 的肤色范围筛选是目前最稳定、最常用的方法。

  • H(色调):0~20(对应黄色、橙色、红色,属于肤色区间)
  • S(饱和度):40~255(过滤灰度区域)
  • V(亮度):80~255(过滤过暗区域)

通过 inRange 函数提取符合条件的像素,再通过形态学操作去噪点,最终得到精准的肤色区域。

1. inRange(hsv, lower, upper, mask)

功能:颜色范围提取 → 生成黑白掩码(Mask)

一句话解释:
把在颜色范围内的像素变成白色(255),范围外变成黑色(0)。

--

2. getStructuringElement(MORPH_RECT, Size(3, 3))

功能:创建一个 3×3 的正方形"刷子/模板"

一句话解释:
给图像处理准备一个 3x3 的小方块工具,用来擦除噪点。


3. morphologyEx(mask, mask, MORPH_OPEN, kernel)

功能:开运算 → 去小噪点、让白色区域更干净

一句话解释:
先腐蚀(缩小白色区域),再膨胀(恢复大小),小黑点自动消失!


总结

HSV 色彩空间凭借其直观的色彩表示方式,在图像处理领域应用广泛。本文不仅讲解了 HSV 的基础原理和操作,还实现了高饱和度特效和肤色检测两个实用案例,并提供了 Android JNI 移植方案。

掌握 HSV,你可以轻松实现:

  • 颜色滤镜、调色等照片编辑功能
  • 基于颜色的目标检测(肤色、交通标志、物体跟踪)
  • 图像分割与背景去除

相关推荐
千里马学框架2 小时前
手机大厂Activity嵌套模式及三分屏SplitScreen功能调研报告-独家干货
android·智能手机·分屏·aaos·安卓framework开发·车机·三分屏
Mr.QingBin2 小时前
SystemUI插件开发指南
android
芋只因2 小时前
MySQL 分库分表与 MyCat 的使用
android
Ehtan_Zheng2 小时前
Jetpack Compose 与 RecyclerView 混合布局的性能债
android
南斯拉夫的铁托2 小时前
YOLO学习笔记
笔记·学习·yolo
Bechamz2 小时前
大数据开发学习Day27
java·大数据·学习
van久2 小时前
Day21 第三周总结 + 用户模块收官复盘(可直接当学习笔记)
学习
Slow菜鸟2 小时前
Docker 学习篇(五)| Docker 常用命令
学习·docker·容器
YJlio2 小时前
8.2Windows 11 如何用 Xbox Game Bar 实时监测电脑性能?CPU、内存、GPU、显存与 FPS 瓶颈判断教程
windows·笔记·学习·chatgpt·架构·电脑·xbox