YUV色彩格式总结

简介: 笔记

YUV色彩格式总结


上一篇文章结合OpenCV的源代码介绍了BGR转YUV的方法(YUV444)。本文主要介绍YUV的3种采样,YUV444,YUV422, YUV420,以及后两种格式转BGR的方法,和BGR转YUV系列的方法。本系列介绍的公式都是结合OpenCV根据OpenCV的计算方法提供的。


YUV格式的采样方式


YUV格式有3中采样方式,分别是YUV444、YUV422、YUV420;其中YUV444也就是我们通常意义上的YUV,YUV420就是平时使用的NV21和NV12,其中NV12和NV21仅仅是存储顺序的差异。YUV422平时使用的相对较少。

上篇文章中介绍过BGR转YUV,我们知道每一组BGR都会获得一组YUV,所以YUV444就是原始的YUV,是不经过采样的。


YUV444


上一篇文章介绍了BGR转YUV444,每一组BGR转换为一组YUV;转换公式如下:

Y = (4899 * R + 9617 * G + 1868 * B) >> 14;           
V = ((R - Y) * 14369 + delta) >> 14;              
U = ((B - Y) * 8061 + delta) >> 14;              
delta = (255 / 2 + 1) * (1 << 14);

YUV444是BGR直接转换,不进行采样的结果;而YUV422以及YUV420是在YUV444的基础上进行采样得到的。如下图所示,展示了YUV444的一种演示方式:

5.png

image

其中实心黑圈作为整体表示UV分量,空心圈表示Y分量;所以每一个Y拥有一组UV分量。需要注意的是,这仅仅是示意图,表示采样方式,不表示数据的真是存储方式。444可以理解为第一行Y和UV的比是4(第一个4):4(第二个4);第二行Y和UV的比是4(第一个4):4(第三个4);因此使用YUV444表示这种采样方式。这也表示水平采样是4:4;垂直采样是4:4.


YUV422


BGR转YUV422的的公式是一样的,只是对YUV444进行采样,便可以得到YUV422. YUV422的表示如下图所示:

6.png

                      image

可以看到,第一行Y与UV的比例是4:2(第一个2);第二行也是4:2(第二个2);也可以理解为,水平方向上的采样比例为4:2;垂直方向是也为4:2.

以上是YUV422的采样方式。

所以在计算的时候,就可以少计算一半的UV分量;数据量也少一半的UV分量,也就是说,YUV422的数据量只有YUV444的2/3.


YUV420


YUV420的表示方式如下图所示:

7.png

                          image

可以看到,每4个Y拥有一组UV;第一行的采样是4:2;第二行的采样是4:0;所以取名YUV420.但是YUV420又有NV21和NV12两种格式,这两种格式的区别,仅仅是UV分量存储方式上的区别。同时,在数据量上,YUV420仅仅是YUV444的1/2.

以上是关于YUV的3种格式的采样方式的介绍,下面会介绍这YUV格式数据的存储方式。


YUV格式的存储方式


YUV与YCr、Cb

YUV444的存储比较单一,Y单独存储,UV交叉存储,这里主要区分一下YUV444和YCrCb;YCrCb和YUV的区别在两方面:

  • 计算系数
  • 存储顺序

下面是RGB转YUV的代码

{
    typedef _Tp channel_type;
    RGB2YCrCb_i(int _srccn, int _blueIdx, bool _isCrCb)
        : srccn(_srccn), blueIdx(_blueIdx), isCrCb(_isCrCb)
    {
        //设置系数
        static const int coeffs_crb[] = { R2Y, G2Y, B2Y, YCRI, YCBI };
        static const int coeffs_yuv[] = { R2Y, G2Y, B2Y, R2VI, B2UI };
        //yuv和YCrCb的系数不同
        memcpy(coeffs, isCrCb ? coeffs_crb : coeffs_yuv, 5*sizeof(coeffs[0]));
        //RGB和BGR的区别,需要交换B分量和R分量的位置
        if(blueIdx==0) std::swap(coeffs[0], coeffs[2]);
    }
    void operator()(const _Tp* src, _Tp* dst, int n) const
    {
        int scn = srccn, bidx = blueIdx;
        //区分是YUV还是YCrCb
        int yuvOrder = !isCrCb; //1 if YUV, 0 if YCrCb
        int C0 = coeffs[0], C1 = coeffs[1], C2 = coeffs[2], C3 = coeffs[3], C4 = coeffs[4];
        //color.hpp +26 : yuv_shift = 14
        int delta = ColorChannel<_Tp>::half()*(1 << yuv_shift);
        n *= 3;
        for(int i = 0; i < n; i += 3, src += scn)
        {
            int Y = CV_DESCALE(src[0]*C0 + src[1]*C1 + src[2]*C2, yuv_shift);
            int Cr = CV_DESCALE((src[bidx^2] - Y)*C3 + delta, yuv_shift);
            int Cb = CV_DESCALE((src[bidx] - Y)*C4 + delta, yuv_shift);
            dst[i] = saturate_cast<_Tp>(Y);
            //YUV和YCrCb计算系数不同
            dst[i+1+yuvOrder] = saturate_cast<_Tp>(Cr);
            dst[i+2-yuvOrder] = saturate_cast<_Tp>(Cb);
        }
    }
    int srccn, blueIdx;
    bool isCrCb;
    int coeffs[5];
};

具体区别在代码中注释了,首先看计算公式:

Y = (4899 * R + 9617 * G + 1868 * B) >> 14;           
Cr = ((R - Y) * 11682 + delta) >> 14;              
Cb = ((B - Y) * 9241 + delta) >> 14;              
delta = (255 / 2 + 1) * (1 << 14);

存储顺序:

dst[i+1+yuvOrder] = saturate_cast<_Tp>(Cr);
dst[i+2-yuvOrder] = saturate_cast<_Tp>(Cb);

可以看到,YCrCb刚好对应YVU,所以仅仅是UV分量的存储顺序有区别;


YUV420


YUV格式的存储方式有很多,YUV格式的数据存储分为two-plane和three-plane两种方式;所谓的two-plane是指Y单独存储一个plane,UV交叉存储,占用一个plane;three-plane是Y U V分别占用一个plane,一共三个plane.three-plane一般叫做YUV420p,two-plane叫做YUV420sp,我们熟知的NV21和NV12便是YUV420sp。

下面是OpenCV种RGB转YUV420的代码,其中有两个标志位interleaved和swapUV,分别用于区分YUV420p和YUV420sp以及NV21和NV12;NV21的存储是VU,而NV12是UV顺序。

struct RGB888toYUV420pInvoker: public ParallelLoopBody
{
    RGB888toYUV420pInvoker(const uchar * _src_data, size_t _src_step,
                           uchar * _y_data, uchar * _uv_data, size_t _dst_step,
                           int _src_width, int _src_height, int _scn, bool swapBlue_, bool swapUV_, bool interleaved_)
        : src_data(_src_data), src_step(_src_step),
          y_data(_y_data), uv_data(_uv_data), dst_step(_dst_step),
          src_width(_src_width), src_height(_src_height),
          scn(_scn), swapBlue(swapBlue_), swapUV(swapUV_), interleaved(interleaved_) { }
    void operator()(const Range& rowRange) const CV_OVERRIDE
    {
        const int w = src_width;
        const int h = src_height;
        const int cn = scn;
        for( int i = rowRange.start; i < rowRange.end; i++ )
        {
            const uchar* brow0 = src_data + src_step * (2 * i);
            const uchar* grow0 = brow0 + 1;
            const uchar* rrow0 = brow0 + 2;
            const uchar* brow1 = src_data + src_step * (2 * i + 1);
            const uchar* grow1 = brow1 + 1;
            const uchar* rrow1 = brow1 + 2;
            if (swapBlue)
            {
                std::swap(brow0, rrow0);
                std::swap(brow1, rrow1);
            }
            uchar* y = y_data + dst_step * (2*i);
            uchar* u;
            uchar* v;
            //区分two-plane or three-plane
            if (interleaved)
            {
                u = uv_data + dst_step * i;
                v = uv_data + dst_step * i + 1;
            }
            else
            {
                u = uv_data + dst_step * (i/2) + (i % 2) * (w/2);
                v = uv_data + dst_step * ((i + h/2)/2) + ((i + h/2) % 2) * (w/2);
            }
            //区分NV21 or NV12
            if (swapUV)
            {
                std::swap(u, v);
            }
            for( int j = 0, k = 0; j < w * cn; j += 2 * cn, k++ )
            {
                int r00 = rrow0[j];      int g00 = grow0[j];      int b00 = brow0[j];
                int r01 = rrow0[cn + j]; int g01 = grow0[cn + j]; int b01 = brow0[cn + j];
                int r10 = rrow1[j];      int g10 = grow1[j];      int b10 = brow1[j];
                int r11 = rrow1[cn + j]; int g11 = grow1[cn + j]; int b11 = brow1[cn + j];
                const int shifted16 = (16 << ITUR_BT_601_SHIFT);
                const int halfShift = (1 << (ITUR_BT_601_SHIFT - 1));
                int y00 = ITUR_BT_601_CRY * r00 + ITUR_BT_601_CGY * g00 + ITUR_BT_601_CBY * b00 + halfShift + shifted16;
                int y01 = ITUR_BT_601_CRY * r01 + ITUR_BT_601_CGY * g01 + ITUR_BT_601_CBY * b01 + halfShift + shifted16;
                int y10 = ITUR_BT_601_CRY * r10 + ITUR_BT_601_CGY * g10 + ITUR_BT_601_CBY * b10 + halfShift + shifted16;
                int y11 = ITUR_BT_601_CRY * r11 + ITUR_BT_601_CGY * g11 + ITUR_BT_601_CBY * b11 + halfShift + shifted16;
                y[2*k + 0]            = saturate_cast<uchar>(y00 >> ITUR_BT_601_SHIFT);
                y[2*k + 1]            = saturate_cast<uchar>(y01 >> ITUR_BT_601_SHIFT);
                y[2*k + dst_step + 0] = saturate_cast<uchar>(y10 >> ITUR_BT_601_SHIFT);
                y[2*k + dst_step + 1] = saturate_cast<uchar>(y11 >> ITUR_BT_601_SHIFT);
                const int shifted128 = (128 << ITUR_BT_601_SHIFT);
                int u00 = ITUR_BT_601_CRU * r00 + ITUR_BT_601_CGU * g00 + ITUR_BT_601_CBU * b00 + halfShift + shifted128;
                int v00 = ITUR_BT_601_CBU * r00 + ITUR_BT_601_CGV * g00 + ITUR_BT_601_CBV * b00 + halfShift + shifted128;
                if (interleaved)
                {
                    u[k*2] = saturate_cast<uchar>(u00 >> ITUR_BT_601_SHIFT);
                    v[k*2] = saturate_cast<uchar>(v00 >> ITUR_BT_601_SHIFT);
                }
                else
                {
                    u[k] = saturate_cast<uchar>(u00 >> ITUR_BT_601_SHIFT);
                    v[k] = saturate_cast<uchar>(v00 >> ITUR_BT_601_SHIFT);
                }
            }
        }
    }
}

BGR转YUV420的转换公式为:

Y = (R *   269484  + G *   528482  + B *   102760 + (1 << 19) + (1 << 16)) >> 20;
U = (R * (-155188) + G * (-305135) + B *   460324 + (1 << 19) + (128 << 20)) >> 20;
V = (R *   460324  + G * (-385875) + B * (-74448) + (1 << 19) + (128 << 20)) >> 20;

另外需要注意的是,YUV420在计算过程中是需要采样的,每4个Y共同使用一组UV,而这组UV则是取的2x2左上角的点——(0,0);代码如下:

int u00 = ITUR_BT_601_CRU * r00 + ITUR_BT_601_CGU * g00 + ITUR_BT_601_CBU * b00 + halfShift + shifted128;
int v00 = ITUR_BT_601_CBU * r00 + ITUR_BT_601_CGV * g00 + ITUR_BT_601_CBV * b00 + halfShift + shifted128;

可以看到OpenCV在计算的时候取用的是(0,0)位置的点。在别的代码中也可能采取其他的采样方式,比如水平方向上对U采样,垂直方向上对V采样,等等;

另外关于转换系数,根据精度不同,系数也会有出入。表现在移动位数不同,比如OpenCV中,目前移动的位数是20;上一篇文章中介绍BGR转YUV,移动的位数是14;所以在自定义的实现中,可以根据对精度的需求进行修改,当然如果移动位数变少,精度也会下降。


目录
相关文章
|
消息中间件 缓存 NoSQL
谈谈高并发系统的设计方法论
设计 `高并发` 系统,就是要让该系统保证它 `整体可用` 的同时,能够尽可能多的 `处理很高的并发用户请求`,能够 `承受很大的负载流量冲击`。
2009 6
|
6月前
|
人工智能 Linux API
OpenClaw 小红书全自动AI创作内容流水线:从选题发文全流程+阿里云/本地部署+模型配置与避坑指南
在内容创作常态化的今天,小红书日更已经成为博主、运营、商家的核心需求,但持续产出带来的选题焦虑、文案卡顿、配图排版耗时,让绝大多数人难以坚持。OpenClaw(Clawdbot)作为轻量化可扩展的AI智能体框架,能够通过标准化工作流,搭建从选题、文案、配图到排版一站式全自动流水线,将原本2–3小时的内容产出压缩到10分钟内,真正实现一人顶起团队级产能。
1696 0
|
存储 安全 API
电商API合规性:确保数据隐私与法规遵守
在数字化电商时代,API作为连接平台、商家与用户的关键枢纽,承载大量敏感数据。面对日益严格的数据隐私法规,如GDPR、CCPA和中国《个人信息保护法》,合规成为企业发展的核心挑战。本文探讨如何通过系统化方法保障电商API的数据安全与法规遵循,涵盖法规要点、技术实现与最佳实践,助力企业在合规基础上稳健发展。
|
3月前
|
索引 Windows
Everything-1.3.2.643b使用步骤详解(附离线搜索与硬盘文件秒搜教程)
Everything是一款超轻量单机文件搜索工具,安装即用、无需联网,秒搜本地硬盘文件。支持通配符(*.jpg)、路径限定(D:\项目)、排除关键词(报告 !草稿)等高效检索方式,并可集成右键菜单、自定义快捷键与字体,大幅提升查找效率。(239字)
|
Linux 开发工具 Android开发
python之实现apk
python之实现apk
|
数据安全/隐私保护 安全
云大使提现账号更改为淘宝账号(绑定的支付宝)操作攻略
由于业务调整,云大使提现绑定的支付宝账户将换绑为淘宝账户所绑定的支付宝账户,具体操作步骤和常见问题攻略如下,便于大使快速完成换绑工作,避免影响大使的后续提现。关于如何查看自己的淘宝账号名称,通过以下两种方式可快速查看
云大使提现账号更改为淘宝账号(绑定的支付宝)操作攻略
|
缓存 监控 Linux
|
机器学习/深度学习 数据采集 TensorFlow
使用Python实现深度学习模型:智能药物研发与筛选
使用Python实现深度学习模型:智能药物研发与筛选
826 15
|
存储 图形学
手机广告常见的10bit是什么?YUV444、YUV422、YUV420、YUV411是什么?
10bit色深相较于8bit,能提供更多的灰阶和显色数,使色彩过渡更加平滑,减少色带现象。YUV444、YUV422、YUV420、YUV411是不同的采样方式,通过减少UV分量来节省空间。YUV420和YUV411虽都是每4个Y分量用1组UV分量,但YUV420在垂直方向上交替存储U和V,而YUV411仅在水平方向上进行4:1抽样。
|
网络安全 Windows
windows安装ssh服务
windows安装ssh服务
814 0

热门文章

最新文章