00001 /* --------------------------------------------------------------------------- 00002 Phission : 00003 Realtime Vision Processing System 00004 00005 Copyright (C) 2003-2006 Philip D.S. Thoren (pthoren@cs.uml.edu) 00006 University of Massachusetts at Lowell, 00007 Laboratory for Artificial Intelligence and Robotics 00008 00009 --------------------------------------------------------------------------- 00010 <Add other copyrights here> 00011 --------------------------------------------------------------------------- 00012 00013 This file is part of Phission. 00014 00015 Phission is free software; you can redistribute it and/or modify 00016 it under the terms of the GNU Lesser General Public License as published by 00017 the Free Software Foundation; either version 2 of the License, or 00018 (at your option) any later version. 00019 00020 Phission is distributed in the hope that it will be useful, 00021 but WITHOUT ANY WARRANTY; without even the implied warranty of 00022 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 00023 GNU Lesser General Public License for more details. 00024 00025 You should have received a copy of the GNU Lesser General Public License 00026 along with Phission; if not, write to the Free Software 00027 Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 00028 00029 ---------------------------------------------------------------------------*/ 00030 #ifdef HAVE_CONFIG_H 00031 #include <phissionconfig.h> 00032 #endif 00033 00034 #include <phStandard.h> 00035 00036 #include <blobify_Filter.h> 00037 00038 #include <phError.h> 00039 #include <phMemory.h> 00040 #include <phPrint.h> 00041 00042 #define TIMESTUFF() 0 00043 00044 /* ---------------------------------------------------------------------- */ 00045 #ifdef __cplusplus 00046 extern "C" 00047 { 00048 #endif 00049 /* ---------------------------------------------------------------------- */ 00050 /* ----------------------- Blob Functions ------------------*/ 00051 /* ---------------------------------------------------------------------- */ 00052 static blobify_blob_type *initBlob( blobify_blob_type *b, unsigned int y, unsigned int x ) 00053 { 00054 b->mass = 1; 00055 b->ul.x=x; 00056 b->ul.y=y; 00057 b->lr.x=x; 00058 b->lr.y=y; 00059 b->cm.x=x; 00060 b->cm.y=y; 00061 b->next = 0; 00062 00063 /* last is used for the returned results */ 00064 b->last = 0; 00065 00066 return (b); 00067 } 00068 00069 /* ---------------------------------------------------------------------- */ 00070 static blobify_blob_type *addPixel( blobify_blob_type *b, unsigned int y, unsigned int x ) 00071 { 00072 if( x < b->ul.x ) 00073 b->ul.x = x; 00074 if( x > b->lr.x ) 00075 b->lr.x = x; 00076 if( y < b->ul.y ) 00077 b->ul.y = y; 00078 if( y > b->lr.y ) 00079 b->lr.y = y; 00080 /* not correct */ 00081 /*b->cm.x =( (float)(b->mass * b->cm.x + x) / (float)(b->mass+1) ); 00082 * b->cm.y =( (float)(b->mass * b->cm.y + y) / (float)(b->mass+1) );*/ 00083 b->mass++; 00084 00085 return (b); 00086 } 00087 00088 /* ---------------------------------------------------------------------- */ 00089 static void joinBlob( blobify_blob_type *self, blobify_blob_type *other ) 00090 { 00091 if(self->mass != 0 && other->mass != 0) 00092 { 00093 if( other->ul.x < self->ul.x ) 00094 self->ul.x = other->ul.x; 00095 00096 if( other->lr.x > self->lr.x ) 00097 self->lr.x = other->lr.x ; 00098 00099 if( other->ul.y < self->ul.y ) 00100 self->ul.y = other->ul.y; 00101 00102 if( other->lr.y > self->lr.y ) 00103 self->lr.y = other->lr.y; 00104 00105 /* Not Correct */ 00106 /* 00107 * self->cm.x=( (self->mass * self->cm.x + other->mass * other->cm.x )/ 00108 * (self->mass + other->mass)); 00109 * self->cm.y=( (self->mass * self->cm.y + other->mass * other->cm.y)/ 00110 * (self->mass + other->mass)); 00111 * */ 00112 self->mass += other->mass; 00113 other->mass = 0; 00114 } 00115 } 00116 00117 /* ---------------------------------------------------------------------- * 00118 void blobify_Filter::deleteBlob( blobify_blob_type *b ) 00119 { 00120 * pthoren - What the? WHY IS THIS BEING DONE? It makes no SENSE! * 00121 * Where are the comments? :-) * 00122 * 00123 b->cm.x = width / 2; 00124 b->cm.y = height / 2; 00125 b->ul.x = width; 00126 b->ul.y = height; 00127 b->lr.x = 0; 00128 b->lr.x = 0; 00129 b->mass = 0; 00130 00131 } 00132 */ 00133 /* ---------------------------------------------------------------------- */ 00134 static int getBlobWidth( blobify_blob_type *b ) 00135 { 00136 return( b->lr.x - b->ul.x ); 00137 } 00138 00139 /* ---------------------------------------------------------------------- */ 00140 static int getBlobHeight( blobify_blob_type *b ) 00141 { 00142 return( b->lr.y - b->ul.y ); 00143 } 00144 00145 /* ---------------------------------------------------------------------- */ 00146 static int getBlobArea( blobify_blob_type *b ) 00147 { 00148 return( getBlobWidth( b ) * getBlobHeight( b ) ); 00149 } 00150 00151 /* not correct, 1 pixel is very dense. */ 00152 /* 00153 * float getBlobDensity( blobify_blob_type *b ) 00154 * { 00155 * return( (float)b->mass / (float)getBlobArea( b ) ); 00156 * } 00157 * */ 00158 00159 00160 /* ---------------------------------------------------------------------- */ 00161 static void sortBlobs( int sortMethod, 00162 blobify_blob_type bloblist[], 00163 unsigned int indexes[], 00164 unsigned int size ) 00165 { 00166 unsigned int i,j = 0; 00167 unsigned int rankTable[blobify_MAXBLOBS]; 00168 00169 00170 for( i = 0; i < blobify_MAXBLOBS; i++) rankTable[i] = i; 00171 00172 switch(sortMethod) 00173 { 00174 00175 case 0:/* Mass */ 00176 for( i = 1; i < size + 1; i++) 00177 { 00178 for(j = i + 1; j < blobify_MAXBLOBS; j++) 00179 { 00180 if(bloblist[rankTable[i]].mass < bloblist[rankTable[j]].mass) 00181 { 00182 phSWAP(rankTable[i],rankTable[j]); 00183 } 00184 } 00185 } 00186 break; 00187 00188 case 1: /* Area */ 00189 for( i = 1; i < size + 1; i++ ) 00190 { 00191 for( j = i + 1; j < blobify_MAXBLOBS; j++ ) 00192 { 00193 /* automattically swap out 0 mass from i spot*/ 00194 if ((bloblist[rankTable[i]].mass == 0) 00195 || 00196 ((bloblist[rankTable[j]].mass != 0) 00197 && 00198 (getBlobArea(&bloblist[rankTable[i]]) < 00199 getBlobArea(&bloblist[rankTable[j]])) 00200 ) 00201 ) 00202 { 00203 phSWAP(rankTable[i],rankTable[j]); 00204 } 00205 } 00206 } 00207 break; 00208 00209 } 00210 00211 for( i = 1; i < (size+1); i++ ) 00212 indexes[i-1]= rankTable[i]; 00213 00214 } 00215 /* ---------------------------------------------------------------------- */ 00216 #ifdef __cplusplus 00217 } /* extern "C" */ 00218 #endif 00219 00220 00221 /* ---------------------------------------------------------------------- */ 00222 blobify_Filter::blobify_Filter(int inChannel, 00223 int low, 00224 int high, 00225 int sortmethod, 00226 unsigned int size, 00227 int drawBox) : 00228 phFilter("blobify_Filter") 00229 { 00230 unsigned int n = 0; 00231 00232 phMemset(&(this->m_bloblist[n]),0,sizeof(blobify_blob_type)*blobify_MAXBLOBS); 00233 00234 this->m_format = phImageRGB24; 00235 00236 this->m_result_size = 0; 00237 this->m_result_copy_size = 0; 00238 this->m_result_array = NULL; 00239 this->m_result_copy = NULL; 00240 this->m_result_new = 0; 00241 this->m_result_inited = 0; 00242 00243 this->m_blobdata = NULL; 00244 this->m_blobdata_size = 0; 00245 this->m_blobdataWidth = 0; 00246 this->m_blobdataHeight = 0; 00247 00248 this->m_blobdata_sizes = NULL; 00249 this->m_blobdata_sizes_size = 0; 00250 00251 this->set( inChannel, low, high, sortmethod, size, drawBox); 00252 } 00253 00254 /* ---------------------------------------------------------------------- */ 00255 blobify_Filter::~blobify_Filter() 00256 { 00257 phFUNCTION("blobify_Filter::~blobify_Filter") 00258 int locked = 0; 00259 uint32_t i = 0; 00260 00261 phTHIS_LOOSE_LOCK(locked); 00262 00263 phFree(this->m_result_array); 00264 phFree(this->m_result_copy); 00265 for (i = 0; i < this->m_blobdataWidth; i++ ) 00266 { 00267 phFree(this->m_blobdata[i]); 00268 } 00269 phFree(this->m_blobdata); 00270 phFree(this->m_blobdata_sizes); 00271 this->m_result_size = 0; 00272 this->m_result_copy_size = 0; 00273 this->m_blobdata_size = 0; 00274 this->m_blobdata_sizes_size = 0; 00275 00276 phTHIS_LOOSE_UNLOCK(locked); 00277 } 00278 00279 /* ------------------------------------------------------------------------ */ 00280 phFilter *blobify_Filter::cloneFilter() 00281 { 00282 phFUNCTION("blobify_Filter::cloneFilter") 00283 int locked = 0; 00284 blobify_Filter *blob = new blobify_Filter( ); 00285 00286 phTHIS_LOOSE_LOCK(locked); 00287 00288 blob->set( this->m_inChannel, 00289 this->m_low, 00290 this->m_high, 00291 this->m_sortmethod, 00292 this->m_size, 00293 this->m_drawBox ); 00294 00295 phTHIS_LOOSE_UNLOCK(locked); 00296 00297 return (phFilter *)blob; 00298 } 00299 00300 /* ---------------------------------------------------------------------- */ 00301 int blobify_Filter::set(int inChannel, 00302 int low, 00303 int high, 00304 int sortmethod, 00305 unsigned int size, 00306 int drawBox ) 00307 { 00308 phFUNCTION("blobify_Filter::set") 00309 int locked = 0; 00310 00311 phTHIS_LOOSE_LOCK(locked); 00312 00313 this->m_inChannel = inChannel; 00314 this->m_low = low; 00315 this->m_high = high; 00316 this->m_sortmethod = sortmethod; 00317 this->m_size = size; 00318 this->m_drawBox = drawBox; 00319 00320 phTHIS_LOOSE_UNLOCK(locked); 00321 00322 return phSUCCESS; 00323 } 00324 00325 /* ---------------------------------------------------------------------- */ 00326 /* getResult: this can either return a safe-enough array that can 00327 * be looked at by one thread safely; or it returns NULL */ 00328 /* ---------------------------------------------------------------------- */ 00329 blobify_blob_type *blobify_Filter::getResult() 00330 { 00331 phFUNCTION("blobify_Filter::blobify") 00332 int r_locked = 0; 00333 00334 phMUTEX_LOCK(this->m_result_lock,r_locked); 00335 00336 if (this->m_result_inited == 0) 00337 { 00338 phMUTEX_UNLOCK(this->m_result_lock,r_locked); 00339 00340 while (this->m_result_inited == 0) 00341 { 00342 phYield(); 00343 } 00344 00345 phMUTEX_LOCK(this->m_result_lock,r_locked); 00346 } 00347 00348 /* We'll have the lock here */ 00349 /* If the result is new, then copy it into the copy array */ 00350 if (this->m_result_new == 1) 00351 { 00352 phDALLOC_RESIZE(this->m_result_copy, 00353 this->m_result_copy_size, 00354 (this->m_result_size / sizeof(blobify_blob_type)), 00355 blobify_blob_type); 00356 phMemcpy(this->m_result_copy,this->m_result_array, 00357 this->m_result_copy_size); 00358 } 00359 00360 error: 00361 phMUTEX_ERROR_UNLOCK(this->m_result_lock,r_locked); 00362 00363 return this->m_result_copy; 00364 } 00365 00366 /* ---------------------------------------------------------------------- */ 00367 int blobify_Filter::filter() 00368 { 00369 phFUNCTION("blobify_Filter::blobify") 00370 00371 00384 unsigned int w,h,i,j,n,k = 0; 00385 int offset, mark1, mark2; 00386 int count; 00387 /*int minBlobNum = 0;*/ 00388 /*int maxBlobNum = 0;*/ 00389 00390 unsigned int maxIndex[blobify_MAXBLOBS] = {0}; 00391 00392 uint8_t *ImagePtr = NULL; 00393 00394 unsigned long row = 0; 00395 unsigned long pixel = 0; 00396 00397 #if TIMESTUFF() 00398 const uint32_t nStamps = 3; 00399 char *tags[nStamps] = 00400 { 00401 "matching & create blobs", 00402 "sortBlobs", 00403 "draw blob rects" 00404 }; 00405 ph_time_db timedb = NULL; 00406 rc = ph_timedb_alloc(&timedb,"blobify_Filter"); 00407 phPRINT_RC(rc,NULL,"ph_timedb_alloc"); 00408 #endif /* TIMESTUFF() */ 00409 00410 /* Begin filter */ 00411 00412 /* Make sure the allocations are set correctly */ 00413 if ((this->m_blobdata == NULL) || 00414 (this->m_blobdataWidth != width) || 00415 (this->m_blobdataHeight != height)) 00416 { 00417 if ((this->m_blobdataWidth < width) && 00418 (this->m_blobdata != NULL)) 00419 { 00420 for ( i = width - 1; i < this->m_blobdataWidth; i++ ) 00421 { 00422 phFree(this->m_blobdata[i]); 00423 } 00424 } 00425 phDALLOC_RESIZE(this->m_blobdata, 00426 this->m_blobdata_size, 00427 width, 00428 unsigned int *); 00429 phDALLOC_RESIZE(this->m_blobdata_sizes, 00430 this->m_blobdata_sizes_size, 00431 width, 00432 unsigned int); 00433 for (w = 0; w < width; w++) 00434 { 00435 phDALLOC_RESIZE(this->m_blobdata[w], 00436 this->m_blobdata_sizes[w], 00437 height, 00438 unsigned int); 00439 } 00440 00441 this->m_blobdataWidth = width; 00442 this->m_blobdataHeight = height; 00443 } 00444 00445 /* Always need to initilize to zero*/ 00446 for (w = 0; w < width; w++) 00447 { 00448 phMemset(this->m_blobdata[w],0,height*sizeof(int)); 00449 } 00450 00451 if(this->m_inChannel == phCHANNEL_BLUE) 00452 { 00453 offset = phCHANNEL_BLUE; 00454 mark1 = phCHANNEL_RED; 00455 mark2 = phCHANNEL_GREEN; 00456 } 00457 else if(this->m_inChannel == phCHANNEL_GREEN) 00458 { 00459 offset = phCHANNEL_GREEN; 00460 mark1 = phCHANNEL_RED; 00461 mark2 = phCHANNEL_BLUE; 00462 } 00463 else if(this->m_inChannel == phCHANNEL_RED) 00464 { 00465 offset = phCHANNEL_RED; 00466 mark1 = phCHANNEL_GREEN; 00467 mark2 = phCHANNEL_BLUE; 00468 } 00469 else 00470 { 00471 phCHECK_RC(-1,NULL,"Invalid Channel."); 00472 } 00473 00474 count = 1; 00475 00476 ImagePtr = (uint8_t *)(Image); 00477 00478 #if TIMESTUFF() 00479 rc = ph_timedb_start(timedb,tags[0]); 00480 phPRINT_RC(rc,NULL,"ph_timedb_start"); 00481 #endif 00482 /*build the blobmap and construct unjoined blobify_blob_type objects*/ 00483 for( h = 0; h < height; h++ ) 00484 { 00485 for(w = 0; w < width; w++, ImagePtr += depth) 00486 { 00487 if ((*(ImagePtr+offset) >= this->m_low) && 00488 (*(ImagePtr+offset) <= this->m_high) ) 00489 { 00490 /*in upper left corner - new blob */ 00491 if(h == 0 && w == 0 && count < blobify_MAXBLOBS) 00492 { 00493 initBlob(&this->m_bloblist[count],h,w); 00494 this->m_blobdata[w][h]= count; 00495 count++; 00496 } 00497 /* if in first col */ 00498 else if(w == 0) 00499 { 00500 if( this->m_blobdata[w][h-1] != 0 ) 00501 { 00502 addPixel(&this->m_bloblist[this->m_blobdata[w][h-1]],h,w); 00503 this->m_blobdata[w][h]=this->m_blobdata[w][h-1]; 00504 } 00505 /*above is off -- new blob*/ 00506 else if (count < blobify_MAXBLOBS) 00507 { 00508 initBlob(&this->m_bloblist[count], h,w); 00509 this->m_blobdata[w][h]=count; 00510 count++; 00511 } 00512 } 00513 /* in first row */ 00514 else if(h == 0) 00515 { 00516 if( this->m_blobdata[w-1][h] != 0 ) 00517 { 00518 addPixel(&this->m_bloblist[this->m_blobdata[w-1][h]],h,w); 00519 this->m_blobdata[w][h]= this->m_blobdata[w-1][h]; 00520 } 00521 /* left is off -- new blob */ 00522 else if (count < blobify_MAXBLOBS) 00523 { 00524 initBlob(&this->m_bloblist[count], h,w); 00525 this->m_blobdata[w][h]=count; 00526 count++; 00527 } 00528 } 00529 00530 else if( this->m_blobdata[w-1][h] != 0 && this->m_blobdata[w][h-1] != 0 ) 00531 { 00532 /* 00533 * see if the pixel to left and on the top are the same blob and add 00534 * this new pixel to the blob if they are 00535 * */ 00536 if(this->m_blobdata[w-1][h] == this->m_blobdata[w][h-1]) 00537 { 00538 addPixel(&this->m_bloblist[this->m_blobdata[w-1][h]],h,w); 00539 this->m_blobdata[w][h] = this->m_blobdata[w-1][h]; 00540 } 00541 else 00542 { 00543 addPixel(&this->m_bloblist[this->m_blobdata[w-1][h]],h,w); 00544 joinBlob(&this->m_bloblist[this->m_blobdata[w-1][h]], 00545 &this->m_bloblist[this->m_blobdata[w][h-1]]); 00546 this->m_blobdata[w][h] = this->m_blobdata[w-1][h]; 00547 00548 n = this->m_blobdata[w][h-1]; 00549 for( i = 0; i <= h; i++ ) 00550 { 00551 for( j = 0; j < width; j++ ) 00552 { 00553 if(this->m_blobdata[j][i] == n) 00554 { 00555 this->m_blobdata[j][i] = this->m_blobdata[w-1][h]; 00556 } 00557 } 00558 } 00559 00560 /*deleteBlob(&this->m_bloblist[this->m_blobdata[w][h-1]]);*/ 00561 } 00562 } 00563 else 00564 { 00565 if( this->m_blobdata[w-1][h] != 0 ) 00566 { 00567 addPixel(&this->m_bloblist[this->m_blobdata[w-1][h]],h,w); 00568 this->m_blobdata[w][h]=this->m_blobdata[w-1][h]; 00569 } 00570 /*top is on -- old blob */ 00571 else if( this->m_blobdata[w][h-1] != 0 ) 00572 { 00573 addPixel(&this->m_bloblist[this->m_blobdata[w][h-1]],h,w); 00574 this->m_blobdata[w][h]=this->m_blobdata[w][h-1]; 00575 } 00576 else if (count < blobify_MAXBLOBS) /* neither left or top on. -- new blob.*/ 00577 { 00578 initBlob(&this->m_bloblist[count],h,w); 00579 this->m_blobdata[w][h]=count; 00580 count++; 00581 } 00582 } 00583 } 00584 } 00585 } 00586 #if TIMESTUFF() 00587 rc = ph_timedb_stop(timedb); 00588 phPRINT_RC(rc,NULL,"ph_timedb_stop"); 00589 #endif 00590 00591 #if TIMESTUFF() 00592 rc = ph_timedb_start(timedb,tags[1]); 00593 phPRINT_RC(rc,NULL,"ph_timedb_start"); 00594 #endif 00595 sortBlobs(this->m_sortmethod, this->m_bloblist, maxIndex, this->m_size); 00596 #if TIMESTUFF() 00597 rc = ph_timedb_stop(timedb); 00598 phPRINT_RC(rc,NULL,"ph_timedb_stop"); 00599 #endif 00600 00601 #if TIMESTUFF() 00602 rc = ph_timedb_start(timedb,tags[2]); 00603 phPRINT_RC(rc,NULL,"ph_timedb_start"); 00604 #endif 00605 if(this->m_drawBox) 00606 { 00607 for( i = 0; i < height; i++ ) 00608 { 00609 row = i * width; 00610 for( j = 0; j < width; j++ ) 00611 { 00612 pixel = (row + j) * depth; 00613 for( k = 0; k < this->m_size; k++ ) 00614 { 00615 if(this->m_blobdata[j][i] == maxIndex[k]) { 00616 (Image)[pixel + offset] = 0; 00617 (Image)[pixel + mark1] = 255; 00618 (Image)[pixel + mark2] = 0; 00619 } 00620 if(this->m_bloblist[maxIndex[k]].mass > 0 ) 00621 { 00622 if ( 00623 ( 00624 (j >= this->m_bloblist[maxIndex[k]].ul.x && 00625 j <= this->m_bloblist[maxIndex[k]].lr.x) 00626 && 00627 (i == this->m_bloblist[maxIndex[k]].ul.y || 00628 i == this->m_bloblist[maxIndex[k]].lr.y) 00629 )|| 00630 ( 00631 (j == this->m_bloblist[maxIndex[k]].ul.x || 00632 j == this->m_bloblist[maxIndex[k]].lr.x) 00633 && 00634 (i >= this->m_bloblist[maxIndex[k]].ul.y && 00635 i <= this->m_bloblist[maxIndex[k]].lr.y) 00636 ) 00637 ) 00638 { 00639 (Image)[pixel + offset] = 255; 00640 (Image)[pixel + mark1] = 255; 00641 (Image)[pixel + mark2] = 255; 00642 } 00643 } 00644 } 00645 } 00646 } 00647 } 00648 #if TIMESTUFF() 00649 rc = ph_timedb_stop(timedb); 00650 phPRINT_RC(rc,NULL,"ph_timedb_stop"); 00651 #endif 00652 00653 rc = this->m_result_lock.lock(); 00654 phPRINT_RC(rc,NULL,"this->m_result_lock.lock()"); 00655 00656 /* Make sure there is memory for the results to be placed... */ 00657 phDALLOC(this->m_result_array, 00658 this->m_result_size, 00659 this->m_size, 00660 blobify_blob_type); 00661 00662 /* Copy the results into the array */ 00663 for (i = 0; i < this->m_size; i++) 00664 { 00665 this->m_result_array[i] = this->m_bloblist[maxIndex[i]]; 00666 } 00667 if (this->m_size > 0) 00668 { 00669 this->m_result_array[this->m_size - 1].last = 1; 00670 } 00671 /* Let the output function know there is new data to be copied */ 00672 this->m_result_new = 1; 00673 /* this should happen once, sure it's a waste of cycles after the 00674 * fact but it makes coding the loop that waits for the 00675 * result data(to exist) easier to code. */ 00676 if (this->m_result_inited == 0) this->m_result_inited = 1; 00677 00678 rc = this->m_result_lock.unlock(); 00679 phPRINT_RC(rc,NULL,"this->m_result_lock.unlock()"); 00680 00681 #if TIMESTUFF() /* phTIME_REPORT_ALL & */ 00682 rc = ph_timedb_report( timedb, phTIME_REPORT_SUM ); 00683 phPRINT_RC(rc,NULL,"ph_timedb_report"); 00684 00685 if (timedb) 00686 { 00687 rc = ph_timedb_free(&timedb); 00688 phPRINT_RC(rc,NULL,"ph_timedb_free"); 00689 } 00690 #endif 00691 return phSUCCESS; 00692 error: 00693 00694 #if TIMESTUFF() /* phTIME_REPORT_ALL & */ 00695 if (timedb) 00696 { 00697 rc = ph_timedb_free(&timedb); 00698 phPRINT_RC(rc,NULL,"ph_timedb_free"); 00699 } 00700 #endif 00701 00702 return phFAIL; 00703 } 00704
Copyright (C) 2002 - 2007 |
Philip D.S. Thoren ( pthoren@users.sourceforge.net ) University Of Massachusetts at Lowell Robotics Lab |