恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
单目深度估计+移动端视差渲染:让2D图片在手机屏幕“跃”出立体感
首页
资讯中心
/
单目深度估计+移动端视差渲染:让2D图片在手机屏幕“跃”出立体感
单目深度估计+移动端视差渲染:让2D图片在手机屏幕“跃”出立体感
发布时间:2026/10/22 5:00:13
1. 一张图“跃”出屏幕的现象与整体链路第一次看到一张普通图片在手机屏幕上“跃”出来的时候我的第一反应是这大概率不是真3D而是用深度信息做的视差效果。后来某潮流电商App在做商品展示时接入了这个能力我才把整条链路彻底摸透。这个功能现在有不少叫法3D空间图片、2D转3D、立体照片、视差图片。核心思路就一句话——给普通2D图片估算出每个像素的深度远近然后让画面在手机倾斜或手指滑动时产生不同速率的位移视觉上就像图片“浮”了起来。这篇文章不聊复杂的多视角重建只讲落到App里最实用的一条路单图深度估计加移动端视差渲染。适合客户端开发、算法工程师、前端同学以及想自己复刻一个Demo的技术爱好者。看完之后你会知道效果背后的技术选型为什么是这样深度图从哪来Shader怎么写上线之后又会踩到哪些坑。1.1 人眼立体感的来处先说一个底层问题为什么一张静止图片经过倾斜或滑动就能产生“立体感”人眼产生立体感的手段其实很丰富。双目视差是近距离时最核心的机制两只眼睛看到的画面略有差别大脑会通过视差算出物体的远近。但单目情况下没有这种天然的双目信息所以更多依赖运动视差当你转动头部时近处的物体在视野里移动得快远处的物体移动得慢。哪怕你闭上一只眼上下左右晃动脑袋依然能感受到空间层次这就是运动视差在起作用。“3D空间图片”复刻的正是运动视差。手机根据陀螺仪或触摸动作改变观察角度不同深度层级的画面在屏幕上位移速度不一样。前景商品跟着手势转得快背景转得慢甚至静止大脑就被欺骗了主动脑补出了前后关系。说白了这是一种视觉“骗术”但骗得足够自然体验上就足够像3D。1.2 为什么不是直接重建3D模型一开始接触这个需求的人都会想既然要立体展示为什么不直接用NeRF或3D重建生成一个真正的模型用3D渲染引擎播放真实产品落地时这个方案基本行不通。单张图片做完整三维重建本身是个病态问题物体背面看不到遮挡关系靠猜光照信息混乱生成出来的网格几乎都会有空洞、扭曲和贴图错误。就算勉强生成一个粗糙模型在商品展示这种高清特写场景里任何一点网格瑕疵都会被放大观感反而不如普通图片。另一方面真3D模型的数据量动辄几MB到几十MB网络加载成本高低端机跑起来还可能卡顿。所以电商类场景几乎都选择2.5D方案。深度图记录了每个像素的远近关系渲染时再配合相机位移产生视差。它没有完整的几何信息但对用户来说“看起来是立体的”已经足够了。深度图可以压得很小渲染也轻发布链路非常快。在交互要求不高的展示场景里这是性价比最高的路径。1.3 端到端技术链路整套能力的完整流程大概是这样的用户上传图片后服务端先做主体识别和图像分割把人像或商品抠出来然后使用单目深度估计模型生成深度图接着对深度图做压缩和编码客户端拿到原图和深度图后通过自定义Shader把深度信息映射为像素偏移最后结合陀螺仪、触摸事件驱动画面位移就得到了“跃屏”效果。这里有两条实现路线可选。一是纯客户端本地跑模型好处是响应快、不上传图片但模型体积和推理耗电是硬伤低端机基本跑不动。另一条是服务端离线生成深度图客户端只负责渲染。电商App普遍选后者因为深度估计模型可以随时更新算法团队不用发版就能迭代而且可以把计算压力集中在服务端客户端只维护一个轻量渲染器。服务端方案的代价是必须有网络。实际产品可以做成预生成策略运营在后台选定一批优质图片先离线生成深度图用户打开时直接加载处理结果。这样省去了实时推理等待体验也稳定得多。2. 深度估计如何让二维图片拥有“远近”深度估计是整条链路里最关键的环节深度图质量直接决定“跃屏”效果是惊艳还是翻车。单张彩色图估计深度在学术上叫单目深度估计Monocular Depth Estimation本质是让模型从颜色、纹理、阴影、边缘、语义先验里推测场景结构。近两年这个方向进展很快开源模型也很多落地时选择并不困难但要处理好细节。2.1 单目深度估计模型的选型经验我实际对比过几类常用方案简单整理一下模型选型特点适合场景落地难度传统MiDaS系列推理稳对纹理复杂场景鲁棒性较好相对深度场景要求不高时够用低有ONNX权重DPT系列精度高细节丰富但模型体积大离线批量生成精度优先中显存要求高ZoeDepth类能预测绝对深度室内外切换自适应需要真实距离信息时可选中体积略大Depth Anything数据预训练充分边缘和遮挡处理优秀电商商品图、通用场景效果惊艳低有轻量版上线首选从我踩过的坑来看选模型不能只看精度指标还要看三点一是推理内存服务端并发高每个请求占太多内存会拖垮集群二是边缘质量商品抠图后深度图边缘如果和主体对不齐渲染时会出现“一圈毛边”三是归一化方式的适配难度有的模型输出的是视差值有的输出的是相对深度处理逻辑完全不同。我最后常选的是Depth Anything这一级别的轻量模型再用少量业务图微调。它最大的优势是对图片内容的理解足够强即使背景混乱、遮挡严重生成出来的深度图也比较干净。实际推理可将输入尺寸限制在518x518左右速度和效果比较平衡。2.2 深度图的预处理与归一化模型输出的深度值不是直接拿来就能用的。不同模型输出的含义不同。视差disparity含义下数值越大代表离相机越近深度depth含义下数值越大代表离相机越远。如果拿到的是视差图通常要先做一次倒数变换depth 1 / disparity否则后期Shader计算会得到完全相反的前后关系。这一点最容易忽略我第一次接模型时就被这个反逻辑坑过画面向左倾斜时前景竟然往右跑特别诡异。拿到深度值之后还需要做归一化。模型输出的原始值范围不固定直接塞给渲染器会导致有的图“深过头”、有的图“平得像纸”。稳妥做法是取一定置信区间的min-max映射比如统计深度分布的0.5%到99.5%分位把这段映射到0到1。为什么要卡分位而不是直接取min和max因为深度图里偶尔会出现个别极端异常值比如某个像素深度值特别大或特别小直接min-max归一化会把正常的深度范围压缩得很窄画面层次感就丢了。归一化时还要注意前景与背景的平衡。商品图通常是一张产品加一个干净背景背景深度应该平滑且远离主体。如果整体图直接归一化背景可能会因为纹理少而出现大量噪点表现出来的就是背景区域深度值乱跳。2.3 主体分割与边界保护这是让商品图“跃屏”效果精良的关键一步也是最容易被忽视的一步。前面说过深度模型会给出整张图的深度。如果不对主体做保护商品和背景之间的深度过渡会很生硬倾斜手机时会看到商品边缘像是被硬生生“剪”出来的撕裂感特别强。实际操作中我会先用分割模型把商品或人像的mask提出来再分别对前景和背景做深度归一化前景的深度范围尽量拉满背景深度压平且推到远处。拿到mask后还要做边界处理。直接把mask当硬开关用边缘依然会有锯齿。我的做法是先对mask做羽化blur然后再用它做前景深度和背景深度的加权混合。这样商品边缘的深度是平滑过渡的视差变化时就不会出现“切纸片”的观感。羽化半径一般取图片宽度的0.5%到1%太大会让边缘模糊太小没效果。另外一个小技巧如果商品本身带有阴影阴影区域常常会被割裂成一块“深渊”画面看起来像是商品下面有个大洞。处理方法是把阴影区域合并进前景mask或者单独压低阴影区域的深度变化幅度。这个细节很多人不做但做了之后整个画面的立体感会自然很多。3. 移动端渲染从深度图到“跃屏”视差深度图生成完毕接下来就是客户端怎么把它变成用户眼睛看到的动态效果。渲染方案很大程度上决定了性能表现和兼容性需要反复权衡。3.1 两种实现路线网格扭曲与Shader偏移把深度变成视差效果主流做法有两种顶点网格扭曲和Shader像素偏移。顶点网格扭曲的做法是把图片均匀切成几十乘几十的顶点网格然后根据深度值将每个顶点在Z轴方向上拉高或压低形成一个真正的3D曲面相机绕场景移动时透视变化是物理正确的。听起来很正统但问题在于网格越细立体感越连续顶点数量越多网格越粗边缘穿帮越明显。而且做商品展示时商品边缘应该和背景分开但如果商品正好落在网格单元中间顶点偏移会把商品边缘拉变形出现“橡皮糖”效果。Shader像素偏移的做法是保持一个平面矩形不变在片元着色器里根据深度值修改纹理采样的UV坐标。简单来说就是让每个像素画出来的时候位置根据深度偏移一点。相机动了前景像素偏移大背景像素偏移小。这个方案的画面不会产生真正的几何变形但运动视差的观感相当接近而且渲染压力只有一次全屏纹理采样开销非常低。我从实际工程角度更倾向于Shader像素偏移。原因很现实电商App里的基础控件大多是列表和详情页渲染节点要轻还要兼容大量中低端机型顶点网格方案一旦场景复杂就吃力而Shader偏移方案在任何GPU上都能跑得很稳定。3.2 Shader实现的核心逻辑一个最简的GLSL片元着色器长这样precision highp float; uniform sampler2D uColorTex; // 原图 uniform sampler2D uDepthTex; // 深度图 uniform vec2 uViewOffset; // 由陀螺仪或触摸换算出的偏移量 uniform float uMaxOffset; // 最大视差偏移范围通常0.02~0.06 uniform float uStrength; // 深度强度系数 varying vec2 vUv; void main() { // 采样深度深度图是单通道灰度 float depth texture2D(uDepthTex, vUv).r; // 深度越大表示越远偏移越小深度越小表示越近偏移越大 float factor (1.0 - depth) * uStrength; vec2 shifted vUv uViewOffset * factor * uMaxOffset; // 边缘保护超出纹理边界的像素直接涂成透明或背景色 if (shifted.x 0.0 || shifted.x 1.0 || shifted.y 0.0 || shifted.y 1.0) { gl_FragColor vec4(0.0, 0.0, 0.0, 0.0); return; } gl_FragColor texture2D(uColorTex, shifted); }这段代码里最核心的是偏移方向。uViewOffset是相机位移导致的UV偏移量depth是采样到的深度值。depth为0的位置代表最近偏移最大depth为1代表最远几乎不动。这样可以保证相机向某个方向移动时前景“压”过去、背景“留”在原位立体感自然出现。实际项目里不会直接判断坐标越界因为那样会露出黑边。更好的方式是对越界部分做纹理拉伸或采样一个模糊的背景图让视觉上不会出现生硬空洞。3.3 边缘补色与背景防露底有过经验的人都知道一旦视差偏移发生原图覆盖不了的区域就会露出来。最典型的表现是图片往右移动时左边会出现一条深色边图片往左移动时右边露馅。这个现象业内叫“露底边”。解决露底边的方法有三层。第一层是在生成深度图时就把图片往外扩一圈内容。可以简单地把原图向四周放大然后让放大后的图作为纹理再叠加深度偏移这样同时把“虚拟边缘”和“边缘过渡”一起处理好。第二层是Shader内部做边界填充CLAMP_TO_EDGE能解决一部分情况但会形成条纹拉伸高级一点的做法是对边缘区域做镜像采样视觉上比较自然。第三层是直接增加一种“背景虚化”策略把露出的底边区域叠加一层高斯模糊后的画面这种在带背景的商品图里效果最好。我做项目时会直接采用“放大纹理加镜像采样”的组合简单且稳。关键点是放大比例要大于最大视差偏移量否则即使放大也不够覆盖。3.4 交互反馈陀螺仪、触摸和阻尼视差效果要让人舒服还需要交互层的配合。陀螺仪交互是最自然的。手机略微倾斜画面就跟随变化用户不需要额外操作。这里要注意处理好坐标系竖屏下重力方向分量的变化要转换成相机在X轴和Y轴的位置偏移。实际使用时要增加低通滤波否则手抖会让画面高频抖动看着头晕。触摸滑动交互则更符合“主动浏览”的行为。手指拖动时相机位置跟随手指位移松手后画面要有阻尼回弹效果慢慢回到居中位置。回弹动画最好用弹簧阻尼模型而不是线性的补间动画。线性动画会有明显的“机械感”弹簧阻尼则更有物理手感。还有一个人性化细节开启3D空间图后用户会频繁晃动手机屏幕容易误触。建议在进入3D展示模式时把页面滚动锁定用户点击“关闭3D”或返回时才恢复。否则用户正看商品细节手指一滑页面就翻走了体验直接崩盘。4. 实操复刻离线生成深度图 客户端渲染Demo理论知识聊完直接进入可以抄作业的实操环节。这里以“Python离线生成深度图 OpenGL渲染视差”为例串起一条最小可用的完整链路。如果你是前端也可以把渲染端换成WebGL思路完全一致。4.1 准备工具和依赖服务端深度估计依赖不多推荐环境如下Python 3.10 onnxruntime opencv-python numpy Pillow模型方面选择开源的Depth Anything ONNX权重下载后放在本地模型目录。这个模型经过大规模数据预训练对商品、人像、户外场景的适配都很好而且有超轻量版本服务端CPU推理也能跑得动。客户端Demo用Android平台的Kotlin加GLSurfaceView只依赖系统OpenGL ES 3.0不需要额外引入引擎。如果你熟悉Web方向完全可以用Three.js的ShaderMaterial复刻同样效果渲染逻辑几乎一致。4.2 Python脚本单图深度估计下面是关键脚本用ONNX Runtime跑一次深度估计并把结果保存为16位PNGimport cv2 import numpy as np import onnxruntime as ort MODEL_PATH depth_anything.onnx def load_model(model_pathMODEL_PATH): sess ort.InferenceSession(model_path, providers[CPUExecutionProvider]) return sess def estimate_depth(sess, img_bgr, input_size518): h, w img_bgr.shape[:2] # 预处理调整尺寸 归一化 img cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB) img cv2.resize(img, (input_size, input_size)) img img.astype(np.float32) / 255.0 mean np.array([0.485, 0.456, 0.406], dtypenp.float32) std np.array([0.229, 0.224, 0.225], dtypenp.float32) img (img - mean) / std img img.transpose(2, 0, 1)[None] # 1,3,input_size,input_size input_name sess.get_inputs()[0].name pred sess.run(None, {input_name: img})[0] # 1,1,size,size depth pred[0, 0] # 双线性插值回原尺寸 depth cv2.resize(depth, (w, h), interpolationcv2.INTER_LINEAR) # 分位归一化避免极端值影响 low, high np.percentile(depth, [0.5, 99.5]) depth np.clip((depth - low) / (high - low 1e-8), 0.0, 1.0) # 保存为16位深度图 depth_16bit (depth * 65535.0).astype(np.uint16) return depth_16bit if __name__ __main__: sess load_model() img cv2.imread(input.jpg) depth estimate_depth(sess, img) cv2.imwrite(depth.png, depth)注意几个容易出错的地方一是ONNX模型输入尺寸要和训练时一致直接用518即可二是输出图需要从模型分辨率resize回原图尺寸否则Shader采样时深度图和原图对不上三是16位PNG保存时OpenCV默认会写成16位不要手动转成8位否则深度层次大量丢失。做到这一步你手上已经有了一张和原图尺寸完全一致的灰度深度图。黑色代表离相机近白色代表离相机远。在电脑上打开这张图应该能明显看到商品边缘轮廓有一个“亮色描边”这正是分割或者说深度突变的表现。4.3 GLSL Shader带边缘保护的商品图视差有了深度图下一步就是在客户端把原图和深度图一起送到Shader里。这里是带边缘镜像采样的改良版GLSL代码precision highp float; uniform sampler2D uColorTex; uniform sampler2D uDepthTex; uniform vec2 uViewOffset; uniform float uMaxOffset; uniform float uStrength; varying vec2 vUv; vec2 clamp_uv(vec2 uv) { // 优先使用镜像采样避免边缘拉伸成条带 return vec2( mix(0.0, 1.0, abs(fract(uv.x) - 0.5) * 2.0), mix(0.0, 1.0, abs(fract(uv.y) - 0.5) * 2.0) ); } void main() { float depth texture2D(uDepthTex, vUv).r; float factor (1.0 - depth) * uStrength; vec2 shifted vUv uViewOffset * factor * uMaxOffset; shifted clamp_uv(shifted); vec4 color texture2D(uColorTex, shifted); // 边缘轻微压暗提示用户已经浏览到边界 float edgeFade smoothstep(0.9, 1.0, abs(shifted.x - 0.5) * 2.0); color.rgb * mix(1.0, 0.88, edgeFade); gl_FragColor color; }这段代码里的clamp_uv是核心技巧。它会将超出[0,1]的坐标按镜像方式映射回图片内部比CLAMP_TO_EDGE的拉伸效果柔和很多。当图片向左移动时右侧露出的边缘不是一条硬黑边而是一段镜像出来的内容视觉上几乎看不出破绽。实际试验中镜像采样配合轻微压暗边缘已经能满足绝大多数商品展示场景。如果你希望效果更精致还可以在Shader里叠加一层“背景虚化层”当UV超出边界时不是镜像原图而是采样一张模糊背景图。这个方案适合商品在纯色背景上的图观感最干净。4.4 参数调优与效果控制渲染效果好不好参数调整占一半。下面给出我常用的参数范围和推荐值参数含义范围推荐值备注uMaxOffset最大视差偏移量0.01~0.080.04过大容易露底边uStrength深度强度0.3~1.00.7商品图建议高一点陀螺仪平滑系数滤波强度0.05~0.30.12越小越灵敏但易抖动回弹阻尼松手后回中速度0.85~0.980.92太大会晃太多次调参时有几个经验可以沿用。看边缘是否先“飘起来”。如果商品和背景的深度差值太大边缘会明显早于其他区域位移形成“纸片滑移”。此时应降低uStrength或者对深度图做轻微模糊。看背景是否会“滚”。如果背景纹理明显倾斜手机时背景跟着动得很厉害画面会显得很“飘”。处理办法是单独给背景深度一个低权重或者直接把背景深度拉平。看低端机是否发热。如果发热明显优先降低渲染分辨率。做法是把原图和深度图都缩小到屏幕宽度的一半再送纹理视觉差异不大但GPU带宽压力能小不少。很多团队会在这里纠结画质实际上在手机屏幕上缩到0.75倍基本看不出区别。5. 上线后的坑与排查速查再好的效果上线前都要过一道残酷的兼容性测试。我在几个不同项目里踩过的坑基本都集中在深度图、Shader精度、性能和后端链路这几个方向。5.1 深度图条带、错乱与边缘撕裂深度图出现横向条带往往是深度图被错误压缩成了8位JPEG。深度信息需要的是连续层次JPEG的压缩噪声和8位量化一叠加渲染时就会出现“水波纹”或“等高线”。排查时第一件事就是确认深度图格式和位深线上建议用16位PNG或WebP无损格式。深度错乱则多半是模型输入尺寸和归一化方式不匹配。更换模型后没有同步修改归一化逻辑导致前景后景反转。我在实际项目里遇到过训练时用0代表近、推理时按0代表远的情况最后画面前后完全颠倒。排查方法是打印深度图的min、max和均值看数值分布是否合理。边缘撕裂多半来自分割mask的边界掉帧。如果商品轮廓被分割模型切掉一部分深度图里那一块就会变成背景深度渲染时商品边缘会出现凹坑。解决办法是对mask做一次闭运算再补一圈固定宽度的深度“过渡带”。5.2 低端机性能与发热3D空间图片是一个高频交互功能用户会不停晃动手机GPU和陀螺仪都处于高负荷状态。在低端机上全分辨率Shader渲染加陀螺仪高频采样电池温度几分钟就能上来。我建议做三个降级策略。第一档判断设备GPU性能较弱时关闭陀螺仪交互只保留触摸拖动且滑动时略微降低视差偏移量第二档渲染分辨率降到屏幕尺寸的0.75倍第三档连续运行30秒以上且温度超过阈值时自动切回普通静态图片并提示用户可点击重新开启3D效果。降级策略不是产品经理眼里的“偷懒”而是保护体验的必要手段。用户不会因为你给了一台千元机3D效果而夸你但会因为手机发烫而卸载App。5.3 人像模式深度图复用这里分享一个特别实用的小经验很多手机自带的人像模式照片会保存一张DepthMap元数据有些甚至存成辅助.jpg文件。如果你要处理的图片来自iPhone或其他品牌手机的人像摄影优先读取自带的深度图再上传成本几乎为零效果会比通用模型好一大截。读取时需要注意坐标系对齐。手机拍摄的深度图分辨率通常远低于原图且可能存在裁切偏移。网上有很多现成工具可以提取我建议在自己的处理流程里把“是否优先使用EXIF深度图”作为第一个判断分支。这个策略在内容社区场景尤其好用因为用户上传的自拍图比例很高。5.4 隐私合规与灰度发布图片是高度隐私的素材深度图虽然只是“远近关系”但结合原图分析可以还原出空间布局。用户上传的图片一旦进入深度估计服务就相当于把图像内容交给了另一个处理链路。上线前务必确认图片传输全程加密、深度图处理服务不留存原图、处理任务自动销毁、用户协议里明确告知3D功能会上传图片做深度处理。功能发布也不要一把梭。建议灰度放量先覆盖平台上最好看的1000张商品图观察用户点击率、停留时长和分享率。这里尤其要看“分享后二次打开率”如果3D效果只是炫技用户截屏分享后点开没有3D效果因为对方看到的是静态图分享转化会明显下降。进阶做法是分享时自动生成一段短小的视频动画这样接收方也能看到立体效果。5.5 问题排查速查表现象可能原因排查动作画面完全无视差深度图全黑或全白检查归一化分位打印深度值范围前景后退、背景前进深度含义反了确认是视差还是深度必要时取倒数商品边缘碎边mask硬边界对mask做羽化和闭运算背景抖动严背景纹理太丰富压平背景深度或提高背景降噪权重图片两侧出现黑边偏移量超过补边范围放大纹理或改用镜像采样低端机卡顿发热全分辨率渲染降级渲染分辨率、关闭陀螺仪部分机型画面出现条带mediump精度不足强制使用highp精度6. 做得多了才会注意到的细节功能做完能跑和功能做好能用中间隔着大量细节。这些内容可能不在任何PRD里但直接决定用户体感。6.1 深度强度不是越大越好新手最容易犯的错误是把深度强度拉满觉得越“凸”越好。实际上过度视差会让图片边缘频繁露底同时会放大深度图的噪声画面变得又“脏”又“晕”。我自己的经验是首次实现先用0.5的强度和平面图放在一起做AB对比让不参与开发的人盲测选“更自然”的那个然后再微调。用户要的是“有点神奇”的立体感不是VR过山车。6.2 让运营主动提供“素材友好”的图片3D效果对输入图片有天然的偏好。纯色背景、主体居中的商品图深度估计效果最好复杂场景、多重遮挡、透明物体的图片效果会明显打折扣。运营初期需要建立一套“3D友好图片”规范主体占比不超过70%、背景尽量简洁、主体边缘清晰。这听起来不像技术问题但实际对上线效果的影响比调参还大。6.3 降级策略比效果本身更重要用户环境千差万别有的手机屏幕刷新率只有60Hz有的用户会把系统动画关掉有的WebView环境不支持高精度Shader。我建议把“3D空间图片”设计成一个渐进增强能力而不是不可降级的开关。检测到设备不支持时显示普通图片支持时默认关闭3D等用户点击“3D看”按钮再开启。这样既保证基础体验又不会因为强制效果流失用户。6.4 最后的实用小技巧最后分享一个我常用的运营手法给纯色背景的商品图手动构造一张“中心近、四周远”的径向渐变深度图前景商品不动背景从中心向四周逐渐远去。这样即使用户不滑动屏幕只要手机轻轻一斜画面就像在一个无限深的空间里缓缓转动。这个手法在活动页、秒杀页里非常好用因为根本没有做任何算法处理只是一张渐变图加一个Shader线上稳定而且成本几乎为零。做这个功能这么久我的体会是3D空间图片与其说是算法问题不如说是渲染工程和体验打磨问题。深度估计现在已经很成熟真正拉开差距的是边缘处理、性能降级、交互手感这些“看不见”的细节。从模仿开源Demo到真正上线中间需要解决的问题远不止这几个但把上面这些坑踩过一遍你已经能比大多数人更懂“一张图跃出屏幕”背后的全部秘密。