Bootstrap

【Python】将不规则凸多边形映射到单位正方形

写在前面

在机器学习领域常需要将数据归一化后才能进行训练等操作,一维数据很容易处理,但对于二维的不规则数据,则需要一些手段,本文就是用来解决这个问题
此外,有时候希望可以用循环遍历一个不规则的二维平面,显然难以直接实现,此时将该平面映射到一个规则的矩形范围内,就能轻松实现这个目标

整体思路

  1. 利用同心映射将 (u,v) 映射到单位圆内的点 (x_disk,y_disk);
  2. 计算该点的极坐标 (r, θ);
  3. 计算从多边形质心沿 θ 方向与多边形边界的交点距离 R;
  4. 最终映射点 = 质心 + (r * R) * (cosθ, sinθ)

具体实现

import numpy as np
import math
import matplotlib.pyplot as plt

def compute_centroid(polygon):
    """计算多边形顶点的重心"""
    polygon = np.array(polygon, dtype=float)
    return np.mean(polygon, axis=0)

def cross2d(a, b):
    """二维向量的叉积 a x b"""
    return a[0]*b[1] - a[1]*b[0]

def compute_ray_polygon_intersection(C, d, polygon):
    """
    给定射线:点 C 和方向 d(单位向量),以及凸多边形(顶点按顺时针或逆时针排列),
    计算射线与多边形各边的交点距离 t(满足 C + t*d 在边上,且 t>=0)。
    返回所有满足条件的 t 值中的最小值,即 R(θ)。
    """
    t_min = None
    n = len(polygon)
    for i in range(n):
        v1 = np.array(polygon[i], dtype=float)
        v2 = np.array(polygon[(i+1) % n], dtype=float)
        e = v2 - v1  # 边向量
        denom = cross2d(d, e)
        if abs(denom) < 1e-8:
            continue  # 平行或共线,无交点
        # 根据公式 t = ((v1 - C) x e) / (d x e)
        t = cross2d(v1 - C, e) / denom
        s = cross2d(v1 - C, d) / denom
        # 判断交点是否在线段上(s in [0,1])和 t >= 0
        if t >= 0 and (s >= 0 and s <= 1):
            if t_min is None or t < t_min:
                t_min = t
    if t_min is None:
        raise ValueError("射线没有与多边形相交")
    return t_min

# Peter Shirley 的同心映射法
def square_to_disk(u, v):
    """
    将 (u,v) ∈ [0,1]^2 映射到 (x,y) ∈ disk,
    输出的 (x,y) ∈ [-1,1]²,且满足 x²+y² <= 1。
    """
    # 先将 [0,1] 映射到 [-1,1]
    a = 2*u - 1
    b = 2*v - 1
    if a == 0 and b == 0:
        return 0.0, 0.0
    if abs(a) > abs(b):
        r = a
        theta = (math.pi/4) * (b/a)
    else:
        r = b
        theta = (math.pi/2) - (math.pi/4) * (a/b)
    return r * math.cos(theta), r * math.sin(theta)

# 将单位正方形内的点 (u,v) ∈ [0,1]^2 映射到凸多边形内的点。
def map_from_unit_square(u, v, polygon):
    # 多边形质心
    C = compute_centroid(polygon)
    # 同心映射到单位圆盘
    x_disk, y_disk = square_to_disk(u, v)
    r = math.sqrt(x_disk**2 + y_disk**2)
    theta = math.atan2(y_disk, x_disk)
    # 计算从 C 沿方向 theta 到多边形边界的距离 R
    d = np.array([math.cos(theta), math.sin(theta)])
    R = compute_ray_polygon_intersection(C, d, polygon)
    # 映射点
    mapped_point = C + (r * R) * d
    return mapped_point

polygon = [(0, 1), (6, 2), (10, 5), (9, 7), (7, 8), (3, 5)]
C = compute_centroid(polygon)
print("多边形质心:", C)

for x in np.linspace(0, 1, 50):
    p_mapped = map_from_unit_square(x, 0, polygon)
    plt.plot(p_mapped[0], p_mapped[1], 'bo')
for y in np.linspace(0, 1, 50):
    p_mapped = map_from_unit_square(1, y, polygon)
    plt.plot(p_mapped[0], p_mapped[1], 'bo')
for x in np.linspace(1, 0, 50):
    p_mapped = map_from_unit_square(x, 1, polygon)
    plt.plot(p_mapped[0], p_mapped[1], 'bo')
for y in np.linspace(1, 0, 50):
    p_mapped = map_from_unit_square(0, y, polygon)
    plt.plot(p_mapped[0], p_mapped[1], 'bo')

for x in np.linspace(0.2, 0.8, 30):
    p_mapped = map_from_unit_square(x, 0.2, polygon)
    plt.plot(p_mapped[0], p_mapped[1], 'bo')
for y in np.linspace(0.2, 0.8, 30):
    p_mapped = map_from_unit_square(0.8, y, polygon)
    plt.plot(p_mapped[0], p_mapped[1], 'bo')
for x in np.linspace(0.8, 0.2, 30):
    p_mapped = map_from_unit_square(x, 0.8, polygon)
    plt.plot(p_mapped[0], p_mapped[1], 'bo')
for y in np.linspace(0.8, 0.2, 30):
    p_mapped = map_from_unit_square(0.2, y, polygon)
    plt.plot(p_mapped[0], p_mapped[1], 'bo')
plt.show()

注意

polygon中的点一定是相邻连续的

;