Python实现立方体网格球体采样点高效空间检索问题
我现有一组共50万个球体的三维坐标及其对应非唯一半径数据,所有数据存储于pandas DataFrame对象中。
我希望构建一套高效搜索流程:对大量采样点坐标进行检测时,流程可返回每个点是否位于某一球体内,若存在包含关系则返回该点所属的球体。
据我了解,笛卡尔搜索网格是实现该需求的可行方案,因此我设定了固定单元格尺寸构建网格,为每个球体分配ID,同时存储了球心与其所在单元格中心的坐标差(供后续计算使用),相关实现代码如下:
import pandas as pd import numpy as np size_cell = 20 fake_coordinates = np.random.uniform(-200,200, (500000, 3)) # 注:原代码np.uniform为笔误,应为np.random.uniform;测试用例中球体会重叠,真实输入不会出现该情况,若点同时在两个球体内,返回第一个匹配的球体即可 data = pd.DataFrame(fake_coordinates, columns=['x','y','z']) data['r'] = np.random.uniform(1,3, 500000) x_vect = np.arange(data.x.min()-np.max(data.r), data.x.max()+np.max(data.r), size_cell) y_vect = np.arange(data.y.min()-np.max(data.r), data.y.max()+np.max(data.r), size_cell) z_vect = np.arange(data.z.min()-np.max(data.r), data.z.max()+np.max(data.r), size_cell) data['i_x'] = ((data.x-x_vect[0])//size_cell).astype(int) data['i_y'] = ((data.y-y_vect[0])//size_cell).astype(int) data['i_z'] = ((data.z-z_vect[0])//size_cell).astype(int) data['dx'] = data.x-(x_vect[data.i_x]+x_vect[data.i_x+1])/2 data['dy'] = data.y-(y_vect[data.i_y]+y_vect[data.i_y+1])/2 data['dz'] = data.z-(z_vect[data.i_z]+z_vect[data.i_z+1])/2
后续我注意到,球心位于相邻单元格的球体可能跨越自身所在单元格的边界,采样点有可能落入这类邻域球体中,因此我计算了每个球体的跨边界属性,相关代码如下:
data['crossing_x_left'] = data.dx < - (size_cell - data.r) data['crossing_x_right'] = data.dx > (size_cell - data.r) data['crossing_y_left'] = data.dy < - (size_cell - data.r) data['crossing_y_right'] = data.dy > (size_cell - data.r) data['crossing_z_down'] = data.dz < - (size_cell - data.r) data['crossing_z_top'] = data.dz > (size_cell - data.r)
完成上述预处理后,我需要生成N个采样点,采样代码如下:
sample = np.random.uniform(-200,200, (1000000, 3))
若仅针对单个采样点、且仅筛选球心与该点位于同一单元格的候选球体,可通过如下代码实现:
i_x = (sample[0,0]-x_vect[0])//size_cell i_y = (sample[0,1]-y_vect[0])//size_cell i_z = (sample[0,2]-z_vect[0])//size_cell ok_x = data['i_x']==i_x ok_y = data['i_y']==i_y ok_z = data['i_z']==i_z candidates = data.loc[ok_x & ok_y & ok_z]
目前我需要实现批量采样点的并行检测,同时将跨边界球体纳入候选范围,但在实现方法上遇到了问题:如何高效完成计算(即通过矩阵运算同时处理所有采样点与球体),判断采样点是否属于某一球体内,若属于则返回对应所属球体?球体跨单元格边界的候选筛选逻辑是我目前的主要难点。
不要尝试做全量采样点和球体的矩阵运算,50万球100万点的矩阵内存占用会直接超过TB级,根本无法运行。你选的单元格尺寸20远大于球体最大半径3,这个参数选择非常合理——任何球体最多只会覆盖33*3=27个单元格(自身+6个面相邻+12个棱相邻+8个角相邻),核心优化思路是提前把每个球体注册到所有它覆盖到的网格单元格中,查询时只需要拿采样点所在单元格的注册球体列表做距离校验即可。
第一步:网格预处理优化
不用单独存储crossing_*类标记,直接计算每个球体覆盖的所有单元格,将球体信息存入对应单元格的候选列表:
from collections import defaultdict # 给每个球体分配唯一ID data['sphere_id'] = np.arange(len(data)) # 构建网格字典:key为(i_x,i_y,i_z)单元格坐标,value为该单元格覆盖的球体数组 grid = defaultdict(list) for _, row in data[['i_x','i_y','i_z','x','y','z','r','dx','dy','dz','sphere_id']].iterrows(): ix, iy, iz = int(row.i_x), int(row.i_y), int(row.i_z) r, sx, sy, sz, sid = row.r, row.x, row.y, row.z, row.sphere_id dx, dy, dz = row.dx, row.dy, row.dz # 计算三个轴向上需要覆盖的邻域偏移 x_off = [0] if dx < -(size_cell - r): x_off.append(-1) if dx > (size_cell - r): x_off.append(1) y_off = [0] if dy < -(size_cell - r): y_off.append(-1) if dy > (size_cell - r): y_off.append(1) z_off = [0] if dz < -(size_cell - r): z_off.append(-1) if dz > (size_cell - r): z_off.append(1) # 将当前球体注册到所有覆盖的单元格 for ox in x_off: for oy in y_off: for oz in z_off: grid[(ix+ox, iy+oy, iz+oz)].append((sx, sy, sz, r, sid)) # 转numpy数组加速后续计算 for k in grid: grid[k] = np.array(grid[k])
该步骤总循环量最高为50万*27=1350万次,普通家用电脑数秒即可跑完。
第二步:批量采样点检测
按采样点所在单元格分组,同单元格的采样点用numpy广播机制批量做距离校验,全程无逐点Python级循环:
# 计算所有采样点所属单元格坐标 sample_ix = ((sample[:,0] - x_vect[0])//size_cell).astype(int) sample_iy = ((sample[:,1] - y_vect[0])//size_cell).astype(int) sample_iz = ((sample[:,2] - z_vect[0])//size_cell).astype(int) # 初始化结果:-1代表点不在任何球体内,其余值为匹配的球体ID res = np.full(len(sample), -1, dtype=int) # 按单元格分组采样点 cell_points = defaultdict(list) for idx in range(len(sample)): cell_points[(sample_ix[idx], sample_iy[idx], sample_iz[idx])].append(idx) # 逐单元格批量计算 for cell, p_ids in cell_points.items(): if cell not in grid: continue spheres = grid[cell] # 维度(n_sphere,5),列顺序:x,y,z,r,sid points = sample[p_ids] # 维度(n_point,3),列顺序:x,y,z # 广播计算点到球心的距离平方(跳过开根号运算提速2倍以上) dist_sq = np.sum( (points[:,None,:] - spheres[None,:,:3])**2, axis=2 ) r_sq = spheres[:,3]**2 in_sphere = dist_sq < r_sq[None,:] # 为每个点匹配第一个符合条件的球体 for i, pid in enumerate(p_ids): first_match = np.argmax(in_sphere[i]) if in_sphere[i, first_match]: res[pid] = int(spheres[first_match,4])
性能说明
- 按测试数据的密度,每个单元格平均仅注册3-5个候选球体,广播计算量极小,100万采样点的检测耗时在普通电脑上仅需1-2秒
- 若需要更高并行度,可将单元格分组拆分后用多进程处理,无共享锁开销,加速比接近CPU核心数
- 只要单元格尺寸≥2倍球体最大半径,333邻域的覆盖逻辑就始终成立,后续球体半径范围调整无需修改核心逻辑
内容的提问来源于stack exchange,提问作者yvrob

