SLAM Engineer

๐ŸŒˆ [SymForce Tutorial 3ํŽธ] Pose-graph Optimization ์‹ค์Šต (Toy Example)


Pose-graph optimization ์ด๋ž€?

  • ๊ณ„์‚ฐ ํšจ์œจ์„ฑ์„ ๋†’์ด๊ธฐ ์œ„ํ•ด Landmark ๋ฅผ state vector ์—์„œ ์ œ์™ธํ•˜๋Š” ์‹œ๋„๋“ค์ด 2000๋…„๋Œ€ ํ›„๋ฐ˜ SLAM ํ•™๊ณ„์—์„œ ์ด์–ด์กŒ๋‹ค.
  • ๊ทธ๋ฆฌ๊ณ  3D ์ •๋ณด๋ฅผ ์ง์ ‘ ์ •๊ตํ•˜๊ฒŒ ์ธก์ •ํ•˜๋Š” LiDAR sensor ๊ฐ€ ๋Œ€์ค‘ํ™”๋˜๋ฉฐ, ambiguous landmark๋ฅผ ๊ตณ์ด ์ตœ์ ํ™”ํ•  ํ•„์š” ์—†์ด, ๋กœ๋ด‡์˜ pose๋งŒ์„ ์ตœ์ ํ™” ํ•˜๋Š” ๊ฒƒ์ด ๋”์šฑ ๋Œ€์ค‘ํ™”๋œ ๋“ฏํ•˜๋‹ค.
  • SymForce ๋ฅผ ์ด์šฉํ•ด์„œ Pose-graph Optimization์„ ๊ตฌํ˜„ํ•ด๋ณด์ž.

Why SymForce for PGO?

  • ์‚ฌ์‹ค Pose-graph optimization ์— ๋Œ€ํ•ด์„œ๋Š” GTSAM์—์„œ ์ด๋ฏธ ๊ทธ ๊ธฐ๋Šฅ์„ (API๋„ ์“ฐ๊ธฐ ์‰ฝ๊ฒŒ) ๋„ˆ๋ฌด ์ž˜ ์ œ๊ณตํ•˜๊ณ  ์žˆ๊ธฐ ๋•Œ๋ฌธ์— ์ƒˆ๋กœ์šด library๊ฐ€ ๊ตณ์ด ํ•„์š”ํ•ด? ๋ผ๋Š” ์ƒ๊ฐ์ด ๋“ค๊ธฐ๋„ ํ•œ๋‹ค.
    • LIO-SAM์ด ๊ทธ์ค‘ ๊ฐ€์žฅ ์œ ๋ช…ํ•œ GTSAM์„ PGO์— ์‚ฌ์šฉํ•œ ์˜ˆ์‹œ์ด๋‹ค (๊ฑฐ์˜ star๋„ ์ด์ œ 2์ฒœ๊ฐœ๊ฐ€ ๋‹ค ๋˜์–ด ๊ฐ„๋‹ค).
      • https://github.com/TixiaoShan/LIO-SAM ์—์„œ mapOptmization.cpp ์„ ๋ณด๋ฉด๋œ๋‹ค.
    • ํ•˜์ง€๋งŒ ๋ณดํ†ต ์ด๋ ‡๊ฒŒ ๋˜๋ฉด ์—”์ง€๋‹ˆ์–ด๊ฐ€ ํ• ์ผ์€ ๋งค์šฐ ์ถ”์ƒํ™”๋˜๊ฒŒ ๋œ๋‹ค.
      1. isam = new ISAM2(parameters);๋ฅผ ์„ ์–ธํ•˜๊ณ ,
      2. gtSAMgraph.add(some factor) ํ•˜๊ณ ,
      3. isam->update(gtSAMgraph, initialEstimate); ํ•˜๋Š” ์‹์ด๋‹ค ..
    • ๊ทธ๋ž˜์„œ ๋‚ด๋ถ€์ ์œผ๋กœ ์–ด๋–ป๊ฒŒ ์‹ค์ œ๋กœ ์ตœ์ ํ™”๊ฐ€ ์ด๋ฃจ์–ด์ง€๋Š”์ง€ ์–ด์ง€๊ฐ„ํ•œ ์—ด์ •์ด ์—†์œผ๋ฉด ๋ชจ๋ฅด๊ณ  ์“ฐ๊ฒŒ ๋œ๋‹ค.
      • ๋‚ด ์–˜๊ธฐ๋‹ค ..
    • GTSAM์€ C++์—์„œ custom loss ๋ฅผ ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ์ง€๋งŒ Ceres์— ๋น„ํ•ด ๊ณต๊ฐœ๋œ ๊ตฌํ˜„์ฒด๋‚˜ docs ๊ฐ€ ์ ์€ ๋Š๋‚Œ์ด๋‹ค. ๊ทธ๋ž˜์„œ ๊ธฐ์กด built-in cost ๋“ค์˜ ๊ตฌํ˜„์„ ํ†บ์•„๋ณด๋ฉฐ ๋”ฐ๋ผ ์ž‘์„ฑํ•ด์•ผ ํ–ˆ๊ณ  .. ๊ทธ๋ฆฌ๊ณ  ์™ ์ง€ ๋ญ”๊ฐ€ ๋‚ด๋ถ€๊ตฌ์กฐ๋ฅผ ์ข€ ์•Œ๊ธฐ๊ฐ€ ์–ด๋ ต๋‹ค๋Š” ์ธ์ƒ์ด ์žˆ์—ˆ๋‹ค. ๋‚ด๋ถ€์ ์œผ๋กœ๋Š” traits ๊ณผ Template ์œผ๋กœ ๋ณต์žกํ•˜๊ฒŒ ์งœ์—ฌ์ ธ์žˆ๋Š”๋ฐ .. ํŒŒ์•…ํ•˜๊ธฐ๊ฐ€ ์ข€ ๋ณต์žกํ•˜๋‹ค.
    • ๋ฐ˜๋ฉด SymForce๋Š” Jacobian ์„ Symbolic ํ•˜๊ฒŒ ๊ณ„์‚ฐํ•ด์„œ ๋ณด์—ฌ์ฃผ๋ฏ€๋กœ ๋ช…ํ™•ํ•œ ์ธ์ƒ์ด ์žˆ๋‹ค..
      • ์•ž์˜ ๋ธ”๋กœ๊ทธ ํŠœํ† ๋ฆฌ์–ผ ์—์„œ Symforce ๊ฐ€ ๊ตฌํ•ด์ค€ Jacobain์„ ๊ฐ€์ ธ๋‹ค ์‚ฌ์šฉํ•ด์„œ ์ง์ ‘ Gauss-Newton iteration ์„ ๊ตฌ์„ฑํ•ด๋ณผ ์ˆ˜ ์žˆ์—ˆ๋‹ค.
      • ๋ฌด์—‡๋ณด๋‹ค python ์—์„œ๋„ custom loss ๋ฅผ ์‰ฝ๊ฒŒ ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ์–ด์„œ ์ด๊ฒƒ์ €๊ฒƒ ํ•ด๋ณด๊ธฐ๊ฐ€ ์ข‹์€๋“ฏ ํ•˜๋‹ค.
    • ps. ํ•œํŽธ, GTSAM ์„ ์‚ฌ์šฉํ•˜๋Š” ๊ทธ๋ฃน์ด ๋ช‡ ์žˆ์—ˆ๊ณ , ๊ทธ ์™ธ์—๋Š” ๋Œ€์ฒด๋กœ Ceres ๋ฅผ ๋งŽ์ด๋“ค ์‚ฌ์šฉํ•˜๋Š” ๋“ฏํ•˜๋‹ค. Ceres ๋กœ PGO๋ฅผ ๊ตฌํ˜„ํ•œ ๊ฐ€์žฅ ์œ ๋ช…ํ•œ ์‚ฌ๋ก€๋Š” ์•„๋งˆ VINS-mono์ผ ๊ฒƒ์ด๋‹ค.
      • https://github.com/HKUST-Aerial-Robotics/VINS-Mono/tree/master/pose_graph ์—ฌ๊ธฐ๋ฅผ ๋ณด๋ฉด๋œ๋‹ค.
  • ๊ทธ๋ž˜์„œ SymForce tutorial ์ฐจ์›์—์„œ ์ด๊ฒƒ์ €๊ฒƒ ์ž‘์„ฑํ•ด๋ณด๊ณ  ์žˆ๋‹ค.
    • Iterative optimizatino์„ From scratch ๋กœ ๊ตฌํ˜„ํ•˜๋Š” ๊ฒƒ์€ ์ง€๋‚œ ํฌ์ŠคํŒ…์—์„œ ํ•ด๋ณด์•˜๊ณ , ์•ž์˜ ๋ธ”๋กœ๊ทธ ํŠœํ† ๋ฆฌ์–ผ์˜ todo ์—์„œ ์–˜๊ธฐํ–ˆ๋“ฏ SymForce ๊ฐ€ ์ œ๊ณตํ•˜๋Š” optimizer class ๋ฅผ ์‚ฌ์šฉํ•ด๋ณด์ž.
    • ๊ด€๋ จ ์˜ˆ์‹œ๋Š” landmark ๋ฅผ bearing measurement ๋กœ ์ธก์ •ํ•ด์„œ global localization ์„ ์ˆ˜ํ–‰ํ•˜๋Š” ๊ฒƒ์ด ๊ณต์‹readme ์— ์žˆ๋‹ค.
    • PGO๊ฐ€ ๋”ฑ ์žˆ์ง€๋Š” ์•Š์€๋ฐ ์–ด์งœํ”ผ ๊ตฌ์กฐ๋Š” ๋น„์Šทํ•˜๋‹ˆ๊นŒ ์ง์ ‘ ์ž‘์„ฑํ•ด๋ณด์ž.

SymForce๊ธฐ๋ฐ˜์˜ PGO ๊ตฌํ˜„

Cost functions

  • PGO์—์„œ ์‚ฌ์šฉํ•˜๋Š” cost function ์€ (์ตœ์†Œํ•œ์œผ๋กœ) prior ์™€ odometry, loop closure ๊ฐ€ ์žˆ๋‹ค.
  • SymForce ๋กœ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์ด ๊ตฌํ˜„๋œ๋‹ค.
      eps = 0.000001
    
      def prior_residual(
              pose    : sf.Pose3, 
              pose_prior: sf.Pose3,
              diagonal_sigmas: sf.V6 
          ) -> sf.V6:
    
          tangent_error = pose.local_coordinates(pose_prior, epsilon=eps)
          return sfT.cast(sf.V6, sf.M.diag(diagonal_sigmas.to_flat_list()).inv() * sf.V6(tangent_error))
    
      def odometry_residual(
              pose_prev: sf.Pose3, 
              pose_next: sf.Pose3, 
              movement : sf.Pose3,
              diagonal_sigmas: sf.V6,
          ) -> sf.V6:
            
          # The original reference of the below lines 
          #  - see https://github.com/symforce-org/symforce/blob/main/symforce/examples/robot_3d_localization/robot_3d_localization.py#L63
          movement_predicted = pose_prev.inverse() * pose_next
          tangent_error = movement_predicted.local_coordinates(movement, epsilon=eps)
          return sfT.cast(sf.V6, sf.M.diag(diagonal_sigmas.to_flat_list()).inv() * sf.V6(tangent_error))
    
      def loop_residual(
              pose_prev: sf.Pose3, 
              pose_next: sf.Pose3, 
              movement : sf.Pose3,
              diagonal_sigmas: sf.V6,
          ) -> sf.V6:
    
          return odometry_residual(pose_prev, pose_next, movement, diagonal_sigmas)
    
  • ์„ค๋ช…
    1. prior ๋Š” pose ์˜ SE(3)์„ direct ๋กœ ์ถ”์ •ํ•˜๋Š” unary factor (์ธ์ˆ˜๊ฐ€ ํ•˜๋‚˜๋งŒ ์“ฐ์ธ๋‹ค๋Š” ๋œป) ์ด๋‹ค.
    2. odometry ๋Š” consecutive ํ•œ node ์‚ฌ์ด์˜ motion ์„ ์ œ์•ฝํ•œ๋‹ค.
      • ๋”ฐ๋ผ์„œ pose $i$ ์™€ pose $i+1$ ์‚ฌ์ด์˜ ์˜ˆ์ธก๋œ ๊ฐ’ (motion model) ์ด ์‹ค์ œ๋กœ ์ธก์ •๋œ ๊ฐ’ (measurement)๊ณผ ์ผ์น˜๋˜๋„๋ก pose $i$์™€ $i+1$ ์˜ ์œ„์น˜๊ฐ€ ์กฐ์ •๋œ๋‹ค.
        • ์ด๋Ÿฐ ํŠน์„ฑ ๋•Œ๋ฌธ์— GTSAM์—์„œ๋Š” ์ด๋ฅผ between factor ๋ผ๊ณ  ์นญํ•˜๊ธฐ๋„ ํ•œ๋‹ค.
      • ์ฆ‰, ๋‘˜ ์‚ฌ์ด์˜ relative ํ•œ ์ œ์•ฝ๋งŒ์„ ๋งŒ์กฑํ•˜๋ฉด ๋˜๊ธฐ ๋•Œ๋ฌธ์— ๋‘ ๋…ธ๋“œ์˜ ์ „์—ญ์ ์ธ ์œ„์น˜ ์ž์ฒด๋Š” ์–ด๋””์— ๋†“์—ฌ์žˆ๋“  ์ƒ๊ด€์ด ์—†๊ฒŒ ๋œ๋‹ค.
        • ์˜ˆ๋ฅผ ๋“ค์–ด, ๋‘ ๋…ธ๋“œ๊ฐ€ 1๊ณผ 2์— ์žˆ์–ด๋„ ๊ทธ ์‚ฌ์ด ๊ฐ„๊ฒฉ์€ 1์ด๊ณ , 2์™€ 3์— ์žˆ์–ด๋„ ๊ทธ ์‚ฌ์ด ๊ฐ„๊ฒฉ์€ 1์ด๋ฏ€๋กœ ์—ฌ์ „ํžˆ ์ œ์•ฝ์„ ๋งŒ์กฑํ•œ๋‹ค.
        • ๋”ฐ๋ผ์„œ ์ตœ์†Œํ•œ์˜ prior ๊ฐ€ ํ•„์š”ํ•˜๋ฉฐ ๋ณดํ†ต SLAM์—์„œ๋Š” ์ฒซ node ์— ์•„์ฃผ ๊ฐ•ํ•œ (==๋งค์šฐ ์ž‘์€ covariance๋ฅผ ๊ฐ€์ง€๋Š”) prior ์ œ์•ฝ์„ ๊ฑธ์–ด์ฃผ๊ฒŒ ๋œ๋‹ค.
        • ํ˜น์€ GPS ๊ฐ™์€ ๊ฐ’์„ ์‚ฌ์šฉํ•˜์—ฌ ์ „์—ญ์ ์ธ ์œ„์น˜๋ฅผ ๊ณ ์ •ํ•  ์ˆ˜๋„ ์žˆ๋‹ค.
          • rotation ์ •๋ณด๊ฐ€ ์—†์ด translation (์ฆ‰, lat, lng, altitude) ๋งŒ ์‚ฌ์šฉ๊ฐ€๋Šฅํ•œ ๊ฒฝ์šฐ, ํ•˜๋‚˜์˜ node ๋งŒ prior ๋ฅผ ๊ฑธ์–ด์ฃผ๋Š” ๊ฒƒ์œผ๋กœ๋Š” ๋ถ€์กฑํ•˜๊ณ  ๋‘˜ ์ด์ƒ์˜ ์„œ๋กœ ๋‹ค๋ฅธ ๋…ธ๋“œ์— prior ๋ฅผ ๊ฑธ์–ด์ฃผ์–ด์•ผ global heading ์„ ์•Œ ์ˆ˜ ์žˆ๊ฒŒ ๋œ๋‹ค.
        • ps. ๋งŒ์•ฝ prior ๋ฅผ ์•ˆ๊ฑธ์–ด์ฃผ๋ฉด ์ด ๋…ธ๋“œ๋“ค์ด ๋ชจ์–‘์ƒˆ๋Š” ์œ ์ง€ํ•˜๋ฉด์„œ ์ „์—ญ์ ์œผ๋กœ ๋ฏธ์ณ๋‚ ๋›ฐ๊ฒŒ (โ€ฆ) ๋˜๋Š”๋ฐ, ์ด๋ฅผ ๊ฒŒ์ด์ง€ ํ”„๋ผ๋ธ”๋Ÿผ ์ด๋ผ๊ณ ๋„ ํ•˜๋Š” ๋“ฏํ•˜๋‹ค. Ceres ์˜ ๊ณต์‹ example ์—์„œ๋„ ๋”ฑ ์ด ์ด์•ผ๊ธฐ๊ฐ€ ์žˆ๋‹ค.
          // The pose graph optimization problem has six DOFs that are not fully
          // constrained. This is typically referred to as gauge freedom. You can apply
          // a rigid body transformation to all the nodes and the optimization problem
          // will still have the exact same cost. The Levenberg-Marquardt algorithm has
          // internal damping which mitigates this issue, but it is better to properly
          // constrain the gauge freedom. This can be done by setting one of the poses
          // as constant so the optimizer cannot change it.
          
    3. loop closure ๋Š” not consecutive ํ•œ ๋‘ node ์‚ฌ์ด์˜ motion์„ ์ œ์•ฝํ•œ๋‹ค.
      • ๋”ฐ๋ผ์„œ ์ˆ˜ํ•™์ ์œผ๋กœ๋Š” odometry ์™€ ์™„์ „ํžˆ ๋™์ผํ•˜๋‹ค.
        • ๊ทธ๋ž˜์„œ ์œ„์˜ ๊ตฌํ˜„์—์„œ๋„ loop_residual ์—์„œ๋Š” ๋‚ด๋ถ€์ ์œผ๋กœ odometry_residual ์„ callํ•˜๋„๋ก ๊ตฌํ˜„ํ•˜์˜€๋‹ค.
      • ๋‹ค๋งŒ ๋ถ„๋ฆฌํ•ด์„œ ์ด์•ผ๊ธฐํ•˜๋Š” ๊ฒƒ์ด ์ข‹์€ ์ด์œ ๋Š”,
        • odometry์™€ loop closure ์—๋Š” noise scale ์„ ๋‹ค๋ฅด๊ฒŒ ์ ์šฉํ•˜๋Š” ๊ฒƒ์ด ํ˜„์‹ค ๋ฌธ์ œ๋ฅผ ํ’€ ๋•Œ์— ๋„์›€์ด ๋˜๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.
          • ์ด๋Š” ๋ณธ์ธ์ด ์‚ฌ์šฉํ•˜๋Š” ์„ผ์„œ์˜ ํ’ˆ์งˆ์— ๋”ฐ๋ผ ๋‹ฌ๋ฆฌ ์ ์šฉํ•ด์•ผ ํ•œ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด odometry๋กœ wheel odometer ๋ฅผ ์‚ฌ์šฉํ•˜๋Š”๋ฐ ๊ทธ๊ฒƒ์˜ ํ’ˆ์งˆ์ด ์ข‹์ง€ ์•Š๋‹ค๋ฉด odometry residual์— ๋Œ€ํ•ด์„œ๋Š” ๋†’์€ noise ๋ฅผ ๋ถ€์—ฌํ•  ์ˆ˜ ์žˆ๊ฒ ๋‹ค. ๋ฐ˜๋ฉด, ๊ณ ์ •๋ฐ€์˜ lidar ์„ผ์„œ scan ์„ ํ†ตํ•ด loop closing ์„ ์ˆ˜ํ–‰ํ•œ๋‹ค๋ฉด loop closure residual์— ๋Œ€ํ•ด์„œ๋Š” ๋‚ฎ์€ noise (==๋†’์€ ์‹ ๋ขฐ๋„) ๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๊ฒ ๋‹ค.
        • ๋˜ํ•œ loop closure factor ๋ฅผ ์ถ”๊ฐ€ํ•˜๊ธฐ ์ด์ „์—, loop closure detection (or aka place recognition) ์ด๋ผ๋Š” ๊ณผ์ •์„ ์ˆ˜ํ–‰ํ•ด์•ผ ํ•˜๋Š”๋ฐ, ์ด๊ฒƒ์ด 100% ํ•ญ์ƒ ์˜ฌ๋ฐ”๋ฅด๋‹ค๋Š” ๋ณด์žฅ์ด ์—†๋‹ค. ๋”ฐ๋ผ์„œ odometry factor ์™€ ๋‹ฌ๋ฆฌ loop closure factor ์— ๋Œ€ํ•ด์„œ๋Š” robust kernel ์„ ์”Œ์›Œ์ฃผ๋Š” ์ž‘์—… ๋“ฑ์ด ์š”๊ตฌ๋˜๊ธฐ๋„ ํ•œ๋‹ค.
      • ๋”ฐ๋ผ์„œ ํ”„๋ž™ํ‹ฐ์ปฌํ•˜๊ฒŒ๋Š” loop closure factor ์™€ odometry factor ๋ฅผ ๋ถ„๋ฆฌํ•ด์„œ ์ƒ๊ฐํ•˜๋Š” ํŽธ์ด ์ข‹๋‹ค.
  • ์„ค๋ช… 2
    • ์œ„์˜ ์ฝ”๋“œ๋ฅผ ๋ณด๋ฉด $\text{SE}(3)$๋ฅผ ์ธํ’‹์œผ๋กœ ๋ฐ›์ง€๋งŒ ์‹ค์ œ๋กœ diff๋Š” local_coordinates๋ผ๋Š” ํ•จ์ˆ˜๋ฅผ ํ†ตํ•ด์„œ ๊ณ„์‚ฐ๋˜๋Š” ๊ฒƒ์„ ์•Œ ์ˆ˜ ์žˆ๋‹ค.
      • SymForce ์˜ ์ฝ”๋“œ๋ฅผ ๋œฏ์–ด๋ณด๋ฉด Pose3์— ๋Œ€ํ•ด local_coordinates ์€ ์•„๋ž˜(์›๋ณธ๋งํฌ)์™€ ๊ฐ™์ด ๊ตฌํ˜„๋˜์–ด ์žˆ๋‹ค.
          @staticmethod
          def local_coordinates(a, b, epsilon):
              # type: (sym.Pose3, sym.Pose3, float) -> T.List[float]
        
              # Total ops: 50
        
              # Input arrays
              _a = a.data
              _b = b.data
        
              # Intermediate terms (4)
              _tmp0 = -_a[0] * _b[0] - _a[1] * _b[1] - _a[2] * _b[2]
              _tmp1 = _a[3] * _b[3]
              _tmp2 = min(1 - epsilon, abs(_tmp0 - _tmp1))
              _tmp3 = (
                  2
                  * (2 * min(0, (0.0 if -_tmp0 + _tmp1 == 0 else math.copysign(1, -_tmp0 + _tmp1))) + 1)
                  * math.acos(_tmp2)
                  / math.sqrt(1 - _tmp2 ** 2)
              )
        
              # Output terms
              _res = [0.0] * 6
              _res[0] = _tmp3 * (-_a[0] * _b[3] - _a[1] * _b[2] + _a[2] * _b[1] + _a[3] * _b[0])
              _res[1] = _tmp3 * (_a[0] * _b[2] - _a[1] * _b[3] - _a[2] * _b[0] + _a[3] * _b[1])
              _res[2] = _tmp3 * (-_a[0] * _b[1] + _a[1] * _b[0] - _a[2] * _b[3] + _a[3] * _b[2])
              _res[3] = -_a[4] + _b[4]
              _res[4] = -_a[5] + _b[5]
              _res[5] = -_a[6] + _b[6]
              return _res
        
      • ์—ฌ๊ธฐ์„œ return ๊ฐ’์€ ๋‘ Pose3์ธ a, b์˜ difference ์ด๋ฉฐ 3-4-5 ์ธ๋ฑ์Šค๊ฐ€ translation ์ฐจ์ด์— ํ•ด๋‹นํ•˜๊ณ , 0-1-2 ์ธ๋ฑ์Šค๊ฐ€ rotation ์ฐจ์ด์— ํ•ด๋‹นํ•œ๋‹ค.
      • Total ops ๋ฅผ ๋ช…๊ธฐํ•œ ๊ฒƒ๋„ ํฅ๋ฏธ๋กœ์šด ๋ถ€๋ถ„์ด๋‹ค. ์ฆ‰, ์ € ์—ฐ์‚ฐ์ด Pose3์— ๋Œ€ํ•ด์„œ deterministic ํ•˜๊ฒŒ ์ตœ์ ํ™”๋˜์–ด ์žˆ๋‹ค๋Š” ๋œป.
    • ์•„๋ฌดํŠผ ์šฐ๋ฆฌ๊ฐ€ ์•Œ์•„์•ผ ํ•  ๊ฒƒ ํ•œ์ค„์š”์•ฝ์€, rotation matrix์€ vector space ๊ฐ€ ์•„๋‹ˆ๊ธฐ ๋•Œ๋ฌธ์—, Pose3 (== $\text{SE}(3)$) type ์‚ฌ์ด์˜ error ๋ฅผ ๊ณ„์‚ฐํ•˜๊ณ  ์‹ถ์„ ๋•Œ๋Š” local_coordinates ๋ผ๋Š” ํ•จ์ˆ˜๋ฅผ ์‚ฌ์šฉํ•ด์•ผ ํ•œ๋‹ค๋Š” ๊ฒƒ์ด๋‹ค.
      • ps. ์ง€๋‚œ from scratch ๊ฒŒ์‹œ๊ธ€์—์„œ๋Š” minimal ํ•œ vector3 ์ธ rotvec์„ ๋จผ์ € ์ •์˜ํ•˜๊ณ , ์ด๋ฅผ ์ด์šฉํ•ด์„œ SO(3)์ธ rotmat์„ ๋งŒ๋“ค์–ด์„œ ์ผ์—ˆ๋‹ค.
            rotvec   = sf.V3.symbolic("Theta") # i.e., angle-axis parametrization
            rotmat   = LieGroupOps.from_tangent(sf.Rot3, rotvec) # for debug, display(rotmat.to_rotation_matrix())
        

Experiments Setting

  • ์•”ํŠผ ์‚ฌ์„ค์ด ๊ธธ์—ˆ๋Š”๋ฐ ์‹คํ—˜์€ ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.

Dataset generation

  • ๋จผ์ € ํ† ์ด ๋ฐ์ดํ„ฐ์…‹์„ ์ง์ ‘ ๋งŒ๋“ค์–ด๋ณด์ž. ์ž์„ธํ•œ ๊ฑด ์ฝ”๋“œ๋ฅผ ์ฐธ๊ณ  (๋งํฌ)๋ฐ”๋žŒ.
  • move() ํ•จ์ˆ˜๋ฅผ ํ†ตํ•ด ์ •๋‹ค๊ฐํ˜• ๋ชจ์–‘์˜ trajectory๋ฅผ ๊ตฌ์„ฑํ•˜๋Š” n๊ฐœ์˜ pose ๋ฅผ ์ƒ์„ฑํ•œ๋‹ค. ์˜ˆ์‹œ๋Š” ์•„๋ž˜์™€ ๊ฐ™๋‹ค.

Hyper parameter setting

  • PGO์—์„œ hyper parameter ๋ผ๊ณ  ํ•˜๋ฉด ์œ„์—์„œ ์ด์•ผ๊ธฐํ•œ 3์ข…๋ฅ˜์˜ factors ์— ๋Œ€ํ•œ noise ๋ฅผ ๊ฒฐ์ •ํ•˜๋Š” ์ผ์ด ๋˜๊ฒ ๋‹ค.
  • ์ด ์˜ˆ์ œ์—์„œ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์ด ๊ตฌ์„ฑํ•ด๋ณด์•˜๋‹ค.
      ## noises 
      prior_diagonal_sigmas = np.array([0.001, 0.001, 0.001, 0.01, 0.01, 0.01]) * 0.001
      odometry_diagonal_sigmas = np.array([0.05, 0.05, 0.05, 0.1, 0.1, 0.1]) 
      loop_diagonal_sigmas = odometry_diagonal_sigmas * 0.1
    
    1. prior ์˜ ๊ฒฝ์šฐ ์–ด์งœํ”ผ ์ œ์ผ ์ฒซ ๋…ธ๋“œ์—๋งŒ ๋ถ€์—ฌํ•  ๊ฒƒ์ด๋ฉฐ, ์ „์ฒด trajectory ๊ฐ€ ํ”๋“ค๋ฆฌ์ง€ ์•Š์•„์•ผ ํ•˜๋ฏ€๋กœ (==gauge problem์— ๋น ์ง€์ง€ ์•Š์•„์•ผ ํ•˜๋ฏ€๋กœ) ์•„์ฃผ ์ž‘์€ ๊ฐ’์œผ๋กœ ์„ค์ •ํ•˜์˜€๋‹ค.
    2. odometry ๋Š” (rad์™€ meter ๋ผ๋Š” ๋‹จ์œ„๋ฅผ ๊ณ ๋ คํ•˜์—ฌ) ์ ๋‹นํ•œ ๊ฐ’์œผ๋กœ ์„ค์ •ํ•˜์˜€๋‹ค.
      • ps. ์ด ๋•Œ ์ด ๊ฐ’์€ std์ผ์ˆ˜๋„ ์žˆ๊ณ  covariance (std์˜ ์ œ๊ณฑ) ์ผ์ˆ˜๋„ ์žˆ๋Š”๋ฐ, library๋งˆ๋‹ค ๋‹ค๋ฅผ ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ ๋ฌธ๋งฅ์ƒ ํŒŒ์•…ํ•˜์—ฌ์•ผ ํ•œ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด GTSAM์—์„œ๋Š” covariance ์ด๋‹ค.
        • ํ•˜์ง€๋งŒ ๊ทธ๊ฒƒ๋ณด๋‹ค ๋” ์ค‘์š”ํ•œ ๊ฑด ์–ด์งœํ”ผ ์ ์ • ์Šค์ผ€์ผ์€ ์‹คํ—˜์ ์œผ๋กœ๋„ ํŒŒ์•…ํ•˜๋Š” ๊ฒƒ์ด ์š”๊ตฌ๋œ๋‹ค.
        • ์ด ์˜ˆ์ œ์—์„œ๋Š” ์•ž์„œ sfT.cast(sf.V6, sf.M.diag(diagonal_sigmas.to_flat_list()).inv() * sf.V6(tangent_error)) ์™€ ๊ฐ™์ด error ์— noise ์˜ inverse ๋ฅผ ๋ฐ”๋กœ ๊ณฑํ•ด์ฃผ๋Š” ํ˜•ํƒœ๋กœ ๊ตฌํ˜„๋˜์—ˆ๋‹ค.
          • ์ฆ‰, std๊ฐ€ ๋œ๋‹ค. ์•„๋ž˜ ์ˆ˜์‹์„ ์ฐธ๊ณ ํ•˜๋ฉด ์ดํ•ด๊ฐ€ ๋œ๋‹ค. (์ถœ์ฒ˜: Factor Graphs for Robot Perception, 2017). error $e$์— ๋ฐ”๋กœ (์—ญ์ˆ˜๊ฐ€)๊ณฑํ•ด์ง€๋Š” ๊ฒƒ์€ covariance ์˜ sqrt์ž„์„ ์•Œ ์ˆ˜ ์žˆ๋‹ค. ๊ทธ๋ž˜์„œ ์•ž์„œ ๊ตฌํ˜„์—์„œ๋„ ๋ณ€์ˆ˜ ์ด๋ฆ„์„ diagonal_sigma ๋ผ๊ณ  ์นญํ•ด๋‘์—ˆ๋‹ค.

    3. loop closure ์˜ noise ๋Š” odometry๋ณด๋‹ค ๋” ์ž‘๊ฒŒ (๋” ๊ฐ•๋ ฅํ•˜๊ฒŒ ์ž‘์šฉํ•˜๋„๋ก) ์„ค์ •ํ•˜์˜€๋‹ค.

Key-based Value Management

  • SymForce ๊ฐ€ ๋˜ ์ข‹์€ ์ ์ด ๋ฐ”๋กœ Key ๊ธฐ๋ฐ˜์œผ๋กœ value ๋ฅผ ๊ด€๋ฆฌํ•˜๋Š” ๊ฒƒ์ด๋‹ค.
  • factors๋“ค์„ ์‹ค์ œ๋กœ ์„ ์–ธํ•  ๋•Œ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ํ•˜๋ฉด ๋œ๋‹ค.
# 1/ Prior factors 
if use_factors['prior']:
    for i in range(num_poses):
        if i not in [0]:
            continue        
        # only add at the first node 
        factors.append(Factor(
            residual=prior_residual,
            keys=[f"poses[{i}]", f"poses_prior[{i}]", f"prior_diagonal_sigmas"],
        ))

# 2/ Odometry factors
if use_factors['odometry']:        
    for i in range(num_poses-1):        
        factors.append(Factor(
            residual=odometry_residual,
            keys=[f"poses[{i}]", f"poses[{i+1}]", f"odoms[{i}]", "odometry_diagonal_sigmas"],
        ))

# 3/ Loop factors
if use_factors['loop']:
    for ii, loops_index_pair in enumerate(loops_indexes):        
        idx_from = loops_index_pair[0]
        idx_to = loops_index_pair[1]
        factors.append(Factor(
            residual=odometry_residual,
            keys=[f"poses[{idx_from}]", f"poses[{idx_to}]", f"loops[{ii}]", "loop_diagonal_sigmas"],
        ))
  • ์œ„์˜ ์ฝ”๋“œ์—์„œ ๋ณด๋‹ค์‹œํ”ผ, residual=์— ํ•จ์ˆ˜๋ฅผ ์ž…๋ ฅํ•ด์ฃผ๊ณ , keys= ์— ์ด ํ•จ์ˆ˜์— ์ธ์ž๋กœ ๋“ค์–ด๊ฐˆ ๋ณ€์ˆ˜๋“ค์„ ์ฐจ๋ก€๋กœ ๋‚˜์—ดํ•ด์ฃผ๋ฉด ๋œ๋‹ค.
    • ๊ทธ๋Ÿฐ๋ฐ ์ด stringfied๋œ ๋ณ€์ˆ˜์ด๋ฆ„๋“ค์€ ์–ด๋””์— ์žˆ๋Š”๊ฐ€? ๋‹ค์Œ๊ณผ ๊ฐ™์ด Values ๋ผ๋Š” ๊ณณ์—๋‹ค๊ฐ€ ๋‹ค ๋‹ด์•„์ฃผ๋ฉด ๋œ๋‹ค.
        initial_values = Values(
            # poses=[sf.Pose3.identity()] * num_poses, # may not converge.
            poses=initial_poses,
                  
            poses_prior=poses_prior,
            prior_diagonal_sigmas=prior_diagonal_sigmas,
      
            odoms=odoms_obs,
            odometry_diagonal_sigmas=odometry_diagonal_sigmas,
                  
            loops=loops_obs,
            loop_diagonal_sigmas=loop_diagonal_sigmas,
        )
      
      • ์šฐ๋ณ€์— ์žˆ๋Š” ๊ฒƒ์ด ์šฐ๋ฆฌ๊ฐ€ ์‚ฌ์ „์— ์ •์˜ํ•ด๋‘” ๋ณ€์ˆ˜๋“ค์ด๋ฉฐ
      • ์ขŒ๋ณ€์— ์‚ฌ์šฉํ•œ ์ด๋ฆ„์ด key๋กœ ์“ฐ์ด๊ฒŒ ๋œ๋‹ค.
  • ๊ทธ๋Ÿฌ๋ฉด ์ตœ์ข…์ ์œผ๋กœ Optimizer class๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์€ ์‰ฝ๋‹ค. ์•„๋ž˜์™€ ๊ฐ™์ด ํ•ด์ฃผ๋ฉด ๋!
    optimizer = Optimizer(
      factors=factors,
      optimized_keys=[f"poses[{i}]" for i in range(num_poses)],
      debug_stats=True,
    )
    
    • ์—ฌ๊ธฐ์„œ๋Š” ์ตœ์ ํ™”ํ•  ๋ณ€์ˆ˜์— ๋Œ€ํ•œ key๋งŒ ๋˜ ๋„ฃ์–ด์ฃผ๊ฒŒ ๋˜๋Š”๋ฐ, ์ด ๋ถ€๋ถ„์€ ์ข€ Pytorch์™€ ๋‹ฎ์€ ๊ฒƒ ๊ฐ™๊ธฐ๋„ ํ•˜๋‹ค.

Results

  • ์‚ฌ์„ค์ด ๋˜ ์—ญ์‹œ ๊ธธ์—ˆ๋Š”๋ฐ, ๊ฒฐ๊ณผ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.

Without a Loop constraint

  • odometry factors ์— ๋Œ€ํ•ด ground-truth ๋Œ€๋น„ perturbation noise ๋ฅผ ์„ž์–ด์ฃผ์—ˆ๊ธฐ ๋•Œ๋ฌธ์— (see perturb_alpha in the codes), loop closure factor ์—†์ด๋Š” ์•„๋ž˜์™€ ๊ฐ™์ด trajectory์— drift ๊ฐ€ ๋ˆ„์ ๋˜๋Š” ๊ฒƒ์„ ๋ณผ ์ˆ˜ ์žˆ๋‹ค.

With only a single Loop constraint

  • ํ•œํŽธ, ์ฒ˜์Œ ๋…ธ๋“œ์™€ ๋งˆ์ง€๋ง‰ ๋…ธ๋“œ ์‚ฌ์ด์— loop closure factor ๋ฅผ ๋‹จ ํ•˜๋‚˜๋งŒ ๋„ฃ์–ด์ฃผ๋”๋ผ๋„, ์•„๋ž˜์™€ ๊ฐ™์ด ์ „์ฒด trajectory์˜ shape ์ด ๋งŽ์ด ๊ฐœ์„ ๋˜๋Š” ๊ฒƒ์„ ๋ณผ ์ˆ˜ ์žˆ๋‹ค.

  • ์‚ฌ์‹ค ์ˆ˜ํ•™์ ์œผ๋กœ ๋‹น์—ฐํ•œ ๊ฒฐ๊ณผ์ด๋‹คใ…Žใ…Ž ๊ทธ๋ž˜๋„ ์ƒˆ๋กœ์šด ํˆด์„ ์ด์šฉํ•ด์„œ ๊ฐ„๋‹จํ•˜๊ฒŒ ์ด๋ ‡๊ฒŒ ์‹ค์Šตํ•ด๋ณด๋Š” ๊ฒƒ์€ ์ฐธ ์žฌ๋ฏธ์žˆ๋‹ค.
    • ์ด๋Ÿฐ ๋ฅ˜์˜ (๊ฝค ์ž‘์€ ์ฝ”๋“œ๋ฒ ์ด์Šค ๊ทœ๋ชจ์˜) PGO ๋Š” SLAM๊ณ„์˜ hello-world ๋ผ๊ณ  ํ•  ์ˆ˜ ์žˆ์ง€ ์•Š์„๊นŒ?

๊ฒฐ๋ก 

  • SymForce ๋กœ Pose-graph Optimization ์„ ์ˆ˜ํ–‰ํ•ด๋ณด์•˜๋‹ค.
    • Pose3 ๋“ค์— ๋Œ€ํ•ด unary (prior factor) or binary (between factor) constraints ๋ฅผ ๊ฑธ์–ด์ฃผ๊ธฐ ์œ„ํ•ด์„œ, measurement ์™€ estimated (direct value or error value) ์‚ฌ์ด์— error ๋ฅผ ๊ณ„์‚ฐํ•ด์•ผ ํ•œ๋‹ค. ์ด๋ฅผ ๊ณ„์‚ฐํ•˜๊ธฐ ์œ„ํ•ด์„œ local_coordinates ์ด๋ผ๋Š” ํ•จ์ˆ˜๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค. ์ด๋Ÿฐ ํ”„๋กœ์„ธ์Šค๋ฅผ tangent space optimization ๋ผ๊ณ ๋„ ๋ถ€๋ฅธ๋‹ค.
  • ps. ๊ทธ ์™ธ์—.. simularity ๋ฅผ ๋ฐฉ์ง€ํ•˜๊ธฐ ์œ„ํ•ด epsilon์„ ์ž˜ ์‚ฌ์šฉํ•ด์ฃผ์–ด์•ผ ํ•œ๋‹ค.

TODO