Feature Matching Fundamentals for Robotics

A comprehensive guide to Keypoints, Descriptors, and Nearest Neighbor Matching with practical SLAM applications

Computer Vision Feature Detection Robotics SLAM OpenCV

Introduction to Visual Feature Matching

Feature matching is the foundation of many computer vision applications in robotics, enabling machines to recognize scenes, track objects, and navigate environments. This tutorial breaks down the three fundamental components:

1
Keypoint Detection

Finding distinctive locations in images that are invariant to transformations

2
Descriptor Extraction

Creating numerical representations of local image regions around keypoints

3
Feature Matching

Finding correspondences between features using nearest neighbor search

Simple Analogy

Imagine you're in a library looking for a specific book:

  • Keypoint = The book's unique position on a shelf (like coordinates 12, 7, 3)
  • Descriptor = The book's title, author, ISBN, and summary
  • Nearest Neighbor = Finding the most similar book by comparing descriptions

In robotics, this process allows a robot to recognize the same physical point from different camera viewpoints.

What are Keypoints?

Formal Definition

A keypoint (or interest point) is a location in an image that has a well-defined position and can be robustly detected under various image transformations. It consists of:

How Keypoint Detectors Work

Keypoint detectors use mathematical operators to find regions that stand out from their surroundings:

Harris Corner Detector Formula

The Harris detector measures intensity changes in all directions using the structure tensor:

$$M = \sum_{x,y} w(x,y) \begin{bmatrix} I_x^2 & I_x I_y \\ I_x I_y & I_y^2 \end{bmatrix}$$

Where $I_x$ and $I_y$ are image derivatives, and $w(x,y)$ is a Gaussian window. Corners are identified when both eigenvalues of M are large.

FAST (Features from Accelerated Segment Test) Algorithm

A pixel $p$ is a corner if there are $n$ contiguous pixels in a circle around $p$ that are all brighter or darker than $p$ by threshold $t$:

$$|I(p) - I(x)| > t \quad \text{for } x \in \text{Bresenham circle of radius 3}$$

Typically $n = 9$ or $12$ for balanced speed and accuracy.

Detector Principle Invariance Speed Common Use
Harris Corner Eigenvalues of gradient matrix Rotation, illumination Medium Traditional applications
FAST Pixel intensity comparisons Rotation Very Fast Real-time systems
SIFT Difference of Gaussians Rotation, scale, illumination Slow High-accuracy matching
ORB oFAST + rBRIEF Rotation, scale Fast Real-time SLAM

What are Descriptors?

A descriptor is a numerical representation of the image patch surrounding a keypoint. It encodes visual information in a way that is:

Descriptor Visualization

Below is a simplified 8×4 descriptor grid showing intensity patterns (darker = lower value, brighter = higher value):

Descriptor Types and Mathematics

SIFT (Scale-Invariant Feature Transform) Descriptor

SIFT creates a 128-element vector from gradient orientations in 4×4 subregions around the keypoint:

For each 4×4 subregion, compute gradient magnitude $m(x,y)$ and orientation $\theta(x,y)$:

$$m(x,y) = \sqrt{(L(x+1,y)-L(x-1,y))^2 + (L(x,y+1)-L(x,y-1))^2}$$

$$\theta(x,y) = \tan^{-1}\left(\frac{L(x,y+1)-L(x,y-1)}{L(x+1,y)-L(x-1,y)}\right)$$

Create an 8-bin orientation histogram for each subregion, resulting in 4×4×8 = 128 dimensions.

ORB (Oriented FAST and Rotated BRIEF) Descriptor

ORB uses a binary descriptor based on intensity comparisons:

For a keypoint at orientation $\theta$, define test pairs $(p_i, q_i)$ rotated by $\theta$:

$$\tau(p; \mathbf{p}, \mathbf{q}) = \sum_{i=1}^{n} 2^{i-1} s(p(\mathbf{p}_i), p(\mathbf{q}_i))$$

Where $s(a,b) = 1$ if $a < b$, else $0$. The result is a 256-bit binary string (32 bytes).

Numerical Example: Creating a Simple Descriptor

Consider a tiny 3×3 image patch around a keypoint:

455048
525553
495150

A simple 4-element descriptor could be:

  1. Mean intensity: (45+50+48+52+55+53+49+51+50)/9 = 50.33
  2. Horizontal gradient: (right - left) = (48+53+50)/3 - (45+52+49)/3 = 50.33 - 48.67 = 1.66
  3. Vertical gradient: (bottom - top) = (49+51+50)/3 - (45+50+48)/3 = 50.0 - 47.67 = 2.33
  4. Variance: Σ(pixel - mean)² / 9 = 6.22

Resulting descriptor vector: [50.33, 1.66, 2.33, 6.22]

Nearest Neighbor Matching

Once we have descriptors, we need to match them between images. This is typically done using Nearest Neighbor (NN) search.

Distance Metrics

To find "nearest" descriptors, we need a way to measure distance between them:

Euclidean Distance (L2 Norm)

For two descriptors $A = [a_1, a_2, ..., a_n]$ and $B = [b_1, b_2, ..., b_n]$:

$$d_{\text{Euclidean}}(A, B) = \sqrt{\sum_{i=1}^{n} (a_i - b_i)^2}$$

Example: For $A = [2, 5, 1]$ and $B = [4, 3, 2]$:

$$d = \sqrt{(2-4)^2 + (5-3)^2 + (1-2)^2} = \sqrt{4 + 4 + 1} = \sqrt{9} = 3$$

Hamming Distance (for binary descriptors)

For binary descriptors (like ORB), count the number of differing bits:

$$d_{\text{Hamming}}(A, B) = \sum_{i=1}^{n} (a_i \oplus b_i)$$

Where $\oplus$ is the XOR operation.

Example: For binary strings $A = 10110101$ and $B = 10011101$:

Positions differ at bits 3 and 5, so $d = 2$.

Matching Example

We have 3 database descriptors and 1 query descriptor:

Database Descriptors
  • D1: [2.1, 5.3, 1.8]
  • D2: [4.2, 3.1, 2.0]
  • D3: [1.9, 4.8, 1.5]
Query Descriptor

Q: [2.0, 5.0, 1.7]

Matching Algorithms

Brute-Force Matching

Compare query descriptor with every descriptor in database:

Complexity: $O(n \cdot m)$ where $n$ = query descriptors, $m$ = database descriptors

Simple but computationally expensive for large databases.

FLANN (Fast Library for Approximate Nearest Neighbors)

Uses optimized data structures like k-d trees or hierarchical k-means trees:

Complexity: $O(\log n)$ for searches after $O(n \log n)$ tree construction

Trades off exact matches for significant speed improvements.

Ratio Test for Robust Matching

To eliminate ambiguous matches, use Lowe's ratio test:

For query descriptor $Q$, find two nearest neighbors $N_1$ and $N_2$ with distances $d_1$ and $d_2$.

Accept match if: $$\frac{d_1}{d_2} < \text{threshold (typically 0.7-0.8)}$$

This ensures the best match is significantly better than the second-best.

Application in Robotic SLAM

SLAM (Simultaneous Localization and Mapping) is a fundamental problem in robotics where a robot builds a map of an unknown environment while simultaneously tracking its location within that map.

Visual SLAM Pipeline

1
Frame Capture

Robot captures image from camera

2
Feature Extraction

Detect keypoints and compute descriptors

3
Feature Matching

Match to previous frame or map features

4
Pose Estimation

Calculate robot motion from matches

5
Map Update

Add new landmarks to map

Detailed Numerical Example: Robot Localization

Let's trace through a simplified example of how a robot uses feature matching to estimate its movement:

Scenario

A robot observes a landmark (a corner) at position (x=2.0m, y=1.5m) in its coordinate system at time t=0.

At time t=1, the robot moves and sees what appears to be the same landmark, but now at (x=1.8m, y=1.7m) in its new coordinate system.

Step 1: Feature Matching Between Frames

The robot detects ORB features in both images:

Step 2: Nearest Neighbor Search

For each feature in Frame 1, perform NN search in Frame 0 database:

Frame 1 Feature Closest Frame 0 Match Distance (Hamming) 2nd Closest Match Distance Ratio Accept?
F1_1 (Desc: 0xA3F1...) F0_42 (Desc: 0xA3F5...) 3 F0_87 (Desc: 0xB3F1...) 12 3/12 = 0.25 ✓ (0.25 < 0.8)
F1_2 (Desc: 0x4C2A...) F0_91 (Desc: 0x4D2A...) 5 F0_12 (Desc: 0x4C2B...) 6 5/6 = 0.83 ✗ (0.83 > 0.8)
F1_3 (Desc: 0x9B01...) F0_33 (Desc: 0x9B01...) 0 F0_67 (Desc: 0x9B81...) 2 0/2 = 0 ✓ (0 < 0.8)

After ratio test: 120 good matches found from 155 features (77% match rate).

Step 3: Motion Estimation (Simplified 2D Case)

From matched features, we can estimate robot motion. For a single matched point:

Let $P_0 = (x_0, y_0)$ be landmark position in Frame 0 coordinates.

Let $P_1 = (x_1, y_1)$ be same landmark in Frame 1 coordinates.

If robot moved by $(\Delta x, \Delta y)$ and rotated by $\theta$, then:

$$P_0 = R(\theta) \cdot P_1 + T$$

Where $R(\theta) = \begin{bmatrix} \cos\theta & -\sin\theta \\ \sin\theta & \cos\theta \end{bmatrix}$ and $T = [\Delta x, \Delta y]^T$.

With multiple matches, we solve for $\theta$, $\Delta x$, $\Delta y$ that minimize reprojection error.

Step 4: Numerical Calculation

Suppose we have 3 good matches with these pixel coordinates:

FeatureFrame 0 (pixels)Frame 1 (pixels)
A(100, 150)(95, 155)
B(200, 100)(205, 95)
C(150, 200)(145, 205)

Using the 8-point algorithm (simplified):

  1. Calculate centroid of points in each frame
  2. Normalize points by subtracting centroid

Practical Code Examples


import cv2
import numpy as np
from matplotlib import pyplot as plt

class FeatureMatcher:
    def __init__(self, detector_type='ORB'):
        """Initialize feature detector and matcher"""
        if detector_type == 'ORB':
            self.detector = cv2.ORB_create(nfeatures=1000, scaleFactor=1.2, nlevels=8)
            self.matcher = cv2.BFMatcher(cv2.NORM_HAMMING, crossCheck=False)
        elif detector_type == 'SIFT':
            self.detector = cv2.SIFT_create(nfeatures=1000)
            self.matcher = cv2.BFMatcher(cv2.NORM_L2, crossCheck=False)
        
        self.detector_type = detector_type
        self.ratio_threshold = 0.75  # Lowe's ratio test threshold
    
    def detect_and_compute(self, image):
        """Detect keypoints and compute descriptors"""
        gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) if len(image.shape) == 3 else image
        keypoints, descriptors = self.detector.detectAndCompute(gray, None)
        return keypoints, descriptors
    
    def match_features(self, desc1, desc2, method='knn'):
        """Match descriptors using specified method"""
        if method == 'knn':
            # k-Nearest Neighbors with ratio test
            matches = self.matcher.knnMatch(desc1, desc2, k=2)
            
            # Apply ratio test
            good_matches = []
            for m, n in matches:
                if m.distance < self.ratio_threshold * n.distance:
                    good_matches.append(m)
            
            return good_matches
        
        elif method == 'bf':
            # Brute-force matching with cross-check
            bf = cv2.BFMatcher(cv2.NORM_HAMMING, crossCheck=True)
            matches = bf.match(desc1, desc2)
            matches = sorted(matches, key=lambda x: x.distance)
            return matches
    
    def visualize_matches(self, img1, kp1, img2, kp2, matches):
        """Visualize matched features between two images"""
        # Draw matches
        match_img = cv2.drawMatches(
            img1, kp1, img2, kp2, matches[:50], None,
            flags=cv2.DrawMatchesFlags_NOT_DRAW_SINGLE_POINTS
        )
        
        # Add statistics
        cv2.putText(match_img, f'Matches: {len(matches)}', (10, 30),
                   cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2)
        
        return match_img

# Example usage
if __name__ == "__main__":
    # Load images
    img1 = cv2.imread('frame1.jpg')
    img2 = cv2.imread('frame2.jpg')
    
    # Initialize matcher
    matcher = FeatureMatcher('ORB')
    
    # Detect features
    kp1, desc1 = matcher.detect_and_compute(img1)
    kp2, desc2 = matcher.detect_and_compute(img2)
    
    print(f"Image 1: {len(kp1)} keypoints")
    print(f"Image 2: {len(kp2)} keypoints")
    
    # Match features
    matches = matcher.match_features(desc1, desc2, method='knn')
    print(f"Good matches: {len(matches)}")
    
    # Visualize
    result = matcher.visualize_matches(img1, kp1, img2, kp2, matches)
    cv2.imshow('Feature Matches', result)
    cv2.waitKey(0)
    cv2.destroyAllWindows()
                    

import numpy as np
import cv2
from scipy.spatial import KDTree
from typing import List, Tuple, Optional

class VisualSLAMFeatureTracker:
    """A simplified visual feature tracker for SLAM applications"""
    
    def __init__(self):
        self.orb = cv2.ORB_create(nfeatures=2000)
        self.flann = cv2.FlannBasedMatcher(
            dict(algorithm=6, table_number=6, key_size=12, multi_probe_level=1),
            dict(checks=50)
        )
        
        # Database of past features
        self.feature_database = {
            'keypoints': [],      # List of keypoint objects
            'descriptors': [],    # List of descriptor arrays
            'positions': [],      # 3D positions in world coordinates
            'frames': []          # Frame indices where features were seen
        }
        
        self.frame_count = 0
        self.min_matches_for_tracking = 10
    
    def process_frame(self, frame: np.ndarray) -> dict:
        """Process a new frame and return tracking results"""
        self.frame_count += 1
        
        # 1. Feature detection
        gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
        keypoints, descriptors = self.orb.detectAndCompute(gray, None)
        
        if descriptors is None:
            return {'status': 'no_features', 'matches': 0}
        
        # 2. Convert descriptors to float32 for FLANN
        descriptors_float = descriptors.astype(np.float32)
        
        # 3. Match with previous frame (if exists)
        matches = []
        if len(self.feature_database['descriptors']) > 0:
            # Convert database descriptors to float32
            db_descriptors = np.array(self.feature_database['descriptors']).astype(np.float32)
            
            # kNN matching with k=2 for ratio test
            knn_matches = self.flann.knnMatch(descriptors_float, db_descriptors, k=2)
            
            # Apply Lowe's ratio test
            matches = []
            for match_pair in knn_matches:
                if len(match_pair) == 2:
                    m, n = match_pair
                    if m.distance < 0.7 * n.distance:
                        matches.append(m)
        
        # 4. Estimate camera motion (simplified - using essential matrix)
        motion_estimate = None
        if len(matches) >= self.min_matches_for_tracking:
            # Extract matched points
            src_pts = np.float32([keypoints[m.queryIdx].pt for m in matches])
            dst_pts = np.float32([self.feature_database['keypoints'][m.trainIdx].pt 
                                 for m in matches])
            
            # Estimate essential matrix
            E, mask = cv2.findEssentialMat(
                src_pts, dst_pts, 
                focal=1.0, pp=(0, 0),
                method=cv2.RANSAC, prob=0.999, threshold=1.0
            )
            
            if E is not None:
                # Recover pose from essential matrix
                _, R, t, mask = cv2.recoverPose(E, src_pts, dst_pts)
                motion_estimate = {'rotation': R, 'translation': t}
        
        # 5. Update feature database (add new features)
        if len(matches) < 100:  # Add new features if we don't have enough matches
            # Select unmatched keypoints
            matched_indices = set([m.queryIdx for m in matches])
            unmatched_indices = [i for i in range(len(keypoints)) 
                               if i not in matched_indices]
            
            # Add top N unmatched features
            n_to_add = min(50, len(unmatched_indices))
            for i in unmatched_indices[:n_to_add]:
                self.feature_database['keypoints'].append(keypoints[i])
                self.feature_database['descriptors'].append(descriptors[i])
                # Initialize with unknown 3D position
                self.feature_database['positions'].append(None)
                self.feature_database['frames'].append(self.frame_count)
        
        return {
            'status': 'success',
            'matches': len(matches),
            'keypoints': len(keypoints),
            'motion': motion_estimate,
            'database_size': len(self.feature_database['descriptors'])
        }
    
    def triangulate_points(self, points1, points2, P1, P2):
        """Triangulate 3D points from 2D correspondences"""
        points_4d = cv2.triangulatePoints(P1, P2, points1.T, points2.T)
        points_3d = points_4d[:3] / points_4d[3]
        return points_3d.T

# Usage example for robot navigation
if __name__ == "__main__":
    # Simulate robot camera frames
    tracker = VisualSLAMFeatureTracker()
    
    # Simulated camera intrinsic parameters
    K = np.array([[800, 0, 320],
                  [0, 800, 240],
                  [0, 0, 1]])
    
    # Process simulated frames
    for i in range(10):
        # In real application, this would be actual camera frames
        # Here we create synthetic frames for demonstration
        frame = np.random.randint(0, 255, (480, 640, 3), dtype=np.uint8)
        
        # Add some features (simulating scene)
        cv2.circle(frame, (100 + i*10, 100), 5, (255, 0, 0), -1)
        cv2.circle(frame, (200, 150 + i*5), 5, (0, 255, 0), -1)
        
        # Process frame
        result = tracker.process_frame(frame)
        
        print(f"Frame {i}: {result['matches']} matches, "
              f"Database: {result['database_size']} features")
        
        if result['motion']:
            print(f"  Estimated motion: {result['motion']['translation'].flatten()}")
                    

import numpy as np
from typing import List, Tuple
from dataclasses import dataclass

@dataclass
class Keypoint:
    """Mathematical representation of a keypoint"""
    x: float          # x-coordinate
    y: float          # y-coordinate
    scale: float      # scale at which keypoint was detected
    orientation: float  # orientation in radians
    response: float   # detector response strength

@dataclass  
class Descriptor:
    """Mathematical representation of a descriptor"""
    vector: np.ndarray  # Feature vector
    keypoint: Keypoint  # Associated keypoint
    type: str           # Descriptor type

class MathematicalFeatureMatcher:
    """Pure mathematical implementation of feature matching algorithms"""
    
    @staticmethod
    def compute_simple_descriptor(image_patch: np.ndarray) -> np.ndarray:
        """
        Compute a simple descriptor from an image patch
        Based on intensity and gradient statistics
        """
        h, w = image_patch.shape
        
        # 1. Intensity histogram (4 bins)
        hist, _ = np.histogram(image_patch, bins=4, range=(0, 255))
        hist = hist / (h * w)  # Normalize
        
        # 2. Gradient magnitude and orientation statistics
        # Compute gradients using Sobel operators
        sobel_x = np.array([[-1, 0, 1], [-2, 0, 2], [-1, 0, 1]])
        sobel_y = np.array([[-1, -2, -1], [0, 0, 0], [1, 2, 1]])
        
        # Ensure patch is large enough
        if h > 2 and w > 2:
            grad_x = np.abs(np.convolve(image_patch.flatten(), sobel_x.flatten(), 'valid'))
            grad_y = np.abs(np.convolve(image_patch.flatten(), sobel_y.flatten(), 'valid'))
            grad_magnitude = np.sqrt(grad_x**2 + grad_y**2)
            
            grad_mean = np.mean(grad_magnitude) if len(grad_magnitude) > 0 else 0
            grad_std = np.std(grad_magnitude) if len(grad_magnitude) > 0 else 0
        else:
            grad_mean, grad_std = 0, 0
        
        # 3. Create descriptor vector
        descriptor = np.concatenate([
            hist,                    # 4 elements
            [grad_mean, grad_std],   # 2 elements
            [np.mean(image_patch), np.std(image_patch)]  # 2 elements
        ])
        
        return descriptor
    
    @staticmethod
    def euclidean_distance(vec1: np.ndarray, vec2: np.ndarray) -> float:
        """Compute Euclidean distance between two vectors"""
        return np.sqrt(np.sum((vec1 - vec2) ** 2))
    
    @staticmethod
    def hamming_distance(bits1: np.ndarray, bits2: np.ndarray) -> int:
        """Compute Hamming distance between two binary vectors"""
        return np.sum(bits1 != bits2)
    
    @staticmethod
    def brute_force_nn(query: np.ndarray, database: List[np.ndarray]) -> Tuple[int, float]:
        """
        Brute-force nearest neighbor search
        Returns (index_of_best_match, distance)
        """
        best_idx = -1
        best_dist = float('inf')
        
        for i, db_vec in enumerate(database):
            dist = np.linalg.norm(query - db_vec)  # Euclidean distance
            if dist < best_dist:
                best_dist = dist
                best_idx = i
        
        return best_idx, best_dist
    
    @staticmethod
    def knn_search(query: np.ndarray, database: List[np.ndarray], k: int = 2) -> List[Tuple[int, float]]:
        """
        k-Nearest Neighbors search
        Returns list of (index, distance) for k closest matches
        """
        distances = []
        
        for i, db_vec in enumerate(database):
            dist = np.linalg.norm(query - db_vec)
            distances.append((i, dist))
        
        # Sort by distance and return top k
        distances.sort(key=lambda x: x[1])
        return distances[:k]
    
    @staticmethod
    def lowes_ratio_test(matches: List[Tuple[int, float]], ratio_threshold: float = 0.8) -> bool:
        """
        Apply Lowe's ratio test to determine if match is reliable
        Returns True if match passes the test
        """
        if len(matches) < 2:
            return False
        
        # matches[0] is best match, matches[1] is second best
        d1 = matches[0][1]  # distance to best match
        d2 = matches[1][1]  # distance to second best
        
        return d1 < ratio_threshold * d2

# Example: Mathematical workflow for feature matching
if __name__ == "__main__":
    matcher = MathematicalFeatureMatcher()
    
    # Create synthetic image patches (simulating regions around keypoints)
    patch1 = np.random.randint(0, 255, (16, 16))
    patch2 = np.random.randint(0, 255, (16, 16))
    patch3 = np.random.randint(0, 255, (16, 16))
    
    # Create database patches
    db_patches = [np.random.randint(0, 255, (16, 16)) for _ in range(10)]
    
    # Compute descriptors
    query_desc = matcher.compute_simple_descriptor(patch1)
    database_descs = [matcher.compute_simple_descriptor(patch) for patch in db_patches]
    
    print(f"Query descriptor shape: {query_desc.shape}")
    print(f"Descriptor values: {query_desc}")
    
    # Perform brute-force NN search
    best_idx, best_dist = matcher.brute_force_nn(query_desc, database_descs)
    print(f"\nBrute-force NN result:")
    print(f"  Best match index: {best_idx}")
    print(f"  Distance: {best_dist:.4f}")
    
    # Perform k-NN search
    knn_results = matcher.knn_search(query_desc, database_descs, k=3)
    print(f"\nk-NN results (k=3):")
    for idx, (match_idx, dist) in enumerate(knn_results):
        print(f"  Match {idx}: index={match_idx}, distance={dist:.4f}")
    
    # Apply Lowe's ratio test
    passes_test = matcher.lowes_ratio_test(knn_results, ratio_threshold=0.8)
    print(f"\nLowe's ratio test: {'PASS' if passes_test else 'FAIL'}")
    
    # Demonstrate with binary descriptors (simulated)
    print(f"\n--- Binary Descriptor Example ---")
    binary_desc1 = np.random.randint(0, 2, 256)  # 256-bit binary descriptor
    binary_desc2 = np.random.randint(0, 2, 256)
    
    hamming_dist = matcher.hamming_distance(binary_desc1, binary_desc2)
    print(f"Binary descriptor 1: {binary_desc1[:8]}... (first 8 bits)")
    print(f"Binary descriptor 2: {binary_desc2[:8]}... (first 8 bits)")
    print(f"Hamming distance: {hamming_dist} bits different")
                    

Summary and Further Resources

Key Takeaways
  • Keypoints are distinctive, repeatable locations in images
  • Descriptors encode local appearance into numerical vectors
  • Nearest Neighbor matching finds correspondences between descriptors
  • Ratio test improves matching robustness
  • These form the foundation for Visual SLAM in robotics
Common Challenges & Solutions
  • Occlusions: Use robust estimators (RANSAC)
  • Scale changes: Use scale-invariant detectors (SIFT, ORB)
  • Lighting changes: Use illumination-invariant descriptors
  • Computational cost: Use efficient algorithms (FAST, binary descriptors)
Recommended Resources
Practice Exercises
  1. Implement a simple corner detector using the Harris formula
  2. Create your own 16-element descriptor using image statistics
  3. Implement brute-force matching and compare with kd-tree acceleration
  4. Simulate a robot moving in 2D and use feature matching to estimate its trajectory