文章目录

本文对图像拼接中所用到的旋转进行学习

前言

FPGA实现图像处理算法的时候,我们要将算法开发与FPGA分离,也就是将Matlab测试成功的算法,在硬件FPGA上映射。

学习目标

本文的学习目标是实现任意角度的图像旋转,通常基于FPGA旋转给出的案例均是水平垂直类型的,比如水平旋转90,180,270等,这种旋转方式的旋转矩阵都是整数且旋转后图像在屏幕上占据的显示区域也没有发生变换,更易于实现。

学习内容

1、了解图像旋转算法
2、用Matlab进行图像旋转
3、将算法映射到FPGA进行实现

图像旋转算法

图像旋转

图像旋转是指图像以某点为中心,转动一定角度的过程,旋转后的图像仍保持原始尺寸。图像旋转后图像的水平对称轴、垂直对称轴及中心坐标原点都可能会发生变换,因此需要对图像旋转中的坐标进行相应转换。

旋转变换矩阵的推导

因此最终的旋转矩阵:

其中[x’,y‘]为旋转后的坐标,而[x,y]为原始坐标。但这种是以原点进行旋转,所以没有偏差的。

以中心点进行旋转的矩阵

已知图像的长宽为Image_W,Image_H。因此其中心点坐标为
(Image_W / 2,Image_H / 2)


因此旋转矩阵就要进行变形

首先以顶点进行旋转的时候矩阵如下,但是如果以中心点旋转,那么[x’,y’]表示原图像坐标减去原图像中心点之差,而[x,y]表示旋转后的图像坐标减去旋转后的图像中心点之差。(旋转前面中心点不会变)

因此最终以中心点进行变换的矩阵如下:

旋转后尺寸矩阵

水平旋转90,180,270等,这种旋转方式的旋转矩阵都是整数且旋转后图像在屏幕上占据的显示区域也没有发生变换。但是对与60度以及30度等旋转角度来说,旋转之后的图像无法在之前所固定的特点区域显示,会出现显示不完全的情况,因此旋转后的尺寸矩阵也是可以根据如下进行计算的:
已知图像的长宽为Image_W,Image_H,旋转角度β,旋转后尺寸Image_W1,Image_H1.

Matlab实现图像60度旋转

根据 theta=pi/6; 即可修改角度,实现任意角度的旋转

%函数功能:以图像中心右转函数,插值方法:双线性插值
clear all;close all; clc;

src=imread('100.bmp');  %读入原图像;
figure(1); %原图
imshow(src);
theta=pi/6;   %规定旋转角度
[m,n,q]=size(src)     %计算原图像的大小
A=[cos(theta),sin(theta);sin(theta),cos(theta)]; %计算图像旋转后尺寸的变换矩阵
C=[m,n]*A;                                  %计算图像旋转后大小
m2=ceil(C(1))+1;n2=ceil(C(2))+1;                   %取整
dst=zeros(m2,n2,q);                             %构造旋转后图像大小的空矩阵

rx=ceil(m/2);ry=ceil(n/2);                       %原图像中心坐标
rx1=ceil(m2/2);ry1=ceil(n2/2);                       %旋转后图像中心坐标

B=[cos(theta),-sin(theta);sin(theta),cos(theta)];   %坐标映射矩阵B,旋转矩阵

%历遍原图像里每一个点,计算对应的旋转后图像的坐标,并对目标图像赋值%
for i=1:m
                          %纵坐标距离中心的距离
    for j=1:n 
         x1=i-rx;   
        y1=j-ry;                        %横坐标距离中心的距离
        D=[x1,y1]*B;                %进行坐标变换
        y=ceil(D(2)+ry1)+1;                %算出新图像下坐标(加上偏差(中心坐标))
        x=ceil(D(1)+rx1)+1;
        for z=1:q
            dst(x,y,z)=src(i,j,z);        %RGB都赋值
        end
    end
end
dst=uint8(dst);
figure(2); &未插值的旋转图片
imshow(dst);

%双线性插值
for i=2:m2-1
                          
    for j=2:n2-1
        for z=1:3
          if(dst(i,j,z) == 0 && dst(i,j-1,z) ~= 0 && dst(i,j+1,z) ~= 0)  
               dst(i,j,z) =dst(i,j-1,z)  ;
         end
         end
    end
  end
dst=uint8(dst);
figure(3);  %双线性插值后的旋转图片
imshow(dst);

FPGA实现图像旋转

旋转矩阵的计算

通过Matlab验证,可知如下的图像旋转矩阵是可行的,因此映射到FPGA上,即可实现不同角度的图像旋转。
该公式是以逆时针旋转推导的,因此右旋转的计算应该注意。

如下进行几个角度的计算:右旋转90,180,60度(右转90度相当于正常逆时针旋转270),同时假设图像的长宽均为100.


右转180度,也相当于左旋转180度


右转60度,相当于左旋180+90+30度

负数的处理

通过上述矩阵可看出,涉及到减法的情况,因此可能出现负数,FPGA一般处理的都是无符号数,因此进行如下的负数改进:

若 Y = - X ,则改进为 Y = Image_W - X

比如说旋转180度的时候如下,我们即可将 x’ = 100 - x


代码如下:

 rotate_x = (IMG_WIDTH-1) - x;
 rotate_y = (IMG_HEIGHT-1) - y;

浮点数的处理

旋转90、180等度数时,正余弦值为整数,但是当旋转60等度数的时候,则会出现1/2的情况,但FPGA不擅长处理浮点数,因此我们需要采用扩大缩小倍数(移位)的方式来实现。
根据旋转60度的矩阵变换可得到:


浮点数处理:


代码如下:

        rotate_x = (128*(cnt_col-50) + 443*(cnt_row-50) + 256*50) >> 8 ;
        rotate_y = (128*(cnt_row-50)-443*(cnt_col-50) + 256*50) >> 8;

整体模块设计

分析完旋转算法后,我们即可对整体模块进行搭建。主要分为 PLL锁相环、按键控制模块、VGA驱动、图像旋转处理(VGA_dispaly)


PLL锁相环生成VGA 25M时钟
按键控制模块用来控制不同的旋转角度
VGA_display:内部例化了ram IP核,存储了一张100_100的mif图片,同时还包含图像旋转算法。

图像旋转的verilog代码

VGA_display模块的代码如下:

module VGA_display
(
		 input   wire                 clk                 , //时钟,25Mhz
		 input   wire                 rst_n               , //复位,低电平有效
		 input   wire    [ 9:0]       VGA_x               , //像素点横坐标
		 input   wire    [ 9:0]       VGA_y               , //像素点纵坐标
		 output  wire    [15:0]       VGA_data  ,            //输出图像数据
		 input                        key_vld,
		 input                        hS,
		 input                        vS,
		 input   wire                 blank,
		 output  reg                  HS_time,
		 output  reg                  VS_time,
		 output  reg                  BLANK_time

);

parameter IMG_x             = 10'd270           ; //图片起始横坐标
parameter IMG_y             = 10'd190           ; //图片起始纵坐标
parameter IMG_WIDTH         = 10'd100           ; //图片宽度
parameter IMG_HEIGHT        = 10'd100           ; //图片高度
parameter IMG_TOTAL         = 14'd10000         ; //图案区域总像素个数

//========================< 信号 >==========================================
reg                [13:0]   addr                ; //读ram地址
reg                [13:0]   rd_addr;
wire                        add_addr            ; 
wire                        end_addr            ; 
wire                        rd_en               ; //读ram使能信号
reg                [ 9:0]   cnt_col;
reg                [ 9:0]   cnt_row;
reg                [ 9:0]   rotate_x;
reg                [ 9:0]   rotate_y;
reg                         rd_en_r             ; //读ram数据有效信号
wire               [15:0]   data                ;
reg                [2:0]    mode;

//从存储器中读取数据需要花费一个clk,因此对VGA的有效信号大一拍,与读出数据保持同步
    always @(posedge clk, negedge rst_n) begin
        if(!rst_n) begin
            HS_time <= 0;
            VS_time <= 0;
            BLANK_time <= 0;
        end 
		  else begin
            HS_time <= hS;
            VS_time <= vS;
            BLANK_time <= blank;
        end
    end

//显示位置,当前像素点坐标位于图案显示区域内时,读ram使能信号拉高
assign rd_en = (VGA_x >= IMG_x) && (VGA_x < IMG_x+IMG_WIDTH ) &&
               (VGA_y >= IMG_y) && (VGA_y < IMG_y+IMG_HEIGHT)
               ? 1'b1 : 1'b0;

//地址
always @(posedge clk or negedge rst_n) begin
    if(!rst_n)
        addr <= 'd0;                            //复位,读地址为0
    else if(add_addr) begin                     
        if(end_addr)                            
            addr <= 'd0;                        //当end_addr为1,读完了,读地址置为0
        else
            addr <= addr + 1'b1;                //每次读ROM操作后,读地址加1
    end
end

assign add_addr = rd_en;                       
assign end_addr = add_addr && addr== IMG_TOTAL - 1;


//行计数
//---------------------------------------------------
always @(posedge 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== IMG_HEIGHT  -10'd1;

//列计数
//---------------------------------------------------
always @(posedge 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== IMG_WIDTH-10'd1;

//==========================================================================
//==    旋转操作,读地址重规划
//==========================================================================
always @(posedge clk or negedge rst_n) begin
    if(!rst_n) begin
        mode <= 3'b0;
    end
    else if(key_vld) begin
        mode <= mode + 1'b1;
    end
end

always @(*) begin
    case(mode)
        3'b000   : begin                                     //原图
                    rotate_x = cnt_col; //相当于x坐标
                    rotate_y = cnt_row;
                  end
        3'b001   : begin                                     //水平镜像(右旋90)
                    rotate_x = cnt_row;
                    rotate_y = (IMG_WIDTH-1) - cnt_col;
                  end
        3'b010   : begin                                     //垂直镜像(右旋180)
                    rotate_x = (IMG_WIDTH-1) - cnt_col;
                    rotate_y = (IMG_HEIGHT-1) - cnt_row;
                  end
        3'b011   : begin                                     //水平垂直镜像(右旋270)
                    rotate_x = (IMG_HEIGHT-1) - cnt_row;
                    rotate_y = cnt_col;
                  end
		 
        3'b100   : begin                                     //右旋转60度
                    rotate_x = (128*(cnt_col-50) + 443*(cnt_row-50) + 256*50) >> 8 ;
                    rotate_y = (128*(cnt_row-50)-443*(cnt_col-50) + 256*50) >> 8;

                  end						
        3'b101   : begin                                     //右旋转45度
                    rotate_x = (181*(cnt_col-50) + 181*(cnt_row-50) + 256*50) >> 8 ;
                    rotate_y = (181*(cnt_row-50)-181*(cnt_col-50) + 256*50) >> 8;

                  end

						default : begin
                    rotate_x = cnt_col;
                    rotate_y = cnt_row;
                  end
    endcase
end

always @(*) begin
    rd_addr = (rotate_y*IMG_WIDTH ) + rotate_x;

end

//==========================================================================
//==    数据输出,地址到数据有一拍的延迟
//==========================================================================
always @(posedge clk or negedge rst_n) begin         
    if (!rst_n) 
        rd_en_r <= 1'b0;
    else
        rd_en_r <= rd_en;             
end

assign VGA_data = rd_en_r ? data : 16'd1111111111111111;  //从ROM中读出的图像数有效时,将其输出显示
//==========================================================================
//==    ram例化
//==========================================================================
//图像是存储在了ram中,因此无需对写地址写使能进行连线
ram ram (
	.clock(clk),
	.data(),
	.rdaddress(rd_addr),
	.rden(rd_en_r),
	.wraddress(),
	.wren(),
	.q(data)
	);

	
endmodule

结果如下:
原图


右旋90


右旋180


右旋45


右旋60


从结果可看出,水平垂直的旋转正确,但是45,60度的旋转出现了区域显示错乱的问题,同时图像变得更模糊,旋转角度的计算是没有问题的,通过黑色部分也能得到角度分别是45,60度。
解决方法:
对于错误:我们要给定具体的显示区域,使得能得到如下的效果如下:
如下显示方式用FPGA实现比较麻烦,对于后面的图像拼接来说,一般是将扭曲的图像摆正,因此我们继续对验证显示,只是验证了旋转算法的正确性。


图像模糊:可以采用双线性插值

收获

对于旋转算法部分,重复的乘法和加法、移位操作可以采用流水线的方式来做。
对于浮点数的处理,采用了扩大缩小倍数的方式,一定程度上可以解决浮点数的问题,但是却对精度有着影响,比如0.5扩大256倍是整数,而1.78扩大256倍仍然浮点数,就要四舍五入。
另外对于三角函数的运算来说,并不适用于FPGA处理,因此可以学习CORDIC算法
CORDIC算法原理的原理就是该算法通过基本的加和移位运算代替乘法运算,使得矢量的旋转和定向的计算不再需要三角函数、乘法、开方、反三角、指数等函数。