def __init__(self): self.last_image = None self.last_image_timestamp = None self.last_draw_image = None # This may need to be changed if you want to use a different image feed. self.image_sub = sub8_ros_tools.Image_Subscriber('/down/left/image_raw', self.image_cb) self.image_pub = sub8_ros_tools.Image_Publisher("down/left/target_info") self.occ_grid = MarkerOccGrid(self.image_sub, grid_res=.05, grid_width=500, grid_height=500, grid_starting_pose=Pose2D(x=250, y=250, theta=0)) #self.range = sub8_ros_tools.get_parameter_range('/color/channel_guide') self.channel_width = 0.2 # sub8_ros_tools.wait_for_param('/vision/channel_width') self.pose_service = rospy.Service("vision/channel_marker/2D", VisionRequest2D, self.request_pipe) self.kernel = np.ones((15, 15), np.uint8) # Occasional status publisher self.timer = rospy.Timer(rospy.Duration(.5), self.publish_target_info)
class PipeFinder: def __init__(self): self.last_image = None self.last_image_timestamp = None self.last_draw_image = None # This may need to be changed if you want to use a different image feed. self.image_sub = sub8_ros_tools.Image_Subscriber('/down/left/image_raw', self.image_cb) self.image_pub = sub8_ros_tools.Image_Publisher("down/left/target_info") self.occ_grid = MarkerOccGrid(self.image_sub, grid_res=.05, grid_width=500, grid_height=500, grid_starting_pose=Pose2D(x=250, y=250, theta=0)) #self.range = sub8_ros_tools.get_parameter_range('/color/channel_guide') self.channel_width = 0.2 # sub8_ros_tools.wait_for_param('/vision/channel_width') self.pose_service = rospy.Service("vision/channel_marker/2D", VisionRequest2D, self.request_pipe) self.kernel = np.ones((15, 15), np.uint8) # Occasional status publisher self.timer = rospy.Timer(rospy.Duration(.5), self.publish_target_info) def publish_target_info(self, *args): if self.last_image is None: return markers = self.find_pipe_new(np.copy(self.last_image)) #self.occ_grid.update_grid(self.last_image_timestamp) #self.occ_grid.add_marker(markers, self.last_image_timestamp) if self.last_draw_image is not None and (markers is not None): self.image_pub.publish(np.uint8(self.last_draw_image)) def image_cb(self, image): '''Hang on to last image and when it was taken.''' self.last_image = image self.last_image_timestamp = self.image_sub.last_image_time def calculate_threshold(self, img, agression=.5): histr = cv2.calcHist([img], [0], None, [179], [0, 179]) threshold = np.uint8((179 - np.argmax(histr)) * agression) return threshold def get_pose(self, mask): # estimate covariance matrix and get corresponding eigenvectors wh = np.where(mask)[::-1] cov = np.cov(wh) eig_vals, eig_vects = np.linalg.eig(cov) # use index of max eigenvalue to find max eigenvector i = np.argmax(eig_vals) max_eigv = eig_vects[:, i] * np.sqrt(eig_vals[i]) # flip indices to find min eigenvector min_eigv = eig_vects[:, 1 - i] * np.sqrt(eig_vals[1 - i]) # define center of pipe center = np.average(wh, axis=1) # define vertical vector (sub's current direction) vert_vect = np.array([0.0, -1.0]) if max_eigv[1] > 0: max_eigv = -max_eigv min_eigv = -min_eigv num = np.cross(max_eigv, vert_vect) denom = np.linalg.norm(max_eigv) * np.linalg.norm(vert_vect) angle_rad = np.arcsin(num / denom) return center, angle_rad, [max_eigv, min_eigv] def find_pipe_new(self, img): #img[:, -100:] = 0 #img = cv2.GaussianBlur(img, (7, 7), 15) last_image_timestamp = self.last_image_timestamp hsv = cv2.cvtColor(img, cv2.COLOR_RGB2HSV) lower = np.array([self.calculate_threshold(hsv), 0, 0]) upper = np.array([179, 255, 255]) # Take the threholded mask, remove noise, find the biggest contour, then draw a the best fit rectangle # around that box, finally use same algorithm on the best fit rectangle. mask = cv2.inRange(hsv, lower, upper) mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, self.kernel) contours, _ = cv2.findContours(np.copy(mask), cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) if len(contours) < 1: print "None found" return # Find biggest area contour self.last_draw_image = np.copy(hsv) areas = [cv2.contourArea(c) for c in contours] max_index = np.argmax(areas) cnt = contours[max_index] # Draw a miniAreaRect around the contour and find the area of that. rect = cv2.minAreaRect(cnt) box = cv2.cv.BoxPoints(rect) box = np.int0(box) mask = np.zeros(shape=mask.shape) cv2.drawContours(mask, [box], 0, 255, -1) rect_area = cv2.contourArea(box) center, angle_rad, [max_eigv, min_eigv] = self.get_pose(mask) # Check if the box is too big or small. xy_position, height = self.occ_grid.get_tf(timestamp=last_image_timestamp) expected_area = self.occ_grid.calculate_marker_area(height) if expected_area * .3 < rect_area < expected_area * 2: cv2.drawContours(self.last_draw_image, [box], 0, (255, 255, 255), -1) else: angle_rad = 0 max_eigv = np.array([0, -20]) min_eigv = np.array([-20, 0]) #cv2.drawContours(self.last_draw_image, [box], 0, (255, 0, 30), -1) print "Size out of bounds!" cv2.line(self.last_draw_image, tuple(np.int0(center)), tuple(np.int0(center + (2 * max_eigv))), (0, 255, 30), 2) cv2.line(self.last_draw_image, tuple(np.int0(center)), tuple(np.int0(center + (2 * min_eigv))), (0, 30, 255), 2) print center, angle_rad print rect_area, expected_area print return center, angle_rad, rect_area def request_pipe(self, data): if self.last_image is None: return False # Fail if we have no images cached timestamp = self.last_image_timestamp position, orientation, _ = self.find_pipe_new(self.last_image) found = (position is not None) if not found: resp = VisionRequest2DResponse( header=Header( stamp=timestamp, frame_id='/down_camera'), found=found, camera_info=self.image_sub.camera_info, ) else: resp = VisionRequest2DResponse( pose=Pose2D( x=position[0], y=position[1], theta=orientation ), max_x=self.last_image.shape[0], max_y=self.last_image.shape[1], camera_info=self.image_sub.camera_info, header=Header( stamp=timestamp, frame_id='/down_camera'), found=found ) return resp # Not currently using these def find_pipe(self, img, timestamp): rows, cols = img.shape[:2] if rows == 0: return None, None, None blur = cv2.GaussianBlur(img, (5, 5), 1000) hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV) channel = 2 marker_scale = self.get_ncc_scale() norc = self.ncc(hsv[::2, ::2, channel], self.range[channel, 0], scale=marker_scale) color_mask = cv2.inRange(hsv, self.range[:, 0], self.range[:, 1]) ncc_mask = cv2.resize(np.uint8(norc >= 0), None, fx=2, fy=2) mask = color_mask & ncc_mask # todo: use svm contours, _ = cv2.findContours(mask, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) blank_img = np.zeros((rows, cols), np.uint8) draw_image = np.copy(img) if len(contours) > 0: # sort contours by area (greatest --> least) contours = sorted(contours, key=cv2.contourArea, reverse=True)[:1] cnt = contours[0] # contour with greatest area # This is not to filter incorrectly sized objects, but to ignore speckle noise if cv2.contourArea(cnt) > 200: cv2.drawContours(blank_img, [cnt], 0, (255, 255, 255), -1) cv2.drawContours(draw_image, [cnt], 0, (255, 255, 255), -1) # get all coordinates (y,x) of pipe why, whx = np.where(blank_img) # align coordinates --> (x,y) wh = np.array([whx, why]) # estimate covariance matrix and get corresponding eigenvectors cov = np.cov(wh) eig_vals, eig_vects = np.linalg.eig(cov) # use index of max eigenvalue to find max eigenvector i = np.argmax(eig_vals) max_eigv = eig_vects[:, i] * np.sqrt(eig_vals[i]) # flip indices to find min eigenvector min_eigv = eig_vects[:, 1 - i] * np.sqrt(eig_vals[1 - i]) # define center of pipe center = np.average(wh, axis=1) # define vertical vector (sub's current direction) vert_vect = np.array([0.0, -1.0]) if max_eigv[1] > 0: max_eigv = -max_eigv min_eigv = -min_eigv num = np.cross(max_eigv, vert_vect) denom = np.linalg.norm(max_eigv) * np.linalg.norm(vert_vect) angle_rad = np.arcsin(num / denom) cv2.line(draw_image, tuple(np.int0(center)), tuple(np.int0(center + (2 * max_eigv))), (0, 255, 30), 2) cv2.line(draw_image, tuple(np.int0(center)), tuple(np.int0(center + (2 * min_eigv))), (0, 30, 255), 2) norc_res = cv2.resize(norc, None, fx=2, fy=2) draw_image[:, :, 0] = norc_res self.last_draw_image = np.copy(draw_image) #print center, angle_rad, cv2.contourArea(cnt) return center, angle_rad, cv2.contourArea(cnt) return None, None, None def get_ncc_scale(self): '''Comptue pixel width of the channel marker''' _, height = self.occ_grid.get_tf() a = np.array(self.occ_grid.cam.project3dToPixel([0.0, self.channel_width, height])) b = self.occ_grid.cam.project3dToPixel([0.0, 0.0, height]) return int(np.nan_to_num(np.linalg.norm(a - b)) / 2.0) def ncc(self, image, mean_thresh, scale=5): '''Compute normalized cross correlation w.r.t a shadowed pillbox fcn TODO: Compute the kernel only once (Too cheap to need) ''' kernel = np.ones((scale, scale)) * -1 midpoint = (scale // 2, scale // 2) cv2.circle(kernel, midpoint, midpoint[0], 1, -1) mean, std_dev = cv2.meanStdDev(image) # Check if the scene is brighter than our a priori target if mean > mean_thresh: kernel = -kernel normalized_cross_correlation = cv2.filter2D((image - mean) / std_dev, -1, kernel) renormalized = normalized_cross_correlation return renormalized