Source code for tracklib.algo.Simplification

'''
**Simplification of GPS tracks**
'''

import sys
import math
import numpy as np

import tracklib.util.Geometry as Geometry
import tracklib.core.Operator as Operator

import tracklib.algo.Geometrics as Geometrics
from tracklib.algo.Segmentation import (
    MODE_SEGMENTATION_MINIMIZE,
    MODE_SEGMENTATION_MAXIMIZE,
)
from tracklib.algo.Segmentation import optimalSegmentation

# --------------------------------------------------------------------------
# Circular import (not satisfying solution)
# --------------------------------------------------------------------------
from tracklib.core.Track import Track

MODE_SIMPLIFY_DOUGLAS_PEUCKER = 1
MODE_SIMPLIFY_VISVALINGAM = 2
MODE_SIMPLIFY_SQUARING = 3
MODE_SIMPLIFY_MINIMIZE_LARGEST_DEVIATION = 4
MODE_SIMPLIFY_MINIMIZE_ELONGATION_RATIO = 5
MODE_SIMPLIFY_PRECLUDE_LARGE_DEVIATION = 6
MODE_SIMPLIFY_FREE = 7
MODE_SIMPLIFY_FREE_MAXIMIZE = 8

SQUARING_RECALL = 0.1


[docs]def simplify(track, tolerance, mode=MODE_SIMPLIFY_DOUGLAS_PEUCKER, verbose=True): """ Generic method to simplify a track. The process "Track simplification" generally returns a new simplified track. Tolerance is in the unit of track observation coordinates. Differents modes of simplification are implemented in tracklib: - MODE_SIMPLIFY_DOUGLAS_PEUCKER (1) tolerance is max allowed deviation with respect to straight line - MODE_SIMPLIFY_VISVALINGAM (2) tolerance is maximal triangle area of 3 consecutive points - MODE_SIMPLIFY_SQUARING (3) tolerance is threshold on flat and right angles - MODE_SIMPLIFY_MINIMIZE_LARGEST_DEVIATION (4) tolerance is typical max deviation with respect to straight line - MODE_SIMPLIFY_MINIMIZE_ELONGATION_RATIO (5) tolerance is typical elongation ratio of min bounding rectangle - MODE_SIMPLIFY_PRECLUDE_LARGE_DEVIATION (6) tolerance is max allowed deviation with respect to straight line - MODE_SIMPLIFY_FREE (7) tolerance is a customed function to minimize - MODE_SIMPLIFY_FREE_MAXIMIZE (8) tolerance is a customed function to maximize """ if mode == MODE_SIMPLIFY_DOUGLAS_PEUCKER: return douglas_peucker(track, tolerance) if mode == MODE_SIMPLIFY_VISVALINGAM: return visvalingam(track, tolerance) if mode == MODE_SIMPLIFY_SQUARING: return squaring(track, tolerance) if mode == MODE_SIMPLIFY_MINIMIZE_LARGEST_DEVIATION: return optimalSimplification( track, __cost_largest_deviation, tolerance, verbose ) if mode == MODE_SIMPLIFY_MINIMIZE_ELONGATION_RATIO: return optimalSimplification(track, __cost_mbr_ratio, tolerance, verbose) if mode == MODE_SIMPLIFY_PRECLUDE_LARGE_DEVIATION: return optimalSimplification( track, __cost_largest_deviation_strict, tolerance, verbose ) if mode == MODE_SIMPLIFY_FREE: return optimalSimplification(track, tolerance, None, verbose) if mode == MODE_SIMPLIFY_FREE_MAXIMIZE: max = MODE_SEGMENTATION_MAXIMIZE return optimalSimplification(track, tolerance, None, max, verbose) sys.exit("Error: track simplification mode " + (str)(mode) + " not implemented yet")
def visvalingam (track, eps): """ Function to simplify a GPS track with Visvalingam algorithm. The Visvalingram algorithm simplify the geometry of the track by reducing the number of points but the result presents less angular results than the Douglas-Peucker algorithm. Parameters ---------- :param track Track: GPS track :param eps float: length threshold epsilon (sqrt of triangle area) :return Track: simplified track """ eps **= 2 output = track.copy() output.addAnalyticalFeature(Geometry.aire_visval, "@aire") while 1: id = output.operate(Operator.Operator.ARGMIN, "@aire") if output.getObsAnalyticalFeature("@aire", id) > eps: break output.removeObs(id) if id > 1: output.setObsAnalyticalFeature( "@aire", id - 1, Geometry.aire_visval(output, id - 1) ) if id < output.size() - 1: output.setObsAnalyticalFeature( "@aire", id, Geometry.aire_visval(output, id) ) output.removeAnalyticalFeature("@aire") return output def douglas_peucker (track, eps): """ Function to simplify a GPS track with Douglas-Peucker algorithm. The Douglas-Peucker algorithm reduce the number of a line by reducing the number of points. The result should keep the original shape. Parameters ---------- :param track Track: GPS track :param eps float: length threshold epsilon (sqrt of triangle area) :return Track: simplified track """ L = track.getObsList() n = len(L) if n <= 2: return Track(L) dmax = 0 imax = 0 for i in range(0, n): x0 = L[i].position.getX() y0 = L[i].position.getY() xa = L[0].position.getX() ya = L[0].position.getY() xb = L[n - 1].position.getX() yb = L[n - 1].position.getY() d = Geometry.distance_to_segment(x0, y0, xa, ya, xb, yb) if d > dmax: dmax = d imax = i if dmax < eps: return Track( [L[0], L[n - 1]], user_id=track.uid, track_id=track.tid, base=track.base ) else: XY1 = Track(L[0:imax], user_id=track.uid, track_id=track.tid, base=track.base) XY2 = Track(L[imax:n], user_id=track.uid, track_id=track.tid, base=track.base) return douglas_peucker(XY1, eps) + douglas_peucker(XY2, eps) # -------------------------------------------------------------------------- # # -------------------------------------------------------------------------- # Input : # - track :: GPS track # - cost :: A cost function that should be minimized or maximized # - eps :: A threshold (used as global parameter in cost function # - mode :: To specify if cost must be minimized or maximized # -------------------------------------------------------------------------- # Output : simplified # -------------------------------------------------------------------------- def optimalSimplification(track, cost, eps, mode=MODE_SEGMENTATION_MINIMIZE): """ Function to simplify a GPS track with dynamic programming. """ simplified = Track(user_id=track.uid, track_id=track.tid, base=track.base) segmentation = optimalSegmentation(track, cost, eps) for i in range(len(segmentation)): simplified.addObs(track.getObs(segmentation[i]).copy()) return simplified # -------------------------------------------------------------------------- # A list of cost functions for free segmentation # -------------------------------------------------------------------------- # ------------------------------------------------------------ # Constraint on largest deviation: # Width of MBR + offset penalty (used with MINIMIZATION) # ------------------------------------------------------------ def __cost_largest_deviation(track, i, j, offset): """TODO""" if j <= i + 1: return offset else: R = Geometrics.boundingShape(track.extract(i, j), Geometrics.MODE_ENCLOSING_MBR) return min(R[2], R[3]) + offset # ------------------------------------------------------------ # Constraint on track elongation # l/L ratio of MBR + offset penalty (used with MINIMIZATION) # ------------------------------------------------------------ def __cost_mbr_ratio(track, i, j, offset): """TODO""" if j <= i + 1: return offset else: R = Geometrics.boundingShape(track.extract(i, j), Geometrics.MODE_ENCLOSING_MBR) l = min(R[2], R[3]) L = max(R[2], R[3]) return l / L + offset # ------------------------------------------------------------ # Strinct constraint on largest deviation # Width should be lower than offset + unit penalty (used with MINIMIZATION) # ------------------------------------------------------------ def __cost_largest_deviation_strict(track, i, j, offset): """TODO""" if j <= i + 1: return offset else: R = Geometrics.boundingShape(track.extract(i, j), Geometrics.MODE_ENCLOSING_MBR) l = min(R[2], R[3]) return 1e300 * (l > offset) + 1 def squaring (track, eps): ''' Function to simplify a GPS track with squaring algorithm. Parameters ---------- :param track Track: GPS track :param eps float: angle threshold on right and flat angles (radians) :return Track: simplified track ''' N = len(track) CR = [] for i in range(0, N-1): p0 = track[(i-1)%N].position p1 = track[i].position p2 = track[(i+1)%N].position du = p0.distanceTo(p1) dv = p1.distanceTo(p2) x0 = p0.getX(); x1 = p1.getX(); x2 = p2.getX() y0 = p0.getY(); y1 = p1.getY(); y2 = p2.getY() ux = x1-x0; uy = y1-y0 vx = x2-x1; vy = y2-y1 if du*dv != 0: arg = max(min((ux*vx + uy*vy)/(du*dv), 1), -1) angle = math.acos(arg) #if abs(angle) < eps: #p1.plot('go') #print(i, "FLAT") if abs(angle-math.pi/2) < eps: #p1.plot('ro') CR.append(i) else: # on a que 2 points print("Warning: identical points") X = [v for pair in zip(track.getX(), track.getY()) for v in pair] for iter in range(5): J = SQUARING_RECALL*np.eye(2*N) B = [v for pair in zip(track.getX(), track.getY()) for v in pair] B = [SQUARING_RECALL*(B[j] - X[j]) for j in range(len(X))] for idx in CR: x0 = X[2*(idx-1)]; x1 = X[2*idx]; x2 = X[2*(idx+1)] y0 = X[2*(idx-1)+1]; y1 = X[2*idx+1]; y2 = X[2*(idx+1)+1] constraint = [0]*(2*N) constraint[2*idx-2] = -(x2-x1) constraint[2*idx-1] = -(y2-y1) constraint[2*idx-0] = (x2-2*x1+x0) constraint[2*idx+1] = (y2-2*y1+y0) constraint[2*idx+2] = (x1-x0) constraint[2*idx+3] = (y1-y0) J = np.vstack([J, constraint]) B.append(-((x1-x0)*(x2-x1) + (y1-y0)*(y2-y1))) B = np.array(B) dX = np.linalg.solve(J.transpose()@J, J.transpose()@B) #print(dX) for i in range(len(X)): X[i] += dX[i] output = track.copy() for i in range(len(output)): output[i].position.setX(X[2*i]) output[i].position.setY(X[2*i+1]) return output ''' for iter in range(5): print("ITER ", iter) J = np.eye(2*N) B = [v for pair in zip(track.getX(), track.getY()) for v in pair] B = [B[j] - X[j] for j in range(len(X))] for i in range(0,N-1): p0 = track[(i-1)%N].position p1 = track[i].position p2 = track[(i+1)%N].position du = p0.distanceTo(p1) dv = p1.distanceTo(p2) x0 = p0.getX(); x1 = p1.getX(); x2 = p2.getX() y0 = p0.getY(); y1 = p1.getY(); y2 = p2.getY() ux = x1-x0; uy = y1-y0 vx = x2-x1; vy = y2-y1 angle = math.acos((ux*vx + uy*vy)/(du*dv)) #if abs(angle) < eps: #p1.plot('go') #print(i, "FLAT") if abs(angle-math.pi/2) < eps: p1.plot('ro') constraint = [0]*(2*N) constraint[2*i-2] = -(x2-x1) constraint[2*i-1] = -(y2-y1) constraint[2*i-0] = (x2-2*x1+x0) constraint[2*i+1] = (y2-2*y1+y0) constraint[2*i+2] = (x1-x0) constraint[2*i+3] = (y1-y0) J = np.vstack([J, constraint]) B.append(-((x1-x0)*(x2-x1) + (y1-y0)*(y2-y1))) #print(i, "RIGHT") B = np.array(B) dX = np.linalg.solve(J.transpose()@J, J.transpose()@B) print(B) for i in range(len(X)): X[i] += dX[i] output = track.copy() for i in range(len(output)): output[i].position.setX(X[2*i]) output[i].position.setY(X[2*i+1]) #output.loop() '''