Skip to content

Commit 08969fe

Browse files
feat: control system rewrite in preperation for expo
1 parent 2b9c1fd commit 08969fe

12 files changed

+492
-451
lines changed

CMakeLists.txt

+2-2
Original file line numberDiff line numberDiff line change
@@ -57,14 +57,14 @@ catkin_python_setup()
5757
add_message_files(
5858
FILES
5959
PointWithCovarianceStamped.msg
60-
RegionOfInterestWithCovarianceStamped.msg
60+
RegionOfInterestWithConfidenceStamped.msg
6161
)
6262

6363
## Generate services in the 'srv' folder
6464
add_service_files(
6565
FILES
66-
MoveitPose.srv
6766
Tf2Transform.srv
67+
Tf2TransformPoseStamped.srv
6868
)
6969

7070
## Generate actions in the 'action' folder

bin/applevision_expo_demo.py

+31-26
Original file line numberDiff line numberDiff line change
@@ -5,38 +5,31 @@
55
import os
66
import subprocess
77
import sys
8-
import select
9-
from threading import Lock, Thread
8+
from threading import Lock
9+
import math
1010

1111
import tf2_ros
1212
import moveit_commander
1313
import rospy
1414
from message_filters import Cache, Subscriber
1515
from moveit_msgs.msg import MoveGroupActionFeedback
16-
from sensor_msgs.msg import Range
1716
from visualization_msgs.msg import Marker
18-
from geometry_msgs.msg import TransformStamped
19-
from applevision_rospkg.srv import Tf2Transform
2017

2118

22-
class ProxyStdout(Thread):
23-
def __init__(self, stdout, prefix, **kwargs):
24-
super(ProxyStdout, self).__init__(**kwargs)
25-
self.daemon = True
26-
self.prefix = prefix
27-
self.stdout = stdout
28-
29-
def run(self):
30-
while not self.stdout.closed:
31-
line = self.stdout.readline()
32-
if not line:
33-
break
34-
rospy.loginfo(self.prefix + line.strip())
19+
def quaternion_multiply(quaternion1, quaternion0):
20+
x0, y0, z0, w0 = quaternion0
21+
x1, y1, z1, w1 = quaternion1
22+
return ([-x1 * x0 - y1 * y0 - z1 * z0 + w1 * w0,
23+
x1 * w0 + y1 * z0 - z1 * y0 + w1 * x0,
24+
-x1 * z0 + y1 * w0 + z1 * x0 + w1 * y0,
25+
x1 * y0 - y1 * x0 + z1 * w0 + w1 * z0])
3526

3627
class ExpoDemo():
3728
BOUNDS_X = (-0.8, -0.2)
3829
BOUNDS_Y = (-1.2, 0)
3930
BOUNDS_Z = (1, 2)
31+
BOUND_ORIENTATION = 20
32+
ORIENTATION_TARGET = (-0.707, 0, 0, 0.707)
4033
EXPO_DEMO_PROGRAM = os.path.join(os.path.dirname(__file__), 'applevision_motion.py')
4134
CHECK_DURATION = rospy.Duration.from_sec(0.1)
4235
GROUP_NAME = 'manipulator'
@@ -85,9 +78,7 @@ def __init__(self):
8578
rospy.on_shutdown(self.kill_robot)
8679

8780
def make_control_system_process(self):
88-
proc = subprocess.Popen(
89-
[self.EXPO_DEMO_PROGRAM], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, universal_newlines=True)
90-
ProxyStdout(proc.stdout, 'EXPO: DEMO {}:'.format(self.demo_seq)).start()
81+
proc = subprocess.Popen([self.EXPO_DEMO_PROGRAM, 'EXPO: DEMO {}:'.format(self.demo_seq)])
9182
self.demo_seq += 1
9283
return proc
9384

@@ -137,6 +128,17 @@ def is_home(self):
137128
def demo_is_running(self):
138129
return self.demo_process is not None and self.demo_process.poll() is None
139130

131+
def is_position_out(self, coords):
132+
return coords.x < ExpoDemo.BOUNDS_X[0] or coords.x > ExpoDemo.BOUNDS_X[1] \
133+
or coords.y < ExpoDemo.BOUNDS_Y[0] or coords.y > ExpoDemo.BOUNDS_Y[1] \
134+
or coords.z < ExpoDemo.BOUNDS_Z[0] or coords.z > ExpoDemo.BOUNDS_Z[1]
135+
136+
def is_orientation_out(self, quat):
137+
# find the smallest angle between this quaternion and the target, and check it's within range
138+
new = quaternion_multiply((-quat.x, -quat.y, -quat.z, quat.w), self.ORIENTATION_TARGET)
139+
angle = abs(180.0 - math.degrees(2*math.atan2(math.sqrt(new[0]**2 + new[1]**2 + new[2]**2), new[3])))
140+
return angle > self.BOUND_ORIENTATION
141+
140142
def check_cb(self, *args):
141143
if self.running_lock.locked():
142144
return
@@ -148,10 +150,10 @@ def check_cb(self, *args):
148150
except Exception as e:
149151
self.die_gracefully('EXPO: tf failed with error {}. Probably time to restart the system.'.format(e))
150152
coords = where_in_world.transform.translation
151-
is_out_bounds = coords.x < ExpoDemo.BOUNDS_X[0] or coords.x > ExpoDemo.BOUNDS_X[1] \
152-
or coords.y < ExpoDemo.BOUNDS_Y[0] or coords.y > ExpoDemo.BOUNDS_Y[1] \
153-
or coords.z < ExpoDemo.BOUNDS_Z[0] or coords.z > ExpoDemo.BOUNDS_Z[1]
154-
153+
is_out_bounds = self.is_position_out(coords)
154+
orientation = where_in_world.transform.rotation
155+
is_out_orientation = self.is_orientation_out(orientation)
156+
155157
# count idle ticks
156158
if self.demo_is_running() and not self.is_moving():
157159
self.idle_count += 1
@@ -175,6 +177,10 @@ def check_cb(self, *args):
175177
coord_str = str(coords).replace('\n', ' ')
176178
rospy.logwarn('EXPO: bounds check failed with coordinates {}, terminating and reseting...'.format(coord_str))
177179
self.kill_robot()
180+
elif is_out_orientation:
181+
# the robot is doing disco moves
182+
rospy.logwarn('EXPO: bounds check failed for orientation, terminating and reseting...')
183+
self.kill_robot()
178184
elif self.idle_count > self.TICK_TIMEOUT:
179185
# the robot has been idle (not controlled by this program) for too long
180186
rospy.logwarn('EXPO: demo got stuck, restarting...')
@@ -192,7 +198,6 @@ def check_cb(self, *args):
192198
self.die_gracefully('EXPO: demo crashed on launch, system cannot recover.')
193199
else:
194200
# the demo is crashed and the robot is stuck in another position
195-
rospy.sleep(1)
196201
rospy.loginfo('EXPO: Sending robot home...')
197202
self.go_home()
198203

bin/applevision_filter.py

+28-58
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,24 @@
11
#!/usr/bin/python3
22

3+
from typing import Optional
34
import rospy
45
import numpy as np
56
from tf2_msgs.msg import TFMessage
67
from geometry_msgs.msg import TransformStamped, PoseWithCovarianceStamped
78
from sensor_msgs.msg import Range
8-
from message_filters import ApproximateTimeSynchronizer, Subscriber
9-
from applevision_rospkg.msg import PointWithCovarianceStamped, RegionOfInterestWithCovarianceStamped
9+
from message_filters import Subscriber
10+
from applevision_rospkg.msg import PointWithCovarianceStamped, RegionOfInterestWithConfidenceStamped
1011
from applevision_rospkg.srv import Tf2Transform
1112
from applevision_kalman.filter import KalmanFilter, EnvProperties
12-
from helpers import RobustServiceProxy, ServiceProxyFailed, HeaderCalc, CameraInfoHelper
13+
from helpers import RobustServiceProxy, ServiceProxyFailed, HeaderCalc, CameraInfoHelper, SynchronizerMinTick
1314

1415

1516
def second_order_var_approx(fprime, fdoubleprime, ftrippleprime, xvar):
1617
# https://en.wikipedia.org/wiki/Taylor_expansions_for_the_moments_of_functions_of_random_variables
1718
return fprime**2 * xvar + 1 / 2 * fdoubleprime**2 * xvar**2 + fprime * ftrippleprime * xvar**2
1819

1920

20-
class MainHandler:
21+
class FilterHandler:
2122

2223
def __init__(self, topic: str, kal: KalmanFilter) -> None:
2324
self.tf_get = RobustServiceProxy('Tf2Transform',
@@ -35,13 +36,8 @@ def __init__(self, topic: str, kal: KalmanFilter) -> None:
3536
self._header = HeaderCalc('palm')
3637
self._gen = np.random.default_rng()
3738

38-
def callback(self, dist: Range,
39-
cam: RegionOfInterestWithCovarianceStamped):
40-
self.cam_info.wait_for_camera_info()
41-
cam_info_obj = self.cam_info.get_last_camera_info()
42-
cam_res = np.array([cam_info_obj.width, cam_info_obj.height])
43-
cam_focal = np.array([cam_info_obj.P[0], cam_info_obj.P[5]])
44-
39+
def callback(self, dist: Optional[Range],
40+
cam: Optional[RegionOfInterestWithConfidenceStamped]):
4541
try:
4642
dist_to_home = self.tf_get('palm', 'applevision_start_pos',
4743
rospy.Time(), rospy.Duration())
@@ -54,46 +50,8 @@ def callback(self, dist: Range,
5450
trans.transform.translation.x, trans.transform.translation.y,
5551
trans.transform.translation.z
5652
]))
57-
range_corrected = dist.range + self.kal.env.apple_r
58-
59-
# publish the kalman filters predicted apple distance
60-
# if bounding box is on the edge the variance is infinite
61-
if cam.w == 0 or cam.h == 0 or cam.x == 0 or cam.x + cam.w == cam_res[
62-
0]:
63-
x_est, p_est, var = self.kal.step_filter(
64-
(0, 0, 0, range_corrected), np.inf, np.inf, np.inf, control)
65-
else:
66-
# compute apple x, y based off of the bounding box width/height avg
67-
# TODO: how to fix this? it's unreliable
68-
# TODO: improve varience calculations
69-
# TODO: initialize robot in correct position
70-
# TODO: this is wrong
71-
est_z = cam_focal[0] * (2 * self.kal.env.apple_r) / cam.w
72-
center_x = cam.x + cam.w / 2 - cam_res[0] / 2
73-
center_y = cam.y + cam.h / 2 - cam_res[1] / 2
74-
est_x = center_x * est_z / cam_focal[0]
75-
est_y = center_y * est_z / cam_focal[1]
76-
77-
z_const = cam_focal[0] * (2 * self.kal.env.apple_r)
78-
z_fprime = z_const * -1 / cam.w**2
79-
z_fdoubleprime = z_const * 2 / cam.w**3
80-
z_ftrippleprime = z_const * -6 / cam.w**4
81-
z_var = second_order_var_approx(z_fprime, z_fdoubleprime,
82-
z_ftrippleprime, cam.w_var)
83-
84-
# assume x and w uncorrelated (probably not true)
85-
center_x_var = cam.x_var + 1 / 4 * cam.w_var
86-
center_y_var = cam.x_var + 1 / 4 * cam.w_var
87-
# https://stats.stackexchange.com/questions/52646/variance-of-product-of-multiple-independent-random-variables
88-
x_var = ((center_x_var + center_x**2) * (z_var + est_z**2) -
89-
(center_x * est_z)**2) * 1 / cam_focal[0]**2
90-
y_var = ((center_y_var + center_y**2) * (z_var + est_z**2) -
91-
(center_y * est_z)**2) * 1 / cam_focal[0]**2
92-
93-
# TODO: kalman filter will runaway sometimes?
94-
x_est, p_est, var = self.kal.step_filter(
95-
(est_x, est_y, est_z, range_corrected), x_var, y_var, z_var,
96-
control)
53+
54+
x_est, p_est, var = self.kal.step_filter(cam, dist, control)
9755

9856
trans_out = TransformStamped()
9957
trans_out.header = self._header.get_header()
@@ -107,8 +65,8 @@ def callback(self, dist: Range,
10765

10866
out = PointWithCovarianceStamped()
10967
out.header = self._header.get_header()
110-
out.camera_stamp = cam.header.stamp
111-
out.distance_stamp = dist.header.stamp
68+
out.camera_stamp = cam.header.stamp if cam else rospy.Time()
69+
out.distance_stamp = dist.header.stamp if dist else rospy.Time()
11270
out.point = x_est.tolist()
11371
out.covariance = p_est.flatten().tolist()
11472
self.p_out.publish(out)
@@ -126,6 +84,11 @@ def main():
12684
rospy.init_node('applevision_filter')
12785
rospy.wait_for_service('Tf2Transform')
12886

87+
rospy.loginfo('Waiting for camera info...')
88+
cam_info = CameraInfoHelper('palm_camera/camera_info')
89+
cam_info.wait_for_camera_info()
90+
rospy.loginfo('Got it! Starting...')
91+
12992
# TODO: Tune these
13093
env = EnvProperties(delta_t_ms=33,
13194
accel_std=0.005,
@@ -134,17 +97,24 @@ def main():
13497
z_std=.005,
13598
backdrop_dist=.5,
13699
apple_r=.040,
137-
dist_fov_rad=np.deg2rad(20))
138-
kal_filter = KalmanFilter(env, 1.5, 0.3, 0.75, 0.9)
100+
dist_fov_rad=np.deg2rad(20),
101+
camera_info=cam_info.get_last_camera_info())
102+
kal_filter = KalmanFilter(
103+
env,
104+
std_range=1.5,
105+
too_close_cam=0.3,
106+
too_far_dist=7,
107+
appl_proportion_low=0.75,
108+
appl_proportion_high=0.9)
139109
rospy.logdebug(
140110
f'Kalman filter using environment {env} and filter {kal_filter}.')
141111

142112
# input('Press any key to start Applevision...')
143-
main_proc = MainHandler('applevision/est_apple_pos', kal_filter)
113+
main_proc = FilterHandler('applevision/est_apple_pos', kal_filter)
144114
dist = Subscriber('applevision/apple_dist', Range)
145115
camera = Subscriber('applevision/apple_camera',
146-
RegionOfInterestWithCovarianceStamped)
147-
sync = ApproximateTimeSynchronizer([dist, camera], 10, 0.017)
116+
RegionOfInterestWithConfidenceStamped)
117+
sync = SynchronizerMinTick([dist, camera], queue_size=10, slop=0.017, min_tick=0.2)
148118
sync.registerCallback(main_proc.callback)
149119

150120
rospy.spin()

0 commit comments

Comments
 (0)