简介:面向光场成像与计算摄影学习者,这套基于Matlab的光场相机数字重聚焦算法演示工程,重点展示利用光场数据在拍摄后自由调整焦点的技术思路。光场不仅记录成像位置,还保存了光线在三维空间中的传播方向与辐射强度;每个空间点在不同方向的辐射能量均被量化存储,正是这种多维光辐射数据,使后期重构不同焦平面的图像成为可能,突破了传统光学系统固定焦深的限制。资源共包含18个文件,以3个MATLAB脚本(主程序、重聚焦、图像变换)为核心,配合9张PNG实验对比图、一套乐高场景重聚焦示例、说明文档及若干备份文件,整体压缩包大小13.59MB,目录结构简洁,便于快速定位源码、结果与说明。目前已有53人学习/浏览。借助可直接运行的MATLAB程序与配套图像,读者能完整复现重聚焦流程,直观理解光线重投影、积分成像、数字对焦等关键步骤,并可在现有代码基础上修改参数或扩展功能,适合作为计算摄影、三维成像方向课程设计或入门研究的参考。
1. 光场相机重聚焦:不依赖深度估计的后期对焦
光场相机与普通相机的本质区别在于,它在传感器前放置了微透镜阵列,一次曝光同时记录光线的空间位置和方向信息,形成四维光场。这个额外的两个维度带来的直接能力就是数字重聚焦:拍摄完成后,可以在软件中任意改变焦平面位置,让原本虚化的主体变清晰。传统相机改变对焦需要移动镜头物理结构,而光场相机只需要对原始数据做一次重投影运算。
在Matlab里实现重聚焦算法,核心是理解光场数据结构和两条技术路线:一种是基于光场渲染理论的时域移位积分,另一种是基于傅里叶切片定理的频域切片提取。前者直观、易调试、适合教学演示,后者计算效率高但需要处理频域中的坐标对齐问题。本文面向有图像处理基础的开发者,从数据格式、原理推导、代码实现到参数调优,逐步把重聚焦的完整链路在Matlab中落一遍。文章使用的所有算法都是该领域的经典实现路径,不依赖任何商业工具箱中的光场专用函数,全部基于Matlab基础图像处理函数手写完成。
2. 光场数据结构与重聚焦原理
2.1 四维光场参数化与Matlab数据组织方式
光场相机采集到的数据一般用双平面参数化描述:光线穿过主透镜平面时用坐标(u,v)表示位置,穿过微透镜平面时用坐标(s,t)表示方向。一张完整的光场就是关于这四个变量的函数L(u,v,s,t)。在实际存储中,LyTRO相机解码后的数据常见格式是“微透镜图像阵列”,每个微透镜下的像素构成一个方向采样点,整幅图切割后重组为(s,t,u,v)四维数组。
在Matlab中组织光场数据,我一般用三维数组的堆叠方式:将角度分辨率定义为A x A,空间分辨率定义为S x T,最终数据可以存储为size [A, A, S, T]的四维数组,或者用元胞数组lf_cell{u, v}存储每个视角下的子孔径图像。后者在调试时更方便,imshow(lf_cell{5, 5})就能查看中心视角图像。
读取原始光场图像时,常用的方法是解析微透镜图像的六边形排列规律:
% 读取光场原始图像并提取微透镜中心网格 raw_img = imread('lf_raw.png'); if size(raw_img, 3) == 3 raw_img = rgb2gray(raw_img); % 灰度化处理 end % 假设微透镜间距为pixels_per_lens,该值由相机标定决定 pixels_per_lens = 12; % 提取中心视角对应像素:每个微透镜下取中心点 [rows, cols] = size(raw_img); center_u = round(pixels_per_lens / 2); center_v = center_u; sub_img = raw_img(center_u:pixels_per_lens:rows, center_v:pixels_per_lens:cols); imshow(sub_img); title('Center View Sub-Aperture Image');这段代码的核心逻辑是按固定间隔采样,间隔大小就是微透镜的像素直径。center_u和center_v决定选取哪个视角——中心视角选择微透镜正中位置,其他视角则偏移采样起点。注意pixels_per_lens这个参数必须准确,它直接决定子孔径图像的分辨率和视角数量。
2.2 重聚焦的物理含义:推迟焦平面的数学等效
传统相机中,对焦过程可以理解为将不同深度物体的光线汇聚到传感器平面。光场相机记录了所有方向的入射光线,相当于把焦平面“冻结”在传感器位置。如果要让焦平面移动,比如从前景移到背景,就需要在软件中重新计算光线传播路径。
对光场做重聚焦,等价于对光场中各方向采样做平移后再叠加。设原始光场采集到的图像是L(u,v,s,t),目标焦平面到当前平面的距离为d,重聚焦后的图像E(s,t)可以表示为:
E(s,t) = ∫∫ L(u,v, s + (u-uc)·d, t + (v-vc)·d) du dv
其中(uc, vc)是中心视角坐标。这个积分在离散域中就是“移位-叠加”操作:将每个视角图像按与视角偏移量成正比的距离平移,然后累加平均。当d为正时焦平面后移(远处清晰),为负时前移(近处清晰)。
2.3 数字重聚焦的两条技术路线对比
时域移位累加方法直观就是按公式对每个视角图像做imtranslate平移再求平均,实现简单、内存占用小,逐视角处理即可完成。频域切片方法则是利用傅里叶切片定理,先对四维光场做3D FFT,在频域中提取一个二维切片再做逆变换。后者在光场空间分辨率较大时更快,但代码复杂度成倍增加,且需要精确处理频域坐标轴的倾斜关系。
下表从实现角度给出两者对比:
| 对比维度 | 时域移位累加法 | 频域傅里叶切片法 |
|---|---|---|
| 计算复杂度 | O(A²·S·T·K),K为插值核尺寸 | O(4D FFT + 2D IFFT) |
| Matlab实现难度 | 低,20行内可完成 | 中等,需要坐标轴映射 |
| 重聚焦质量 | 依赖插值核,双线性即可 | 无插值误差,但存在频域截断 |
| 适合场景 | 教学演示、小尺寸光场 | 分辨率大、批量处理 |
选择哪条路线完全取决于应用需求。做教学演示和算法验证,时域方法更友好,因为每一步视觉效果都能直接看到。如果后续要做实时重聚焦或者处理亿级像素的大光场,频域方法的优势就体现出来。先实现时域方法,跑通后再切换到频域,这是最稳的推进路径。
3. 用Matlab实现光场重聚焦:从最小示例到核心函数
3.1 最小可运行示例:11x11视角的移位累加重聚焦
先写一个最简版本验证流程,使用合成光场数据。这里用Matlab内置函数生成一个模拟光场:中心是一个圆环图案,每个视角按视差偏移来模拟深度层次。
% 生成合成光场:11x11视角,每个视角64x64像素 clear; close all; clc; num_u = 11; num_v = 11; img_size = 64; % 创建一个基础纹理图,用作场景内容 [X, Y] = meshgrid(1:img_size, 1:img_size); base_img = double(uint8(50 + 100 * (abs(X - 32) < 8) .* (abs(Y - 32) < 8))); lf_data = zeros(num_u, num_v, img_size, img_size, 'single'); % 模拟深度:中心物体位于深度d=1,背景d=0 % 每个视角根据(u,v)偏移叠加 for u = 1:num_u for v = 1:num_v offset_u = (u - 6) * 0.5; % 视角偏移量 offset_v = (v - 6) * 0.5; view_img = imtranslate(base_img, [offset_u, offset_v], 'bilinear', 'crop'); lf_data(u, v, :, :) = view_img; end end fprintf('Synthetic light field size: %d x %d views, each %d x %d\n', ... num_u, num_v, img_size, img_size);合成数据生成的核心是imtranslate做图像平移,偏移量与视角序号线性相关:视角远离中心时偏移量增大,这模拟了从不同角度观察场景时前景物体的视差变化。'crop'参数保证平移后图像尺寸不变,超出边界部分补零。
3.2 重聚焦核心函数及其参数说明
重聚焦的核心就是把3.1节的合成光场按公式投影到某一焦平面。下面函数接受四维光场数组、目标深度和插值方式,输出重聚焦后的二维图像:
function [rf_img] = refocus_shift_sum(lf_data, depth, interp_method) % REFOCUS_SHIFT_SUM 基于移位累加的光场重聚焦 % 输入: % lf_data : 4D数组 [num_u, num_v, S, T] % depth : 归一化深度,正数后移焦平面,负数前移 % interp_method : 'nearest'或'bilinear' % 输出: % rf_img : 重聚焦结果图像 [S, T] [num_u, num_v, S, T] = size(lf_data); center_u = (num_u + 1) / 2; center_v = (num_v + 1) / 2; rf_img = zeros(S, T, 'single'); weight_sum = 0; for u = 1:num_u for v = 1:num_v % 计算该视角相对中心视角的偏移量 shift_u = (u - center_u) * depth; shift_v = (v - center_v) * depth; % 取出当前视角的子图像 view = squeeze(lf_data(u, v, :, :)); % 平移并累加 shifted = imtranslate(view, [shift_v, shift_u], interp_method, 'crop'); rf_img = rf_img + shifted; weight_sum = weight_sum + 1; end end rf_img = rf_img / weight_sum; end这个函数里有三个关键点。第一,偏移量计算方式(u - center_u) * depth意味着深度是角度位置的线性系数,当depth=0时所有视角不移动直接叠加,得到的是中心视角的等效成像。第二,imtranslate中的平移向量顺序是[x, y]即[列, 行],所以先传shift_v再传shift_u,这和图像坐标系的行列顺序容易搞反。第三,累加后除以视角数量做归一化,保证对比度一致。
调用方式非常简单:
depth_range = -2:0.5:2; figure('Name', 'Refocus results'); for i = 1:length(depth_range) rf = refocus_shift_sum(lf_data, depth_range(i), 'bilinear'); subplot(2, 4, i); imshow(rf, []); title(sprintf('depth = %.1f', depth_range(i))); end这里用imshow(rf, [])将图像动态范围拉伸到全展示区间,直接看对比度更清楚。视觉上会看到深度值变化时,不同位置的清晰区域在移动,这就是数字重聚焦的直观体现。
3.3 深度标定与焦点堆栈生成
光场重聚焦最实用的输出之一是焦点堆栈(Focal Stack),即一系列不同深度参数下的重聚焦图像堆叠。焦点堆栈在后续深度估计、全聚焦合成中都是基础数据。
生成焦点堆栈需要注意深度步长的选择。步长过大,相邻层之间清晰区域跳跃明显;步长过小,层数增多但相邻层信息冗余。我一般的经验是,对于11x11视角的光场,深度范围[-3, 3]取0.25步长,生成25层左右,既有足够深度分辨率又不会数据量过大。
num_focus = 25; focus_stack = zeros(img_size, img_size, num_focus, 'single'); depth_step = 6 / (num_focus - 1); for i = 1:num_focus depth_val = -3 + (i - 1) * depth_step; focus_stack(:, :, i) = refocus_shift_sum(lf_data, depth_val, 'bilinear'); end % 保存焦点堆栈供后续深度估计使用 save('focus_stack.mat', 'focus_stack', 'depth_step');逐层重聚焦会重复读取光场数据,如果光场尺寸大,这里可以用parfor并行循环加速,把depth分配给多个worker。不过我建议先把串行版本调通,确认输出正确再优化速度——重聚焦的调试难点在于偏移方向的正负号,串行版本更容易定位符号错误。
4. 频域傅里叶切片重聚焦与全聚焦图像合成
4.1 傅里叶切片定理在重聚焦中的应用
时域移位累加在概念上直白,但当光场分辨率达到3000x3000以上、视角数上千时,逐视角插值平移的运算量会变得难以接受。傅里叶切片定理提供了另一条路:四维光场在频域中恰好对应一组二维切片,每个切片对应一个重聚焦深度。
核心思想是:对四维光场先做三维傅里叶变换,得到L^(fx, fy, fu, fv),然后深度d的重聚焦图像在频域等于该四维频谱中的某个二维切片,切片方程为:
Ê(fx, fy, d) = L̂(fx, fy, fx·d, fy·d)
也就是说,在频域中沿斜率为(d, d)的方向截取一个二维平面,再做二维逆傅里叶变换,就得到了重聚焦图像。这个操作完全不需要插值,计算量主要在三步:一次4D FFT、一次提取切片、一次2D IFFT。
4.2 基于Matlab的频域切片核心代码
提取频域切片的关键是:对四维频谱的第三、四维做非整数下标采样。Matlab中不直接支持三维切片任意倾斜提取,但可以通过线性插值在频域采样:
function refocused = refocus_fourier_slice(lf_freq, depth, S, T) % REFOCUS_FOURIER_SLICE 频域切片法重聚焦 % 输入: % lf_freq : 4D FFT结果,size [num_u, num_v, S, T] % depth : 重聚焦深度参数 % S, T : 空间分辨率 [num_u, num_v, ~, ~] = size(lf_freq); % 频域坐标网格 half_s = floor(S / 2); half_t = floor(T / 2); [s_freq, t_freq] = meshgrid(-half_t:(T - half_t - 1), ... -half_s:(S - half_s - 1)); % 对角度维度做频域偏移映射 % 每个频点(s, t)对应角度索引为 depth*s, depth*t u_indices = round(depth * s_freq / S * num_u) + (num_u / 2 + 1); v_indices = round(depth * t_freq / T * num_v) + (num_v / 2 + 1); % 防止越界 u_indices = max(min(u_indices, num_u), 1); v_indices = max(min(v_indices, num_v), 1); % 提取切片 slice_2d = zeros(S, T); for i = 1:S for j = 1:T slice_2d(i, j) = lf_freq(u_indices(i, j), v_indices(i, j), i, j); end end refocused = real(ifft2(ifftshift(slice_2d))); end这个实现为清晰展示切片原理做了很多简化:用round做最近邻插值,用嵌套循环提取数据,性能不是最优。实际工程中这里应该用interpn做三维插值,避免坐标量化误差。频域方法的优点是无需逐视角处理,但代码复杂度更高,且需要仔细处理FFT移位和坐标原点对齐。我第一次实现在这里踩了坑,最典型的问题是频率零点和角度中心没对齐,导致输出图像出现条纹伪影。
4.3 全聚焦图像合成与景深扩展
有了焦点堆栈之后,可以进一步合成全聚焦图像,即每个像素都清晰的图像。常见做法是逐像素从焦点堆栈中选取对比度最大的层作为该像素的清晰值。对比度可以用局域方差或拉普拉斯响应来衡量:
function [all_in_focus, depth_map] = focus_stack_fusion(focus_stack) % FOCUS_STACK_FUSION 焦点堆栈融合产生全聚焦图和深度图 % focus_stack: [S, T, N],N为层数 [S, T, N] = size(focus_stack); depth_map = zeros(S, T); all_in_focus = zeros(S, T); % 定义3x3拉普拉斯核 laplace_kernel = [0 1 0; 1 -4 1; 0 1 0]; for i = 1:S for j = 1:T max_focus = -inf; max_depth = 0; for n = 1:N patch = focus_stack(max(i-1,1):min(i+1,S), max(j-1,1):min(j+1,T), n); response = sum(sum(abs(conv2(patch, laplace_kernel, 'same')))); if response > max_focus max_focus = response; max_depth = n; end end depth_map(i, j) = max_depth; all_in_focus(i, j) = focus_stack(i, j, max_depth); end end end全聚焦合成的效果很大程度取决于焦点堆栈的层间距。层间距太大时,逐像素选出的“最清晰层”并不一定真的是该像素的准确对焦深度,只是最近的一层。对于景深较深的场景,我一般会把depth范围扩大同时增加层数,或者先做一次粗粒度筛选确定前景背景大致深度范围,再在该范围内加密层间距。
5. 影响重聚焦质量的4个关键参数与调参技巧
5.1 视角数量与空间分辨率的取舍
光场相机的硬件分辨率固定,微透镜阵列把像素分配给角度维度越多,每个视角下的空间分辨率就越低。在算法层面,这个取舍直接决定重聚焦的精度和视觉质量。
| 视角数(A) | 每视角空间分辨率 | 重聚焦精度 | 计算耗时(时域) |
|---|---|---|---|
| 3x3 | 高 | 低,虚化变化粗糙 | 快 |
| 5x5 | 中高 | 中等,可用于演示 | 较快 |
| 9x9 | 中 | 良好,边缘较平滑 | 中等 |
| 15x15 | 低 | 优秀,连续变焦平滑 | 慢 |
日常做算法验证和教学演示,5x5到9x9视角是比较合理的区间。小于5x5时,重聚焦的平滑过渡无法体现,虚化边缘会出现明显阶跃;大于15x15时,单视角分辨率可能低于50x50,重聚焦图像本身清晰度就不够。
5.2 插值方式对重聚焦边缘的影响
时域移位累加中,imtranslate的插值方式影响输出质量。最近邻插值速度最快但会产生锯齿边缘,双线性插值均衡了速度和效果,双三次插值在高倍率偏移时边缘更干净但耗时明显增加。
实践中的建议是:如果目标深度变化范围在±3像素以内,双线性足够;如果深度范围更大,比如产生明显的视角偏移,改用双三次插值更稳妥。测试时可以用同一场景、不同插值方式生成重聚焦图,用拉普拉斯响应做定量对比——响应值越高代表边缘越锐利。
% 对比插值方式对重聚焦质量的影响 depth_test = 1.5; rf_near = refocus_shift_sum(lf_data, depth_test, 'nearest'); rf_bil = refocus_shift_sum(lf_data, depth_test, 'bilinear'); rf_bic = refocus_shift_sum(lf_data, depth_test, 'bicubic'); % 计算拉普拉斯响应作为清晰度指标 metric = @(img) sum(abs(conv2(img, [0 1 0; 1 -4 1; 0 1 0], 'same')), 'all'); fprintf('nearest: %.2f, bilinear: %.2f, bicubic: %.2f\n', ... metric(rf_near), metric(rf_bil), metric(rf_bic));这个对比能直观看到插值质量差距。需要注意指标只能作为参考,拉普拉斯响应同时也会放大噪声,如果光场数据本身噪声较大,这个指标可能失真。
5.3 修正偏移方向正负号的排错方法
重聚焦最容易踩的坑是图像清晰区域和预期方向相反:想要背景清晰却变成前景清晰。原因是偏移方向的符号约定和坐标系定义不一致。
我常用的排错方法是建立一个“已知深度结构的测试场景”:合成光场时让前景物体偏移量为正、背景偏移量为负,然后改变焦深度正负号,观察前景区域和背景区域谁先变清晰。如果前景在depth为负时清晰,说明偏移符号约定与预期相反,在shift_u = (u - center_u) * depth前面加负号即可。
这个排错三板斧处理很有效:第一步生成合成数据验证基本功能,第二步用中心视角图像和重聚焦图像叠加重合度检查偏移量是否正确,第三步打印出中心视角偏移量数值,确认视角越界导致的异常行为。
5.4 大光场数据的纵向分块计算
当光场空间分辨率达到数千万像素时,整个四维数组在Matlab中会占用过大内存。11x11视角、单视角2048x2048的光场,存储为single类型需要约2GB内存,加上中间变量很容易撑爆内存。
分块重聚焦是一种高效策略,沿空间维度切块处理:
% 纵向分块重聚焦:每次处理一个横向条带 block_height = 256; [S, T] = size(view_ref); num_blocks = ceil(S / block_height); rf_final = zeros(S, T, 'single'); for b = 1:num_blocks row_start = (b - 1) * block_height + 1; row_end = min(b * block_height, S); lf_block = lf_data(:, :, row_start:row_end, :); rf_final(row_start:row_end, :) = refocus_shift_sum(lf_block, depth, 'bilinear'); end分块后每块独立重聚焦再拼接,理论上结果和整体计算完全一致,因为移位累加本质上是逐像素独立的。注意处理分块边界时要保留少量重叠区域(比如16像素),避免插值在边界处出现轻微伪影。
5.5 交互式重聚焦演示:从参数到滑块
写一个简单的GUI让使用者拖着滑块看不同深度下的对焦效果,对演示和教学非常加分。Matlab中可以用uifigure和uislider快速搭建:
function refocus_demo_gui(lf_data) f = uifigure('Name', 'Light Field Refocus Demo', 'Position', [100 100 800 600]); g = uiaxes(f, 'Position', [50 50 600 450]); sld = uislider(f, 'Position', [150 20 400 3], 'Limits', [-3 3], 'Value', 0); sld.ValueChangedFcn = @(sld, event) update_refocus(sld.Value); function update_refocus(depth) rf = refocus_shift_sum(lf_data, depth, 'bilinear'); imshow(rf, [], 'Parent', g); title(g, sprintf('Depth: %.2f', depth)); end end滑块移动时实时调用重聚焦函数,Matlab的JIT加速足够支撑9x9视角、128x128分辨率的实时响应。如果分辨率更大,在更新函数中加入drawnow limitrate避免界面卡顿。
6. 用EPI图像斜率校验重聚焦结果的准确性
验证重聚焦结果是否准确的经典方法是裁剪一个水平截线,分析极限平面图像(EPI)的斜率。EPI图像是固定角度维度的一个值(比如固定v和行坐标s),将光场展开成2D图像,其中每个物体的特征表现为一条直线段,直线的斜率与该物体的深度成正比。
对任意一个行位置s0,EPI构造方式如下:
function plot_epi(lf_data, v_fixed, s_fixed) % v_fixed: 固定垂直视角索引 % s_fixed: 固定空间行坐标 [num_u, num_v, S, T] = size(lf_data); epi = squeeze(lf_data(:, v_fixed, s_fixed, :)); % 得到[u, T]二维图像 figure; imshow(epi, []); title(sprintf('EPI at v=%d, s=%d', v_fixed, s_fixed)); xlabel('T direction (column)'); ylabel('U direction (view)'); % 显示一条辅助线验证斜率 hold on; line([1 T], [1 num_u], 'Color', 'r', 'LineWidth', 1); endEPI中一条很具有代表性的观察结果是:当重聚焦深度正好等于场景中某物体的真实深度时,该物体在EPI中对应区域的斜率方向会发生反转,从正倾斜翻转为负倾斜。用重聚焦后的图像重新生成EPI,如果目标是背景处斜率接近零、前景处斜率较大,说明深度参数基本正确。
数字重聚焦的验证还可以用一组定量指标:计算重聚焦前后EPI的斜率一致性。对EPI做边缘检测并拟合直线斜率,与理论预期值对比。实际项目里这一招最可靠,能防止“看着清晰但实际是对焦场中选了个巧合深度”的错觉。重聚焦的调参也因此变成有明确目标的工作,幻灯片推到头,斜率归零,那一层就是准确焦平面。
本文还有配套的精品资源,点击获取