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)
예제 #2
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
예제 #3
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)
예제 #4
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()
예제 #5
0
    def fit_to_image(self, rect:Region, screen_bounds:Region):
        """ Modifies rect so that rect is visible within bounds. """
        if rect.left < screen_bounds.left:
            rect.x = screen_bounds.left
        if rect.top < screen_bounds.top:
            rect.y = screen_bounds.top

        if rect.right > screen_bounds.right:
            rect.x = screen_bounds.right - rect.width

        if rect.bottom > screen_bounds.bottom:
            rect.y = screen_bounds.bottom - rect.height
    def load_track_meta(
        self,
        track_meta,
        frames_per_second,
        tag_precedence,
        min_confidence,
    ):
        self.from_metadata = True
        self._id = track_meta["id"]
        extra_info = track_meta
        if "data" in track_meta:
            extra_info = track_meta["data"]

        self.start_s = extra_info["start_s"]
        self.end_s = extra_info["end_s"]
        self.fps = frames_per_second
        self.predicted_tag = extra_info.get("tag")
        self.all_class_confidences = extra_info.get("all_class_confidences",
                                                    None)
        self.predictions = extra_info.get("predictions")

        self.track_tags = track_meta.get("TrackTags")
        self.prediction_classes = extra_info.get("classes")
        tag = Track.get_best_human_tag(self.track_tags, tag_precedence,
                                       min_confidence)
        if tag:
            self.tag = tag["what"]
            self.confidence = tag["confidence"]

        positions = extra_info.get("positions")
        if not positions:
            return False
        self.bounds_history = []
        self.frame_list = []
        for position in positions:
            if isinstance(position, list):
                frame_number = round(position[0] * frames_per_second)
                region = Region.region_from_array(position[1], frame_number)
            else:
                region = Region.region_from_json(position)

            if self.start_frame is None:
                self.start_frame = region.frame_number
            self.end_frame = region.frame_number
            self.bounds_history.append(region)
            self.frame_list.append(region.frame_number)
        self.current_frame_num = 0
        return True
    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)
    def from_meta(clip_id, clip_meta, track_meta, predictions=None):
        """Creates a track header from given metadata."""
        correct_prediction = track_meta.get("correct_prediction", None)
        start_time = dateutil.parser.parse(track_meta["start_time"])
        end_time = dateutil.parser.parse(track_meta["end_time"])
        duration = (end_time - start_time).total_seconds()
        location = clip_meta.get("location")
        num_frames = track_meta["frames"]
        camera = clip_meta["device"]
        frames_per_second = clip_meta.get("frames_per_second",
                                          FRAMES_PER_SECOND)
        # get the reference levels from clip_meta and load them into the track.
        track_start_frame = track_meta["start_frame"]
        frame_temp_median = np.float32(
            clip_meta["frame_temp_median"][track_start_frame:num_frames +
                                           track_start_frame])

        ffc_frames = clip_meta.get("ffc_frames", [])
        sample_frames = track_meta.get("sample_frames")
        skipped_frames = track_meta.get("skipped_frames")
        regions = [None] * len(track_meta["bounds_history"])
        f_i = 0
        for bounds, mass in zip(track_meta["bounds_history"],
                                track_meta["mass_history"]):
            r = Region.region_from_array(bounds,
                                         np.uint16(f_i + track_start_frame))
            r.mass = np.uint16(mass)
            if r.mass == 0:
                r.blank = True
            regions[f_i] = r
            f_i += 1
        header = TrackHeader(
            clip_id=int(clip_id),
            track_id=int(track_meta["id"]),
            label=track_meta["tag"],
            start_time=start_time,
            num_frames=num_frames,
            duration=duration,
            camera=camera,
            location=location,
            score=float(track_meta["score"]),
            regions=regions,
            frame_temp_median=frame_temp_median,
            frames_per_second=frames_per_second,
            predictions=predictions,
            correct_prediction=correct_prediction,
            start_frame=track_start_frame,
            res_x=clip_meta.get("res_x", CPTV_FILE_WIDTH),
            res_y=clip_meta.get("res_y", CPTV_FILE_HEIGHT),
            ffc_frames=ffc_frames,
            sample_frames_indices=sample_frames,
            skipped_frames=skipped_frames,
        )
        return header
예제 #9
0
def get_region_score(last_bound: Region, region: Region):
    """
    Calculates a score between 2 regions based of distance and area.
    The higher the score the more similar the Regions are
    """
    distance = last_bound.average_distance(region)

    # ratio of 1.0 = 20 points, ratio of 2.0 = 10 points, ratio of 3.0 = 0 points.
    # area is padded with 50 pixels so small regions don't change too much
    size_difference = abs(region.area - last_bound.area) / (last_bound.area + 50)

    return distance, size_difference
예제 #10
0
    def add_frame(self, bounds: Region):
        """
        Adds a new point in time bounds and mass to track
        :param bounds: new bounds region
        """
        self.bounds_history.append(bounds.copy())
        self.frames_since_target_seen = 0

        if len(self) >= 2:
            self.vel_x = self.bounds_history[-1].mid_x - self.bounds_history[-2].mid_x
            self.vel_y = self.bounds_history[-1].mid_y - self.bounds_history[-2].mid_y
        else:
            self.vel_x = self.vel_y = 0
예제 #11
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,
    )
예제 #12
0
    def load_track_meta(
        self,
        track_meta,
        frames_per_second,
        include_filtered_channel,
        tag_precedence,
        min_confidence,
    ):
        self.from_metadata = True
        self._id = track_meta["id"]
        self.include_filtered_channel = include_filtered_channel
        data = track_meta["data"]
        self.start_s = data["start_s"]
        self.end_s = data["end_s"]
        self.fps = frames_per_second
        self.track_tags = track_meta.get("TrackTags")
        tag = Track.get_best_human_tag(track_meta, tag_precedence,
                                       min_confidence)
        if tag:
            self.tag = tag["what"]
            self.confidence = tag["confidence"]
        else:
            return False

        positions = data.get("positions")
        if not positions:
            return False
        self.bounds_history = []
        self.frame_list = []
        for position in positions:
            frame_number = round(position[0] * frames_per_second)
            if self.start_frame is None:
                self.start_frame = frame_number
            self.end_frame = frame_number
            region = Region.region_from_array(position[1], frame_number)
            self.bounds_history.append(region)
            self.frame_list.append(frame_number)
        self.current_frame_num = 0
        return True
 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])
예제 #14
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
예제 #15
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
예제 #16
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
예제 #17
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 get_track(
        self,
        clip_id,
        track_number,
        start_frame=None,
        end_frame=None,
        original=False,
        frame_numbers=None,
        channels=None,
    ):
        """
        Fetches a track data from database with optional slicing.
        :param clip_id: id of the clip
        :param track_number: id of the track
        :param start_frame: first frame of slice to return (inclusive).
        :param end_frame: last frame of slice to return (exclusive).
        :return: a list of numpy arrays of shape [channels, height, width] and of type np.int16
        """
        with HDF5Manager(self.database) as f:
            clips = f["clips"]
            track_node = clips[str(clip_id)][str(track_number)]

            bounds = track_node.attrs["bounds_history"]
            if start_frame is None:
                start_frame = 0
            if end_frame is None:
                end_frame = track_node.attrs["frames"]
            track_start = track_node.attrs.get("start_frame")
            bad_frames = track_node.attrs.get("skipepd_frames", [])
            result = []
            if original:
                track_node = track_node["original"]
            else:
                if "cropped" in track_node:
                    track_node = track_node["cropped"]

            if frame_numbers is None:
                frame_iter = range(start_frame, end_frame)
            else:
                frame_iter = iter(frame_numbers)

            for frame_number in frame_iter:

                if original:
                    frame = track_node[str(frame_number)][:, :]
                    result.append(
                        Frame.from_channels([frame], [TrackChannels.thermal],
                                            frame_number))
                else:
                    if frame_number in bad_frames:
                        continue
                    region = Region.region_from_array(bounds[frame_number])
                    if channels is None:
                        try:
                            frame = track_node[str(frame_number)][:, :, :]
                            result.append(
                                Frame.from_array(
                                    frame,
                                    frame_number + track_start,
                                    flow_clipped=True,
                                    region=region,
                                ))
                        except:
                            logging.error(
                                "trying to get clip %s track %s frame %s",
                                clip_id,
                                track_number,
                                frame_number + track_start,
                                exc_info=True,
                            )
                    else:
                        try:
                            frame = track_node[str(frame_number)][
                                channels, :, :]
                            result.append(
                                Frame.from_channels(
                                    frame,
                                    channels,
                                    frame_number + track_start,
                                    region=region,
                                ))
                        except:
                            logging.error(
                                "trying to get clip %s track %s frame %s",
                                clip_id,
                                track_number,
                                frame_number + track_start,
                                exc_info=True,
                            )

        return result
    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()