DAP-weights / erp2cubemap.py
Insta360-Research's picture
Upload 372 files
f4d2177 verified
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
ERP (equirectangular panorama, 2:1) -> Cube Map (6 faces)
依赖:
pip install opencv-python numpy
用法示例:
python erp2cubemap.py input.jpg --size 1024 --out out_dir
python erp2cubemap.py input.jpg --size 1024 --layout cross --out cube_cross.png
"""
import os
import math
import argparse
import numpy as np
import cv2
def build_face_map(face_size, face):
"""
为指定面构建从 cube-face 像素到 ERP 的映射。
返回: map_x, map_y (float32, 用于 cv2.remap)
约定的面及朝向(右手坐标, X 右, Y 上, Z 向前):
+X: right
-X: left
+Y: top
-Y: bottom
+Z: front
-Z: back
每个面 FOV = 90°,u,v ∈ [-1,1] 覆盖该面。
"""
# 像素网格中心采样
s = face_size
jj, ii = np.meshgrid(np.arange(s, dtype=np.float32),
np.arange(s, dtype=np.float32))
# 将像素坐标映射到 [-1,1](中心为0),保证像素中心采样
a = 2.0 * (jj + 0.5) / s - 1.0
b = 2.0 * (ii + 0.5) / s - 1.0
# 为不同面定义方向向量 (dx, dy, dz)
if face == 'right': # +X
dx, dy, dz = np.ones_like(a), -b, -a
elif face == 'left': # -X
dx, dy, dz = -np.ones_like(a), -b, a
elif face == 'top': # +Y
dx, dy, dz = a, np.ones_like(a), b
elif face == 'bottom': # -Y
dx, dy, dz = a, -np.ones_like(a), -b
elif face == 'front': # +Z
dx, dy, dz = a, -b, np.ones_like(a)
elif face == 'back': # -Z
dx, dy, dz = -a, -b, -np.ones_like(a)
else:
raise ValueError(f'Unknown face: {face}')
# 归一化方向
norm = np.sqrt(dx*dx + dy*dy + dz*dz)
dx /= norm; dy /= norm; dz /= norm
# ERP 映射(经纬 -> 像素)
# 经度 theta ∈ (-pi, pi],按 X->Z 的 atan2;纬度 phi ∈ [-pi/2, pi/2]
theta = np.arctan2(dz, dx) # 水平角:朝 +Z 为 0 -> 正确前向(front)
phi = np.arcsin(dy) # 垂直角:+Y 为 +pi/2 顶部
# 输出 map_x, map_y 是 ERP 图像坐标(列x/行y)
# 假设输入宽 W, 高 H:
# x = (theta + pi) / (2*pi) * W
# y = (pi/2 - phi) / pi * H (phi=+pi/2 -> y=0 顶部)
# W,H 先占位,后面 remap 时会依据实际尺寸使用比例缩放
# 为了支持任意输入尺寸,我们先输出归一化坐标 [0,1),再在 remap 前乘以 W/H
map_x_norm = (theta + math.pi) / (2.0 * math.pi)
map_y_norm = (math.pi/2 - phi) / math.pi
return map_x_norm.astype(np.float32), map_y_norm.astype(np.float32)
def remap_face(erp_img, map_x_norm, map_y_norm, interp, border):
H, W = erp_img.shape[:2]
map_x = map_x_norm * (W - 1)
map_y = map_y_norm * (H - 1)
return cv2.remap(erp_img, map_x, map_y, interpolation=interp, borderMode=border)
def save_six_faces(faces_dict, out_dir, base):
os.makedirs(out_dir, exist_ok=True)
for name, img in faces_dict.items():
cv2.imwrite(os.path.join(out_dir, f"{base}_{name}.png"), img)
def make_cross_layout(faces, face_size):
"""
生成常见 4x3 横向十字拼图:
[ ][top ][ ][ ]
[left][front][right][back]
[ ][bottom][ ][ ]
画布大小: (3H, 4W) = (3S, 4S),空白填充黑色。
"""
S = face_size
canvas = np.zeros((3*S, 4*S, 3), dtype=np.uint8)
# 放置
def put(name, row, col):
canvas[row*S:(row+1)*S, col*S:(col+1)*S] = faces[name]
put('top', 0, 1)
put('left', 1, 0)
put('front', 1, 1)
put('right', 1, 2)
put('back', 1, 3)
put('bottom', 2, 1)
return canvas
def parse_args():
ap = argparse.ArgumentParser(description='ERP -> CubeMap converter')
ap.add_argument('input', help='输入 ERP 图片路径(宽高比约 2:1)')
ap.add_argument('--size', type=int, default=1024, help='每个立方体面的尺寸(像素),默认 1024')
ap.add_argument('--out', default='out', help='输出目录(六图模式)或输出文件(十字模式)')
ap.add_argument('--layout', choices=['six', 'cross'], default='six',
help='输出布局: six=六张面, cross=十字拼图')
ap.add_argument('--interp', choices=['linear','nearest','cubic','lanczos'], default='lanczos',
help='重采样插值方式')
ap.add_argument('--border', choices=['wrap','reflect','constant'], default='wrap',
help='经度边界处理(wrap推荐),纬度超界会按选项处理')
return ap.parse_args()
def main():
args = parse_args()
interp_map = {
'nearest': cv2.INTER_NEAREST,
'linear' : cv2.INTER_LINEAR,
'cubic' : cv2.INTER_CUBIC,
'lanczos': cv2.INTER_LANCZOS4,
}
border_map = {
'wrap' : cv2.BORDER_WRAP,
'reflect' : cv2.BORDER_REFLECT_101,
'constant': cv2.BORDER_CONSTANT,
}
erp = cv2.imread(args.input, cv2.IMREAD_COLOR)
if erp is None:
raise SystemExit(f'读取失败: {args.input}')
H, W = erp.shape[:2]
if abs((W / max(1,H)) - 2.0) > 0.2:
print(f'警告: 输入宽高比看起来不是 2:1(实际 {W}:{H}),请确认这是 ERP 全景图。')
faces_order = ['right','left','top','bottom','front','back']
maps = {}
for name in faces_order:
maps[name] = build_face_map(args.size, name)
faces = {}
for name in faces_order:
map_x_norm, map_y_norm = maps[name]
face_img = remap_face(erp, map_x_norm, map_y_norm,
interp=interp_map[args.interp],
border=border_map[args.border])
faces[name] = face_img
if args.layout == 'six':
base = os.path.splitext(os.path.basename(args.input))[0]
save_six_faces(faces, args.out, base)
print(f'已输出六个面的图片到目录: {args.out}\n面名称: {faces_order}')
else:
cross = make_cross_layout(faces, args.size)
ok = cv2.imwrite(args.out, cross)
if not ok:
raise SystemExit(f'写入失败: {args.out}')
print(f'已输出十字拼图: {args.out}\n面名称(布局行列见代码注释): {faces_order}')
if __name__ == '__main__':
main()