def get_burrows_from_image(self, mask, ground_line): """ load burrow polygons from an image """ # turn image into gray scale height, width = mask.shape # get a polygon for cutting away the sky above_ground = ground_line.get_polygon(0, left=0, right=width) # determine contours in the mask contours = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)[1] # iterate through the contours burrows = [] for contour in contours: points = contour[:, 0, :] if len(points) <= 2: continue # get the burrow area area = cv2.contourArea(contour) if area < self.params['scale_bar/area_max']: # object could be a scale bar rect = shapes.Rectangle(*cv2.boundingRect(contour)) at_left = (rect.left < self.params['scale_bar/dist_left']*width) max_dist_bottom = self.params['scale_bar/dist_bottom'] at_bottom = (rect.bottom > (1 - max_dist_bottom) * height) hull = cv2.convexHull(contour) hull_area = cv2.contourArea(hull) is_simple = (hull_area < 2*area) if at_left and at_bottom and is_simple: # the current polygon is the scale bar _, (w, h), _ = cv2.minAreaRect(contour) if max(w, h) > self.params['scale_bar/length_min']: raise RuntimeError('Found something that looks like a ' 'scale bar') if area > self.params['burrow/area_min']: # build polygon out of the contour points burrow_poly = geometry.Polygon(points) # regularize the points to remove potential problems burrow_poly = regions.regularize_polygon(burrow_poly) # build the burrow polygon by removing the sky burrow_poly = burrow_poly.difference(above_ground) # create a burrow from the outline boundary = regions.get_enclosing_outline(burrow_poly) burrow = Burrow(boundary.coords, parameters=self.params['burrow_parameters']) burrows.append(burrow) logging.info('Found %d polygon(s)' % len(burrows)) return burrows
def merge(self, other): """ merge this burrow with another one """ polygon = self.polygon.union(other.polygon) self.contour = regions.get_enclosing_outline(polygon) # set the centerline to the longest of the two if other.length > self.length: self.centerline = other.centerline
def extend_outline(self, extension_polygon, simplify_threshold): """ extends the contour of the burrow to also enclose the object given by polygon """ # get the union of the burrow and the extension burrow = self.polygon.union(extension_polygon) # determine the contour of the union outline = regions.get_enclosing_outline(burrow) outline = outline.simplify(simplify_threshold*outline.length) self.contour = np.asarray(outline, np.int32)
def extend_outline(self, extension_polygon, simplify_threshold): """ extends the contour of the burrow to also enclose the object given by polygon """ # get the union of the burrow and the extension burrow = self.polygon.union(extension_polygon) # determine the contour of the union outline = regions.get_enclosing_outline(burrow) outline = outline.simplify(simplify_threshold * outline.length) self.contour = np.asarray(outline, np.int32)
def _connect_burrow_to_structure(self, contour, structure): """ extends the burrow contour such that it connects to the ground line or to other burrows """ outline = geometry.Polygon(contour) # determine burrow points close to the structure dist = structure.distance(outline) conn_points = [] while len(conn_points) == 0: dist += self.params['burrows/width']/2 conn_points = [point for point in contour if structure.distance(geometry.Point(point)) < dist] conn_points = np.array(conn_points) # cluster the points to detect multiple connections # this is important when a burrow has multiple exits to the ground if len(conn_points) >= 2: dist_max = self.params['burrows/width'] data = cluster.hierarchy.fclusterdata(conn_points, dist_max, method='single', criterion='distance') else: data = np.ones(1, np.int) burrow_width_min = self.params['burrows/width_min'] for cluster_id in np.unique(data): p_exit = conn_points[data == cluster_id].mean(axis=0) p_ground = curves.get_projection_point(structure, p_exit) line = geometry.LineString((p_exit, p_ground)) tunnel = line.buffer(distance=burrow_width_min/2, cap_style=geometry.CAP_STYLE.flat) # add this to the burrow outline outline = outline.union(tunnel.buffer(0.1)) # get the contour points outline = regions.get_enclosing_outline(outline) outline = regions.regularize_linear_ring(outline) contour = np.array(outline.coords) # fill the burrow mask, such that this extension does not have to be # done next time again cv2.fillPoly(self.burrow_mask, [np.asarray(contour, np.int32)], 1) return contour
def extend_burrow_by_mouse_trail(self, burrow): """ takes a burrow shape and extends it using the current mouse trail """ if 'cage_interior_rectangle' in self._cache: cage_interior_rect = self._cache['cage_interior_rectangle'] else: w, h = self.video.size points = [[1, 1], [w - 1, 1], [w - 1, h - 1], [1, h - 1]] cage_interior_rect = geometry.Polygon(points) self._cache['cage_interior_rectangle'] = cage_interior_rect # get the buffered mouse trail trail_width = self.params['burrows/width_min'] mouse_trail = geometry.LineString(self.mouse_trail) mouse_trail_buffered = mouse_trail.buffer(trail_width) # extend the burrow contour by the mouse trail and restrict it to the # cage interior polygon = burrow.polygon.union(mouse_trail_buffered) polygon = polygon.intersection(cage_interior_rect) burrow.contour = regions.get_enclosing_outline(polygon) # update the centerline if the mouse trail is longer if mouse_trail.length > burrow.length: burrow.centerline = self.mouse_trail
def get_features(self, tails=None, use_annotations=False, ret_raw=False): """ calculates a feature mask based on the image statistics """ # calculate image statistics ksize = self.params['detection/statistics_window'] _, var = image.get_image_statistics(self._frame, kernel='circle', ksize=ksize) # threshold the variance to locate features threshold = self.params['detection/statistics_threshold']*np.median(var) bw = (var > threshold).astype(np.uint8) if ret_raw: return bw if tails is None: tails = tuple() # add features from the previous frames if present for tail in tails: # fill the features with the interior of the former tail polys = tail.polygon.buffer(-self.params['detection/shape_max_speed']) if not isinstance(polys, geometry.MultiPolygon): polys = [polys] for poly in polys: cv2.fillPoly(bw, [np.array(poly.exterior.coords, np.int)], color=1) # calculate the distance to other tails to bound the current one buffer_dist = self.params['detection/shape_max_speed'] poly_outer = tail.polygon.buffer(buffer_dist) for tail_other in tails: if tail is not tail_other: dist = tail.polygon.distance(tail_other.polygon) if dist < buffer_dist: # shrink poly_outer poly_other = tail_other.polygon.buffer(dist/2) poly_outer = poly_outer.difference(poly_other) # make sure that this tail is separated from all the others try: coords = np.array(poly_outer.exterior.coords, np.int) except AttributeError: # can happen when the poly_outer is complex pass else: cv2.polylines(bw, [coords], isClosed=True, color=0, thickness=2) # debug.show_image(self._frame, bw, wait_for_key=False) if use_annotations: lines = self.annotations['segmentation_dividers'] if lines: logging.debug('Found %d annotation lines for segmenting', len(lines)) for line in lines: cv2.line(bw, tuple(line[0]), tuple(line[1]), 0, thickness=3) else: logging.debug('Found no annotations for segmenting') # remove features at the edge of the image border = self.params['detection/border_distance'] image.set_image_border(bw, size=border, color=0) # remove very thin features cv2.morphologyEx(bw, cv2.MORPH_OPEN, np.ones((3, 3)), dst=bw) # find features in the binary image contours = cv2.findContours(bw.astype(np.uint8), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)[1] # determine the rectangle where objects can lie in h, w = self._frame.shape rect = shapes.Rectangle(x=0, y=0, width=w, height=h) rect.buffer(-2*border) rect = geometry.Polygon(rect.contour) boundary_length_max = self.params['detection/boundary_length_max'] bw[:] = 0 num_features = 0 for contour in contours: if cv2.contourArea(contour) > self.params['detection/area_min']: # check whether the object touches the border feature = geometry.Polygon(np.squeeze(contour)) if rect.exterior.intersects(feature): # fill the hole in the feature difference = rect.difference(feature) if isinstance(difference, geometry.Polygon): difference = [difference] #< make sure we handle a list for diff in difference: # check the length along the rectangle boundary_length = diff.intersection(rect.exterior).length if boundary_length < boundary_length_max: feature = feature.union(diff) # reduce feature, since detection typically overshoots features = feature.buffer(-0.5*self.params['detection/statistics_window']) if not isinstance(features, geometry.MultiPolygon): features = [features] for feature in features: if feature.area > self.params['detection/area_min']: #debug.show_shape(feature, background=self._frame) # extract the contour of the feature contour = regions.get_enclosing_outline(feature) contour = np.array(contour.coords, np.int) # fill holes inside the objects num_features += 1 cv2.fillPoly(bw, [contour], num_features) # debug.show_image(self._frame, var, bw, wait_for_key=False) return bw, num_features
def get_burrows_from_image(self, mask, ground_line): """ load burrow polygons from an image """ # turn image into gray scale height, width = mask.shape # get a polygon for cutting away the sky above_ground = ground_line.get_polygon(0, left=0, right=width) # determine contours in the mask contours = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)[1] # iterate through the contours burrows = [] for contour in contours: points = contour[:, 0, :] if len(points) <= 2: continue # get the burrow area area = cv2.contourArea(contour) if area < self.params['scale_bar/area_max']: # object could be a scale bar rect = shapes.Rectangle(*cv2.boundingRect(contour)) at_left = (rect.left < self.params['scale_bar/dist_left'] * width) max_dist_bottom = self.params['scale_bar/dist_bottom'] at_bottom = (rect.bottom > (1 - max_dist_bottom) * height) hull = cv2.convexHull(contour) hull_area = cv2.contourArea(hull) is_simple = (hull_area < 2 * area) if at_left and at_bottom and is_simple: # the current polygon is the scale bar _, (w, h), _ = cv2.minAreaRect(contour) if max(w, h) > self.params['scale_bar/length_min']: raise RuntimeError('Found something that looks like a ' 'scale bar') if area > self.params['burrow/area_min']: # build polygon out of the contour points burrow_poly = geometry.Polygon(points) # regularize the points to remove potential problems burrow_poly = regions.regularize_polygon(burrow_poly) # debug.show_shape(geometry.Polygon(points), above_ground, # background=mask) # build the burrow polygon by removing the sky burrow_poly = burrow_poly.difference(above_ground) # create a burrow from the outline boundary = regions.get_enclosing_outline(burrow_poly) burrow = Burrow(boundary.coords, parameters=self.params['burrow_parameters']) burrows.append(burrow) logging.info('Found %d polygons' % len(burrows)) return burrows
def store_burrows(self): """ associates the current burrows with burrow tracks """ burrow_tracks = self.result['burrows/tracks'] ground_polygon = geometry.Polygon(self.get_ground_polygon_points()) # check whether we already know this burrow # the burrows in self.burrows will always be larger than the burrows # in self.active_burrows. Consequently, it can happen that a current # burrow overlaps two older burrows, but the reverse cannot be true for burrow in self.burrows: # find all tracks to which this burrow may belong track_ids = [track_id for track_id, burrow_last in self.active_burrows() if burrow_last.intersects(burrow)] if len(track_ids) > 1: # merge all burrows to a single track and keep the largest one track_longest, length_max = None, 0 for track_id in track_ids: burrow_last = burrow_tracks[track_id].last # find track with longest burrow if burrow_last.length > length_max: track_longest, length_max = track_id, burrow_last.length # merge the burrows burrow.merge(burrow_last) # keep the burrow parts that are below the ground line try: polygon = burrow.polygon.intersection(ground_polygon) except geos.TopologicalError: continue if polygon.is_empty: continue try: burrow.contour = regions.get_enclosing_outline(polygon) except TypeError: # can occur in corner cases where the enclosing outline cannot # be found continue # make sure that the burrow centerline lies within the ground region ground_poly = geometry.Polygon(self.get_ground_polygon_points()) if burrow.linestring.length > 0: line = burrow.linestring.intersection(ground_poly) else: line = None if isinstance(line, geometry.multilinestring.MultiLineString): # pick the longest line if there are multiple index_longest = np.argmax(l.length for l in line) line = line[index_longest] is_line = isinstance(line, geometry.linestring.LineString) if not is_line or line.is_empty or line.length <= 1: # the centerline disappeared # => calculate a new centerline from the burrow contour end_point = self.burrow_estimate_exit(burrow)[0] self.calculate_burrow_centerline(burrow, point_start=end_point) else: # adjust the burrow centerline to reach to the ground line # it could be that the whole line was underground # => move the first data point onto the ground line line = np.array(line, np.double) line[0] = curves.get_projection_point(self.ground.linestring, line[0]) # set the updated burrow centerline burrow.centerline = line # store the burrow if it is valid if burrow.is_valid: if len(track_ids) > 1: # add the burrow to the longest track burrow_tracks[track_longest].append(self.frame_id, burrow) elif len(track_ids) == 1: # add the burrow to the matching track burrow_tracks[track_ids[0]].append(self.frame_id, burrow) else: # create the burrow track burrow_track = BurrowTrack(self.frame_id, burrow) burrow_tracks.append(burrow_track) # use the new set of burrows in the next iterations self.burrows = [b.copy() for _, b in self.active_burrows(time_interval=0)]
def store_burrows(self): """ associates the current burrows with burrow tracks """ burrow_tracks = self.result['burrows/tracks'] ground_polygon = geometry.Polygon(self.get_ground_polygon_points()) # check whether we already know this burrow # the burrows in self.burrows will always be larger than the burrows # in self.active_burrows. Consequently, it can happen that a current # burrow overlaps two older burrows, but the reverse cannot be true for burrow in self.burrows: # find all tracks to which this burrow may belong track_ids = [ track_id for track_id, burrow_last in self.active_burrows() if burrow_last.intersects(burrow) ] if len(track_ids) > 1: # merge all burrows to a single track and keep the largest one track_longest, length_max = None, 0 for track_id in track_ids: burrow_last = burrow_tracks[track_id].last # find track with longest burrow if burrow_last.length > length_max: track_longest, length_max = track_id, burrow_last.length # merge the burrows burrow.merge(burrow_last) # keep the burrow parts that are below the ground line try: polygon = burrow.polygon.intersection(ground_polygon) except geos.TopologicalError: continue if polygon.is_empty: continue burrow.contour = regions.get_enclosing_outline(polygon) # make sure that the burrow centerline lies within the ground region ground_poly = geometry.Polygon(self.get_ground_polygon_points()) line = burrow.linestring.intersection(ground_poly) if isinstance(line, geometry.multilinestring.MultiLineString): # pick the longest line if there are multiple index_longest = np.argmax(l.length for l in line) line = line[index_longest] is_line = isinstance(line, geometry.linestring.LineString) if not is_line or line.is_empty or line.length <= 1: # the centerline disappeared # => calculate a new centerline from the burrow contour end_point = self.burrow_estimate_exit(burrow)[0] self.calculate_burrow_centerline(burrow, point_start=end_point) else: # adjust the burrow centerline to reach to the ground line # it could be that the whole line was underground # => move the first data point onto the ground line line = np.array(line, np.double) line[0] = curves.get_projection_point(self.ground.linestring, line[0]) # set the updated burrow centerline burrow.centerline = line # store the burrow if it is valid if burrow.is_valid: if len(track_ids) > 1: # add the burrow to the longest track burrow_tracks[track_longest].append(self.frame_id, burrow) elif len(track_ids) == 1: # add the burrow to the matching track burrow_tracks[track_ids[0]].append(self.frame_id, burrow) else: # create the burrow track burrow_track = BurrowTrack(self.frame_id, burrow) burrow_tracks.append(burrow_track) # use the new set of burrows in the next iterations self.burrows = [ b.copy() for _, b in self.active_burrows(time_interval=0) ]
def connect_burrow_chunks(self, burrow_chunks): """ takes a list of burrow chunks and connects them such that in the end all burrow chunks are connected to the ground line. """ if len(burrow_chunks) == 0: return [] dist_max = self.params['burrows/chunk_dist_max'] # build the contour profiles of the burrow chunks linear_rings = [geometry.LinearRing(c) for c in burrow_chunks] # handle all burrows close to the ground connected, disconnected = [], [] for k, ring in enumerate(linear_rings): ground_dist = self.ground.linestring.distance(ring) if ground_dist < dist_max: # burrow is close to ground if 1 < ground_dist: burrow_chunks[k] = \ self._connect_burrow_to_structure(burrow_chunks[k], self.ground.linestring) connected.append(k) else: disconnected.append(k) assert (set(connected) | set(disconnected)) == set(range(len(burrow_chunks))) # calculate distances to other burrows burrow_dist = np.empty([len(burrow_chunks)]*2) np.fill_diagonal(burrow_dist, np.inf) for x, contour1 in enumerate(linear_rings): for y, contour2 in enumerate(linear_rings[x+1:], x+1): dist = contour1.distance(contour2) burrow_dist[x, y] = dist burrow_dist[y, x] = dist # handle all remaining chunks, which need to be connected to other chunks while connected and disconnected: # find chunks which is closest to all the others dist = burrow_dist[disconnected, :][:, connected] k1, k2 = np.unravel_index(dist.argmin(), dist.shape) if dist[k1, k2] > dist_max: # don't connect structures that are too far from each other break c1, c2 = disconnected[k1], connected[k2] # k1 is chunk to connect, k2 is closest chunk to connect it to # connect the current chunk to the other structure structure = geometry.LinearRing(burrow_chunks[c2]) enlarged_chunk = self._connect_burrow_to_structure(burrow_chunks[c1], structure) # merge the two structures poly1 = geometry.Polygon(enlarged_chunk) poly2 = regions.regularize_polygon(geometry.Polygon(structure)) poly = poly1.union(poly2).buffer(0.1) # find and regularize the common contour contour = regions.get_enclosing_outline(poly) contour = regions.regularize_linear_ring(contour) contour = contour.coords # replace the current chunk by the merged one burrow_chunks[c1] = contour # replace all other burrow chunks with the same id id_c2 = id(burrow_chunks[c2]) for k, bc in enumerate(burrow_chunks): if id(bc) == id_c2: burrow_chunks[k] = contour # mark the cluster as connected del disconnected[k1] connected.append(c1) # return the unique burrow structures burrows = [] connected_chunks = (burrow_chunks[k] for k in connected) for contour in unique_based_on_id(connected_chunks): contour = regions.regularize_contour_points(contour) try: burrow = Burrow(contour) except ValueError: continue else: if burrow.area >= self.params['burrows/area_min']: burrows.append(burrow) return burrows