swarm / companies/sparrow/seed/core.py

raw file 4160 bytes
#!/usr/bin/env python3
"""Sparrow core pipeline (seed v1 — WORKS, but crude: yours to improve).

The seed is a functioning end-to-end baseline: it detects corners, tracks
them frame to frame, and keeps a naive sparse map. It is deliberately
unimpressive so the company has real work to do (see backlog).

Conventions the whole repo follows:
  - extract(frame)      -> (points Nx2 float32, scores)
  - step(prev, frame, prev_pts) -> (tracked Nx2, lost_fraction, pair idx)
  - study of the fallback modules: features.py / tracker.py / map.py
    may be created by the team and are imported if present (duck-typed
    to these same APIs — the rest of the repo won't know or care).
"""
import cv2
import numpy as np

try:
    from features import extract as feature_extract
except Exception:
    feature_extract = None

try:
    from tracker import step as tracker_step
except Exception:
    tracker_step = None

try:
    from map import SparseMap
except Exception:
    SparseMap = None

TIME_STEP = 1.0 / 30.0


def extract_builtin(frame):
    import cv2
    gray = frame if frame.ndim == 2 else cvt_gray(frame)
    pts = cv2.goodFeaturesToTrack(gray, 200, qualityLevel=0.01, minDistance=8)
    return (pts.reshape(-1, 2), np.ones(len(pts))) if pts is not None else (np.zeros((0, 2)), np.zeros(0))


def flow_builtin(gray_prev, gray_cur, pts):
    p1, st, err = cv2.calcOpticalFlowPyrLK(gray_prev, gray_cur, pts, None, winSize=(21, 21))
    st = st.ravel().astype(bool)
    missing = int((~st).sum())
    return p1[st].reshape(-1, 2), pts[st].reshape(-1, 2), missing / max(1, len(st))


def cvt_gray(f):
    import cv2
    return cv2.cvtColor(f, cv2.COLOR_BGR2GRAY)


def extract(frame):
    """Extract features (seed = plain goodFeaturesToTrack)."""
    return feature_extract(frame) if feature_extract else extract_builtin(frame)


def step(prev_gray, gray, prev_pts):
    """Track points into the frame; returns (cur_pts, prev_pts_stage, lost_frac)."""
    if tracker_step:
        return tracker_step(prev_gray, gray, prev_pts)
    return flow_builtin(prev_gray, gray, prev_pts)


class WeakMap:
    """Seed map: just the current points + snapshot(). Replace via map.py."""

    def __init__(self):
        self.points = {}          # idx -> [x, z, age]
        self.nid = 0

    def update(self, pts, prev_pts):
        n = len(pts)
        for i in range(n):
            self.nid += 1
            self.points[self.nid] = [float(pts[i][0]), float(prev_pts[i][1]), 0.0]

    def snapshot(self):
        return {
            "image_pts": [(v[0], v[1], v[2]) for v in list(self.points.values())],
            "count": len(self.points),
        }


if SparseMap:
    WeakMap = SparseMap


class Pipeline:
    def __init__(self):
        self.prev_gray = None
        self.prev_pts = None
        self.map = WeakMap()

    def on_frame(self, frame):
        import cv2
        gray = frame if frame.ndim == 2 else cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
        if self.prev_gray is None or self.prev_pts is None or len(self.prev_pts) < 4:
            pts, scores = extract(frame)
            self.prev_gray, self.prev_pts = gray, pts.astype(np.float32)
            return {"features": len(pts), "lost": 0.0, "new": len(pts)}
        cur, prev_matched, lost = step(self.prev_gray, gray, self.prev_pts)
        if len(cur) < 4:
            pts, _ = extract(frame)
            self.prev_gray, self.prev_pts = gray, pts.astype(np.float32)
            return {"features": len(pts), "lost": 1.0, "new": len(pts)}
        self.map.update(cur, prev_matched)
        # reseed occasionally so the stream never starves
        if len(cur) < 60:
            pts, _ = extract(frame)
            self.prev_pts = np.vstack([cur, pts.astype(np.float32)])
        else:
            self.prev_pts = cur
        self.prev_gray = gray
        return {"features": len(cur), "lost": float(lost), "new": len(cur) - len(prev_matched)}


if __name__ == "__main__":
    import cv2
    import scene
    p = Pipeline()
    t = 0.0
    for _ in range(10):
        f = scene.render(t)
        r = p.on_frame(f)
        print({k: round(v, 2) for k, v in r.items()})
        t += TIME_STEP