镜像变换又分为水平镜像和竖直镜像。水平镜像即将图像左半部分和右半部分以图像竖直中轴线为中心轴进行对换;而竖直镜像则是将图像上半部分和下半部分以图像水平中轴线为中心轴进行对换,如图所示。
水平镜像的变换关系为:
对矩阵求逆得到:
竖直镜像的变换关系为:
对矩阵求逆得到:
一、MATLAB实现
1、函数法实现
%-------------------------------------------------------------------------- % 函数法镜像 %-------------------------------------------------------------------------- clc; clear all; RGB = imread('monkey.jpg'); %读取图像 [ROW,COL,N] = size(RGB); tform1 = maketform('affine',[-1 0 0;0 1 0;COL 0 1]); %定义水平镜像变换矩阵 tform2 = maketform('affine',[1 0 0;0 -1 0;0 ROW 1]); %定义垂直镜像变换矩阵 tform3 = maketform('affine',[-1 0 0;0 -1 0;COL ROW 1]); %定义水平垂直镜像变换矩阵 H_mirror = imtransform(RGB,tform1,'nearest'); V_mirror = imtransform(RGB,tform2,'nearest'); HV_mirror = imtransform(RGB,tform3,'nearest'); subplot(2,2,1),imshow(RGB); title('原图'); subplot(2,2,2),imshow(H_mirror); title('水平镜像'); subplot(2,2,3),imshow(V_mirror); title('垂直镜像'); subplot(2,2,4),imshow(HV_mirror);title('水平垂直镜像');
参数 transformtype指定了变换的类型,如常见的 ‘affine’ 为二维或多位仿射变换,包括平移、旋转、比例、拉伸和错切等。
点击运行得到如下结果:
2、公式法实现
光靠函数法还是不行,我们得自己缔造公式来实现。它的原理如下所示:
Q 为输出,I 为输入,Xt 和Yt 为像素坐标,width 和 height 为图像宽度和高度。因此镜像算法就是讲输入坐标和图像的宽度高度做减法得到输出坐标,同时由于减法的结果必然小于被减数,固这实际上是单纯的无符号数的减法。
由此我们得到如下 MATLAB 代码:
%-------------------------------------------------------------------------- % 公式法镜像 %-------------------------------------------------------------------------- clc; clear all; RGB = imread('monkey.jpg'); %读取图像 [ROW,COL,N] = size(RGB); H_mirror = uint8(zeros(ROW, COL,N)); %Horizontal mirror V_mirror = uint8(zeros(ROW, COL,N)); %Vertical mirror HV_mirror = uint8(zeros(ROW, COL,N)); %H&V miirror %水平镜像 for i =1:ROW for j=1:COL for k=1:N x = i; y = COL-j+1; z = k; H_mirror(x,y,z) =RGB(i,j,k); end end end %垂直镜像 for i =1:ROW for j=1:COL for k=1:N x = ROW-i+1; y = j; z = k; V_mirror(x,y,z) =RGB(i,j,k); end end end %水平垂直镜像 for i =1:ROW for j=1:COL for k=1:N x = ROW-i+1; y = COL-j+1; z = k; HV_mirror(x,y,z) =RGB(i,j,k); end end end subplot(2,2,1),imshow(RGB); title('原图'); subplot(2,2,2),imshow(H_mirror); title('水平镜像'); subplot(2,2,3),imshow(V_mirror); title('垂直镜像'); subplot(2,2,4),imshow(HV_mirror);title('水平垂直镜像');
点击运行得到如下结果:
和函数法的结果一致,表明我们的公式法是可行的。
二、FPGA实现
镜像需要一整帧的像素进行坐标转换,因此必须对一帧图片进行缓存,假设一个缓存器,我们可以通过公式计算到变换后的坐标和数值的对应关系,逐渐的写入缓存器,而后就可以直接从缓存区顺序读出,最后的结果就是镜像的了。
缓存器件比较常用的有FIFO、RAM、SDRAM、DDR2、DDR3等,由于FIFO没有地址的概念,所以首先排除,而SDRAM和DDR2、DDR3的接口较复杂,因此本次设计采用 RAM 来缓存一帧图像,RAM容量小,我选择了缓存 140x140x16bit 的图像,再增大我的FPGA芯片就支持不住了。虽然图片小,但足以验证算法,如果后续遇到实际项目需要大分辨率的图片,再考虑使用 SDRAM 或 DDR2、DDR3 也是可以的。
1、端口信号
本次设计输入是串口,输出是TFT屏,因此本模块共有读写两个不同的时钟。本次设计用到坐标变换,因此采用之前在博客《协议——VGA》中写好的 TFT 屏驱动程序,将坐标信号引进来,其他则没什么特别的:
module Mirror //========================< 端口 >========================================== ( //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 //输出图像数据 );
2、参数设计
本工程的 TFT 屏是 480x272 的,而图片我选择的是 140x140,我希望图片能显示在 TFT 屏的中间,这样更好看,当然还有图片的长度和高度也都用参数化的形式表示。
parameter COL = 10'd140 ; //图片长度 parameter ROW = 10'd140 ; //图片高度 parameter IMG_x = 10'd170 ; //图片起始横坐标 parameter IMG_y = 10'd66 ; //图片起始纵坐标
3、缓存buffer写
前面说到缓存器可以用 RAM ,但在 OpenS Lee 的源程序中,我学到了一种新的方法来替代RAM:二维数组。二维数组用的好就和RAM没有区别,而且比RAM要省事,毕竟RAM还要我们鼠标点击去生成IP核,又要例化,还是有点麻烦。
首先是定义: reg [15:0] buffer[COL*ROW-1:0] ; //类似RAM ,这样就相当于申请了一块RAM,不过形式是数组。
接着我们要对这个数组进行写操作,特别注意的是读写操作是不同的时钟。
写是直接写,没有什么猫腻,写进去就行,时钟为写时钟。
//========================================================================== //== 缓存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
4、读使能
本来是没有读使能的,这里设置一个读使能,目的在于确定图像在TFT屏中的显示位置,此外给下面的行列规划提供加一条件。
//读使能,确定显示位置 //--------------------------------------------------- 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;
5、行列规划
//行计数 //--------------------------------------------------- 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;
6、镜像操作
通过公式计算镜像后的坐标,这是本篇博客的核心代码,有4种不同的模式,通过按键调节模式变化,采用case语句进行判断,时钟是读时钟。
//========================================================================== //== 镜像操作,读地址重规划 //========================================================================== always @(posedge rd_clk or negedge rst_n) begin if(!rst_n) begin mode <= 2'b00; end else if(key_vld) begin mode <= mode + 1'b1; end end always @(*) begin case(mode) 2'b00 : begin //原图 mirror_x = cnt_col; mirror_y = cnt_row; end 2'b01 : begin //水平镜像 mirror_x = (COL-1) - cnt_col; mirror_y = cnt_row; end 2'b10 : begin //垂直镜像 mirror_x = cnt_col; mirror_y = (ROW-1) - cnt_row; end 2'b11 : begin //水平垂直镜像 mirror_x = (COL-1) - cnt_col; mirror_y = (ROW-1) - cnt_row; end default : begin mirror_x = cnt_col; mirror_y = cnt_row; end endcase end
7、缓存buffer读
//========================================================================== //== 缓存buffer,读操作 //========================================================================== always @(posedge rd_clk or negedge rst_n) begin if(!rst_n) rd_addr <= 'd0; else rd_addr <= mirror_y * COL + mirror_x; end always @(posedge rd_clk) begin rd_en_r <= rd_en; end assign TFT_data = rd_en_r ? buffer[rd_addr] : 16'hffff;
注意时钟,全程都要注意。这个buffer同时解决了串口和TFT屏之间跨时钟域的问题。
三、上板验证
总共4种模式,模式0、1、2、3 分别得到如下结果:
把这4副图也按MATLAB的样子拼到一块看看吧:
和上面的 MATLAB 实验结果对比,可以看到此次图像镜像实验成功。
视频演示如下:
参考资料:
[1] OpenS Lee:FPGA开源工作室(公众号)
[2] 张铮, 王艳平, 薛桂香. 数字图像处理与机器视觉[M]. 人民邮电出版社, 2010.