Source code for tracklib.algo.Comparison

"""to manage comparisons of GPS tracks"""


import sys
import math
from typing import Iterable, Literal, Union   
import progressbar
import numpy as np
import matplotlib.pyplot as plt

from tracklib.core.TrackCollection import TrackCollection
from tracklib.core.Track import Track

import tracklib.algo.Cinematics as Cinematics
import tracklib.algo.Dynamics as Dynamics
import tracklib.algo.Interpolation as Interpolation

from tracklib.util.Geometry import dist_point_to_segment

MODE_COMPARAISON_NEAREST_NEIGHBOUR = 1
MODE_COMPARAISON_DTW = 2
MODE_COMPARAISON_FDTW = 3


[docs]def plotDifferenceProfile( profile, track2, af_name="pair", sym="g--", NO_DATA_VALUE: int = -1): """Difference profile plot :param profile: TODO :param track2: TODO :param af_name: TODO :param sym: TODO :param NO_DATA_VALUE: TODO """ for i in range(profile.size()): if profile.getObsAnalyticalFeature(af_name, i) == NO_DATA_VALUE: continue x1 = profile.getObs(i).position.getX() y1 = profile.getObs(i).position.getY() x2 = track2.getObs(profile.getObsAnalyticalFeature(af_name, i)).position.getX() y2 = track2.getObs(profile.getObsAnalyticalFeature(af_name, i)).position.getY() plt.plot([x1, x2], [y1, y2], sym, linewidth=2)
[docs]def differenceProfile(track1, track2, mode: Literal["NN", "DTW", "FDTW"] = "NN", ends=False, p=1, verbose: bool = True): """Profile of difference between two traces Three possible modes: - **NN** (Nearest Neighbour): :math:`O(n^2)` time and :math:`O(n)` space - **DTW** (Dynamic Time Warping): :math:`O(n^3)` time and :math:`O(n^2)` space - **FDTW** (Fast Dynamic Time Warping): same as DTW with reduced search space. In this particular case, 'ends' parameter is an integer giving the number of points to search for a match ahead and behind the current point. E.g. for ends=0, there is a strict matching track1[i] <-> track2[i] for each epoch i. For ends=10 for example, each point track[i] can be matched with any point chronologically between the bounds track2[i-10] and track2[i+10]. Default is equal to 3, meaning that track1 may be at most 3 times faster or slower than track2 on ant given sub-interval. Note that this method is designed for pairs of tracks having about same number of points. Otherwise, it is strongly advised to perform a spatial resampling before applying FDTW :param track1: TODO :param track2: TODO :param mode: Mode for the interpolation. Three modes are possible : NN, DTW and FDTW :param ends: TODO :param p: TODO :param verbose: Verbose mode :return: A track objet, with an analytical feature diff containing shortest distance of each point of track t1, to the points of track t2. We may get profile as a list with :func:`output.getAbsCurv()` and :func:`output.getAnalyticalFeature("diff")` The selected candidate in registerd in AF "pair" Set "ends" parameter to True to force end points to meet p is Minkowski's exponent for distance computation. Default value is - 1 for summation of distances, - 2 for least squares solution - and 10 for an approximation of Frechet solution. """ output = track1.copy() output.createAnalyticalFeature("diff") output.createAnalyticalFeature("pair") output.createAnalyticalFeature("ex") output.createAnalyticalFeature("ey") # -------------------------------------------------------- # Nearest Neighbor (NN) algorithm # -------------------------------------------------------- if mode == "NN": to_run = range(output.size()) if verbose: to_run = progressbar.progressbar(to_run) for i in to_run: val_min = sys.float_info.max id_min = 0 for j in range(track2.size()): distance = output.getObs(i).distance2DTo(track2.getObs(j)) if distance < val_min: val_min = distance id_min = j output.setObsAnalyticalFeature("diff", i, val_min) output.setObsAnalyticalFeature("pair", i, id_min) ex = track1.getObs(i).position.getX() - track2.getObs(id_min).position.getX() ey = track1.getObs(i).position.getY() - track2.getObs(id_min).position.getY() output.setObsAnalyticalFeature("ex", i, ex) output.setObsAnalyticalFeature("ey", i, ey) # -------------------------------------------------------- # Dynamic time warping (DTW) algorithm # -------------------------------------------------------- if mode == "DTW": p = max(min(p, 15), 1e-2) track1 = track1.copy() track2 = track2.copy() # Forming distance matrix D = np.zeros((track1.size(), track2.size())) for i in range(track1.size()): for j in range(track2.size()): D[i, j] = track1.getObs(i).distance2DTo(track2.getObs(j)) ** p # Optimal path with dynamic programming T = np.zeros((track1.size(), track2.size())) M = np.zeros((track1.size(), track2.size())) T[0, 0] = D[0, 0] M[0, 0] = -1 # Forward step step_to_run = range(1, T.shape[0]) if verbose: step_to_run = progressbar.progressbar(step_to_run) for i in step_to_run: T[i, 0] = T[i - 1, 0] + D[i, 0] M[i, 0] = 0 for j in range(1, T.shape[1]): K = D[i, 0 : (j + 1)] for k in range(j - 1, -1, -1): K[k] = K[k] + K[k + 1] V = T[i - 1, 0 : (j + 1)] + K M[i, j] = np.argmin(V) T[i, j] = V[int(M[i, j])] # Backward step S = [0] * (track1.size()) if ends: S[track1.size() - 1] = int(M[track1.size() - 1, track2.size() - 1]) else: S[track1.size() - 1] = np.argmin(T[track1.size() - 1, :]) for i in range(track1.size() - 2, -1, -1): S[i] = int(M[i + 1, S[i + 1]]) # print((T[track1.size()-1, S[track1.size()-1]] / track1.size())**(1.0/p)) # plt.plot(S, 'r-') # plt.imshow(M) __fillAFProfile(track1, track2, output, S) # -------------------------------------------------------- # Fast Dynamic time warping (FDTW) algorithm # -------------------------------------------------------- if mode == "FDTW": if isinstance(ends, bool): if not ends: ends = 12 S = lambda track, k: [ p for p in range(max(0, k - ends), min(len(track2) - 1, k + ends)) ] Q = lambda i, j, k, t: (j < i + 30) * (j >= i) * 1 P = lambda s, y, k, t: math.exp(-track2[s].position.distance2DTo(y)) Dynamics.HMM(S, Q, P).estimate( output, ["x", "y"], mode=Dynamics.MODE_OBS_AS_2D_POSITIONS, verbose=2 * verbose, ) __fillAFProfile(track1, track2, output, output["hmm_inference"]) Cinematics.computeAbsCurv(output) return output
[docs]def __fillAFProfile(track1, track2, output, S): """TODO :param track1: TODO :param track2: TODO :param output: TODO :param S: TODO """ for i in range(track1.size()): d = track1.getObs(i).distance2DTo(track2.getObs(S[i])) ex = track1.getObs(i).position.getX() - track2.getObs(S[i]).position.getX() ey = track1.getObs(i).position.getY() - track2.getObs(S[i]).position.getY() output.setObsAnalyticalFeature("diff", i, d) output.setObsAnalyticalFeature("pair", i, S[i]) output.setObsAnalyticalFeature("ex", i, ex) output.setObsAnalyticalFeature("ey", i, ey)
[docs]def synchronize(track1, track2): """Resampling of 2 tracks with linear interpolation on a common base of timestamps :param track: track to synchronize with """ Interpolation.synchronize(track1, track2)
[docs]def compare(track1, track2) -> float: """Comparison of 2 tracks. Tracks are interpolated linearly on a common base of timestamps :param track1: track to compare with :param track2: track to compare with :return: TODO """ trackA = track1.copy() trackB = track2.copy() Interpolation.synchronize(trackA, trackB) rmse = 0 for i in range(trackA.size()): rmse += trackA.getObs(i).distanceTo(trackB.getObs(i)) ** 2 return math.sqrt(rmse / trackA.size())
[docs]def centralTrack(tracks: Union[TrackCollection, Iterable[Track]], mode: Literal["NN", "DTW", "FDTW"] = "NN", verbose: bool = True) -> Track: """Computes central track of a track collection :param tracks: TrackCollection or list of tracks :param mode: "NN", "DTW" or "FDTW" for track pair matching (see the documentation of :func:`differenceProfile` function for more infos on modes) :return: The central track """ tracks = tracks.copy() if isinstance(tracks, list): tracks = TrackCollection(tracks) base = tracks.toENUCoordsIfNeeded() central = tracks[0].copy() for i in range(1, len(tracks)): diff = differenceProfile(tracks[0], tracks[i], mode=mode, verbose=verbose) for j in range(len(central)): dx = tracks[i][diff["pair", j]].position.getX() dy = tracks[i][diff["pair", j]].position.getY() dz = tracks[i][diff["pair", j]].position.getZ() central[j].position.translate(dx, dy, dz) for j in range(len(central)): central[j].position.scale(1.0 / len(tracks)) if not base is None: central.toGeoCoords(base) return central
[docs]def premiereComposanteHausdorff(track1, track2): ''' Première composante de Hausdorff. Parameters ---------- track1 : Track the first track track2 : Track the second track Returns ------- double directed Hausdorff distance ''' result = 0 for p in range(track1.size()): point = track1.getObs(p) distmin = track2.getFirstObs().distanceTo(point); for i in range(0, track2.size() - 1): obs2i = track2.getObs(i) obs2ip1 = track2.getObs(i+1) dist = dist_point_to_segment(point.position, [obs2i.position.getX(), obs2i.position.getY(), obs2ip1.position.getX(), obs2ip1.position.getY()]) distmin = min(dist, distmin) result = max(distmin, result) return result
[docs]def hausdorff(track1, track2): ''' General Hausdorff distance between two tracks. Parameters ---------- track1 : Track the first track track2 : Track the second track Returns ------- double Hausdorff distance ''' return max(premiereComposanteHausdorff(track1, track2), premiereComposanteHausdorff(track2, track1))
[docs]def discreteFrechet(track1, track2): sizeP = track1.size() sizeQ = track2.size() ca = [] for i in range(sizeP): ca.append([]) for j in range(sizeQ): ca[i].append(-1) d = discreteFrechetCouplingMeasure(track1, track2, sizeP - 1, sizeQ - 1, ca); return d;
[docs]def discreteFrechetCouplingMeasure(track1, track2, i, j, ca): if ca[i][j] > -1: return ca[i][j] d = track1.getObs(i).distanceTo(track2.getObs(j)) if i == 0 and j == 0: ca[i][j] = d return d if i > 0 and j == 0: ca[i][j] = max( discreteFrechetCouplingMeasure(track1, track2, i - 1, j, ca), d) return ca[i][j] if i == 0 and j > 0: ca[i][j] = max(discreteFrechetCouplingMeasure(track1, track2, i, j - 1, ca), d) return ca[i][j] if i > 0 and j > 0: ca[i][j] = max( min(discreteFrechetCouplingMeasure(track1, track2, i - 1, j, ca), min(discreteFrechetCouplingMeasure(track1, track2, i - 1, j - 1, ca), discreteFrechetCouplingMeasure(track1, track2, i, j - 1, ca))), d) return ca[i][j] ca[i][j] = sys.float_info.max return ca[i][j]
[docs]def medoid (tracks: Union[TrackCollection, Iterable[Track]], mode: Literal["Hausdorff"] = "Hausdorff", verbose: bool = True) -> Track: tracks = tracks.copy() if isinstance(tracks, list): tracks = TrackCollection(tracks) base = tracks.toENUCoordsIfNeeded() medoid = tracks[0].copy() for i in range(1, len(tracks)): for j in range(len(medoid)): d = 0 #diff = differenceProfile(tracks[0], tracks[i], mode=mode, verbose=verbose) #for j in range(len(medoid)): #dx = tracks[i][diff["pair", j]].position.getX() #dy = tracks[i][diff["pair", j]].position.getY() #dz = tracks[i][diff["pair", j]].position.getZ() #medoid[j].position.translate(dx, dy, dz) #for j in range(len(medoid)): # medoid[j].position.scale(1.0 / len(tracks)) if not base is None: medoid.toGeoCoords(base) return medoid