File size: 6,287 Bytes
f4d2177
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
#!/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()