Пример #1
0
class WallFollow():
    def __init__(self, direction):
        if direction not in [RIGHT, LEFT]:
            rospy.loginfo("incorect %s wall selected.  choose left or right")
            rospy.signal_shutdown()

        self.direction = direction

        rospy.loginfo("To stop TurtleBot CTRL + C")
        rospy.on_shutdown(self.shutdown)

        if SHOW_VIS:
            self.viz = DynamicPlot()
            self.viz.initialize()

        self.sub = rospy.Subscriber("/scan",
                                    LaserScan,
                                    self.lidarCB,
                                    queue_size=1)
        self.cmd_vel_pub = rospy.Publisher('/cmd_vel_mux/input/wall_follower',
                                           Twist,
                                           queue_size=10)

        if PUBLISH_LINE:
            self.line_pub = rospy.Publisher("/viz/line_fit",
                                            PolygonStamped,
                                            queue_size=1)

        # computed control instructions
        self.control = None
        self.steering_hist = CircularArray(HISTORY_SIZE)

        # containers for laser scanner related data
        self.data = None
        self.xs = None
        self.ys = None
        self.m = 0
        self.c = 0

        # flag to indicate the first laser scan has been received
        self.received_data = False

        # flag for start/stop following
        self.follow_the_wall = False

        # cached constants
        self.min_angle = None
        self.max_angle = None
        self.direction_muliplier = 0
        self.laser_angles = None

        self.drive_thread = Thread(target=self.apply_control)
        self.drive_thread.start()

        if SHOW_VIS:
            while not rospy.is_shutdown():
                self.viz_loop()
                rospy.sleep(0.1)

    def stop(self):
        self.follow_the_wall = False

    def start(self):
        self.follow_the_wall = True

    def publish_line(self):
        # find the two points that intersect between the fan angle lines and the found y=mx+c line
        x0 = self.c / (np.tan(FAN_ANGLE) - self.m)
        x1 = self.c / (-np.tan(FAN_ANGLE) - self.m)

        y0 = self.m * x0 + self.c
        y1 = self.m * x1 + self.c

        poly = Polygon()
        p0 = Point32()
        p0.y = x0
        p0.x = y0

        p1 = Point32()
        p1.y = x1
        p1.x = y1
        poly.points.append(p0)
        poly.points.append(p1)

        polyStamped = PolygonStamped()
        polyStamped.header.frame_id = "base_link"
        polyStamped.polygon = poly

        self.line_pub.publish(polyStamped)

    def drive_straight(self):
        while not rospy.is_shutdown():
            move_cmd = Twist()
            move_cmd.linear.x = 0.1
            move_cmd.angular.z = 0

            self.cmd_vel_pub.publish(move_cmd)

            # don't spin too fast
            rospy.sleep(1.0 / PUBLISH_RATE)

    def apply_control(self):
        while not rospy.is_shutdown():
            if self.control is None:
                #print "No control data"
                rospy.sleep(0.5)
            else:
                if self.follow_the_wall:
                    self.steering_hist.append(self.control[0])
                    # smoothed_steering = self.steering_hist.mean()
                    smoothed_steering = self.steering_hist.median()

                    # print smoothed_steering, self.control[0]

                    move_cmd = Twist()
                    move_cmd.linear.x = self.control[1]
                    move_cmd.angular.z = smoothed_steering

                    self.cmd_vel_pub.publish(move_cmd)

                rospy.sleep(1.0 / PUBLISH_RATE)

    # given line parameters cached in the self object, compute the pid control
    def compute_pd_control(self):
        if self.received_data:
            # given the computed wall slope, compute theta, avoid divide by zero error
            if np.abs(self.m) < EPSILON:
                theta = np.pi / 2.0
                x_intercept = 0
            else:
                theta = np.arctan(1.0 / self.m)
                # solve for y=0 in y=mx+c
                x_intercept = self.c / self.m

            # x axis is perp. to robot but not perpindicular to wall
            # cosine term solves for minimum distance to wall
            wall_dist = np.abs(np.cos(theta) * x_intercept)

            # control proportional to angular error and distance from wall
            distance_term = self.direction_muliplier * KP * (wall_dist -
                                                             TARGET_DISTANCE)
            angle_term = KD * theta
            control = angle_term + distance_term
            # avoid turning too sharply
            self.control = (np.clip(control, -0.1, 0.1), SPEED)

    def fit_line(self):
        if self.received_data and self.xs.shape[0] > 0:
            # fit line to euclidean space laser data in the window of interest
            slope, intercept, r_val, p_val, std_err = stats.linregress(
                self.xs, self.ys)
            self.m = slope
            self.c = intercept

    # window the data, compute the line fit and associated control
    def lidarCB(self, msg):
        if not self.received_data:
            rospy.loginfo("success! first message received")

            # populate cached constants
            if self.direction == RIGHT:
                center_angle = -math.pi / 2
                self.direction_muliplier = -1
            else:
                center_angle = math.pi / 2
                self.direction_muliplier = 1

            self.min_angle = center_angle - FAN_ANGLE
            self.max_angle = center_angle + FAN_ANGLE
            self.laser_angles = (np.arange(len(msg.ranges)) *
                                 msg.angle_increment) + msg.angle_min

        # filter lidar data to clean it up and remove outlisers
        self.received_data = True

        if self.follow_the_wall:

            self.data = msg.ranges
            tmp = np.array(msg.ranges)

            # invert lidar(flip mounted)
            values = tmp[::-1]
            #values = tmp

            # remove out of range values
            ranges = values[(values > msg.range_min)
                            & (values < msg.range_max)]
            angles = self.laser_angles[(values > msg.range_min)
                                       & (values < msg.range_max)]

            # apply median filter to clean outliers
            filtered_ranges = signal.medfilt(ranges, MEDIAN_FILTER_SIZE)

            # apply a window function to isolate values to the side of the car
            window = (angles > self.min_angle) & (angles < self.max_angle)
            filtered_ranges = filtered_ranges[window]
            filtered_angles = angles[window]

            # convert from polar to euclidean coordinate space
            self.ys = filtered_ranges * np.cos(filtered_angles)
            self.xs = filtered_ranges * np.sin(filtered_angles)

            self.fit_line()
            self.compute_pd_control()

            if PUBLISH_LINE:
                self.publish_line()

            if SHOW_VIS:
                # cache data for development visualization
                self.filtered_ranges = filtered_ranges
                self.filtered_angles = filtered_angles

    def viz_loop(self):
        if self.received_data == True:
            self.viz.laser_angular.set_data(self.laser_angles, self.data)
            self.viz.laser_filtered.set_data(self.filtered_angles,
                                             self.filtered_ranges)
            self.viz.laser_euclid.set_data(self.xs, self.ys)
            self.viz.laser_regressed.set_data(self.xs,
                                              self.m * self.xs + self.c)
            self.viz.redraw()

    def shutdown(self):
        rospy.loginfo("Stop BigFoot")
        self.cmd_vel_pub.publish(Twist())
        rospy.sleep(1)