在前两篇推文中,我们了解了色彩空间、像素、图像和视频之间的组成关系,并且比较详细的学习了色彩空间RGB、YUV的采样存储格式。今天,我们基于这些内容,再补充一些重要的关联知识。 我们已经知道,像素是图像的基本组成单元,所以对视频图像的存储,实际上是对像素的存储。计算机在处理图像时,需要按一定规则将像素数据从内存中读取出来。这里的规则,首先基于色彩的采样存储格式,其规定了色彩分量的存储顺序以及分平面存储逻辑。但仅知道这些信息,对单纯的计算机来说还是不够的,我们必须明确地告诉它:要读取多少字节长度的数据,这里就会引申出定量的规则。一、图像位深 由点及面,先从像素开始,了解每个像素在计算机里是如何定量存储的,再扩展到视频图像上。要学习这部分内容,先给大家介绍一个新面孔:图像位深。 其实,在之前音频要素的推文中,我们已接触到音频采样位深的概念。音频采样位深,指的是用多大的字节空间来存储声音的量化值。一般来说,音频采样位深越大,则声音采样量化的精度越高、失真越少。现在,我们要把位深的概念延伸到视频图像领域。 在视频图像领域,关于位深的概念比较多,诸如:通道位深、像素位深、色彩位深和图像位深等。为了避免混淆,在本文中我们要将相关定义统一一下,并以RGB图像举例说明如下。 对于RGB图像,如果我们分别使用8bit(1个字节)来存储色彩空间的各个通道分量,则一个完整的RGB像素将占用3824bit空间(3个字节)。此时,我们称:通道位深:8bit,表示存储色彩空间的一个分量(通道)需要8bit空间;像素位深:24bit,表示存储一个RGB像素需要24bit空间。 注:本文中,除非特别说明外,我们提及的图像位深均指像素位深。 需要补充的是,图像位深24bit、通道位深8bit是比较标准的位深配置,大家可能还会接触到诸如32bit、16bit、8bit等图像位深,它们并不是3的倍数,无法平摊到RGB或者YUV的三个通道上。我们应该如何理解这些不规则的图像位深呢? 其实,我们只要确认到具体的通道位深,就可以比较清晰的理解了,如下:32bit图像位深:在24bitRGB图像的基础上,增加了一个8bit的透明通道A。比如我们上篇推文提到的RGBA、BGRA等等,可以称为RGBA32、BGRA32;16bit图像位深:R、G、B通道分量,分别使用5bit、6bit、5bit通道位深,可以称为RGB565;8bit图像位深:R、G、B通道分量,分别使用2bit、3bit、3bit通道位深,可以称为RGB233。 除上述举例外,还会有诸如RGBA4444、RGB555等等情况。当脱离本文范畴,大家在实际应用中接触到图像位深时,仍需要明确其具体含义,究竟是像素位深、还是通道位深、每个通道又是怎么分配的,避免混淆。 现在,让我们再回到图像位深为24bit、通道位深为8bit的配置上。在该配置下,RGB的每一个通道分量可表示28256个值。这意味着,如果仅考虑R分量,就会有256种深浅不同的红色。以此类推,三个通道综合,即可以得到(28)316,777,216种不同的组合,每种组合表示不同的颜色。这也就是之前的推文中我们说RGB色彩空间可以表示约1677万种色彩的原因。 显然,图像位深越大,其像素可表示的颜色数量就越多,视频图像的色彩自然也就越丰富、细腻,在色彩渐变处也会更加平滑。有一个比较极端的比喻可以用于帮助理解:试想我们要绘制一幅包含了七色彩虹的画,那么拥有七支不同颜色的画笔(高位深)和只拥有单颜色的画笔(低位深),在绘制效果上自然会呈现出巨大的差异。 参考下图,分别为同一张图像,在24bit位深、8bit位深(28256种颜色)、4bit位深(2416种颜色)下的表现。 图1:24bit 图2:8 bit 图3:4bit 可以看到,在24bit图像位深下,蓝天、云彩、企鹅绒毛的颜色自然细腻、过渡平滑,画面主次鲜明。而位深越低,由于可表示的颜色数量减少,部分颜色数据丢失并被替代,开始出现颜色趋同或断层,画面也越发的不自然。除了降低位深外,位深24bit若往上升,也有更高的30bit(通道位深10bit)、36bit(通道位深12bit)等等。 那么问题来了,参考上面的对比效果,我们是否应该无条件地使用高位深呢? 答案是否定的。 需要注意的是,虽然图像的位深越大,能够表示的颜色越多,但相应需要的存储空间也越大,传输所需的带宽也越多,带来成本的提升,对于软硬件的要求也更苛刻。更何况,24bit图像位深已包含1677万种颜色,这远远超过了人眼的视觉感知能力,足以满足绝大部分业务场景。综合考量,现阶段仍主要使用24bit的图像位深。 以上就是本期课程关于图像位深的相关知识。 C音视频学习资料免费获取方法:关注音视频开发T哥,点击链接即可免费获取2023年最新C音视频开发进阶独家免费学习大礼包!二、图像宽高(Width、Height)与跨距(Stride) 再回到全文的开始:图像的基本组成单元为像素,对视频图像的存储,实际上是对像素的存储。基于图像位深,我们可以确定存储一个像素所需的字节数,下面,可以开始指导计算机如何定量读取图像数据了。 像素在图像中是一行一行排列、并逐行存储在内存中的,计算机在读取图像时,就需要逐行地、正确地读取出每一行的像素。这里就引出两个问题:每一行究竟有多少个像素?计算机每获取一行数据需要读取多少个字节呢?要解答这两个问题,我们需要再学习两个概念:图像的宽高(Width、Height)和跨距(Stride)。1、图像宽、高 说到图像的宽高,大家直觉上可能会联想到厘米、英寸等长度单位,其实,从图像处理的角度并非如此。在视频图像处理上,描述图像宽高时,通常使用的是计数单位,而其具体的数值,则由图像的分辨率决定。 关于视频图像的分辨率,在系列推文中还没有和大家正式介绍,但大家对分辨率肯定是不陌生的。在各大视频图片网站上、在各种视频图像文件规格中,我们常看到诸如540x960(540P)、720x1280(720P)、1080x1920(1080P)等参数,它们就是所谓的分辨率,其表示的含义为:图像在水平方向、垂直方向上,每行、每列的像素个数。宽(Width):水平方向每行的像素个数,等于图像分辨率的宽高(Height):垂直方向每列的像素个数,等于图像分辨率的高 如下图所示,对于分辨率为540x960(宽x高)的RGB图像,其水平方向每行有540个RGB像素,垂直方向每列有960个RGB像素。 图4:像素排布,分辨率540x960,宽x高 不难发现,分辨率宽高相乘得到的数值图像中像素的总个数,540x960的RGB图像中包含518400个像素,分辨率越高,像素的个数也就越多。 关于分辨率的知识,我们今后还会有专题作进一步讨论,今天大家了解到分辨率与图像宽高、像素个数的关系即可。 现在,我们已经通过分辨率信息,确定了图像每行的像素个数,可以尝试计算每行数据的长度(字节)。因为视频图像的处理通常是逐行进行的,计算机更关注每行有多少数据,而对于具体有多少行(Height)没有太多的要求。 以24bit的RGB图像为例,假设分辨率为538x960,因为每个像素的R、G、B分量都连续存储在同一平面上(详见前文色彩和色彩空间中篇),我们可以通过如下步骤,计算每行像素的字节长度:每行像素的个数图像分辨率宽538每行像素的字节长度像素位深x每行像素的个数24bitx5381614byte(注:1byte8bit) 如上,我们得到结论:对于分辨率为538x960的RGB图像,每行有1614byte数据。计算过程看起来清晰明了,有理有据,于是我们信心满满地将1614byte这个字节长度告知计算机,计算机也一丝不苟地按要求去读取一张538x960的图片。却可能会得到如下的结果: 图5:原图,分辨率538x960 图6:按每行1614byte数据,进行读取和渲染 我们发现,实际渲染出来的图像,呈现出规则的斜条纹,与原图相比已面目全非。 为什么会出现这样的问题呢?难道是计算机出现了Bug?或者说,计算机是无辜的,图像每行的像素个数实际上并不等于图像的分辨率宽度?要解答这些问题,我们就需要了解另外一个概念:跨距(Stride)。2、图像跨距 我们知道,计算机的处理器主要是32位或64位的,当处理器执行运算时,一次读取的完整数据量最好为4字节或8字节的倍数。如果我们要求计算机读取非4字节或非8字节对齐的数据,它就需要进行额外的处理工作。额外工作的引入,势必会影响效率和性能。为了规避这样的问题,就需要在原始数据的基础上,再增加一些无效数据,使待处理的数据量对齐到4字节或8字节。这样计算机才能以最高效的方式工作。当然,对齐规则也不一定是4字节8字节的倍数,实际仍取决于具体的软硬件系统。 回过头来,看看前面计算得到的1614byte,大家是否发现问题了呢? 是的,这并非一个4字节或8字节倍数的数值。所以,基于前述的考量,如果在一个要求4字节或8字节对齐的系统内存中存储该图像,往往需要增加一些额外数据,将1614byte对齐到比如1616byte。而这里的1616byte,即称为图像的跨距(Stride)。 跨距(Stride),是图像存储在内存中,每一行数据所占空间的真实大小,它大于或等于通过图像分辨率宽度计算的字节长度。每读取一个Stride长度的数据,意味着完整读取了图像的一行,下次读取就该换行了。其中,用于补齐至Stride而增加的额外数据,我们称之为填充(Padding)。Padding仅影响图像在内存中的存储方式,无需(也不可以)用于实际渲染。 我们可以通过下图,直观的理解Width、Padding和Stride的关系。 图7:Width、Padding和Stride 参考上图,从Start位置开始,计算机只有按Stride读取每一行图像数据,再按Width进行实际的渲染,避免将无效的Padding渲染出来,才能显示出正常的图像。如果仅使用Width计算Stride(比如上面,我们告诉计算机将Stride设置为1614byte),那么就可能会误将部分Padding,视为有效的图像数据进行渲染,行与行之间的像素相对位置也将发生累计偏移,出现诸如斜条纹等异常。 我们也可以通过一些简化的方式,来理解斜条纹产生的原理。 参考下图,我们先忽略字节长度,简单地把图像数据、填充数据的单位都统一至像素。假设原图的WidthxHeight6x8,存储时将Stride对齐为8。图中彩色部分为真实图像(原图左侧),黑色部分为填充的Padding(原图右测),中间存在的空白间隙仅为方便区分。 图8:原图,Stride8,Width6 若使用正确的配置,Stride8进行读取,Width6进行渲染,则仅会显示出彩色部分,黑色部分的Padding在渲染时会被忽略。 如果使用错误的Stride7,正确的Width6,会出现如下问题:从第一行开始,少读取了一块Padding,并将这部分少读取的Padding,误当作第二行的有效图像进行读取、排列。最终,计算机再以Width6进行渲染时,将得到如下图像,出现了右侧下沉的斜条纹效果。 图9:错误,Stride7,Width6,右侧下沉的斜条纹 同理,Stride偏大、Width偏大、Width偏小,都会影响图片的读取和渲染,大家在处理时需要注意。我们在下面也展示出相关的简化参考图: 图10:Stride9,Width6,左测下沉的斜条纹 图11:Stride8,Width7,多渲染出一列Padding数据 注意,实际应用中如果Padding数据被错误渲染出来,不一定都是黑色的,具体由填充的数据而定。如果都使用0值填充,那么RGB图像的Padding为黑色,YUV图像的Padding则为绿色。其他可能的错误情况,大家可以自己尝试推演一下,在此就不过多展开。3、分平面YUV的Width、Stride 上面对于Width、Stride的讨论,都是基于RGB图像来举例。对于RGB图像,其色彩空间分量是同一平面、连续存储的,一般只需考虑一个平面的Width和Stride。 而YUV图像比较特殊,它可能使用分平面(Planar、SemiPlanar)的存储方式(详见色彩和色彩空间中篇)。 从整个图像的角度看,YUV图像的每一行依旧有Width720个像素。但是从存储的角度看,Y、U、V分量可能存放在不同的平面,计算机想要理解YUV色彩,就需要知道:在每个平面上、每次要读取多少数据,才能正确地组合成原始图像的一行像素。 在每个平面上、每次要读取多少数据,意味着需要知道每个平面的Width和Stride。而考虑到U、V分量相对于Y分量可能有降采样,各个分量平面的Width、Stride可能不同,必需要按存储规则分别求取。 下面,我们针对常见的YUV格式:I422、I420和NV21,具体讨论一下,何谓分平面的Width和Stride。 我们将基于通道位深8bit,图像分辨率Width720,Height1280,展开后续内容的讲解。为方便理解、简化过程,我们假设处理器以4字节对齐,通过各平面Width计算得到的数据长度若满足4的倍数,即可作为各平面的Stride,无需考虑Padding填充。 关于这三种YUV格式的采样存储原理,大家可详细参考上一篇推文色彩和色彩空间中篇,下面用到时仅做简述。 由于I422、I420和NV21的Y平面采样逻辑相同,Y分量均为全采样,我们先统一进行计算。 对于Y平面,因为Y分量为全采样,故:WidthYPlane每行Y分量个数图像每行像素个数Width720StrideYPlaneWidthYPlanex通道位深WidthYPlanex8bit720byte 注:因为WidthYPlanex8bit720byte满足预设的对齐要求,故直接作为StrideYPlane,实际应用中,需要另外进行确认。后面若有类似处理,不再重复说明。 对于U、V平面,因为U、V分量在不同YUV格式下有不同的采样、存储逻辑,需要按规则具体计算。3。1I422 I422的采样和存储逻辑简述为:采样:Y分量全采集,宽度方向每两个Y分量共用一组UV分量,高度方向每行独立采集UV分量存储:Y、U、V分别存储于三个平面,对于一个宽度为4个像素、高度为2个像素的采样区域,三个平面分别为4x2、2x2、2x2的数组 对于U平面,U分量水平方向的采样为Y分量的12,故:WidthUPlane每行U分量个数每行像素个数2Width2360StrideUPlaneWidthUPlanex通道位深360byte 对于V平面,其采样存储逻辑与U平面一致,故:StrideVPlaneWidthVPlanex通道位深360byte。 如果使用数组StrideI422〔3〕记录三个平面的跨距(字节长度),即有StrideI422〔3〕{Width,Width2,Width2}(使用Width的数值大小来表示)3。2I420 I420的采样和存储逻辑简述为:采样:Y分量全采集,宽度方向和高度方向每四个Y分量共用一组UV分量,也即第二行复用第一行的UV采样;存储:Y、U、V分别存储于三个平面,对于一个宽度为4个像素、高度为2个像素的采样区域,三个平面分别为4x2、2x1、2x1的数组。 对于U平面,U分量水平方向的采样为Y分量的12(每行),故:WidthUPlane每行U分量个数每行像素个数2Width2360StrideUPlaneWidthUPlanex通道位深360byte 对于V平面,其采样存储逻辑与U平面一致,故:StrideVPlaneWidthVPlanex通道位深360byte 如果使用数组StrideI420〔3〕记录三个平面的跨距(字节长度),即为StrideI420〔3〕{Width,Width2,Width2}3。3NV21 NV21的采样和存储逻辑简述为:采样:Y分量全采集,宽度方向和高度方向每四个Y分量共用一组UV分量,也即第二行复用第一行的UV采样存储:Y、UV分别存储于两个平面,对于一个宽度为4个像素、高度为2个像素的采样区域,两个平面分别为4x2、4x1的数组,UV共同存储于第二个平面,并按V、U的顺序交错存放 对于UV平面,因为U、V水平方向采样均为Y分量的12(每行),并且连续交错存储,故:WidthUVPlane每行U分量个数V分量个数每行像素个数2每行像素个数2Width720StrideUVPlaneWidthUVPlanex通道位深720byte 如果使用数组StrideNV21〔2〕记录两个平面的跨距(字节长度),即为StrideNV21〔2〕{Width,Width} 需要特别注意的是,虽然综合所有平面来说,I422、I420、NV21每次读取的Stride总和,均为Widthx2:StrideI422〔0〕StrideI422〔1〕StrideI422〔2〕Widthx2StrideI420〔0〕StrideI420〔1〕StrideI420〔2〕Widthx2StrideNV21〔0〕StrideNV21〔1〕Widthx2 但对于I420和NV21,因其宽度方向和高度方向每四个Y分量,共用一组UV分量的特性,每次读取U、V平面、或UV平面的一行数据,实际是供Y平面的两行数据共用的。因此,平均下来,读取整张图像的数据总量会存在差异:DataI422DataYDataUDataV HeightxWidthHeightxWidth2HeightxWidth2 HeightxWidthx2DataI420DataYDataUDataV HeightxWidthHeight2xWidth2Height2xWidth2 HeightxWidthx1。5DataNV21DataYDataUV HeightxWidthHeight2xWidth HeightxWidthx1。5 可以看到,DataI422大于其余两个。这也证明了,YUV420相对于YUV422,前者采样数据量更小、压缩率更大。三、总结 以上,即为常见YUV格式Width、Stride的计算方法。如果大家在理解上有些难度,可以再回顾一下色彩和色彩空间中篇的内容,结合进行梳理。需要再次强调的是,为方便理解,上面的讲述中默认:使用Width直接计算得到的Stride符合对齐要求,无需考虑Padding填充,而实践中考虑到不同系统、硬件芯片的对齐处理差异,真实的Stride是否要做补齐,仍需再具体确认。 至此,关于计算机如何正确地、定量读取视频图像数据,我们也有了一定的了解。考虑到硬件芯片、操作系统的多样性,色彩空间采样存储格式的多样性,要完全厘清所有的定量规则,还是比较麻烦的。 对于集成ZEGOSDK开发音视频应用的同学,ZEGO音视频引擎已适配了主流的平台和系统,大家可放心地将视频图像的采集、处理、转换、渲染工作交给SDK,从这些繁琐的细节中解放出来、专注于业务玩法的设计与实现。当然,考虑到灵活性,ZEGOSDK也提供了自定义视频采集的功能,允许开发者自行采集、处理原始视图数据,以满足特定的采集源(比如屏幕采集)或者做进阶的视频前处理(比如美颜特效)需求。开发者只需要将采集、处理后的数据,通过指定接口塞给SDK即可。 不过,在使用自定义视频采集功能时,前面提及的色彩空间、采样存储格式、Width和Stride等概念,就需要你了然于胸,否则就可能出现诸如斜条纹的问题。 最后,我们通过一个思维导图,再梳理一下本文的核心内容。 作者:ZEGO即构链接:https:juejin。cnpost7163505102588739597 音视频开发把地球的故事讲给宇宙