从一段普通手机视频生成左右格式立体画面,或输出Z-depth通道,过去通常依赖双摄像头标定或深度传感器。现在借助MiDaS这类单目深度估计模型,只需要一张张RGB帧就能得到稠密深度图。通过把深度值映射为视差,再对原始画面做像素偏移,可以快速得到左右眼画面;把深度图做伪彩色或数值导出,则能得到合成软件常用的Z-depth素材。整个流程不需要相机内参,也不要求视频来源是固定机位,适合快速验证和轻量后期制作。

MiDaS的输出并不是真实物理尺度下的深度,而是相对深度。也就是说,它能够准确判断画面中哪些区域更近、哪些更远,但不会直接给出厘米或米级别的距离值。这一点对视差生成和Z-depth预览影响不大,因为视差本身只需要相对关系,Z-depth在多数合成软件中也会重新映射到0到1范围。真正需要注意的是,如果视频场景中物体距离变化很大,或者镜头运动明显,相对深度在不同帧之间可能发生尺度漂移,后面必须做归一化和平滑处理,否则深度视频会出现明显闪烁。
MiDaS模型选型与推理前准备
MiDaS提供了多个模型版本,比较常用的是DPT_Hybrid、DPT_Large和MiDaS_small。DPT_Hybrid在精度和速度之间平衡较好,DPT_Large细节更丰富但推理更慢,MiDaS_small则适合CPU环境或长视频快速处理。对于视频逐帧处理来说,如果使用GPU,DPT_Hybrid通常可以在几十毫秒内完成一帧;如果只有CPU,MiDaS_small是更现实的选择。
模型加载可以通过PyTorch Hub直接完成,官方仓库会缓存权重文件。需要注意的是,不同模型对应的预处理transform不同,DPT系列使用dpt_transform,MiDaS_small使用small_transform。如果混用,输入张量的尺寸和归一化方式会不一致,推理结果会出现明显异常。
import cv2
import torch
import numpy as np
model_type = "DPT_Hybrid"
midas = torch.hub.load("intel-isl/MiDaS", model_type)
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
midas.to(device)
midas.eval()
midas_transforms = torch.hub.load("intel-isl/MiDaS", "transforms")
if model_type in ["DPT_Large", "DPT_Hybrid"]:
transform = midas_transforms.dpt_transform
else:
transform = midas_transforms.small_transform
print("模型已加载到", device)
transform会把输入图像转换成归一化后的张量,并自动调整到模型需要的输入尺寸。调用midas(input_batch)得到的是缩放到模型输出分辨率的深度图,因此还需要用双三次插值恢复到原始视频帧尺寸。推理时务必使用torch.no_grad()上下文,并调用midas.eval()关闭Dropout等训练层,这样可以降低显存占用并加速计算。
视频逐帧推理与深度抖动控制
视频深度估计最容易出现的问题不是单帧精度,而是连续播放时的深度闪烁。由于模型对每一帧独立推理,输出深度图的数值范围并不稳定。第一帧的近处像素可能被映射到0.9,第二帧同样位置的像素可能变成0.7,这会导致伪彩色画面不断跳动。解决思路是先统计一个稳定的数值范围,然后用统一的最小值和最大值做归一化,而不是每一帧单独使用自己的极值。
下面的代码使用第一帧的2%和98%分位数作为全局范围,后续所有帧都使用这个范围做归一化。这种方式适合场景深度变化不剧烈的视频。如果视频前半段是室内近景、后半段是户外远景,固定范围会失效,此时更适合使用滑动窗口动态更新范围,但更新速度要慢,避免重新引入闪烁。
cap = cv2.VideoCapture("input.mp4")
fps = cap.get(cv2.CAP_PROP_FPS)
w = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
h = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
out = cv2.VideoWriter(
"depth_color.mp4",
cv2.VideoWriter_fourcc(*"mp4v"),
fps,
(w, h),
)
global_min = None
global_max = None
while True:
ret, frame = cap.read()
if not ret:
break
rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
input_batch = transform(rgb).to(device)
with torch.no_grad():
prediction = midas(input_batch)
prediction = torch.nn.functional.interpolate(
prediction.unsqueeze(1),
size=(h, w),
mode="bicubic",
align_corners=False,
).squeeze()
depth = prediction.cpu().numpy().astype(np.float32)
if global_min is None:
global_min = float(np.percentile(depth, 2))
global_max = float(np.percentile(depth, 98))
depth_norm = (depth - global_min) / (global_max - global_min)
depth_norm = np.clip(depth_norm, 0.0, 1.0)
depth_u8 = (depth_norm * 255).astype(np.uint8)
depth_color = cv2.applyColorMap(depth_u8, cv2.COLORMAP_INFERNO)
out.write(depth_color)
cap.release()
out.release()
如果仍然存在轻微闪烁,可以对连续深度图做时间平滑。常见做法是维护上一帧深度,并用加权平均更新当前帧,例如smoothed = 0.7 * previous + 0.3 * current。但平滑必须在同一尺寸、同一场景结构下进行,如果镜头快速切换或物体大幅运动,简单混合会留下残影。对于固定机位视频,这种平滑非常有效;对于运动镜头,更稳妥的方案是降低伪彩色映射的敏感度,而不是直接修改深度数值。
性能方面,DPT_Hybrid在主流GPU上处理1080p视频通常每秒可以跑10到20帧,MiDaS_small在CPU上可能只有几帧每秒。实际项目中建议先把视频缩放到960×540或1280×720,生成深度后再放大回原始分辨率。这样既能减少推理时间,又不会明显损失深度边缘,因为深度信息本身是低频信号。
从深度图到视差图与左右眼画面
2D视频转3D视差效果的核心是把深度关系转换为像素平移。近处物体应该产生更大的左右眼位移,远处物体位移接近零。对于MiDaS输出的相对深度,可以直接使用反比关系近似视差:disparity = 1.0 / (depth + epsilon),然后归一化到0到1范围。最大平移像素通常设置在16到32之间,过大容易造成重影和视觉疲劳,过小则立体感不明显。
得到视差图后,可以用cv2.remap对原始帧做像素偏移。左眼视图需要把像素向右偏移,右眼视图需要把像素向左偏移,偏移量由视差图控制。下面代码使用网格坐标加减位移图,并利用边界复制减少边缘空洞。对于轻度视差效果,这个方法足够稳定;如果视差较大,物体边缘会出现明显拉伸,需要进一步做遮挡填充或深度图边缘平滑。
depth_norm = (depth - global_min) / (global_max - global_min)
depth_norm = np.clip(depth_norm, 0.0, 1.0)
disparity = 1.0 / (depth_norm + 1e-5)
disparity_min = disparity.min()
disparity_max = disparity.max()
if disparity_max > disparity_min:
disparity_norm = (disparity - disparity_min) / (disparity_max - disparity_min)
else:
disparity_norm = np.zeros_like(disparity)
max_shift_px = 24.0
shift_map = disparity_norm * max_shift_px
h, w = frame.shape[:2]
x, y = np.meshgrid(np.arange(w, dtype=np.float32), np.arange(h, dtype=np.float32))
map_x_left = (x - shift_map).astype(np.float32)
map_y_left = y.astype(np.float32)
map_x_right = (x + shift_map).astype(np.float32)
map_y_right = y.astype(np.float32)
left_view = cv2.remap(frame, map_x_left, map_y_left, cv2.INTER_LINEAR, borderMode=cv2.BORDER_REPLICATE)
right_view = cv2.remap(frame, map_x_right, map_y_right, cv2.INTER_LINEAR, borderMode=cv2.BORDER_REPLICATE)
side_by_side = np.hstack([left_view, right_view])
除了左右并排格式,也可以生成红蓝立体画面。红蓝模式把左眼视图的红色通道保留,右眼视图的绿色和蓝色通道保留,然后合成到一张图。这种格式在普通屏幕上配合红蓝眼镜就能看到立体效果,但色彩会有损失。左右并排格式更适合在VR设备、3D电视或后期软件中使用。合成时要注意两路视图的分辨率一致,否则直接拼接会导致画面错位。
最终立体视频写入时,编码器选择和输入视频保持一致即可。OpenCV的mp4v编码兼容性较好,但如果要保留更高画质,可以输出为图像序列再交给FFmpeg处理。无论哪种方式,都建议先输出一小段测试视频,确认视差强度和边缘质量后再跑完整视频。
Z-depth图生成与后期合成应用
Z-depth图本质上是把深度信息编码成灰度或伪彩色图像,用来表示画面元素的前后关系。在After Effects、Nuke、DaVinci Resolve等工具中,Z-depth常用于雾效、景深、遮挡关系以及粒子发射范围控制。MiDaS输出的浮点深度需要先归一化,再根据用途决定输出8位预览图还是16位、32位浮点文件。
如果只是用于预览或快速确认深度关系,8位伪彩色图足够直观。使用cv2.applyColorMap把0到255的灰度值映射到颜色带,近处物体通常用暖色、远处物体用冷色。但伪彩色图不适合作为精确的Z通道参与合成,因为颜色映射会丢失数值线性关系。更严谨的做法是保存16位PNG或32位浮点EXR,让后期软件读取原始深度值。
depth_u16 = (depth_norm * 65535).astype(np.uint16)
cv2.imwrite("depth_z.png", depth_u16)
# 保存浮点深度供合成软件使用
depth_float32 = depth.astype(np.float32)
cv2.imwrite("depth_z.exr", depth_float32)
16位PNG适合大部分常规合成需求,0表示最远、65535表示最近。EXR文件能够保留浮点数值范围,适合需要深度合成、三维重建或自定义着色器的场景。需要注意的是,不同软件对Z通道的朝向定义可能不同,有的以黑为近、白为远,有的相反,使用前要在具体软件中确认映射方向。
MiDaS生成Z-depth时,透明物体、反射表面和重复纹理区域容易产生错误估计。比如玻璃窗、水面、光滑地面可能被误判为不同的深度平面。对于这些区域,可以在后期使用模糊、手动遮罩或边缘保护滤波进行局部修正。虽然单目深度估计不能完全替代真实深度传感器,但作为视频后期的辅助通道,它已经能覆盖大量2D转3D、背景替换和视觉特效需求。