跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客GitHub 精选镜像AI 生图工具UI配色美学隐私政策关于联系
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
MATLAB / OctaveAI算法

图像畸变矫正原理及 MATLAB 与 FPGA 实现

图像畸变矫正通过几何变换恢复图像真实结构,主要涉及径向和切向畸变模型。详细阐述了矫正原理,展示了 MATLAB 生成映射表的算法实现,并深入探讨了 FPGA 端的硬件优化策略,包括稀疏网格存储、定点量化及流水线设计。内容涵盖从理论推导到 Verilog 代码实现的完整流程,重点分析了焦距参数作用及双线性插值方法,为嵌入式视觉系统的实时畸变校正提供参考。

MqEngine发布于 2026/4/7更新于 2026/7/326 浏览
图像畸变矫正原理及 MATLAB 与 FPGA 实现

一、概述

图像畸变矫正是图像处理中的关键任务,主要用于修正因镜头畸变或拍摄角度导致的图像失真。其核心在于几何变换,通过数学模型将失真的像素映射回理想位置。

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 方向进行线性插值。

计算公式如下:

文章配图

文章配图

文章配图

文章配图

文章配图

目录

  1. 一、概述
  2. 1. 图像畸变的类型
  3. 2. 畸变矫正原理
  4. (1)径向畸变模型
  5. (2)切向畸变模型
  6. (3)实现顺序
  7. 二、MATLAB 具体实现
  8. 三、FPGA 实现
  9. 1. 流程与优化
  10. 2. 关键技术
  11. 3. 映射表压缩效果示例
  12. 4. 存储优化方案对比
  13. 5. FPGA 核心代码及仿真
  14. 6. 资源分析
  15. 四、结果分析
  16. 五、扩展知识
  17. 1. 焦距(像素单位)详解
  18. 2. 与家用投影仪校正的区别
  19. 3. 双线性插值
  • 免费图片AI生成工具免费生成了解详情
  • Magick API 一键接入全球大模型注册送1000万token查看
  • 免费图片视频在线生成30秒,将你的创意变成现实开始设计
  • X/Twitter免费视频下载器免登陆无限额度免费视频解析下载了解详情
  • 100+免费在线小游戏爽一把
极客日志微信公众号二维码

微信扫一扫,关注极客日志

微信公众号「极客日志V2」,在微信中扫描左侧二维码关注。展示文案:极客日志V2 zeeklog

更多推荐文章

查看全部
  • Trae 集成 Figma MCP 实现前端代码自动生成
  • Java 流程控制:从条件判断到循环遍历
  • CycleGAN 详解与实现
  • 为什么 AI 开发首选 Python 而非 Java 或 C++?
  • 解决 Java 源发行版 17 与目标发行版不匹配问题
  • AI 鉴伪技术解析:人脸视频、AIGC 图像及文档篡改检测
  • C++ STL 容器常用函数与实战技巧
  • 滑动窗口算法详解:经典例题解析
  • 向量数据库与 RAG 应用架构解析(上)
  • Java 社区跑腿家政上门服务商城解决方案
  • 基于 Leaflet 和天地图的长沙免费运动场所 WebGIS 可视化
  • Windows 系统下 Stable Diffusion WebUI 环境搭建指南
  • CosyVoice3 支持 ARPAbet 音素标注,提升英文发音准确性
  • C++ 哈希表底层实现:unordered_map/set、位图与布隆过滤器
  • Python 开发 MongoDB 数据库 MCP Server 实战指南
  • Stable Diffusion WebUI 无障碍改造:键盘导航与屏幕阅读器适配
  • 数据结构初阶:基于动态顺序表实现通讯录
  • LangChain 框架核心模块与使用指南
  • OpenClaw 本地 AI 智能体入门与实战指南
  • 宇树 G1 机器人强化学习训练实战:环境配置与奖励函数

相关免费在线工具

  • 加密/解密文本

    使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online

  • RSA密钥对生成器

    生成新的随机RSA私钥和公钥pem证书。 在线工具,RSA密钥对生成器在线工具,online

  • Mermaid 预览与可视化编辑

    基于 Mermaid.js 实时预览流程图、时序图等图表,支持源码编辑与即时渲染。 在线工具,Mermaid 预览与可视化编辑在线工具,online

  • 随机西班牙地址生成器

    随机生成西班牙地址(支持马德里、加泰罗尼亚、安达卢西亚、瓦伦西亚筛选),支持数量快捷选择、显示全部与下载。 在线工具,随机西班牙地址生成器在线工具,online

  • Gemini 图片去水印

    基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,online

  • Base64 字符串编码/解码

    将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online