/** @file path_follow.c @author Yves Hwang. @date 25/2/2002 @brief Path following program for the eyebot equipped with cameras. */ /**************************************** * -=| PATH FOLLOWING DEMO |=- * * * * The program follows a path on * * a board using the distinct * * colour/contrast differences to * * differentiate the path from * * other possible glitches. * * * * written by Yves Hwang * * hwang-y@ee.uwa.edu.au * * 25/2/2002 * * * *****************************************/ #include #include #include #define PROCESS_LOWER_ROW 47 /** The lower bound for the area of interest on the image*/ #define PROCESS_UPPER_ROW 25 /** The upper bound for the area of interest on the image*/ #define PIX_DIFF 70 /** The maximum amout of pixel difference to determine path on the image*/ #define LCD_DISPLAY_WIDTH 54 /*LCD display width for strings*/ #define LCD_STRING_ROW 6 /* maximum number of lines(characters) down the LCD display*/ #define LCD_STRING_COLUMN 15 /* maximum number of character across the LCD display*/ #define INVALID -1 /*Arbitrary value for an invalid co-ordinate*/ #define INFINITY 1000000 /*Arbitrary value for infinity*/ #define CENTRE_OF_SCREEN_X (imagecolumns-2)/2 /*The x co-ordinate value for the centre of screen*/ #define MAX_OFFSET 20 /*Maximum allowable offset before values read becomes unreliable*/ #define FORWARD_SPEED 0.03 /*Eyebot forward speed*/ #define TURN_SPEED 0.25 /*Eyebot turn speed*/ #define FORWARD_GRADIENT 1.5 /*Minimum gradient needed for forward motion*/ #define FORTY_FIVE_DEGREES 1.0 /*gradient of 45 degrees*/ #define DELTA_GRADIENT 0.5 /*Change in gradient*/ #define PI M_PI /*Definition of PI using the math library*/ #define MARGIN 10 /*Definition of the margin from the sides of the image*/ #define DELTA_RAD PI/8 /*Definition for the change (increment) in rad*/ typedef struct { int x; int y; }point; /*a point consist of co-ordinate x and y*/ typedef struct { point point_one; point point_two; float gradient; int offset; int direction; }path; /*a path consist of two points, a gradient, an offset from the centre of eyebot and a possible direction.*/ enum { SERVO_MIN_POSITION = 0, /**< enum value SERVO_MIN_POSITION. */ SERVO_MAX_POSITION = 255 /**< enum value SERVO_MAX_POSITION. */ }; enum { NO_EDGE =0, /**< enum value NO_EDGE.*/ ONE_EDGE=1, /**< enum value ONE_EDGE.*/ MORE_THAN_TWO_EDGS=2 /**< enum value MORE_THAN_TWO_EDGS.*/ }; enum{ UP =-1, /**< enum value UP.*/ DOWN=1 /**< enum value DOWN.*/ }; enum{ STRAIGHT = 0, /**< enum value STRAIGHT.*/ RIGHT=1, /**< enum value RIGHT.*/ LEFT=-1 /**< enum value LEFT.*/ }; #define INIT_CAM_POS 130 /*Initial camera position*/ #define DELTA_CAM_MOVEMENT 25 /*Camera position increment*/ static void initcam(int, ServoHandle *, int); static void release_cam(ServoHandle *); static void initVW(VWHandle *); static void release_VW(VWHandle *); static path path_determination(); static point point_detection(image *, int, int *, int *, int, int); static float calc_gradient(point *, point *); static int calc_path_offset(point *); static void path_follow(path *, VWHandle *); static int horizontal_detection(image *, int, int); /** * The main() for the path following demo. Initialisation of VW handle and camera servo is done here. * Then the path_follow() is called and the eyebot will follow the path according to the path layout. * The program exists when KEY4 is pressed. */ int main(void) { path path_a; ServoHandle cam_servo; VWHandle vw_handle; release_cam(&cam_servo); /*release before init to ensure proper functionality of the hardware.*/ release_VW(&vw_handle); initVW(&vw_handle); initcam(AUTOBRIGHTNESS, &cam_servo,INIT_CAM_POS ); LCDPrintf("PATH FOLLOWING\n\nYves Hwang 2002\n\n"); LCDPrintf("Press anykey\n"); while(KEYRead()==0); path_follow(&path_a, &vw_handle); release_cam(&cam_servo); release_VW(&vw_handle); return 0; } /** * The function is called when a path is determined and the eyebot needs to decide where it wants to go. * The fuction is called by main(). After vigorous testing and experimenting, the values for * FORWARD_SPEED and TURN_SPEED is then chosen. However, a possible improvement is to construct a * mathmatical model to determine the optimum speed and turning angle using the gradient and offset as * its constraints. The mathematical model may provide a greater accuracy, simplify the algoritm and * perhaps a smoother transition through the path. * @param path *p, VWHandle *vw_handle. * @return NONE. * @see main(). */ static void path_follow(path *p, VWHandle *vw_handle) { int prev_direction=STRAIGHT; double prev_gradient=0.0, rad=DELTA_RAD; LCDClear(); do{ *p=path_determination(); if(prev_gradient!= 0.0) rad=PI/4; /*reset the turning radians*/ if(p->gradient == 0.0 && p->offset == 0) { switch(prev_direction) { case STRAIGHT: /*possible horizontal path*/ VWSetSpeed(*vw_handle,0,0);/* 0 m per sec, 0 rad per sec*/ /*now the eyebot will have to choose which direction to turn and search for a horizontal path using previously recorded values*/ if(rad0) VWDriveTurn(*vw_handle, -rad, 5*FORWARD_SPEED); else VWDriveTurn(*vw_handle, rad, 5*FORWARD_SPEED); rad+=DELTA_RAD; VWDriveWait(*vw_handle); } break; case LEFT: VWSetSpeed(*vw_handle, FORWARD_SPEED, TURN_SPEED); break; case RIGHT: VWSetSpeed(*vw_handle, FORWARD_SPEED, -TURN_SPEED); break; } } if(abs(p->offset) < MAX_OFFSET)/*reliable reading of the path*/ { /* forward motion: gradient > FORWARD_GRADIENT * meterspersec: FORWARD_SPEED * radpersec: 0 */ if(abs(p->gradient) >FORWARD_GRADIENT) VWSetSpeed(*vw_handle, FORWARD_SPEED, 0); /* curve left and right: gradient > 45 degrees * meterspersec: FORWARD_SPEED * radpersec: FORWARD_SPEED/6 (optimum value) */ if(p->gradient > FORTY_FIVE_DEGREES) VWSetSpeed(*vw_handle, FORWARD_SPEED, -FORWARD_SPEED/6); if(p->gradient < -FORTY_FIVE_DEGREES) VWSetSpeed(*vw_handle, FORWARD_SPEED, FORWARD_SPEED/6); /* curve left and right * meterspersec: FORWARD_SPEED * radpersec: FORWARD_SPEED/5 (optimum value) */ if(p->gradient > FORTY_FIVE_DEGREES-DELTA_GRADIENT && p->gradient < FORTY_FIVE_DEGREES) VWSetSpeed(*vw_handle, FORWARD_SPEED, -FORWARD_SPEED/5); if(p->gradient < -FORTY_FIVE_DEGREES+DELTA_GRADIENT && p->gradient > -FORTY_FIVE_DEGREES) VWSetSpeed(*vw_handle, FORWARD_SPEED, FORWARD_SPEED/5); if(p->gradient > 0 && p->gradient < FORTY_FIVE_DEGREES-DELTA_GRADIENT) VWSetSpeed(*vw_handle, FORWARD_SPEED, -FORWARD_SPEED); if(p->gradient < 0 && p->gradient > -FORTY_FIVE_DEGREES+DELTA_GRADIENT) VWSetSpeed(*vw_handle, FORWARD_SPEED, FORWARD_SPEED); } else /*offset larger than max_offset: unreliable reading*/ { if(p->offset < 0 ) VWSetSpeed(*vw_handle, FORWARD_SPEED, FORWARD_SPEED); if(p->offset > 0 ) VWSetSpeed(*vw_handle, FORWARD_SPEED, -FORWARD_SPEED); } prev_gradient = p->gradient; /*remember the previous gradient*/ prev_direction= p->direction; /*remember the previous direction (horizontal)*/ }while(KEYRead()!=KEY4); } /** * The function is called by main() to initialise the VW handle. * @param VWHanel *vw_handle. * @return none. * @see main(). */ static void initVW(VWHandle *vw_handle) { *vw_handle = VWInit(VW_DRIVE, 1); if (*vw_handle == 0) { LCDPrintf("VWInit() error\n"); return; } VWStartControl(*vw_handle, 7, 0.3, 7, 0.1); /*optimum drive control*/ /* Vv: 7 (proportional component of the v-controller) * Tv: 0.3 (integrating component of the v-controller) * Vw: 7 (proportional component of the w-controller) * Tw: 0.1 (integrating component of the w-controller) */ } /** * The function is called by main() to release the VW handle. * @param VWHanel *vw_handle. * @return none. * @see main(). */ static void release_VW(VWHandle *vw_handle) { VWStopControl(*vw_handle); VWRelease(*vw_handle); } /** * The function is called by path_determination() to determine the offset of the path. * It calculates how far off the first point of the path is from the centre of the eyebot. * @param point *pt. * @return returns the offset as an int. * @see path_determination(). */ static int calc_path_offset(point *pt) { /*used to calculate how far is the eybot from the centre of path*/ if( pt->x == INVALID) return 0; /*horizontal gradient; eyebot is not on the path*/ return pt->x - CENTRE_OF_SCREEN_X; } /** * The function is called by path_determination() to determine the gradient of the path. * It calculates the gradient using two points on the path. * @param point *one, point *two. * @return returns the gradient as a float. * @see path_determination(). */ static float calc_gradient(point *one, point *two) { if ( one->x == INVALID || one->y == INVALID || two->x == INVALID || two->y == INVALID) return 0.0; /*horizontal gradient*/ else { if( ( two->x - one->x) == 0) return INFINITY; /*vertical asymtote*/ return ((float)(-two->y - -one->y)/(float)(two->x - one->x)); } /*the minus signs in front of the y co-ordinates help to translate the y value into the correct mathematical cartesian value.*/ } /** * The function is used to determine a possible direction for the eyebot. The value obtained using this * function will only be useful if the path has a 90 degrees sharp turn. This function is called in * path_determination(). * @param image *img, int margin_from_side, int pix_difference. * @return returns direciton as an int. * @see path_determination(). */ static int horizontal_detection(image *img, int margin_from_side, int pix_difference) { int i, column, left_edges[margin_from_side], right_edges[margin_from_side], right_edge, left_edge; double direction_left = 0.0, direction_right= 0.0; /* init to NO_EDGE*/ for(i=1;ipix_difference) { if(left_edges[column]==NO_EDGE) left_edges[column]=ONE_EDGE; if(left_edges[column]==ONE_EDGE) left_edges[column]=MORE_THAN_TWO_EDGS; } if( abs( (*img)[i][(imagecolumns-2)-column] - (*img)[i+1][(imagecolumns-2)-column]) >pix_difference) { if(right_edges[column]==NO_EDGE) right_edges[column]=ONE_EDGE; if(right_edges[column]==ONE_EDGE) right_edges[column]=MORE_THAN_TWO_EDGS; } } } for(i=1;i= (MORE_THAN_TWO_EDGS + ONE_EDGE)/2 ) left_edge = MORE_THAN_TWO_EDGS; if(direction_right>= (MORE_THAN_TWO_EDGS + ONE_EDGE)/2 ) right_edge = MORE_THAN_TWO_EDGS; /*to determine a possible horizontal/90 degrees turn..*/ if(left_edge==MORE_THAN_TWO_EDGS && right_edge!=MORE_THAN_TWO_EDGS) return LEFT; else if(right_edge==MORE_THAN_TWO_EDGS && left_edge!=MORE_THAN_TWO_EDGS) return RIGHT; else return STRAIGHT; } /** * The function is called to determine a point on the path in the captured image. The method used to * determine a point is of an edge detection method. A possible point in a path consists of two edges and * a path consists of at least two points. * @param image *img, int array, int *flag, int *edge_count, int pix_difference, int up_down. * @return returns direciton as an int. * @see path_determination(). */ static point point_detection(image *greyimg, int array, int *flag, int *edge_count, int pix_difference, int up_down) { point pt; int i; /* specified array is out of the valid range*/ if(array > PROCESS_LOWER_ROW || array < PROCESS_UPPER_ROW) { pt.x = INVALID; pt.y = INVALID; return pt; } for(i=1;i<(imagecolumns-2); i++) { /*edge detection; if the difference in pixel is greater than pix_difference*/ if( abs( (*greyimg)[array][i] - (*greyimg)[array][i+1]) >pix_difference) { switch(*flag) { case NO_EDGE: *flag=ONE_EDGE; pt.x=i+1; (*edge_count)=1; break; case ONE_EDGE: *flag=MORE_THAN_TWO_EDGS; pt.x+=i+1; (*edge_count)++; break; case MORE_THAN_TWO_EDGS: pt.x+=i+1; (*edge_count)++; break; } } } /*average the two edges to get the 1st pt on path in the lower bound*/ switch(*flag) { case NO_EDGE: /* recursive call until the specified array is out of the valid range*/ pt=point_detection(greyimg, array+up_down, flag, edge_count, pix_difference, up_down); break; case ONE_EDGE: if(pt.x==(imagecolumns-2)) /*if the screen is blank..*/ { pt.x = INVALID; pt.y= INVALID; } else pt.y=array; break; case MORE_THAN_TWO_EDGS: /* average the value of detected edges to reduce error*/ pt.x/=*edge_count; pt.y=array; break; } return pt; } /** * The function is called in path_follow() to determine all the attribute of a possible path. * @param none. * @return returns path. * @see path_follow(). */ static path path_determination(void) { path path_one; image greyimg, sobel_image; int lower_edge, upper_edge, lower_edge_count, upper_edge_count; lower_edge=NO_EDGE; upper_edge=NO_EDGE; CAMGetFrame(&greyimg); IPSobel(&greyimg, &sobel_image); LCDPutGraphic(&sobel_image); /*determine pt1 on lower bound*/ path_one.point_one=point_detection(&sobel_image, PROCESS_LOWER_ROW, &lower_edge, &lower_edge_count, PIX_DIFF, UP); /*determine pt2 on upper bound*/ path_one.point_two=point_detection(&sobel_image, PROCESS_UPPER_ROW, &upper_edge, &upper_edge_count, PIX_DIFF, DOWN); path_one.gradient=calc_gradient(&path_one.point_one, &path_one.point_two); path_one.offset = calc_path_offset(&path_one.point_one); LCDPrintf("(%d,%d) (%d,%d)\ngrad %0.2f offset %d\n" ,path_one.point_one.x,path_one.point_one.y, path_one.point_two.x, path_one.point_two.y, path_one.gradient, path_one.offset); path_one.direction = horizontal_detection(&sobel_image, MARGIN, PIX_DIFF); return path_one; } /** * The function is called by main() to initialise the camera and its servo. * @param int camera_mode, ServoHandle *camera_servo, int init_cam_position. * @return none. * @see main(). */ static void initcam(int camera_mode, ServoHandle *camera_servo, int init_cam_pos) { int camera, bright, hue, sat; /* Initialize servos for the camera*/ if(!(*camera_servo = SERVOInit(SERVO11))) LCDPrintf("CameraSerInit\nError!\n"); else SERVOSet(*camera_servo, init_cam_pos); camera = CAMInit(NORMAL); if (camera < COLCAM) LCDPrintf("No colour camera!"); else { if (camera == NOCAM) LCDPrintf("No camera!\n"); else if (camera == INITERROR) LCDPrintf("CAMInit!\n"); } if (camera_mode == AUTOBRIGHTNESS) CAMMode(AUTOBRIGHTNESS); else { LCDPrintf("no autobrightness\n"); CAMGet(&bright, &hue, &sat); CAMMode(NOAUTOBRIGHTNESS); CAMSet(135, 130, sat); /*ideal value for camera setting * brightness: 135 * offset: 130 */ } } /** * The fucntion is called in main() to release the camera and its servo handle. * @param ServoHandle *cam_servo. * @return NONE. */ static void release_cam(ServoHandle *cam_servo) { SERVORelease(*cam_servo); CAMRelease(); }