opencv 色彩空间
注:本文使用opencv3.4.2
灰度色彩空间
单通道,取值范围[0,255]
RGB色彩空间(opencv中习惯用BGR)
计算机色彩显示器和彩色电视机显示色彩的原理一样,都是采用R、G、B相加混色的原理,通过发射出三种不同强度的电子束,使屏幕内侧覆盖的红、绿、蓝磷光材料发光而产生色彩。这种色彩的表示方法称为RGB色彩空间表示。
在RGB颜色空间中,任意色光F都可以用R、G、B三色不同分量的相加混合而成:
RGB色彩空间还可以用一个三维的立方体来描述。当三基色分量都为0(最弱)时混合为黑色光;当三基色都为k(最大,值由存储空间决定)时混合为白色光。
opencv中R,G,B三通道取值范围均为[0,255]。
HSV/HSL色彩空间
HSV是一种将RGB色彩空间中的点在倒圆锥体中的表示方法。HSV即色相(Hue)、饱和度(Saturation)、明度(Value),又称HSB(B即Brightness)。色相是色彩的基本属性,就是平常说的颜色的名称,如红色、黄色等。饱和度(S)是指色彩的纯度,越高色彩越纯,低则逐渐变灰,取0-100%的数值。明度(V),取0-max(计算机中HSV取值范围和存储的长度有关)。HSV颜色空间可以用一个圆锥空间模型来描述。圆锥的顶点处,V=0,H和S无定义,代表黑色。圆锥的顶面中心处V=max,S=0,H无定义,代表白色。
还有一种圆柱形表示
HSL和HSV稍有区别,一般我们常用的是HSV模型:
https://blog.csdn.net/binglan520/article/details/56288135
Hue 定义在圆周上,取值范围
。
S和V取值范围是
。
opencv中:
- 图片数据类型为uchar(CV_8U)和ushort(CV_16U)时,默认将Hue映射到
,区间
和区间
重合,此时可以修改
fullRange
为true
将Hue映射到 ,但是我们一般不这样做。 - 图片类型为float(CV_32F)时,将Hue映射到 。
- S和V取值范围是 。
opencv源码:
int hrange = depth == CV_32F ? 360 : isFullRange ? 256 : 180;
CIE-Lab色彩空间
占坑
opencv中的存储
c++ 中,R,G和B通道值的常规范围是:
- CV_8U图像为0到255
- 对于CV_16U图像,为0到65535
- CV_32F图像为0到1
色彩空间转换
使用void cvtColor( InputArray _src, OutputArray _dst, int code, int dcn )
函数。
- src,dst,code,dcn:原图片,目标图片,转换格式,目标图片通道数。
这部分代码位于opencv_imgproc项目的color.cpp中,需要源码编译才能查看(源码编译步骤:https://blog.csdn.net/qq_33485434/article/details/78488710):
下面是cvtColor
的主要代码,篇幅过长,没有全放上来。
void cvtColor( InputArray _src, OutputArray _dst, int code, int dcn )
{
CV_INSTRUMENT_REGION()
if(dcn <= 0) //如果dcn错误,通过code获得正确的dcn
dcn = dstChannels(code);
//这部分涉及UMat(需要opencl,能够使用gpu计算),调用的是ocl_cvtColor()函数
CV_OCL_RUN( _src.dims() <= 2 && _dst.isUMat() &&
!(CV_MAT_DEPTH(_src.type()) == CV_8U && (code == COLOR_Luv2BGR || code == COLOR_Luv2RGB)),
ocl_cvtColor(_src, _dst, code, dcn) )
//代码主要部分,通过switch函数将不同的转换分成不同任务
switch( code )
{
case COLOR_BGR2GRAY: case COLOR_BGRA2GRAY:
case COLOR_RGB2GRAY: case COLOR_RGBA2GRAY:
cvtColorBGR2Gray(_src, _dst, swapBlue(code));
break;
case COLOR_GRAY2BGR:
case COLOR_GRAY2BGRA:
cvtColorGray2BGR(_src, _dst, dcn);
break;
case COLOR_BGR2HSV: case COLOR_BGR2HSV_FULL:
case COLOR_RGB2HSV: case COLOR_RGB2HSV_FULL:
cvtColorBGR2HSV(_src, _dst, swapBlue(code), isFullRangeHSV(code));
break;
case COLOR_HSV2BGR: case COLOR_HSV2BGR_FULL:
case COLOR_HSV2RGB: case COLOR_HSV2RGB_FULL:
cvtColorHSV2BGR(_src, _dst, dcn, swapBlue(code), isFullRangeHSV(code));
break;
...
}
BGR空间到灰度空间
理论公式:
从公式上可以看出,绿色通道占主要部分。
opencv源码:
void cvtColorBGR2Gray( InputArray _src, OutputArray _dst, bool swapb)
{
//CvtHelper是一个opencv自定义类型,定义在Color.hpp中,
//CvtHelper(InputArray _src, OutputArray _dst, int dcn)
//功能是:
//1.验证一下src和dst的数据类型,通道数是否支持;
//2.判断src和dst是否相同。如果src和dst相同,先对src进行copy,操作完再用copy的img覆盖src
//3.涉及到yuv空间有一个尺寸上的变换
//4.把处理后的image赋给自身的成员
CvtHelper< Set<3, 4>, Set<1>, Set<CV_8U, CV_16U, CV_32F> > h(_src, _dst, 1);
//调用cvtBGRtoGray
hal::cvtBGRtoGray(h.src.data, h.src.step, h.dst.data, h.dst.step, h.src.cols, h.src.rows,
h.depth, h.scn, swapb);
}
// 支持三种数据类型:8u, 16u, 32f
void cvtBGRtoGray(const uchar * src_data, size_t src_step,
uchar * dst_data, size_t dst_step,
int width, int height,
int depth, int scn, bool swapBlue)
{
CV_INSTRUMENT_REGION()
//涉及 OpenCV Hardware Acceleration Layer (HAL),不去管它
CALL_HAL(cvtBGRtoGray, cv_hal_cvtBGRtoGray, src_data, src_step, dst_data, dst_step, width, height, depth, scn, swapBlue);
//检查是否支持intel的ipp库,就是那个讨厌的ippicv,这部分我去掉了
...
int blueIdx = swapBlue ? 2 : 0;
if( depth == CV_8U )
//核心代码用CvtColorLoop调用RGB2Gray
//CvtColorLoop里边:
//1.并行处理RGB2Gray每次传入一个pixel,共调用n=width*height次
//2.对像素归一化,像素映射到[0,1]
CvtColorLoop(src_data, src_step, dst_data, dst_step, width, height, RGB2Gray<uchar>(scn, blueIdx, 0));
else if( depth == CV_16U )
CvtColorLoop(src_data, src_step, dst_data, dst_step, width, height, RGB2Gray<ushort>(scn, blueIdx, 0));
else
CvtColorLoop(src_data, src_step, dst_data, dst_step, width, height, RGB2Gray<float>(scn, blueIdx, 0));
}
/*
color.hpp中R2Y, G2Y, B2Y等定义
//constants for conversion from/to RGB and Gray, YUV, YCrCb according to BT.601
const float B2YF = 0.114f;
const float G2YF = 0.587f;
const float R2YF = 0.299f;
enum
{
yuv_shift = 14,
xyz_shift = 12,
R2Y = 4899, // == R2YF*16384
G2Y = 9617, // == G2YF*16384
B2Y = 1868, // == B2YF*16384
BLOCK_SIZE = 256
};
*/
template<typename _Tp> struct RGB2Gray
{
typedef _Tp channel_type;
//RGB2Gray参数
//_srccn:原图片通道数
//蓝色通道index,决定是否对通道转换,BGR=0,RGB=2,BGR和RGB就差在这了
//coeffs:各通道权重
RGB2Gray(int _srccn, int blueIdx, const float* _coeffs) : srccn(_srccn)
{
//0.299f, 0.587f, 0.114f
static const float coeffs0[] = { R2YF, G2YF, B2YF };
memcpy( coeffs, _coeffs ? _coeffs : coeffs0, 3*sizeof(coeffs[0]) );
if(blueIdx == 0)
std::swap(coeffs[0], coeffs[2]);
}
void operator()(const _Tp* src, _Tp* dst, int n) const
{
int scn = srccn;
float cb = coeffs[0], cg = coeffs[1], cr = coeffs[2];
for(int i = 0; i < n; i++, src += scn)
//核心代码!!!
//// 0.299f*src[0]+0.587f*src[1]+0.114f*src[2]
dst[i] = saturate_cast<_Tp>(src[0]*cb + src[1]*cg + src[2]*cr);
}
int srccn;
float coeffs[3];
};
其它转换方法:
https://www.cnblogs.com/zhangjiansheng/p/6925722.html
灰度空间到BGR空间
由于灰度空间到BGR空间的转换进行了升维,我们并不知道B、G、R三个通道所占的概率,所以认为是等概率的,此外,如果有alpha通道,直接设为最大值。公式如下:
前面的通道数、尺寸、数据类型检查、封装、并行加速等和上面类似,直接放核心代码:
template<typename _Tp>
struct Gray2RGB
{
typedef _Tp channel_type;
Gray2RGB(int _dstcn) : dstcn(_dstcn) {}
void operator()(const _Tp* src, _Tp* dst, int n) const
{
if( dstcn == 3 )
for( int i = 0; i < n; i++, dst += 3 )
{
dst[0] = dst[1] = dst[2] = src[i];
}
else
{
_Tp alpha = ColorChannel<_Tp>::max();
for( int i = 0; i < n; i++, dst += 4 )
{
dst[0] = dst[1] = dst[2] = src[i];
dst[3] = alpha;
}
}
}
int dstcn;
};
实验:将彩色图转为灰度图再转回彩色图
int main()
{
Mat img = imread("dog.jpg");
resize(img, img, Size(img.cols / 2,img.rows / 2));//长、宽缩小一倍
Mat grayimg,newimg;
cvtColor(img, grayimg, COLOR_BGR2GRAY);
cvtColor(grayimg, newimg, COLOR_GRAY2BGR);
imshow("origin", img);
imshow("gray img", grayimg);
imshow("new img", newimg);
waitKey(0);
destroyAllWindows();
}
可以看出灰度图转无法真正的转为彩色图。因为并不知道RGB与灰度的比例关系,只能简单地设每个像素的 R=G=B=灰度。转换之后,图片看上去还是灰色。
BGR空间与HSV空间相互转换
直接上官网的截图了:
opencv源码:
rgb转hsv,uchar或ushort型的类RGB2HSV_b
float型的RGB2HSV_f和它很相似,就不放了。
struct RGB2HSV_b
{
typedef uchar channel_type;
//构造函数,初始化列表为:
//source image 通道数
//蓝色分量所在通道的index BGR是0,RGB是2
//h通道范围 180或256
RGB2HSV_b(int _srccn, int _blueIdx, int _hrange)
: srccn(_srccn), blueIdx(_blueIdx), hrange(_hrange)
{
//断言,判断h通道范围是否正确
CV_Assert( hrange == 180 || hrange == 256 );
}
void operator()(const uchar* src, uchar* dst, int n) const
{
int i, bidx = blueIdx, scn = srccn;
const int hsv_shift = 12;
static int sdiv_table[256];
static int hdiv_table180[256];
static int hdiv_table256[256];
static volatile bool initialized = false;
int hr = hrange;
//根据h的取值范围决定hdiv_table指向哪个数组
const int* hdiv_table = hr == 180 ? hdiv_table180 : hdiv_table256;
n *= 3;
if( !initialized )
{
sdiv_table[0] = hdiv_table180[0] = hdiv_table256[0] = 0;
for( i = 1; i < 256; i++ )
{
sdiv_table[i] = saturate_cast<int>((255 << hsv_shift)/(1.*i));
hdiv_table180[i] = saturate_cast<int>((180 << hsv_shift)/(6.*i));
hdiv_table256[i] = saturate_cast<int>((256 << hsv_shift)/(6.*i));
}
initialized = true;
}
for( i = 0; i < n; i += 3, src += scn )
{
//提取分量
int b = src[bidx], g = src[1], r = src[bidx^2];
int h, s, v = b;
int vmin = b;
int vr, vg;
/*
注释中的代码位于precomp.hpp中
extern const uchar icvSaturate8u_cv[]; //数组icvSaturate8u_cv[] 位于tables.cpp中
#define CV_FAST_CAST_8U(t) ( (-256 <= (t) && (t) <= 512) ?icvSaturate8u_cv[(t)+256] : 0 )
#define CV_CALC_MIN_8U(a,b) (a) -= CV_FAST_CAST_8U((a) - (b))
#define CV_CALC_MAX_8U(a,b) (a) += CV_FAST_CAST_8U((b) - (a))
*/
/*获得v(vmax)和vmin*/
//CV_CALC_MAX_8U(a,b) 利用宏命令和查表,将a,b中较大的数赋给a
//CV_CALC_MIN_8U(a,b) 利用宏命令和查表,将a,b中较小的数赋给a
CV_CALC_MAX_8U( v, g );
CV_CALC_MAX_8U( v, r );
CV_CALC_MIN_8U( vmin, g );
CV_CALC_MIN_8U( vmin, r );
/*计算s和h*/
uchar diff = saturate_cast<uchar>(v - vmin);
vr = v == r ? -1 : 0;
vg = v == g ? -1 : 0;
s = (diff * sdiv_table[v] + (1 << (hsv_shift-1))) >> hsv_shift;
h = (vr & (g - b)) +
(~vr & ((vg & (b - r + 2 * diff)) + ((~vg) & (r - g + 4 * diff))));
h = (h * hdiv_table[diff] + (1 << (hsv_shift-1))) >> hsv_shift;
h += h < 0 ? hr : 0;
dst[i] = saturate_cast<uchar>(h);
dst[i+1] = (uchar)s;
dst[i+2] = (uchar)v;
}
}
int srccn, blueIdx, hrange;
};
hsv转rgb uchar或ushort型的类HSV2RGB_b
struct HSV2RGB_b
{
typedef uchar channel_type;
HSV2RGB_b(int _dstcn, int _blueIdx, int _hrange)
: dstcn(_dstcn), blueIdx(_blueIdx), hscale(6.0f / _hrange)
{
}
void operator()(const uchar* src, uchar* dst, int n) const
{
int j = 0, dcn = dstcn;
uchar alpha = ColorChannel<uchar>::max();
for( ; j < n * 3; j += 3, dst += dcn )
{
float buf[6];
buf[0] = src[j];
buf[1] = src[j+1] * (1.0f / 255.0f);
buf[2] = src[j+2] * (1.0f / 255.0f);
//核心代码,调用了HSV2RGB_native
HSV2RGB_native(buf, buf + 3, hscale, blueIdx);
dst[0] = saturate_cast<uchar>(buf[3] * 255.0f);
dst[1] = saturate_cast<uchar>(buf[4] * 255.0f);
dst[2] = saturate_cast<uchar>(buf[5] * 255.0f);
if( dcn == 4 )
dst[3] = alpha;
}
}
int dstcn;
int blueIdx;
float hscale;
};
inline void HSV2RGB_native(const float* src, float* dst, const float hscale, const int bidx)
{
float h = src[0], s = src[1], v = src[2];
float b, g, r;
if( s == 0 )
b = g = r = v;
else
{
static const int sector_data[][3]=
{{1,3,0}, {1,0,2}, {3,0,1}, {0,2,1}, {0,1,3}, {2,1,0}};
float tab[4];
int sector;
h *= hscale;
if( h < 0 )
do h += 6; while( h < 0 );
else if( h >= 6 )
do h -= 6; while( h >= 6 );
sector = cvFloor(h);
h -= sector;
if( (unsigned)sector >= 6u )
{
sector = 0;
h = 0.f;
}
tab[0] = v;
tab[1] = v*(1.f - s);
tab[2] = v*(1.f - s*h);
tab[3] = v*(1.f - s*(1.f - h));
b = tab[sector_data[sector][0]];
g = tab[sector_data[sector][1]];
r = tab[sector_data[sector][2]];
}
dst[bidx] = b;
dst[1] = g;
dst[bidx^2] = r;
}
通过对opencv代码的简单阅读,发现大量使用了查找表来简化计算:
https://blog.csdn.net/qq_23968185/article/details/51282049