face_tracker.py
Go to the documentation of this file.
00001 #!/usr/bin/env python
00002 
00003 """ face_tracker.py - Version 0.22 2012-01-20
00004 
00005     Track a face using the OpenCV Haar detector to initially locate the face, then OpenCV's
00006     Good-Features-to-Track and Lucas-Kanade Optical Flow to track the face features over 
00007     subsequent frames.
00008     
00009     Can also be used to track arbitrarily selected patches by setting the parameter
00010     auto_face_tracking to False and using the mouse to select the desired region.
00011 
00012     Created for the Pi Robot Project: http://www.pirobot.org
00013     Copyright (c) 2011 Patrick Goebel.  All rights reserved.
00014 
00015     This program is free software; you can redistribute it and/or modify
00016     it under the terms of the GNU 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.5
00019     
00020     This program 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 General Public License for more details at:
00024     
00025     http://www.gnu.org/licenses/gpl.html
00026       
00027 """
00028 
00029 import roslib
00030 roslib.load_manifest('pi_face_tracker')
00031 import rospy
00032 import cv
00033 import sys
00034 from sensor_msgs.msg import RegionOfInterest, Image
00035 from math import sqrt, isnan
00036 from ros2opencv import ROS2OpenCV
00037 from pi_face_tracker.srv import *
00038 
00039 class PatchTracker(ROS2OpenCV):
00040     def __init__(self, node_name):
00041         ROS2OpenCV.__init__(self, node_name)
00042         
00043         self.node_name = node_name       
00044         
00045         self.auto_face_tracking = rospy.get_param("~auto_face_tracking", True)
00046         self.use_haar_only = rospy.get_param("~use_haar_only", False)
00047         self.use_depth_for_detection = rospy.get_param("~use_depth_for_detection", False)
00048         self.fov_width = rospy.get_param("~fov_width", 1.094)
00049         self.fov_height = rospy.get_param("~fov_height", 1.094)
00050         self.max_face_size = rospy.get_param("~max_face_size", 0.28)
00051         self.use_depth_for_tracking = rospy.get_param("~use_depth_for_tracking", False)
00052         self.auto_min_features = rospy.get_param("~auto_min_features", True)
00053         self.min_features = rospy.get_param("~min_features", 50) # Used only if auto_min_features is False
00054         self.abs_min_features = rospy.get_param("~abs_min_features", 6)
00055         self.std_err_xy = rospy.get_param("~std_err_xy", 2.5)
00056         self.pct_err_z = rospy.get_param("~pct_err_z", 0.42) 
00057         self.max_mse = rospy.get_param("~max_mse", 10000)
00058         self.good_feature_distance = rospy.get_param("~good_feature_distance", 5)
00059         self.add_feature_distance = rospy.get_param("~add_feature_distance", 10)
00060         self.flip_image = rospy.get_param("~flip_image", False)
00061         self.feature_type = rospy.get_param("~feature_type", 0) # 0 = Good Features to Track, 1 = SURF
00062         self.expand_roi_init = rospy.get_param("~expand_roi", 1.02)
00063         self.expand_roi = self.expand_roi_init
00064         
00065         self.camera_frame_id = "kinect_depth_optical_frame"
00066         
00067         self.cog_x = self.cog_y = 0
00068         self.cog_z = -1
00069             
00070         self.detect_box = None
00071         self.track_box = None
00072         self.features = []
00073         
00074         self.grey = None
00075         self.pyramid = None
00076         self.small_image = None
00077         
00078         """ Set up the face detection parameters """
00079         self.cascade_frontal_alt = rospy.get_param("~cascade_frontal_alt", "")
00080         self.cascade_frontal_alt2 = rospy.get_param("~cascade_frontal_alt2", "")
00081         self.cascade_profile = rospy.get_param("~cascade_profile", "")
00082         
00083         self.cascade_frontal_alt = cv.Load(self.cascade_frontal_alt)
00084         self.cascade_frontal_alt2 = cv.Load(self.cascade_frontal_alt2)
00085         self.cascade_profile = cv.Load(self.cascade_profile)
00086 
00087         self.min_size = (20, 20)
00088         self.image_scale = 2
00089         self.haar_scale = 1.5
00090         self.min_neighbors = 1
00091         self.haar_flags = cv.CV_HAAR_DO_CANNY_PRUNING
00092         
00093         self.grey = None
00094         self.pyramid = None
00095         
00096         """ Set the Good Features to Track and Lucas-Kanade parameters """
00097         self.night_mode = False       
00098         self.quality = 0.01
00099         self.win_size = 10
00100         self.max_count = 200
00101         self.block_size = 3
00102         self.use_harris = False
00103         self.flags = 0
00104         
00105         self.frame_count = 0
00106         
00107         """ Set the SURF parameters """
00108         self.surf_hessian_quality = rospy.get_param("~surf_hessian_quality", 100)
00109         
00110         """ A service to handle 'keystroke' commands sent from other nodes """
00111         self.key_command = None
00112         rospy.Service('~key_command', KeyCommand, self.key_command_callback)
00113         
00114         """ A service to allow setting the ROI to track """
00115         rospy.Service('~set_roi', SetROI, self.set_roi_callback)
00116                 
00117         """ Wait until the image topics are ready before starting """
00118         rospy.wait_for_message(self.input_rgb_image, Image)
00119         
00120         if self.use_depth_for_detection or self.use_depth_for_tracking:
00121             rospy.wait_for_message(self.input_depth_image, Image)
00122         
00123     def process_image(self, cv_image):
00124         #self.frame_count = self.frame_count + 1
00125         """ If parameter use_haar_only is True, use only the OpenCV Haar detector to track the face """
00126         if (self.use_haar_only or not self.detect_box) and self.auto_face_tracking:
00127             self.detect_box = self.detect_face(cv_image)
00128 
00129         """ Otherwise, track the face using Good Features to Track and Lucas-Kanade Optical Flow """
00130         if not self.use_haar_only:
00131             if self.detect_box:
00132                 if not self.track_box or not self.is_rect_nonzero(self.track_box):
00133                     self.features = []
00134                     self.track_box = self.detect_box
00135                 self.track_box = self.track_lk(cv_image)
00136                 
00137                 """ Prune features that are too far from the main cluster """
00138                 if len(self.features) > 0:
00139                     ((mean_x, mean_y, mean_z), mse_xy, mse_z, score) = self.prune_features(min_features = self.abs_min_features, outlier_threshold = self.std_err_xy, mse_threshold=self.max_mse)
00140                     
00141                     if score == -1:
00142                         self.detect_box = None
00143                         self.track_box = None
00144                         return cv_image
00145                 
00146                 """ Add features if the number is getting too low """
00147                 if len(self.features) < self.min_features:
00148                     self.expand_roi = self.expand_roi_init * self.expand_roi
00149                     self.add_features(cv_image)
00150                 else:
00151                     self.expand_roi = self.expand_roi_init      
00152             else:
00153                 self.features = []
00154                 self.track_box = None
00155         
00156         # If using depth info, get the cluster centroid
00157         if len(self.features) > 0 and (self.use_depth_for_detection or self.use_depth_for_tracking):
00158             (self.cog_x, self.cog_y, self.cog_z) = self.get_cluster_centroid()
00159         
00160         return cv_image
00161     
00162     def detect_face(self, cv_image):
00163         if self.grey is None:
00164             """ Allocate temporary images """      
00165             self.grey = cv.CreateImage(self.image_size, 8, 1)
00166             
00167         if self.small_image is None:
00168             self.small_image = cv.CreateImage((cv.Round(self.image_size[0] / self.image_scale),
00169                        cv.Round(self.image_size[1] / self.image_scale)), 8, 1)
00170     
00171         """ Convert color input image to grayscale """
00172         cv.CvtColor(cv_image, self.grey, cv.CV_BGR2GRAY)
00173         
00174         """ Equalize the histogram to reduce lighting effects. """
00175         cv.EqualizeHist(self.grey, self.grey)
00176     
00177         """ Scale input image for faster processing """
00178         cv.Resize(self.grey, self.small_image, cv.CV_INTER_LINEAR)
00179     
00180         """ First check one of the frontal templates """
00181         if self.cascade_frontal_alt:
00182             faces = cv.HaarDetectObjects(self.small_image, self.cascade_frontal_alt, cv.CreateMemStorage(0),
00183                                           self.haar_scale, self.min_neighbors, self.haar_flags, self.min_size)
00184                                          
00185         """ If that fails, check the profile template """
00186         if not faces:
00187             if self.cascade_profile:
00188                 faces = cv.HaarDetectObjects(self.small_image, self.cascade_profile, cv.CreateMemStorage(0),
00189                                              self.haar_scale, self.min_neighbors, self.haar_flags, self.min_size)
00190 
00191             if not faces:
00192                 """ If that fails, check a different frontal profile """
00193                 if self.cascade_frontal_alt2:
00194                     faces = cv.HaarDetectObjects(self.small_image, self.cascade_frontal_alt2, cv.CreateMemStorage(0),
00195                                          self.haar_scale, self.min_neighbors, self.haar_flags, self.min_size)
00196             
00197         if not faces:
00198             if self.show_text:
00199                 hscale = 0.4 * self.image_size[0] / 160. + 0.1
00200                 vscale = 0.4 * self.image_size[1] / 120. + 0.1
00201                 text_font = cv.InitFont(cv.CV_FONT_VECTOR0, hscale, vscale, 0, 1, 8)
00202                 cv.PutText(self.marker_image, "LOST FACE!", (50, int(self.image_size[1] * 0.9)), text_font, cv.RGB(255, 255, 0))
00203             return None
00204                 
00205         for ((x, y, w, h), n) in faces:
00206             """ The input to cv.HaarDetectObjects was resized, so scale the 
00207                 bounding box of each face and convert it to two CvPoints """
00208             pt1 = (int(x * self.image_scale), int(y * self.image_scale))
00209             pt2 = (int((x + w) * self.image_scale), int((y + h) * self.image_scale))
00210             face_width = pt2[0] - pt1[0]
00211             face_height = pt2[1] - pt1[1]
00212 
00213             if self.use_depth_for_detection:
00214                 """ Get the average distance over the face box """
00215                 ave_face_distance = 0
00216                 i = 0
00217                 for x in range(pt1[0], pt2[0]):
00218                     for y in range(pt1[1], pt2[1]):
00219                         try:
00220                             face_distance = cv.Get2D(self.depth_image, y, x)
00221                             z = face_distance[0]
00222                         except:
00223                             continue
00224                         if isnan(z):
00225                             continue
00226                         else:
00227                             ave_face_distance += z
00228                             i = i + 1
00229 
00230                 """ If we are too close to the Kinect, we will get NaN for distances so just accept the detection. """
00231                 if i == 0:
00232                     face_size = 0
00233                 
00234                 else:
00235                     """ Compute the size of the face in meters (average of width and height)
00236                         The Kinect's FOV is about 57 degrees wide which is, coincidentally about 1 radian.
00237                     """
00238                     ave_face_distance = ave_face_distance / float(i)
00239                     arc = (self.fov_width * float(face_width) / float(self.image_size[0]) + self.fov_height * float(face_height) / float(self.image_size[1])) / 2.0
00240                     face_size = ave_face_distance * arc
00241                 
00242                 if face_size > self.max_face_size:
00243                     continue
00244                 
00245             face_box = (pt1[0], pt1[1], face_width, face_height)
00246 
00247             """ Break out of the loop after the first face """
00248             return face_box
00249 
00250     def track_lk(self, cv_image):
00251         feature_box = None
00252         
00253         """ Initialize intermediate images if necessary """
00254         if not self.pyramid:
00255             self.grey = cv.CreateImage(cv.GetSize (cv_image), 8, 1)
00256             self.prev_grey = cv.CreateImage(cv.GetSize (cv_image), 8, 1)
00257             self.pyramid = cv.CreateImage(cv.GetSize (cv_image), 8, 1)
00258             self.prev_pyramid = cv.CreateImage(cv.GetSize (cv_image), 8, 1)
00259             self.features = []
00260             
00261         """ Create a grey version of the image """
00262         cv.CvtColor(cv_image, self.grey, cv.CV_BGR2GRAY)
00263         
00264         """ Equalize the histogram to reduce lighting effects """
00265         cv.EqualizeHist(self.grey, self.grey)
00266             
00267         if self.track_box and self.features != []:
00268             """ We have feature points, so track and display them """
00269 
00270             """ Calculate the optical flow """
00271             self.features, status, track_error = cv.CalcOpticalFlowPyrLK(
00272                 self.prev_grey, self.grey, self.prev_pyramid, self.pyramid,
00273                 self.features,
00274                 (self.win_size, self.win_size), 3,
00275                 (cv.CV_TERMCRIT_ITER|cv.CV_TERMCRIT_EPS, 20, 0.01),
00276                 self.flags)
00277 
00278             """ Keep only high status points """
00279             self.features = [ p for (st,p) in zip(status, self.features) if st]        
00280                                     
00281         elif self.track_box and self.is_rect_nonzero(self.track_box):
00282             """ Get the initial features to track """
00283                     
00284             """ Create a mask image to be used to select the tracked points """
00285             mask = cv.CreateImage(cv.GetSize(cv_image), 8, 1) 
00286             
00287             """ Begin with all black pixels """
00288             cv.Zero(mask)
00289 
00290             """ Get the coordinates and dimensions of the track box """
00291             try:
00292                 x,y,w,h = self.track_box
00293             except:
00294                 return None
00295             
00296             if self.auto_face_tracking:
00297 #                """ For faces, the detect box tends to extend beyond the actual object so shrink it slightly """
00298 #                x = int(0.97 * x)
00299 #                y = int(0.97 * y)
00300 #                w = int(1 * w)
00301 #                h = int(1 * h)
00302                 
00303                 """ Get the center of the track box (type CvRect) so we can create the
00304                     equivalent CvBox2D (rotated rectangle) required by EllipseBox below. """
00305                 center_x = int(x + w / 2)
00306                 center_y = int(y + h / 2)
00307                 roi_box = ((center_x, center_y), (w, h), 0)
00308                 
00309                 """ Create a filled white ellipse within the track_box to define the ROI. """
00310                 cv.EllipseBox(mask, roi_box, cv.CV_RGB(255,255, 255), cv.CV_FILLED)      
00311             else:
00312                 """ For manually selected regions, just use a rectangle """
00313                 pt1 = (x, y)
00314                 pt2 = (x + w, y + h)
00315                 cv.Rectangle(mask, pt1, pt2, cv.CV_RGB(255,255, 255), cv.CV_FILLED)
00316             
00317             """ Create the temporary scratchpad images """
00318             eig = cv.CreateImage (cv.GetSize(self.grey), 32, 1)
00319             temp = cv.CreateImage (cv.GetSize(self.grey), 32, 1)
00320 
00321             if self.feature_type == 0:
00322                 """ Find keypoints to track using Good Features to Track """
00323                 self.features = cv.GoodFeaturesToTrack(self.grey, eig, temp, self.max_count,
00324                     self.quality, self.good_feature_distance, mask=mask, blockSize=self.block_size, useHarris=self.use_harris, k=0.04)
00325             
00326             elif self.feature_type == 1:
00327                 """ Get the new features using SURF """
00328                 (surf_features, descriptors) = cv.ExtractSURF(self.grey, mask, cv.CreateMemStorage(0), (0, self.surf_hessian_quality, 3, 1))
00329                 for feature in surf_features:
00330                     self.features.append(feature[0])
00331             
00332             if self.auto_min_features:
00333                 """ Since the detect box is larger than the actual face or desired patch, shrink the number of features by 10% """
00334                 self.min_features = int(len(self.features) * 0.9)
00335                 self.abs_min_features = int(0.5 * self.min_features)
00336         
00337         """ Swapping the images """
00338         self.prev_grey, self.grey = self.grey, self.prev_grey
00339         self.prev_pyramid, self.pyramid = self.pyramid, self.prev_pyramid
00340         
00341         """ If we have some features... """
00342         if len(self.features) > 0:
00343             """ The FitEllipse2 function below requires us to convert the feature array
00344                 into a CvMat matrix """
00345             try:
00346                 self.feature_matrix = cv.CreateMat(1, len(self.features), cv.CV_32SC2)
00347             except:
00348                 pass
00349                         
00350             """ Draw the points as green circles and add them to the features matrix """
00351             i = 0
00352             for the_point in self.features:
00353                 if self.show_features:
00354                     cv.Circle(self.marker_image, (int(the_point[0]), int(the_point[1])), 2, (0, 255, 0, 0), cv.CV_FILLED, 8, 0)
00355                 try:
00356                     cv.Set2D(self.feature_matrix, 0, i, (int(the_point[0]), int(the_point[1])))
00357                 except:
00358                     pass
00359                 i = i + 1
00360     
00361             """ Draw the best fit ellipse around the feature points """
00362             if len(self.features) > 6:
00363                 feature_box = cv.FitEllipse2(self.feature_matrix)
00364             else:
00365                 feature_box = None
00366             
00367             """ Publish the ROI for the tracked object """
00368             try:
00369                 (roi_center, roi_size, roi_angle) = feature_box
00370             except:
00371                 rospy.loginfo("Patch box has shrunk to zero...")
00372                 feature_box = None
00373     
00374             if feature_box and not self.drag_start and self.is_rect_nonzero(self.track_box):
00375                 self.ROI = RegionOfInterest()
00376                 self.ROI.x_offset = min(self.image_size[0], max(0, int(roi_center[0] - roi_size[0] / 2)))
00377                 self.ROI.y_offset = min(self.image_size[1], max(0, int(roi_center[1] - roi_size[1] / 2)))
00378                 self.ROI.width = min(self.image_size[0], int(roi_size[0]))
00379                 self.ROI.height = min(self.image_size[1], int(roi_size[1]))
00380                 
00381             self.pubROI.publish(self.ROI)
00382             
00383             """ If using depth info Publish the centroid of the tracked cluster as a PointStamped message """
00384             if self.use_depth_for_detection or self.use_depth_for_tracking:
00385                 if feature_box is not None and not self.drag_start and self.is_rect_nonzero(self.track_box):
00386                     self.cluster3d.header.frame_id = self.camera_frame_id
00387                     self.cluster3d.header.stamp = rospy.Time()
00388                     self.cluster3d.point.x = self.cog_x
00389                     self.cluster3d.point.y = self.cog_y
00390                     self.cluster3d.point.z = self.cog_z
00391                     self.pub_cluster3d.publish(self.cluster3d)
00392             
00393         if feature_box is not None and len(self.features) > 0:
00394             return feature_box
00395         else:
00396             return None
00397         
00398     def add_features(self, cv_image):
00399         """ Look for any new features around the current feature cloud """
00400         
00401         """ Create the ROI mask"""
00402         roi = cv.CreateImage(cv.GetSize(cv_image), 8, 1) 
00403         
00404         """ Begin with all black pixels """
00405         cv.Zero(roi)
00406         
00407         """ Get the coordinates and dimensions of the current track box """
00408         try:
00409             ((x,y), (w,h), a) = self.track_box
00410         except:
00411             rospy.loginfo("Track box has shrunk to zero...")
00412             return
00413         
00414         """ Expand the track box to look for new features """
00415         w = int(self.expand_roi * w)
00416         h = int(self.expand_roi * h)
00417         
00418         roi_box = ((x,y), (w,h), a)
00419         
00420         """ Create a filled white ellipse within the track_box to define the ROI. """
00421         cv.EllipseBox(roi, roi_box, cv.CV_RGB(255,255, 255), cv.CV_FILLED)
00422         
00423         """ Create the temporary scratchpad images """
00424         eig = cv.CreateImage (cv.GetSize(self.grey), 32, 1)
00425         temp = cv.CreateImage (cv.GetSize(self.grey), 32, 1)
00426         
00427         if self.feature_type == 0:
00428             """ Get the new features using Good Features to Track """
00429             features = cv.GoodFeaturesToTrack(self.grey, eig, temp, self.max_count,
00430             self.quality, self.good_feature_distance, mask=roi, blockSize=3, useHarris=0, k=0.04)
00431         
00432         elif self.feature_type == 1:
00433             """ Get the new features using SURF """
00434             features = []
00435             (surf_features, descriptors) = cv.ExtractSURF(self.grey, roi, cv.CreateMemStorage(0), (0, self.surf_hessian_quality, 3, 1))
00436             for feature in surf_features:
00437                 features.append(feature[0])
00438                 
00439         """ Append new features to the current list if they are not too far from the current cluster """
00440         for new_feature in features:
00441             try:
00442                 distance = self.distance_to_cluster(new_feature, self.features)
00443                 if distance > self.add_feature_distance:
00444                     self.features.append(new_feature)
00445             except:
00446                 pass
00447                 
00448         """ Remove duplicate features """
00449         self.features = list(set(self.features))
00450 
00451     def distance_to_cluster(self, test_point, cluster):
00452         min_distance = 10000
00453         for point in cluster:
00454             if point == test_point:
00455                 continue
00456             """ Use L1 distance since it is faster than L2 """
00457             distance = abs(test_point[0] - point[0]) + abs(test_point[1] - point[1])
00458             if distance < min_distance:
00459                 min_distance = distance
00460         return min_distance
00461     
00462     def get_cluster_centroid(self):
00463         """ compute the 3D centroid (COG) of the current cluster """
00464         n_xy = n_z = 0
00465         sum_x = sum_y = sum_z = 0
00466         
00467         (cols, rows) = cv.GetSize(self.depth_image)
00468         
00469         for point in self.features:
00470             sum_x = sum_x + point[0]
00471             sum_y = sum_y + point[1]
00472             n_xy += 1
00473             
00474             try:
00475                 z = cv.Get2D(self.depth_image, min(rows - 1, int(point[1])), min(cols - 1, int(point[0])))
00476             except cv.error:
00477                 rospy.loginfo("Get2D Index Error: " + str(int(point[1])) + " x " + str(int(point[0])))
00478                 continue
00479 
00480             """ Depth values can be NaN which should be ignored """
00481             if isnan(z[0]):
00482                 continue
00483             else:
00484                 sum_z = sum_z + z[0]
00485                 n_z += 1
00486                 
00487         #rospy.loginfo(n_z)
00488         
00489         if n_xy > 0:
00490             cog_x = sum_x / n_xy
00491             cog_y = sum_y / n_xy
00492             
00493         """ The Kinect returns NaN depth values when closer than about 0.5 meters.  If the target is closer than 0.5 meters
00494             then use 0.5 meters as a fudge """
00495         if n_z > 0:
00496            cog_z = sum_z / n_z
00497         else:
00498             cog_z = 0.5
00499         # Convert the cog_x and cog_y pixel values to meters using the fact that the Kinect's FOV is about 57 degrees or 1 radian.
00500         cog_x = cog_z * self.fov_width * (cog_x - self.image_size[0] / 2.0) / float(self.image_size[0])
00501         cog_y = cog_z * self.fov_height * (cog_y - self.image_size[1] / 2.0) / float(self.image_size[1])
00502                         
00503         return (cog_x, cog_y, cog_z)    
00504     
00505     def prune_features(self, min_features, outlier_threshold, mse_threshold):
00506         sum_x = 0
00507         sum_y = 0
00508         sum_z = 0
00509         sse = 0
00510         features_xy = self.features
00511         features_z = self.features
00512         n_xy = len(self.features)
00513         n_z = 0
00514         mean_z = mse_z = -1
00515         
00516         if self.use_depth_for_tracking:
00517             if not self.depth_image:
00518                 return ((0, 0, 0), 0, 0, -1)
00519             else:
00520                 (cols, rows) = cv.GetSize(self.depth_image)
00521         
00522         """ If there are no features left to track, start over """
00523         if n_xy == 0:
00524             return ((0, 0, 0), 0, 0, -1)
00525         
00526         """ Compute the COG (center of gravity) of the cluster """
00527         for point in self.features:
00528             sum_x = sum_x + point[0]
00529             sum_y = sum_y + point[1]
00530         
00531         mean_x = sum_x / n_xy
00532         mean_y = sum_y / n_xy
00533 
00534         """ Compute the x-y MSE (mean squared error) of the cluster in the camera plane """
00535         for point in self.features:
00536             sse = sse + (point[0] - mean_x) * (point[0] - mean_x) + (point[1] - mean_y) * (point[1] - mean_y)
00537             #sse = sse + abs((point[0] - mean_x)) + abs((point[1] - mean_y))
00538         
00539         """ Get the average over the number of feature points """
00540         mse_xy = sse / n_xy
00541         
00542         """ The MSE must be > 0 for any sensible feature cluster """
00543         if mse_xy == 0 or mse_xy > mse_threshold:
00544             return ((0, 0, 0), 0, 0, -1)
00545         
00546         """ Throw away the outliers based on the x-y variance """
00547         max_err = 0
00548         for point in self.features:
00549             std_err = ((point[0] - mean_x) * (point[0] - mean_x) + (point[1] - mean_y) * (point[1] - mean_y)) / mse_xy
00550             if std_err > max_err:
00551                 max_err = std_err
00552             if std_err > outlier_threshold:
00553                 features_xy.remove(point)
00554                 try:
00555                         features_z.remove(point)
00556                         n_z = n_z - 1
00557                 except:
00558                         pass
00559                 
00560                 n_xy = n_xy - 1
00561                                 
00562         """ Now do the same for depth """
00563         if self.use_depth_for_tracking:
00564             for point in self.features:
00565                 try:
00566                     z = cv.Get2D(self.depth_image, min(rows - 1, int(point[1])), min(cols - 1, int(point[0])))
00567                 except:
00568                     continue
00569 
00570                 """ Depth values can be NaN which should be ignored """
00571                 if isnan(z[0]):
00572                     continue
00573                 else:
00574                     sum_z = sum_z + z[0]
00575                     n_z += 1
00576             
00577             if n_z != 0:        
00578                 mean_z = sum_z / n_z
00579                 
00580                 sse = 0
00581                 n_z = 0
00582                 for point in self.features:
00583                     try:
00584                         z = cv.Get2D(self.depth_image, min(rows - 1, int(point[1])), min(cols - 1, int(point[0])))
00585                         sse = sse + (z[0] - mean_z) * (z[0] - mean_z)
00586                         n_z += 1
00587                     except:
00588                         continue
00589                 
00590                 if n_z != 0:
00591                     mse_z = sse / n_z
00592                     
00593                     """ Throw away the outliers based on depth using percent error rather than standard error since depth
00594                          values can jump dramatically at object boundaries  """
00595                     for point in features_z:
00596                         try:
00597                             z = cv.Get2D(self.depth_image, min(rows - 1, int(point[1])), min(cols - 1, int(point[0])))
00598                         except:
00599                             continue
00600                         try:
00601                             pct_err = abs(z[0] - mean_z) / mean_z
00602                             if pct_err > self.pct_err_z:
00603                                 features_xy.remove(point)
00604                         except:
00605                             pass
00606         
00607         self.features = features_xy
00608                
00609         """ Consider a cluster bad if we have fewer than abs_min_features left """
00610         if len(self.features) < self.abs_min_features:
00611             score = -1
00612         else:
00613             score = 1
00614                             
00615         return ((mean_x, mean_y, mean_z), mse_xy, mse_z, score)
00616     
00617     def key_command_callback(self, req):
00618         self.key_command = req.command
00619         return KeyCommandResponse()
00620     
00621     def set_roi_callback(self, req):
00622         self.keypoints = []
00623         self.track_box = None
00624         self.detect_box = (req.roi.x_offset, req.roi.y_offset, req.roi.width, req.roi.height)
00625         return SetROIResponse()
00626 
00627 def main(args):
00628     """ Display a help message if appropriate """
00629     help_message =  "Hot keys: \n" \
00630           "\tq - quit the program\n" \
00631           "\tc - delete current features\n" \
00632           "\tt - toggle text captions on/off\n" \
00633           "\tf - toggle display of features on/off\n" \
00634           "\tn - toggle \"night\" mode on/off\n" \
00635           "\ta - toggle auto face tracking on/off\n"
00636 
00637     print help_message
00638     
00639     """ Fire up the Face Tracker node """
00640     PT = PatchTracker("pi_face_tracker")
00641 
00642     try:
00643       rospy.spin()
00644     except KeyboardInterrupt:
00645       print "Shutting down face tracker node."
00646       cv.DestroyAllWindows()
00647 
00648 if __name__ == '__main__':
00649     main(sys.argv)


pi_face_tracker
Author(s): Patrick Goebel
autogenerated on Mon Oct 6 2014 03:26:27