本例利用之前介绍的基础函数进行功能级实现。
void clahe_accl(hls::stream<T_AXIU(IMAGE_BPP, IMAGE_NPPC)>& src, hls::stream<T_AXIU(IMAGE_BPP, IMAGE_NPPC)>& dst, u16 width, u16 height, u32 limit_th, u32 scale_th, u8 mode)
{
#pragma HLS INTERFACE axis register both port=src
#pragma HLS INTERFACE axis register both port=dst
#pragma HLS INTERFACE s_axilite port=return bundle=CONTROL_BUS
#pragma HLS INTERFACE s_axilite port=width bundle=CONTROL_BUS
#pragma HLS INTERFACE s_axilite port=height bundle=CONTROL_BUS
#pragma HLS INTERFACE s_axilite port=limit_th bundle=CONTROL_BUS
#pragma HLS INTERFACE s_axilite port=scale_th bundle=CONTROL_BUS
#pragma HLS INTERFACE s_axilite port=mode bundle=CONTROL_BUS
#pragma HLS INTERFACE ap_stable port=width
#pragma HLS INTERFACE ap_stable port=height
#pragma HLS INTERFACE ap_stable port=limit_th
#pragma HLS INTERFACE ap_stable port=scale_th
#pragma HLS INTERFACE ap_stable port=mode
HistLutMap<WIN_W, WIN_H> hist;
static HistLutMap<WIN_W, WIN_H> hist_last;
clahe_compute(src, dst, hist, hist_last, width, height, limit_th, scale_th, mode);
for (u16 i = 0; i < WIN_H; i++)
{
for (u16 j = 0; j < WIN_W; j++)
{
for (u16 k = 0; k < 256; k++)
{
hist_last.data_[i][j][k] = hist.data_[i][j][k];
}
}
}
}
本函数,利用算法级函数clahe_compute进行数据计算,并输出计算结果,
然后,用一个三层嵌套for循环体,进行逐点处理。
将hist中的数据,逐点拷贝到hist2中。
这里,定义的hist2,是一个static局部对象,
所以,它的作用,是记录last_result。
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++
来看看clahe_compute函数。
void clahe_compute(hls::stream<T_AXIU(IMAGE_BPP, IMAGE_NPPC)>& src, hls::stream<T_AXIU(IMAGE_BPP, IMAGE_NPPC)>& dst, HistLutMap<WIN_W, WIN_H>& hist, HistLutMap<WIN_W, WIN_H>& hist2, u16 width, u16 height, u32 limit_th, u32 scale_th, u8 mode)
{
xf::Mat<IMAGE_TYPE, IMAGE_HEIGHT, IMAGE_WIDTH, IMAGE_NPPC> img0(height, width);
xf::Mat<IMAGE_TYPE, IMAGE_HEIGHT, IMAGE_WIDTH, IMAGE_NPPC> img1(height, width);
xf::Mat<XF_8UC1, IMAGE_HEIGHT, IMAGE_WIDTH, IMAGE_NPPC> img2(height, width);
xf::Mat<XF_8UC1, IMAGE_HEIGHT, IMAGE_WIDTH, IMAGE_NPPC> img3(height, width);
xf::Mat<XF_8UC1, IMAGE_HEIGHT, IMAGE_WIDTH, IMAGE_NPPC> img4(height, width);
xf::Mat<XF_8UC1, IMAGE_HEIGHT, IMAGE_WIDTH, IMAGE_NPPC> img4_0(height, width);
xf::Mat<XF_8UC1, IMAGE_HEIGHT, IMAGE_WIDTH, IMAGE_NPPC> img4_1(height, width);
xf::Mat<XF_8UC1, IMAGE_HEIGHT, IMAGE_WIDTH, IMAGE_NPPC> img4_resize(height, width/2);
xf::Mat<XF_8UC1, IMAGE_HEIGHT, IMAGE_WIDTH, IMAGE_NPPC> img4_blur(height, width);
xf::Mat<IMAGE_TYPE, IMAGE_HEIGHT, IMAGE_WIDTH, IMAGE_NPPC> img5(height, width);
xf::Mat<IMAGE_TYPE, IMAGE_HEIGHT, IMAGE_WIDTH, IMAGE_NPPC> img6(height, width);
#pragma HLS stream variable=img0.data dim=1 depth=1024
#pragma HLS stream variable=img1.data dim=1 depth=64
#pragma HLS stream variable=img2.data dim=1 depth=4096
#pragma HLS stream variable=img3.data dim=1 depth=4096
#pragma HLS stream variable=img4.data dim=1 depth=64
#pragma HLS stream variable=img4_0.data dim=1 depth=64
#pragma HLS stream variable=img4_1.data dim=1 depth=64
#pragma HLS stream variable=img4_resize.data dim=1 depth=64
#pragma HLS stream variable=img4_blur.data dim=1 depth=64
#pragma HLS stream variable=img5.data dim=1 depth=64
#pragma HLS stream variable=img6.data dim=1 depth=1024
#pragma HLS dataflow
xf::AXIvideo2xfMat(src, img0);
xf::rgb2hsv<IMAGE_TYPE, IMAGE_HEIGHT, IMAGE_WIDTH, IMAGE_NPPC>(img0, img1);
xfMat_split<IMAGE_TYPE, IMAGE_BPP, IMAGE_HEIGHT, IMAGE_WIDTH, IMAGE_NPPC, XF_8UC1, 8>(img1, img2, img3, img4);
xfMat_duplicate(img4, img4_0, img4_1);
xfMat_skip_pixel<XF_8UC1, 8, IMAGE_HEIGHT, IMAGE_WIDTH, IMAGE_NPPC>(img4_0, img4_resize);
calc_block_hist<XF_8UC1, 8, IMAGE_HEIGHT, IMAGE_WIDTH, IMAGE_NPPC, WIN_W, WIN_H>(img4_resize, hist, limit_th, scale_th);
clahe_lut<XF_8UC1, 8, IMAGE_HEIGHT, IMAGE_WIDTH, IMAGE_NPPC, WIN_W, WIN_H>(img4_1, img4_blur, hist2, mode);
xfMat_merge<XF_8UC1, 8, IMAGE_HEIGHT, IMAGE_WIDTH, IMAGE_NPPC, IMAGE_TYPE, IMAGE_BPP>(img2, img3, img4_blur, img5);
xf::hsv2rgb<IMAGE_TYPE, IMAGE_TYPE, IMAGE_HEIGHT, IMAGE_WIDTH, IMAGE_NPPC>(img5, img6);
xf::xfMat2AXIvideo(img6, dst);
}
其中用到了xfopencv提供的一些函数,也用到了自定义的公共函数。
首先,调用xf::AXIvideo2xfMat,将输入的AXIS数据流,转换成xfMat,
然后,调用xf::rgb2hsv,将RGB转换成HSV色彩空间,
然后,调用 xfMat_split,将xfMat分割成H,S,V三个xfMat,成为灰度图像,
然后,调用xfMat_duplicat,将xfmat复制成两个。
然后,调用xfMat_skip_pixel,进行抽点处理,缩小图像尺寸,
然后,调用calc_block_hist,计算直方图,
然后,调用clahe_lut,计算LUT映射关系,进行灰度重建。
然后,调用xfMat_merge,将重建后的三个灰度图合并,
然后,调用xf::hsv2rgb,将HSV色彩空间转换成RGB空间,
然后,调用xf::xfMat2AXIvideo,将xfMat转换成AXIS数据流。
这其中,涉及到两个算法级函数,calc_block_hist,clahe_lut,
+++++++++++++++++++++++++++++++++++++++++++++++++++++
先来看看calc_block_hist
首先,是封装了一个struct,在C++中,推荐封装为class。
template<int W, int H>
class HistLutMap
{
public:
u8 data_[H][W][256];
};
这是一个三维的数组,每个像素,都对应一个size为256的一维数组,每个单元,存储一个直方概率值。
calc_block_hist中需要用到这个struct。
template<int TYPE, int BPP, int ROWS, int COLS, int NPPC, int W, int H>
void calc_block_hist(xf::Mat<TYPE, ROWS, COLS, NPPC>& src, HistLutMap<W, H>& lut_map, u32 limit_th, u32 scale_th)
{
u16 width = src.cols;
u16 height = src.rows;
u16 bw = width / W;//计算一个block的column尺寸
u16 bh = height / H;//计算一个block的row尺寸
u32 throw_pixels = bw * bh / 8;//计算一个block的总像素个数,并计算出丢弃阈值,
ap_fixed<32,16> bw_div = div_to_mux(W, width); //求bw的倒数
ap_fixed<32,16> bh_div = div_to_mux(H, height); //求bh的倒数
#pragma HLS ALLOCATION instances=div_to_mux limit=1 function
syslog("width=%d, height=%d, bw=%d, bh=%d\n", width, height, bw, bh);
#if DEBUG
std::cout << "bw_div: " << bw_div << std::endl;
std::cout << "bh_div: " << bh_div << std::endl;
#endif
u32 hist[W][256];
u32 sum_hist[256];
u32 src_idx = 0;
//最外层循环体,按row block处理。合并到H行内。 整个循环体,处理一个合并后的直方图的一行。
for (u16 i = 0; i < H; i++)
{
#pragma HLS loop_tripcount avg = H max = H
//按列逐点处理,初始化hist。
for (u16 j = 0; j < W; j++)
{
#pragma HLS loop_tripcount avg = W max = W
for (u16 k = 0; k < 256; k++)
{
#pragma HLS pipeline II = 1
#pragma HLS loop_flatten off
hist[j][k] = 0;
}
}
//一个row block内,按行逐行处理,合并到一行内。
for (u16 m = 0; m < bh; m++)
{
#pragma HLS loop_tripcount avg = ROWS/H max = ROWS/H
//一行内,按列逐点计算直方统计值,合并到W个点内。
for (u16 j = 0; j < W; j++)
{
#pragma HLS loop_tripcount avg = W max = W
//一个column block内,按列逐点读取src,并更新统计值。合并到一个点内。
for (u16 n = 0; n < bw; n++)
{
#pragma HLS loop_tripcount avg = COLS/W max = COLS/W
#pragma HLS pipeline II = 2
#pragma HLS loop_flatten off
u8 srcpixel = src.read(src_idx++);
hist[j][srcpixel]++;
}
}
}
//按列逐点处理,对合并后的W宽的直方图进行处理,
for (u16 j = 0; j < W; j++)
{
#pragma HLS loop_tripcount avg = W max = W
u32 sum_low = 0, sum_high = 0, k_low = 0, k_high = 255;
//对当前点,按值逐值处理,计算左区间累加和,及右区间累加和,
for (u16 k = 0; k < 256; k++)
{
// 如果之前的左区间累加和,小于丢弃阈值,则继续累加,否则跳过,
if (sum_low < throw_pixels) {
sum_low += hist[j][k];
k_low = k;
}
// 如果之前的右区间累加和,小于丢弃阈值,则继续累加,否则跳过,
if (sum_high < throw_pixels) {
sum_high += hist[j][255 - k];
k_high = 255 - k;
}
}
//计算limit值
u32 limit = limit_th + limit_th * (k_high - k_low) * scale_th / 256;
u32 steal = 0;
//对当前点,按值逐值处理,
for (u16 k = 0; k < 256; k++)
{
#pragma HLS pipeline II = 1
#pragma HLS loop_flatten off
// 如果当前直方统计值,高于limit限值,则进行限幅处理,并将差值累加到折损值steal中。
if (hist[j][k] > limit)
{
steal += (hist[j][k] - limit);
hist[j][k] = limit;
}
}
//将折损值均分到每个值
u32 bonus = steal / 256;
// 对当前点,按值逐值处理
for (u16 k = 0; k < 256; k++)
{
#pragma HLS pipeline II = 1
#pragma HLS loop_flatten off
// 将均分的折损值,叠加到每个值的直方统计值中。
hist[j][k] += bonus;
}
// 对当前点,按值逐值处理,计算各个值的左区间累加和
for (u16 k = 0; k < 256; k++)
{
#pragma HLS pipeline II = 2
#pragma HLS loop_flatten off
// 对当前值,计算左区间累加和,
if( k == 0) {
sum_hist[k] = hist[j][k];
}
else {
sum_hist[k] = sum_hist[k - 1] + hist[j][k];
}
}
// 对当前点,按值逐值处理,
for (u16 k = 0; k < 256; k++)
{
#pragma HLS pipeline II = 1
#pragma HLS loop_flatten off
// 利用计算的各个值的左区间累加和,计算各个值的左区间累加概率,并乘以最大值MAX_VALUE,
// 从而求出当前点的LUT映射表。
//LUT表用于将原值转换成均衡后的值。
ap_fixed<64,48> sum = sum_hist[k];
sum *= 255;
sum *= bw_div;
sum *= bh_div;
//限幅保护
if (sum > 255)
sum = 255;
//对当前点,当前值,将计算出的均衡值,写入LUT的对应位置
lut_map.data_[i][j][k] = sum;
}
}
}
}
++++++++++++++++++++++++++++++++++++++++++++++++++++++++
再来看看clahe_lut
template<int TYPE, int BPP, int ROWS, int COLS, int NPPC, int W, int H>
void clahe_lut(xf::Mat<TYPE, ROWS, COLS, NPPC>& src, xf::Mat<TYPE, ROWS, COLS, NPPC>& dst, HistLutMap<W, H>& lut_map, u8 mode)
{
u16 width = src.cols;
u16 height = src.rows;
u16 nw = W * 2;
u16 nh = H * 2;
u16 bw = width / nw; //计算block的尺寸
u16 bh = height / nh; //计算block的尺寸
ap_fixed<32,16> bw_div = div_to_mux(nw, width);// 求倒数
ap_fixed<32,16> bh_div = div_to_mux(nh, height); // 求倒数
#pragma HLS ALLOCATION instances=div_to_mux limit=1 function
#if DEBUG
std::cout << "bw_div: " << bw_div << std::endl;
std::cout << "bh_div: " << bh_div << std::endl;
#endif
u32 src_idx = 0, dst_idx = 0;
u32 tmp_map[W * 2][256];
//输入的LUT表,和图像的尺寸之间,存在比例关系,
//将图像分成一个个block,每个block对应于LUT表中的一个点。
// 按row block处理,逐个row block处理,
for (u16 i = 0; i < nh; i++)
{
#pragma HLS loop_tripcount avg = H max = H
//按column block处理,逐个column block处理,
//相邻的两个column block,参考同一个LUT中的点。
//所以,需要计算折半游标。
for (u16 j = 0; j < nw; j++)
{
#pragma HLS loop_tripcount avg = W max = W
//计算折半的游标,作为LUT表中的索引
u16 ni = (i == 0) ? 0 : ((i - 1) / 2);
u16 nj = (j == 0) ? 0 : ((j - 1) / 2);
//对当前点,按值逐值处理,
for (u16 k = 0; k < 256; k++)
{
#pragma HLS pipeline
#pragma HLS loop_flatten off
//对当前值,获取LUT中对应点处的均衡值,
u32 a0 = lut_map.data_[ni][nj][k];
//对当前值,获取LUT中的2X2邻域处的均衡值,
u32 a1 = (ni < (H - 1)) ? lut_map.data_[ni + 1][nj][k] : a0;
u32 a2 = (nj < (W - 1))? lut_map.data_[ni][nj + 1][k] : a0;
u32 a3 = (ni < (H - 1) && (nj < (W - 1))) ? lut_map.data_[ni + 1][nj + 1][k] : a0;
//对当前值,将2X2邻域的均衡值,打包到一个u32中去。
u32 mv = a0 | (a1 << 8) | (a2 << 16) | (a3 << 24);
//将打包后的均衡值,写入临时数组。
tmp_map[j][k] = mv;
}
}
//在一个row block内,逐行处理,所有的行,公用同一个tmp_map
for (u16 m = 0; m < bh; m++)
{
#pragma HLS loop_tripcount avg = ROWS/H max = ROWS/H
//按column block,逐个block处理
for (u16 j = 0; j < nw; j++)
{
#pragma HLS loop_tripcount avg = W max = W
// 在一个column block中,逐点处理,
for (u16 n = 0; n < bw; n++)
{
#pragma HLS loop_tripcount avg = COLS/W max = COLS/W
#pragma HLS pipeline II = 1
#pragma HLS loop_flatten off
//对当前点,读取数据,
u8 val = src.read(src_idx++);
//从临时映射表中,根据值,找到对应的均衡值。
//同一个column block,公用同一个点的LUT。
u32 mv = tmp_map[j][val];
//取出各个邻域点均衡值
u8 a0 = mv & 0xff;
u8 a1 = (mv >> 8) & 0xff;
u8 a2 = (mv >> 16) & 0xff;
u8 a3 = (mv >> 24) & 0xff;
//计算扩展坐标
//对于Y坐标,根据当前的row block号,选择
u16 ny = ((i & 0x1) == 0) ? (bh + m) : m;
//对于X坐标,根据当前的column block号,选择
u16 nx = ((j & 0x1) == 0) ? (bw + n) : n;
// 计算对应在邻域中的线性亚坐标。并进行限幅保护。
ap_fixed<32,16> u = ny * bh_div * 0.5;
ap_fixed<32,16> v = nx * bw_div * 0.5;
if ((i == 0) || (i == (nh - 1)))
u = 0;
if ((j == 0) || (j == (nw - 1)))
v = 0;
//计算双线性插值,并限幅保护
ap_fixed<32,16> out= a3 * u * v + a2 * (1 - u) * v + a1 * u * (1 - v) + a0 * (1 - u) * (1 - v);
if (out > 255)
out = 255;
// MUX,选取源数据或者双线性插值数据,
u8 dstpixel = (mode == 0) ? (val) : ((u8)out);
//将数据写入dst中。
dst.write(dst_idx++, dstpixel);
}
}
}
}
}
+++++++++++++++++++++++++++++++++++++++++++++++++++++
再来看testbench。
#include "clahe_accl.h"
#include "clahe_accl_tb.h"
#include "opencv/cv.h"
#include "opencv/highgui.h"
#include "opencv2/imgproc/imgproc.hpp"
//#include "common/xf_sw_utils.h"
#include "common/xf_axi.h"
using namespace cv;
需要使用xfopencv库,以及namespace。
int main(int argc, char *argv[])
{
if (argc != 2)
{
printf("usage: %s bayer.png\n", argv[0]);
return -1;
}
cv::Mat img;
img = cv::imread(argv[1]);
if (img.data == NULL)
{
fprintf(stderr,"Cannot open image at %s\n", argv[1]);
return 0;
}
int width = img.size().width;
int height = img.size().height;
width = 2112;//ceil(((float)width) / 16) * 16;
height = 1216;//ceil(((float)height) / 16) * 16;
printf("(%dx%d)\n", width, height);
cv::Mat in_img;
cv::resize(img, in_img, cv::Size(width, height));
u32 limit_th = (width * height / 2 / WIN_W / WIN_H / 256 * 1 );
u32 scale_th = 2;
u8 mode = 1;
cv::Mat out_img;
out_img.create(height, width, CV_8UC3);
hls::stream<T_AXIU(IMAGE_BPP, IMAGE_NPPC)> src;
hls::stream<T_AXIU(IMAGE_BPP, IMAGE_NPPC)> dst;
cvMat2AXIvideoxf<IMAGE_NPPC>(in_img, src);
clahe_accl(src, dst, width, height, limit_th, scale_th, mode);
AXIvideo2cvMatxf<IMAGE_NPPC>(dst, out_img);
cvMat2AXIvideoxf<IMAGE_NPPC>(in_img, src);
clahe_accl(src, dst, width, height, limit_th, scale_th, mode);
AXIvideo2cvMatxf<IMAGE_NPPC>(dst, out_img);
cv::imwrite("hls.bmp", out_img);
imwrite("image.bmp", in_img);
printf("test ok!\n");
return 0;
}
主体上和基本框架类似,
这里,调用 cv::resize调整尺寸,并用到了cv::Size类的对象。
后面,通过连续两次执行DUT,起到刷新static局部变量的作用。
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++
补充:
全图直方均衡表,
如果是进行全图直方均衡,那么只需要全图统计后,只需要一个LUT映射表即可。这是一个一维数组。
如果要得到更精细的直方均衡,那么需要使用
区块直方均衡图,
将原图分为多个区块,在每个区块内进行直方统计,并针对每一个区块,输出一个LUT映射表,这是一个一维数组,但是每个区块的直方均衡表,则共同组成了一个三维数组。
可以看出,原图和区块图之间,存在一个缩放比例关系。
在后续使用区块直方均衡图时,需要使用到邻域关系,用来做双线性插值。
线性插值,
是基于邻域共同作用,共同影响的思想设计的。
单线性插值,
是假设某个点的值,应该为两个相邻的离散的参考点的值的线性叠加。加权程度,以该点分别距离两个参考点的远近来衡量。归一化尺度为两个参考点的距离。
即,两个参考点的距离,归一化为一。
假设,A距离参考点R0,归一化距离为u,A距离参考点R1,归一化距离为v,u+v=1,
则A的值,应该为
Va = Vr0 + (Vr1- Vr0)*u,
= Vr0 + Vr1*u -Vr0*u
= Vr0*(1-u) + Vr1*u
= Vr0 * v + Vr1 * u
可以看出,参考点的值,不是乘以相对于自己的距离,而是乘以相对于另一个参考点的距离,或者说,乘以相对于自己的互补距离,简称补距。
推广到双线性插值,
是假设某个点的值,应该为一个矩形的四个角点,作为离散的参考点的值的线性叠加。加权程度,以该点分别距离四个参考点的X方向和Y方向的远近来衡量。归一化尺度为X方向或者Y方向的参考点的距离,即边长。
双线性插值,是一个二维操作,可以先降维到一维,再计算单线性插值。
例如,对于一个矩形,有四个角点R00,R10,R01,R11,
对于点A,X方向归一化坐标为v,Y方向归一化坐标为u,
首先,降维,用Y方向的归一化坐标u,计算两组参考点的等效参考点的值。
Ver0 = Vr00 * (1-u) + Vr01 * u
Ver1 = Vr10 * (1-u) +Vr11 * u
然后,在X方向上,用等效参考点的值,计算A点的值。
Va = Ver0 * (1-v) + Ver1 * v
= Vr00 * (1-u)*(1-v) + vr01 * u * (1-v) + Vr10 * (1-u) * v + Vr11 * u * v
可以看出,双线性插值,加权系数,是X方向和Y方向的补距积。
马赛克效应,
最简单的应用下,每个区块,用自己的区块直方均衡表进行均衡,
但是这样的问题是,不同的区块,就会有相当明显的差别,
所以,需要相邻的区块之间,使用双线性插值。
由四个相邻的区块的直方均衡表一起使用。
这就涉及到,区块内有多个像素,那么区块的直方表,具体对应到区块的哪个位置?
显然,区块直方表,应该对应到区块的中心点上。即,中心绑定或者中心映射。
在使用了中心映射的时候,就存在一个现象,区块内的像素,相对于中心参考点的距离,有正有负,如何处理?
这就需要将区块均分为四象限。每个象限,成为一个更细的区块,这里称为象限区块。
每个区块的一个象限,会和其他三个相邻的区块的一个象限,共同组成一个新的虚拟区块。
现在的问题是,边界上的象限区块,不能组成完整的虚拟区块,如何处理?
这就需要虚拟参考点,假设在边界外,存在一圈虚拟的区块,这些虚拟的区块,复制了边界区块的参考点的值。
在算法设计时,还有一种思路,就是参考点散列,将区块中心点的参考点,按照规则散列到各个象限区块的角点上去。
这样,后续就可以直接以象限区块为单位,以象限区块的角点为参考点,进行双线性插值处理了。
这也是本例中选择的设计路线。