展示效果
代码实现完成于半年前,本文由 claude opus 4.6 协助创造。
起因
之前看到苹果 ios26 里相册原生支持了图片伪 3d 展示的功能,觉得挺有意思。
一张普通的 2D 照片,如果能根据画面内容产生真实的深度感,转动的时候前景和背景有视差——那不就是全息的感觉了吗?
能不能自动把一张普通照片变成全息图?
直接在网页中展示,让安卓也能体验到类似的效果。
核心思路很简单:
- 用 AI 模型估计照片的深度信息
- 把图片按深度分层(前景/背景)
- 用 Three.js 把多个层叠在一起做 3D 渲染
- 手机上用陀螺仪控制视角,电脑上用鼠标拖拽
处理图片
1. 深度估计
使用 Depth-Anything-V2 模型生成深度图。这个模型效果相当不错,能从单张照片中估计出比较准确的深度关系。
python test.py input.png gray.png输入一张照片,输出一张灰度深度图,越亮的像素表示离相机越近。
对性能的要求也不高,直接用电脑跑就挺快的,手机硬件应该也足够。
2. 基于梯度的图层分割
有了深度图之后,关键问题是:怎么把前景和背景分开?
最直观的方法是按灰度值阈值切割,但这样边缘会很生硬。我实际采用用了基于 Sobel 算子的梯度分割方案:
// Sobel 算子
const sobelX = [
[-1, 0, 1],
[-2, 0, 2],
[-1, 0, 1],
];
const sobelY = [
[-1, -2, -1],
[0, 0, 0],
[1, 2, 1],
];对深度图每个像素做 3×3 卷积,计算梯度幅值。梯度大的地方就是深度突变的边缘——也就是前景和背景的分界线。
具体流程:
- 计算梯度:对每个像素施加 Sobel 算子,得到水平和垂直方向的梯度
- 阈值检测:标记梯度幅值超过阈值的像素为边缘
- 连通域分析:用 flood fill 找到边缘像素的聚类,过滤掉太小的区域
- 梯度扩散:沿梯度方向在 180° 半圆内扩散,生成更平滑的分割蒙版(主要是为了深度图扩散,避免深度变化太快导致 threejs 展示效果不好)
最终会生成一个背景图和主体图,其中主体图中梯度过大的地方也会变成透明像素,这里主要是为了避免梯度过大的地方在 threejs 中渲染效果很不好看。 以及还有背景图对应的深度图,主体图对应的深度图(经过扩散处理过,避免边缘处梯度变化)。
这里因为考虑到背景修复比较慢,所以没有对图片进行多层分割,只按照两层来处理,所以像上面图里的腿的边缘出就会出现断裂的情况。
一个更好的方案可能是,按照灰度阈值将图片分隔为多层,灰度阈值采用能让梯度阈值像素点下降最多的阈值,这样可以获取多个灰度阈值,直到尽可能把图里的超过梯度阈值的像素点减少。
让一个图片分成更多层,展示效果更好。 (但是处理更麻烦,需要对多层图片做补全,对性能要求更高)
3. Inpainting 背景修复
前景被抠出来之后,背景上会留下一个”洞”。这里用了 LAMA(Large Mask Inpainting)模型来填补。
LAMA 是一个专门做图像修复的神经网络,效果也差强人意吧,只能说勉强能看。主要是能在笔记本上可以直接跑,虽然一张图可能也要跑几分钟…
如果有条件,用最新的 ai 补全的模型效果会更好。
4. 输出资源
最终每张图片会生成四个文件:
main.png— 前景图(带透明通道)main-depth.png— 前景深度图bg.png— 修复后的完整背景图bg-depth.png— 背景深度图
如果切割更多层数,文件会更多。
再用 pngquant 做有损压缩,减小体积。
Three.js 3D 渲染
有了分层的图片和深度图,就可以用 Three.js 渲染了。
核心原理是 Displacement Mapping(位移贴图):把深度图作为位移贴图绑定到平面网格上,深度值会控制每个顶点沿法线方向的偏移量,让平面产生凹凸起伏。
const material = new THREE.MeshStandardMaterial({
map: imageTexture, // 彩色贴图
displacementMap: depthTexture, // 深度图作为位移贴图
displacementScale: 0.5, // 位移强度
transparent: true,
alphaTest: 0.1,
side: THREE.DoubleSide,
});
const geometry = new THREE.PlaneGeometry(
planeWidth, planeHeight,
512, 512 // 足够的细分段数才能体现位移效果
);前景和背景各一个平面,叠在一起。由于前景的深度图数值更大(更亮),位移后自然就”浮”在背景前面了。转动视角时,两层之间就会产生视差,形成立体感。
在手机上,通过 DeviceOrientationEvent 获取陀螺仪数据,把手机的倾斜角度映射为 3D 场景中的旋转角度:
// 计算相对于初始位置的变化
const deltaBeta = event.beta - initial.beta;
const deltaGamma = event.gamma - initial.gamma;
// 映射到旋转角度,限制在 ±10 度内
targetRotation.rotationY =
THREE.MathUtils.clamp(deltaGamma / maxTilt, -1, 1) * maxRotationRad;
targetRotation.rotationX =
THREE.MathUtils.clamp(deltaBeta / maxTilt, -1, 1) * maxRotationRad;为了让旋转手感更好,加了线性插值做平滑:
const newRotationX = current.rotationX +
(target.rotationX - current.rotationX) * smoothing;还做了一些细节处理:
- 自动回中:手机静止 800ms 后,视角缓慢回到正中间
- 呼吸动画:桌面端无操作 2 秒后,画面会轻微摇摆,像在”呼吸”
- 按需渲染:只在有变化时才渲染,节省性能
性能优化
- 像素比限制为
Math.min(devicePixelRatio, 2),避免高分屏过度渲染 - 网格细分段数根据图片尺寸自适应,在 512~1024 之间
- 启用视锥体剔除
frustumCulled = true - 使用
LinearFilter代替 mipmap,减少纹理内存
最后
这个项目从想法到完成大概花了半个国庆假期,半年后再看,可能交给现在最新的模型,实现时间更短,实现效果也会更好。
批量生成了一些图,效果都还不错。可能像画面切腿的问题还需要靠分割更多层来解决。
本来是想搞个网页能支持手机上传自定义图片后预览全息效果,在手机跑这一套流程的。手机性能的话,可能图片修复需要用性能要求更低的算法,其他的应该没啥性能问题,但可能会有模型下载的问题,模型体积会比较大。
感觉太麻烦了遂作罢hhh
以及再扯一句,由普通图片生成裸眼 3d 的视差图片,也是一样的技术原理。
感谢看到这里~