Spaces:
Configuration error
Configuration error
| """ | |
| 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.") | |