镜像变换又分为水平镜像和竖直镜像。水平镜像即将图像左半部分和右半部分以图像竖直中轴线为中心轴进行对换;而竖直镜像则是将图像上半部分和下半部分以图像水平中轴线为中心轴进行对换,如图所示。

水平镜像的变换关系为:

对矩阵求逆得到:

竖直镜像的变换关系为:

对矩阵求逆得到:

一、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 mirroring V_mirror = uint8(zeros(ROW, COL,N)); %Vertical mirroring HV_mirror = uint8(zeros(ROW, COL,N)); %H&V miirroring %水平镜像 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 , //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、参数设计
本次算法加上原图来说一共有4种不同的结果,我们可以通过在端口设置一个信号mode来传递不同的选择,也可以直接在顶层参数化MODE:
`define MODE 2 //0原图,1水平镜像,2垂直镜像,3水平垂直镜像
本工程的 TFT 屏是 480x272 的,而图片我选择的是 140x140,我希望图片能显示在 TFT 屏的中间,这样更好看,当然还有图片的长度和高度也都用参数化的形式表示。
parameter COL = 10‘d140 ; //图片长度 parameter ROW = 10‘d140 ; //图片高度 parameter IMG_x = 10‘d170 ; //图片起始横坐标 parameter IMG_y = 10‘d66 ; //图片起始纵坐标
3、行列规划
//========================================================================== //== 行列划分 //========================================================================== always @(posedge wr_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 = din_vld; assign end_cnt_col = add_cnt_col && cnt_col== COL-10‘d1; always @(posedge wr_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;
4、镜像算法
通过公式计算镜像后的坐标,这是本篇博客的核心代码,有4种不同的模式,因此采用case语句进行判断:
//========================================================================== //== 镜像 //========================================================================== always @(*) begin case(`MODE) ‘d0 : begin //原图 mirror_cnt_col = cnt_col; mirror_cnt_row = cnt_row; end ‘d1 : begin //水平镜像 mirror_cnt_col = (COL-1) - cnt_col; mirror_cnt_row = cnt_row; end ‘d2 : begin //垂直镜像 mirror_cnt_col = cnt_col; mirror_cnt_row = (ROW-1) - cnt_row; end ‘d3 : begin //水平垂直镜像 mirror_cnt_col = (COL-1) - cnt_col; mirror_cnt_row = (ROW-1) - cnt_row; end default : begin mirror_cnt_col = cnt_col; mirror_cnt_row = cnt_row; end endcase end
5、RAM或数组缓存一帧
前面说到缓存器可以用 RAM ,但在 OpenS Lee 的源程序中,我学到了一种新的方法来替代RAM:二维数组。二维数组用的好就和RAM没有区别,而且比RAM要省事,毕竟RAM还要我们鼠标点击去生成IP核,又要例化,毕竟麻烦。
首先是定义: reg [15:0] buffer[COL*ROW-1:0] ; //类似RAM ,这样就相当于申请了一块RAM,不过形式是数组。
接着我们要对这个数组进行读写操作,特别注意的是读写操作是不同的时钟:
//========================================================================== //== 缓存buffer,作用类似RAM //========================================================================== //写地址 //--------------------------------------------------- always @(posedge wr_clk or negedge rst_n) begin if(!rst_n) wr_addr <= ‘d0; else if(din_vld) wr_addr <= mirror_cnt_row * COL + mirror_cnt_col; end //写数据 //--------------------------------------------------- always @(posedge wr_clk) begin buffer[wr_addr] <= din; 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) rd_addr <= ‘d0; else if(rd_addr==COL*ROW-1) rd_addr <= ‘d0; else if(rd_en) rd_addr <= rd_addr + 1‘b1; end //读数据 //--------------------------------------------------- always @(posedge rd_clk or negedge rst_n) begin if(!rst_n) rd_data <= ‘d0; else if(rd_en) rd_data <= buffer[rd_addr]; end
调试完这段代码,我发现数组真的和 RAM 很像。顺便说一句,上面的 rd_en 信号的产生是使得图像最终能显示在 TFT 屏的中间,不懂的话就回去再研究研究 VGA 时序吧。
6、数据输出,打拍对齐
最后是打拍对齐,并且将中间 140x140 以外的地方显示成白色,更好看些:
//========================================================================== //== 数据输出,打拍对齐 //========================================================================== always @(posedge rd_clk) begin rd_en_r <= rd_en; end assign TFT_data = rd_en_r ? rd_data : 16‘hffff;
三、上板验证
总共4种模式,模式0、1、2、3 分别得到如下结果:




把这4副图也按MATLAB的样子拼到一块看看吧:

和上面的 MATLAB 实验结果对比,可以看到此次图像镜像实验成功。
参考资料:
[1] OpenS Lee:FPGA开源工作室(公众号)
[2] 张铮, 王艳平, 薛桂香. 数字图像处理与机器视觉[M]. 人民邮电出版社, 2010.
原文:https://www.cnblogs.com/xianyufpga/p/12499406.html