书接上文,趁着今天休假,采用SpinalHDL做一个小的demo,看看在SpinalHDL里如何优雅的实现Sobel边缘检测。
Sobel边缘检测
Sobel边缘检测原理教材网上一大堆,核心为卷积处理。
Sobel卷积因子为:
该算子包含两组3x3的矩阵,分别为横向及纵向,将之与图像作平面卷积,即可分别得出横向及纵向的亮度差分近似值。如果以A代表原始图像,Gx及Gy分别代表经横向及纵向边缘检测的图像灰度值,其公式如下:
图像的每一个像素的横向及纵向灰度值通过以下公式结合,来计算该点灰度的大小:
通常,为了提高效率使用不开平方的近似值:
最后,当计算出来的值大于某一阈值时即认为为边缘像素点。
归结起来,Sobel边缘检测分为三大步:卷积计算、灰度计算、阈值比较处理。结合上文实现的bufWindow,在SpinalHDL里实现Sobel边缘检测也就几行代码的事情(如果是写Verilog我还是拒绝的)。
卷积计算
通过bufWindow,我们可以得到一个3x3的矩阵窗口,拿到结果第一步即是计算卷积,由于卷积因子是带符号的,而在做卷积时又需要考虑位宽扩展的事情,在写Verilog时还是需要小心的设计下的,而在SpinalHDL里,两行代码:
val Gx=(windowbuf.io.dataOut.payload(0)(2).expand.asSInt-^windowbuf.io.dataOut.payload(0)(0).expand.asSInt)+|
((windowbuf.io.dataOut.payload(1)(2).expand.asSInt-^windowbuf.io.dataOut.payload(1)(0).expand.asSInt)<<1)+|
(windowbuf.io.dataOut.payload(2)(2).expand.asSInt-^windowbuf.io.dataOut.payload(2)(0).expand.asSInt)
val Gy=(windowbuf.io.dataOut.payload(0)(0).expand.asSInt-^windowbuf.io.dataOut.payload(2)(0).expand.asSInt)+|
((windowbuf.io.dataOut.payload(0)(1).expand.asSInt-^windowbuf.io.dataOut.payload(2)(1).expand.asSInt)<<1)+|
(windowbuf.io.dataOut.payload(0)(2).expand.asSInt-^windowbuf.io.dataOut.payload(2)(2).expand.asSInt)
首先将bufWindow输出的窗口矩阵值扩展一位位宽转换为有符号值,然后进行计算卷积。计算卷积运用了两个运算符“-^”,"+|"来处理加减运算时的位宽处理(可参照SpinalHDL手册或本公众号的《SpinalHDL—数据类型:UInt/SIn》)。最终得到Gx、Gy。
灰度计算
灰度计算这里采用近似值,通过取绝对值的方式进行实现,在SpinalHDL里也就一行代码的事情:
sobelResult.payload:= (sobelConv.payload(0).abs+| sobelConv.payload(1).abs).fixTo(cfg.dataWidth-1 downto 0,RoundType.ROUNDUP)
由于在卷积计算时有扩展位宽,这里计算最后调用fixTo进行高位饱和处理。最终得到位宽与输入保持一致(想想你在Veirlog里实现这一步要做多少事情,少年😎)。
阈值比较
阈值比较就很简单了,比较两个值大小取两个极端:
when(sobelResult.payload>io.thresholdValue){
io.dataOut.payload:=(default->true)
}otherwise{
io.dataOut.payload:=(default->false)
}
最终实现Sobel边缘检测代码如下:
case class sobelProc(cfg:lineBufferCfg) extends Component{
require(cfg.lineNum==3)
val io=new Bundle{
val thresholdValue =in UInt(cfg.dataWidth bits)
val dataIn=slave Flow(UInt(cfg.dataWidth bits))
val dataOut=master Flow(UInt(cfg.dataWidth bits))
dataOut.valid.setAsReg().init(False)
dataOut.payload.setAsReg().init(0)
}
noIoPrefix()
val sobel=new Area{
val windowbuf=bufWindow(cfg)
val sobelConv=Reg(Flow(Vec(SInt(),2)))
val sobelResult=Reg(Flow(UInt(cfg.dataWidth bits)))
sobelConv.valid.init(False)
sobelResult.valid.init(False)
io.dataIn<>windowbuf.io.dataIn
val Gx=(windowbuf.io.dataOut.payload(0)(2).expand.asSInt-^windowbuf.io.dataOut.payload(0)(0).expand.asSInt)+|
((windowbuf.io.dataOut.payload(1)(2).expand.asSInt-^windowbuf.io.dataOut.payload(1)(0).expand.asSInt)<<1)+|
(windowbuf.io.dataOut.payload(2)(2).expand.asSInt-^windowbuf.io.dataOut.payload(2)(0).expand.asSInt)
val Gy=(windowbuf.io.dataOut.payload(0)(0).expand.asSInt-^windowbuf.io.dataOut.payload(2)(0).expand.asSInt)+|
((windowbuf.io.dataOut.payload(0)(1).expand.asSInt-^windowbuf.io.dataOut.payload(2)(1).expand.asSInt)<<1)+|
(windowbuf.io.dataOut.payload(0)(2).expand.asSInt-^windowbuf.io.dataOut.payload(2)(2).expand.asSInt)
sobelConv.valid:=windowbuf.io.dataOut.valid
sobelConv.payload(0):=Gx
sobelConv.payload(1):=Gy
sobelResult.valid:=sobelConv.valid
sobelResult.payload:= (sobelConv.payload(0).abs+| sobelConv.payload(1).abs).fixTo(cfg.dataWidth-1 downto 0,RoundType.ROUNDUP)
io.dataOut.valid:=sobelResult.valid
when(sobelResult.payload>io.thresholdValue){
io.dataOut.payload:=(default->true)
}otherwise{
io.dataOut.payload:=(default->false)
}
}
}
区区不到四十行代码,简洁而优雅,基本上就是描述算法,出错概率应该很小吧!
仿真
做图像处理的小伙伴想想在做仿真验证时需要怎么搞,matlab生成灰度图像二进制数据放在文件里,然后仿真时再导入,仿真完成后将结果保存到文件里,最后再在matlab里做对比。
太麻烦。SpinalHDL提供了仿真支持,而SpinalHDL是基于Scala的,可以完美实现整个仿真验证流程:从图片直接获取数据,然后进行仿真验证,仿真结果直接再次生成图片。这里仿真在SpinalHDL里进行完成,从网上拉取一张图片:
仿真里调用javax.imageio.ImageIO等库读取图像,转换成灰度图:
随后将数据灌入到测试单元里,得到的数据流直接生成图片:
END
作者:玉骐
原文链接:https://mp.weixin.qq.com/s/wLMMuWre63potK5JCzZ6CA
微信公众号:
推荐阅读
更多SpinalHDL技术干货请关注Spinal FPGA专栏。