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
Ejemplo n.º 2
0
 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
Ejemplo n.º 3
0
    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
Ejemplo n.º 4
0
    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)
Ejemplo n.º 5
0
    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)
Ejemplo n.º 6
0
    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
Ejemplo n.º 7
0
 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
Ejemplo n.º 8
0
    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
Ejemplo n.º 9
0
    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
Ejemplo n.º 11
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
            
            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)]
Ejemplo n.º 12
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)
        ]
Ejemplo n.º 13
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