1. 概述
本文介绍基于 FPGA 的任意角度图像旋转实现。通过 Cordic 算法获取正余弦数值,结合坐标变换公式完成像素映射。

2. 图像旋转代码设计思路
2.1 旋转后的图像尺寸
在一副图像经过旋转后,原本像素的位置肯定会发生变化,图像总的面积虽然保持不变但是各别位置的尺寸会改变。比如一副 100x100 像素的图像进行旋转,我们只需要获得它的最长距离也就是对角线的尺寸作为旋转后的图像的显示范围。这样无论怎样旋转都能完整显示图像。
如下代码,Pixel_X 和 Pixel_Y 为旋转后图像的尺寸。ROW 和 COL 为原始图像尺寸,利用勾股定理求出对角线的值即可。
reg [12:0] row_size;
reg [12:0] col_size;
assign Pixel_X = row_size;
assign Pixel_Y = col_size;
wire [31:0] cosout_abs = (cosout[31]) ? -cosout : cosout;
wire [31:0] sinout_abs = (sinout[31]) ? -sinout : sinout;
always @(posedge clk_i, negedge rstn_i) begin
if (!rstn_i) begin
row_size <= 'd0;
col_size <= 'd0;
end else begin
// h --> row
// w --> col
row_size <= (ROW*cosout_abs + COL*sinout_abs) >> 14;
col_size <= (COL*cosout_abs + ROW*sinout_abs) >> 14;
end
end
2.2 旋转后图像的有效位置
这个旋转后的有效位置可以自由设定,这里设定为屏幕中心处的位置。具体设计如下:
使用一块 480*272 的 LCD 屏幕,具体时序参数如下:
//parameter define localparam
localparam H_SYNC = 11'd41, //行同步
H_BACK = 11'd2, //行时序后沿
H_LEFT = 11'd0, //行时序左边框
H_VALID = 11'd480, //行有效数据
H_RIGHT = 11'd0, //行时序右边框
H_FRONT = 11'd2, //行时序前沿
H_TOTAL = 11'd525; //行扫描周期
localparam V_SYNC = 11'd10, //场同步
V_BACK = 11'd2, //场时序后沿
V_TOP = 11'd0, //场时序左边框
V_VALID = 11'd272, //场有效数据
V_BOTTOM = 11'd0, //场时序右边框
V_FRONT = 11'd2, //场时序前沿
V_TOTAL = 11'd286; //场扫描周期
//cnt_h:行扫描计数器
//cnt_v:场扫描计数器
//data_req:数据请求信号
wire data_req = (((cnt_h >= (((H_VALID - Pixel_X)>>1) + H_SYNC + H_BACK - 'd5)) && (cnt_h < (((H_VALID - Pixel_X)>>1) + Pixel_X + H_SYNC + H_BACK - 'd5))) &&((cnt_v >= ((V_VALID - Pixel_Y)>>1) + V_SYNC + V_BACK - 'd5) && ((cnt_v < (((V_VALID - Pixel_Y)>>1) + Pixel_Y + V_SYNC + V_BACK - 'd5)))));
2.3 第一级流水线
在图像有效信号有效时进行行场计数。
always @(posedge clk_i, negedge rstn_i) begin
if (!rstn_i) r_rotate_valid <= 1'b0;
else r_rotate_valid <= data_req;
end
always @(posedge clk_i, negedge rstn_i) begin
if (!rstn_i) r_rotate_end <= 'd0;
else if (r_rotate_valid && (vcnt == row_abs - 1) && (hcnt == col_abs - 2)) r_rotate_end <= 1'b1;
else r_rotate_end <= 'd0;
end
always @(posedge clk_i, negedge rstn_i) begin
if (!rstn_i) hcnt <= 'd0;
else if (r_rotate_valid && (r_rotate_end || (hcnt == col_abs - 1))) hcnt <= 'd0;
else if (r_rotate_valid) hcnt <= hcnt + 'd1;
end
always @(posedge clk_i, negedge rstn_i) begin
if (!rstn_i) vcnt <= 'd0;
else if (r_rotate_valid && r_rotate_end) vcnt <= 'd0;
else if (r_rotate_valid && (hcnt == col_abs - 1)) vcnt <= vcnt + 'd1;
end
2.4 第二级流水
这里开始计算图像旋转公式。以中心点为起始坐标,相当于坐标系的 (0,0) 点,这样四个象限的所有坐标点都可以表示出来。并且按照上图的公式组合起来即可,最终右移 14 位。

reg signed [12:0] x_cos;
reg signed [12:0] y_sin;
reg signed [12:0] y_cos;
reg signed [12:0] x_sin;
assign row_abs = row_size;
assign col_abs = col_size;
// 得到旋转后图片的中点
assign row1 = row_abs >> 1;
assign col1 = col_abs >> 1;
always @(posedge clk_i, negedge rstn_i) begin
if (!rstn_i) x_cos <= 'd0;
else if(r_rotate_valid_1d) x_cos <= ((hcnt - col1 ) * cosout) >>>14;
else x_cos <= x_cos;
end
always @(posedge clk_i, negedge rstn_i) begin
if (!rstn_i) y_sin <= 'd0;
else if(r_rotate_valid_1d) y_sin <= ((vcnt-row1 ) * sinout) >>>14;
else y_sin <= y_sin;
end
always @(posedge clk_i, negedge rstn_i) begin
if (!rstn_i) y_cos <= 'd0;
else if(r_rotate_valid_1d) y_cos <= ((vcnt - row1 ) * cosout) >>>14;
else y_cos <= y_cos;
end
always @(posedge clk_i, negedge rstn_i) begin
if (!rstn_i) x_sin <= 'd0;
else if(r_rotate_valid_1d) x_sin <= ((hcnt - col1 ) * sinout) >>>14;
else x_sin <= x_sin;
end
2.5 第三级流水
同样的,按照公式进行排列组合得出旋转后图像映射到原始图像的坐标位置。在这里有几个可能不好理解的地方:原始屏幕坐标 (hcnt, vcnt) 转换到中心坐标系(减去 COL/2 和 ROW/2)-> 中心坐标 (hcnt - COL/2, vcnt - ROW/2) -> 应用旋转公式 -> 旋转后的中心坐标 -> 转换回屏幕坐标系(加上 COL/2 和 ROW/2)。
always @(posedge clk_i, negedge rstn_i) begin
if (!rstn_i) r_rotate_valid_2d <= 'd0;
else r_rotate_valid_2d <= r_rotate_valid_1d;
end
always @(posedge clk_i, negedge rstn_i) begin
if (!rstn_i) hcnt_rotate <= 'd0;
else if(r_rotate_valid_2d==1'b1) hcnt_rotate <= x_cos - y_sin + (COL>>1 );
else hcnt_rotate <= 'd0;
end
always @(posedge clk_i, negedge rstn_i) begin
if (!rstn_i) vcnt_rotate <= 'd0;
else if(r_rotate_valid_2d==1'b1) vcnt_rotate <= y_cos + x_sin + (ROW>>1);
else vcnt_rotate <= 'd0;
end
2.6 第四级流水线
hcnt_rotate在 0 到 COL-1 之间(在原图像列范围内)vcnt_rotate在 0 到 ROW-1 之间(在原图像行范围内)- 已读取的像素数小于图像总像素数(ROW*COL)
- 每行有 COL 个像素,所以第 vcnt_rotate 行的起始地址是 COL*vcnt_rotate
- 再加上该行内的列偏移 hcnt_rotate
data_cnt 计数器用于限制读取的像素总数不超过原图像的总像素数(ROW*COL)。这是为了防止地址溢出或重复读取。最后连接一个 ROM IP 核,里面存放着预先处理好的图像数据,生成地址和使能信号读就可以了。ROM 读出数据是延迟一个时钟,所以第五级流水就是为了对齐而已。
always @(posedge clk_i, negedge rstn_i) begin
if (!rstn_i) r_rotate_valid_3d <= 'd0;
else r_rotate_valid_3d <= r_rotate_valid_2d;
end
always @(posedge clk_i, negedge rstn_i) begin
if (!rstn_i) begin
rden <= 'd0;
addra <= 'd0;
end else if(r_rotate_valid_3d==1'b1) begin
if((hcnt_rotate>='d0)&&(hcnt_rotate<COL)&&(vcnt_rotate>='d0)&&(vcnt_rotate<ROW)&&data_cnt<ROW*COL) begin
rden <= 1'b1;
addra<= COL*vcnt_rotate + hcnt_rotate;
end else begin
rden <= 1'b0;
addra<= 'd0;
end
end else begin
rden <= 'd0;
addra <= 'd0;
end
end
always @(posedge clk_i, negedge rstn_i) begin
if (!rstn_i) data_cnt <= 'd0;
else if (data_cnt == ROW*COL - 1) data_cnt <= 'd0;
else if (r_rotate_valid_3d && (hcnt_rotate>='d0)&&(hcnt_rotate<COL)&&(vcnt_rotate>='d0)&&(vcnt_rotate<ROW)) data_cnt <= data_cnt + 'd1;
end
img_mem_gen img_mem_gen_inst (
.address ( addra ),
.clock ( clk_i ),
.rden ( rden ),
.q ( rom_data )
);
3. 仿真结果
30°旋转

图片横着看,逆时钟旋转 30°。

-30°


228°


问题不大,任意角度,任意方向,其它的我就不列举了。
4. 结语
目前总的来看功能是正常的,时序和代码架构已验证,可供参考。
