3. 色彩空间转换
任务目标
在嵌入式GPU(以Jetson Nano / CUDA为例)上实现RGB与YUV(BT.601限定范围)及RGB与HSV之间的双向转换,验证正反变换误差在可接受范围内。
环境依赖
- CUDA Toolkit ≥ 10.0,OpenCV ≥ 4.1(仅用于图像读写与显示,不参与转换计算)
- 支持C++11的编译器(g++ / nvcc)
核心实现
以下CUDA kernel完成RGB↔YUV与RGB↔HSV双向转换。每个kernel按像素并行处理,使用uchar3表示RGB,float3表示中间浮点结果,避免整数截断累积误差。
#include <cuda_runtime.h>
#include <vector_functions.h>
#include <cmath>
// RGB(0-255) → YUV(BT.601, Y:16-235, UV:16-240)
__global__ void rgb2yuv_kernel(const uchar3* rgb, uchar3* yuv, int n) {
int i = blockIdx.x * blockDim.x + threadIdx.x;
if (i >= n) return;
float r = rgb[i].x, g = rgb[i].y, b = rgb[i].z;
float y = 0.257f * r + 0.504f * g + 0.098f * b + 16.0f;
float u = -0.148f * r - 0.291f * g + 0.439f * b + 128.0f;
float v = 0.439f * r - 0.368f * g - 0.071f * b + 128.0f;
yuv[i] = make_uchar3((unsigned char)y, (unsigned char)u, (unsigned char)v);
}
// YUV → RGB (逆变换)
__global__ void yuv2rgb_kernel(const uchar3* yuv, uchar3* rgb, int n) {
int i = blockIdx.x * blockDim.x + threadIdx.x;
if (i >= n) return;
float y = yuv[i].x, u = yuv[i].y, v = yuv[i].z;
float c = y - 16.0f, d = u - 128.0f, e = v - 128.0f;
float r = 1.164f * c + 1.596f * e;
float g = 1.164f * c - 0.813f * d - 0.391f * e;
float b = 1.164f * c + 2.018f * d;
rgb[i] = make_uchar3(
(unsigned char)fminf(255.0f, fmaxf(0.0f, r)),
(unsigned char)fminf(255.0f, fmaxf(0.0f, g)),
(unsigned char)fminf(255.0f, fmaxf(0.0f, b)));
}
// RGB(0-255) → HSV(H:0-360, S/V:0-1)
__global__ void rgb2hsv_kernel(const uchar3* rgb, float3* hsv, int n) {
int i = blockIdx.x * blockDim.x + threadIdx.x;
if (i >= n) return;
float r = rgb[i].x / 255.0f, g = rgb[i].y / 255.0f, b = rgb[i].z / 255.0f;
float mx = fmaxf(r, fmaxf(g, b)), mn = fminf(r, fminf(g, b));
float delta = mx - mn;
float h = 0.0f;
if (delta > 1e-6f) {
if (mx == r) h = 60.0f * fmodf((g - b) / delta, 6.0f);
else if (mx == g) h = 60.0f * ((b - r) / delta + 2.0f);
else h = 60.0f * ((r - g) / delta + 4.0f);
if (h < 0.0f) h += 360.0f;
}
float s = (mx > 1e-6f) ? delta / mx : 0.0f;
hsv[i] = make_float3(h, s, mx);
}
// HSV → RGB
__global__ void hsv2rgb_kernel(const float3* hsv, uchar3* rgb, int n) {
int i = blockIdx.x * blockDim.x + threadIdx.x;
if (i >= n) return;
float h = hsv[i].x, s = hsv[i].y, v = hsv[i].z;
float c = v * s;
float hp = h / 60.0f;
float x = c * (1.0f - fabsf(fmodf(hp, 2.0f) - 1.0f));
float r = 0, g = 0, b = 0;
if (hp < 1.0f) { r = c; g = x; }
else if (hp < 2.0f) { r = x; g = c; }
else if (hp < 3.0f) { g = c; b = x; }
else if (hp < 4.0f) { g = x; b = c; }
else if (hp < 5.0f) { r = x; b = c; }
else { r = c; b = x; }
float m = v - c;
rgb[i] = make_uchar3(
(unsigned char)((r + m) * 255.0f + 0.5f),
(unsigned char)((g + m) * 255.0f + 0.5f),
(unsigned char)((b + m) * 255.0f + 0.5f));
}
// 主机端调用封装
void launch_rgb2yuv(const uchar3* d_rgb, uchar3* d_yuv, int n) {
rgb2yuv_kernel<<<(n + 255) / 256, 256>>>(d_rgb, d_yuv, n);
}
void launch_yuv2rgb(const uchar3* d_yuv, uchar3* d_rgb, int n) {
yuv2rgb_kernel<<<(n + 255) / 256, 256>>>(d_yuv, d_rgb, n);
}
void launch_rgb2hsv(const uchar3* d_rgb, float3* d_hsv, int n) {
rgb2hsv_kernel<<<(n + 255) / 256, 256>>>(d_rgb, d_hsv, n);
}
void launch_hsv2rgb(const float3* d_hsv, uchar3* d_rgb, int n) {
hsv2rgb_kernel<<<(n + 255) / 256, 256>>>(d_hsv, d_rgb, n);
}
编译与运行
# 编译(假设main.cpp包含图像读写与调用)
nvcc -O2 -o color_convert main.cpp rgb2yuv.cu -I/usr/local/include/opencv4 \
-L/usr/local/lib -lopencv_core -lopencv_imgcodecs
# 运行(输入任意RGB图像)
./color_convert input.jpg
验证方法
- YUV往返误差:对同一图像执行RGB→YUV→RGB,计算两幅RGB图像逐像素差。由于YUV量化到8bit,单通道最大误差≤2,平均误差≤1视为通过。
- HSV往返误差:RGB→HSV→RGB,因HSV以float保存中间量,仅最终量化一次,理论上完全可逆(误差仅来自最后一次取整,≤1)。
- 可视化检查:将YUV的Y通道、HSV的H/S/V分量分别保存为灰度图或伪彩色图,确认亮度/色相结构符合预期。
常见问题
- 若YUV转换后图像偏色,检查是否误用了全范围(0-255)BT.601系数——本实现采用广播级限定范围(Y:16-235),逆变换必须使用对应系数。
- HSV的H分量以度为单位(0-360),若需存储为8bit图像,应除以2再存入uchar。
- 确保输入图像已连续存储在GPU显存(使用
cudaMalloc分配,而非OpenCV的cv::cuda::GpuMat的非连续区域)。