
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,你可以轻松实现:
- 颜色滤镜、调色等照片编辑功能
- 基于颜色的目标检测(肤色、交通标志、物体跟踪)
- 图像分割与背景去除
