在视频图像几何校正或图像配准坐标系转换中,往往需要进行缩放、旋转、透视变换等几何变换操作。要想进行缩放旋转操作,就要得到输入输出图像之间的映射关系,也可称之为几何变换关系。
前向映射适合于处理流输入,例如 来自一个摄像机的输入,其每个输入像素 能被映射到 指定输出图像中的位置。
逆向映射更适合于产生数据流输出,例如图像数据流输出到显示器,因为对于每个输出像素,逆向映射指定了 像素值来自于 输入图像的什么位置
地址的话,一般由行列计数器
提供,通过行列计数器获得某点坐标,从而根据二维坐标到一维地址的转换
计算得到地址。
但是按照这种映射关系,输出图像的像素可能被映射到输入图像的非整数坐标上,因此需要采用插值技术
。
前向映射的缺点:通过输入位置和相应映射关系来求输出位置,那么当得到非整数输出像素位置的时候,就需要进行四舍五入等,那么会出现如下的问题:
1、当缩放变换系数大于1(放大图像)的时候,四舍五入导致出现空洞
,让映射后的一些输出位置没有像素值
2、四舍五入导致像素覆盖
,映射后的几个输入像素可能被映射到同一个输出像素位置,那么后一个输出像素值会将前一个覆盖
除此之外,还有一个缺点是输出图像某点像素值不能直接得到
,需要遍历输入图像的所有像素值,对其进行坐标变换,分配像素值到整数位置,才能得到输出图像各像素点的像素值。
结论:相比前向映射,逆向映射法更直观。对于逆向映射来说,我们知道输出图像上整数点位置(x’,y’)在变换前位于输入图像上的位置(x,y),一般来说这是个非整数点位置,利用其周围整数像素位置以及对应的像素值(双线性插值使用周围四个整数像素位置)进行插值,就得到了该点的像素值。我们遍历输出图像,经过坐标变换、插值两步操作,我们就能将其像素值一个个地计算出来,因此逆向映射又叫图像填充映射。如下图所示:
网上有很多解释,可自己查阅
最近邻采用最常见的坐标系,以图像的左上角为原点(0,0);假如我们已知源图像src大小为3 * 3,且知道3 * 3图像内9个点的像素值,现在将src源图像扩大成4*4的目标图像dst,并将对应的像素值填入,在填入对应像素值的时候有如下公式(1):
> srcX = dstX* (src_Width/dst_Width)
>
> srcY = dstY * (src_Height/dst_Height)
src_Width / ds_tWidth 和 src_Height / dst_Height = 3/4 ,表示两幅图像的长宽边长比。
dstX和dstY我们可以根据目标图像得到,代表目标图像中每个点的横纵坐标。
因此根据dstX和dstY、以及边长比,即可算出对应的源图像坐标,将对应的像素值读出进行填补即可。
举例:
目标图像左上角(0,0),带入上述公式,即可找到对应源图像中的坐标(0*(3/4),0*(3/4))=(0,0),因此目标图像(0,0)位置对应的像素值就是源图像(0,0)点对应的像素值2。
同理计算目标图像(0,1)点对应源图像中的(0,3/4)位置,在源图像中像素坐标值都是整数,因此这里的小数3/4需要四舍五入,为(0,3/4)=(0,0.75) = (0,1),因此对应像素值为38。
依次计算,最终可得到4*4图像的全部像素值,此时就实现了图像的最邻近插值放大。
总结:通过上述插值可知,当遇到浮点数的时候直接四舍五入取值,拿0.75来说,我们直接取1,这样就会出现较大的误差。因此我们采用距离权重的方式来减少这种误差。既然0.75位于0和1直接,我们即可采用0.75到0,1的距离来分配权重,最终通过整数位置的像素值,得到浮点数0.75处的像素值,这也称为线性插值。
上述介绍了最邻近插值,误差较大,我们引入了距离权重。将一个浮点数坐标(i + u ,j + v)根据权重分配到距离其最近的四个整数坐标上,进而通过这四个整数坐标的像素值,来得到该浮点坐标的像素值。
双线性插值公式(2):
其中i j 表示浮点数的整数部分,u v表示浮点数的小数部分。 (i+u,j+v) 最近的原图像中的整数坐标为 (i,j)、(i+1,j)、(i,j+1)、(i+1,j+1)。
比如下图所示,已知四个红色点的像素值且为整数,求最终落在非整数位置的P的像素值。我们可以先在x方向上进行插值,得到插值结果R1和R2,然后在y方向上继续进行插值,即可得到插值点P。因此当我们知道四个整数坐标以及它们的像素值时,即可根据双线性插值公式,计算出P点的像素值。
上述我们所讲的原点都是图像的左上角,所以其坐标的计算都由公式(1)得到,这里存在一个问题,当33图像扩大成55,对于5*5图像来说,中心坐标是(2,2),用公式(1)计算出对应的源坐标是(1.2,1.2),但相对于源图像来说,其中心坐标是(1,1),因此我们插值的时候所利用的图像像素整体偏右下,而不是均匀地分布整个图像,为了均匀分布整个图像,我们在取原点的时候,不取左上角,而是取中心对齐的方式来插值,因此源坐标和目标坐标之间的映射关系变成公式(3):
> SrcX = (dstX+0.5)* (src_Width/dst_Width) -0.5
>
> SrcY = (dstY+0.5) * (src_Height/dst_Height)-0.5
此时将目标图中心坐标(2,2)带入,得到对应的源图像坐标(1,1),和源图像中心坐标重合,即可均匀的分布与整幅图像。
主要工作:得到浮点数(i + u ,j +v)的整数部分的值 i j ;得到四个插值系数 u , 1-u ,v , 1-v
模块接口:
我这里dst_width直接带入的数值,其实可以引出该信号并赋值,更直观。通过src_width和dst_width来计算源图像x y坐标。
//高10位为整数部分
assign coordinate_x = src_x[19:10]; // i
assign coordinate_y = src_y[19:10]; // j
//低10位为小数部分,但本来小数部分是9位,因此高位补0
assign coordinate_xx = {1'b0,src_x[8:0]}; //u
assign coordinate_yy = {1'b0,src_y[8:0]}; //v
assign coefficient2 = {1'b0,src_x[8:0]}; //u
assign coefficient1 = 'd512 - coefficient2; // 1-u
assign coefficient4 = {1'b0,src_y[8:0]}; //v
assign coefficient3 = 'd512 - coefficient4; //1-v
(i,j)= (srcX[19:10] ,srcY[19:10]);
(i+1,j) = (srcX[19:0] + 'd1,srcY[19:0]);
(i,j+1) = (srcX[19:0],srcY[19:10] + 'd1);
(i+1,j+1) = (srcX[19:0] + 'd1 ,srcY[19:10] + 'd1);
有了四个整数坐标,即可将二维坐标转换成一维地址,分别从四个存储里面读对应的像素值:
assign address_bx = (coordinate_x == width)?coordinate_x + coordinate_y*src_width - 'd1:coordinate_x + coordinate_y*src_width;
assign address_bx1 = (coordinate_x == width)?address_bx:address_bx + 'd1;
assign address_by = (coordinate_y==width)?address_bx:coordinate_x + (coordinate_y+'d1)*src_width;
assign address_by1 = (coordinate_x == width)?address_by:address_by + 'd1;
目前是插值系数,以及四个整数坐标对应的像素值都已经有了,因此
代入公式:
assign data_1 = coefficient1*coefficient3*doutbx; //(1-u)*(1-v)*f(i,j)
assign data_2 = coefficient2*coefficient3*doutbx1;//u*(1-v)*f(i+1,j)
assign data_3 = coefficient1*coefficient4*doutby;//(1-u)*v*f(i,j+1)
assign data_4 = coefficient2*coefficient4*doutby1;//u*1-v*f(i+1,j+1)
将四个data进行相加:
assign data_a = data_1 + data_2;
assign data_b = data_3 + data_4;
assign data_oq = data_aq + data_bq;
因为扩大了512倍数,因此还要将结果缩小512倍。所以最终的结果我们要取高8位。
data_o <= data_oq[25:18];
该设计计算量大,不便于仿真数值的查看,因此我们将计算出的数据以txt文本的方式,导出,然后用matlab读取txt文档,直接进行图像的显示。
导出方式:在tb文件中添加如下代码
`timescale 10 ns/ 10 ps
module bilinear_tb();
reg[7:0] save_data[0:256*256-1]; //表示有65536个8位的图像数据
reg clk;
reg start;
wire [7:0] VGA_RGB;
wire VGA_BLK;
wire hsync;
wire vsync;
wire en_o;
//实例化顶层文件
bilinear i1 (
.clk (clk),
.start (start),
.VGA_RGB (VGA_RGB),
.VGA_BLK (VGA_BLK),
.hsync (hsync),
.vsync (vsync),
.en_o(en_o)
);
initial
begin
clk =1;
start =0;
#10;
start = 1;
end
//产生时钟激励
always #1 clk =~clk;
//打开post_img.txt文件
//---------------------------------------------------
integer post_img_txt;
initial begin
post_img_txt = $fopen("post_img.txt");
end
//像素写入到txt中
//---------------------------------------------------
reg [20:0] pixel_cnt;
always @(posedge clk) begin
if(!start) begin
pixel_cnt <= 0;
end
else if(en_o) begin
pixel_cnt = pixel_cnt + 1;
$fdisplay(post_img_txt,"%h",VGA_RGB);
if(pixel_cnt == 65536)
$stop;
end
end
endmodule
txt文档用matlab处理,以生成相应图片:
fid0=fopen('savedata.txt','r'); %读入所需要的txt文件
[a,count]=fscanf(fid0,'%x'); %a为data.txt文件数据读入的矩阵,以16进制形式,count为该矩阵元素个数
b=reshape(a,256,256); % 构建成100*100的矩阵形式
c=b'; % 需要再转置一次方为图片行列方向的矩阵
imshow(c,[]); %显示图片
title('verilog插值后的图像')
当加入VGA模块的时候,使用上述方法出现问题,因此tb中的计数器是顺序的,因此像素也是按顺序逐脉冲的写入,而对于VGA显示来说,它有行场消隐时间,因此采用逐脉冲顺序写入的方式,会出现无用的像素点,搞清楚原因后没有继续修改,因此如果加上了VGA模块,我们只需要调用锁相环,提供25Mhz时钟,然后上板验证即可。
代码
参考