def centerline(self): """ determine the center line of the tail """ mask, offset = self.mask dist_map = cv2.distanceTransform(mask, cv2.DIST_L2, 5) # setup active contour algorithm ac = ActiveContour( blur_radius=self.centerline_blur_radius, closed_loop=False, alpha=0, #< line length is constraint by beta beta=self.centerline_bending_stiffness, gamma=self.centerline_adaptation_rate) ac.max_iterations = self.centerline_max_iterations ac.set_potential(dist_map) # find centerline starting from the ventral_side points = curves.translate_points(self.ventral_side, -offset[0], -offset[1]) spacing = self.centerline_spacing points = curves.make_curve_equidistant(points, spacing=spacing) # use the active contour algorithm points = ac.find_contour(points) points = curves.make_curve_equidistant(points, spacing=spacing) # translate points back into global coordinate system points = curves.translate_points(points, offset[0], offset[1]) # orient the centerline such that it starts at the posterior end dist1 = curves.point_distance(points[0], self.endpoints[0]) dist2 = curves.point_distance(points[-1], self.endpoints[0]) if dist1 > dist2: points = points[::-1] return points
def centerline(self): """ determine the center line of the tail """ mask, offset = self.mask dist_map = cv2.distanceTransform(mask, cv2.DIST_L2, 5) # setup active contour algorithm ac = ActiveContour(blur_radius=self.centerline_blur_radius, closed_loop=False, alpha=0, #< line length is constraint by beta beta=self.centerline_bending_stiffness, gamma=self.centerline_adaptation_rate) ac.max_iterations = self.centerline_max_iterations ac.set_potential(dist_map) # find centerline starting from the ventral_side points = curves.translate_points(self.ventral_side, -offset[0], -offset[1]) spacing = self.centerline_spacing points = curves.make_curve_equidistant(points, spacing=spacing) # use the active contour algorithm points = ac.find_contour(points) points = curves.make_curve_equidistant(points, spacing=spacing) # translate points back into global coordinate system points = curves.translate_points(points, offset[0], offset[1]) # orient the centerline such that it starts at the posterior end dist1 = curves.point_distance(points[0], self.endpoints[0]) dist2 = curves.point_distance(points[-1], self.endpoints[0]) if dist1 > dist2: points = points[::-1] return points
def energy_curvature(pos): """ part of the energy related to the curvature of the ground line """ # get curvature part: http://en.wikipedia.org/wiki/Menger_curvature p_c = (p[0] + pos*dy, p[1] - pos*dx) a = curves.point_distance(p_p, p_c) b = curves.point_distance(p_c, p_n) c = curves.point_distance(p_n, p_p) # determine curvature of circle through the three points A = regions.triangle_area(a, b, c) curvature = 4*A/(a*b*c)*spacing # We don't scale by with the arc length a + b, because this # would increase the curvature weight in situations where # fitting is complicated (close to burrow entries) return curvature_energy_factor*curvature
def extend_mouse_trail(self): """ extends the mouse trail using the current mouse position """ ground_line = self.ground.linestring # remove points which are in front of the mouse if self.mouse_trail: spacing = self.params['mouse/model_radius'] trail = np.array(self.mouse_trail) # get distance between the current point and the previous ones dist = np.hypot(trail[:, 0] - self.mouse_pos[0], trail[:, 1] - self.mouse_pos[1]) points_close = (dist < spacing) # delete obsolete points if np.any(points_close): i = np.nonzero(points_close)[0][0] del self.mouse_trail[i:] # check the two ends of the mouse trail if self.mouse_trail: # move first point to ground ground_point = curves.get_projection_point(ground_line, self.mouse_trail[0]) self.mouse_trail[0] = ground_point # check whether a separate point needs to be inserted p1, p2 = self.mouse_trail[-1], self.mouse_pos if curves.point_distance(p1, p2) > spacing: mid_point = (0.5 * (p1[0] + p2[0]), 0.5 * (p1[1] + p2[1])) self.mouse_trail.append(mid_point) # append the current point self.mouse_trail.append(self.mouse_pos) ground_dist = curves.curve_length(self.mouse_trail) else: # create a mouse trail if it is not too far from the ground # the latter can happen, when the mouse suddenly appears underground ground_point = curves.get_projection_point(ground_line, self.mouse_pos) ground_dist = curves.point_distance(ground_point, self.mouse_pos) if ground_dist < self.params['mouse/speed_max']: self.mouse_trail = [ground_point, self.mouse_pos] return ground_dist
def extend_mouse_trail(self): """ extends the mouse trail using the current mouse position """ ground_line = self.ground.linestring # remove points which are in front of the mouse if self.mouse_trail: spacing = self.params['mouse/model_radius'] trail = np.array(self.mouse_trail) # get distance between the current point and the previous ones dist = np.hypot(trail[:, 0] - self.mouse_pos[0], trail[:, 1] - self.mouse_pos[1]) points_close = (dist < spacing) # delete obsolete points if np.any(points_close): i = np.nonzero(points_close)[0][0] del self.mouse_trail[i:] # check the two ends of the mouse trail if self.mouse_trail: # move first point to ground ground_point = curves.get_projection_point(ground_line, self.mouse_trail[0]) self.mouse_trail[0] = ground_point # check whether a separate point needs to be inserted p1, p2 = self.mouse_trail[-1], self.mouse_pos if curves.point_distance(p1, p2) > spacing: mid_point = (0.5*(p1[0] + p2[0]), 0.5*(p1[1] + p2[1])) self.mouse_trail.append(mid_point) # append the current point self.mouse_trail.append(self.mouse_pos) ground_dist = curves.curve_length(self.mouse_trail) else: # create a mouse trail if it is not too far from the ground # the latter can happen, when the mouse suddenly appears underground ground_point = curves.get_projection_point(ground_line, self.mouse_pos) ground_dist = curves.point_distance(ground_point, self.mouse_pos) if ground_dist < self.params['mouse/speed_max']: self.mouse_trail = [ground_point, self.mouse_pos] return ground_dist
def determine_endpoint_indices(self, tail_prev=None): """ locate the end points as contour points with maximal distance The posterior point is returned first. If `tail_prev` is given, the end points are oriented in the same way as in the previous tail, otherwise they are assigned automatically. """ # get the points which are farthest away from each other dist = distance.squareform(distance.pdist(self.contour)) indices = np.unravel_index(np.argmax(dist), dist.shape) if tail_prev is None: # determine the mass of tissue to determine posterior end mass = [] for k in indices: radius = self.endpoint_mass_radius endzone = geometry.Point(self.contour[k]).buffer(radius) poly = self.polygon.buffer(0) #< clean the polygon mass.append(poly.intersection(endzone).area) # determine posterior end point by measuring the surrounding if mass[1] < mass[0]: indices = indices[::-1] else: # sort end points according to previous frame prev_p, prev_a = tail_prev.endpoints this_1 = self.contour[indices[0]] this_2 = self.contour[indices[1]] dist1 = curves.point_distance(this_1, prev_p) + \ curves.point_distance(this_2, prev_a) dist2 = curves.point_distance(this_1, prev_a) + \ curves.point_distance(this_2, prev_p) if dist2 < dist1: indices = indices[::-1] # save indices in cache self.persistence_data['endpoint_indices'] = indices return indices
def get_track_graph(self, tracks, threshold, highlight_nodes=None): """ builds a weighted, directed graph representing the possible trajectories """ if highlight_nodes: highlight_nodes = set(highlight_nodes) else: highlight_nodes = set() graph = nx.DiGraph() # find all possible connections time_scale = self.params['tracking/time_scale'] tolerated_overlap = self.params['tracking/tolerated_overlap'] look_back_count = int(tolerated_overlap) + 5 for a_idx, a in enumerate(tracks): # compare to other nodes (look back into past, too) for b in tracks[max(a_idx - look_back_count, 0):]: if a is b or graph.has_edge(a, b): continue # don't add self-loops or multiple loops gap_length = b.start - a.end #< time gap between the two chunks if gap_length > -tolerated_overlap: # calculate the cost of this gap # lower is better; all terms should be normalized to one distance = curves.point_distance(a.last.pos, b.first.pos) cost = ( #+ (2 - a.mouse_score - b.mouse_score) # is it a mouse? + distance/self.params['mouse/speed_max'] # how close are the positions + abs(gap_length)/time_scale # is it a long gap? ) # add the edge if the cost is not too high if cost < threshold: graph.add_edge(a, b, cost=cost) # highlight node if necessary if a in graph: graph.node[a]['highlight'] = (a_idx in highlight_nodes) return graph
def _get_burrow_exit_length(self, burrow): """ calculates the length of all exists of the given burrow """ # identify all points that are close to the ground line dist_max = burrow.parameters['ground_point_distance'] g_line = self.ground_line.linestring points = burrow.contour exitpoints = [g_line.distance(geometry.Point(point)) < dist_max for point in points] # find the indices of contiguous true regions indices = math.contiguous_true_regions(exitpoints) # find the total length of all exits exit_length = 0 for a, b in indices: exit_length += curves.curve_length(points[a : b+1]) # handle the first and last point if they both belong to an exit if exitpoints[0] and exitpoints[-1]: exit_length += curves.point_distance(points[0], points[-1]) return exit_length
def add_tracks_to_trajectory(self, tracks, trajectory): """ iterates through all tracks and adds them to the trajectory, using linear interpolation where necessary and appropriate """ time_last, obj_last = None, None for track in tracks: # check the connection between the previous point and the current one if obj_last is not None: time_now, obj_now = track.start, track.first time_gap = time_now - time_last small_gap = (time_gap < self.params['tracking/maximal_gap']) obj_dist = curves.point_distance(obj_last.pos, obj_now.pos) small_jump = (obj_dist < self.params['tracking/maximal_jump']) # check whether we should interpolate if small_gap and small_jump: frames = np.arange(time_last + 1, time_now) ratio = np.linspace(0, 1, len(frames) + 2)[1:-1] x1, x2 = obj_last.pos[0], obj_now.pos[0] trajectory[frames, 0] = x1 + (x2 - x1)*ratio y1, y2 = obj_last.pos[1], obj_now.pos[1] trajectory[frames, 1] = y1 + (y2 - y1)*ratio # check whether the tracking went wrong if time_gap <= 1 and not small_jump: # make sure that there is a gap indicated by nan between # the two chunks trajectory[time_now - 1:time_last + 1, :] = np.nan # add the data of this track directly for time, obj in track: if np.all(np.isfinite(trajectory[time, :])): # average overlapping tracks trajectory[time, :] = (trajectory[time, :] + obj.pos)/2 else: trajectory[time, :] = obj.pos time_last, obj_last = time, obj
def _get_burrow_exit_length(self, burrow): """ calculates the length of all exists of the given burrow """ # identify all points that are close to the ground line dist_max = burrow.parameters['ground_point_distance'] g_line = self.ground_line.linestring points = burrow.contour exitpoints = [ g_line.distance(geometry.Point(point)) < dist_max for point in points ] # find the indices of contiguous true regions indices = math.contiguous_true_regions(exitpoints) # find the total length of all exits exit_length = 0 for a, b in indices: exit_length += curves.curve_length(points[a:b + 1]) # handle the first and last point if they both belong to an exit if exitpoints[0] and exitpoints[-1]: exit_length += curves.point_distance(points[0], points[-1]) return exit_length
def adapt_tail_contours(self, tails): """ adapt tail contour to _frame, assuming that they are already close """ # get the tails that we want to adapt if self.params['detection/every_frame']: tails_estimate = self.locate_tails_roughly(tails) else: tails_estimate = tails[:] #< copy list if len(tails_estimate) != len(tails): raise RuntimeError('Found %d instead of %d tails in this frame.' % (len(tails), len(tails_estimate))) # setup active contour algorithm ac = ActiveContour(blur_radius=self.params['contour/blur_radius'], closed_loop=True, alpha=self.params['contour/line_tension'], beta=self.params['contour/bending_stiffness'], gamma=self.params['contour/adaptation_rate']) ac.max_iterations = self.params['contour/max_iterations'] #potential_approx = self.threshold_gradient_strength(gradient_mag) ac.set_potential(self.contour_potential) # debug.show_shape(*[t.contour for t in tails], # background=self.contour_potential) # get rectangle describing the interior of the _frame height, width = self._frame.shape region_center = shapes.Rectangle(0, 0, width, height) region_center.buffer(-self.params['contour/border_anchor_distance']) # iterate through all previous tails for k, tail_prev in enumerate(tails): # find the tail that is closest center = tail_prev.centroid idx = np.argmin([curves.point_distance(center, t.centroid) for t in tails_estimate]) # adapt this contour to the potential tail_estimate = tails_estimate.pop(idx) # determine the points close to the boundary that will be anchored # at their position, because there are no features to track at the # boundary anchor_idx = ~region_center.points_inside(tail_estimate.contour) # disable anchoring for points at the posterior end of the tail ps = tail_estimate.contour[anchor_idx] dists = spatial.distance.cdist(ps, [tail_estimate.endpoints[0]]) dist_threshold = self.params['contour/typical_width'] anchor_idx[anchor_idx] = (dists.flat > dist_threshold) # use an active contour algorithm to adapt the contour points contour = ac.find_contour(tail_estimate.contour, anchor_idx, anchor_idx) logging.debug('Active contour took %d iterations.', ac.info['iteration_count']) # update the old tail to keep the identity of sides tails[k] = Tail.create_similar(contour, tail_prev) return tails
def classify_mouse_track(self): """ classifies the mouse at all times """ self.log_event('Pass 2 - Start classifying the mouse.') # load the mouse, the ground, and the burrows mouse_track = self.data['pass2/mouse_trajectory'] ground_profile = self.data['pass2/ground_profile'] burrow_tracks = self.data['pass1/burrows/tracks'] # load some variables mouse_radius = self.params['mouse/model_radius'] trail_spacing = self.params['burrows/centerline_segment_length'] burrow_next_change = 0 mouse_trail = None for frame_id, mouse_pos in enumerate(mouse_track.pos): if not np.all(np.isfinite(mouse_pos)): # the mouse position is invalid continue # initialize variables state = {} # check the mouse position ground = ground_profile.get_ground_profile(frame_id) if ground is not None: if ground.above_ground(mouse_pos): state['underground'] = False if mouse_pos[1] + mouse_radius < ground.get_y(mouse_pos[0]): state['location'] = 'air' elif mouse_pos[1] < ground.midline: state['location'] = 'hill' else: state['location'] = 'valley' mouse_trail = None # get index of the ground line dist = np.linalg.norm(ground.points - mouse_pos[None, :], axis=1) ground_idx = np.argmin(dist) # get distance from ground line ground_dist = ground.linestring.distance(geometry.Point(mouse_pos)) else: state['underground'] = True # check the burrow structure if frame_id >= burrow_next_change: burrows, burrow_next_change = \ burrow_tracks.find_burrows(frame_id, ret_next_change=True) # check whether the mouse is inside a burrow mouse_point = geometry.Point(mouse_pos) for burrow in burrows: if burrow.polygon.contains(mouse_point): state['location'] = 'burrow' break # keep the ground index from last time ground_idx = mouse_track.ground_idx[frame_id - 1] # handle mouse trail if mouse_trail is None: # start a new mouse trail and initialize it with the # ground point closest to the mouse mouse_prev = mouse_track.pos[frame_id - 1] ground_point = curves.get_projection_point(ground.linestring, mouse_prev) mouse_trail = [ground_point, mouse_pos] else: # work with an existing mouse trail p_trail = mouse_trail[-2] if curves.point_distance(p_trail, mouse_pos) < trail_spacing: # old trail should be modified if len(mouse_trail) > 2: # check whether the trail has to be shortened p_trail = mouse_trail[-3] if curves.point_distance(p_trail, mouse_pos) < trail_spacing: del mouse_trail[-1] #< shorten trail mouse_trail[-1] = mouse_pos else: # old trail must be extended mouse_trail.append(mouse_pos) # get distance the mouse is under ground ground_dist = -curves.curve_length(mouse_trail) # set the mouse state mouse_track.set_state(frame_id, state, ground_idx, ground_dist) self.log_event('Pass 2 - Finished classifying the mouse.')
def classify_mouse_state(self, mouse_track): """ classifies the mouse in the current _frame """ if (not np.all(np.isfinite(self.mouse_pos)) or self.ground is None): # Not enough information to do anything self.mouse_trail = None return # initialize variables state = {} margin = self.params['mouse/model_radius']/2 mouse_radius = self.params['mouse/model_radius'] # check the horizontal position if self.mouse_pos[0] > self.background.shape[1]//2: state['position_horizontal'] = 'right' else: state['position_horizontal'] = 'left' # compare y value of mouse and ground (y-axis points down) if self.mouse_pos[1] > self.ground.get_y(self.mouse_pos[0]) + margin: # handle mouse trail ground_dist = self.extend_mouse_trail() # store the ground distance as a negative number ground_dist *= -1 # score the burrow based on its entry point if self.ground_idx is None: # only necessary if mouse starts inside burrow dist = np.linalg.norm(self.ground.points - self.mouse_pos[None, :], axis=1) self.ground_idx = np.argmin(dist) entry_point = self.ground.points[self.ground_idx] if entry_point[1] > self.ground.midline: state['location'] = 'burrow' else: state['location'] = 'dimple' # check whether we are at the end of the burrow for burrow in self.burrows: dist = curves.point_distance(burrow.end_point, self.mouse_pos) if dist < mouse_radius: state['location_detail'] = 'end point' break else: state['location_detail'] = 'general' else: if self.mouse_pos[1] + 2*mouse_radius < self.ground.get_y(self.mouse_pos[0]): state['location'] = 'air' elif self.mouse_pos[1] < self.ground.midline: state['location'] = 'hill' else: state['location'] = 'valley' state['location_detail'] = 'general' # get index of the ground line dist = np.linalg.norm(self.ground.points - self.mouse_pos[None, :], axis=1) self.ground_idx = np.argmin(dist) # get distance from ground line mouse_point = geometry.Point(self.mouse_pos) ground_dist = self.ground.linestring.distance(mouse_point) # report the distance as negative, if the mouse is under the ground line if self.mouse_pos[1] > self.ground.get_y(self.mouse_pos[0]): ground_dist *= -1 # reset the mouse trail since the mouse is over the ground self.mouse_trail = None # determine whether the mouse is moving or not velocity = self.data['pass2/mouse_trajectory'].velocity[self.frame_id, :] speed = np.hypot(velocity[0], velocity[1]) if speed > self.params['mouse/moving_threshold_pixel_frame']: state['dynamics'] = 'moving' else: state['dynamics'] = 'stationary' # set the mouse state mouse_track.set_state(self.frame_id, state, self.ground_idx, ground_dist)
def classify_mouse_state(self, mouse_track): """ classifies the mouse in the current _frame """ if (not np.all(np.isfinite(self.mouse_pos)) or self.ground is None): # Not enough information to do anything self.mouse_trail = None return # initialize variables state = {} margin = self.params['mouse/model_radius'] / 2 mouse_radius = self.params['mouse/model_radius'] # check the horizontal position if self.mouse_pos[0] > self.background.shape[1] // 2: state['position_horizontal'] = 'right' else: state['position_horizontal'] = 'left' # compare y value of mouse and ground (y-axis points down) if self.mouse_pos[1] > self.ground.get_y(self.mouse_pos[0]) + margin: # handle mouse trail ground_dist = self.extend_mouse_trail() # store the ground distance as a negative number ground_dist *= -1 # score the burrow based on its entry point if self.ground_idx is None: # only necessary if mouse starts inside burrow dist = np.linalg.norm(self.ground.points - self.mouse_pos[None, :], axis=1) self.ground_idx = np.argmin(dist) entry_point = self.ground.points[self.ground_idx] if entry_point[1] > self.ground.midline: state['location'] = 'burrow' else: state['location'] = 'dimple' # check whether we are at the end of the burrow for burrow in self.burrows: dist = curves.point_distance(burrow.end_point, self.mouse_pos) if dist < mouse_radius: state['location_detail'] = 'end point' break else: state['location_detail'] = 'general' else: if self.mouse_pos[1] + 2 * mouse_radius < self.ground.get_y( self.mouse_pos[0]): state['location'] = 'air' elif self.mouse_pos[1] < self.ground.midline: state['location'] = 'hill' else: state['location'] = 'valley' state['location_detail'] = 'general' # get index of the ground line dist = np.linalg.norm(self.ground.points - self.mouse_pos[None, :], axis=1) self.ground_idx = np.argmin(dist) # get distance from ground line mouse_point = geometry.Point(self.mouse_pos) ground_dist = self.ground.linestring.distance(mouse_point) # report the distance as negative, if the mouse is under the ground line if self.mouse_pos[1] > self.ground.get_y(self.mouse_pos[0]): ground_dist *= -1 # reset the mouse trail since the mouse is over the ground self.mouse_trail = None # determine whether the mouse is moving or not velocity = self.data['pass2/mouse_trajectory'].velocity[ self.frame_id, :] speed = np.hypot(velocity[0], velocity[1]) if speed > self.params['mouse/moving_threshold_pixel_frame']: state['dynamics'] = 'moving' else: state['dynamics'] = 'stationary' # set the mouse state mouse_track.set_state(self.frame_id, state, self.ground_idx, ground_dist)