一、概述
图像畸变矫正是图像处理中的关键任务,主要用于修正因镜头畸变或拍摄角度导致的图像失真。其核心在于几何变换,通过数学模型将失真的像素映射回理想位置。
1. 图像畸变的类型
常见的畸变主要分为两类:
- 径向畸变(Radial Distortion):表现为图像中心到边缘的变形。典型如'桶形畸变'(边缘向外膨胀)和'枕形畸变'(边缘向内收缩)。
- 切向畸变(Tangential Distortion):通常由镜头装配不精确引起,导致图像出现不规则的切向偏移。
2. 畸变矫正原理
矫正的目标是恢复图像的几何结构。我们通常采用以下模型进行建模:
(1)径向畸变模型
公式如下:

其中 $r$ 是像素到图像中心的距离,$k_1, k_2$ 为径向畸变系数。
(2)切向畸变模型
切向畸变的矫正公式可表示为:

这里 $p_1$ 和 $p_2$ 是切向畸变参数。
(3)实现顺序
建议先矫正径向畸变,再处理切向畸变。因为径向畸变对几何形状影响更大,优先消除主要偏差能减少后续计算的干扰。
MATLAB 提供了强大的工具箱支持这一过程,一般步骤包括相机标定获取内参和畸变系数,随后利用这些参数进行图像矫正。
二、MATLAB 具体实现
在 FPGA 部署前,通常先用 MATLAB 计算逆向映射表,生成硬件所需的查找表(LUT)。核心逻辑如下:
% 读取畸变图像
img = imread('distorted_image.jpg');
[height, width, ~] = size(img);
% 相机标定参数:焦距和图像中心
f_x = 1000; % 水平焦距
f_y = 1000; % 垂直焦距
cx = width / 2; % 水平主点
cy = height / 2; % 垂直主点
% 畸变系数(径向和切向畸变系数)
k1 = -0.2;
k2 = 0.03;
p1 = 0.001;
p2 = -0.001;
% 创建一个空的矫正图像
undistorted_img = zeros(height, width, 3, 'uint8');
% 遍历每个像素点
for i = 1:height
for j = 1:width
% 计算像素点到图像中心的距离
x = j - cx;
y = i - cy;
% 转换到相机坐标系(单位:像素)
x_normalized = x / f_x;
y_normalized = y / f_y;
r = sqrt(x_normalized^2 + y_normalized^2);
% 计算径向畸变
radial_distortion = 1 + k1 * r^2 + k2 * r^4;
% 计算切向畸变
tangential_distortion_x = 2 * p1 * x_normalized * y_normalized + p2 * (r^2 + 2 * x_normalized^2);
tangential_distortion_y = p1 * (r^2 + 2 * y_normalized^2) + 2 * p2 * x_normalized * y_normalized;
% 计算畸变后的坐标(相机坐标系)
x_prime_normalized = x_normalized * radial_distortion + tangential_distortion_x;
y_prime_normalized = y_normalized * radial_distortion + tangential_distortion_y;
% 转换回像素坐标系
x_prime = x_prime_normalized * f_x + cx;
y_prime = y_prime_normalized * f_y + cy;
% 将畸变后的坐标转换为图像坐标系中的整数值
x_prime_img = round(x_prime);
y_prime_img = round(y_prime);
% 检查坐标是否在图像范围内
if x_prime_img >= 1 && x_prime_img <= width && y_prime_img >= 1 && y_prime_img <= height
undistorted_img(y_prime_img, x_prime_img, :) = img(i, j, :);
end
end
end
% 显示矫正后的图像
imshow(undistorted_img);
代码中 fx 和 fy 代表水平和垂直方向的焦距,cx 和 cy 是图像主点。引入焦距是为了确保坐标转换考虑了图像的内参,使归一化坐标与物理焦距保持一致。这段代码展示了如何从畸变图像计算出目标坐标,为 FPGA 提供映射依据。
三、FPGA 实现
1. 流程与优化
FPGA 擅长并行处理和流水线操作,非常适合高帧率、低延迟的实时校正系统。但在硬件上直接计算每个像素的映射关系非常耗时,因此核心策略是逆向映射与查找表(LUT)压缩。
常见做法是在 MATLAB 中预先计算好所有坐标的映射关系,生成'逆向映射表'存入 FPGA 片上存储器(ROM)。工作时,FPGA 只需根据当前像素坐标查表,获得原图对应坐标后进行插值。
挑战与方案: 高清图像的映射表可能超出片上 ROM 容量。解决方案是采用稀疏网格存储技术,只存储部分网格点的映射值,运行时通过线性插值电路在线重建任意像素的完整映射坐标,大幅降低存储需求。
典型的 FPGA 校正流水线包含:图像缓存(FIFO)、坐标生成器、映射表查找与插值、像素插值计算、输出同步等模块,确保多像素同时在不同阶段被处理。
2. 关键技术
- 稀疏网格存储:仅存储关键点,运行时插值。
- 定点量化:浮点数转定点数,适配 FPGA 运算。
- MIF 格式输出:直接生成 FPGA ROM 初始化文件。
3. 映射表压缩效果示例

4. 存储优化方案对比

注意: (a) 精度平衡:网格大小建议在 8×8 到 32×32 之间,需测试不同值对图像质量的影响; (b) FPGA 实现:双线性插值用于重建完整映射; (c) 实时更新:若畸变参数可变,映射表应存储在可重配置的 RAM 中。
5. FPGA 核心代码及仿真
以下是基于状态机的 Verilog 实现片段,展示了读写控制逻辑:
always@(posedge clk) begin
if(rst) begin
status <= IDLE;
ena_odd <= 0;
wea_odd <= 0;
addra_odd <= 16'hffff;
dina_odd <= 0;
enb_odd <= 0;
web_odd <= 0;
addrb_odd <= 16'hffff;
dinb_odd <= 0;
ena_eve <= 0;
wea_eve <= 0;
addra_eve <= 16'hffff;
dina_eve <= 0;
enb_eve <= 0;
web_eve <= 0;
addrb_eve <= 16'hffff;
dinb_eve <= 0;
end else begin
case(status)
IDLE: begin
if(write_str) begin
status <= WRITE;
if(row[0]==0)//从第 0 行开始,0 行为偶数
begin
ena_odd <= 0;
wea_odd <= 0;
addra_odd <= 16'hffff;
dina_odd <= 0;
ena_eve <= 1;
wea_eve <= 1;
addra_eve <= ((row>>1)<<8) + ((row>>1)<<6) + col;
dina_eve <= din_d1;
end
else
begin
ena_odd <= 1;
wea_odd <= 1;
addra_odd <= (((row-1)>>1)<<8) + (((row-1)>>1)<<6) + col;
dina_odd <= din_d1;
ena_eve <= 0;
wea_eve <= 0;
addra_eve <= 16'hffff;
dina_eve <= 0;
end
end else begin
status <= IDLE;
ena_odd <= 0;
wea_odd <= 0;
addra_odd <= 16'hffff;
dina_odd <= 0;
enb_odd <= 0;
web_odd <= 0;
addrb_odd <= 16'hffff;
dinb_odd <= 0;
ena_eve <= 0;
wea_eve <= 0;
addra_eve <= 16'hffff;
dina_eve <= 0;
enb_eve <= 0;
web_eve <= 0;
addrb_eve <= 16'hffff;
dinb_eve <= 0;
end
end
WRITE: begin
if(write_end) begin
status <= DELAY;
ena_odd <= 0;
wea_odd <= 0;
addra_odd <= 16'hffff;
dina_odd <= 0;
ena_eve <= 0;
wea_eve <= 0;
addra_eve <= 16'hffff;
dina_eve <= 0;
end else begin
status <= WRITE;
if(row[0]==0)//从第 0 行开始,0 行为偶数
begin
ena_odd <= 0;
wea_odd <= 0;
addra_odd <= 16'hffff;
dina_odd <= 0;
ena_eve <= 1;
wea_eve <= 1;
addra_eve <= ((row>>1)<<8) + ((row>>1)<<6) + col;
dina_eve <= din_d1;
end
else
begin
ena_odd <= 1;
wea_odd <= 1;
addra_odd <= (((row-1)>>1)<<8) + (((row-1)>>1)<<6) + col;
dina_odd <= din_d1;
ena_eve <= 0;
wea_eve <= 0;
addra_eve <= 16'hffff;
dina_eve <= 0;
end
end
end
DELAY: begin
if(dly_cnt == 32) begin
status <= READ;
dly_cnt <= 0;
end else begin
status <= DELAY;
dly_cnt <= dly_cnt + 1;
end
end
READ: begin
if((row_rd == 10'd255) && (col_rd == 319)) begin
status <= DELAY1;
col_rd <= col_rd;
row_rd <= row_rd;
end else begin
status <= READ;
if(col_rd == 319) begin
row_rd <= row_rd + 1;
col_rd <= 0;
end else begin
row_rd <= row_rd;
col_rd <= col_rd + 1;
end
end
if(v1[0] == 0) begin
ena_odd <= 1;
wea_odd <= 0;
addra_odd <= ((((v2>>6)-1)>>1)<<8) + ((((v2>>6)-1)>>1)<<6) + (u1>>6);
enb_odd <= 1;
web_odd <= 0;
addrb_odd <= ((((v2>>6)-1)>>1)<<8) + ((((v2>>6)-1)>>1)<<6) + (u2>>6);
ena_eve <= 1;
wea_eve <= 0;
addra_eve <= (((v1>>6)>>1)<<8) + (((v1>>6)>>1)<<6) + (u1>>6);
enb_eve <= 1;
web_eve <= 0;
addrb_eve <= (((v1>>6)>>1)<<8) + (((v1>>6)>>1)<<6) + (u2>>6);
end else begin
if(v1 == 16320)//255<<6
begin
ena_odd <= 1;
wea_odd <= 0;
addra_odd <= ((((v1>>6)-1)>>1)<<8) + ((((v1>>6)-1)>>1)<<6) + (u1>>6);
enb_odd <= 1;
web_odd <= 0;
addrb_odd <= ((((v1>>6)-1)>>1)<<8) + ((((v1>>6)-1)>>1)<<6) + (u2>>6);
ena_eve <= 1;
wea_eve <= 0;
addra_eve <= ((((v2>>6)-1)>>1)<<8) + ((((v2>>6)-1)>>1)<<6) + (u1>>6);
enb_eve <= 1;
web_eve <= 0;
addrb_eve <= ((((v2>>6)-1)>>1)<<8) + ((((v2>>6)-1)>>1)<<6) + (u2>>6);
end
else
begin
ena_odd <= 1;
wea_odd <= 0;
addra_odd <= ((((v1>>6)-1)>>1)<<8) + ((((v1>>6)-1)>>1)<<6) + (u1>>6);
enb_odd <= 1;
web_odd <= 0;
addrb_odd <= ((((v1>>6)-1)>>1)<<8) + ((((v1>>6)-1)>>1)<<6) + (u2>>6);
ena_eve <= 1;
wea_eve <= 0;
addra_eve <= (((v2>>6)>>1)<<8) + (((v2>>6)>>1)<<6) + (u1>>6);
enb_eve <= 1;
web_eve <= 0;
addrb_eve <= (((v2>>6)>>1)<<8) + (((v2>>6)>>1)<<6) + (u2>>6);
end
end
end
DELAY1: begin
if(dly_cnt1 == 32) begin
status <= IDLE;
dly_cnt1<= 0;
col_rd <= 0;
row_rd <= 0;
ena_odd <= 0;
wea_odd <= 0;
addra_odd <= 16'hffff;
dina_odd <= 0;
enb_odd <= 0;
web_odd <= 0;
addrb_odd <= 16'hffff;
dinb_odd <= 0;
ena_eve <= 0;
wea_eve <= 0;
addra_eve <= 16'hffff;
dina_eve <= 0;
enb_eve <= 0;
web_eve <= 0;
addrb_eve <= 16'hffff;
dinb_eve <= 0;
end else begin
status <= DELAY1;
dly_cnt1<= dly_cnt1 + 1;
col_rd <= col_rd;
row_rd <= row_rd;
if(v1[0] == 0) begin
ena_odd <= 1;
wea_odd <= 0;
addra_odd <= ((((v2>>6)-1)>>1)<<8) + ((((v2>>6)-1)>>1)<<6) + (u1>>6);
enb_odd <= 1;
web_odd <= 0;
addrb_odd <= ((((v2>>6)-1)>>1)<<8) + ((((v2>>6)-1)>>1)<<6) + (u2>>6);
ena_eve <= 1;
wea_eve <= 0;
addra_eve <= (((v1>>6)>>1)<<8) + (((v1>>6)>>1)<<6) + (u1>>6);
enb_eve <= 1;
web_eve <= 0;
addrb_eve <= (((v1>>6)>>1)<<8) + (((v1>>6)>>1)<<6) + (u2>>6);
end else begin
if(v1 == 16320)//255<<6
begin
ena_odd <= 1;
wea_odd <= 0;
addra_odd <= ((((v1>>6)-1)>>1)<<8) + ((((v1>>6)-1)>>1)<<6) + (u1>>6);
enb_odd <= 1;
web_odd <= 0;
addrb_odd <= ((((v1>>6)-1)>>1)<<8) + ((((v1>>6)-1)>>1)<<6) + (u2>>6);
ena_eve <= 1;
wea_eve <= 0;
addra_eve <= ((((v2>>6)-1)>>1)<<8) + ((((v2>>6)-1)>>1)<<6) + (u1>>6);
enb_eve <= 1;
web_eve <= 0;
addrb_eve <= ((((v2>>6)-1)>>1)<<8) + ((((v2>>6)-1)>>1)<<6) + (u2>>6);
end
else
begin
ena_odd <= 1;
wea_odd <= 0;
addra_odd <= ((((v1>>6)-1)>>1)<<8) + ((((v1>>6)-1)>>1)<<6) + (u1>>6);
enb_odd <= 1;
web_odd <= 0;
addrb_odd <= ((((v1>>6)-1)>>1)<<8) + ((((v1>>6)-1)>>1)<<6) + (u2>>6);
ena_eve <= 1;
wea_eve <= 0;
addra_eve <= (((v2>>6)>>1)<<8) + (((v2>>6)>>1)<<6) + (u1>>6);
enb_eve <= 1;
web_eve <= 0;
addrb_eve <= (((v2>>6)>>1)<<8) + (((v2>>6)>>1)<<6) + (u2>>6);
end
end
end
end
default: begin
end
endcase
end
end

6. 资源分析

四、结果分析

上图展示了 MATLAB 与 FPGA 实现结果的比较。整体效果尚可。在 FPGA 实现过程中,为了简化验证,原始图所有像素都进行了缓存,尚未完全应用稀疏网格存储优化,后续可进一步验证压缩方案的效果。
五、扩展知识
1. 焦距(像素单位)详解
在计算机视觉中,fx 和 fy 是将物理距离转换为像素距离的缩放因子。它们与物理焦距的关系如下:
$$ fx = \frac{\text{物理焦距} f}{\text{传感器像素尺寸} dx} $$ $$ fy = \frac{\text{物理焦距} f}{\text{传感器像素尺寸} dy} $$
例如,8mm 镜头搭配 0.004mm 像素尺寸的传感器,则 $fx = fy = 2000$ 像素。
在畸变矫正代码中,fx 和 fy 是内参矩阵的核心。忽略它们会破坏几何基础,导致校正失效。公式 $x_{norm} = (u - cx) / fx$ 的作用是将像素坐标归一化到以光心为原点的物理三维空间坐标系中,这一步不可省略。
2. 与家用投影仪校正的区别
家用投影仪的梯形校正与相机畸变矫正原理相似(均涉及透视变换),但实现路径不同。投影仪通常内置自动化系统替代离线计算,属于实时版校正程序。

3. 双线性插值
双线性插值用于估算二维空间中未知点的值。假设已知四个邻近点的函数值,对于任意点 $(x,y)$,先在 x 方向进行线性插值,再在 y 方向进行线性插值。
计算公式如下:







