ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

基于FPGA的图像放大与插值处理:算法选型、硬件架构与实战避坑指南

基于FPGA的图像放大与插值处理:算法选型、硬件架构与实战避坑指南 做视频采集和显示项目的时候最躲不开的一件事就是图像放大。传感器输出的分辨率是固定的但显示端的需求千奇百怪1080p的摄像头可能要接4K的屏720p的MIPI源又希望做算法前把分辨率抬到1080p所以FPGA里面做插值处理基本是必修课。这一节我们先不急着写代码把“基于FPGA的图像放大和插值处理”整条线的方案选型、硬件代价和常见坑讲清楚后面再逐个展开具体实现。适合正在做视频接口、图像预处理、或者刚把FPGA开发板跑起来想往图像方向走的朋友参考。1. 为什么偏要在FPGA里做图像放大1.1 视频链路里分辨率转换到底卡在哪先说一个最基础的场景MIPI摄像头输出1080p60fps接的屏幕是4K60。传感器这边像素时钟跑在148.5MHz左右一帧画面是1920x1080逐行扫描屏幕那边需要3840x2160像素时钟要跑到594MHz。两个接口的分辨率和时序都不一样你不能直接把数据流一股脑怼过去得在中间做一次缩放。那放大到底做了什么事本质上就是从输入的1920x1080个像素里按照一定的规则“生成”出3840x2160个像素。多出来的像素没有真实对应物只能靠周围已知像素去推断。这个“推断”就是插值处理。你可能想用一颗带GPU的SoC或者CPU来做不香吗很多场景确实可以但FPGA的优势在于流水线和确定性延迟。CPU处理一帧图像延迟可能是几十毫秒而且容易受系统调度影响FPGA的缩放模块是逐像素流式处理的输入一个像素经过固定的流水线周期输出一个或多个像素延迟可以做到只有几十个时钟周期。如果你要做实时视频通路、无人机图传、医疗内窥镜这类对延迟敏感的系统FPGA这个特性就很难替代。再补充一点FPGA做缩放不需要把一整帧图像都存下来再算它只需要缓存几行数据就够了具体几行取决于插值算法的邻域大小。这一点对存储资源紧张的工程特别关键。小分辨率放大到大分辨率比如从640x480放大到1920x1080如果用帧缓存内部存储或者外部DDR的带宽压力会非常大用行缓存的话几十KB的BRAM就能搞定外部DDR带宽可以腾出来给其他模块用。1.2 插值处理在FPGA中的角色和代价插值算法选什么直接决定视频质量和逻辑资源。最简单的是最近邻实现起来就是一个取整操作几乎不消耗逻辑复杂一点的双线性插值需要2x2的邻域也就是至少缓存两行像素双三次插值要4x4的邻域需要缓存四行同时要做16个像素的加权乘法DSP资源消耗不算小。我在实际项目里见过不少同学一上来就选双三次觉得画质最好结果综合完资源超了时序也跑到300MHz上不去。其实很多显示场景人的眼睛对双线性和双三次的差异感知并不明显尤其内容在运动的时候。插值处理的设计一定要从系统级去看输入分辨率多少输出分辨率多少颜色格式是RGB888还是YUV422数据率多大可用的BRAM、DSP有多少这些先列出来再定算法。另外有个隐藏成本容易被忽略——行缓存的数量和宽度。行缓存宽度跟像素位宽和每行像素数直接相关。比如1920x1080的RGB888一行是1920乘24bit约46080bit两条行缓存就是92Kbit左右。如果设计里BRAM本身紧张这个开销会直接影响算法选型。还有带宽问题。放大后输出像素数变多了例如1080p放大到4K输出像素率是输入的4倍。如果你的架构是输出时钟等于输入时钟那就得考虑多像素并行输出如果允许输出时钟比输入时钟高比如用PLL产生594MHz那么逐像素输出也能跟上。这个时钟方案在架构设计阶段就要定下来否则后面改起来牵一发动全身。2. 插值算法的选型和取舍2.1 最近邻最便宜但真的糙最近邻的实现思路一句话输出像素坐标映射回输入坐标四舍五入取最近的像素值。比如输出第3个像素对应输入的第1.6个像素取第2个像素的值。Verilog里就是一堆移位和加法不需要任何乘法器。代价小到什么程度可以说几乎没有额外资源BRAM只缓存一行像素甚至可以不缓存直接用输入像素时钟打拍。但效果呢——放大到整数倍还勉强能看一旦放大倍数不是整数比如从1920放大到2560图像边缘会出现明显的锯齿和“阶梯”文字的笔画歪歪扭扭。所以最近邻的适用范围很窄。我一般只在两种情况下用它一是项目初期用来验证缩放通路是否打通先跑通整个链路后面再替换算法二是对画质完全无感的场景比如把分辨率降低用作算法前处理显示或者做缩略图预览。2.2 双线性性价比最高的大众之选双线性插值的基本原理是取输出像素映射到输入坐标后的周围2x2像素按水平和垂直方向的距离做两次线性加权。假设输入坐标为(u, v)整数部分为(i, j)小数部分为(dx, dy)那么输出像素值等于上方行P_top P(i, j) * (1 - dx) P(i1, j) * dx 下方行P_bottom P(i, j1) * (1 - dx) P(i1, j1) * dx 最终结果P_out P_top * (1 - dy) P_bottom * dy先用小数部分算一行内相邻像素的插值再用垂直方向做一次插值所以叫双线性。硬件实现需要缓存两行像素因为每次计算要同时用到当前行和下一行的数据。这套逻辑在FPGA里很顺乘法次数也不多对8位像素和8位小数权重来说一次输出像素大概需要4次乘法和若干次加法两三个DSP够用。画质方面双线性能有效消除最近邻那种强烈的锯齿感文字边缘虽然有点发虚但是整体观感可接受。我做过不少工程真正部署到产品里的还是双线性居多。原因也很简单画质好、资源不夸张、时序容易收敛。如果你的项目没有极端的画质要求选双线性基本不会错。2.3 双三次画质上限更高但资源翻着跟头涨双三次插值用4x4邻域总共16个像素按照距离权重加权计算。权重函数一般用三次多项式近似常用的有Catmull-Rom、B-spline等。它比双线性更平滑边缘保持能力也更好放大后图像更锐利但代价是硬件复杂度的暴涨。首先是行缓存从2条变成4条1920宽的RGB888就要多占约184Kbit的BRAM。其次乘法数量翻了好几倍每个像素要算16个权重乘法、大概15次加法还要处理权重函数本身的计算。如果直接用DSP硬算资源很容易到几百个DSP对很多中低端FPGA来说压力山大。有一种折中做法是把常用的权重预先算好存到ROM里比如把0~1的小数部分量化成256个等级每种等级对应4个权重这样一来现场就不用算权重函数了只做查表和乘加。这个方法我试过能把DSP消耗压下来不少代价是BRAM多占一点点时序也更好收敛。双三次适合用什么场景图像的静态内容多、画质要求高、比如医疗影像、工业检测、专业监视器。如果只是视频聊天窗口或者安防监控的预览画面有点奢侈了。2.4 更高阶方案方向参考再往上有Lanczos插值需要更大窗口8x8甚至更多以及基于边缘导向的自适应插值这些算法在FPGA上要么资源爆炸、要么控制逻辑复杂到让人头秃。常见做法其实是把缩放分成两阶段先做一次普通缩放再用锐化卷积核做增强视觉上能逼近一部分更贵算法的效果。这个思路在工程上很有价值后面做图像增强模块时可以展开聊。3. 硬件架构设计动手写代码前必须想清楚的3.1 行缓存Line Buffer的设计与BRAM规划不管选哪种插值算法行缓存都是避不开的基础结构。行缓存的作用是让当前输出像素能同时看到多行输入数据。双线性需要2行双三次需要4行。实际写代码时一般用FPGA厂商提供的Shift Register IP比如Xilinx的RAM-based Shift Register配置成多行缓存模式用起来非常方便。但直接RAM-based Shift Register有个坑它会用一整块BRAM去实现即使一行数据没占满一整块BRAM这块资源也被占了。比如720p的RGB565一行是720x16bit约11520bit而Xilinx 7系列的一块BRAM是36Kbit两个行缓存就要占两块BRAM利用率只有三分之一左右。所以规划BRAM的时候建议按一整块来估算而不是按实际位宽和深度来算。我自己习惯的做法是先写一个行缓存模块参数化行长度、行数、像素位宽例化时直接传参。行缓存输出需要和输入数据流严格对齐特别要注意每行结束和每帧结束时的清空行为否则会出现图像错行。3.2 缩放坐标生成小数坐标怎么算、边界怎么处理插值算法的核心输入是输出像素对应的输入坐标带小数。坐标计算公式很简单src_coord dst_coord * src_size / dst_size。但FPGA里做除法很费资源所以工程上通常用定点整数乘加或者递推的方式来避免除法。以x方向为例假设输入宽度src_w输出宽度dst_w放大倍数为dst_w/src_w。输出坐标每前进1输入坐标前进src_w/dst_w。把这个比值用定点数表示例如用16位定点8位整数、8位小数每次累加这个增量即可。因为增量恒定所以只需要一个加法器不需要乘法器坐标的整数部分拿来寻址小数部分拿来算权重。这样一个简单的递推坐标生成器三个时钟周期出一个坐标非常节省资源。边界处理也很关键。当输出坐标映射到输入坐标时可能落在图像边缘外部比如坐标为负或者大于宽度减1。常见做法是clamp即超出左边缘就取第一个像素超出右边缘就取最后一个像素也可以做镜像。工程上首推clamp逻辑简单且视觉上最自然。每行首尾各补一个像素即可让插值窗口在边界处不越界。3.3 带宽估算和时钟方案别等上板才发现跑不动图像缩放模块的带宽估算是架构阶段必须交的作业。举个例子输入1080p60RGB888像素时钟148.5MHz带宽约148.5M乘24bit3.564Gbps这个数据量对FPGA内部逻辑来说小意思但如果你接的是外部DDR帧缓存DDR的有效带宽会被其他模块抢占就可能出现反压。所以数据反压设计valid/ready握手机制在视频通路里几乎是标配。输出侧的时钟要单独看。同样是1080p放大到4K输出像素率是输入4倍。如果PLL能产生594MHz给缩放核心而且流水线每一级都能一个周期处理一个像素那逐像素输出没问题如果输出时钟不能到594MHz就得在缩放核心内部做2像素或者4像素并行处理俗称“多相”或者“多像素并行”架构。这个决策要提前做因为多像素并行会直接改变行缓存和坐标生成器的实现方式。关于时序收敛插值模块的组合逻辑主要集中在乘加树。1080p下用148MHz问题不大但是到了4K输出594MHz下任何长的组合逻辑路径都会让时序收敛很痛苦。我的经验是乘加树尽量拆成两到三级流水每一级之间插寄存器宁可增加一点点延迟也要保证时序跑过。如果设计中需要帧缓存比如输入和输出帧率不同步或者分辨率差异太大导致行缓存扛不住那么就要引入DDR3/DDR4控制器。这个场景下建议先在仿真阶段就把DDR3的仿真模型跑通测清楚读写带宽和延迟否则上板之后大概率是各种花屏和卡顿。4. 实操过程从软件模型到FPGA验证4.1 先写软件模型不要直接上RTL我一直强调一个习惯任何图像算法先在PC上用Python或者Matlab写一版浮点模型把输出结果存成图片肉眼确认效果没问题再量化到定点再转RTL。直接上手写Verilog等仿真出来发现图像大面积偏色或者错位你会完全不知道是算法错了还是RTL写错了。以双线性插值为例Python版本几十行就能写完。核心逻辑是遍历输出坐标计算对应的输入坐标和权重取2x2邻域做加权平均。跑完之后用OpenCV的resize函数做对比计算PSNR如果PSNR很高或者肉眼几乎无差异说明算法理解正确。这一步能帮你隔离问题域。软件模型是“参考金标准”RTL仿真的结果要和它对比而不是只凭眼睛看有没有“花”。我见过很多工程团队RTL仿真看到图片“大概对”就觉得没问题结果上板之后边缘有明显的彩色条纹回头查才发现权重计算少了一拍图像错位了。4.2 RTL实现要点参数、流水线、截位RTL实现时我一般把模块参数化parameter SRC_WIDTH 1920; parameter SRC_HEIGHT 1080; parameter DST_WIDTH 3840; parameter DST_HEIGHT 2160; parameter DATA_WIDTH 24; // RGB888 parameter INTERP_MODE 1; // 0: nearest, 1: bilinear, 2: bicubic这样在仿真时可以换成小的分辨率比如64x64放大到128x128跑得快调试也方便。权重计算用定点数常见做法是把坐标的小数部分量化成8bit对应权重精度1/256。8bit权重乘8bit像素结果是16bit累加之后再右移8位截位。截位时注意是有符号还是无符号RGB数据是无符号的但权重有正负双三次的权重可能为负这种情况下乘法结果要做有符号处理否则出来的数据完全不对。双线性插值的流水线我一般拆成这样几级第一级计算输入坐标的整部和分第二级从行缓存读取2x2邻域像素第三级计算水平方向的两次插值第四级计算垂直方向插值并输出。每级之间打一拍寄存器总延迟大概6到8个时钟周期对视频流来说可以忽略。坐标生成的递推加法器要特别注意累计误差。坐标增量是定点数不停地累加小数部分会截断时间长了坐标漂移。解决办法是用更长的定点位宽比如32位定点其中高16位是整数、低16位是小数这样漂移被压得很低。我实测过用16位定点跑4K输出坐标误差大约在0.001像素以内完全无感。4.3 仿真验证小图测试、PSNR对比、ILA上板调试仿真验证建议按这个顺序来第一步用纯RTL仿真跑一张小图比如32x32的渐变图放大到64x64把结果导出成BMP和Python模型对比。这一步能快速抓逻辑错误。第二步加大分辨率跑完整图同时统计PSNR。第三步上板验证用内部ILA抓关键节点的数据流。ILA调试有一个很实用的技巧把行缓存输出、坐标整数部分、权重这几个信号全部拉出来然后在ILA里用触发条件抓某个像素坐标的瞬间对着波形手动计算一下权重的值是否符合预期。这一步能确认RTL内部运算没问题而不仅仅是最终图像“看起来正常”。上板调试时如果出现花屏第一个怀疑对象不是插值模块而是时序没对齐。比如行缓存输出延迟和坐标生成延迟不一致导致拿到的邻域像素错行。这种情况用ILA抓几个相邻像素的坐标和像素值对比一下就能定位。5. 常见问题与排查技巧实录5.1 图像错位和花屏先查行缓存时序行缓存是最容易出问题的地方。典型现象是图像整体上下错位或者局部出现行乱序。原因通常是行缓存的读指针没有和输入的行同步信号对齐或者帧开头没有把缓存清干净。排查思路很固定先用ILA抓vsync、hsync、de数据有效、行缓存输出这几个信号对比每一行数据的有效时机。行缓存的输出必须比输入延迟固定的行数比如2行并且每一行开始和结束的节拍完全一致。用固定测试图卡比如彩条去跑如果彩条在输出端整齐地下移了两行那说明行缓存时序和预期一致如果彩条歪了逐行对就完了。5.2 乘法器多了导致时序违例双三次插值如果按最朴素的方式写乘法器数量和组合逻辑深度会瞬间爆炸。综合完以后时序报告一片红这在工程上叫组合逻辑路径太长。解决办法是拆流水线把16个乘法拆成4个一组每级之间插寄存器。我踩过一次坑做双三次插值时把权重计算和加权求和放在同一个周期结果1080p输出时148MHz都跑不到时序违例了十几纳秒。后来把权重查表和加权求和拆成两级流水立刻收敛到200MHz以上。所以时序违例先别急着换芯片拆流水线往往是成本最低的解决方案。5.3 边缘发虚和“抽丝”感不一定是算法问题图像放大后边缘发虚这个要分情况。如果是双线性边缘本来就比原图软这是正常的但如果出现明显“抽丝”或者条纹状纹理多半不是插值本身而是前级的视频信号质量问题比如Sensor的RAW数据没有做坏点校正或者去马赛克模块引入了伪彩色。遇到这种情况我的习惯是先做“绕过测试”——把插值模块旁路原分辨率直接显示看画面是否正常。如果原图也发虚说明问题在更前级如果原图正常、放大后才出现再回头查插值模块的权重和数据位宽。这一步能快速缩小排查范围避免在错误的方向上浪费时间。5.4 资源占用超出预期记得看实现方式有时候同样的算法不同人写出来资源差好几倍。问题往往出在乘法器实现方式上。FPGA里的乘法器有DSP硬核和LUT逻辑实现两种方式如果你用了非常规位宽的乘法综合器可能不使用DSP而改用LUTLUT消耗就会暴涨。我的建议是核心乘法尽量用DSP让位宽保持8bit乘8bit、16bit乘16bit这类常见宽度如果位宽太奇怪比如9bit乘7bit综合器大概率会用LUT兜底。另外能查表就查表双三次插值的权重预先算好存ROM能省掉一大片组合逻辑。6. 后续章节内容预告与个人体会这一节只是把图像缩放和插值处理的全貌画了个框架接下来的内容会更偏编码和调试。双线性插值的完整RTL实现、行缓存模块的通用写法、坐标生成器的定点精度分析、以及如何把缩放模块接入HDMI显示通路这些都会逐步展开。每块内容我都会附上完整的仿真脚本和上板验证步骤方便直接照做。做图像缩放这个东西真正难的不是算法本身而是数据在流水线里每一级的状态一致性。坐标差一点、权重差一拍、行缓存边界多一个像素最终画面上的表现就是歪的、花的、糊的。所以每次写完代码我都喜欢先跑一遍小图仿真再用肉眼PSNR双重确认最后才放心上板。如果只是做入门学习建议先拿最近邻跑通全链路再换双线性体会一下资源占用和画质变化最后再挑战双三次。这条路径最稳也最容易建立感觉。等行缓存、坐标生成、流水线这些基本功扎实了后面再做缩放之外的图像算法你会发现很多套路是相通的。
返回列表