00001 /* --------------------------------------------------------------------------- 00002 Phission : 00003 Realtime Vision Processing System 00004 00005 Copyright (C) 2003 Philip D.S. Thoren (pthoren@cs.uml.edu) 00006 University of Massachusetts at Lowell, 00007 Laboratory for Artificial Intelligence and Robotics 00008 00009 This file is part of Phission. 00010 00011 ---------------------------------------------------------------------------*/ 00012 #include <phSimpleVisionTest.h> 00013 #include <phission.h> 00014 #include <fcntl.h> 00015 #include <errno.h> 00016 00017 /* ------------------------------------------------------------------------ */ 00018 int glbl_disable_displays = 0; 00019 int glbl_usenet_displays = 0; 00020 int glbl_demo_filters = 0; 00021 int glbl_print_histogram = 0; 00022 int glbl_print_blobs = 0; 00023 int glbl_color_count = 0; 00024 int glbl_test = 0; 00025 00026 /* ------------------------------------------------------------------------ */ 00027 void usage() 00028 { 00029 printf("\n\n\tUsage:\n"); 00030 printf("\t\t\t--help\t\t\tdisplay usage\n"); 00031 printf("\t\t\t--blobs\t\t\tenable the printing of blob data\n"); 00032 printf("\t\t\t--hists\t\t\tenable the printing of hist data\n"); 00033 printf("\t\t\t--net\t\t\tdisable normal displays and use only NetDisplay functionality.\n"); 00034 printf("\t\t\t--demo\t\t\tdemo all the filters by iterating through them; train and track are last.\n"); 00035 printf("\t\t\t--nodisplay\tdisable the allocation, opening or any use of a display.\n"); 00036 printf("\t\t\t--test <value>\tsleep 'value' seconds between filters and then close test\n"); 00037 printf("\n\n"); 00038 00039 exit(1); 00040 } 00041 00042 /* ------------------------------------------------------------------------ */ 00043 /* This allows me to set the non blocking flag on stdin so I can check to 00044 * see if the user has closed the displays. We can exit that way easily */ 00045 /* ------------------------------------------------------------------------ */ 00046 int set_blocking(FILE *f, int block) 00047 { 00048 phFUNCTION("set_blocking") 00049 int flags = 0; 00050 00051 flags = fcntl(fileno(f),F_GETFL,0); 00052 phCHECK_RC(flags,"fcntl:F_GETFL","fcntl failed to get fd flags"); 00053 00054 if (block) 00055 flags &- ~O_NONBLOCK; 00056 else 00057 flags |= O_NONBLOCK; 00058 00059 rc = fcntl(fileno(f),F_SETFL,flags); 00060 phCHECK_RC(rc,"fcntl:F_SETFL","fcntl failed to set fd flags"); 00061 00062 return phSUCCESS; 00063 error: 00064 return phFAIL; 00065 } 00066 00067 /* ------------------------------------------------------------------------ */ 00068 int wait_for(int wait,phDisplayInterface *d) 00069 { 00070 phFUNCTION("wait_for_user") 00071 int retrc = phSUCCESS; 00072 char byte = 0; 00073 00074 if (d == NULL) 00075 { 00076 #if !defined(WIN32) 00077 if (!wait) 00078 { 00079 rc = set_blocking(stdin,0); /* 0 : don't block */ 00080 phPRINT_RC(rc,NULL,"set_blocking failed."); 00081 } 00082 #endif 00083 00084 do 00085 { 00086 errno = 0; 00087 byte = getc(stdin); 00088 if (byte == -1) 00089 { 00090 if (errno == EAGAIN) 00091 { 00092 retrc = 1; 00093 phMSleep(1); 00094 } 00095 else 00096 { 00097 retrc = phFAIL; 00098 phPRINT_RC(byte,"getc","getc failed."); 00099 } 00100 } 00101 else if (byte == 'c') 00102 { 00103 retrc = phSUCCESS; 00104 } 00105 else// if (byte == '\n') 00106 { 00107 retrc = 1; 00108 } 00109 } while ((retrc == 1) && (wait == 1)); 00110 00111 rc = set_blocking(stdin,1); 00112 phPRINT_RC(rc,NULL,"set_blocking failed."); 00113 } 00114 else if (d != NULL) 00115 { 00116 do 00117 { 00118 if (d->isOpen()) 00119 { 00120 retrc = 1; 00121 } 00122 else 00123 { 00124 retrc = phSUCCESS; 00125 } 00126 } while ((retrc == 1) && (wait)); 00127 00128 if (retrc == phSUCCESS) 00129 { 00130 rc = d->open(); 00131 phCHECK_RC(rc,NULL,"d->open()"); 00132 } 00133 } 00134 00135 return retrc; 00136 error: 00137 return phFAIL; 00138 } 00139 00140 /* ------------------------------------------------------------------------ */ 00141 void print_menu() 00142 { 00143 phPRINT("\n 1.) empty | e\n"); 00144 phPRINT(" 2.) motion| m \n"); 00145 phPRINT(" 3.) canny | c\n"); 00146 phPRINT(" 4.) sobel | s\n"); 00147 phPRINT(" 5.) gaussian | g\n"); 00148 phPRINT(" 6.) mean\n"); 00149 phPRINT(" 7.) median | med\n"); 00150 phPRINT(" 8.) inverse | i\n"); 00151 phPRINT(" 9.) threshold | t\n"); 00152 phPRINT("10.) add | a\n"); 00153 phPRINT("11.) subtract | sub\n\n"); 00154 phPRINT("12.) quit | q\n\n"); 00155 phPRINT("Enter your choice: "); 00156 } 00157 00158 /* ------------------------------------------------------------------------ */ 00159 int demo_filters(phSimpleVision *vision) 00160 { 00161 phFUNCTION("demo_filters") 00162 int32_t done = 0; 00163 int32_t do_print_menu = 1; 00164 char *ptr = NULL; 00165 char command[255]; 00166 uint32_t command_length = 253; 00167 00168 uint32_t loop_count = 0; 00169 char *commands[] = { "empty ", 00170 "motion ", 00171 "canny ", 00172 "sobel ", 00173 "gaussian ", 00174 "mean ", 00175 "median ", 00176 "inverse ", 00177 "threshold ", 00178 "add ", 00179 "subtract ", 00180 "quit " }; 00181 uint32_t num_commands = sizeof(commands) / sizeof(char *); 00182 00183 phDisplayInterface *d1 = NULL; 00184 phDisplayInterface *d2 = NULL; 00185 00186 if (!glbl_disable_displays) 00187 { 00188 d1 = vision->getDisplay(phSVO_Capture); 00189 phCHECK_PTR(d1,NULL,"vision->getDisplay(phSVO_Capture)"); 00190 00191 d2 = vision->getDisplay(phSVO_Pipeline); 00192 phCHECK_PTR(d2,NULL,"vision->getDisplay(phSVO_Pipeline)"); 00193 } 00194 00195 set_blocking(stdin,0); 00196 while (!done) 00197 { 00198 /* Print the menu */ 00199 if (do_print_menu) 00200 { 00201 print_menu(); 00202 do_print_menu = 0; 00203 } 00204 00205 /* If we're testing, then cycle through the commands */ 00206 if (glbl_test) 00207 { 00208 snprintf(command,255,"%s",commands[loop_count]); 00209 loop_count++; 00210 if (loop_count > num_commands) loop_count = 0; 00211 } 00212 /* otherwise get the input from the user */ 00213 else 00214 { 00215 errno = 0; 00216 ptr = fgets(command,command_length,stdin); 00217 if (ptr == NULL) 00218 { 00219 if ((errno == 0) || (errno == EAGAIN)) 00220 { 00221 if (!glbl_disable_displays) 00222 { 00223 if ((!d1->isOpen()) && (!d2->isOpen())) 00224 { 00225 done = 1; 00226 } 00227 } 00228 phMSleep(100); 00229 continue; 00230 } 00231 else 00232 { 00233 phCONT_RC(-1,"fgets","fgets(command,command_length,stdin)"); 00234 } 00235 } 00236 } 00237 00238 if (strlen(command) > 0) 00239 { 00240 command[strlen(command) - 1] = '\0'; 00241 } 00242 00243 if ((strncmp("empty",command,command_length) == 0) || 00244 (strncmp("e",command,command_length) == 0) || 00245 (strncmp("1",command,command_length) == 0)) 00246 { 00247 phPRINT("set empty filter\n"); 00248 rc = vision->empty(); 00249 phCHECK_RC(rc,NULL,"vision->empty()"); 00250 do_print_menu = 1; 00251 } 00252 else if ((strncmp("motion",command,command_length) == 0) || 00253 (strncmp("m",command,command_length) == 0) || 00254 (strncmp("2",command,command_length) == 0)) 00255 { 00256 phPRINT("set motion filter\n"); 00257 rc = vision->motion(); 00258 phCHECK_RC(rc,NULL,"vision->motion()"); 00259 do_print_menu = 1; 00260 } 00261 else if ((strncmp("canny",command,command_length) == 0) || 00262 (strncmp("c",command,command_length) == 0) || 00263 (strncmp("3",command,command_length) == 0)) 00264 { 00265 phPRINT("set canny filter\n"); 00266 rc = vision->canny(); 00267 phCHECK_RC(rc,NULL,"vision->canny()"); 00268 do_print_menu = 1; 00269 } 00270 else if ((strncmp("sobel",command,command_length) == 0) || 00271 (strncmp("s",command,command_length) == 0) || 00272 (strncmp("4",command,command_length) == 0)) 00273 { 00274 phPRINT("set sobel filter\n"); 00275 rc = vision->sobel(); 00276 phCHECK_RC(rc,NULL,"vision->sobel()"); 00277 do_print_menu = 1; 00278 } 00279 else if ((strncmp("gaussian",command,command_length) == 0) || 00280 (strncmp("g",command,command_length) == 0) || 00281 (strncmp("5",command,command_length) == 0)) 00282 { 00283 phPRINT("set gaussian filter\n"); 00284 rc = vision->gaussian(); 00285 phCHECK_RC(rc,NULL,"vision->gaussian()"); 00286 do_print_menu = 1; 00287 } 00288 else if ((strncmp("mean",command,command_length) == 0) || 00289 (strncmp("6",command,command_length) == 0)) 00290 { 00291 phPRINT("set mean filter\n"); 00292 rc = vision->mean(); 00293 phCHECK_RC(rc,NULL,"vision->mean()"); 00294 do_print_menu = 1; 00295 } 00296 else if ((strncmp("median",command,command_length) == 0) || 00297 (strncmp("med",command,command_length) == 0) || 00298 (strncmp("7",command,command_length) == 0)) 00299 { 00300 phPRINT("set median filter\n"); 00301 rc = vision->median(); 00302 phCHECK_RC(rc,NULL,"vision->median()"); 00303 do_print_menu = 1; 00304 } 00305 else if ((strncmp("inverse",command,command_length) == 0) || 00306 (strncmp("i",command,command_length) == 0) || 00307 (strncmp("8",command,command_length) == 0)) 00308 { 00309 phPRINT("set inverse filter\n"); 00310 rc = vision->inverse(); 00311 phCHECK_RC(rc,NULL,"vision->inverse()"); 00312 do_print_menu = 1; 00313 } 00314 else if ((strncmp("threshold",command,command_length) == 0) || 00315 (strncmp("t",command,command_length) == 0) || 00316 (strncmp("9",command,command_length) == 0)) 00317 { 00318 phPRINT("set threshold filter\n"); 00319 rc = vision->threshold(); 00320 phCHECK_RC(rc,NULL,"vision->threshold()"); 00321 do_print_menu = 1; 00322 } 00323 else if ((strncmp("add",command,command_length) == 0) || 00324 (strncmp("a",command,command_length) == 0) || 00325 (strncmp("10",command,command_length) == 0)) 00326 { 00327 phPRINT("set add filter\n"); 00328 rc = vision->add(); 00329 phCHECK_RC(rc,NULL,"vision->add()"); 00330 do_print_menu = 1; 00331 } 00332 else if ((strncmp("subtract",command,command_length) == 0) || 00333 (strncmp("sub",command,command_length) == 0) || 00334 (strncmp("11",command,command_length) == 0)) 00335 { 00336 phPRINT("set subtract filter\n"); 00337 rc = vision->subtract(); 00338 phCHECK_RC(rc,NULL,"vision->subtract()"); 00339 do_print_menu = 1; 00340 } 00341 else if ((strncmp("quit",command,command_length) == 0) || 00342 (strncmp("12",command,command_length) == 0) || 00343 (strncmp("q",command,command_length) == 0)) 00344 { 00345 done = 1; 00346 } 00347 else 00348 { 00349 phPRINT("Invalid choice.\n\n"); 00350 do_print_menu = 1; 00351 } 00352 /* sleep here during testing; allows the filter to run */ 00353 if (glbl_test) 00354 { 00355 phSleep(glbl_test); 00356 } 00357 } 00358 00359 set_blocking(stdin,1); 00360 return phSUCCESS; 00361 error: 00362 set_blocking(stdin,1); 00363 return phFAIL; 00364 } 00365 00366 /* ------------------------------------------------------------------------ */ 00367 int train_and_track(phSimpleVision *vision) 00368 { 00369 phFUNCTION("train_and_track") 00370 int hrc = phSUCCESS; 00371 int brc = phSUCCESS; 00372 int newblobdata = 0; 00373 int32_t done = 0; 00374 int32_t state = 0; 00375 00376 phDisplayInterface *d1 = NULL; 00377 phDisplayInterface *d2 = NULL; 00378 00379 phHistogramData hist_data; 00380 phBlobData blob_data; 00381 phblob max_blob; 00382 int32_t min_size = 100; 00383 00384 /* --------------------------------------------------------------------- */ 00385 /* Remove the code below when using this code as an example. 00386 * 00387 * This just checks whether "--test" has been specified with 00388 * a time value argument. It's for testing all the examples 00389 * without the need for human intervention. */ 00390 int test_loop_count = 0; 00391 int count = 0; 00392 00393 if (!glbl_disable_displays) 00394 { 00395 d1 = vision->getDisplay(phSVO_Capture); 00396 phCHECK_PTR(d1,NULL,"vision->getDisplay(phSVO_Capture)"); 00397 00398 d2 = vision->getDisplay(phSVO_Pipeline); 00399 phCHECK_PTR(d2,NULL,"vision->getDisplay(phSVO_Pipeline)"); 00400 } 00401 00402 /* Begining mode is training mode */ 00403 phPRINT("Training Mode: color 0\n"); 00404 00405 /* Inform the user */ 00406 if (glbl_usenet_displays) 00407 { 00408 phPRINT("Press 'c' & <enter> to continue from train to track mode.\n"); 00409 phPRINT("Press 'c' & <enter> again to quit the program.\n"); 00410 } 00411 else 00412 { 00413 phPRINT("Close the filter output window w/'q' or ESC to progress.\n"); 00414 } 00415 00416 /* Connect the blob data and histogram data objects */ 00417 rc = blob_data.connect(vision->getBlobFilter()->getLiveBlobOutput()); 00418 phCHECK_RC(rc,NULL,"blobData.connect()"); 00419 00420 rc = hist_data.connect(vision->getHistogramFilter()->getLiveHistogramOutput()); 00421 phCHECK_RC(rc,NULL,"histData.connect()"); 00422 00423 00424 /* Keep going until all the windows are closed; 00425 * if the user closes the windows, done will be set 00426 * if the windows aren't enabled, then 'c' will be pressed and done will 00427 * be set to 1 */ 00428 /* This transitions from 'train' mode to 'track' mode */ 00429 while ((state < (glbl_color_count + 1)) && (!done)) 00430 { 00431 /* training : histogramming */ 00432 if (state < glbl_color_count) 00433 { 00434 rc = vision->train(); 00435 phPRINT_RC(rc,NULL,"vision->train()"); 00436 00437 /* Get the most recent histogram data */ 00438 hrc = hist_data.update(); 00439 phPRINT_RC(rc,NULL,"hist_data.update()"); 00440 00441 if (glbl_print_histogram) 00442 { 00443 if (hrc == phLiveObjectUPDATED) 00444 { 00445 /* hist_data.print_data(); */ 00446 hist_data.print_stats(); 00447 } 00448 } 00449 } 00450 /* Tracking : blobbing */ 00451 else if (state == glbl_color_count) 00452 { 00453 newblobdata = vision->track(); 00454 phPRINT_RC(newblobdata,NULL,"vision->track()"); 00455 00456 if ((glbl_print_blobs) && 00457 (vision->getBlobCount(min_size) > 0) && 00458 (newblobdata == phSimpleVision_UPDATED)) 00459 { 00460 /* Get the most recent blob data */ 00461 brc = blob_data.update(); 00462 00463 if ((brc == phLiveObjectUPDATED) && (blob_data.getTotalBlobs(min_size))) 00464 { 00465 blob_data.print_data(min_size); 00466 } 00467 } 00468 } 00469 00470 /* Yielding is optional. This gives up the thread's timeslice 00471 * to prevent slow response in other threads. It consumes more 00472 * CPU cycles than sleeping. Use it instead of sleeping if 00473 * this loop is processing anything */ 00474 00475 phYield(); 00476 00477 /* Remove this if block when using this code as an example */ 00478 00479 /* Set the loop control value to end the loop when testing */ 00480 if (glbl_test > 0) 00481 { 00482 phSleep(glbl_test); /* test's value should be a time (in secs) value > 0*/ 00483 d1->close(); 00484 d2->close(); 00485 } 00486 00487 if (!glbl_disable_displays) 00488 { 00489 /* Check to see if the user closed the displays, move to the next 00490 * state if they are closed and reopen the displays if we're coming 00491 * from the train state */ 00492 if ((!d1->isOpen()) && (!d2->isOpen())) 00493 { 00494 /* Coming from train code */ 00495 if (state < glbl_color_count) 00496 { 00497 /* Restart the displays that were closed */ 00498 rc = d1->open(); 00499 phCHECK_RC(rc,NULL,"d1->open()"); 00500 00501 rc = d2->open(); 00502 phCHECK_RC(rc,NULL,"d2->open()"); 00503 00504 phColor thresh = vision->getThreshold(); 00505 /* thresh.hsv24.s = (uint8_t)(thresh.hsv24.s * 1.2); */ 00506 thresh.hsv24.v = thresh.hsv24.v * 4; 00507 00508 rc = vision->setThreshold( thresh ); 00509 phPRINT_RC(rc,NULL,"vision->setThreshold"); 00510 00511 rc = vision->applyTraining(phSimpleVision_ADD); 00512 phPRINT_RC(rc,NULL,"vision->applyTraining") 00513 } 00514 state++; 00515 00516 if (state < glbl_color_count) 00517 { 00518 phPRINT("Training Mode: color %d\n",state); 00519 } 00520 else if (state < (glbl_color_count + 1)) 00521 { 00522 phPRINT("Tracking Mode\n"); 00523 } 00524 else 00525 { 00526 phPRINT("\n\nDone.\n"); 00527 } 00528 } 00529 } 00530 else if (wait_for(0,NULL) == phSUCCESS) 00531 { 00532 state++; 00533 00534 if (state < glbl_color_count) 00535 { 00536 phPRINT("Training Mode: color %d\n",state); 00537 } 00538 else if (state < (glbl_color_count + 1)) 00539 { 00540 phPRINT("Tracking Mode\n"); 00541 } 00542 else 00543 { 00544 phPRINT("\n\nDone.\n"); 00545 } 00546 } 00547 } 00548 00549 return phSUCCESS; 00550 error: 00551 return phFAIL; 00552 } 00553 /* ------------------------------------------------------------------------ */ 00554 int main( int argc, char *argv[] ) 00555 { 00556 phFUNCTION("main") 00557 00558 char *capture_path = NULL; 00559 int channel = 0; 00560 00561 phSimpleVision *vision = NULL; 00562 phSystem *system = NULL; 00563 phImageCapture *capture = NULL; 00564 phArgTable *arg_parser = new phArgTable(); 00565 00566 rc = arg_parser->add("--test",&glbl_test,phARG_INT); 00567 phCHECK_RC(rc,NULL,"arg_parser->add"); 00568 00569 rc = arg_parser->add("--blobs",&glbl_print_blobs,phARG_BOOL); 00570 phCHECK_RC(rc,NULL,"arg_parser->add"); 00571 00572 rc = arg_parser->add("--hists",&glbl_print_histogram,phARG_BOOL); 00573 phCHECK_RC(rc,NULL,"arg_parser->add"); 00574 00575 rc = arg_parser->add("--colors",&glbl_color_count,phARG_INT); 00576 phCHECK_RC(rc,NULL,"arg_parser->add"); 00577 00578 rc = arg_parser->add("--nodisplay",&glbl_disable_displays,phARG_BOOL); 00579 phCHECK_RC(rc,NULL,"arg_parser->add"); 00580 00581 rc = arg_parser->add("--net",&glbl_usenet_displays,phARG_BOOL); 00582 phCHECK_RC(rc,NULL,"arg_parser->add"); 00583 00584 rc = arg_parser->add("--demo",&glbl_demo_filters,phARG_BOOL); 00585 phCHECK_RC(rc,NULL,"arg_parser->add"); 00586 00587 rc = arg_parser->add("--help",(void *)&usage,phARG_FUNC); 00588 phCHECK_RC(rc,NULL,"arg_parser->add"); 00589 00590 rc = arg_parser->add("--path",&capture_path,phARG_CHAR); 00591 phCHECK_RC(rc,NULL,"arg_parser->add"); 00592 00593 rc = arg_parser->add("--channel",&channel,phARG_INT); 00594 phCHECK_RC(rc,NULL,"arg_parser->add"); 00595 00596 rc = arg_parser->parse(argc,argv); 00597 phCHECK_RC(rc,NULL,"arg_parser->parse"); 00598 00599 if (glbl_color_count <= 0) 00600 { 00601 glbl_color_count = 1; 00602 } 00603 if (glbl_test > 0) 00604 { 00605 glbl_print_histogram = 0; 00606 glbl_print_blobs = 0; 00607 } 00608 00609 /* Allocate the phSimpleVision object */ 00610 vision = new phSimpleVision(); 00611 phCHECK_PTR(vision,NULL,"new phSimpleVision"); 00612 00613 system = vision->getSystem(); 00614 phCHECK_PTR(system,NULL,"vision->getSystem()"); 00615 00616 capture = vision->getCapture(); 00617 phCHECK_PTR(capture,NULL,"vision->getCapture()"); 00618 00619 phPROGRESS("\n"); 00620 /* Net displays aren't enabled by default, so enable them now */ 00621 if (glbl_usenet_displays) 00622 { 00623 /* only using the net displays, disable the other ones */ 00624 glbl_disable_displays = 1; 00625 00626 rc = vision->enableNetDisplay(phSVO_Capture); 00627 phCHECK_RC(rc,NULL,"vision->enableNetDisplay(phSVO_Capture)"); 00628 00629 rc = vision->enableNetDisplay(phSVO_Pipeline); 00630 phCHECK_RC(rc,NULL,"vision->enableNetDisplay(phSVO_Pipeline)"); 00631 } 00632 00633 phPROGRESS("\n"); 00634 if (glbl_disable_displays) 00635 { 00636 rc = vision->disableDisplay(phSVO_Capture); 00637 phCHECK_RC(rc,NULL,"vision->disableDisplay(phSVO_Capture)"); 00638 00639 rc = vision->disableDisplay(phSVO_Pipeline); 00640 phCHECK_RC(rc,NULL,"vision->disableDisplay(phSVO_Pipeline)"); 00641 } 00642 00643 #if 1 00644 capture->setPath(capture_path); 00645 /* Set the system specific capture card settings */ 00646 capture->setChannel(channel); 00647 capture->setHue(34000); 00648 capture->setColour(39000); 00649 capture->setContrast(25000); 00650 capture->setBrightness(26000); 00651 #else 00652 capture->setChannel(channel); 00653 capture->setColour(39000); 00654 capture->setHue(29700); 00655 capture->setContrast(39000); 00656 capture->setBrightness(35000); 00657 #endif 00658 00659 phPROGRESS("\n"); 00660 00661 rc = vision->start(); 00662 phCHECK_RC(rc,NULL,"vision->start()"); 00663 00664 if (glbl_usenet_displays) 00665 { 00666 /* Wait till the user continues by pressing 'c' */ 00667 phPRINT("Ready for user to connect to net display\n"); 00668 wait_for(1,NULL); 00669 } 00670 00671 phPROGRESS("\n"); 00672 if (glbl_demo_filters) 00673 { 00674 rc = demo_filters(vision); 00675 phPRINT_RC(rc,NULL,"demo_filters"); 00676 } 00677 else 00678 { 00679 rc = train_and_track(vision); 00680 phPRINT_RC(rc,NULL,"train_and_track"); 00681 } 00682 error: 00683 phPRINT("\n\n\n"); 00684 00685 phDelete(vision); 00686 00687 phFree(capture_path); 00688 phDelete(arg_parser); 00689 00690 return phSUCCESS; 00691 }
Copyright (C) 2002 - 2007 |
Philip D.S. Thoren ( pthoren@users.sourceforge.net ) University Of Massachusetts at Lowell Robotics Lab |