initial commit
This commit is contained in:
@@ -0,0 +1,234 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
target_math.py — 纯 numpy 的"像素 -> 三维目标"定位核心(不依赖 ROS,方便离线测试)。
|
||||
|
||||
输入:一帧点云(雷达系)+ 查询像素
|
||||
输出:目标在相机系/雷达系下的坐标、距离、置信度、是否有多个深度层等
|
||||
"""
|
||||
import numpy as np
|
||||
|
||||
import calib
|
||||
|
||||
|
||||
def _fit_plane_ray(P, ray, max_residual=0.05):
|
||||
"""PCA 拟合平面,再让射线与平面求交。返回 (点, 残差)。"""
|
||||
if P.shape[0] < 6:
|
||||
return None, None
|
||||
c = P.mean(axis=0)
|
||||
_, _, vt = np.linalg.svd(P - c)
|
||||
n = vt[2]
|
||||
denom = float(n @ ray)
|
||||
if abs(denom) < 1e-6:
|
||||
return None, None
|
||||
lam = float(n @ c) / denom
|
||||
if lam <= 0:
|
||||
return None, None
|
||||
p = lam * ray
|
||||
residual = float(np.sqrt(np.mean(((P - c) @ n) ** 2)))
|
||||
if residual > max_residual:
|
||||
return None, residual
|
||||
return p, residual
|
||||
|
||||
|
||||
def locate(xyz, query_u, query_v, conf=None, radius=10.0, depth_band=0.6,
|
||||
fit_plane=True, max_residual=0.05, min_conf=None, min_z=0.05):
|
||||
"""在点云里找查询像素方向上最近的可见点。
|
||||
|
||||
参数
|
||||
xyz : (N,3) 雷达系点云
|
||||
query_u/v : 查询像素
|
||||
conf : (N,) 置信度,可选
|
||||
radius : 候选像素半径
|
||||
返回 dict(未找到时 found=False)
|
||||
"""
|
||||
xyz = np.asarray(xyz, dtype=np.float64)
|
||||
good = np.isfinite(xyz).all(axis=1) & (np.abs(xyz).sum(axis=1) > 1e-6)
|
||||
if conf is not None and min_conf is not None:
|
||||
good &= np.asarray(conf) >= min_conf
|
||||
idx = np.where(good)[0]
|
||||
pts = xyz[good]
|
||||
|
||||
p_cam = calib.lidar_to_cam(pts)
|
||||
u, v, ok = calib.project_cam_to_pixel(p_cam)
|
||||
|
||||
dpx = np.hypot(u - query_u, v - query_v)
|
||||
cand = ok & (dpx <= radius) & (p_cam[:, 2] > min_z)
|
||||
|
||||
rng = np.linalg.norm(p_cam, axis=1)
|
||||
res = {
|
||||
'found': False, 'n_valid': int(pts.shape[0]), 'n_cand': int(cand.sum()),
|
||||
'radius': float(radius), 'query_u': float(query_u), 'query_v': float(query_v),
|
||||
}
|
||||
if not cand.any():
|
||||
return res
|
||||
|
||||
cand_idx = np.where(cand)[0]
|
||||
order = cand_idx[np.argsort(rng[cand_idx])]
|
||||
sel = int(order[0])
|
||||
|
||||
ray = calib.pixel_to_ray(query_u, query_v)
|
||||
p_near = p_cam[sel]
|
||||
|
||||
# 深度分层:候选距离跨度大 => 可能擦过边缘
|
||||
r_sorted = np.sort(rng[cand_idx])
|
||||
multi = bool(len(r_sorted) > 3 and (r_sorted[-1] - r_sorted[0]) > 2.0)
|
||||
|
||||
p_final = p_near
|
||||
plane_used = False
|
||||
residual = None
|
||||
if fit_plane:
|
||||
band = p_cam[cand_idx][rng[cand_idx] <= rng[sel] + depth_band]
|
||||
p_fit, residual = _fit_plane_ray(band, ray, max_residual)
|
||||
if p_fit is not None:
|
||||
p_final = p_fit
|
||||
plane_used = True
|
||||
|
||||
res.update({
|
||||
'found': True,
|
||||
'p_cam': p_final,
|
||||
'range': float(np.linalg.norm(p_final)),
|
||||
'z': float(p_final[2]),
|
||||
'p_lidar': calib.cam_to_lidar(p_final)[0],
|
||||
'hit_u': float(u[sel]),
|
||||
'hit_v': float(v[sel]),
|
||||
'px_err': float(dpx[sel]),
|
||||
'lidar_index': int(idx[sel]),
|
||||
'plane_used': plane_used,
|
||||
'plane_residual': residual,
|
||||
'multi_layer': multi,
|
||||
'cand_ranges': rng[cand_idx],
|
||||
'confidence': float(conf[idx[sel]]) if conf is not None else None,
|
||||
})
|
||||
return res
|
||||
|
||||
|
||||
class TargetIndex:
|
||||
"""一帧点云的「像素格子 -> 点索引」倒排表。
|
||||
|
||||
为什么需要:
|
||||
locate() 每次点击都会把 N 个点全量投影一遍。N≈5 万时一次几毫秒到
|
||||
几十毫秒,点一次无所谓;但同一帧反复点、或在板子上跑时,这部分是
|
||||
重复劳动。这里在【每帧】只做一次全量投影 + 分桶,之后每次点击只
|
||||
检查查询像素周围几个格子里的点,平面拟合也只在局部候选里做。
|
||||
|
||||
用法:
|
||||
idx = TargetIndex(xyz, conf=conf) # 每帧建一次
|
||||
res = idx.locate(u, v, radius=10) # 每次点击,返回结构和 locate 一样
|
||||
|
||||
注意:
|
||||
- 桶按点的像素坐标分,格子越大,查询时扫到的点越多;默认 32 px。
|
||||
- 只索引落在图像内的点;图像外的点本来也不会被点到。
|
||||
"""
|
||||
|
||||
def __init__(self, xyz, conf=None, cell=32, min_z=0.05):
|
||||
xyz = np.asarray(xyz, dtype=np.float64)
|
||||
good = np.isfinite(xyz).all(axis=1) & (np.abs(xyz).sum(axis=1) > 1e-6)
|
||||
self.idx = np.where(good)[0]
|
||||
self.xyz = xyz[good]
|
||||
self.conf = None if conf is None else np.asarray(conf)[good]
|
||||
|
||||
self.p_cam = calib.lidar_to_cam(self.xyz)
|
||||
self.u, self.v, ok = calib.project_cam_to_pixel(self.p_cam)
|
||||
self.rng = np.linalg.norm(self.p_cam, axis=1)
|
||||
self.ok = ok & (self.p_cam[:, 2] > float(min_z))
|
||||
|
||||
self.cell = max(4, int(cell))
|
||||
self.nx = int(np.ceil(calib.IMAGE_WIDTH / float(self.cell)))
|
||||
self.ny = int(np.ceil(calib.IMAGE_HEIGHT / float(self.cell)))
|
||||
|
||||
sel = np.where(self.ok)[0]
|
||||
if sel.size:
|
||||
cx = np.clip((self.u[sel] // self.cell).astype(np.int64),
|
||||
0, self.nx - 1)
|
||||
cy = np.clip((self.v[sel] // self.cell).astype(np.int64),
|
||||
0, self.ny - 1)
|
||||
keys = cy * self.nx + cx
|
||||
order = np.argsort(keys, kind='stable')
|
||||
keys = keys[order]
|
||||
sel = sel[order]
|
||||
uniq, starts = np.unique(keys, return_index=True)
|
||||
ends = np.append(starts[1:], keys.size)
|
||||
self.buckets = {int(k): sel[s:e]
|
||||
for k, s, e in zip(uniq, starts, ends)}
|
||||
else:
|
||||
self.buckets = {}
|
||||
|
||||
def _gather(self, query_u, query_v, radius):
|
||||
c = self.cell
|
||||
x0 = max(0, int(np.floor((query_u - radius) / c)))
|
||||
x1 = min(self.nx - 1, int(np.floor((query_u + radius) / c)))
|
||||
y0 = max(0, int(np.floor((query_v - radius) / c)))
|
||||
y1 = min(self.ny - 1, int(np.floor((query_v + radius) / c)))
|
||||
|
||||
parts = []
|
||||
for cy in range(y0, y1 + 1):
|
||||
base = cy * self.nx
|
||||
for cx in range(x0, x1 + 1):
|
||||
arr = self.buckets.get(base + cx)
|
||||
if arr is not None and arr.size:
|
||||
parts.append(arr)
|
||||
if not parts:
|
||||
return np.empty(0, dtype=np.int64)
|
||||
return np.concatenate(parts)
|
||||
|
||||
def locate(self, query_u, query_v, radius=10.0, depth_band=0.6,
|
||||
fit_plane=True, max_residual=0.05, min_conf=None):
|
||||
"""局部查找,返回与 locate() 相同的字段。"""
|
||||
query_u = float(query_u)
|
||||
query_v = float(query_v)
|
||||
|
||||
res = {
|
||||
'found': False, 'n_valid': int(self.idx.size),
|
||||
'n_cand': 0, 'radius': float(radius),
|
||||
'query_u': query_u, 'query_v': query_v,
|
||||
'indexed': True,
|
||||
}
|
||||
|
||||
cand = self._gather(query_u, query_v, radius)
|
||||
if cand.size == 0:
|
||||
return res
|
||||
|
||||
dpx = np.hypot(self.u[cand] - query_u, self.v[cand] - query_v)
|
||||
keep = dpx <= radius
|
||||
if min_conf is not None and self.conf is not None:
|
||||
keep &= self.conf[cand] >= min_conf
|
||||
cand = cand[keep]
|
||||
dpx = dpx[keep]
|
||||
res['n_cand'] = int(cand.size)
|
||||
if cand.size == 0:
|
||||
return res
|
||||
|
||||
# 距离最近的点
|
||||
order = np.argsort(self.rng[cand])
|
||||
cand = cand[order]
|
||||
dpx = dpx[order]
|
||||
sel = int(cand[0])
|
||||
|
||||
ray = calib.pixel_to_ray(query_u, query_v)
|
||||
r_sorted = np.sort(self.rng[cand])
|
||||
multi = bool(r_sorted.size > 3 and (r_sorted[-1] - r_sorted[0]) > 2.0)
|
||||
|
||||
p_final = self.p_cam[sel]
|
||||
plane_used = False
|
||||
residual = None
|
||||
if fit_plane:
|
||||
band = self.p_cam[cand[self.rng[cand] <= self.rng[sel] + depth_band]]
|
||||
p_fit, residual = _fit_plane_ray(band, ray, max_residual)
|
||||
if p_fit is not None:
|
||||
p_final = p_fit
|
||||
plane_used = True
|
||||
|
||||
res.update({
|
||||
'found': True,
|
||||
'p_cam': p_final,
|
||||
'p_lidar': calib.cam_to_lidar(p_final)[0],
|
||||
'hit_u': float(self.u[sel]),
|
||||
'hit_v': float(self.v[sel]),
|
||||
'px_err': float(dpx[0]),
|
||||
'lidar_index': int(self.idx[sel]),
|
||||
'plane_used': plane_used,
|
||||
'plane_residual': residual,
|
||||
'multi_layer': multi,
|
||||
'confidence': None if self.conf is None else float(self.conf[sel]),
|
||||
})
|
||||
return res
|
||||
Reference in New Issue
Block a user