一、前言
canny边缘检测主要用于提取图像的边缘,是最常用且有效的边缘检测算法。在AMD赛灵思提供的库函数中,使用xf::cv::Canny和xf::cv::EdgeTracing两个函数实现canny边缘提取。本文举例说明如何在vitis HLS 2023.1中实现canny算法。
二、xf::cv::Canny和xf::cv::EdgeTracing函数解析
首先看一下这两个函数的调用接口:
1、xf::cv::Canny函数
template<int FILTER_TYPE, //sobel滤波宽度,仅支持3和5int NORM_TYPE, //范数类型,支持L1范数和L2范数int SRC_T, //输入图像类型,仅支持8UC1int DST_T, //输出图像类型,仅支持2UC1int ROWS, //最大图像行数int COLS, //最大图像列数int NPC, //输入图像的单个时钟处理像素数int NPC1, //输出图像的单个时钟处理像素数bool USE_URAM=false, //是否使用URAMint XFCVDEPTH_IN_1 = _XFCVDEPTH_DEFAULT, //输入图像深度int XFCVDEPTH_OUT_1 = _XFCVDEPTH_DEFAULT> //输出图像深度
void Canny(xf::cv::Mat<SRC_T, ROWS, COLS, NPC, XFCVDEPTH_IN_1> &_src_mat, //输入图像矩阵xf::cv::Mat<DST_T, ROWS, COLS, NPC1, XFCVDEPTH_OUT_1> & _dst_mat, //输出图像矩阵unsigned char _lowthreshold, //边缘提取低阈值unsigned char _highthreshold) //边缘提取高阈值
在xf::cv::Canny算法中,首先通过3*3的高斯噪声滤波器对图像进行滤波;此后使用Sobel梯度函数计算沿着x和y方向的梯度,用以计算像素的幅度和相位;然后使用最大值抑制算法,得到对应的边缘点进行输出,输出结果的单个像素的位宽为2bits,,然后经过打包输出。
xf::cv::Canny函数输出的图像像素2bits,含义表示如下:
00-表示背景
01-表示弱边缘
11-表示强边缘
2、xf::cv::EdgeTracing函数
template<int SRC_T, //输入图像类型int DST_T, //输出图像类型int ROWS, //图像最大行数int COLS, //图像最大列数int NPC_SRC,//输入图像的NPPC,每个时钟处理像素数int NPC_DST,//输出图像的NPPC,每个时钟处理像素数bool USE_URAM=false, //是否使用URAMint depthm = -1> //图像深度
void EdgeTracing(xf::cv::Mat<SRC_T, ROWS, COLS, NPC_SRC, depthm> & _src,//输入图像矩阵xf::cv::Mat<DST_T, ROWS, COLS, NPC_DST, depthm> & _dst) //输出图像矩阵
xf::cv::EdgeTracing函数主要用于处理canny算法,将离散的强边缘和弱边缘进行边缘跟踪,将离散的边缘点串联起来,最终将2UC1的图像输出为一个8UC1的图像。
对于此函数需要特别注意,其无法实现数据的DATAFLOW,只能采取内存映射读取的方式进行读写访问。并且在综合的时候,需要在cflag总添加编译指令"-D__SDA_MEM_MAP__",否则综合时会报错。具体可以参考后面的示例。
关于其余详细信息,可以参考:xilinx.github.io/Vitis_Libraries/vision/2022.1/api-reference.html#canny-edge-detection
三、vitis HLS canny算法中的具体代码实现
这部分的代码实现不难,在赛灵思提供的示例程序中就有现成的参考示例,不过是在L2文件夹下,主要是vitis下的实现demo。不过稍微进行更改,就可以在vitisHLS中成功完成综合和联合仿真了。
若想要查看赛灵思提供的参考示例,请访问Xilinx/Vitis_Libraries: Vitis Libraries (github.com)。
下面主要描述在vitisHLS中是如何完成vitisHLS代码的。
1、首先提供一下头文件define.h代码
#include "ap_int.h"
#include "common/xf_common.hpp"
#include "common/xf_utility.hpp"
#include "hls_stream.h"
#include "imgproc/xf_canny.hpp"
#include "imgproc/xf_edge_tracing.hpp"#define FILTER_WIDTH 3#define NORM_TYPE XF_L1NORM#define XF_USE_URAM false#define IMAGE_PTR_WIDTH 64#define WIDTH 512
#define HEIGHT 512#define THRES_LOW 120 //边缘提取低阈值
#define THRES_HIGH 180 边缘提取高阈值void canny_accel(ap_uint<IMAGE_PTR_WIDTH>* img_inp,ap_uint<IMAGE_PTR_WIDTH>* img_out,int rows,int cols,int low_threshold,int high_threshold) ;void edgetracing_accel(ap_uint<IMAGE_PTR_WIDTH>* img_inp,ap_uint<IMAGE_PTR_WIDTH>* img_out,int rows,int cols);
2、xf::cv::Canny的代码示例
#include "define.h"static constexpr int _XF_DEPTH_I = (HEIGHT * WIDTH * (XF_PIXELWIDTH(XF_8UC1, XF_NPPC8))) / (IMAGE_PTR_WIDTH);
static constexpr int _XF_DEPTH_O = (HEIGHT * WIDTH * (XF_PIXELWIDTH(XF_2UC1, XF_NPPC32))) / (IMAGE_PTR_WIDTH);void canny_accel(ap_uint<IMAGE_PTR_WIDTH>* img_inp,ap_uint<IMAGE_PTR_WIDTH>* img_out,int rows,int cols,int low_threshold,int high_threshold) {
// clang-format off#pragma HLS INTERFACE m_axi port=img_inp depth=_XF_DEPTH_I bundle=gmem1#pragma HLS INTERFACE m_axi port=img_out depth=_XF_DEPTH_O bundle=gmem2// clang-format on// clang-format off#pragma HLS INTERFACE s_axilite port=rows #pragma HLS INTERFACE s_axilite port=cols #pragma HLS INTERFACE s_axilite port=low_threshold #pragma HLS INTERFACE s_axilite port=high_threshold #pragma HLS INTERFACE s_axilite port=return// clang-format onint npcCols = cols;int divNum = (int)(cols / 32);int npcColsNxt = (divNum + 1) * 32;if (cols % 32 != 0) {npcCols = npcColsNxt;}//printf("actual number of cols is %d \n", npcCols);xf::cv::Mat<XF_8UC1, HEIGHT, WIDTH, XF_NPPC8> in_mat(rows, cols);xf::cv::Mat<XF_2UC1, HEIGHT, WIDTH, XF_NPPC32> dst_mat(rows, npcCols);#pragma HLS DATAFLOWxf::cv::Array2xfMat<IMAGE_PTR_WIDTH, XF_8UC1, HEIGHT, WIDTH, XF_NPPC8>(img_inp, in_mat);xf::cv::Canny<FILTER_WIDTH, NORM_TYPE, XF_8UC1, XF_2UC1, HEIGHT, WIDTH, XF_NPPC8, XF_NPPC32, XF_USE_URAM>(in_mat, dst_mat, low_threshold, high_threshold);xf::cv::xfMat2Array<IMAGE_PTR_WIDTH, XF_2UC1, HEIGHT, WIDTH, XF_NPPC32>(dst_mat, img_out);
}
此部分代码注意几点如下:
1、#pragma HLS DATAFLOW 实现数据的流水线处理。
2、输出图像为XF_2UC1格式,无法直接在opencv中显示。
3、图像列数需要为32的整数倍
4、CFLAG正常设置即可,我这边设置的是 "-D__SDSVHLS__ -IE:/vitis_hls_image/vitis_hls_tutorial/include -std=c++0x -O3"
3、xf::cv::EdgeTracing的代码示例
#include "define.h"static constexpr int _XF_DEPTH_I = (HEIGHT * WIDTH * (XF_PIXELWIDTH(XF_2UC1, XF_NPPC32))) / (IMAGE_PTR_WIDTH);
static constexpr int _XF_DEPTH_O = (HEIGHT * WIDTH * (XF_PIXELWIDTH(XF_8UC1, XF_NPPC8))) / (IMAGE_PTR_WIDTH);void edgetracing_accel(ap_uint<IMAGE_PTR_WIDTH>* img_inp,ap_uint<IMAGE_PTR_WIDTH>* img_out,int rows,int cols) {
// clang-format off#pragma HLS INTERFACE m_axi port=img_inp depth=_XF_DEPTH_I bundle=gmem3#pragma HLS INTERFACE m_axi port=img_out depth=_XF_DEPTH_O bundle=gmem4
// clang-format on// clang-format off#pragma HLS INTERFACE s_axilite port=rows #pragma HLS INTERFACE s_axilite port=cols #pragma HLS INTERFACE s_axilite port=return// clang-format onint npcCols = cols;int divNum = (int)(cols / 32);int npcColsNxt = (divNum + 1) * 32;if (cols % 32 != 0) {npcCols = npcColsNxt;}int npcCols_8 = cols;int divNum_8 = (int)(cols / 8);int npcColsNxt_8 = (divNum_8 + 1) * 8;if (cols % 8 != 0) {npcCols_8 = npcColsNxt_8;}// printf("actual number of cols is %d \n", npcCols);// printf("actual number of cols is multiple 8 :%d \n", npcCols_8);// printf("\nbefore allocate\n");xf::cv::Mat<XF_2UC1, HEIGHT, WIDTH, XF_NPPC32> _dst1(rows, npcCols, img_inp);xf::cv::Mat<XF_8UC1, HEIGHT, WIDTH, XF_NPPC8> _dst2(rows, npcCols_8, img_out);// printf("\nbefore kernel call\n");xf::cv::EdgeTracing<XF_2UC1, XF_8UC1, HEIGHT, WIDTH, XF_NPPC32, XF_NPPC8, XF_USE_URAM>(_dst1, _dst2);// printf("\nafter kernel call\n");
}
此部分代码注意几点如下:
1、不能添加指令:#pragma HLS DATAFLOW 。否则综合会报错
2、图像列数需要为32的整数倍
3、CFLAG设置需要额外注意添加__SDA_MEM_MAP指令,否则综合报错。我这边设置的是 "-D__SDSVHLS__ -D__SDA_MEM_MAP__ -IE:/vitis_hls_image/vitis_hls_tutorial/include -std=c++0x -O3"
4、综合注意事项
我刚开始编译的时候,总以为可以将xf::cv::Canny和xf::cv::EdgeTracing两个函数综合到1个IP核里,但是最终我失败了。这两个函数,一个是DATAFLOW形式,一个是__SDA_MEM_MAP__的编译方式,是无法在一个IP核中编译成功的,需要将2个函数分别编译成一个IP,在vivado中按照下图的方式相连。
5、testbench的代码示例
#include <iostream>
#include <stdio.h>
#include <opencv2/opencv.hpp>
#include <opencv/cv.h>
#include <opencv2/highgui.hpp>
#include <opencv/cxcore.h>
#include <opencv2/imgproc.hpp>#include "define.h"
#include "common/xf_sw_utils.hpp"int main(int argc, char* argv[])
{//------------1、读取图像转化为灰度图像---------------------------printf("argc == %d \n",argc);if (argc != 2) {printf("input error: PLEASE INPUT IMAGE PATH 1\n");return 1;}//opencv canny边缘处理cv::Mat img_in; //输入图像img_in = cv::imread(argv[1],cv::IMREAD_GRAYSCALE);//按照GRAY图读取图像cv::imwrite("opencv读取的图像.png",img_in);//显示//-----直接进行canny处理cv::Mat image_canny_only(img_in.rows, img_in.cols, img_in.type());cv::Canny(img_in,image_canny_only,THRES_LOW,THRES_HIGH);cv::imwrite("openCV处理后图像-无高斯滤波.png",image_canny_only);////----预先进行高斯滤波处理cv::Mat image_gaus(img_in.rows, img_in.cols, img_in.type());cv::Mat image_canny(img_in.rows, img_in.cols, img_in.type());//实际上HLS处理中,首先进行了高斯滤波,因此在opencv中也加入高斯滤波cv::GaussianBlur( img_in, image_gaus, cv::Size(3,3),0, 0,cv::BORDER_CONSTANT);cv::Canny(image_gaus,image_canny,THRES_LOW,THRES_HIGH);cv::imwrite("openCV处理后图像-高斯滤波.png",image_canny);////2、HLS canny边缘处理---------------------------xf::cv::Mat<XF_8UC1,HEIGHT,WIDTH,XF_NPPC8> image_in; //输入图像image_in = xf::cv::imread<XF_8UC1,HEIGHT,WIDTH,XF_NPPC8>(argv[1],cv::IMREAD_GRAYSCALE);//按照GRAY图读取图像xf::cv::imwrite<XF_8UC1,HEIGHT,WIDTH,XF_NPPC8>("HLS读取的图像.png",image_in);//显示xf::cv::Mat<XF_2UC1,HEIGHT,WIDTH,XF_NPPC32>hls_image_canny;canny_accel( (ap_uint<IMAGE_PTR_WIDTH>*) image_in.data,(ap_uint<IMAGE_PTR_WIDTH>*) hls_image_canny.data,512,512,THRES_LOW,THRES_HIGH);xf::cv::Mat<XF_8UC1,HEIGHT,WIDTH,XF_NPPC8> hls_image_edge;edgetracing_accel( (ap_uint<IMAGE_PTR_WIDTH>*) hls_image_canny.data,(ap_uint<IMAGE_PTR_WIDTH>*) hls_image_edge.data,HEIGHT,WIDTH);xf::cv::imwrite<XF_8UC1,HEIGHT,WIDTH,XF_NPPC8>("HLS处理后图像.png",hls_image_edge);//cv::waitKey(0);/// 等待用户按任意按键退出程序return 0;
}
对输入测试激励tina.png,原图、opencv处理结果(无高斯滤波),opencv处理(有高斯滤波),HLS处理结果分别如下:
原图 | |
opencv处理结果(无高斯滤波) | |
opencv处理(有高斯滤波) | |
HLS处理 |
分析上面的结果,可以看到HLS处理的canny图像边缘,由于包含了高斯滤波,所以与opencv处理(有高斯滤波)的处理结果最接近,且结果基本正确。
实测,该函数综合、联合仿真结果均正确。
四、完整的vitisHLS示例工程
有兴趣的可以参考完整的示例代码:vitis-HLScanny算法实现图像边缘检测资源-CSDN文库
(我偷懒了点,将xf::cv::Canny和xf::cv::EdgeTracing放在一个工程的两个函数中。在实际使用时,需要分别将xf::cv::Canny和xf::cv::EdgeTracing对应的函数设置为顶层函数,分别进行综合、导出RTL文件,这样就可以得到2个IP了,把这两个IP都添加到vivado中综合编译即可。)