496 lines
16 KiB
Python
496 lines
16 KiB
Python
"""The class implementing high-level striker behavior."""
|
|
|
|
from __future__ import print_function
|
|
from __future__ import division
|
|
|
|
from threading import Thread
|
|
from math import pi, tan, asin, atan, radians
|
|
from time import sleep, time, strftime
|
|
from collections import deque
|
|
|
|
from .imagereaders import NaoImageReader
|
|
from .finders import BallFinder, GoalFinder, FieldFinder
|
|
from .movements import NaoMover
|
|
from naoqi import ALProxy
|
|
|
|
|
|
class Striker(object):
|
|
|
|
def __init__(self, nao_ip, nao_port, res, ball_hsv, goal_hsv, field_hsv,
|
|
ball_min_radius, do_capture=False):
|
|
"""For an example initialization see `__main__.py`
|
|
|
|
Parameters
|
|
----------
|
|
nao_ip : string
|
|
Address of the NAO robot.
|
|
nao_port : int
|
|
TCP port on which the AIProxy listens.
|
|
res : int
|
|
Nao camera resolution. 0, 1, 2 or 3, see `imagereaders.py` for
|
|
res description.
|
|
ball_hsv : list
|
|
Ball HSV parameters in format [[LH, LS, LV], [HH, HS, HV]]
|
|
goal_hsv : list
|
|
Goal HSV parameters in format [[LH, LS, LV], [HH, HS, HV]]
|
|
field_hsv : list
|
|
Field HSV parameters in format [[LH, LS, LV], [HH, HS, HV]]
|
|
ball_min_radius : float
|
|
Minimal radius of the ball, as a fraction of the frame height
|
|
(for example 0.01 is 1% of the frame height)
|
|
do_capture : bool
|
|
Record the POV video or not (don't use, it's gonna crash the
|
|
robot)
|
|
|
|
"""
|
|
|
|
# Maintenance
|
|
self.run_id = strftime('%Y%m%d%H%M%S')
|
|
self.is_over = False
|
|
self.last_goal = 'right'
|
|
self.doing_caputre = do_capture
|
|
|
|
# Motion
|
|
self.mover = NaoMover(nao_ip=nao_ip, nao_port=nao_port)
|
|
|
|
# Sight
|
|
self.upper_camera = NaoImageReader(
|
|
nao_ip, port=nao_port, res=res, fps=10, cam_id=0,
|
|
)
|
|
self.lower_camera = NaoImageReader(
|
|
nao_ip, port=nao_port, res=1, fps=10, cam_id=1,
|
|
)
|
|
|
|
# POV
|
|
if do_capture:
|
|
self.upper_pov = NaoImageReader(
|
|
nao_ip, port=nao_port, res=1, fps=10, cam_id=0,
|
|
video_file='./cam0_' + self.run_id + '.avi'
|
|
)
|
|
|
|
self.lower_pov = NaoImageReader(
|
|
nao_ip, port=nao_port, res=1, fps=10, cam_id=1,
|
|
video_file='./cam1_' + self.run_id + '.avi'
|
|
)
|
|
self.pov_thread = Thread(target=self._pov)
|
|
self.pov_thread.start()
|
|
|
|
# Recognition
|
|
self.ball_finder = BallFinder(tuple(ball_hsv[0]), tuple(ball_hsv[1]),
|
|
ball_min_radius)
|
|
self.field_finder = FieldFinder(tuple(field_hsv[0]),
|
|
tuple(field_hsv[1]))
|
|
self.goal_finder = GoalFinder(tuple(goal_hsv[0]), tuple(goal_hsv[1]))
|
|
|
|
# Talking
|
|
self.player = ALProxy('ALAudioPlayer', bytes(nao_ip), nao_port)
|
|
self.tts = ALProxy('ALTextToSpeech', bytes(nao_ip), nao_port)
|
|
self.speach_queue = deque(maxlen=4)
|
|
self.speach_history = []
|
|
self.tts_thread = Thread(target=self._speaker)
|
|
self.tts_thread.start()
|
|
|
|
# Debugging
|
|
self._timer_start = 0
|
|
self._timer_stop = 0
|
|
|
|
def close(self):
|
|
"""Finish everyting and rest."""
|
|
self.is_over = True
|
|
|
|
if self.tts_thread.isAlive():
|
|
self.tts_thread.join()
|
|
if self.doing_caputre and self.pov_thread.isAlive():
|
|
self.pov_thread.join()
|
|
self.upper_pov.close()
|
|
self.lower_pov.close()
|
|
|
|
self.upper_camera.close()
|
|
self.lower_camera.close()
|
|
self.mover.stop_moving()
|
|
|
|
def _speaker(self):
|
|
"""Internal method: don't use."""
|
|
while not self.is_over:
|
|
while self.speach_queue:
|
|
text = self.speach_queue.pop()
|
|
if text in ('hasta', 'tiger'):
|
|
file_id = self.player.loadFile(
|
|
'/home/nao/audio/' + text + '.mp3'
|
|
)
|
|
self.player.play(file_id)
|
|
else:
|
|
self.tts.say(text)
|
|
sleep(0.5)
|
|
|
|
def _pov(self):
|
|
"""Don't use."""
|
|
while not self.is_over:
|
|
try:
|
|
self.upper_pov.get_frame()
|
|
self.lower_pov.get_frame()
|
|
sleep(0.1)
|
|
except RuntimeError as e:
|
|
print(e)
|
|
continue
|
|
|
|
def speak(self, text):
|
|
"""Say stuff (useful for debugging)."""
|
|
if not self.speach_history or self.speach_history[-1] != text:
|
|
self.speach_queue.appendleft(text)
|
|
self.speach_history.append(text)
|
|
|
|
def scan_rotation(self):
|
|
"""Intelligently rotate the robot to search for stuff."""
|
|
self.mover.stop_moving()
|
|
self.rotating = False
|
|
yaw, pitch = self.mover.get_head_angles()
|
|
|
|
# determine direction of head rotation
|
|
sign = 1 if yaw >= 0 else -1
|
|
|
|
# the robot starts to move arround his z-Axis in the direction where
|
|
# his head is aligned when the head yaw angle has reached his maximum
|
|
if yaw > pi/3:
|
|
self.mover.set_head_angles(-pi / 8, pitch, 0.5)
|
|
sleep(0.5)
|
|
elif yaw < -pi/3:
|
|
self.mover.move_to(0, 0, -pi / 4)
|
|
self.mover.wait()
|
|
# rotate head to the left, if head yaw angle is equally zero or larger
|
|
# rotate head to the right, if head yaw angle is smaller than zero
|
|
else:
|
|
self.mover.change_head_angles(sign * pi / 8, 0, 0.5)
|
|
sleep(0.3)
|
|
|
|
def get_ball_angles_from_camera(self, cam, mask=True):
|
|
"""Detect the ball and return its angles in camera coordinates.
|
|
|
|
Parameters
|
|
----------
|
|
cam : NaoImageReader
|
|
A camera object form which to read
|
|
mask : bool
|
|
Mask out the field or not
|
|
|
|
Returns
|
|
-------
|
|
tuple or None
|
|
(x, y) coordinates in camera angles in radians or None if ball
|
|
not found
|
|
|
|
"""
|
|
try:
|
|
frame = cam.get_frame()
|
|
except RuntimeError as e: # Sometimes camera doesn't return an image
|
|
print(e)
|
|
return None
|
|
|
|
if cam == self.lower_camera:
|
|
mask = False
|
|
|
|
if mask:
|
|
field = self.field_finder.find(frame)
|
|
frame = self.field_finder.mask_it(frame, field)
|
|
ball = self.ball_finder.find(frame)
|
|
|
|
if ball is None:
|
|
return None
|
|
|
|
(x, y), _ = ball
|
|
x, y = cam.to_relative(x, y)
|
|
x, y = cam.to_angles(x, y)
|
|
return x, y
|
|
|
|
def get_goal_angles_from_camera(self, cam, mask=True):
|
|
"""Get the goal left post, center and right post.
|
|
|
|
Parameters
|
|
----------
|
|
cam : NaoImageReader
|
|
A camera object from which to get frames.
|
|
mask : bool
|
|
Mask out the field or not.
|
|
|
|
Returns
|
|
-------
|
|
tuple or None
|
|
(left, right, center) camera angles of goal in radians or None
|
|
if not found
|
|
|
|
"""
|
|
try:
|
|
frame = cam.get_frame()
|
|
except RuntimeError as e: # Sometimes camera doesn't return an image
|
|
print(e)
|
|
return None
|
|
|
|
if mask:
|
|
field = self.field_finder.find(frame)
|
|
frame = self.field_finder.mask_it(frame, field, inverse=True)
|
|
|
|
goal = self.goal_finder.find(frame)
|
|
if goal is None:
|
|
return None
|
|
|
|
goal_l, goal_r = self.goal_finder.left_right_post(goal)
|
|
goal_c = self.goal_finder.goal_center(goal)
|
|
|
|
goal_l, _ = cam.to_relative(goal_l, 0)
|
|
goal_l, _ = cam.to_angles(goal_l, 0)
|
|
goal_r, _ = cam.to_relative(goal_r, 0)
|
|
goal_r, _ = cam.to_angles(goal_r, 0)
|
|
goal_c, _ = cam.to_relative(goal_c, 0)
|
|
goal_c, _ = cam.to_angles(goal_c, 0)
|
|
return goal_l, goal_r, goal_c
|
|
|
|
def distance_to_ball(self):
|
|
"""Measure the distance to the ball in meters.
|
|
|
|
Raises
|
|
------
|
|
ValueError
|
|
The ball could not be seen :(
|
|
|
|
"""
|
|
camera = 'upper'
|
|
angles = self.get_ball_angles_from_camera(self.upper_camera)
|
|
if angles is None:
|
|
camera = 'lower'
|
|
angles = self.get_ball_angles_from_camera(self.lower_camera)
|
|
if angles is None:
|
|
raise ValueError('No ball in sight')
|
|
y_angle = angles[1]
|
|
y_angle = pi/2 - y_angle - radians(16.5) - (radians(39)
|
|
if camera == 'lower'
|
|
else 0)
|
|
print('Ball distance through', camera, 'camera')
|
|
print('Angles', angles)
|
|
return 0.5 * tan(y_angle)
|
|
|
|
def walking_direction(self, lr, d, hypo):
|
|
return (asin(0.43 / d) if hypo == 'bdist' else atan(0.23 / d)) * lr
|
|
|
|
def ball_tracking(self, soll=0, tol=0.15):
|
|
"""Turn the robot to the ball, find the ball if cannot see it.
|
|
Call this until this returns True - then the robot is facing
|
|
the ball.
|
|
|
|
This will block until the robot sees the ball and is facing the ball.
|
|
|
|
Parameters
|
|
----------
|
|
soll : float
|
|
X-Angle in radians, under which the ball should appear in the
|
|
robot's camera (maybe we want the robot not to look at the ball
|
|
directly).
|
|
tol : float
|
|
Allowable deviation in radians from soll-angle.
|
|
|
|
"""
|
|
|
|
ball_locked = False
|
|
tried_step_back = False
|
|
while not ball_locked:
|
|
# visibility check
|
|
for i in range(3):
|
|
cams = [self.lower_camera, self.upper_camera]
|
|
in_sight = False
|
|
|
|
for cam in cams:
|
|
ball_angles = self.get_ball_angles_from_camera(cam)
|
|
if ball_angles is not None:
|
|
x, y = ball_angles
|
|
self._timer_start = time()
|
|
in_sight = True
|
|
|
|
break
|
|
if in_sight:
|
|
break
|
|
# stop visibility check
|
|
|
|
if not in_sight:
|
|
if not tried_step_back:
|
|
self.mover.move_to(-0.1, 0, 0)
|
|
self.mover.wait()
|
|
self.mover.stand_up()
|
|
tried_step_back = True
|
|
else:
|
|
self.scan_rotation()
|
|
continue
|
|
|
|
ball_locked = self.turn_to_ball(x, y, soll=soll, tol=tol)
|
|
print()
|
|
|
|
def run_to_ball(self, d):
|
|
"""Fancy name for GO FORWARD."""
|
|
self.mover.move_to(d, 0, 0)
|
|
# self.mover.wait()
|
|
|
|
def turn_to_ball(self, ball_x, ball_y, tol=0.15, soll=0):
|
|
"""Align robot to the ball, assuming the ball is visible.
|
|
|
|
This method is mainly used by `turn_to_ball`.
|
|
|
|
If head is not centered at the ball (within tolerance), then
|
|
turn head to ball. If after that the angle of head to body
|
|
becomes too big, rotate the body by the head angle and
|
|
simultaneously rotate head into 0 position to achieve alignment.
|
|
|
|
Parameters
|
|
----------
|
|
soll : float
|
|
X-Angle in radians, under which the ball should appear in the
|
|
robot's camera (maybe we want the robot not to look at the ball
|
|
directly).
|
|
tol : float
|
|
Allowable deviation in radians from soll-angle.
|
|
|
|
"""
|
|
|
|
# only the x ball angle is relevant for alignment
|
|
d_yaw, d_pitch = ball_x, 0
|
|
print('ball yaw in camera:', d_yaw)
|
|
|
|
# center head at the ball
|
|
if (abs(d_yaw) > 0.01):
|
|
self.mover.change_head_angles(d_yaw, d_pitch,
|
|
min(1, abs(d_yaw) * 1.25))
|
|
sleep(0.1)
|
|
|
|
head_yaw, head_pitch = self.mover.get_head_angles()
|
|
self._timer_stop = time()
|
|
print('Ball to head', self._timer_stop - self._timer_start)
|
|
print('Head yaw', head_yaw, end=' ')
|
|
d_yaw = head_yaw - soll
|
|
print('Head d_yaw', d_yaw)
|
|
print('Allowed tolerance', tol)
|
|
|
|
if abs(d_yaw) > tol:
|
|
self.mover.stop_moving()
|
|
print('Going to rotate by', d_yaw)
|
|
self.mover.set_head_angles(soll, head_pitch, 0.3)
|
|
self.mover.move_to(0, 0, d_yaw)
|
|
self.mover.wait()
|
|
return False
|
|
else:
|
|
print('Ball locked')
|
|
return True
|
|
|
|
def align_to_ball(self):
|
|
"""Perform one step of aligning behind the ball for the kick.
|
|
|
|
Call this until this returns True.
|
|
|
|
Returns
|
|
-------
|
|
bool
|
|
True if nicely aligned else False.
|
|
"""
|
|
ball_angles = self.get_ball_angles_from_camera(self.lower_camera,
|
|
mask=False)
|
|
if ball_angles is None:
|
|
raise ValueError('No ball')
|
|
x, y = ball_angles
|
|
goal_x, goal_y = 0.092, 0.38
|
|
dx, dy = goal_x - x, goal_y - y
|
|
|
|
dx = -dx * 0.2 if abs(dx) > 0.03 else 0
|
|
dy = dy * 0.3 if abs(dy) > 0.05 else 0
|
|
if dx == 0 and dy == 0:
|
|
return True
|
|
print('Moving to dxdy', dx, dy)
|
|
self.mover.move_to(dy, dx, 0)
|
|
self.mover.wait()
|
|
return False
|
|
|
|
def align_to_goal(self):
|
|
"""Perform one step of aligning ball and goal.
|
|
|
|
Call this until this returns True.
|
|
|
|
Returns
|
|
-------
|
|
bool
|
|
True if nicely aligned else False.
|
|
"""
|
|
ball_angles = self.get_ball_angles_from_camera(self.lower_camera,
|
|
mask=False)
|
|
if ball_angles is None:
|
|
self.mover.move_to(-0.1, 0, 0)
|
|
self.mover.wait()
|
|
ball_angles = self.get_ball_angles_from_camera(self.lower_camera,
|
|
mask=False)
|
|
if ball_angles is None:
|
|
raise ValueError('No ball')
|
|
x, y = ball_angles
|
|
|
|
print(x, y)
|
|
self.turn_to_ball(x, y, tol=0.15)
|
|
sleep(0.2)
|
|
|
|
goal = self.goal_search()
|
|
if goal is None:
|
|
return False
|
|
|
|
gcl, gcr, gcc = goal
|
|
print('Goal:', gcl, gcr, gcc)
|
|
|
|
if gcl > 0.15 > -0.22 > gcr:
|
|
return True
|
|
|
|
if y > 0.38:
|
|
self.mover.move_to(-0.05, 0, 0)
|
|
self.mover.wait()
|
|
# return False
|
|
elif y < 0.28:
|
|
self.mover.move_to(0.05, 0, 0)
|
|
self.mover.wait()
|
|
# return False
|
|
|
|
sign = -1 if gcc > 0 else 1
|
|
if sign == 1:
|
|
self.speak('Goal is on the right')
|
|
elif sign == -1:
|
|
self.speak('Goal is on the left')
|
|
|
|
for _ in range(2):
|
|
self.mover.move_to(0, 0.05 * sign, 0)
|
|
self.mover.wait()
|
|
return False
|
|
|
|
def goal_search(self):
|
|
"""Find the goal. Return the angles in the robot frame.
|
|
|
|
Returns
|
|
-------
|
|
The same as the `get_goal_angles_from_camera`.
|
|
|
|
"""
|
|
self.speak('Searching for goal')
|
|
print('Last goal:', self.last_goal)
|
|
goal_angles = None
|
|
positions = [0, pi/6, pi/4, pi/3, pi/2]
|
|
direction = 1 if self.last_goal == 'right' else -1
|
|
angles = [-p for p in positions] + [p for p in positions][1:]
|
|
angles = [a * direction for a in angles]
|
|
|
|
for angle in angles:
|
|
self.mover.set_head_angles(angle, -0.3)
|
|
sleep(0.5)
|
|
for i in range(5):
|
|
print(i, goal_angles)
|
|
goal_angles = self.get_goal_angles_from_camera(
|
|
self.upper_camera
|
|
)
|
|
if goal_angles is not None:
|
|
goal_angles = tuple(gc + angle for gc in goal_angles)
|
|
self.mover.set_head_angles(0, 0)
|
|
print('Goal found:', str(goal_angles))
|
|
self.last_goal = 'left' if goal_angles[2] > 0 else 'right'
|
|
return goal_angles
|
|
print('Goal not found at ', str(angle))
|
|
|
|
self.mover.set_head_angles(0, 0)
|
|
return None
|