Spaces:
Configuration error
Configuration error
File size: 7,855 Bytes
5d3daa9 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 | """
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.")
|