Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

921 kinematics pose estimation #1089

Merged
merged 29 commits into from
Feb 16, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
1ce5b00
Add tachometer, odometer and kinematics part
Ezward Jan 1, 2023
deeb6fc
refactor pose estimation into a single part
Ezward Jan 8, 2023
d31b163
comment out logging of pose to quiet the console
Ezward Jan 8, 2023
9710c16
add arduino encoder sketches
Ezward Jan 16, 2023
6d66d2c
remove unicode characters that prevent compilation of arduino sketch
Ezward Jan 17, 2023
ff95fdf
clean up comments a little
Ezward Jan 17, 2023
e2a9a54
Add more documentation to the quadrature_encoder.ino sketch
Ezward Jan 18, 2023
325e89b
Add interrupt mode to the mono encoder
Ezward Jan 24, 2023
a5c6a44
Remove spurious unicode beta character
Ezward Jan 24, 2023
ae4ab8f
removed digital write high in mono encoder
Ezward Jan 24, 2023
8f2f859
Fix syntax error if only using one channel in int mode
Ezward Jan 26, 2023
e6eb5cf
Added a quadrature encoder sketch with no libraries
Ezward Jan 28, 2023
162ff35
Fix bug in mono encoder sketch
Ezward Jan 28, 2023
5d05b11
Fix bug in quadrature nolib sketch
Ezward Jan 28, 2023
a4ede0a
minor change to quadrature nolib
Ezward Jan 28, 2023
896cc26
Updated quadrature_encoder.ino to not require library
Ezward Jan 31, 2023
1cf569d
fix merge error in path_follow.py
Ezward Feb 4, 2023
a90421d
Fix RPi_GPIO_Servo part so it does not need GPIO in constructor
Ezward Feb 5, 2023
92af322
added non-threaded run() to UnicyclePose and BicyclePose for testing
Ezward Feb 8, 2023
4e6c371
Improved accuracy of MockEncoder by propagating fractional ticks
Ezward Feb 8, 2023
6d4f506
Updated add_odometry() so we could add as non-threaded for testing.
Ezward Feb 8, 2023
9f2ab20
Vehicle loop prints out number of iterations and total time
Ezward Feb 8, 2023
e9cac78
Rewrite of the kinematics unit tests
Ezward Feb 8, 2023
0cda9b0
Adjust bicycle kinematics to use front wheel reference
Ezward Feb 9, 2023
a1d4f78
relax orientation test to 3% accuracy
Ezward Feb 9, 2023
024fc0f
updated based on PR feedback
Ezward Feb 9, 2023
0ae8db9
removed hard-coded logging level
Ezward Feb 12, 2023
ee22a1b
Update vehicle.py
Ezward Feb 13, 2023
78711a7
Update setup.py version="4.4.dev5"
Ezward Feb 13, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
refactor pose estimation into a single part
- UnicyclePose handles encoder/tachometer/odometer/kinematics
  for differential drive.
- BicyclePose handles encoder/tachometer/odomter/kinematics
  for car-like vehicle
- add a mock encoder that is driven by throttle and steering
  input.  This allows use to simulate the vehicle without
  a full-on simulator.
- Fix the drawing of the path so that it handles the
  fact that Y increases going north.
  • Loading branch information
Ezward committed Feb 14, 2023
commit deeb6fc5762a9313538a16766ddccca32ba08e0c
20 changes: 10 additions & 10 deletions donkeycar/parts/kinematics.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ def run(self, forward_distance:float, steering_angle:float, timestamp:float=None
logger.info(result)
return result

return (0, 0, 0, 0, 0, 0, 0, 0, self.timestamp)
return 0, 0, 0, 0, 0, 0, 0, 0, self.timestamp

def shutdown(self):
self.running = False
Expand Down Expand Up @@ -182,7 +182,7 @@ def run(self, forward_velocity:float, angular_velocity:float, timestamp:float=No
steering_angle = bicycle_steering_angle(self.wheel_base, forward_velocity, angular_velocity)
self.timestamp = timestamp

return (forward_velocity, steering_angle, timestamp)
return forward_velocity, steering_angle, timestamp


def bicycle_steering_angle(wheel_base:float, forward_velocity:float, angular_velocity:float) -> float:
Expand Down Expand Up @@ -235,7 +235,7 @@ def __init__(self, wheel_base:float, max_forward_velocity:float, max_steering_an

def run(self, normalized_angular_velocity:float) -> float:
if abs(normalized_angular_velocity) > 1:
print("Warning: normalized_angular_velocity must be between -1 and 1")
logger.error("Warning: normalized_angular_velocity must be between -1 and 1")
return normalized_angular_velocity * self.max_angular_velocity


Expand Down Expand Up @@ -345,8 +345,7 @@ def run(self, left_distance:float, right_distance:float, timestamp:float=None) -
self.timestamp
)


return (0, 0, 0, 0, 0, 0, 0, 0, self.timestamp)
return 0, 0, 0, 0, 0, 0, 0, 0, self.timestamp

def shutdown(self):
self.running = False
Expand All @@ -369,6 +368,7 @@ def __init__(self, axle_length:float, wheel_radius:float, min_speed:float, max_s

self.wheel_diameter = 2 * wheel_radius
self.wheel_circumference = math.pi * self.wheel_diameter

def run(self, forward_velocity:float, angular_velocity:float, timestamp:float=None) -> Tuple[float, float, float]:
"""
Convert turning velocity in radians and forward velocity (like meters per second)
Expand All @@ -393,7 +393,7 @@ def run(self, forward_velocity:float, angular_velocity:float, timestamp:float=No
self.timestamp = timestamp

# left/right linear speeds and timestamp
return (left_linear_speed, right_linear_speed, timestamp)
return left_linear_speed, right_linear_speed, timestamp

def shutdown(self):
pass
Expand Down Expand Up @@ -453,7 +453,7 @@ def __init__(self, wheel_radius:float, axle_length:float, max_forward_velocity:f

def run(self, normalized_angular_velocity:float) -> float:
if abs(normalized_angular_velocity) > 1:
print("Warning: normalized_angular_velocity must be between -1 and 1")
logger.error("Warning: normalized_angular_velocity must be between -1 and 1")
return normalized_angular_velocity * self.max_angular_velocity


Expand Down Expand Up @@ -489,7 +489,7 @@ def run(self, steering_angle) -> float:
return 0
return -steering # positive steering angle is negative normalized

def shutdown():
def shutdown(self):
pass


Expand Down Expand Up @@ -532,7 +532,7 @@ def run(self, steering) -> float:

return self.max_steering_angle * steering * -s

def shutdown():
def shutdown(self):
pass


Expand All @@ -549,7 +549,7 @@ def wheel_rotational_velocity(wheel_radius:float, speed:float) -> float:
return speed / wheel_radius


def differential_steering(throttle: float, steering: float, steering_zero: float = 0.01) -> Tuple[float, float]:
def differential_steering(throttle: float, steering: float, steering_zero: float = 0) -> Tuple[float, float]:
"""
Turn steering angle and speed/throttle into
left and right wheel speeds/throttle.
Expand Down
10 changes: 7 additions & 3 deletions donkeycar/parts/path.py
Original file line number Diff line number Diff line change
Expand Up @@ -244,10 +244,14 @@ def run(self, img, path):
for iP in range(0, len(path) - 1):
ax, ay = path[iP]
bx, by = path[iP + 1]

#
# y increases going north, so handle this with scale
#
self.plot_line(ax * self.scale + self.offset[0],
ay * self.scale + self.offset[1],
ay * -self.scale + self.offset[1],
bx * self.scale + self.offset[0],
by * self.scale + self.offset[1],
by * -self.scale + self.offset[1],
draw,
color)
return img
Expand Down Expand Up @@ -279,7 +283,7 @@ def plot_circle(self, x, y, rad, draw, color, width=1):
def run(self, img, x, y):
draw = ImageDraw.Draw(img)
self.plot_circle(x * self.scale + self.offset[0],
y * self.scale + self.offset[1],
y * -self.scale + self.offset[1], # y increases going north
self.radius,
draw,
self.color)
Expand Down
232 changes: 232 additions & 0 deletions donkeycar/parts/pose.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
import logging
import time
from typing import Tuple

from donkeycar.utils import is_number_type
from donkeycar.parts.kinematics import NormalizeSteeringAngle, UnnormalizeSteeringAngle, TwoWheelSteeringThrottle
from donkeycar.parts.kinematics import Unicycle, InverseUnicycle, UnicycleUnnormalizeAngularVelocity
from donkeycar.parts.kinematics import Bicycle, InverseBicycle, BicycleUnnormalizeAngularVelocity
from donkeycar.parts.kinematics import differential_steering
from donkeycar.parts.tachometer import Tachometer, MockEncoder

logger = logging.getLogger(__name__)
logging.basicConfig(level=logging.INFO)
DocGarbanzo marked this conversation as resolved.
Show resolved Hide resolved

class UnicycleDistance:
def run(self, left, right):
if is_number_type(left) and is_number_type(right):
return (left + right) / 2.0
else:
logger.error("left and right must be floats")
return 0.0


class BicyclePose:
"""
Part that integrates encoder+tachometer+odometer+kinematics
for the purposes of estimating the pose of a car-like vehicle over time.
"""
def __init__(self, cfg, poll_delay_secs:float=0):
from donkeycar.parts.serial_port import SerialPort
from donkeycar.parts.tachometer import (Tachometer, SerialEncoder,
GpioEncoder, EncoderChannel,
MockEncoder)
from donkeycar.parts.odometer import Odometer

# distance_per_revolution = cfg.ENCODER_PPR * cfg.MM_PER_TICK / 1000
distance_per_revolution = cfg.WHEEL_RADIUS * 2 * 3.141592653589793

self.poll_delay_secs = poll_delay_secs
self.encoder = None
self.tachometer = None
self.odometer = None
self.steerer = None
self.bicycle = None
self.Running = False

self.inputs = (0, 0) # (throttle, steering)
self.reading = (0, 0, 0, 0, 0, 0, 0, 0, None)

if cfg.ENCODER_TYPE == "GPIO":
from donkeycar.parts import pins
self.encoder = GpioEncoder(gpio_pin=pins.input_pin_by_id(cfg.ODOM_PIN),
debounce_ns=cfg.ENCODER_DEBOUNCE_NS,
debug=cfg.ODOM_DEBUG)
elif cfg.ENCODER_TYPE == "arduino":
self.encoder = SerialEncoder(serial_port=SerialPort(cfg.ODOM_SERIAL, cfg.ODOM_SERIAL_BAUDRATE), debug=cfg.ODOM_DEBUG)
elif cfg.ENCODER_TYPE.upper() == "MOCK":
self.encoder = MockEncoder(cfg.ENCODER_PPR * 10) # max 10 revolutions per second
else:
print("No supported encoder found")

if self.encoder:
self.tachometer = Tachometer(
self.encoder,
ticks_per_revolution=cfg.ENCODER_PPR,
direction_mode=cfg.TACHOMETER_MODE,
poll_delay_secs=1.0 / (cfg.DRIVE_LOOP_HZ * 3),
debug=cfg.ODOM_DEBUG)

if self.tachometer:
self.odometer = Odometer(
distance_per_revolution=distance_per_revolution,
smoothing_count=cfg.ODOM_SMOOTHING,
debug=cfg.ODOM_DEBUG)

self.steerer = UnnormalizeSteeringAngle(cfg.MAX_STEERING_ANGLE)
self.bicycle = Bicycle(cfg.WHEEL_BASE, cfg.ODOM_DEBUG)
self.running = True
else:
logger.error("Unable to initialize BicyclePose part")

def update(self):
"""
This will get called on it's own thread.
throttle: sign of throttle is use used to determine direction.
timestamp: timestamp for update or None to use current time.
This is useful for creating deterministic tests.
"""
while self.running:
if self.tachometer:
timestamp = None
throttle, steering = self.inputs
if isinstance(self.encoder, MockEncoder):
self.encoder.run(throttle, timestamp)
revolutions, timestamp = self.tachometer.run(throttle, timestamp)
distance, velocity, timestamp = self.odometer.run(revolutions, timestamp)
steering_angle = self.steerer.run(steering)
self.reading = self.bicycle.run(distance, steering_angle, timestamp)

time.sleep(self.poll_delay_secs) # give other threads time

def run_threaded(self, throttle:float=0.0, steering:float=0.0, timestamp:float=None) -> Tuple[float, float, float, float, float, float, float, float, float]:
if self.running and self.tachometer:
if throttle is None:
throttle = 0
if steering is None:
steering = 0

self.inputs = (throttle, steering)

return self.reading

return 0, 0, 0, 0, 0, 0, 0, 0, timestamp


class UnicyclePose:
"""
Part that integrates encoders+tachometers+odometers+kinematics
for the purposes of estimating the pose of a
differential drive vehicle over time.
"""
def __init__(self, cfg, poll_delay_secs:float=0):
from donkeycar.parts.serial_port import SerialPort
from donkeycar.parts.tachometer import (SerialEncoder,
GpioEncoder, EncoderChannel)
from donkeycar.parts.odometer import Odometer

# distance_per_revolution = cfg.ENCODER_PPR * cfg.MM_PER_TICK / 1000
distance_per_revolution = cfg.WHEEL_RADIUS * 2 * 3.141592653589793

self.poll_delay_secs = poll_delay_secs
self.left_throttle = 0
self.right_throttle = 0
self.encoder = None
self.tachometer = None
self.odometer = None
self.unicycle = None

self.inputs = (0, 0) # (left_throttle, right_throttle)
self.reading = (0, 0, 0, 0, 0, 0, 0, 0, None)

if cfg.ENCODER_TYPE == "GPIO":
from donkeycar.parts import pins
self.encoder = [
GpioEncoder(gpio_pin=pins.input_pin_by_id(cfg.ODOM_PIN),
debounce_ns=cfg.ENCODER_DEBOUNCE_NS,
debug=cfg.ODOM_DEBUG),
GpioEncoder(gpio_pin=pins.input_pin_by_id(cfg.ODOM_PIN_2),
debounce_ns=cfg.ENCODER_DEBOUNCE_NS,
debug=cfg.ODOM_DEBUG)
]
elif cfg.ENCODER_TYPE == "arduino":
serial_encoder = SerialEncoder(serial_port=SerialPort(cfg.ODOM_SERIAL, cfg.ODOM_SERIAL_BAUDRATE), debug=cfg.ODOM_DEBUG)
self.encoder = [
serial_encoder,
EncoderChannel(encoder=serial_encoder, channel=1)
]
elif cfg.ENCODER_TYPE.upper() == "MOCK":
self.encoder = [
MockEncoder(cfg.ENCODER_PPR * 10),
MockEncoder(cfg.ENCODER_PPR * 10)
]
else:
print("No supported encoder found")

if self.encoder:
self.tachometer = [
Tachometer(
self.encoder[0],
ticks_per_revolution=cfg.ENCODER_PPR,
direction_mode=cfg.TACHOMETER_MODE,
poll_delay_secs=0,
debug=cfg.ODOM_DEBUG),
Tachometer(
self.encoder[1],
ticks_per_revolution=cfg.ENCODER_PPR,
direction_mode=cfg.TACHOMETER_MODE,
poll_delay_secs=0,
debug=cfg.ODOM_DEBUG)
]

if self.tachometer:
self.odometer = [
Odometer(
distance_per_revolution=distance_per_revolution,
smoothing_count=cfg.ODOM_SMOOTHING,
debug=cfg.ODOM_DEBUG),
Odometer(
distance_per_revolution=distance_per_revolution,
smoothing_count=cfg.ODOM_SMOOTHING,
debug=cfg.ODOM_DEBUG)
]
self.unicycle = Unicycle(cfg.AXLE_LENGTH, cfg.ODOM_DEBUG)
self.running = True

def update(self):
"""
This will get called on it's own thread.
throttle: sign of throttle is use used to determine direction.
timestamp: timestamp for update or None to use current time.
This is useful for creating deterministic tests.
"""
while self.running:
if self.tachometer:
left_timestamp = None
right_timestamp = None
left_throttle, right_throttle = self.inputs
if isinstance(self.encoder[0], MockEncoder):
self.encoder[0].run(left_throttle, left_timestamp)
self.encoder[1].run(right_throttle, right_timestamp)

left_revolutions, left_timestamp = self.tachometer[0].run(left_throttle, left_timestamp)
right_revolutions, right_timestamp = self.tachometer[1].run(right_throttle, right_timestamp)

left_distance, left_velocity, left_timestamp = self.odometer[0].run(left_revolutions, left_timestamp)
right_distance, right_velocity, right_timestamp = self.odometer[1].run(right_revolutions, right_timestamp)

self.reading = self.unicycle.run(left_distance, right_distance, right_timestamp)

time.sleep(self.poll_delay_secs) # give other threads time

def run_threaded(self, throttle:float=0.0, steering:float=0.0, timestamp:float=None) -> Tuple[float, float, float, float, float, float, float, float, float]:
if self.running and self.tachometer:
if throttle is None:
throttle = 0
if steering is None:
steering = 0

self.inputs = differential_steering(throttle, steering)
return self.reading

return 0, 0, 0, 0, 0, 0, 0, 0, timestamp
Loading