一、前言
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和5
int NORM_TYPE, //范数类型,支持L1范数和L2范数
int SRC_T, //输入图像类型,仅支持8UC1
int DST_T, //输出图像类型,仅支持2UC1
int ROWS, //最大图像行数
int COLS, //最大图像列数
int NPC, //输入图像的单个时钟处理像素数
int NPC1, //输出图像的单个时钟处理像素数
bool USE_URAM=false, //是否使用URAM
int 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, //是否使用URAM
int 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 on
int 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 DATAFLOW
xf::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 on
int 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中综合编译即可。)