src/indoor_positioning/positioning_plus.py
Go to the documentation of this file.
1 #!/usr/bin/env python
2 """
3 Use this class to perform indoor positioning with the metraTec IPS+ system. The position of a receiver is evaluated by
4 UWB ranging of RF beacons placed in the environment. Requires a YAML file with the configuration of beacon positions.
5 
6 Usage:
7 Initialize the class by passing the directory of the config file. Then collect a number of beacon responses to ranging
8 requests (SRG) and use the parse_srg() function to get a list of beacon objects and their respective ranges from a list
9 of SRG responses.
10 Use this list as input for the trilaterate() function to estimate the position of the receiver in 3D space.
11 
12 A few examples can be found in the main function below.
13 """
14 
15 import math
16 import operator
17 from scipy.optimize import minimize
18 from shapely.geometry import Point
19 from shapely.geometry.polygon import Polygon
20 from positioning import Positioning
21 
22 
23 class PositioningPlus(Positioning):
24  """Class to extend the functionality of the Positioning class with UWB ranging capabilities."""
25 
26  # options for minimization function used for trilateration
27  minimization_params = dict(method='TNC', # L-BFGS-B, TNC or SLSQP
28  options={'disp': True})
29 
30  def __init__(self, config_dir, min_beacons=4, max_z=None, dilation=0.0):
31  """
32  Initialize the class with zone and beacon definitions from YAML configuration file.
33  :param config_dir: String: Directory of the YAML config file
34  :param min_beacons: Int: Minimum number of beacons to use for trilateration
35  :param max_z: Float: Maximum z position the receiver can have after ranging
36  """
37  # initialize the positioning class that this class inherits
38  Positioning.__init__(self, config_dir)
39 
40  # minimum number of beacons (and ranges) to use for position estimation
41  self.min_beacons = min_beacons
42 
43  # store EIDs of configured beacons for easy access
44  self.eids = self.get_defined_beacons()
45 
46  # maximum z value the position estimate can have (used as initialization and upper bound for trilateration)
47  self.max_z = max_z
48 
49  # dilation for the polygon check
50  self.dilation = dilation
51 
52  def get_top_beacons(self, pings, n):
53  """
54  Get the top n beacons in terms of RSSI values from a list of beacon pings.
55  :param pings: [String]: list of beacon pings
56  :param n: Int: Number of top beacons to return
57  :return: [Beacon]: list of beacon objects ordered according to RSSI values
58  """
59  means = self.get_mean(pings)
60  ret = []
61  # sort mean dictionary by values and return n beacons with the highest signal strength that are configured in
62  # the config-file and meet the threshold criteria
63  for key, value in sorted(means.items(), key=lambda (k, v): v, reverse=True):
64  # look for key (EID) in every zone and return when match is found
65  for z in self.zones: # iterate over zones
66  # continue to next zone if mean RSSI value is lower than the configured threshold
67  if value < z.threshold:
68  continue
69  for b in z.beacons: # iterate over beacons inside the zone
70  if b.eid == key: # if the EID of the beacon is the same as the current key (EID) return the zone
71  ret.append(b)
72  if len(ret) >= n: # TODO continue with next key-value-pair when was found
73  return ret
74  return ret
75 
76  def get_range(self, responses):
77  # get range from a single SRG response if message is valid
78  pass
79 
80  def parse_srg(self, responses):
81  """
82  Parse a list of SRG ranging responses of UWB beacons. For each message (if it is valid) return the positions,
83  frame id and estimated ranges.
84  :param responses: [String]: list of SRG responses from UWB beacons
85  :return: [(Beacon, Float)]: list beacon-object and range pairs
86  """
87  ranges = []
88  # iterate through each response and fill output lists if ranging messages are valid
89  for r in responses:
90  # split single message on spaces
91  split = r.split(' ')
92  # skip this message if it is invalid
93  if len(split) != 6:
94  print('Encountered invalid SRG response: {}'.format(r))
95  continue
96  # get beacon object from EID
97  beacon = self.get_beacon(split[1])
98  # calculate range
99  range = float(split[2]) / 1000.
100  # append to output
101  ranges.append((beacon, range))
102  return ranges
103 
104  def in_range(self, pings, threshold=None):
105  """
106  Get a list of beacons that are in range and are configured in the configuration YAML file from a list of
107  beacon pings recorded over a short duration. The returned beacons (EIDs) can be used for UWB ranging
108  :param pings: [String]: List of BCN pings
109  :param threshold: Int: Minimum RSSI value a beacon should have to be returned.
110  :return: [String]: List of EIDs that can be used for ranging
111  """
112  eids = []
113  # get unique list of beacon EIDs from list of pings and their average RSSI value
114  mean = self.get_mean(pings)
115  # sort mean dictionary according to RSSI values
116  srtd = sorted(mean.items(), key=operator.itemgetter(1), reverse=True)
117  for s in srtd:
118  # Don't append this beacon to list if the RSSI value is too low
119  if threshold is not None and s[1] < threshold:
120  break
121  # check if beacon is configured in yml file
122  if s[0] in self.eids:
123  eids.append(s[0])
124  return eids
125 
126  def trilaterate(self, ranges):
127  """
128  Estimate the position of the receiver by using trilateration with at least three UWB responses
129  :param ranges: [([Float, Float, Float], Float)]: list of position and range pairs (obtained from parse_srg) or
130  [(Beacon, Float)]: list of beacon object and range pairs (output from parse_srg())
131  :return: [Float, Float, Float]: 3D coordinates of estimated receiver location
132  """
133  # check whether enough points are given
134  if len(ranges) < self.min_beacons:
135  print('Not enough beacons for position estimation ({}/{})'.format(len(ranges), self.min_beacons))
136  return False, None
137  print('Using {} beacons for trilateration.'.format(len(ranges)))
138  # get points and distances from input and compute sum of distances
139  points, poly_points, distances, dsum = [], [], [], 0
140  for r in ranges:
141  # get position of beacon directly from input list or alternatively from Beacon object
142  p = r[0].position if r[0].__class__.__name__ == 'Beacon' else r[0]
143  points.append(p)
144  poly_points.append((p[0], p[1]))
145  distances.append(r[1])
146  dsum += r[1]
147  # get weighted centroid
148  initial = [0, 0, 0]
149  for p, d in zip(points, distances):
150  weight = 1. - d / dsum
151  initial = [initial[0] + p[0] * weight, initial[1] + p[1] * weight, initial[2] + p[2] * weight]
152  initial = [initial[0] / len(ranges), initial[1] / len(ranges), initial[2] / len(ranges)]
153  if self.max_z is not None and initial[2] > self.max_z:
154  initial[2] = self.max_z
155  # minimize root mean square error to get estimated position
156  print('guess: ', initial)
157  # impose upper bound on z coordinate
158  bnds = ((None, None), (None, None), (None, self.max_z)) if self.max_z is not None else None
159  res = minimize(self.mse, initial, args=(points, distances), bounds=bnds, **self.minimization_params)
160  print('final: ', res.x)
161  # check if result is within bounds
162  poly = Polygon(poly_points)
163  return poly.convex_hull.buffer(self.dilation).contains(Point(res.x[0], res.x[1])), res.x
164 
165  @staticmethod
166  def mse(x, points, distances):
167  """
168  Cost-function to use for position estimation. Minimize the squared error of the distance between a variable
169  point x and each beacon and the measured UWB distance. mse = SUM(distance - dist(x, points)^2
170  :param x: [Float, Float, Float]: vector with variable components to be optimized
171  :param points: [[x, y, z], ...]: list of positions of the beacons used for UWB ranging
172  :param distances: [Float]: estimated distances from UWB ranging of above beacons
173  :return: Float: mean squared error for all beacon positions
174  """
175  mse = 0.
176  for p, d in zip(points, distances):
177  d_calc = math.sqrt((x[0]-p[0])**2 + (x[1]-p[1])**2 + (x[2]-p[2])**2)
178  mse += (d_calc-d)**2
179  return mse # / len(points)
180 
181 
182 if __name__ == '__main__':
183  """Testing the class and its methods."""
184  # initialize class
185  pos = PositioningPlus('/home/metratec/catkin_ws/src/indoor_positioning/config/zones.yml')
186  # create a list of pings that would usually be collected from the receiver and stored in a buffer
187  dummy_pings = ['BCN 00124B00090593E6 -090', 'BCN 00124B00090593E6 -090', 'BCN 00124B00090593E6 -090',
188  'BCN 00124B00050CD41E -090', 'BCN 00124B00050CD41E -070', 'BCN 00124B00050CD41E -050',
189  'BCN 00124B00050CDC0A -070', 'BCN 00124B00050CDC0A -060', 'BCN 00124B00050CDC0A -090']
190  # get the beacon object with a specified EID
191  # Note: the above beacons have to be defined in the config file for this to work
192  beacon = pos.get_beacon('00124B00090593E6')
193  # get the current zone the receiver is in based on the passed list of pings
194  zone = pos.get_zone(dummy_pings)
195 
196  # create a list of UWB ranging responses that would usually be collected from a receiver and stored in a buffer
197  resp = ['SRG 00124B00050CDA71 01650 -076 -081 047\r', 'SRG 00124B00090593E6 04300 -076 -080 105\r',
198  'SRG 00124B00050CD41E 00800 -079 -082 049\r']
199  # get beacons in range for ranging
200  in_range = pos.in_range(dummy_pings)
201  # get beacon objects and respective ranges from a list of UWB ranging responses
202  ranges = pos.parse_srg(in_range)
203  # use trilateration (with at least 3 points) to estimate the receiver location from beacon positions and ranges
204  tril = pos.trilaterate(ranges)
205  # break here to investigate variables etc.
206  breakpoint = 0
def __init__(self, config_dir, min_beacons=4, max_z=None, dilation=0.0)


indoor_positioning
Author(s): Christian Arndt
autogenerated on Mon Jun 10 2019 13:33:13