ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

RGA图像处理:实宽高与虚宽高的区别及对齐避坑指南

RGA图像处理:实宽高与虚宽高的区别及对齐避坑指南 两年前我在RK平台上调一个视频缩放模块NV12的画面怎么调都是斜的——说“斜”也不准确图像像被人一行一行往右下角推最后形成阶梯式错位。我一开始怀疑是格式填错把NV12当成了NV21传UV顺序翻来覆去试了三次没用。后来把原始buffer dump出来看内存里的数据本身完全正确问题出在我告诉RGA的“一行宽度”不对这块buffer真实的行跨度是2048我却按1920填进了虚拟宽高。硬件从第二行开始就找错了下一行的起点等于每一行都比上一行偏移了一小截视觉上就是整幅画面阶梯斜切。那次之后我养成了一个习惯凡是RGA图像出现偏移、斜切、颜色分块错乱第一件事不是排查颜色空间而是先把实宽高和虚宽高打印出来。这两组参数共同决定硬件怎么解释一块内存填错一个后面的缩放、旋转、裁剪全部白搭。这篇是RGA系列的第三篇重点拆解实宽高、虚宽高和RGA硬件的对齐约束也会把我在实际项目中踩过的坑和完整的排错思路一起放出来适合正在用RGA做视频处理、图像缩放、格式转换的嵌入式开发者参考。1. 为什么一幅图会有两个宽高——从一次斜切事故说起1.1 那次让我调了两天的NV12斜切问题先把事故现场还原一下。当时的输入源是一路1080p的NV12视频帧buffer由DMA分配每行实际分配了2048像素的跨度但图像逻辑分辨率是1920x1080。我调用RGA做缩放输出到640x480的目标buffer配置大致是这样的源buffer宽高填1920x1080虚拟宽高也填了1920x1080目标buffer填640x480同步执行缩放。代码在逻辑上完全没毛病但输出画面出现了规律的斜向错位。更麻烦的是这个错位不是整体平移而是每往下一行画面就往右偏一点多行累积之后整幅画面就像被水平剪切过一样。当时我一度怀疑是NV12的UV交错问题反复调整了色度平面偏移画面还是一样。直到我把源帧的每行起始像素数据按行打印出来才发现软件视角里图像的第一行是第0~1919列第二行应该从第1920列开始读但由于RGA被错误告知“一行就是1920宽”它从第一行结束的位置直接接着读了第二行也就是从实际buffer的第1920列开始读——可这一列根本不是下一行的起点而是第一行末尾的填充区域。于是每一行都错位了128个像素累积下来就成了斜切。1.2 实宽高图像真正“看见”的区域实宽高这个概念其实很直观。它就是图像的实际分辨率或者说你真正希望硬件去处理、显示、输出的那一块区域。比如上面例子里的1920x1080就是这帧画面的实宽高缩放目标中的640x480同样是目标画面的实宽高。在RGA的老接口rga_info_t里对应的是src_act_w、src_act_h、dst_act_w、dst_act_h这些字段在librga的im2d接口里对应的是rga_buffer_t结构体里的width、height。这个值决定了“图像本身有多宽多高”也决定了一个矩形区域内有几行、每行又有多少个有效像素。硬件读内存时靠的就是这个值去界定有效内容范围。很多刚接触RGA的开发者容易把实宽高理解为“buffer有多大”这是两个完全不同的概念。buffer大小描述的是内存占用实宽高描述的是图像内容大小。内存可能比图像内容大因为里面塞了对齐填充、额外的边际、甚至是为了性能预留的空白区域。一旦把“内容大小”和“内存布局大小”混为一谈图像解析就会出现偏差。1.3 虚宽高内存里一行真正跨越的步长虚宽高才是那块更容易让人绕晕的“第二套尺寸”。它的本质是整个buffer的行跨度stride——也就是内存布局里从这一行起始位置到下一行起始位置之间到底间隔了多少个像素单位。为什么需要这个值因为图像在内存里并不是紧凑排列的。硬件、DMA、Cache都有对齐要求分配buffer时往往会在每一行末尾追加一些填充像素让行宽对齐到某个整数值。例如实际宽1920的NV12帧分配buffer时可能给每行2048的跨度实际宽1280的RGB图像分配时可能给每行1280、但缓存行对齐后变成1296甚至1312。虚宽高在rga_info_t里对应src_vir_w、src_vir_h、dst_vir_w、dst_vir_h在im2d的rga_buffer_t里对应wstride、hstride。如果实宽高是“这一行里有1920个有效像素”那么虚宽高就是“从一行开头跨到下一行开头要前进2048个位置”。只有同时告诉硬件这两组数据它才能准确地跳到每一行真正的内容起始地址然后从那里往后读1920个有效像素。打个比方你在一间阶梯教室里找人座位表告诉你每排有50个座位但现实中每两排之间隔着一条过道找到第2排的人必须跨过过道而不是直接顺着第1排末尾接着数。虚宽高就是这条“过道”的距离你不告诉硬件这条过道的存在它一定找错人。2. 虚宽高背后的对齐约束硬件为什么这么严格2.1 三个层面的对齐地址、行宽、尺寸奇偶搞懂虚宽高之后下一个自然的问题是为什么buffer的行宽不能直接用1920非要搞出一个2048来原因可以拆成三个层面来看分别是对齐的三种约束。第一层是地址对齐。RGA是硬件引擎数据要通过DMA搬运DMA和Cache Line都要求buffer的起始地址按一定字节边界对齐。常见的要求是4字节、16字节、64字节对齐有的平台还要求物理地址页对齐。地址不对齐时轻则性能下降重则出现总线错误或者花屏。这个约束主要在分配buffer那一步解决使用RGA时更多是检查和验证。第二层是行宽对齐。图像每一行的字节数最好也按硬件引擎擅长的对齐宽度来排布。16字节对齐是最常见的底线64字节对齐则通常在追求更好的Cache利用率时采用。对于RGBA8888这种每像素4字节的格式16字节对齐等价于一行像素数必须是4的倍数RGB565每像素2字节16字节对齐等价于一行像素数必须是8的倍数NV12这类YUV半平面格式Y平面每像素1字节行字节数本身就是像素数所以像素跨度对齐到16的倍数即可。第三层是尺寸奇偶。这个约束来自YUV420家族的采样方式U和V平面是隔行隔列采样的也就是说水平方向和垂直方向都按2:1降采样。因此NV12、NV21这类格式逻辑宽高必须是偶数否则色度平面没办法完整映射回亮度分辨率。RGB类格式没有这个限制任意宽高都能处理。2.2 YUV420的UV平面偏移为什么和虚宽高关系密切NV12这类半平面格式还有一个非常隐蔽的坑它的Y平面和UV平面在内存里是连续排布的UV平面的起始地址不是靠“height后面紧跟着”就能算的它取决于Y平面的实际跨度也就是虚宽高。如果你的buffer行宽是2048实宽高1920x1080那么Y平面实际占用的内存大小是2048 * 1080字节而不是1920 * 1080字节。UV平面必须从Y平面结束的位置也就是基于2048这个跨度算出的偏移量开始。如果你在RGA接口里把虚宽高填成1920硬件计算UV平面偏移时也会跟着错——它以为Y平面只占了1920 * 1080字节于是UV平面的读取位置整体提前了。结果就是画面不仅可能有亮度行的斜切还会出现色度错乱、绿边、紫边之类的诡异颜色。类似地如果你在外部代码里手工计算NV12的UV偏移计算方式也必须严格基于buffer实际分配时的stride而不是图像逻辑宽度。很多从视频采集模块拿到的buffer自带bytesperline信息这个字段就是真实的行跨度务必直接拿来使用。2.3 常用格式的对齐要求速查不同格式的宽高、stride约束差异非常大。我把几个常用格式的经验值整理成了一张表方便直接对照查阅。需要说明的是具体芯片版本、librga版本不同最终约束会有细微差别运行前最好先用小尺寸测试图验证一遍。格式stride建议像素宽高约束备注RGBA88884的倍数对应16字节对齐无特殊限制每像素4字节常用UI叠加层RGB5658的倍数对应16字节对齐无特殊限制每像素2字节低配屏常用NV12 / NV2116的倍数UV随Y宽度和高度必须为偶数Y与UV平面连续排布UV偏移依赖Y平面strideYUYV8的倍数每2像素共享一组UV宽度必须为偶数打包格式行字节数宽度*2压缩格式AFBC等按block对齐通常64/128取决于压缩块大小建议直接查询平台rmd不手工推断这张表的价值在于当你怀疑对齐问题时可以快速判断是“格式本身的约束”还是“buffer分配带来的约束”。前者属于RGA的硬性限制后者属于你的内存管理问题处理思路完全不同。3. 接口字段怎么填从rga_info_t到im2d的实虚宽高3.1 老接口rga_info_t里那些绕口的字段在librga还以rga_info_t为主的年代接口字段是最容易让人看懵的。这个结构体里同时存在act_w/act_h、vir_w/vir_h、rect等字段各自作用如下act_w/act_h实际宽高也就是图像的逻辑分辨率。实宽高填这里。vir_w/vir_h虚拟宽高也就是图像在内存中的跨度。虚宽高填这里。rect裁剪矩形在源图像内部定义实际参与运算的区域坐标基于实际宽高。很多坑就出在vir_w和act_w的配对使用上。比如有人把act_w1920、vir_w1920但buffer实际是2048跨度的于是整个画面斜切有人反过来把vir_w填得很大act_w填得较小硬件在拷贝时可能会把多余的行尾填充也当成有效数据扫进去导致输出画面右侧出现异常条纹。在老驱动时代vir_w还常常被要求按16像素对齐。换句话说即使你的图像实际宽是1920但驱动规定虚拟宽必须是16的倍数而1920恰好满足如果换成1282这类宽度就需要把vir_w对齐到1296之类的位置同时确保buffer真的按这个跨度去分配。这里的核心经验是虚拟宽高不是“我想填多少就填多少”它必须严格等于buffer分配时的真实跨度。先分配后赋值顺序不要反。3.2 im2d的rga_buffer_t与wrapbuffer系列函数新一代librga主推im2d接口结构体换成了rga_buffer_t字段更清晰但换了个名字照样有人填错。核心成员和含义如下typedef struct _rga_buffer_t { uint32_t width; // 实际宽度相当于实宽高里的“宽” uint32_t height; // 实际高度相当于实宽高里的“高” uint32_t wstride; // 虚拟宽行跨度单位通常为像素 uint32_t hstride; // 虚拟高极少使用一般等于height int format; // 像素格式如RK_FORMAT_NV12 } rga_buffer_t;常用封装函数是这样的#include im2d.h #include rga.h rga_buffer_t src wrapbuffer_fd(src_fd, 1920, 1080, 2048, 1080, RK_FORMAT_NV12); rga_buffer_t dst wrapbuffer_fd(dst_fd, 640, 480, 640, 480, RK_FORMAT_NV12); IM_STATUS ret imscale(src, dst, {}, RGA_INTERP_LINEAR); if (ret ! IM_STATUS_SUCCESS) { // 实际项目里建议打印 imStrError(ret) }其中wrapbuffer_fd的第二个和第三个参数是width和height对应实宽高第四个和第五个参数是wstride和hstride对应虚宽高。这里有一个版本差异必须提醒不同版本的librga对wstride的单位定义可能不同有的按像素、有的按字节。我在不同平台上都碰到过最稳妥的做法是翻一下手头librga版本的头文件注释或者写一个已知buffer的小测试确认当前库的行为之后再大规模使用。3.3 最常见的三种错误填法我在代码评审里见过最多的错误集中在下面三种形态。第一种只填width/height不填wstride然后指望硬件“聪明地”自动给你补。wrapbuffer_*系列函数在stride为0时确实会按width帮你做默认对齐但这个默认值不一定等于你的buffer真实跨度。如果你的内存分配器已经做过对齐真实stride是2048而函数默认按1920算斜切画面就又回来了。正确做法是把stride显式传进去并且从buffer分配信息里读取而不是自己推算。// 错误默认stride可能与实际buffer跨度不一致 rga_buffer_t src wrapbuffer_fd(fd, 1920, 1080, 0, 0, RK_FORMAT_NV12); // 正确显式传入分配时的行跨度 rga_buffer_t src wrapbuffer_fd(fd, 1920, 1080, 2048, 1080, RK_FORMAT_NV12);第二种旋转之后想把dst的stride也一起交换。旋转90°时输出图像的宽高确实要交换但wstride描述的是“目标buffer内存布局”不是“图像旋转后的显示跨度”。假设你为输出分配了一个1080x1920的bufferwidth1080、height1920没问题可wstride仍然由这个buffer分配时的跨距决定比如是1088你不能为了“应变”而把它填成1920。序列化这三个值必须分开看待。第三种把V4L2采集到的bytesperline直接当wstride用。V4L2的bytesperline单位是字节如果librga的wstride单位是像素这里就存在一个换算关系。RGBA格式需要除以4RGB565需要除以2NV12这类格式则要小心因为bytesperline通常对应Y平面的stride而Y平面每像素1字节数值上刚好可以直接填入以像素计的wstride但前提是平台没有额外做特殊处理。最好的习惯是写一个统一的换算函数并且打印日志核对不要靠肉眼看代码想当然。4. 四种高频场景的宽高配置对照4.1 全尺寸缩放源和目标stride都刚好对齐最理想的情况是来源和目标buffer的行宽都已经对齐且你自己清楚这些值。配置方式很直接把源实宽高、源stride、目标实宽高、目标stride全部显式写入即可。// 源1920x1080buffer行宽1920恰好对齐 // 目标640x480buffer行宽640恰好对齐 rga_buffer_t src wrapbuffer_fd(src_fd, 1920, 1080, 1920, 1080, RK_FORMAT_NV12); rga_buffer_t dst wrapbuffer_fd(dst_fd, 640, 480, 640, 480, RK_FORMAT_NV12); IM_STATUS ret imscale(src, dst, {}, RGA_INTERP_LINEAR); if (ret ! IM_STATUS_SUCCESS) { // 处理失败 }这类场景出错率最低但仍然建议把wstride/hstride显式写上而不是依赖默认对齐。因为一旦你看不到stride犯错时很难直觉发现问题。4.2 从摄像头未对齐stride直接做缩放摄像头采集buffer的bytesperline在一些老驱动或者特殊分辨率下可能不是16的倍数。此时有两个选择一是让RGA直接消费这个未对齐的stride前提是你的librga版本允许二是先把数据拷贝到一个按规范对齐的中间buffer再做RGA处理。两种方式都能出图但性能差别很大。我的实际建议是优先尝试直接消费原始bytesperline。新版本librga对stride的兼容性比老驱动好很多直接传入真实跨度往往能跑通省去一次全帧内存拷贝。只有当你确认当前版本确实不支持、或者驱动日志明确报对齐错误时再走中间buffer拷贝。拷贝本身要按目标stride一行一行复制别用memcpy整块搬否则还是错位。// 以NV12为例bytesperline1920时像素跨度1920 // 如果bytesperline1920641984则wstride1984 rga_buffer_t src wrapbuffer_fd(camera_fd, v4l2_width, v4l2_height, v4l2_bytesperline, v4l2_height, RK_FORMAT_NV12);注意这里v4l2_height同时填了实高度和虚高度因为大部分采集buffer在垂直方向没有额外padding。如果采集驱动在垂直方向也有对齐比如hstride大于实际高度这里就需要单独取v4l2_sizeimage / v4l2_bytesperline来反推实际行数再做决定。4.3 旋转90度实宽高需要交换stride不需要旋转是最容易把宽高体系带跑的场景。RGA的旋转接口大致如下调用模式rga_buffer_t src wrapbuffer_fd(src_fd, 1920, 1080, 2048, 1080, RK_FORMAT_NV12); rga_buffer_t dst wrapbuffer_fd(dst_fd, 1080, 1920, dst_stride, dst_hstride, RK_FORMAT_NV12); IM_STATUS ret imrotate(src, dst, IM_HAL_TRANSFORM_ROT_90, RGA_INTERP_LINEAR);这里src.width1920, src.height1080而dst.width1080, dst.height1920实宽高确实交换了。但dst_stride是“目标buffer分配时那一行到底占多宽”不会因为你旋转90°就跟着变成1920或者别的值。如果你重新分配了一个1080x1920的buffer它的行宽大概率是1088按16字节对齐后的像素值然后传给dst_stride1088。不要简单写成dst_stridedst.width也别顺手把旋转后的逻辑高度填进stride位置。判断是否写对有个很简单的验证法旋转后画面应该是一个完整的矩形没有阶梯错位、没有左右漂移、没有奇怪的多余边条。出现任何一行行的错位几乎都是stride没写对而不是角度参数没写对。4.4 裁剪显示区域rect和stride如何协作当你只需要处理源图的某个子区域时问题会更绕一点。源图本身是1920x1080你想裁剪中间640x480的区域送到RGA。这时候src.width/height仍然填源图的完整尺寸src.wstride也仍然是整幅buffer的行跨度裁剪框通过im_rect_t结构单独传入坐标基于实宽高的逻辑坐标。rga_buffer_t src wrapbuffer_fd(src_fd, 1920, 1080, 2048, 1080, RK_FORMAT_NV12); rga_buffer_t dst wrapbuffer_fd(dst_fd, 640, 480, 640, 480, RK_FORMAT_NV12); im_rect_t crop {.x 640, .y 300, .w 640, .h 480}; IM_STATUS ret improcess(src, dst, {}, crop, {}, RGA_INTERP_LINEAR);这里有个关键点裁剪坐标和宽高虽然基于实宽高但硬件计算内存偏移时仍然依赖stride来决定“从第y行开始需要跳到哪”。如果你以为裁剪区域小、就不用填源图的stride或者把stride改成裁剪区域的宽度那么硬件在计算行偏移时会全部乱套。裁剪只是减少读入范围内存布局没有变stride就必须保持原样。5. 一个NV12缩放案例的完整排错过程5.1 现象旋转90°后出现周期性竖条纹项目里曾遇到一个反馈一张NV12源图旋转90°输出到目标buffer后画面出现规律的竖条纹像梳子一样而且条纹的间距以及错位程度随行号变化。值班的同事第一反应是UV顺序错了把RK_FORMAT_NV12改成RK_FORMAT_NV21画面颜色乱了又怀疑目标buffer太小改大容量条纹依旧。5.2 逐步缩小怀疑范围我把现场的数据要过来之后做了一组对照实验。先用纯红色图像做输入旋转后输出仍然有周期条纹再把旋转角度改成180°条纹消失画面完全正常。这就排除了格式、颜色空间这类问题——因为如果UV错位180°旋转时也会花。接下来怀疑点集中在目标buffer的stride上。由于旋转90°会让输出宽高变为1080x1920代码里大概率需要把dst.width/height设置成1080/1920但dst_stride可能被随手写成了dst.width也就是1080。5.3 定位到真实原因目标stride被写成了逻辑宽度我去查目标buffer的实际分配记录发现内部使用的是带对齐的行宽实际stride是1088。而代码里传给rga_buffer_t的wstride却是1080。这一差就是8个像素看起来不多但旋转后每一行的读取起点都往后偏了8个像素越往下累积越明显最终形成了周期性的竖条纹。随后我用一行网格线做了验证。让输出第一行显示纯绿其余行显示纯黑结果绿色行只有前半段正常后半段被下一行内容污染再打印目标buffer的bytesperline确认实际为1088。到这里根因已经很明确代码对“旋转后宽高互换”做了正确处理却漏掉了“stride不随旋转交换”这个事实。5.4 修复与验证修复方式很简单把目标buffer的wstride从1080改成1088并确认height字段不跟着乱改。改完后重新跑旋转用例网格线横平竖直纯色帧全部正常再叠加真实视频流验证了半小时画面稳定。这个case很有代表性它看似是“旋转有问题”实际是“stride字段填写错误”。排查时最有效的手段不是反复猜测而是把每个字段和实际buffer信息打印出来一一核对。建议所有RGA相关代码的调试日志里至少包含以下信息源实宽高、源stride、目标实宽高、目标stride、buffer实际分配大小、格式。有了这组数据90%的错位问题都能一眼定位。6. 关于对齐我在实战中总结的边界经验6.1 对齐应该在分配阶段做而不是使用阶段补救很多性能问题的根源不是RGA本身慢而是数据格式不满足对齐要求导致系统在硬处理之前引入了额外的软件拷贝。最理想的流程是把对齐思维提前到buffer分配阶段申请内存时stride直接按对齐要求算好之后往buffer里填数据、传给RGA、再做后续操作全程都使用同一个对齐后的stride。不要在使用阶段临时填充、挪动数据那样既徒增耗时又容易引入新的错位。6.2 老驱动和新librga的兼容性实测过才放心不同芯片版本、不同librga版本对stride的宽容度差别很大。老驱动通常强校验各种对齐有一点不满足就直接返回参数错误新库普遍更宽容甚至会在内部做自动对齐但自动对齐的结果未必符合你的数据分布。跨平台迁移或者升级librga时建议先用一套包含不同stride情况的测试用图跑一遍确认行为一致再交付。不要只看API文档觉得“应该没问题”。6.3 快速自查清单我把常见的RGA图像异常和对应的检查方向整理成了清单排查时按顺序过一遍异常现象优先检查项次要检查项阶梯式斜切、画面整体偏移源buffer的wstride是否等于真实行跨度源buffer的hstride是否被改变旋转后出现竖纹/横纹目标buffer的stride是否等于分配跨度而非逻辑宽旋转后width/height是否互换颜色错乱、出现绿边紫边格式是否写对NV12/NV21YUV420宽高是否偶数UV偏移是否受stride影响输出画面有额外边条目标buffer分配大小是否足够hstride是否被错误设置性能突然下降是否发生了额外的软件拷贝地址是否真正按64字节对齐这套清单基本覆盖了日常开发和联调中的高频问题。RGA本身不算难用真正让人翻车的永远是对“内存布局”和“图像内容”这两套概念的混淆。实宽高是内容虚宽高是布局对齐约束是硬件的前提条件三者是一体的。无论你的代码里用的是老接口还是im2d接口只要把这组关系理顺RGA相关的图像问题基本就不会再让你熬夜了。
返回列表