|
| 1 | +""" |
| 2 | +
|
| 3 | +2D (x, y, yaw) pose optimization SLAM |
| 4 | +
|
| 5 | +author: Atsushi Sakai |
| 6 | +
|
| 7 | +Ref: |
| 8 | +- [A Compact and Portable Implementation of Graph\-based SLAM](https://www.researchgate.net/publication/321287640_A_Compact_and_Portable_Implementation_of_Graph-based_SLAM) |
| 9 | +
|
| 10 | +- [GitHub \- furo\-org/p2o: Single header 2D/3D graph\-based SLAM library](https://github.com/furo-org/p2o) |
| 11 | +
|
| 12 | +""" |
| 13 | + |
| 14 | +import sys |
| 15 | +import time |
| 16 | +import math |
| 17 | +import numpy as np |
| 18 | +import matplotlib.pyplot as plt |
| 19 | +from scipy import sparse |
| 20 | +from scipy.sparse import linalg |
| 21 | + |
| 22 | + |
| 23 | +class Optimizer2D: |
| 24 | + |
| 25 | + def __init__(self): |
| 26 | + self.verbose = False |
| 27 | + self.animation = False |
| 28 | + self.p_lambda = 0.0 |
| 29 | + self.init_w = 1e10 |
| 30 | + self.stop_thre = 1e-3 |
| 31 | + self.dim = 3 # state dimension |
| 32 | + |
| 33 | + def optimize_path(self, nodes, consts, max_iter, min_iter): |
| 34 | + |
| 35 | + graph_nodes = nodes[:] |
| 36 | + prev_cost = sys.float_info.max |
| 37 | + |
| 38 | + for i in range(max_iter): |
| 39 | + start = time.time() |
| 40 | + cost, graph_nodes = self.optimize_path_one_step( |
| 41 | + graph_nodes, consts) |
| 42 | + elapsed = time.time() - start |
| 43 | + if self.verbose: |
| 44 | + print("step ", i, " cost: ", cost, " time:", elapsed, "s") |
| 45 | + |
| 46 | + # check convergence |
| 47 | + if (i > min_iter) and (prev_cost - cost < self.stop_thre): |
| 48 | + if self.verbose: |
| 49 | + print("converged:", prev_cost |
| 50 | + - cost, " < ", self.stop_thre) |
| 51 | + break |
| 52 | + prev_cost = cost |
| 53 | + |
| 54 | + if self.animation: |
| 55 | + plt.cla() |
| 56 | + plot_nodes(nodes, color="-b") |
| 57 | + plot_nodes(graph_nodes) |
| 58 | + plt.axis("equal") |
| 59 | + plt.pause(1.0) |
| 60 | + |
| 61 | + return graph_nodes |
| 62 | + |
| 63 | + def optimize_path_one_step(self, graph_nodes, constraints): |
| 64 | + |
| 65 | + indlist = [i for i in range(self.dim)] |
| 66 | + numnodes = len(graph_nodes) |
| 67 | + bf = np.zeros(numnodes * self.dim) |
| 68 | + tripletList = TripletList() |
| 69 | + |
| 70 | + for con in constraints: |
| 71 | + ida = con.id1 |
| 72 | + idb = con.id2 |
| 73 | + assert 0 <= ida and ida < numnodes, "ida is invalid" |
| 74 | + assert 0 <= idb and idb < numnodes, "idb is invalid" |
| 75 | + r, Ja, Jb = self.calc_error( |
| 76 | + graph_nodes[ida], graph_nodes[idb], con.t) |
| 77 | + |
| 78 | + trJaInfo = Ja.transpose() @ con.info_mat |
| 79 | + trJaInfoJa = trJaInfo @ Ja |
| 80 | + trJbInfo = Jb.transpose() @ con.info_mat |
| 81 | + trJbInfoJb = trJbInfo @ Jb |
| 82 | + trJaInfoJb = trJaInfo @ Jb |
| 83 | + |
| 84 | + for k in indlist: |
| 85 | + for m in indlist: |
| 86 | + tripletList.push_back( |
| 87 | + ida * self.dim + k, ida * self.dim + m, trJaInfoJa[k, m]) |
| 88 | + tripletList.push_back( |
| 89 | + idb * self.dim + k, idb * self.dim + m, trJbInfoJb[k, m]) |
| 90 | + tripletList.push_back( |
| 91 | + ida * self.dim + k, idb * self.dim + m, trJaInfoJb[k, m]) |
| 92 | + tripletList.push_back( |
| 93 | + idb * self.dim + k, ida * self.dim + m, trJaInfoJb[m, k]) |
| 94 | + |
| 95 | + bf[ida * self.dim: ida * self.dim + 3] += trJaInfo @ r |
| 96 | + bf[idb * self.dim: idb * self.dim + 3] += trJbInfo @ r |
| 97 | + |
| 98 | + for k in indlist: |
| 99 | + tripletList.push_back(k, k, self.init_w) |
| 100 | + |
| 101 | + for i in range(self.dim * numnodes): |
| 102 | + tripletList.push_back(i, i, self.p_lambda) |
| 103 | + |
| 104 | + mat = sparse.coo_matrix((tripletList.data, (tripletList.row, tripletList.col)), |
| 105 | + shape=(numnodes * self.dim, numnodes * self.dim)) |
| 106 | + |
| 107 | + x = linalg.spsolve(mat.tocsr(), -bf) |
| 108 | + |
| 109 | + out_nodes = [] |
| 110 | + for i in range(len(graph_nodes)): |
| 111 | + u_i = i * self.dim |
| 112 | + pos = Pose2D( |
| 113 | + graph_nodes[i].x + x[u_i], |
| 114 | + graph_nodes[i].y + x[u_i + 1], |
| 115 | + graph_nodes[i].theta + x[u_i + 2] |
| 116 | + ) |
| 117 | + out_nodes.append(pos) |
| 118 | + |
| 119 | + cost = self.calc_global_cost(out_nodes, constraints) |
| 120 | + |
| 121 | + return cost, out_nodes |
| 122 | + |
| 123 | + def calc_global_cost(self, nodes, constraints): |
| 124 | + cost = 0.0 |
| 125 | + for c in constraints: |
| 126 | + diff = self.error_func(nodes[c.id1], nodes[c.id2], c.t) |
| 127 | + cost += diff.transpose() @ c.info_mat @ diff |
| 128 | + |
| 129 | + return cost |
| 130 | + |
| 131 | + def error_func(self, pa, pb, t): |
| 132 | + ba = self.calc_constraint_pose(pb, pa) |
| 133 | + error = np.array([ba.x - t.x, |
| 134 | + ba.y - t.y, |
| 135 | + self.pi2pi(ba.theta - t.theta)]) |
| 136 | + return error |
| 137 | + |
| 138 | + def calc_constraint_pose(self, l, r): |
| 139 | + diff = np.array([l.x - r.x, l.y - r.y, l.theta - r.theta]) |
| 140 | + v = self.rot_mat_2d(-r.theta) @ diff |
| 141 | + v[2] = self.pi2pi(l.theta - r.theta) |
| 142 | + return Pose2D(v[0], v[1], v[2]) |
| 143 | + |
| 144 | + def rot_mat_2d(self, theta): |
| 145 | + return np.array([[math.cos(theta), -math.sin(theta), 0.0], |
| 146 | + [math.sin(theta), math.cos(theta), 0.0], |
| 147 | + [0.0, 0.0, 1.0] |
| 148 | + ]) |
| 149 | + |
| 150 | + def calc_error(self, pa, pb, t): |
| 151 | + e0 = self.error_func(pa, pb, t) |
| 152 | + dx = pb.x - pa.x |
| 153 | + dy = pb.y - pa.y |
| 154 | + dxdt = -math.sin(pa.theta) * dx + math.cos(pa.theta) * dy |
| 155 | + dydt = -math.cos(pa.theta) * dx - math.sin(pa.theta) * dy |
| 156 | + Ja = np.array([[-math.cos(pa.theta), -math.sin(pa.theta), dxdt], |
| 157 | + [math.sin(pa.theta), -math.cos(pa.theta), dydt], |
| 158 | + [0.0, 0.0, -1.0] |
| 159 | + ]) |
| 160 | + Jb = np.array([[math.cos(pa.theta), math.sin(pa.theta), 0.0], |
| 161 | + [-math.sin(pa.theta), math.cos(pa.theta), 0.0], |
| 162 | + [0.0, 0.0, 1.0] |
| 163 | + ]) |
| 164 | + return e0, Ja, Jb |
| 165 | + |
| 166 | + def pi2pi(self, rad): |
| 167 | + |
| 168 | + val = math.fmod(rad, 2.0 * math.pi) |
| 169 | + if val > math.pi: |
| 170 | + val -= 2.0 * math.pi |
| 171 | + elif val < -math.pi: |
| 172 | + val += 2.0 * math.pi |
| 173 | + |
| 174 | + return val |
| 175 | + |
| 176 | + |
| 177 | +class TripletList: |
| 178 | + |
| 179 | + def __init__(self): |
| 180 | + self.row = [] |
| 181 | + self.col = [] |
| 182 | + self.data = [] |
| 183 | + |
| 184 | + def push_back(self, irow, icol, idata): |
| 185 | + self.row.append(irow) |
| 186 | + self.col.append(icol) |
| 187 | + self.data.append(idata) |
| 188 | + |
| 189 | + |
| 190 | +class Pose2D: |
| 191 | + |
| 192 | + def __init__(self, x, y, theta): |
| 193 | + self.x = x |
| 194 | + self.y = y |
| 195 | + self.theta = theta |
| 196 | + |
| 197 | + |
| 198 | +class Constrant2D: |
| 199 | + |
| 200 | + def __init__(self, id1, id2, t, info_mat): |
| 201 | + self.id1 = id1 |
| 202 | + self.id2 = id2 |
| 203 | + self.t = t |
| 204 | + self.info_mat = info_mat |
| 205 | + |
| 206 | + |
| 207 | +def plot_nodes(nodes, color ="-r", label = ""): |
| 208 | + x, y = [], [] |
| 209 | + for n in nodes: |
| 210 | + x.append(n.x) |
| 211 | + y.append(n.y) |
| 212 | + plt.plot(x, y, color, label=label) |
| 213 | + |
| 214 | + |
| 215 | +def load_data(fname): |
| 216 | + nodes, consts = [], [] |
| 217 | + |
| 218 | + for line in open(fname): |
| 219 | + sline = line.split() |
| 220 | + tag = sline[0] |
| 221 | + |
| 222 | + if tag == "VERTEX_SE2": |
| 223 | + data_id = int(sline[1]) |
| 224 | + x = float(sline[2]) |
| 225 | + y = float(sline[3]) |
| 226 | + theta = float(sline[4]) |
| 227 | + nodes.append(Pose2D(x, y, theta)) |
| 228 | + elif tag == "EDGE_SE2": |
| 229 | + id1 = int(sline[1]) |
| 230 | + id2 = int(sline[2]) |
| 231 | + x = float(sline[3]) |
| 232 | + y = float(sline[4]) |
| 233 | + th = float(sline[5]) |
| 234 | + c1 = float(sline[6]) |
| 235 | + c2 = float(sline[7]) |
| 236 | + c3 = float(sline[8]) |
| 237 | + c4 = float(sline[9]) |
| 238 | + c5 = float(sline[10]) |
| 239 | + c6 = float(sline[11]) |
| 240 | + t = Pose2D(x, y, th) |
| 241 | + info_mat = np.array([[c1, c2, c3], |
| 242 | + [c2, c4, c5], |
| 243 | + [c3, c5, c6] |
| 244 | + ]) |
| 245 | + consts.append(Constrant2D(id1, id2, t, info_mat)) |
| 246 | + |
| 247 | + print("n_nodes:", len(nodes)) |
| 248 | + print("n_consts:", len(consts)) |
| 249 | + |
| 250 | + return nodes, consts |
| 251 | + |
| 252 | + |
| 253 | +def main(): |
| 254 | + print("start!!") |
| 255 | + |
| 256 | + fnames = ["intel.g2o", |
| 257 | + "manhattan3500.g2o", |
| 258 | + "mit_killian.g2o" |
| 259 | + ] |
| 260 | + |
| 261 | + max_iter = 20 |
| 262 | + min_iter = 3 |
| 263 | + |
| 264 | + # parameter setting |
| 265 | + optimizer = Optimizer2D() |
| 266 | + optimizer.p_lambda = 1e-6 |
| 267 | + optimizer.verbose = True |
| 268 | + optimizer.animation = True |
| 269 | + |
| 270 | + for f in fnames: |
| 271 | + nodes, consts = load_data(f) |
| 272 | + |
| 273 | + start = time.time() |
| 274 | + final_nodes = optimizer.optimize_path(nodes, consts, max_iter, min_iter) |
| 275 | + print("elapsed_time", time.time() - start, "sec") |
| 276 | + |
| 277 | + # plotting |
| 278 | + plt.cla() |
| 279 | + plot_nodes(nodes, color="-b", label="before") |
| 280 | + plot_nodes(final_nodes, label="after") |
| 281 | + plt.axis("equal") |
| 282 | + plt.grid(True) |
| 283 | + plt.legend() |
| 284 | + plt.pause(3.0) |
| 285 | + |
| 286 | + print("done!!") |
| 287 | + |
| 288 | + |
| 289 | +if __name__ == '__main__': |
| 290 | + main() |
0 commit comments