RCLane_2D_Detection / encode.py
shin0412's picture
feat(RCLane): add PyTorch port of RCLane
5d3daa9
Raw
History Blame Contribute Delete
7.86 kB
"""
RCLane label encoder -- ported to PyTorch pipeline from src/lane_codec.py::encode
(MindSpore repo). Pure NumPy + shapely, framework-agnostic.
Converts lane annotations (a list of polylines) into the 5 dense ground-truth maps
that the network is trained to predict:
seg_map, up_arrow, down_arrow, up_bound, down_bound
This is a training-time preprocessing step (run per image, ideally cached), NOT part
of the network. It is the exact counterpart of the relay-chain decode.
Algorithm (per foreground pixel of a thick lane raster):
1. Draw a circle of radius `step_length` around the pixel.
2. Intersect the circle boundary with the nearest lane -> if it yields 2 points
(MultiPoint), the pixel is a "fine" foreground point.
3. The two intersection vectors become up_arrow / down_arrow (by smaller / larger y).
4. Vectors to the lane endpoints give the up/down directions; a circle spanning
pixel->endpoint intersected with the lane gives the arc length to the endpoint,
stored (scaled) as up_bound / down_bound.
Output layout (matches rclane.py / loss.py, NCHW-friendly):
seg_map: (H, W) float32 in {0, 1}
up/down_arrow/bound: (2, H, W) float32
"""
import numpy as np
import cv2
from shapely.geometry import Point, LineString, MultiPoint # noqa: F401
# Defaults from the original default_config.yaml
IMG_SIZE = (320, 800) # (H, W)
STEP_LENGTH = 10
LINE_WIDTH = 5
SEG_THRESHOLD = 0.5
BOUND_SCALE = 100.0 # bound value = arc_length / BOUND_SCALE + 1 (original uses 100)
BUFFER_QUAD_SEGS = 64 # circle smoothness (original used resolution=100)
def _to_linestrings(lanes_points):
"""lanes_points: list of lanes, each a list/array of (x, y). -> list of LineString."""
out = []
for pts in lanes_points:
pts = np.asarray(pts, dtype=float)
if pts.ndim == 2 and len(pts) >= 2:
out.append(LineString(pts))
return out
def _rasterize(linestrings, H, W, line_width):
"""Thick lane raster (coarse foreground mask), like cv2.polylines in the original."""
mask = np.zeros((H, W), np.uint8)
for ls in linestrings:
xs, ys = ls.xy
pts = np.stack([np.asarray(xs), np.asarray(ys)], axis=1).astype(np.int32)
cv2.polylines(mask, [pts], isClosed=False, color=255, thickness=line_width)
return (mask > 10).astype(np.float32)
def _points_of(geom):
"""Flatten a shapely geometry into a list of (x, y) points."""
if geom.is_empty:
return []
gt = geom.geom_type
if gt == "Point":
return [(geom.x, geom.y)]
if gt == "MultiPoint":
return [(p.x, p.y) for p in geom.geoms]
if gt == "GeometryCollection":
pts = []
for g in geom.geoms:
if g.geom_type == "Point":
pts.append((g.x, g.y))
return pts
return []
def _up_down(points, center):
"""Split vectors (point - center) into the one with smallest y (up) and largest y (down)."""
c = np.asarray(center, dtype=float)
vecs = [np.asarray(p, dtype=float) - c for p in points]
up = min(vecs, key=lambda v: v[1])
down = max(vecs, key=lambda v: v[1])
return up, down
def _arc_length(center_xy, delta, nearest):
"""
Length of the lane arc between the pixel and an endpoint, approximated by the
lane portion inside a circle whose diameter is that pixel->endpoint segment.
Falls back to the straight-line half distance on failure (as in the original).
"""
half = np.asarray(delta, dtype=float) / 2.0
r = float(np.hypot(half[0], half[1]))
if r <= 0:
return 0.0
ctr = np.asarray(center_xy, dtype=float) + half
try:
circle = Point(ctr[0], ctr[1]).buffer(r, quad_segs=BUFFER_QUAD_SEGS)
length = circle.intersection(nearest).length
if length == 0:
length = r
except Exception:
length = r
return length
def encode(lanes_points, img_size=IMG_SIZE, step_length=STEP_LENGTH,
line_width=LINE_WIDTH, seg_threshold=SEG_THRESHOLD):
"""
Args:
lanes_points: list of lanes, each a list/array of (x, y) in the target
image space (default 800 wide x 320 high).
Returns:
dict with 'seg_map' (H,W) and 'up_arrow'/'down_arrow'/'up_bound'/'down_bound'
each (2, H, W), all float32.
"""
H, W = img_size
seg = np.zeros((H, W), np.float32)
up_arrow = np.zeros((H, W, 2), np.float32)
down_arrow = np.zeros((H, W, 2), np.float32)
up_bound = np.zeros((H, W, 2), np.float32)
down_bound = np.zeros((H, W, 2), np.float32)
lanes = _to_linestrings(lanes_points)
if len(lanes) > 0:
coarse = _rasterize(lanes, H, W, line_width)
ys, xs = np.where(coarse > seg_threshold)
for y, x in zip(ys.tolist(), xs.tolist()):
cp = Point(float(x), float(y))
ring = cp.buffer(step_length, quad_segs=BUFFER_QUAD_SEGS).exterior
nearest = min(lanes, key=lambda l: l.distance(cp))
inter = ring.intersection(nearest)
if inter.geom_type != "MultiPoint":
continue # near an endpoint the circle hits the lane once -> skip
pts = _points_of(inter)
if len(pts) < 2:
continue
seg[y, x] = 1.0
u, d = _up_down(pts, (x, y))
up_arrow[y, x] = u # (dx, dy) toward the upper intersection
down_arrow[y, x] = d # (dx, dy) toward the lower intersection
end_pts = _points_of(nearest.boundary) # the 2 lane endpoints
if len(end_pts) >= 2:
u_end, d_end = _up_down(end_pts, (x, y))
else:
u_end, d_end = u, d
up_bound[y, x] = _arc_length((x, y), u_end, nearest) / BOUND_SCALE + 1
down_bound[y, x] = _arc_length((x, y), d_end, nearest) / BOUND_SCALE + 1
chw = lambda a: np.ascontiguousarray(a.transpose(2, 0, 1)) # (H,W,2) -> (2,H,W)
return {
"seg_map": seg,
"up_arrow": chw(up_arrow),
"down_arrow": chw(down_arrow),
"up_bound": chw(up_bound),
"down_bound": chw(down_bound),
}
# --------------------------------------------------------------------------- #
# smoke test
# --------------------------------------------------------------------------- #
if __name__ == "__main__":
H, W = IMG_SIZE
# a single curved lane crossing the image (points in 800x320 space)
ys = np.linspace(20, 300, 40)
xs = 400 + 180 * np.sin(ys / 300 * np.pi) # S-curve
lane = list(zip(xs.tolist(), ys.tolist()))
gt = encode([lane])
print("shapes:")
print(f" seg_map {gt['seg_map'].shape}")
for k in ("up_arrow", "down_arrow", "up_bound", "down_bound"):
print(f" {k:10s} {gt[k].shape}")
n_fg = int(gt["seg_map"].sum())
print(f"foreground pixels: {n_fg}")
assert n_fg > 0, "no foreground produced!"
ys_fg, xs_fg = np.where(gt["seg_map"] > 0.5)
up_dy = gt["up_arrow"][1][ys_fg, xs_fg] # y-component of up arrow
down_dy = gt["down_arrow"][1][ys_fg, xs_fg] # y-component of down arrow
# up arrow should point up (dy < 0), down arrow down (dy > 0), on average
print(f"mean up_arrow.dy = {up_dy.mean():+.2f} (expect < 0)")
print(f"mean down_arrow.dy = {down_dy.mean():+.2f} (expect > 0)")
assert up_dy.mean() < 0 < down_dy.mean(), "arrow directions look wrong!"
ub = gt["up_bound"][0][ys_fg, xs_fg]
print(f"up_bound range = [{ub.min():.2f}, {ub.max():.2f}] (>= 1)")
assert ub.min() >= 1.0, "bound values must be >= 1 (scaled + 1 offset)!"
# arrow step length should be ~ step_length
step = np.hypot(gt["up_arrow"][0][ys_fg, xs_fg], up_dy)
print(f"mean up_arrow length = {step.mean():.2f} (expect ~= {STEP_LENGTH})")
print("OK -- encode produces sane GT maps.")