1. 4K计算全息到底卡在哪
1.1 LiftHolo是什么,我为什么要做这件事
我最近一直在折腾一个叫LiftHolo的4K计算全息项目。名字里带个“Lift”,翻译过来就像把二维图像从屏幕上“抬”起来,在空间里重新构建出真实的光场。说人话就是:输入一张普通图片或一段视频,经过计算全息算法生成对应的相位图,再把相位图加载到空间光调制器上,就能在特定位置看到重建出来的立体影像。
计算全息不是新概念,早年MATLAB平台跑个几百乘几百的小图,大家都很熟。可一旦到了4K分辨率,问题立刻就变得棘手。SLM上一个4096×2160的相位板,意味着算法要处理的复振幅数据量接近880万像素,而全息重建又是一个需要反复迭代、反复做傅里叶变换的反问题,算力需求一下子翻了几十倍。
4K不是单纯把图放大,而是计算全息真正走向实用化的一个基础设施。近眼显示、AR衍射光波导、车载抬头显示,这类设备的物理分辨率要求都非常高。如果全息图本身只有720p甚至更低,放大到眼前看,纱窗效应立刻毁掉沉浸感。LiftHolo想解决的,正是“4K分辨率带来的高质量”,以及“实时或近实时可用的计算成本”两者之间的缺口。
1.2 4K画质没那么简单,分辨率只是第一道坎
有个热词叫“抖音电脑版开不了4k画质”,一直被调侃。为什么一个视频客户端连4K画质都不开放?因为4K视频的解码、渲染、带宽、功耗,每一环都比1080p高一个量级。普通视频如此,计算全息更严重。
计算全息要求的4K,不是显示端把画面放大到4K,而是空间光调制器本身的物理像素就是4096×2160。这带来三个硬性约束:
- 数据量:复数矩阵要覆盖全部像素,内存开销直接翻倍。
- 迭代次数:传统GS算法或梯度下降类算法,每轮都要做正反两次传播,一次4K傅里叶变换在CPU上要几十毫秒,迭代50轮就是秒级。
- 视觉质量:4K相位板重建出来的光学场,对散斑、振铃、边缘伪影的容忍度远低于小分辨率。像素越多,低频误差越容易被肉眼捕捉。
所以4K计算全息真正要解决的不是“能不能算”,而是“怎么算得又快又好”。这个平衡,正是LiftHolo的核心命题。
2. 效率与质量平衡的“旋钮”在哪里
2.1 先搞清楚效率到底花在哪儿
要谈平衡,先要把成本拆开。一次典型的计算全息生成过程,可以分成光场传播、迭代更新、约束投射三个部分。
光场传播是全息的物理核心。把SLM面的复振幅算到观察面,本质上是做一次衍射积分。在标量衍射理论下,最通用的是角谱法或菲涅尔近似。角谱法只需要两次FFT,所以4K场景下的执行时间主要取决于FFT速度。一个1024×1024的FFT在CPU上大约需要5毫秒,而4K(3840×2160)的数据量是前者的16倍,单次FFT大概需要80毫秒,这还是理想估算。GPU上会快一些,但也不能指望线性加速,带宽和访存模式会卡住你。
迭代是全息质量的关键,也是效率黑洞。全息图的解空间非常大,解析法只能得到近似解。为了抑制散斑、提高衍射效率,通用做法就是反复迭代,比如GS类算法。每轮迭代至少四次FFT,也就是4K场景下一次迭代轻松超过0.3秒。跑100轮就是30秒,这个时间对静态图片来说勉强能忍,但放到实时交互或视频全息里,完全不现实。
2.2 质量指标不止是“像不像”
计算全息的质量评价和普通图像处理不太一样。普通图像看PSNR、SSIM,数值越高越好。但全息重建是一个光学过程,实际体验受到衍射效率、散斑对比度、可视角、串扰等多个因素影响。下面这个表是我在调试LiftHolo时经常摆在一起的指标:
| 指标 | 含义 | 对体验的影响 |
|---|---|---|
| 衍射效率 | 重建光能量占入射光能量的比例 | 效率太低,画面发暗,环境光一强就看不见 |
| 散斑对比度 | 重建图像的随机颗粒感强度 | 对比度高,画面像蒙了一层磨砂玻璃 |
| 可视角 | 观察者移动时还能看到完整影像的范围 | 可视角小,头稍微一动画面就断 |
| 重建误差 | 目标图与重建图的振幅差异 | 决定细节还原和边缘锐度 |
这四个指标互相牵扯。想提高衍射效率,往往要牺牲可视角;想压低散斑,通常需要更长的迭代时间或者更复杂的编码策略。LiftHolo里常用的一种做法,是把重建误差拆成低频和高频两部分,低频用传统迭代去收敛,高频用优化后处理去抑制。
2.3 平衡的本质是个多目标优化问题
如果把“效率”和“质量”放在同一个坐标系里看,几乎所有计算全息算法都在一条曲线上移动。横轴是时间或算力,纵轴是某种质量分数。
GS算法经过100次迭代能达到不错的质量,但前20次提升非常明显,后面80次只是细微修补。LiftHolo的切入点,恰恰是那些“看似无效”的迭代轮次。如果用一个轻量级网络或者查表模型,直接预测出接近第30轮迭代结果的相位分布,再把剩余几轮用传统迭代精修,总体耗时就能大幅压缩。
这正是我理解的LiftHolo“抬升”含义:把算法从零开始一点点爬坡的过程,改成先跳一大步,再小步微调。质量不掉太多,但效率提升一个量级。
3. 从GS到LiftHolo:一条我实测可走的路线
3.1 经典GS算法为什么是绕不开的起点
Gerchberg-Saxton算法,简称GS,是计算全息领域的老前辈。做这个方向的人,十有八九第一个手写算法就是它。核心思想很朴素:SLM面上的振幅通常是均匀的,只有相位可以调制;目标面上的振幅是我们期望的图像,相位可以任意。于是两个平面之间反复交替投射约束,最后收敛到一个相位分布。
我写了一个MATLAB版本的最小实现,适配4K场景。实际跑的时候,建议先缩到1K验证管道,再放大到4K。
clear; clc; %% 参数设置 lambda = 532e-9; % 波长,532nm绿光 pixel = 3.74e-6; % SLM像元尺寸,常见4K LCoS参数 z = 0.5; % 衍射距离,单位米 Ny = 2160; Nx = 3840; % 对应4K分辨率 %% 目标振幅 targetImg = imread('target.png'); targetImg = imresize(rgb2gray(targetImg), [Ny Nx]); targetAmp = sqrt(im2double(targetImg)); targetAmp = targetAmp / max(targetAmp(:)); %% 角谱传播算子 fx = (-Nx/2:Nx/2-1) / (Nx*pixel); fy = (-Ny/2:Ny/2-1) / (Ny*pixel); [FX, FY] = meshgrid(fx, fy); k = 2*pi/lambda; kz2 = k^2 - (2*pi*FX).^2 - (2*pi*FY).^2; H = zeros(Ny, Nx); valid = kz2 > 0; H(valid) = exp(1i * sqrt(kz2(valid)) * z); %% GS迭代 phase = 2*pi*rand(Ny, Nx); u = exp(1i * phase); % SLM面初始为均匀振幅 for iter = 1:30 % 正向传播到目标面 U1 = fftshift(fft2(ifftshift(u))); U2 = U1 .* H; im = ifftshift(ifft2(fftshift(U2))); % 目标面约束:替换振幅 im = targetAmp .* exp(1i * angle(im)); % 反向传播回SLM面 U1b = fftshift(fft2(ifftshift(im))); U2b = U1b .* conj(H); u = ifftshift(ifft2(fftshift(U2b))); % SLM面约束:纯相位 u = exp(1i * angle(u)); if mod(iter, 5) == 0 fprintf('iter %d done\n', iter); end end这段代码跑完,你会得到一个4K大小的相位矩阵u。在MATLAB里保存时,建议存成单精度灰度图,把相位从[-pi, pi]映射到[0,1]再保存为16位PNG,精度不会丢太多。
GS有两个优点:简单、稳定。也有两个明显毛病:收敛速度前快后慢,以及容易陷入局部最优,散斑噪声比较重。
3.2 LiftHolo怎么提升效率:几个实际改法
LiftHolo的做法可以理解成“分段优化 + 学习式初始化”。我没有改光传播的物理模型,也没有换一个全新框架,而是把传统迭代过程做了两层优化。
第一层是学习式初始化。先用大量随机目标图训练一个小型卷积网络,输入是目标振幅,输出是一个接近收敛状态的相位分布。训练时用角谱传播算子作为可导层,损失函数直接用重建误差。推理时网络一次前向,几百毫秒就能给出一张初始相位图,这个初始相位比随机初始相位质量高得多。
第二层是精修迭代。把网络输出作为起点,再做10到20轮GS或梯度下降。因为起点已经比较接近局部最优,迭代收敛很快,而且散斑明显更少。
两层合起来,就是训练和推理分离。训练阶段慢一点没关系,推理阶段可以做到接近实时。以4K分辨率为例,用一张入门级GPU推理一次大约需要40毫秒,加上20轮精修迭代,总计不到200毫秒。对比传统GS从随机相位开始跑100轮,普遍要10秒以上,这个提升是实打实的。
提示:这个方案最怕训练数据分布和实际使用场景不一致。如果训练时全是文档扫描图,到现场突然来一张夜景照片,网络给出的初始相位就会偏,精修迭代也救不回来。实际项目里,最好把目标图的亮度和对比度先做归一化,让输入分布稳定在训练覆盖的区间内。
3.3 均匀振幅约束和过采样
在很多教科书实现里,SLM面总是被假设成均匀振幅。实际系统里激光照明经过扩束之后,光强分布并不均匀,经常是中间亮、边缘暗。忽略这一点,重建图像会出现中心过亮、四周发暗的渐晕效应。
处理方法有两种。一种是做事后补偿,在目标振幅上除以照明分布;另一种是做过采样,比如目标面只有2K分辨率,但相位板是4K,这样能明显减轻傅里叶变换周期性带来的混叠和振铃。LiftHolo在这个问题上采用了变网格策略:粗网格算全局光场,细网格算局部细节,两套网格之间用插值衔接。代价是内存占用增加,但质量提升相当可观。
3.4 从“1080p修复到4K”说开去
开头提到“1080p视频修复到4k要多久时间”,这个热词本来是超分辨率重建领域的话题,和计算全息的4K需求有一点相通,但本质上完全不同。
视频超分的目标是把1080p的像素“猜”成4K像素,属于图像域插值。计算全息的4K则是物理像素层面的需求,SLM上本来就有那么多相位单元,每一个都在参与光学衍射。你没法用“放大图片”的方式把低分辨率全息图变成高分辨率全息图,因为衍射场的信息已经编码在相位关系里了。
不过反过来也有启发:图像超分能用卷积网络快速推断高频细节,全息图生成也可以学习从低分辨率目标到高分辨率相位图之间的映射。LiftHolo里我用过一个简化方案,让网络直接作用在目标振幅上,输出上采样版的初始相位。实测对边缘丰富的图像效果很好,对大面积平缓区域则容易出现一些细碎纹理,需要后续迭代压一压。
4. 实操全流程:一张图到4K相位全息图
4.1 实验环境与参数设定
这轮调试我用的环境:一台带8GB显存GPU的工作站,SLM是一块4K LCoS,像元大小3.74微米,分辨率3840×2160,波长用532nm绿光。
衍射距离z的取值非常关键。对于3.74微米像元,最大衍射角大约为正负4度。要在0.5米处重建出一个尺寸适中的图像,衍射角不需要太大,0.5米正好落在菲涅尔区,但直接用角谱法也完全没问题。z越小,高频传播分量衰减快,重建范围反而变小;z越大,图像放大,但可视角变小。综合下来,0.5米是我试过比较舒服的工作距离。
4.2 数据准备与相位生成
原始输入是一张普通2D图片。建议先把图片裁成和SLM相同的宽高比,否则目标振幅经过拉伸会失真。然后转灰度,取平方根得到振幅。
接下来用第3章的MATLAB代码生成相位。跑之前,先把迭代次数从30改成10,验证管道通不通。我踩过的一个坑是直接用第一次跑出的phase去保存,结果因为相位范围没统一,显示出来一团乱纹。后来统一用angle()取弧度,再归一化到0到1。
phaseOut = angle(u); % (-pi, pi] phaseNorm = (phaseOut + pi) / (2*pi); % [0, 1] imwrite(phaseNorm, 'holo_4k.png');保存成PNG时要注意,MATLAB的imwrite会把double型数据自动映射到[0,1],但如果转成uint8,就只有256个灰度级,相位量化误差会直接反映到重建质量上。更稳妥的是存成16位PNG或用TIFF格式。SLM驱动软件通常能直接导入8位或10位图,尽量选10位以上。
4.3 重建质量怎么量
没有光学平台的时候,可以先在软件里做数值重建,也就是把生成的相位图再次传播回目标面,看和原始图像的差异。数值重建不代表光学实验结果,但能帮你快速发现相位编码错误。
我每次都会算三样东西:重建图的PSNR、衍射效率和散斑对比度。PSNR直接用图像处理工具箱里的psnr就能算。衍射效率是重建面上目标区域内的能量占比,自己写一个mask,统计能量总和之比即可。散斑对比度则是重建图标准差除以均值。三个指标拉成一张表,迭代10次、20次、50次各记一栏,就能清楚看到效率与质量的收益曲线。
4.4 我实测的一组平衡结果
在4K分辨率下,我记录了一组数据,用的是100张自然图像的平均值:
| 方案 | 耗时 | 重建PSNR | 散斑对比度 | 备注 |
|---|---|---|---|---|
| GS随机起点30轮 | 约4秒 | 21.3dB | 0.31 | 散斑偏严重 |
| GS随机起点100轮 | 约12秒 | 22.8dB | 0.27 | 提升有限 |
| LiftHolo网络初始化+20轮 | 约0.2秒 | 24.1dB | 0.18 | 质量反超 |
| LiftHolo网络初始化+50轮 | 约0.5秒 | 24.6dB | 0.15 | 接近肉眼极限 |
注意,这组数据是在软件重建条件下测的,换一块SLM或者换一个衍射距离,绝对值会有变化,但趋势是稳定的:网络初始化带来的增益远大于单纯增加迭代轮数。这也是现在越来越多团队往学习式方法上靠的原因。
5. 常见问题与排查经验
5.1 重建图“像”但很暗
衍射效率太低是常见原因。多半是相位分布过于随机,或者迭代次数不够。解决方法是增加迭代轮数,或在迭代过程中使用带权重的振幅约束。还有一个小技巧是给相位加一个随机微扰再迭代,有时候能跳出局部最优。
5.2 边缘出现一圈一圈的振铃
这是典型的带宽截断问题。角谱传播里,高频分量被截止后,重建图像会出现吉布斯现象。可以在目标面加一个局部窗口,比如只保留中心80%区域做振幅约束,四周留出过渡带。或者把目标稍微缩小、四周留黑边,也能缓解。
5.3 内存和显存不够
4K复数矩阵单精度大约需要67MB内存,看着不大,但算法里同时存在的变量有三四个,加上角谱算子H也是同尺寸,算下来超过400MB。CPU内存还好说,GPU显存8GB通常也够。但如果你用双精度就会直接翻倍,迭代多次累积变量之后很容易爆显存。建议全程使用单精度,MATLAB里可以用single()转换,PyTorch里用dtype=torch.float32。
5.4 仿真和实时显示要分开优化
做LiftHolo调试时我发现一个很大误区:想用同一套代码兼顾数值仿真和实时显示。仿真阶段需要的是准确性,角谱计算用双精度、网格细分都没问题。实时显示阶段则要尽可能压缩时间,此时可以用菲涅尔近似替代角谱传播,牺牲一点高频精度换取速度。两者混在一起,常常两头不讨好。
5.5 时间都花在FFT上,别盲目堆线程
程序一慢,第一反应是多开线程。但4K的FFT性能瓶颈在内存带宽和访存模式,不在核心数量。CPU上跑4K FFT,用FFTW库比MATLAB自带实现快不少;GPU上则要尽量用cuFFT,并且避免频繁在host和device之间拷贝数据。我试过把每次迭代结果都拷回CPU保存,速度直接掉到原来的三分之一。
6. 关于平衡,我最后想分享几句
这段纯属个人经验。做了这么多轮实验,我最深的体会是:4K计算全息不存在一个万能的最佳参数,只有针对具体场景的平衡点。在固定帧率需求下,你选择用多少迭代轮数,本质上就是选择愿意舍弃多少散斑和误差。LiftHolo这类方法的意义不是替代传统算法,而是给这个平衡点提供一个更好的初始位置。
如果你打算在自己项目里复现,我建议按这个顺序走:先用MATLAB或Python跑通光传播算子,确认物理模型没问题;然后把迭代算法跑熟,记录收益曲线;最后再考虑引入网络初始化。别一开始就上深度学习,否则出问题时,你根本分不清是物理问题还是网络问题。
还有一点很实际:保存和加载相位图时,格式和位深比很多人想象中更重要。如果你的SLM驱动只支持8位,算法再准也会被量化误差拖累,重建图出现条带状伪影。预算允许的话,尽量选10位或12位驱动的SLM,这个投入对最终效果的影响,往往比多跑几十轮迭代更明显。
4K计算全息这条路还很长,效率和质量的平衡也远没到尘埃落定的时候。但至少从LiftHolo的实践来看,通过学习式初始化和分段优化,我们完全可以在不牺牲太多质量的前提下,把生成耗时压缩一个量级。后面如果有更新进展,我再专门写一篇关于光学实验结果和散斑抑制细节的文章。