GitHub - wpumacay/RoboND-Rover-Project: Project repository for the Unity rover search and sample return project. (original) (raw)

Project: Search and Sample Return

Settings : 640x480 - Config : fantastic - FPS : 27

Test video : https://www.youtube.com/watch?v=p5QjsrKnGeM



1) Notebook Analysis

Image processing - color thresholding

As explained here, I added some functions to use HSV color thresholding to segmentate the rock samples. The code from the notebook is shown below.

HSV color thresholding

def hsv_thresh( img, lo, hi ) : _img_hsv = cv2.cvtColor( img, cv2.COLOR_BGR2HSV ) _res = cv2.inRange( _img_hsv, lo, hi ) return _res

First, we convert the image to HSV space, and then just pick the range given by lo and hi .

After some tuning, the lo and hi ranges to segment the samples were set to :

ranges for rock-sample segmentation

g_hsv_threshold_rock_lo = np.array( [ 50, 98, 150 ] ) g_hsv_threshold_rock_hi = np.array( [ 140, 255, 255 ] )

Below you can see the results of segmenting the rock samples using the previous method :

img img img img

For the navigable terrain and the obstacles I used rgb-thresholding as shown in the following snippet :

RGB color thresholding

def rgb_thresh( img, lo, hi ) : _res = cv2.inRange( img, lo, hi ) return _res

For the lo and hi ranges we picked the values from the lecture, and as suggested, just inverted the nav-terrain ranges to get the obstacle ranges. The values and the results are shown below.

ranges for obstacle segmentation

g_rgb_threshold_obstacles_lo = np.array( [ 0, 0, 0 ] ) g_rgb_threshold_obstacles_hi = np.array( [ 160, 160, 160 ] )

ranges for terrain segmentation

g_rgb_threshold_terrain_lo = np.array( [ 160, 160, 160 ] ) g_rgb_threshold_terrain_hi = np.array( [ 255, 255, 255 ] )

img

Image processing - full pipeline and worldmap creation

I filled the process_image method with all the methods involved in the image processing pipeline as requested. The implementation is in the notebook, and these are the parts involved.

1) Define source and destination points for perspective transform

_dst_size = 5
_btm_offset = 6
_w = img.shape[1]
_h = img.shape[0]
_src_pts = np.float32( [ [16, 141], [303 ,141],[201, 98], [120, 98] ] )
_dst_pts = np.float32( [ [ _w / 2 - _dst_size, _h - _btm_offset ],
[ _w / 2 + _dst_size, _h - _btm_offset ],
[ _w / 2 + _dst_size, _h - 2 * _dst_size - _btm_offset ],
[ _w / 2 - _dst_size, _h - 2 * _dst_size - _btm_offset ],
])
These are the mapping of the trapezoid in the calibration image ( which represents a 1mx1m square ) to a square of size 10px in rover coordinates ( as represented by _dst_size, which is half the size of the square ). There is also an offset that represents the distance between the square mapped and the actual position of the camera respect to the rover.
Once we have these mapping points we can call our perspective transform method :

2) Apply perspective transform

_img_warped = perspect_transform( img, _src_pts, _dst_pts )
The result of this step is an image like this :

perspective transform

3) Apply color threshold to identify navigable terrain/obstacles/rock samples

ranges for terrain segmentation

_rgb_threshold_terrain_lo = np.array( [ 160, 160, 160 ] )
_rgb_threshold_terrain_hi = np.array( [ 255, 255, 255 ] )

ranges for obstacle segmentation

_rgb_threshold_obstacles_lo = np.array( [ 0, 0, 0 ] )
_rgb_threshold_obstacles_hi = np.array( [ 160, 160, 160 ] )

ranges for rock segmentation

_hsv_threshold_rocks_lo = np.array( [ 20, 98, 40 ] )
_hsv_threshold_rocks_hi = np.array( [ 100, 255, 255 ] )
_img_threshed_terrain = rgb_thresh( _img_warped, _rgb_threshold_terrain_lo, _rgb_threshold_terrain_hi )
_img_threshed_obstacles = rgb_thresh( _img_warped, _rgb_threshold_obstacles_lo, _rgb_threshold_obstacles_hi )
_img_threshed_rocks = hsv_thresh( _img_warped, _hsv_threshold_rocks_lo, _hsv_threshold_rocks_hi )
This would give us three images to work with in the next stage. An example is shown below.

after thresholding state

camera_to_rover_transform

This pixels are used in the decision step to give the rover a direction to go ( terrain navigable pixels ) and the direction to a rock, if there is one ( rock pixels ) as you could see by the two arrows in the images above.

x_obstacles_world, y_obstacles_world = pix_to_world( x_obstacles_rover,
y_obstacles_rover,
_x, _y, _yaw,
_world_size,
_rover2world_scale )
x_rocks_world, y_rocks_world = pix_to_world( x_rocks_rover,
y_rocks_rover,
_x, _y, _yaw,
_world_size, _rover2world_scale )
Again, there is a scale factor in here, as each pixel represents a square of 1mx1m. Taking this into account and the fact that the pixels from the previous stage represent 0.1mx0.1m, we set the scale factor to 10. Also, the size of the world is given, which is also used in the transformation.
This would give us a result like the following :

rover_to_world

output_image[0:img.shape[0], 0:img.shape[1]] = img
warped = perspect_transform(img, source, destination)
output_image[0:img.shape[0], img.shape[1]:] = warped
map_add = cv2.addWeighted(data.worldmap, 1, data.ground_truth, 0.5, 0)
output_image[img.shape[0]:, 0:data.worldmap.shape[1]] = np.flipud(map_add)
Also, we send some other info to the simulator, namely the original frame from the rover camera, the warped image and the worldmap itself. each of these images is used to make a video using moviepy, as shown in the next image.

worldmap

2) Autonomous Navigation and Mapping

Perception and decision steps

2) Apply perspective transform

_img_warped = perspect_transform( Rover.img, _src_pts, _dst_pts )

3) Apply color threshold to identify navigable terrain/obstacles/rock samples

ranges for terrain segmentation

_rgb_threshold_terrain_lo = np.array( [ 160, 160, 160 ] )
_rgb_threshold_terrain_hi = np.array( [ 255, 255, 255 ] )

ranges for obstacle segmentation

_rgb_threshold_obstacles_lo = np.array( [ 0, 0, 0 ] )
_rgb_threshold_obstacles_hi = np.array( [ 160, 160, 160 ] )

ranges for rock segmentation

_hsv_threshold_rocks_lo = np.array( [ 50, 98, 150 ] )
_hsv_threshold_rocks_hi = np.array( [ 140, 255, 255 ] )
_img_threshed_terrain = rgb_thresh( _img_warped, _rgb_threshold_terrain_lo, _rgb_threshold_terrain_hi )
_img_threshed_obstacles = rgb_thresh( _img_warped, _rgb_threshold_obstacles_lo, _rgb_threshold_obstacles_hi )
_img_threshed_rocks = hsv_thresh( _img_warped, _hsv_threshold_rocks_lo, _hsv_threshold_rocks_hi )
#_img_threshed_rocks = hsv_thresh( Rover.img, _hsv_threshold_rocks_lo, _hsv_threshold_rocks_hi )
#_img_threshed_warped_rocks = perspect_transform( _img_threshed_rocks, _src_pts, _dst_pts )

4) Update Rover.vision_image (this will be displayed on left side of screen)

# Example: Rover.vision_image[:,:,0] = obstacle color-thresholded binary image  
#          Rover.vision_image[:,:,1] = rock_sample color-thresholded binary image  
#          Rover.vision_image[:,:,2] = navigable terrain color-thresholded binary image  

Rover.vision_image[:, :, 0] = _img_threshed_obstacles
Rover.vision_image[:, :, 1] = _img_threshed_rocks
Rover.vision_image[:, :, 2] = _img_threshed_terrain

5) Convert map image pixel values to rover-centric coords

x_navigable_rover, y_navigable_rover = rover_coords( _img_threshed_terrain )
x_obstacles_rover, y_obstacles_rover = rover_coords( _img_threshed_obstacles )
x_rocks_rover, y_rocks_rover = rover_coords( _img_threshed_rocks )

6) Convert rover-centric pixel values to world coordinates

_rover2world_scale = 10. # 1pix in world is 1m, whereas 1pix in rover is 0.1m
_world_size = 200
_x = Rover.pos[0]
_y = Rover.pos[1]
_yaw = Rover.yaw
x_navigable_world, y_navigable_world = pix_to_world( x_navigable_rover, y_navigable_rover,
_x, _y, _yaw, _world_size, _rover2world_scale )
x_obstacles_world, y_obstacles_world = pix_to_world( x_obstacles_rover, y_obstacles_rover,
_x, _y, _yaw, _world_size, _rover2world_scale )
x_rocks_world, y_rocks_world = pix_to_world( x_rocks_rover, y_rocks_rover,
_x, _y, _yaw, _world_size, _rover2world_scale )

7) Update Rover worldmap (to be displayed on right side of screen)

# Example: Rover.worldmap[obstacle_y_world, obstacle_x_world, 0] += 1  
#          Rover.worldmap[rock_y_world, rock_x_world, 1] += 1  
#          Rover.worldmap[navigable_y_world, navigable_x_world, 2] += 1  

_isRollOk = ( Rover.roll < 1 and Rover.roll >= 0 ) or ( Rover.roll <= 360 and Rover.roll > 359 )
_isPitchOk = ( Rover.pitch < 1 and Rover.pitch >= 0 ) or ( Rover.pitch <= 360 and Rover.pitch > 359 )
if _isRollOk and _isPitchOk :
Rover.worldmap[y_obstacles_world, x_obstacles_world, 0] += 1
Rover.worldmap[y_rocks_world, x_rocks_world, 1] += 1
Rover.worldmap[y_navigable_world, x_navigable_world, 2] += 1
The slight changed made was that the rover might bump into something, and this would make the perspective transform parameters used for the transformation invalid ( we were assuming the camera was always facing the world in the same angle ). To avoid this I followed the hint that they gave in the classroom of just updating the worldmap if the rover orientation is in some valid range ( pitch and roll ), as shown in the next snippet ( lines 170 to 176 ).
_isRollOk = ( Rover.roll < 1 and Rover.roll >= 0 ) or ( Rover.roll <= 360 and Rover.roll > 359 )
_isPitchOk = ( Rover.pitch < 1 and Rover.pitch >= 0 ) or ( Rover.pitch <= 360 and Rover.pitch > 359 )
if _isRollOk and _isPitchOk :
Rover.worldmap[y_obstacles_world, x_obstacles_world, 0] += 1
Rover.worldmap[y_rocks_world, x_rocks_world, 1] += 1
Rover.worldmap[y_navigable_world, x_navigable_world, 2] += 1

else :
Rover.sample_in_range['exists'] = False
Rover.sample_in_range['dir'] = 0

As you can see, it's a wrapper of some other functionality that is implemented in other classes. One of the main parts of the controller is its speed PID controller, which I used to make the rover navigate at a certain speed. For the direction I implemented a steer PID controller, but after some testing I ended up using just the raw average direction given by the perception step.
Then, the other main component is the ai component, which is a state machine in charge of the navigation logic. I shall explain each component in the following part.

The default parameters are the speed controller tunned parameters.

Basically, I make the rover start in the looking for path state. After the rover founds navigable terrain, then it switches to the forward state. If there is no navigable terrain ahead, then it switches from the forward to the braking state. From this state we wait until it stops and after that we go back to the looking for path state and start the same process again.

Launching in autonomous mode

Make a note of your simulator settings (resolution and graphics quality set on launch) and frames per second (FPS output to terminal by drive_rover.py) in your writeup when you submit the project so your reviewer can reproduce your results.**