Leuven.MapMatching

repository·master·Indexed 42 days ago

https://github.com/wannesm/leuvenmapmatching

A Python library for aligning GPS measurement traces to maps or road segments using a Hidden Markov Model (HMM) with non-emitting states. It supports handling missing data, custom transition and emission probability distributions, and incremental map matching for streaming observations. The library provides concrete implementations such as SimpleMatcher and DistanceMatcher, along with map representations like InMemMap and SqliteMap.

Tokens
7.9K
Snippets
22
Records
30
Agent score
53%

What's inside Leuven.MapMatching

  1. What is Leuven.MapMatching?

    master

    Leuven.MapMatching is a library designed to align a trace of coordinates (such as GPS measurements) to a map of road segments.

    It utilizes a Hidden Markov Model (HMM) with non-emitting states to perform the matching. This approach allows the model to handle missing data effectively. Additionally, the library is extensible, allowing users to plug in custom transition and emission probability distributions.

  2. Overview of Leuven.MapMatching components

    master

    The leuvenmapmatching library is organized into three primary functional areas:

    1. Matcher: Contains the logic for matching trajectories to a map. This includes the BaseMatcher abstraction, as well as specific implementations like SimpleMatcher and DistanceMatcher.
    2. Map: Handles the underlying map data structures. It provides a BaseMap interface with implementations such as InMemMap (in-memory) and SqliteMap (SQLite-backed).
    3. Util: Provides utility classes like Segment to support matching operations.

    To perform map matching, you typically use a Matcher implementation configured with a specific Map implementation.

  3. Create an InMemMap using OSMnx and GeoPandas

    master

    Alternatively, you can use osmnx to retrieve a graph for a specific place and populate InMemMap. There are two primary approaches:

    Approach 1: Using projected GeoDataFrames Retrieve a projected graph, convert it to GeoDataFrames, and add nodes/edges using the projected coordinates.

    Approach 2: Using unprojected GeoDataFrames or NetworkX Convert GeoDataFrames to a specific CRS (e.g., EPSG:3395) to access lat/lon for nodes, or iterate directly through the networkx graph edges to add them to the map.

    import osmnx as ox
    # ... setup map_con ...
    
    graph = ox.graph_from_place('Leuven, Belgium', network_type='drive', simplify=False)
    graph_proj = ox.project_graph(graph)
    
    # Approach 1
    nodes_proj, edges_proj = ox.graph_to_gdfs(graph_proj, nodes=True, edges=True)
    for nid, row in nodes_proj[['x', 'y']].iterrows():
        map_con.add_node(nid, (row['x'], row['y']))
    for eid, _ in edges_proj.iterrows():
        map_con.add_edge(eid[0], eid[1])
    
    # Approach 2
    nodes, edges = ox.graph_to_gdfs(graph_proj, nodes=True, edges=True)
    nodes_proj = nodes.to_crs("EPSG:3395")
    edges_proj = edges.to_crs("EPSG:3395")
    for nid, row in nodes_proj.iterrows():
        map_con.add_node(nid, (row['lat'], row['lon']))
    
    # Extract edges directly from networkx graph
    for nid1, nid2, _ in graph.edges:
        map_con.add_edge(nid1, nid2)
  4. Project Latitude-Longitude to X-Y coordinates

    master

    To improve performance, you can transform latitude-longitude coordinates into a Cartesian (X-Y) coordinate system. This can be done using the built-in to_xy() method on a Map instance or by using external tools like pyproj.

    Using Leuven.MapMatching to_xy()

    You can create a new map instance projected to X-Y and then convert your GPS route points using latlon2yx().

    Using pyproj

    You can use pyproj to project coordinates manually. Note on Coordinate Order: pyproj typically uses (longitude, latitude) (x-y convention), whereas Leuven.MapMatching follows ISO 6709 and uses (latitude, longitude). If using pyproj, you may need to use the axisswap option to align with the toolbox's expected order.

    from leuvenmapmatching.map.inmem import InMemMap
    
    # 1. Create a lat-lon map
    map_con_latlon = InMemMap("myosm", use_latlon=True)
    
    # 2. Project to X-Y
    map_con_xy = map_con_latlon.to_xy()
    
    # 3. Convert GPS route points
    route_latlon = [(50.864, 4.67878), (50.86381, 4.68054)] # (lat, lon)
    route_xy = [map_con_xy.latlon2yx(latlon) for latlon in route_latlon]
  5. Use Latitude-Longitude directly in Map objects

    master

    You can configure a Map object to handle latitude-longitude coordinates directly by setting the use_latlon flag to True. This is useful when working with raw OSM data, but note that map matching with latitude-longitude is computationally more expensive than using Euclidean distances because it requires repeated non-Euclidean computations.

    from leuvenmapmatching.map.inmem import InMemMap
    
    # Initialize map with use_latlon=True
    map_con = InMemMap("myosm", use_latlon=True)
    
    # Example: Adding nodes with (lat, lon) tuples
    map_con.add_node(entity.id, (entity.lat, entity.lon))
    
    # Example: Adding edges
    map_con.add_edge(node_a, node_b)
    map_con.add_edge(node_b, node_a)
    
    map_con.purge()
  6. Download OpenStreetMap data via Overpass API

    master

    You can download a specific region of OpenStreetMap as an XML file using the Overpass API. This is useful for providing raw map data to osmread for processing.

    from pathlib import Path
    import requests
    xml_file = Path(".") / "osm.xml"
    url = 'http://overpass-api.de/api/map?bbox=4.694933,50.870047,4.709256000000001,50.879628'
    r = requests.get(url, stream=True)
    with xml_file.open('wb') as ofile:
        for chunk in r.iter_content(chunk_size=1024):
            if chunk:
                ofile.write(chunk)
  7. Create an InMemMap using osmread and XML data

    master

    To build an in-memory map from an OSM XML file, use osmread to parse the file and InMemMap to store the nodes and edges.

    When iterating through osmread.Way entities, you can filter for specific tags (like highway) and add edges. To handle one-way roads as two-way for the matcher, you should add the edge in both directions (node_a to node_b and node_b to node_a).

    from leuvenmapmatching.map.inmem import InMemMap
    import osmread
    
    map_con = InMemMap("myosm", use_latlon=True, use_rtree=True, index_edges=True)
    for entity in osmread.parse_file(str(xml_file)):
        if isinstance(entity, osmread.Way) and 'highway' in entity.tags:
            for node_a, node_b in zip(entity.nodes, entity.nodes[1:]):
                map_con.add_edge(node_a, node_b)
                # Some roads are one-way. We'll add both directions.
                map_con.add_edge(node_b, node_a)
        if isinstance(entity, osmread.Node):
            map_con.add_node(entity.id, (entity.lat, entity.lon))
    map_con.purge()
  8. Inspect the matching lattice

    master

    The matcher.lattice contains all possible paths through the lattice. It is a dictionary where each key is an observation index and each value is a LatticeColumn object.

    Accessing Matches

    For a specific observation, you can retrieve all possible Matching objects using matcher.lattice[index].values_all().

    Backtracking through the Lattice

    You can backtrack from the end of a path to find the sequence of matches. To find the best match for the final element, you can use max() on the values with the logprob attribute as the key:

    # Find the best match for the last element
    m = max(matcher.lattice[len(path)-1].values_all(), key=lambda m: m.logprob)

    Once you have a Matching object m, you can inspect its connections to previous steps:

    • m.prev: Returns a set containing the best previous match(es) with a connection (multiple if they have equal probability).
    • m.prev_other: Returns a set of all previous matches in the lattice that have a connection to the current match.
    >>> matcher.lattice
    {0: <leuvenmapmatching.matcher.base.LatticeColumn at 0x12369bf40>,
     1: <leuvenmapmatching.matcher.base.LatticeColumn at 0x123639dc0>,
     ...}
    
    >>> matcher.lattice[0].values_all()
    {Matching<A-B-0-0>,
     Matching<A-B-0-1>,
     Matching<A-C-0-0>,
     ...}
    
    >>> # Find best match for the last element
    >>> m = max(matcher.lattice[len(path)-1].values_all(), key=lambda m: m.logprob)
    >>> m.logprob
    -0.6835815469734807
    
    >>> m.prev  # Best previous match with a connection
    {Matching<E-F-20-0>}
    >>> m.prev_other  # All previous matches in the lattice with a connection
    {Matching<C-E-20-0>,
     Matching<D-E-20-0>,
     Matching<F-E-20-0>,
     Matching<Y-E-20-0>}