FPGA实现图像几何变换:缩放
假设图像x轴方向的缩放比率Sx,y轴方向的缩放比率Sy,相应的变换表达式为:

其逆运算如下:

直接根据缩放公式计算得到的目标图像中,某些映射源坐标可能不是整数,从而找不到对应的像素位置。例如,当Sx=Sy=2时,图像放大2倍,放大图像中的像素(0, 1)对应于原图中的像素(0, 0.5),这不是整数坐标位置,自然也就无法提取其灰度值。因此我们必须进行某种近似处理,这里介绍一-种简单的策略即直接将它最邻近的整数坐标位置(0,0)或者(0,1)处的像素灰度值赋给它,这就是所谓的最近邻插值。当然还可以通过其他插值算法来近似处理。
然而,FPGA实现插值算法比较困难,足可以作为一篇论文来讨论了,为了简化操作,本次设计采用简单的像素复制和像素阉割的方式来实现图像的放大和缩小。
一、MATLAB实现
%-------------------------------------------------------------------------- %-- 图像的放大和缩小 %-------------------------------------------------------------------------- clear all close all clc img = imread('monkey.jpg'); %读取输入图片的数据 A = imresize(img,2); imwrite(A,'放大2倍.jpg'); B = imresize(img,0.5); imwrite(B,'缩小2倍.jpg');
MATLAB自带缩放函数,就懒得自己写了。默认采用的是最近邻插值法,也可以选择双线性插值法(bilinear)、双三次插值法(bicubic)。因为MATLAB中的 imshow 会让图片看起来尺寸一样,所以选择另存为图片,用电脑图片查看软件打开:

二、FPGA实现图像放大
1、实现原理
FPGA实现各种插值算法难度较大,我也没这个心情去深究,直接采用像素复制的办法。
假设一张图片如下所示:

现将图片扩大为原先的2倍,则图片变成如下所示:

接下来就用Verilog来实现这一算法。
2、代码设计
这次的代码设计和之前的镜像、旋转类似,关键都在于地址的选择,SDRAM 控制器比较复杂,懒得改,拿一个 RAM 来做缓存和跨时钟域的处理,图片分辨率为 140x140x16bit。
这次同样引入一个外部按键,用于控制放大的倍数,共有 1、2、4、8 四种倍数,如下所示:
always @(posedge rd_clk or negedge rst_n) begin if(!rst_n) begin n <= 2'b00; end else if(key_vld) begin n <= n + 1'b1; end end
接下来我们就可以利用这个 n 来设计放大的地址了,请看代码:
//**************************************************************************
// *** 名称 : Zoom_up.v
// *** 作者 : xianyu_FPGA
// *** 博客 : https://www.cnblogs.com/xianyufpga/
// *** 日期 : 2019-06-23
// *** 描述 : 放大操作,尺寸不变
//**************************************************************************
module Zoom_up
//========================< 端口 >==========================================
(
//system --------------------------------------------
input wire rst_n , //复位,低电平有效
//uart ----------------------------------------------
input wire wr_clk , //50m
input wire [15:0] din ,
input wire din_vld ,
//key -----------------------------------------------
input wire key_vld , //按键切换模式
//TFT_driver ----------------------------------------
input wire rd_clk , //9m
input wire [ 9:0] TFT_x , //得到显示区域横坐标
input wire [ 9:0] TFT_y , //得到显示区域纵坐标
output wire [15:0] TFT_data //输出图像数据
);
//========================< 参数 >==========================================
parameter COL = 10'd140 ; //图片长度
parameter ROW = 10'd140 ; //图片高度
parameter IMG_x = 10'd170 ; //图片起始横坐标
parameter IMG_y = 10'd66 ; //图片起始纵坐标
//========================< 信号 >==========================================
reg [15:0] buffer[COL*ROW-1:0] ; //类似RAM
reg [14:0] wr_addr ;
reg [14:0] rd_addr ;
//---------------------------------------------------
wire rd_en ;
reg rd_en_r ;
reg [ 9:0] cnt_col ;
wire add_cnt_col ;
wire end_cnt_col ;
reg [ 9:0] cnt_row ;
wire add_cnt_row ;
wire end_cnt_row ;
//---------------------------------------------------
reg [ 1:0] n ;
reg [ 9:0] zoom_x ;
reg [ 9:0] zoom_y ;
//==========================================================================
//== 缓存buffer,写操作
//==========================================================================
//写数据
//---------------------------------------------------
always @(posedge wr_clk) begin
buffer[wr_addr] <= din;
end
//写地址
//---------------------------------------------------
always @(posedge wr_clk or negedge rst_n) begin
if(!rst_n) begin
wr_addr <= 'd0;
end
else if(din_vld) begin
wr_addr <= wr_addr + 1'b1;
end
end
//==========================================================================
//== 行列规划
//==========================================================================
//读使能,确定显示位置
//---------------------------------------------------
assign rd_en = (TFT_x >= IMG_x) && (TFT_x < IMG_x + COL) &&
(TFT_y >= IMG_y) && (TFT_y < IMG_y + ROW)
? 1'b1 : 1'b0;
//行计数
//---------------------------------------------------
always @(posedge rd_clk or negedge rst_n) begin
if(!rst_n)
cnt_col <= 10'd0;
else if(add_cnt_col) begin
if(end_cnt_col)
cnt_col <= 10'd0;
else
cnt_col <= cnt_col + 10'd1;
end
end
assign add_cnt_col = rd_en;
assign end_cnt_col = add_cnt_col && cnt_col== COL-10'd1;
//列计数
//---------------------------------------------------
always @(posedge rd_clk or negedge rst_n) begin
if(!rst_n)
cnt_row <= 10'd0;
else if(add_cnt_row) begin
if(end_cnt_row)
cnt_row <= 10'd0;
else
cnt_row <= cnt_row + 10'd1;
end
end
assign add_cnt_row = end_cnt_col;
assign end_cnt_row = add_cnt_row && cnt_row== ROW-10'd1;
//==========================================================================
//== 镜像操作,读地址重规划
//==========================================================================
always @(posedge rd_clk or negedge rst_n) begin
if(!rst_n) begin
n <= 2'b00;
end
else if(key_vld) begin
n <= n + 1'b1;
end
end
//偏移量公式:+ [side*(n-1)/2],n为放大倍数
//---------------------------------------------------
always @(*) begin
case(n)
2'b00 : begin //原图
zoom_x = cnt_col;
zoom_y = cnt_row;
end
2'b01 : begin //2倍
zoom_x = (cnt_col+70)>>1;
zoom_y = (cnt_row+70)>>1;
end
2'b10 : begin //4倍
zoom_x = (cnt_col+210)>>2;
zoom_y = (cnt_row+210)>>2;
end
2'b11 : begin //8倍
zoom_x = (cnt_col+490)>>3;
zoom_y = (cnt_row+490)>>3;
end
default : begin
zoom_x = cnt_col;
zoom_y = cnt_row;
end
endcase
end
//==========================================================================
//== 缓存buffer,读操作
//==========================================================================
always @(posedge rd_clk or negedge rst_n) begin
if(!rst_n)
rd_addr <= 'd0;
else
rd_addr <= zoom_y * COL + zoom_x;
end
always @(posedge rd_clk) begin
rd_en_r <= rd_en;
end
assign TFT_data = rd_en_r ? buffer[rd_addr] : 16'hffff;
endmodule
如果没有偏移量,那么图像的放大将从左上角开始,放大后的图像出现偏移,因此引入偏移量,使图片放大后的中间点还是原图片位置的中间点。偏移量公式为:[side * (n-1)/2],n为放大倍数,由按键提供。side为边长,这里我选用的图片是140x140,边长一样都是140。这个偏移公式实际是数学问题,不理解的话对照着上面的示意图写写算算就懂了。
3、上板验证
上板后首先看到的是原图:

放大2倍:

放大4倍:

放大8倍:

视频演示如下:
此次使用FPGA实现放大功能的实验成功。另外说一点的是,本次的设计尽管图像放大了,但是图像的尺寸没有变化,超过尺寸的图像直接舍去了。如果确实需要,我们也可以改成图像尺寸随着放大的尺寸而跟着变化,重点无非一样是显示的坐标设计。
为了避免除法器,改为移位计算,得到1、2、4、8倍放大,如果采用除法,则可以实现任意整数倍放大。
三、FPGA实现图像缩小
1、实现原理
假设一张图片如下所示:

现将图片变为原先的1/2,则图片变成如下所示:

尺寸变成了原先的1/2,此外数据也减少了,显示采用隔行隔列处理,将像素压缩为原先的1/2。(这种图片做的有点问题,但大致就是这个原理。)
2、FPGA实现
这次的代码设计和之前的镜像、旋转类似,关键都在于地址的选择,SDRAM 控制器比较复杂,懒得改,拿一个 RAM 来做缓存和跨时钟域的处理,图片分辨率为 140x140x16bit。
这次同样引入一个外部按键,用于控制缩小的倍数,共有 1、2、4、8 四种倍数,如下所示:
//缩小倍数 //--------------------------------------------------- always @(posedge rd_clk or negedge rst_n) begin if(!rst_n) begin n <= 2'b00; end else if(key_vld) begin n <= n + 1'b1; end end //读地址坐标 //--------------------------------------------------- assign zoom_x = cnt_col << n; assign zoom_y = cnt_row << n;
直接利用 n 来移位,达到缩小倍数的需求。
此外缩小会导致尺寸减小,因此最后的输出显示的尺寸也需要改动一下,如果不改动,那么可能会出现4张缩小的图片同时显示,比较难看。
这是原先的输出范围,行列计数时用过,本来最后输出也是用它的。
//读使能,确定显示位置 //--------------------------------------------------- assign rd_en = (TFT_x >= IMG_x) && (TFT_x < IMG_x + COL) && (TFT_y >= IMG_y) && (TFT_y < IMG_y + ROW) ? 1'b1 : 1'b0;
现在改动一下输出的尺寸:
//只显示左上角第一个缩小图像 //--------------------------------------------------- assign zoom_rd_en = (TFT_x >= IMG_x) && (TFT_x < IMG_x + (COL>>n)) && (TFT_y >= IMG_y) && (TFT_y < IMG_y + (ROW>>n)) ? 1'b1 : 1'b0;
//assign TFT_data = rd_en ? buffer[rd_addr] : 16'h0000; assign TFT_data = zoom_rd_en ? buffer[rd_addr] : 16'h0000;
这样就只显示4张缩小的图片的第1张图片了,好看多了。
//**************************************************************************
// *** 名称 : Zoom_down.v
// *** 作者 : xianyu_FPGA
// *** 博客 : https://www.cnblogs.com/xianyufpga/
// *** 日期 : 2019-06-23
// *** 描述 : 缩小操作,尺寸变小
//**************************************************************************
module Zoom_down
//========================< 端口 >==========================================
(
//system --------------------------------------------
input wire rst_n , //复位,低电平有效
//uart ----------------------------------------------
input wire wr_clk , //50m
input wire [15:0] din ,
input wire din_vld ,
//key -----------------------------------------------
input wire key_vld , //按键切换模式
//TFT_driver ----------------------------------------
input wire rd_clk , //9m
input wire [ 9:0] TFT_x , //得到显示区域横坐标
input wire [ 9:0] TFT_y , //得到显示区域纵坐标
output wire [15:0] TFT_data //输出图像数据
);
//========================< 参数 >==========================================
parameter COL = 10'd140 ; //图片长度
parameter ROW = 10'd140 ; //图片高度
parameter IMG_x = 10'd170 ; //图片起始横坐标
parameter IMG_y = 10'd66 ; //图片起始纵坐标
//========================< 信号 >==========================================
reg [15:0] buffer[COL*ROW-1:0] ; //类似RAM
reg [14:0] wr_addr ;
reg [14:0] rd_addr ;
//---------------------------------------------------
wire rd_en ;
reg [ 9:0] cnt_col ;
wire add_cnt_col ;
wire end_cnt_col ;
reg [ 9:0] cnt_row ;
wire add_cnt_row ;
wire end_cnt_row ;
//---------------------------------------------------
reg [ 1:0] n ;
wire [ 9:0] zoom_x ;
wire [ 9:0] zoom_y ;
wire zoom_rd_en ;
//==========================================================================
//== 缓存buffer,写操作
//==========================================================================
//写数据
//---------------------------------------------------
always @(posedge wr_clk) begin
buffer[wr_addr] <= din;
end
//写地址
//---------------------------------------------------
always @(posedge wr_clk or negedge rst_n) begin
if(!rst_n) begin
wr_addr <= 'd0;
end
else if(din_vld) begin
wr_addr <= wr_addr + 1'b1;
end
end
//==========================================================================
//== 行列规划
//==========================================================================
//读使能,确定显示位置
//---------------------------------------------------
assign rd_en = (TFT_x >= IMG_x) && (TFT_x < IMG_x + COL) &&
(TFT_y >= IMG_y) && (TFT_y < IMG_y + ROW)
? 1'b1 : 1'b0;
//行计数
//---------------------------------------------------
always @(posedge rd_clk or negedge rst_n) begin
if(!rst_n)
cnt_col <= 10'd0;
else if(add_cnt_col) begin
if(end_cnt_col)
cnt_col <= 10'd0;
else
cnt_col <= cnt_col + 10'd1;
end
end
assign add_cnt_col = rd_en;
assign end_cnt_col = add_cnt_col && cnt_col== COL-10'd1;
//列计数
//---------------------------------------------------
always @(posedge rd_clk or negedge rst_n) begin
if(!rst_n)
cnt_row <= 10'd0;
else if(add_cnt_row) begin
if(end_cnt_row)
cnt_row <= 10'd0;
else
cnt_row <= cnt_row + 10'd1;
end
end
assign add_cnt_row = end_cnt_col;
assign end_cnt_row = add_cnt_row && cnt_row== ROW-10'd1;
//==========================================================================
//== 镜像操作,读地址重规划
//==========================================================================
//缩小倍数
//---------------------------------------------------
always @(posedge rd_clk or negedge rst_n) begin
if(!rst_n) begin
n <= 2'b00;
end
else if(key_vld) begin
n <= n + 1'b1;
end
end
//读地址坐标
//---------------------------------------------------
assign zoom_x = cnt_col << n;
assign zoom_y = cnt_row << n;
//==========================================================================
//== 缓存buffer,读操作
//==========================================================================
//读地址
//---------------------------------------------------
always @(posedge rd_clk or negedge rst_n) begin
if(!rst_n)
rd_addr <= 'd0;
else
rd_addr <= zoom_y * COL + zoom_x;
end
//只显示左上角第一个缩小图像
//---------------------------------------------------
assign zoom_rd_en = (TFT_x >= IMG_x) && (TFT_x < IMG_x + (COL>>n)) &&
(TFT_y >= IMG_y) && (TFT_y < IMG_y + (ROW>>n))
? 1'b1 : 1'b0;
assign TFT_data = zoom_rd_en ? buffer[rd_addr] : 16'h0000;
endmodule
3、上板验证
上板后首先看到的是原图:

缩小2倍:

缩小4倍:

缩小8倍:

视频演示如下:
此次使用FPGA实现缩小功能的实验成功。另外说一点的是,本次的设计的显示图像为左上角,没有调到正中间。如果确实需要,可以进一步改进。
为了避免除法器,改为移位计算,得到1、2、4、8倍缩小,如果采用除法,则可以实现任意整数倍缩小。
后记
FPGA实现几何变换的博客到此为止了,一共实现了:裁剪、镜像、旋转、平移和缩放。其中裁剪是最简单的,而后面4个都是利用了图片缓存的地址做文章,镜像一篇重点介绍了图片缓存地址的设置,后面几篇对此提的少,仅列出不同部分,如果看不懂可以回到镜像那篇博客仔细阅读。
很多设计都是小demo,实现的比较粗糙,但也是图像处理的一种,扩展了我们图像处理的思路,提高了Verilog的设计能力。
四、补充更新(二倍放大)
无意间想到一种二倍放大的简易实现方法,故更新一下。
1、原理
如果一副图像的宽度需要变为原先的一半,要怎么做?很简单,降采样就行,假如原先一行像素是1、2、3、4、5、6、7、8,那么降采样后就是 1、3、5、7 或2、4、6、8,行宽就变成一半了。高度变为原先一半也是一样的道理,那么二倍缩小就是行列都降采样就行。那么二倍放大就可以反过来想,每 1/2 行读两次,每一整行再重读一次。直接用 ram 就能实现了,如下所示:

这样一幅 4x2 的图片就二倍放大为 8x4 了,理论存在,实践开始。
2、时序分析

如图所示,设计 VGA_req 信号,使之为原先行的 1/2,列只取奇数部分,偶数的则填充上一行奇数的,但是输出的 de 则是完整的,代码如下所示:
`timescale 1 ns/1 ns
//**************************************************************************
// *** 名称 : VGA_enlarge.v
// *** 作者 : xianyu_FPGA
// *** 博客 : https://www.cnblogs.com/xianyufpga/
// *** 日期 : 2019-06-26
// *** 描述 : 进行2倍放大后进行VGA输出
//**************************************************************************
module VGA_enlarge
//========================< 参数 >==========================================
// 640x480 @60Hz 25Mhz
#(
parameter H_ADDR = 640 , //行有效数据
parameter H_SYNC = 96 , //行同步
parameter H_BACK = 48 , //行显示后沿
parameter H_TOTAL = H_ADDR+H_SYNC+H_BACK , //行扫描周期
parameter V_ADDR = 480 , //场有效数据
parameter V_SYNC = 2 , //场同步
parameter V_BACK = 33 , //场显示后沿
parameter V_TOTAL = V_ADDR+V_SYNC+V_BACK //场扫描周期
)
//========================< 端口 >==========================================
(
//system --------------------------------------------
input wire clk , //时钟
input wire rst_n , //复位,低电平有效
//VGA_display ---------------------------------------
output wire VGA_req , //请求图像数据
input wire [23:0] VGA_din , //得到图像数据
//VGA output ----------------------------------------
output wire VGA_hsync , //VGA接口行信号
output wire VGA_vsync , //VGA接口场信号
output wire [23:0] VGA_data , //VGA接口数据信号
output reg VGA_de //VGA接口数据有效指示信号
);
//========================< 信号 >==========================================
reg [15:0] cnt_h ;
wire add_cnt_h ;
wire end_cnt_h ;
reg [15:0] cnt_v ;
wire add_cnt_v ;
wire end_cnt_v ;
//---------------------------------------------------
reg ram_wr_en ;
reg [ 9:0] ram_wr_addr ;
wire [23:0] ram_wr_data ;
wire ram_rd_en ;
reg [ 9:0] addr_cnt ;
wire [ 9:0] ram_rd_addr ;
wire [23:0] ram_rd_data ;
//==========================================================================
//== 行、场计数
//==========================================================================
always @(posedge clk or negedge rst_n) begin
if(!rst_n)
cnt_h <= 0;
else if(add_cnt_h) begin
if(end_cnt_h)
cnt_h <= 0;
else
cnt_h <= cnt_h + 1;
end
end
assign add_cnt_h = 1;
assign end_cnt_h = add_cnt_h && cnt_h==H_TOTAL-1;
always @(posedge clk or negedge rst_n) begin
if(!rst_n)
cnt_v <= 0;
else if(add_cnt_v) begin
if(end_cnt_v)
cnt_v <= 0;
else
cnt_v <= cnt_v + 1;
end
end
assign add_cnt_v = end_cnt_h;
assign end_cnt_v = add_cnt_v && cnt_v==V_TOTAL-1;
//==========================================================================
//== VGA端口输出
//==========================================================================
//时钟
assign VGA_clk = clk;
//行场同步
assign VGA_hsync = (cnt_h <= H_SYNC - 1) ? 0 : 1;
assign VGA_vsync = (cnt_v <= V_SYNC - 1) ? 0 : 1;
//==========================================================================
//== 数据请求,提前3拍:行的前一半,隔行
//== 提前3拍,发出读请求给上游
//== 提前2拍,上游读出数据,同时写入ram
//== 提前1拍,发出ram读请求
//== 正常0拍,ram读出数据,赋值给VGA
//==========================================================================
assign VGA_req = (cnt_h >= H_SYNC + H_BACK - 3) && (cnt_h < H_SYNC + H_BACK + H_ADDR/2 - 3) &&
(cnt_v >= V_SYNC + V_BACK ) && (cnt_v < V_SYNC + V_BACK + V_ADDR ) &&
cnt_v[0];
//==========================================================================
//== ram写
//==========================================================================
always @(posedge clk) begin
ram_wr_en <= VGA_req;
end
always @(posedge clk or negedge rst_n) begin
if(!rst_n) begin
ram_wr_addr <= 10'b0;
end
else if(ram_wr_addr == H_ADDR/2 -1)begin
ram_wr_addr <= 10'b0;
end
else if(ram_wr_en) begin
ram_wr_addr <= ram_wr_addr + 1'b1;
end
end
assign ram_wr_data = VGA_din;
//==========================================================================
//== ram
//==========================================================================
ram_24x1024 u_ram_24x1024
(
.clock (clk ),
.wren (ram_wr_en ),
.wraddress (ram_wr_addr ),
.data (ram_wr_data ),
//---------------------------------------------------
.rden (ram_rd_en ),
.rdaddress (ram_rd_addr ),
.q (ram_rd_data )
);
//==========================================================================
//== ram读
//==========================================================================
assign ram_rd_en = (cnt_h >= H_SYNC + H_BACK - 1) && (cnt_h < H_SYNC + H_BACK + H_ADDR - 1) &&
(cnt_v >= V_SYNC + V_BACK ) && (cnt_v < V_SYNC + V_BACK + V_ADDR );
always @(posedge clk or negedge rst_n) begin
if(!rst_n) begin
addr_cnt <= 10'b0;
end
else if(addr_cnt == H_ADDR-1)begin
addr_cnt <= 10'b0;
end
else if(ram_rd_en) begin
addr_cnt <= addr_cnt + 1'b1;
end
end
assign ram_rd_addr = {1'b0,addr_cnt[9:1]};
//==========================================================================
//== VGA数据输出
//==========================================================================
always @(posedge clk) begin
VGA_de <= ram_rd_en;
end
assign VGA_data = VGA_de ? ram_rd_data : 24'b0;
endmodule
3、实现效果
这次就不上板了,用 Matlab 和 Modelsim 联合仿真弄一下吧。

仿真结果表明,处理后的图片比处理前的像素点放大了两倍,图像显示也是正确的。关于仿真平台可以看我另一篇博客《Modelsim联合Matlab搭建FPGA图像仿真平台》。
参考资料:
[1] OpenS Lee:FPGA开源工作室(公众号)
[2] 张铮, 王艳平, 薛桂香. 数字图像处理与机器视觉[M]. 人民邮电出版社, 2010.

浙公网安备 33010602011771号