MiDaS数据格式详解:PFM与PNG文件读写源码解析
引言:深度估计中的数据格式挑战
在单目深度估计(Monocular Depth Estimation)领域,精确的深度图存储与交换是算法开发与应用部署的关键环节。MiDaS(Monocular Depth Estimation)作为当前最先进的开源深度估计算法之一,采用PFM(Portable Float Map)格式存储高精度浮点深度数据,同时支持PNG(Portable Network Graphics)格式的可视化输出。本文将系统解析这两种格式的技术细节,结合MiDaS源码实现,提供从文件结构到工程实践的完整指南。
PFM格式技术规范
1. 文件结构解析
PFM格式采用二进制存储,由三部分组成:
[文件头][图像尺寸][缩放因子][像素数据]
文件头标识:
PF\n:彩色图像(3通道浮点数据)Pf\n:灰度图像(单通道浮点数据)
关键技术特性:
- 支持任意尺寸的浮点图像存储
- 原生支持大/小端字节序(Endian)标识
- 像素数据按行存储,从上到下(与PNG相反)
2. MiDaS中的PFM读写实现
2.1 读取实现(read_pfm函数)
def read_pfm(path):
with open(path, "rb") as file:
# 解析文件头
header = file.readline().rstrip()
if header.decode("ascii") == "PF":
color = True # 彩色图像标识
elif header.decode("ascii") == "Pf":
color = False # 深度图为灰度图像
else:
raise Exception("Not a PFM file: " + path)
# 解析图像尺寸
dim_match = re.match(r"^(\d+)\s(\d+)\s$", file.readline().decode("ascii"))
if dim_match:
width, height = map(int, dim_match.groups())
else:
raise Exception("Malformed PFM header.")
# 解析缩放因子与字节序
scale = float(file.readline().decode("ascii").rstrip())
if scale < 0:
endian = "<" # 小端字节序
scale = -scale
else:
endian = ">" # 大端字节序
# 读取像素数据
data = np.fromfile(file, endian + "f") # 按指定字节序读取float32
shape = (height, width, 3) if color else (height, width)
data = np.reshape(data, shape)
data = np.flipud(data) # 垂直翻转校正存储顺序
return data, scale
2.2 写入实现(write_pfm函数)
def write_pfm(path, image, scale=1):
with open(path, "wb") as file:
# 验证数据类型
if image.dtype.name != "float32":
raise Exception("Image dtype must be float32.")
# 垂直翻转以符合PFM存储顺序
image = np.flipud(image)
# 确定图像类型
if len(image.shape) == 3 and image.shape[2] == 3:
color = True # 彩色图像
elif (len(image.shape) == 2 or
len(image.shape) == 3 and image.shape[2] == 1):
color = False # 深度图为单通道
else:
raise Exception("Image must have H x W x 3, H x W x 1 or H x W dimensions.")
# 写入文件头
file.write("PF\n".encode() if color else "Pf\n".encode())
file.write(f"{image.shape[1]} {image.shape[0]}\n".encode())
# 处理字节序
endian = image.dtype.byteorder
if endian == "<" or (endian == "=" and sys.byteorder == "little"):
scale = -scale # 负缩放因子表示小端字节序
file.write(f"{scale}\n".encode())
image.tofile(file) # 写入二进制浮点数据
3. 错误处理机制
MiDaS实现了多层次的异常检测:
- 文件类型验证:通过文件头严格检查PFM标识
- 维度解析:使用正则表达式确保尺寸格式正确
- 数据类型强制:要求输入图像必须为float32类型
- 形状验证:确保深度图为H×W单通道结构
PNG格式深度图处理
1. 格式转换原理
PNG格式存储深度数据面临两大挑战:
- 浮点数据到整数的映射
- 可视化与精度的平衡
MiDaS采用线性缩放映射策略:
depth_normalized = (depth - depth_min) / (depth_max - depth_min)
uint_data = depth_normalized * max_val # max_val=255(8位)或65535(16位)
2. 写入实现(write_depth函数)
def write_depth(path, depth, grayscale, bits=1):
# 处理非有限值
if not np.isfinite(depth).all():
depth = np.nan_to_num(depth, nan=0.0, posinf=0.0, neginf=0.0)
print("WARNING: Non-finite depth values present")
# 线性归一化到目标位深范围
depth_min = depth.min()
depth_max = depth.max()
max_val = (2**(8*bits)) - 1 # 8位:255, 16位:65535
if depth_max - depth_min > np.finfo("float").eps:
out = max_val * (depth - depth_min) / (depth_max - depth_min)
else:
out = np.zeros(depth.shape, dtype=depth.dtype)
# 色彩映射处理
if not grayscale:
out = cv2.applyColorMap(np.uint8(out), cv2.COLORMAP_INFERNO)
# 按位深写入PNG
if bits == 1:
cv2.imwrite(path + ".png", out.astype("uint8")) # 8位PNG
elif bits == 2:
cv2.imwrite(path + ".png", out.astype("uint16")) # 16位PNG
3. 关键参数对比
| 参数 | 8位PNG | 16位PNG | PFM |
|---|---|---|---|
| 数据类型 | uint8 | uint16 | float32 |
| 动态范围 | 0-255 | 0-65535 | ±3.4×10³⁸ |
| 文件大小 | 最小 | 中等 | 最大 |
| 用途 | 快速可视化 | 精确存档 | 算法输入/输出 |
| 无损性 | 可视化有损 | 精度有限 | 完全无损 |
工程实践指南
1. 格式选择决策树
2. 性能优化建议
- 内存管理:PFM加载时使用
np.fromfile直接映射,避免中间拷贝 - 错误处理:实现
try-except捕获PFM解析异常,尤其针对:try: depth_data, scale = read_pfm("depth.pfm") except Exception as e: print(f"PFM解析失败: {str(e)}") # 实现降级方案,如加载备用PNG - 批处理优化:处理序列帧时复用文件句柄,避免频繁IO操作
3. 跨平台兼容性
- 字节序处理:Windows默认小端,Linux/macOS默认大端,需依赖PFM的缩放因子符号自动适配
- 路径处理:使用
os.path模块确保跨平台路径兼容性:import os pfm_path = os.path.join(output_dir, f"frame_{frame_idx:06d}.pfm")
常见问题解决方案
Q1: PFM文件体积过大如何处理?
A1: 可采用zstd压缩,MiDaS推荐命令:
# 压缩
zstd -19 depth.pfm -o depth.pfm.zst
# 解压(保持原格式兼容)
zstdcat depth.pfm.zst > depth.pfm
Q2: 如何验证PNG深度图精度?
A2: 实现校验函数:
def verify_depth_accuracy(original_pfm, png_path, bits=2):
# 读取原始PFM与PNG重建深度
depth_gt, _ = read_pfm(original_pfm)
depth_png = cv2.imread(png_path, cv2.IMREAD_ANYDEPTH)
# 计算均方误差
mse = np.mean((depth_gt - depth_png) ** 2)
return mse < 1e-3 # 16位PNG典型误差阈值
Q3: 浏览器中如何预览PFM文件?
A3: MiDaS提供转换工具链:
# PFM转彩色PNG(带深度色标)
depth_data, _ = read_pfm("input.pfm")
write_depth("preview", depth_data, grayscale=False, bits=2)
总结与扩展
PFM格式作为MiDaS的核心数据交换格式,提供了科学计算所需的浮点精度;而PNG格式则满足了可视化与轻量化需求。通过本文解析的read_pfm、write_pfm和write_depth三个核心函数,开发者可实现深度数据的完整生命周期管理。
扩展应用方向:
- 集成到ROS2消息机制(参考
ros/midas_cpp包实现) - 移动端优化(
mobile/android目录提供PNG高效解码方案) - 与ONNX Runtime结合实现跨平台部署(
tf/run_onnx.py)
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



