实时2D转3D:基于深度估计与DIBR的移动端SBS渲染实践
2026/9/24 21:06:49 网站建设 项目流程

1. 为什么折腾到 tachi 与 SBS 这条路上

这件事其实是被逼出来的。手头有几个 VR 盒子,头显也入了门,但每次想找点片源看看就头疼。网上能下到的 3D 资源基本还是十年前那批,分辨率低、码率差,有些还带着莫名其妙的 logo。而新出的电影、游戏实况、综艺,几乎全部是普通 2D 片源,拿头显看也就是放大了的 2D 画面,跟窝在沙发上看电视没有本质区别。于是我开始琢磨,能不能在手机上把 2D 内容实时变成 3D 再喂给头显或者裸眼 3D 设备看。

翻了一圈市面上的方案,各自的短板都很明显。先列一下我当时比较过的几条路线:

  • 离线 2D 转 3D 工具:像 DVDFab 那类软件,转换效果确实可以做到很精细,但一部电影要转几个小时甚至一宿,转完硬盘还要多占好几个 G。最要命的是它不实时,等转换完成,我连看片的兴致都没了。
  • 直接下载成品 3D 片源:资源少,更新慢,很多是重编码的"翻录版",画质损失严重。遇到想看的片没有 3D 版,等于白搭。
  • 播放器自带的假 3D 滤镜:手机上某些播放器有"仿 3D"效果,其实就是给画面边缘加了点模糊和偏移,看起来像纸片人贴图,毫无空间感。
  • 专用的实时 2D 转 3D 方案:例如 GitHub 上 tachi 这类项目,用端侧推理做单目深度估计,再通过视差渲染输出 SBS(Side-by-Side)画面。这条路实现难度最大,但一旦跑通,任何 2D 视频、相机画面、游戏画面都能随时变成 3D。

我最终就是被 tachi 这几个字给"勾住"的。它解决了我最痛的点:不挑片源,打开就能用,而且是实时的。SBS 则是最稳妥的输出格式——几乎所有的 VR 盒子、VR 头显、裸眼 3D 手机屏幕都认它,甚至电视上切换到"左右 3D"模式也能直接显示。它在技术上没有那么花哨,但兼容性就是它的最大价值。

如果你也是那种"手里的设备吃灰太久、不甘心"的人,或者本身就是对端侧 AI 落地感兴趣的开发者,这篇文章应该能给你一些参考。我尽量把原理和实操都讲透。

2. 先把核心原理理清楚:视差是假的,但效果是真的

很多人一听"2D 转 3D",第一反应是"这玩意儿不靠谱吧,一张图怎么可能凭空多出深度"。这个怀疑是有道理的,因为整个过程确实不是"复原"真实的三维场景,而是根据图像内容"推断"出哪块近、哪块远,再用这个推断结果生成一对带有视差的左右眼视图,骗过我们的大脑。

2.1 人眼立体感的来源:这事情比想象中简单

人眼感知立体,核心靠的是两个眼睛看同一物体时,因为视角不同而产生的视差。你把手指伸到眼前,闭上左眼用右眼看,再闭上右眼用左眼看,会发现手指相对背景的位置变了,这个变化就是视差。大脑把左右眼的两幅画面叠加起来,根据差异大小判断距离。

所以 2D 转 3D 的本质就一句话:制造出合理的左右眼视差。只要左右眼视图的视差关系符合物体的前后位置关系,大脑就会自动产生深度感。至于这个深度是从哪算出来的,大脑其实并不关心。

2.2 从一张图算出深度:单目深度估计模型在做什么

关键在于"怎么知道画面里谁远谁近"。传统做法是双目摄像头、结构光或者 ToF 传感器,但手机上普遍只有一个摄像头,只能靠单目深度估计

单目深度估计的思路听起来有点"反直觉":没有第二个视角,也没有主动照射,纯靠一张彩色图去猜每个像素的相对距离。它凭什么能猜出来?靠的是经验。模型在大量带深度标注的数据集上训练,学习了大量"环境先验"——比如远处的山是灰蓝色的、近处的地面纹理更清楚、同一个物体表面的颜色通常连续、物体之间的遮挡关系可以提供前后信息等等。推理时,模型把这些先验综合起来,输出一张深度图,每个像素值代表该位置离相机的大致远近。

tachi 底层用的就是这类基于深度学习的单目深度估计模型。常见的开源模型有 MiDaS、ZoeDepth、DPT 等,其中 MiDaS 系列因为对移动端算力的要求相对宽松,被很多端侧实时项目拿来用。MiDaS 的核心特点是在多数据集上联合训练,对户外、室内、人物、风景都有不错的泛化能力。

2.3 深度图怎么变成左右眼视图:DIBR 渲染链路

拿到了深度图,不等于就完成了 3D 转换。深度图只是告诉你"谁深谁浅",要生成左右眼视图,还需要一步叫DIBR(Depth-Image-Based Rendering,基于深度图像的渲染)的操作。

DIBR 的原理可以理解为:假设我们要模拟左眼的位置,那么画面里的近处物体应该相对向右偏移,因为左眼看近物时它偏右侧;反过来模拟右眼时,近处物体应该向左偏移。偏移量的大小由该像素的深度值决定——越近的物体偏移越大,越远的物体偏移越小,远景甚至偏移几乎为零。

实际实现时,一般流程是:

  1. 把深度图转换为视差图,视差值的公式是视差 = 基线距离 × 焦距 / 深度。这个公式是从双目匹配的原理推过来的,你可以简单类比成:你左右移动脑袋去看同一个物体,移动距离越大、离得越近的东西,在视野里"跳"得越多。
  2. 以原始 2D 图像为基准,逐像素地按照视差图向左或向右挪动像素位置。
  3. 左右眼各自产生一张"新视角"的图。

但这里马上会遇到一个实际问题:像素挪动之后,原来被前景遮挡的背景区域会出现空洞,有点像我们把贴纸从墙上撕下来,墙上留出空白。处理空洞有几招:

  • 邻域采样填充:直接用空洞周围的像素填进去,适合空洞不大的情况。
  • 边缘扩展拉伸:把边缘像素向外复制,适合背景纹理简单的画面。
  • 更彻底的方案:先做背景修复,或者在渲染时根据深度做多层背景插值。

实时场景下,基本只能选第一种或第二种,因为计算量有限。实际效果上,只要空洞不集中在人眼最关注的物体边缘,观感是完全可接受的。

2.4 SBS 结构:左右眼画面怎么并排放进一个画面

SBS 全称 Side-by-Side,就是左右眼画面并排放在同一个视频帧里的格式。一个 1920×1080 的 SBS 画面,左半部分是左眼视图,右半部分是右眼视图。更细分有全宽 SBS半宽 SBS

  • 全宽 SBS:左右眼各自用了完整的 1920×1080 分辨率,总画面是 3840×1080,需要播放设备具备 4K 级别的解码能力。
  • 半宽 SBS:左右眼各自是 960×1080,整个画面是 1920×1080,观看时再被拉伸到全屏。这是移动端最常用的格式,节省带宽和渲染压力。

tachi 的 SBS 实现方案里,半宽 SBS 基本是默认选项。原因很简单:手机解码全宽 SBS 压力极大,帧率稳不住,而半宽 SBS 虽然横向分辨率砍半,但经过头显镜片放大后,实际观感损失并不至于不可接受。尤其是对动态画面,人眼的注意力更多在空间感和流畅度上,而不是静态解析力。

3. tachi 的 SBS 实时实现链路拆解

这一章是整篇博文的重头戏。前面那些原理如果算"道",那这一步就是"术":具体怎么在手机上把一条完整的实时管线搭起来。tachi 的整体架构大概是下面这个链路:

源帧获取(相机 / 视频解码) → 帧缩放与格式转换 → 端侧深度推理 → 深度图后处理 → DIBR 视差渲染 → SBS 合成 → 显示输出

这中间每一步都有不少坑,我一个个拆开讲。

3.1 端侧推理框架选型:NCNN vs ONNX Runtime vs MNN

tachi 在模型推理层选了 NCNN,这个选择我实际用下来认为是对的。当时我在 NCNN、ONNX Runtime Mobile、MNN 三个框架之间反复横跳,最后发现 NCNN 更适合这个场景,大概有这几个原因:

  • 移动端算子覆盖全:单目深度估计模型里常见的卷积、上采样、激活函数、残差连接,NCNN 做了大量 ARM 汇编级优化,在手机 CPU 上跑得很快。尤其是 4×4、2×2 的上采样,NCNN 有专门的优化实现。
  • 部署体积小:整个 NCNN 核心库加在一起只有几百 KB 级别,对安装在手机上的 App 来说非常友好。
  • 量化工具链成熟:NCNN 提供了从.pt.onnx转换的完整工具链,也支持 FP16、INT8 量化。

当然 ONNX Runtime Mobile 也有它的强大之处,尤其是对 GPU 算子的支持和跨平台一致性更好,但内存占用比 NCNN 多一些。MNN 的推理速度在部分芯片上能跟 NCNN 打成平手,但生态和文档相对 NCNN 还是差一点。

如果你是第一次接触这个领域,我建议:CPU 推理为主就选 NCNN,想利用 GPU 加速就仔细考虑 Vulkan 或者 OpenCL 后端,不要一上来就追求多框架支持,先跑通一条链路比什么都重要。

3.2 模型压缩与量化:让深度模型跑在手机算力上

单目深度估计模型原始版本体积不小,比如 MiDaS 的大模型 DPT 系列,单次推理在桌面级 GPU 上也要几十到上百毫秒,放到手机上直接卡成幻灯片。tachi 的思路是引入轻量级 backbone 的 MiDaS 变体,比如用 MobileNet 系列的变体模型,把输入分辨率控制在比较低的水平,比如 256×256,再配合量化压缩。

我实际试验下来,FP16 量化是实时处理的最佳平衡点。INT8 量化虽然能进一步提速,但深度图的边缘会明显发糊,后续 DIBR 时容易产生"边缘锯齿"和"天空撕裂"一样的伪影。FP16 在主流手机上都能由 CPU 直接运算,或者在 GPU/NPU 上获得不错的加速。

模型推理分辨率的选择也很有讲究。原始相机画面如果是 1080p,你完全不需要在 1080p 上做逐像素深度推理,那算力要求太高了。通常的做法是:

  • 把原始帧缩小到 320×240 或者 480×270 左右;
  • 在这个低分辨率上做深度估计;
  • 再把深度图上采样回需要的渲染尺寸,作为渲染阶段坐标偏移的依据。

深度图分辨率低于原始画面反而有好处:深度本身的精度不需要特别高,低分辨率图自带一定的平滑效果,能抑制深度图的高频噪声。

3.3 视频取流与帧同步:MediaCodec 和相机两条入口

tachi 需要同时支持视频文件/流播放和相机实时画面两类来源,这两条路的取帧方式完全不同。

先看视频场景。手机上的视频解码基本都是通过 MediaCodec 做的,而 MediaCodec 最典型的接入方式有两种:一是直接输出到 Surface 显示,二是配合SurfaceTexture把解码帧作为 OpenGL 纹理做后处理。tachi 选择的是第二种:MediaCodec 解码输出到 SurfaceTexture,然后渲染线程通过updateTexImage()获取最新纹理,再交给后续管线

这里有个大坑:解码帧率并不是均匀的,尤其在高码率视频或者硬解能力不足的机型上,帧与帧之间的间隔波动很大。如果你的处理管线过于繁琐,很容易出现一帧还没处理完,下一帧就已经到了的情况。典型的解决办法是维护一个帧缓冲池,渲染线程和解码线程解耦,解码线程只管往缓冲池里塞帧,渲染线程空闲时再取最新帧处理。中间允许丢旧帧,保证实时性优先。

再看相机场景。相机的取流走 Camera2 API,tachi 的做法是使用ImageReader或者ProcessingSurface方式获取 YUV 帧。如果 Camera2 的输出帧率跟不上处理速度,同样会出现缓冲区堆积。更常见的问题是相机参数变化对推理结果的干扰:自动对焦、曝光变化、白平衡漂移都会让深度推理输出的深度图产生跳变。

我建议在做相机类内容时,尽量固定对焦模式为连续,或者干脆锁定在无穷远,把曝光时间限制在一个相对稳定的区间。这样深度估计的输入视频帧特征比较稳定,输出深度图不会频繁跳变。

3.4 DIBR 实现:用 OpenGL ES Shader 把视差渲染做到接近零拷贝

深度推理出来的深度图要参与渲染,最忌讳的是把深度图从 GPU 内存拷回 CPU,做完偏移计算再拷回 GPU,那带宽和延迟直接劝退。正确的做法是把 DIBR 整个搬到 GPU,用 Shader 做像素级处理。

以 OpenGL ES 为例,核心思路是:

  1. 把原始彩色帧作为采样纹理u_colorTexture
  2. 把深度图作为另一个采样纹理u_depthTexture
  3. 顶点着色器不做什么特殊操作,片元着色器里对每个像素做一次视差偏移采样。

片元着色器的核心逻辑大致如下(半宽 SBS 渲染场景):

// 片元着色器:为左眼/右眼生成偏移采样坐标 precision mediump float; uniform sampler2D u_colorTexture; uniform sampler2D u_depthTexture; uniform float u_baseOffset; // 基础偏移强度,可调,用于控制视差强度 uniform int u_eye; // 0 表示左眼,1 表示右眼 varying vec2 v_texCoord; void main() { // 采样当前像素的深度值,normalize到 [0,1],越近深度值越小(具体映射视模型而定) float depth = texture2D(u_depthTexture, v_texCoord).r; // 根据左右眼方向计算偏移方向 float direction = (u_eye == 0) ? 0.5 : -0.5; // 近处物体偏移大,远处物体偏移小 float offset = (1.0 - depth) * u_baseOffset * direction; // 半宽 SBS 下,左右眼的采样坐标要映射到原始画面的一半区域 vec2 colorCoord = v_texCoord; // 这里 v_texCoord 在 SBS 输出帧里的坐标范围是 [0,1],但左右两半各对应原画面 // 因此需要把 SBS 的半区域坐标映射回原图坐标 vec2 st = vec2(colorCoord.x * 2.0 - float(u_eye), colorCoord.y); // 加上视差偏移并采样 vec2 finalCoord = st + vec2(offset, 0.0); // 处理边缘越界,用边缘像素填充 finalCoord = clamp(finalCoord, 0.0, 1.0); gl_FragColor = texture2D(u_colorTexture, finalCoord); }

这段 Shader 做了三件事:读取深度、按眼别计算偏移方向、把 SBS 输出坐标映射回原图。实际工程里还要加上u_baseOffset的调节,这个参数对观感影响极大,后面我会专门说。

当然,这只是一个最简版本的 DIBR。更精细的实现会考虑空洞填充、边缘羽化和深度图平滑,这些可以在片元着色器里加几行逻辑,但性能成本要自己掂量。

在 tachi 的实际代码里,合成 SBS 的阶段是利用 FBO(帧缓冲对象)把左右眼图像分别渲染到输出画面的左半和右半区域。整个过程在 GPU 内部完成,不需要 CPU 介入,因此称为"近零拷贝"。

3.5 系统管线串联:从源帧到 SBS 输出的路径整理

把前面几节串起来,整个处理流是这样的:

  1. 源帧输入:MediaCodec 或者 Camera2 把帧作为 GL 纹理送进来。
  2. 预处理:通过渲染到 FBO 的方式,把纹理缩放到模型输入尺寸,同时做 YUV → RGBA 转换(如果源是 YUV)。
  3. 深度推理:把预处理后的纹理数据上传到 NCNN 推理框架(通常会走 NCNN 的 GPU 后端,需要做纹理到 Tensor 的转换),输出深度图。
  4. 深度后处理:把深度图上采样到渲染分辨率,必要时做一次高斯模糊去噪。
  5. DIBR 与 SBS 合成:用片元着色器分别渲染左眼和右眼视图,输出到屏幕或者录制缓冲。

线程模型上,推荐渲染线程 + 推理线程 + 取流线程三线程分离。取流线程只负责与相机/解码器交互;推理线程独立持有模型实例处理最新帧;渲染线程负责 GL 资源管理,在垂直同步信号到来时绘制最终画面。推理线程的输出跟渲染线程之间通过共享纹理或共享显存完成,尽量少用锁。

这里要说一个很实际的经验:整个链路最容易被忽略的是 GL 上下文和线程绑定问题。OpenGL ES 的上下文默认不能在多线程同时使用,如果推理线程要从 GPU 拿到纹理数据,需要用到eglMakeCurrent做双上下文共享纹理,这个配置不对,会让你在部分手机上崩溃,而且是偶发崩溃,非常难排查。

4. 帧率与温度的博弈:性能实测和调参心得

光说不练假把式。tachi 跑起来之后,我在几台不同定位的手机上做了实测,数据和结论比任何理论都更真实。

4.1 不同手机芯平台上的帧率、延迟、温度、功耗数据

测试条件统一为:1080p 视频源,半宽 SBS 输出,模型输入分辨率 256×192,FP16 量化,深度图输出分辨率 480×270。测试结果如下:

手机平台深度推理耗时整链路渲染耗时稳定帧率发热状态(30分钟后)
旗舰:骁龙 8 Gen2约 18ms约 45ms稳定 30fps,峰值能跑到 40fps温热,约 40℃
中端:骁龙 778G约 35ms约 70ms25-30fps 波动明显发热,约 44℃
中低端:天玑 900约 60ms约 90ms15-20fps烫手,开始降频
旧款:麒麟 990约 50ms约 80ms20-25fps发热明显,偶发掉帧

需要强调的是,这个成绩是关闭手机系统省电模式、调高电源策略后的结果。如果默认模式下运行,天玑 900 那档基本没法看,帧率会掉到 10fps 以下。

结论很直观:中端平台推荐把推理分辨率降到 192×144,输出深度图再上采样,这样可以换来大约 15-20% 的帧率提升。旗舰机也不要盲目开高,因为手机散热是持续输出的瓶颈,不是瞬时算力。

4.2 关键参数矩阵:怎么配置才能不卡

我在 tachi 里做了参数可配置化,这里给出一份我反复验证过的参数组合:

场景模型输入深度图分辨率推理间隔输出分辨率备注
看视频(旗舰)256×192480×270每帧推理1080p SBS流畅,立体感强
看视频(中端)192×144480×270每2帧推理1080p SBS保留深度连续性
相机实时(旗舰)256×192480×270每帧推理1080p SBS注意锁定曝光
相机实时(中端)192×144320×180每2帧推理720p SBS优先保帧率
裸眼3D手机256×192480×270每帧推理根据屏幕像素映射需要逐机型适配

注意"每 2 帧推理"这种策略:深度估计不必每帧都做,可以在两次深度推理之间,采用上一帧深度图 + 当前帧彩色图来渲染,画面运动不快时几乎看不出区别。对于动态场景,比如镜头快速晃动,深度图滞后会带来明显的"边缘错位",这时候就得强制回到每帧推理。

4.3 延迟优化的几个细碎技巧

延迟是实时处理的关键指标。从源帧到最终显示的延迟,我实测下来主要消耗在三个环节:

  • 深度推理本身是大头,所以选择的模型体积和输入分辨率影响最大。
  • 纹理上传是隐藏拖后腿选手。从 GL 纹理到 NCNN Tensor 的转换,如果不使用 GPU 后端(NCNN Vulkan 或 OpenCL),回读 CPU 会很耗时。tachi 默认走 GPU 后端,这个坑能避开。
  • 渲染线程与垂直同步的同步等待。如果一帧渲染刚好错过 vsync 窗口,需要多等一整个刷新周期(约 16ms)。解决办法是使用三重缓冲,或者干脆让渲染线程以固定 60fps 的节奏主动等待,而不要被动等 vsync。

还有一个很多人没想到的小技巧:延迟一帧渲染。意思是不要追求当前帧立即出结果,而是让渲染线程用上一帧的深度图渲染,等待推理线程输出当前帧深度图。这样虽然从绝对时间上延迟没有变化,但避免了推理线程和渲染线程互相等待产生的抖动,实际体感更顺滑。

4.4 发热降频策略:长看不烫手怎么做到

实时推理持续运转,发热是绕不开的。头显本就是贴脸设备,发烫体验会很难受。我折腾下来的做法分三层:

  • 限制帧率上限:默认锁 30fps,不要开放 60fps。30fps 配合 SBS 的立体效果,观感已经足够流畅。
  • 动态分辨率调整:在系统温度达到 41℃ 时,把模型输入从 256×192 降到 192×144;达到 43℃ 时再把深度图分辨率降一档,同时把推理间隔从每帧改成每 2 帧。这套策略在 tachi 里可以用温控模块配置。
  • 边缘算力调停:让深度推理的优先级低于渲染线程,渲染线程永不阻塞。如果推理赶不上,就用上一帧深度渲染,这样画面永远是流畅的,只是立体感会偶尔滞后一点。

5. 上了真机之后:3D 效果调优与踩坑实录

参数和链路都通了,设备连上头显或者让裸眼 3D 屏亮起来,真正的"玄学"阶段才开始。这章写的全是我自己踩过的坑,每一个都能单独写一篇长文了。

5.1 3D 效果不明显的罪魁祸首:视差强度和基线

我第一次跑通 tachi 时,戴上头显后的第一反应是:"这跟原视频有什么区别?"画面虽然确实是 SBS,左眼和右眼的内容也确实是不同视角,但立体感几乎为零。问题出在视差强度设置上。DIBR 渲染时的u_baseOffset(也就是基线距离的等效值)被我设得太小了,左右眼画面差异过弱,大脑自然感知不到深度。

把基础偏移调大之后,立体感立刻出来了,但紧接着又是一个新问题:画面中近处物体的边缘开始出现明显重影,看久了犯晕。这个平衡点需要反复试,我个人的经验是:先暂停视频,看静止画面的边缘重影情况。如果前景和背景边缘在左右眼视图里错位超过大约 1.5% 的屏幕宽度,观感就危险了。把基础偏移调整到"边缘看起来依然干净,但你明显能感觉到屏幕里前后有层次"的状态,就差不多了。

不同内容需要不同的强度。大场景航拍,强度可以适当调高;拍人像特写,强度要调低,否则人脸边缘会非常容易"裂开"。

5.2 画面闪烁、深度跳变:时域平滑与后处理

深度模型是逐帧独立推理的,没有时间记忆,所以同一个像素在当前帧被判为"近",下一帧可能因为光照变化、物体轻微移动就变成了"远"。这种深度图抖动在连续播放时,表现为物体边缘的"呼吸感"和"闪烁纹"。

我的解决办法是给深度图加一个时域平滑滤波:维护一个上一帧的深度纹理,当前帧深度纹理与上一帧深度纹理按比例混合(比如 7:3 或 8:2)。混合比例很关键:比例太大,运动物体会出现"拖影";太小,闪烁压制不住。实测下来,对于绝大多数视频内容,7:3 的混合比例比较稳妥。如果镜头运动剧烈,可以把比例动态调低,但不要低于 5:5。

空间维度上,深度图在输入 DIBR 之前,做一个高斯模糊也会有奇效。因为深度图不需要非常精细的边缘,模糊之后反而能减少边缘的锯齿感。

5.3 Camera2 取流的那些坑

相机实时 2D 转 3D,是 tachi 最让人兴奋也最折磨人的场景。我踩过的坑按严重程度排序如下:

  • 预览尺寸与显示尺寸不一致:很多手机的后置主摄默认输出 4:3 比例,但屏幕是 16:9 甚至 20:9,直接做 SBS 拉伸会把人脸拉变形。需要先通过 Crop 或 CenterInside 方式裁剪到目标比例,再送进处理管线。
  • 自动对焦与自动曝光对深度推理的干扰:相机聚焦位置变化、曝光幅度跳变,都会让深度图的绝对深度分布发生剧烈变化。后面做时域平滑时,这些变化会造成整个画面"忽远忽近"的呼吸现象。解决办法是尽量固定对焦和曝光参数,或者对深度图做逐帧归一化后再使用。
  • 横竖屏旋转:手机旋转时,相机传感器方向与纹理坐标系方向可能错位,导致深度图方向与画面方向不一致,出来的 SBS 左右眼变成"上下错位"。处理时一定要根据传感器旋转角度,在采样阶段对深度图做相应旋转。
5.4 MediaCodec 播放流程的时序坑

视频播放场景下,MediaCodec 的输出时机是帧率不稳定的根源。tachi 的管线设计里,取流线程与渲染线程是完全解耦的,这自然带来了更平滑的体验,但也引入了一个我一开始没预料到的坑:杀掉解码线程后,MediaCodec 会卡在 release 上

在切换视频源或者退出播放时,如果 MediaCodec 还处于CodecState.SUBMITTED状态,release 操作会导致主线程卡死好几秒。后来我总结出的正确流程是:先停止渲染线程、再通知解码线程退出、在解码线程内部调用stop()release()。顺序反了,基本都要踩一次 ANR(Application Not Responding)。

另一个坑是断了声画同步。当 SBS 处理管线让画面延迟了大约 100ms 之后,音频和视频会明显不同步,嘴型对不上。tachi 的做法是把音频也按固定延迟推后播放(通过AudioTracksetPlaybackPositionUpdateListener做缓冲控制),或者在播放器层面利用投递队列强制 audio sink 跟随 video 的节奏。但这里没有万能解,因为不同播放器的MediaCodecAudioTrack行为略有差异。

5.5 不同观看设备的适配:裸眼 3D 手机、VR 盒子、头显

SBS 虽然兼容性好,但不同设备对 SBS 画面的处理方式并不完全一样,必须对症下药。

  • VR 盒子(手机插进去的那种):最省心,只要把输出画面切成半宽 SBS,让屏幕横屏全屏播放,左右两半正好对应两只眼睛。这里唯一要注意的是手机屏的尺寸和视场角,不同尺寸的盒子对画面中心距的设定不太一样,但一般不需要调整。
  • VR 头显(一体机串联手机):主流头显支持 SBS 输入模式,但有的只支持"左右眼各一个 4K 视频流"的推流方式,而不是单画面 SBS。如果出现画面左右颠倒或者上下颠倒,先检查头显设置里"左右眼映射"的选项,不要在 tachi 里硬转方向。
  • 裸眼 3D 手机:这个是最麻烦的。裸眼 3D 屏幕带有柱状透镜,它的像素映射是固定的,而且不同型号的手机透镜间距不同。tachi 虽然能输出 SBS,但要让画面在裸眼 3D 屏上准确呈现出立体效果,需要在输出时按屏幕像素映射关系做二次采样——左右眼内容必须被对齐到透镜下的精确像素位置。否则要么立体感消失,要么出现奇怪的"摩尔纹"。

6. 后续想继续做的事

tachi 这个项目走到现在,2D 转 3D 实时 SBS 的核心链路已经跑通了。但离我理想中的状态还有一段距离,我给自己列了几个后续方向,也分享给有意折腾的朋友:

  • 用 Vulkan 替代 OpenGL ES:Vulkan 可以让渲染线程与推理线程更好地共享资源,利用 subpass 特性减少内存带宽消耗,延迟应该还能再压掉 10-20ms。
  • 给深度估计加入光流信息:目前深度图是逐帧独立的,时域平滑只能缓解抖动,如果有一份轻量级的光流信息去校正深度传播,运动物体的立体感会明显稳定很多。
  • 支持更多输出格式:除了 SBS,上下格式(Top-Bottom)和帧交替格式(Frame Sequential)在一些快门式 3D 眼镜和 3D 电视上有需求。做格式封装并不难,难的是不同输出设备下的同步信号适配。
  • 把处理能力抽成通用 SDK:tachi 的核心管线其实可以模块化成独立的"2D 转 3D 实时引擎",这样无论是第三方播放器还是相机 App 都能用复用,推广价值更大。

说到最后,我最想分享的经验是:2D 转 3D 这个项目,真正的难关不在模型或者算法本身,而在系统管线的整体优化。模型推理稍微慢一点可以有各种手段弥补,但管线设计不合理,比如取流卡顿、线程互相等待、纹理频繁拷贝,那才是直接把体验击穿的关键问题。这也是我折腾完 tachi 之后最大的感悟。

如果你也打算在自己手机上复现这条链路,建议先别急着追求画质,先用最低分辨率跑通全链路,确认每一环都无损流畅,再逐步提高参数。过程中如果遇到深度图边缘闪烁、画面发晕、帧率不稳的问题,多半能从这篇文章里找到对应的解决思路。祝你们玩得开心。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询