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.")