예제 #1
0
    def add_class_results(self, draw, track, frame_offset, rect,
                          track_predictions, screen_bounds):
        prediction = track_predictions[track]
        if track not in track_predictions:
            return

        label = globs._classifier.labels[prediction.label_at_time(
            frame_offset)]
        score = prediction.score_at_time(frame_offset) * 10
        novelty = prediction.novelty_history[frame_offset]
        prediction_format = "({:.1f} {})\nnovelty={:.2f}"
        current_prediction_string = prediction_format.format(
            score * 10, label, novelty)

        header_size = self.font_title.getsize(self.track_descs[track])
        footer_size = self.font.getsize(current_prediction_string)

        # figure out where to draw everything
        header_rect = Region(rect.left * self.frame_scale,
                             rect.top * self.frame_scale - header_size[1],
                             header_size[0], header_size[1])
        footer_center = ((rect.width * self.frame_scale) - footer_size[0]) / 2
        footer_rect = Region(rect.left * self.frame_scale + footer_center,
                             rect.bottom * self.frame_scale, footer_size[0],
                             footer_size[1])

        self.fit_to_image(header_rect, screen_bounds)
        self.fit_to_image(footer_rect, screen_bounds)

        draw.text((header_rect.x, header_rect.y),
                  self.track_descs[track],
                  font=self.font_title)
        draw.text((footer_rect.x, footer_rect.y),
                  current_prediction_string,
                  font=self.font)
def add_text_to_track(draw,
                      rect,
                      header_text,
                      footer_text,
                      screen_bounds,
                      v_offset=0,
                      scale=1):
    font = get_font()
    font_title = get_title_font()
    header_size = font_title.getsize(header_text)
    footer_size = font.getsize(footer_text)
    # figure out where to draw everything
    header_rect = Region(
        rect.left * scale,
        (v_offset + rect.top) * scale - header_size[1],
        header_size[0],
        header_size[1],
    )
    footer_center = ((rect.width * scale) - footer_size[0]) / 2
    footer_rect = Region(
        rect.left * scale + footer_center,
        (v_offset + rect.bottom) * scale,
        footer_size[0],
        footer_size[1],
    )

    fit_to_image(header_rect, screen_bounds)
    fit_to_image(footer_rect, screen_bounds)

    draw.text((header_rect.x, header_rect.y), header_text, font=font_title)
    draw.text((footer_rect.x, footer_rect.y), footer_text, font=font)
예제 #3
0
    def export_clip_preview(self,
                            filename,
                            tracker: TrackExtractor,
                            track_predictions=None):
        """
        Exports a clip showing the tracking and predictions for objects within the clip.
        """

        # increased resolution of video file.
        # videos look much better scaled up
        if tracker.stats:
            self.auto_max = tracker.stats['max_temp']
            self.auto_min = tracker.stats['min_temp']
        else:
            print("Do not have temperatures to use.")
            return

        if bool(track_predictions
                ) and self.preview_type == self.PREVIEW_CLASSIFIED:
            self.create_track_descriptions(tracker, track_predictions)

        if self.preview_type == self.PREVIEW_TRACKING and not tracker.frame_buffer.flow:
            tracker.generate_optical_flow()

        mpeg = MPEGCreator(filename)

        for frame_number, thermal in enumerate(tracker.frame_buffer.thermal):
            if self.preview_type == self.PREVIEW_RAW:
                image = self.convert_and_resize(thermal)

            if self.preview_type == self.PREVIEW_TRACKING:
                image = self.create_four_tracking_image(
                    tracker.frame_buffer, frame_number)
                image = self.convert_and_resize(image, 3.0, mode=Image.NEAREST)
                draw = ImageDraw.Draw(image)
                regions = tracker.region_history[frame_number]
                self.add_regions(draw, regions)
                self.add_regions(draw, regions, v_offset=120)
                self.add_tracks(draw, tracker.tracks, frame_number)

            if self.preview_type == self.PREVIEW_BOXES:
                image = self.convert_and_resize(thermal, 4.0)
                draw = ImageDraw.Draw(image)
                screen_bounds = Region(0, 0, image.width, image.height)
                self.add_tracks(draw, tracker.tracks, frame_number)

            if self.preview_type == self.PREVIEW_CLASSIFIED:
                image = self.convert_and_resize(thermal, 4.0)
                draw = ImageDraw.Draw(image)
                screen_bounds = Region(0, 0, image.width, image.height)
                self.add_tracks(draw, tracker.tracks, frame_number,
                                track_predictions, screen_bounds)

            mpeg.next_frame(np.asarray(image))

            # we store the entire video in memory so we need to cap the frame count at some point.
            if frame_number > 9 * 60 * 10:
                break
        mpeg.close()
    def add_debug_text(
        draw,
        track,
        region,
        screen_bounds,
        text=None,
        v_offset=0,
        frame_offset=0,
        scale=1,
    ):
        font = get_font()
        if text is None:
            text = "id {}".format(track.get_id())
            if region.pixel_variance:
                text += "mass {} var {} vel ({},{})".format(
                    region.mass,
                    round(region.pixel_variance, 2),
                    track.vel_x[frame_offset],
                    track.vel_y[frame_offset],
                )
        footer_size = font.getsize(text)
        footer_center = (
            (region.width * self.frame_scale) - footer_size[0]) / 2

        footer_rect = Region(
            region.right * scale - footer_center / 2.0,
            (v_offset + region.bottom) * self.frame_scale,
            footer_size[0],
            footer_size[1],
        )
        fit_to_image(footer_rect, screen_bounds)

        draw.text((footer_rect.x, footer_rect.y), text, font=font)
예제 #5
0
    def smooth(self, frame_bounds: Rectangle):
        """
        Smooths out any quick changes in track dimensions
        :param frame_bounds The boundaries of the video frame.
        """
        if len(self.bounds_history) == 0:
            return

        new_bounds_history = []
        prev_frame = self.bounds_history[0]
        current_frame = self.bounds_history[0]
        next_frame = self.bounds_history[1]

        for i in range(len(self.bounds_history)):

            prev_frame = self.bounds_history[max(0, i-1)]
            current_frame = self.bounds_history[i]
            next_frame = self.bounds_history[min(len(self.bounds_history)-1, i+1)]

            frame_x = current_frame.mid_x
            frame_y = current_frame.mid_y
            frame_width = (prev_frame.width + current_frame.width + next_frame.width) / 3
            frame_height = (prev_frame.height + current_frame.height + next_frame.height) / 3
            frame = Region(int(frame_x - frame_width / 2), int(frame_y - frame_height / 2), int(frame_width), int(frame_height))
            frame.crop(frame_bounds)

            new_bounds_history.append(frame)

        self.bounds_history = new_bounds_history
예제 #6
0
def best_trackless_region(clip):
    """Choose a frame for clips without any track"""
    best_region = None
    THUMBNAIL_SIZE = 64

    # if we have regions take best mass of un tracked regions
    for regions in clip.region_history:
        for region in regions:
            if best_region is None or region.mass > best_region.mass:
                best_region = region
    if best_region is not None:
        return best_region

    # take region with greatest filtered mean values, and
    # if zero take thermal mean values
    best_frame_i = np.argmax(clip.stats.frame_stats_mean)
    best_frame = clip.frame_buffer.get_frame(best_frame_i).thermal
    frame_height, frame_width = best_frame.shape
    best_filtered = best_frame - clip.background
    best_region = None
    for y in range(frame_height - THUMBNAIL_SIZE):
        for x in range(frame_width - THUMBNAIL_SIZE):
            thermal_sum = np.mean(
                best_frame[y : y + THUMBNAIL_SIZE, x : x + THUMBNAIL_SIZE]
            )
            filtered_sum = np.mean(
                best_filtered[y : y + THUMBNAIL_SIZE, x : x + THUMBNAIL_SIZE]
            )
            if best_region is None:
                best_region = ((y, x), filtered_sum, thermal_sum)
            elif best_region[1] > 0:
                if best_region[1] < filtered_sum:
                    best_region = ((y, x), thermal_sum, filtered_sum)
            elif best_region[2] < thermal_sum:
                best_region = ((y, x), thermal_sum, filtered_sum)
    return Region(
        best_region[0][1],
        best_region[0][1],
        THUMBNAIL_SIZE,
        THUMBNAIL_SIZE,
        frame_number=best_frame_i,
    )
 def add_blank_frame(self, buffer_frame=None):
     """Maintains same bounds as previously, does not reset framce_since_target_seen counter"""
     if self.frames > Track.MIN_KALMAN_FRAMES:
         region = Region(
             int(self.predicted_mid[0] - self.last_bound.width / 2.0),
             int(self.predicted_mid[1] - self.last_bound.height / 2.0),
             self.last_bound.width,
             self.last_bound.height,
         )
         if self.crop_rectangle:
             region.crop(self.crop_rectangle)
     else:
         region = self.last_bound.copy()
     region.blank = True
     region.mass = 0
     region.pixel_variance = 0
     region.frame_number = self.last_bound.frame_number + 1
     self.bounds_history.append(region)
     self.prev_frame_num = region.frame_number
     self.update_velocity()
     self.blank_frames += 1
     self.frames_since_target_seen += 1
     prediction = self.kalman_tracker.predict()
     self.predicted_mid = (prediction[0][0], prediction[1][0])
예제 #8
0
    def _get_regions_of_interest(
        self, clip, component_details, filtered, prev_filtered
    ):
        """
                Calculates pixels of interest mask from filtered image, and returns both the labeled mask and their bounding
                rectangles.
                :param filtered: The filtered frame
        =        :return: regions of interest, mask frame
        """

        if prev_filtered is not None:
            delta_frame = np.abs(filtered - prev_filtered)
        else:
            delta_frame = None

        # we enlarge the rects a bit, partly because we eroded them previously, and partly because we want some context.
        padding = self.frame_padding
        # find regions of interest
        regions = []
        for i, component in enumerate(component_details[1:]):

            region = Region(
                component[0],
                component[1],
                component[2],
                component[3],
                mass=component[4],
                id=i,
                frame_number=clip.frame_on,
            )
            old_region = region.copy()
            region.crop(clip.crop_rectangle)
            region.was_cropped = str(old_region) != str(region)
            region.set_is_along_border(clip.crop_rectangle)
            if self.config.cropped_regions_strategy == "cautious":
                crop_width_fraction = (
                    old_region.width - region.width
                ) / old_region.width
                crop_height_fraction = (
                    old_region.height - region.height
                ) / old_region.height
                if crop_width_fraction > 0.25 or crop_height_fraction > 0.25:
                    continue
            elif (
                self.config.cropped_regions_strategy == "none"
                or self.config.cropped_regions_strategy is None
            ):
                if region.was_cropped:
                    continue
            elif self.config.cropped_regions_strategy != "all":
                raise ValueError(
                    "Invalid mode for CROPPED_REGIONS_STRATEGY, expected ['all','cautious','none'] but found {}".format(
                        self.config.cropped_regions_strategy
                    )
                )
            region.enlarge(padding, max=clip.crop_rectangle)

            if delta_frame is not None:
                region_difference = region.subimage(delta_frame)
                region.pixel_variance = np.var(region_difference)

            # filter out regions that are probably just noise
            if (
                region.pixel_variance < self.config.aoi_pixel_variance
                and region.mass < self.config.aoi_min_mass
            ):
                continue
            regions.append(region)
        return regions
예제 #9
0
    def _get_regions_of_interest(self, clip, labels, stats, thresh, filtered,
                                 prev_filtered, mass):
        """
                Calculates pixels of interest mask from filtered image, and returns both the labeled mask and their bounding
                rectangles.
                :param filtered: The filtered frame
        =        :return: regions of interest, mask frame
        """

        if prev_filtered is not None:
            delta_frame = np.abs(filtered - prev_filtered)
        else:
            delta_frame = None

        # we enlarge the rects a bit, partly because we eroded them previously, and partly because we want some context.
        padding = self.frame_padding
        edge = self.config.edge_pixels
        # find regions of interest
        regions = []
        for i in range(1, labels):

            region = Region(
                stats[i, 0],
                stats[i, 1],
                stats[i, 2],
                stats[i, 3],
                stats[i, 4],
                0,
                i,
                clip.frame_on,
            )
            # want the real mass calculated from before the dilation
            # region.mass = np.sum(region.subimage(thresh))
            region.mass = mass
            # Add padding to region and change coordinates from edgeless image -> full image
            region.x += edge - padding
            region.y += edge - padding
            region.width += padding * 2
            region.height += padding * 2

            old_region = region.copy()
            region.crop(clip.crop_rectangle)
            region.was_cropped = str(old_region) != str(region)
            region.set_is_along_border(clip.crop_rectangle)
            if self.config.cropped_regions_strategy == "cautious":
                crop_width_fraction = (old_region.width -
                                       region.width) / old_region.width
                crop_height_fraction = (old_region.height -
                                        region.height) / old_region.height
                if crop_width_fraction > 0.25 or crop_height_fraction > 0.25:
                    continue
            elif self.config.cropped_regions_strategy == "none":
                if region.was_cropped:
                    continue
            elif self.config.cropped_regions_strategy != "all":
                raise ValueError(
                    "Invalid mode for CROPPED_REGIONS_STRATEGY, expected ['all','cautious','none'] but found {}"
                    .format(self.config.cropped_regions_strategy))

            if delta_frame is not None:
                region_difference = region.subimage(delta_frame)
                region.pixel_variance = np.var(region_difference)

            # filter out regions that are probably just noise
            if (region.pixel_variance < self.config.aoi_pixel_variance
                    and region.mass < self.config.aoi_min_mass):
                continue
            regions.append(region)
        return regions
    def export_clip_preview(self, filename, clip: Clip, predictions=None):
        """
        Exports a clip showing the tracking and predictions for objects within the clip.
        """

        logging.info("creating clip preview %s", filename)

        # increased resolution of video file.
        # videos look much better scaled up
        if not clip.stats:
            logging.error("Do not have temperatures to use.")
            return

        if self.debug:
            footer = Previewer.stats_footer(clip.stats)
        if predictions and (self.preview_type == self.PREVIEW_CLASSIFIED
                            or self.preview_type == self.PREVIEW_TRACKING):
            self.create_track_descriptions(clip, predictions)

        if clip.stats.min_temp is None or clip.stats.max_temp is None:
            thermals = [frame.thermal for frame in clip.frame_buffer.frames]
            clip.stats.min_temp = np.amin(thermals)
            clip.stats.max_temp = np.amax(thermals)
        mpeg = MPEGCreator(filename)
        frame_scale = 4.0
        for frame_number, frame in enumerate(clip.frame_buffer):
            if self.preview_type == self.PREVIEW_RAW:
                image = self.convert_and_resize(frame.thermal,
                                                clip.stats.min_temp,
                                                clip.stats.max_temp)
                draw = ImageDraw.Draw(image)
            elif self.preview_type == self.PREVIEW_TRACKING:
                image = self.create_four_tracking_image(
                    frame,
                    clip.stats.min_temp,
                    clip.stats.max_temp,
                )
                draw = ImageDraw.Draw(image)
                self.add_tracks(
                    draw,
                    clip.tracks,
                    frame_number,
                    predictions,
                    scale=frame_scale,
                )

            elif self.preview_type == self.PREVIEW_BOXES:
                image = self.convert_and_resize(
                    frame.thermal,
                    clip.stats.min_temp,
                    clip.stats.max_temp,
                    frame_scale=frame_scale,
                )
                draw = ImageDraw.Draw(image)
                screen_bounds = Region(0, 0, image.width, image.height)
                self.add_tracks(
                    draw,
                    clip.tracks,
                    frame_number,
                    colours=[(128, 255, 255)],
                    scale=frame_scale,
                )

            elif self.preview_type == self.PREVIEW_CLASSIFIED:
                image = self.convert_and_resize(
                    frame.thermal,
                    clip.stats.min_temp,
                    clip.stats.max_temp,
                    frame_scale=frame_scale,
                )
                draw = ImageDraw.Draw(image)
                screen_bounds = Region(0, 0, image.width, image.height)
                self.add_tracks(
                    draw,
                    clip.tracks,
                    frame_number,
                    predictions,
                    screen_bounds,
                    scale=frame_scale,
                )
            if frame.ffc_affected:
                self.add_header(draw, image.width, image.height,
                                "Calibrating ...")
            if self.debug and draw:
                self.add_footer(draw, image.width, image.height, footer,
                                frame.ffc_affected)
            mpeg.next_frame(np.asarray(image))

            # we store the entire video in memory so we need to cap the frame count at some point.
            if frame_number > clip.frames_per_second * 60 * 10:
                break
        clip.frame_buffer.close_cache()
        mpeg.close()
예제 #11
0
    def get_regions_of_interest(self, filtered, prev_filtered=None):
        """
        Calculates pixels of interest mask from filtered image, and returns both the labeled mask and their bounding
        rectangles.
        :param filtered: The filtered frame
        :param prev_filtered: The previous filtered frame, required for pixel deltas to be calculated
        :return: regions of interest, mask frame
        """

        frame_height, frame_width = filtered.shape

        # get frames change
        if prev_filtered is not None:
            # we need a lot of precision because the values are squared.  Float32 should work.
            delta_frame = np.abs(np.float32(filtered) -
                                 np.float32(prev_filtered))
        else:
            delta_frame = None

        # remove the edges of the frame as we know these pixels can be spurious value
        edgeless_filtered = self.crop_rectangle.subimage(filtered)

        thresh = np.uint8(blur_and_return_as_mask(edgeless_filtered, threshold=self.threshold))
        dilated = thresh

        # Dilation groups interested pixels that are near to each other into one component(animal/track)
        if self.config.dilation_pixels > 0:
            size = self.config.dilation_pixels * 2 + 1
            kernel = np.ones((size, size), np.uint8)
            dilated = cv2.dilate(dilated, kernel, iterations=1)

        labels, small_mask, stats, _ = cv2.connectedComponentsWithStats(dilated)

        # make mask go back to full frame size without edges chopped
        edge = self.config.edge_pixels
        mask = np.zeros(filtered.shape)
        mask[edge:frame_height - edge, edge:frame_width - edge] = small_mask

        # we enlarge the rects a bit, partly because we eroded them previously, and partly because we want some context.
        padding = self.frame_padding

        # find regions of interest
        regions = []
        for i in range(1, labels):

            region = Region(
                stats[i, 0],
                stats[i, 1],
                stats[i, 2],
                stats[i, 3],
                stats[i, 4], 0, i, self.frame_on
            )

            # want the real mass calculated from before the dilation
            region.mass = np.sum(region.subimage(thresh))

            # Add padding to region and change coordinates from edgeless image -> full image
            region.x += edge - padding
            region.y += edge - padding
            region.width += padding * 2
            region.height += padding * 2

            old_region = region.copy()
            region.crop(self.crop_rectangle)
            region.was_cropped = str(old_region) != str(region)

            if self.config.cropped_regions_strategy == "cautious":
                crop_width_fraction = (old_region.width - region.width) / old_region.width
                crop_height_fraction = (old_region.height - region.height) / old_region.height
                if crop_width_fraction > 0.25 or crop_height_fraction > 0.25:
                    continue
            elif self.config.cropped_regions_strategy == "none":
                if region.was_cropped:
                    continue
            elif self.config.cropped_regions_strategy != "all":
                raise ValueError(
                    "Invalid mode for CROPPED_REGIONS_STRATEGY, expected ['all','cautious','none'] but found {}".format(
                        self.config.cropped_regions_strategy))

            if delta_frame is not None:
                region_difference = np.float32(region.subimage(delta_frame))
                region.pixel_variance = np.var(region_difference)

            # filter out regions that are probably just noise
            if region.pixel_variance < self.config.aoi_pixel_variance and region.mass < self.config.aoi_min_mass:
                continue

            regions.append(region)

        return regions, mask
예제 #12
0
    def remove_background_animals(self, initial_frame, initial_diff):
        """
        Try and remove animals that are already in the initial frames, by
        checking for connected components in the intital_diff frame
        (this is the maximum change between first frame and all other frames in the clip)
        """
        # remove some noise
        initial_diff[initial_diff < self.background_thresh] = 0
        initial_diff[initial_diff > 255] = 255
        initial_diff = np.uint8(initial_diff)
        initial_diff = cv2.fastNlMeansDenoising(initial_diff, None)

        _, lower_mask, lower_objects = detect_objects(initial_diff, otsus=True)

        max_region = Region(0, 0, self.res_x, self.res_y)
        for component in lower_objects[1:]:
            region = Region(component[0], component[1], component[2],
                            component[3])
            region.enlarge(2, max=max_region)
            if region.width >= self.res_x or region.height >= self.res_y:
                logging.info(
                    "Background animal bigger than max, probably false positive %s %s",
                    region,
                    component[4],
                )
                continue
            background_region = region.subimage(initial_frame)
            norm_back = background_region.copy()
            norm_back, _ = normalize(norm_back, new_max=255)
            sub_components, sub_connected, sub_stats = detect_objects(
                norm_back, otsus=True)

            if sub_components <= 1:
                continue
            overlap_image = region.subimage(lower_mask) * 255
            overlap_pixels = np.sum(sub_connected[overlap_image > 0])
            overlap_pixels = overlap_pixels / float(component[4])

            # filter out components which are too big, or dont match original causes
            # for filtering
            if (overlap_pixels < Clip.MIN_ORIGIN_OVERLAP
                    or sub_stats[1][4] == 0 or sub_stats[1][4] == region.area):
                logging.info(
                    "Invalid components mass: %s, components: %s region area %s overlap %s",
                    sub_stats[1][4],
                    sub_components,
                    region.area,
                    overlap_pixels,
                )
                continue

            sub_connected[sub_connected > 0] = 1
            # remove this component from the background by painting with
            # colours of neighbouring pixels
            background_region[:] = cv2.inpaint(
                np.float32(background_region),
                np.uint8(sub_connected),
                3,
                cv2.INPAINT_TELEA,
            )
        return initial_frame