Skip to content

suggestions

sleap.gui.suggestions

Module for generating lists of suggested frames (for labeling or reviewing).

Classes:

Name Description
VideoFrameSuggestions

Class for generating lists of suggested frames.

VideoFrameSuggestions

Bases: object

Class for generating lists of suggested frames.

Implements various algorithms as methods: * sample (either random or evenly spaced sample frames from each video) * image features (raw images/brisk -> pca -> k-means) * prediction_score (frames with number of instances below specified score)

Each of algorithm method should accept labels; other parameters will be passed from the params dict given to :meth:suggest.

Methods:

Name Description
basic_sample_suggestion_method

Generate suggestions randomly or by taking strides through video.

frame_chunk

Add consecutive frame chunk to label suggestion

image_feature_based_method

Method to generate suggestions based on image features.

max_point_displacement

Finds frames with maximum point displacement above a threshold.

prediction_score

Method to generate suggestions for proofreading frames with low score.

suggest

This is the main entry point for generating lists of suggested frames.

velocity

Finds frames for proofreading with high node velocity.

Source code in sleap/gui/suggestions.py
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
class VideoFrameSuggestions(object):
    """
    Class for generating lists of suggested frames.

    Implements various algorithms as methods:
    * sample (either random or evenly spaced sample frames from each video)
    * image features (raw images/brisk -> pca -> k-means)
    * prediction_score (frames with number of instances below specified score)

    Each of algorithm method should accept `labels`; other parameters will be
    passed from the `params` dict given to :meth:`suggest`.
    """

    @classmethod
    def suggest(cls, params: dict, labels: "Labels" = None) -> List[SuggestionFrame]:
        """
        This is the main entry point for generating lists of suggested frames.

        Args:
            params: A dictionary with all params to control how we generate
                suggestions, minimally this will have a "method" key with
                the name of one of the class methods.
            labels: A `Labels` object for which we are generating suggestions.

        Returns:
            List of `SuggestionFrame` objects.
        """

        # map from method param value to corresponding class method
        method_functions = dict(
            sample=cls.basic_sample_suggestion_method,
            image_features=cls.image_feature_based_method,
            prediction_score=cls.prediction_score,
            velocity=cls.velocity,
            frame_chunk=cls.frame_chunk,
            max_point_displacement=cls.max_point_displacement,
        )

        method = str.replace(params["method"], " ", "_")
        if method_functions.get(method, None) is not None:
            return method_functions[method](labels=labels, **params)
        else:
            raise ValueError(
                f"No {'' if method == '_' else method + ' '}method found for "
                "generating suggestions."
            )

    # Functions corresponding to "method" param

    @classmethod
    def basic_sample_suggestion_method(
        cls,
        labels,
        videos: List[Video],
        per_video: int = 20,
        sampling_method: str = "random",
        **kwargs,
    ):
        """Generate suggestions randomly or by taking strides through video."""
        suggestions = []
        sugg_idx_dict: Dict[Video, list] = {video: [] for video in labels.videos}

        for sugg in labels.suggestions:
            sugg_idx_dict[sugg.video].append(sugg.frame_idx)

        for video in videos:
            # Get unique sample space
            vid_idx = list(range(len(video)))
            vid_sugg_idx = sugg_idx_dict[video]
            unique_idx = list(set(vid_idx) - set(vid_sugg_idx))
            n_frames = len(unique_idx)

            if sampling_method == "stride":
                frame_increment = n_frames // per_video
                frame_increment = 1 if frame_increment == 0 else frame_increment
                stride_idx = list(range(0, n_frames, frame_increment))[:per_video]
                vid_suggestions = [unique_idx[idx] for idx in stride_idx]
            else:
                # random sampling
                frames_num = per_video
                frames_num = n_frames if (frames_num > n_frames) else frames_num
                if n_frames == 1:
                    vid_suggestions = list(unique_idx)
                else:
                    vid_suggestions = random.sample(unique_idx, frames_num)

            group = labels.videos.index(video)
            suggestions.extend(
                cls.idx_list_to_frame_list(vid_suggestions, video, group)
            )

        return suggestions

    @classmethod
    def image_feature_based_method(
        cls,
        labels,
        videos: List[Video],
        per_video,
        sample_method,
        scale,
        merge_video_features,
        feature_type,
        pca_components,
        n_clusters,
        per_cluster,
        **kwargs,
    ):
        """
        Method to generate suggestions based on image features.

        This is a wrapper for `feature_suggestion_pipeline` implemented in
        `sleap.info.feature_suggestions`.
        """

        brisk_threshold = kwargs.get("brisk_threshold", 80)
        vocab_size = kwargs.get("vocab_size", 20)

        # Propose new suggestions
        pipeline = FeatureSuggestionPipeline(
            per_video=per_video,
            scale=scale,
            sample_method=sample_method,
            feature_type=feature_type,
            brisk_threshold=brisk_threshold,
            vocab_size=vocab_size,
            n_components=pca_components,
            n_clusters=n_clusters,
            per_cluster=per_cluster,
        )

        if merge_video_features == "across all videos":
            # Run single pipeline with all videos
            proposed_suggestions = pipeline.get_suggestion_frames(videos=videos)
        else:
            # Run pipeline separately (in parallel) for each video
            proposed_suggestions = ParallelFeaturePipeline.run(pipeline, videos)

        suggestions = VideoFrameSuggestions.filter_unique_suggestions(
            labels, videos, proposed_suggestions
        )

        return suggestions

    @classmethod
    def prediction_score(
        cls,
        labels: "Labels",
        videos: List[Video],
        score_limit,
        instance_limit_upper,
        instance_limit_lower,
        **kwargs,
    ):
        """Method to generate suggestions for proofreading frames with low score."""
        score_limit = float(score_limit)
        instance_limit_upper = int(instance_limit_upper)
        instance_limit_lower = int(instance_limit_lower)

        proposed_suggestions = []
        for video in videos:
            proposed_suggestions.extend(
                cls._prediction_score_video(
                    video,
                    labels,
                    score_limit,
                    instance_limit_upper,
                    instance_limit_lower,
                )
            )

        suggestions = VideoFrameSuggestions.filter_unique_suggestions(
            labels, videos, proposed_suggestions
        )

        return suggestions

    @classmethod
    def _prediction_score_video(
        cls,
        video: Video,
        labels: "Labels",
        score_limit: float,
        instance_limit_upper: int,
        instance_limit_lower: int,
    ):
        lfs = labels.find(video)
        frames = len(lfs)

        # initiate an array filled with -1 to store frame index (starting from 0).
        idxs = np.full((frames), -1, dtype="int")

        for i, lf in enumerate(lfs):
            # Scores from visible instances in frame
            pred_fs = get_instances_to_show(lf)
            frame_scores = np.array(
                [inst.score for inst in pred_fs if hasattr(inst, "score")]
            )
            # Gets the number of instances with scores lower than <score_limit>
            n_qualified_instance = np.nansum(frame_scores <= score_limit)

            if (
                n_qualified_instance >= instance_limit_lower
                and n_qualified_instance <= instance_limit_upper
            ):
                # idxs saves qualified frame index at corresponding entry,
                # otherwise the entry is -1
                idxs[i] = lf.frame_idx

        # Finds non-negative entries in idxs
        result = sorted(idxs[idxs >= 0].tolist())

        return cls.idx_list_to_frame_list(result, video)

    @classmethod
    def velocity(
        cls,
        labels: "Labels",
        videos: List[Video],
        node: Union[int, str],
        threshold: float,
        **kwargs,
    ):
        """Finds frames for proofreading with high node velocity."""

        if isinstance(node, str):
            node_name = node
        else:
            try:
                node_name = labels.skeletons[0].nodes[node]
            except IndexError:
                node_name = ""

        proposed_suggestions = []
        for video in videos:
            proposed_suggestions.extend(
                cls._velocity_video(video, labels, node_name, threshold)
            )

        suggestions = VideoFrameSuggestions.filter_unique_suggestions(
            labels, videos, proposed_suggestions
        )

        return suggestions

    @classmethod
    def _velocity_video(
        cls, video: Video, labels: "Labels", node_name: str, threshold: float
    ):
        from sleap.info.summary import StatisticSeries

        displacements = StatisticSeries(labels).get_primary_point_displacement_series(
            video=video, reduction="sum", primary_node=node_name
        )
        data_range = np.ptp(displacements)
        data_min = np.min(displacements)

        frame_idxs = list(
            map(
                int,
                np.squeeze(
                    np.argwhere(displacements - data_min > data_range * threshold)
                ),
            )
        )

        return cls.idx_list_to_frame_list(frame_idxs, video)

    @classmethod
    def max_point_displacement(
        cls,
        labels: "Labels",
        videos: List[Video],
        displacement_threshold: float,
        **kwargs,
    ):
        """Finds frames with maximum point displacement above a threshold."""

        proposed_suggestions = []
        for video in videos:
            proposed_suggestions.extend(
                cls._max_point_displacement_video(video, labels, displacement_threshold)
            )

        suggestions = VideoFrameSuggestions.filter_unique_suggestions(
            labels, videos, proposed_suggestions
        )

        return suggestions

    @classmethod
    def _max_point_displacement_video(
        cls, video: Video, labels: "Labels", displacement_threshold: float
    ):
        # Get numpy of shape (frames, tracks, nodes, x, y)
        labels_numpy = labels.numpy(video=video, untracked=False)

        # Return empty list if not enough frames
        n_frames, n_tracks, n_nodes, _ = labels_numpy.shape

        if n_frames < 2:
            return []

        # Calculate displacements
        diff = labels_numpy[1:] - labels_numpy[:-1]  # (frames - 1, tracks, nodes, x, y)
        euc_norm = np.linalg.norm(diff, axis=-1)  # (frames - 1, tracks, nodes)
        mean_euc_norm = np.nanmean(euc_norm, axis=-1)  # (frames - 1, tracks)

        # Find frames where mean displacement is above threshold
        threshold_mask = np.any(
            mean_euc_norm > displacement_threshold, axis=-1
        )  # (frames - 1,)
        frame_idxs = list(
            np.argwhere(threshold_mask).flatten() + 1
        )  # [0, len(frames - 1)]

        return cls.idx_list_to_frame_list(frame_idxs, video)

    @classmethod
    def frame_chunk(
        cls,
        labels: "Labels",
        videos: List[Video],
        frame_from: int,
        frame_to: int,
        **kwargs,
    ):
        """Add consecutive frame chunk to label suggestion"""

        proposed_suggestions = []

        # Check the validity of inputs, frame_from <= frame_to
        if frame_from > frame_to:
            return proposed_suggestions

        for video in videos:
            # Make sure when targeting all videos the from and to do not exceed
            # frame number
            if frame_from > len(video):
                continue
            this_video_frame_to = min(frame_to, len(video))
            # Generate list of frame numbers
            idx = list(range(frame_from - 1, this_video_frame_to))
            proposed_suggestions.extend(cls.idx_list_to_frame_list(idx, video))

        suggestions = VideoFrameSuggestions.filter_unique_suggestions(
            labels, videos, proposed_suggestions
        )
        return suggestions

    # Utility functions

    @staticmethod
    def idx_list_to_frame_list(
        idx_list, video: "Video", group: Optional[GroupType] = None
    ) -> List[SuggestionFrame]:
        return [SuggestionFrame(video, frame_idx) for frame_idx in idx_list]

    @staticmethod
    def filter_unique_suggestions(
        labels: "Labels",
        videos: List[Video],
        proposed_suggestions: List[SuggestionFrame],
    ) -> List[SuggestionFrame]:
        # Create log of suggestions that already exist
        sugg_idx_dict: Dict[Video, list] = {video: [] for video in labels.videos}
        for sugg in labels.suggestions:
            sugg_idx_dict[sugg.video].append(sugg.frame_idx)

        # Filter for suggestions that already exist
        unique_suggestions = [
            sugg
            for sugg in proposed_suggestions
            if sugg.frame_idx not in sugg_idx_dict[sugg.video]
        ]

        return unique_suggestions

basic_sample_suggestion_method(labels, videos, per_video=20, sampling_method='random', **kwargs) classmethod

Generate suggestions randomly or by taking strides through video.

Source code in sleap/gui/suggestions.py
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
@classmethod
def basic_sample_suggestion_method(
    cls,
    labels,
    videos: List[Video],
    per_video: int = 20,
    sampling_method: str = "random",
    **kwargs,
):
    """Generate suggestions randomly or by taking strides through video."""
    suggestions = []
    sugg_idx_dict: Dict[Video, list] = {video: [] for video in labels.videos}

    for sugg in labels.suggestions:
        sugg_idx_dict[sugg.video].append(sugg.frame_idx)

    for video in videos:
        # Get unique sample space
        vid_idx = list(range(len(video)))
        vid_sugg_idx = sugg_idx_dict[video]
        unique_idx = list(set(vid_idx) - set(vid_sugg_idx))
        n_frames = len(unique_idx)

        if sampling_method == "stride":
            frame_increment = n_frames // per_video
            frame_increment = 1 if frame_increment == 0 else frame_increment
            stride_idx = list(range(0, n_frames, frame_increment))[:per_video]
            vid_suggestions = [unique_idx[idx] for idx in stride_idx]
        else:
            # random sampling
            frames_num = per_video
            frames_num = n_frames if (frames_num > n_frames) else frames_num
            if n_frames == 1:
                vid_suggestions = list(unique_idx)
            else:
                vid_suggestions = random.sample(unique_idx, frames_num)

        group = labels.videos.index(video)
        suggestions.extend(
            cls.idx_list_to_frame_list(vid_suggestions, video, group)
        )

    return suggestions

frame_chunk(labels, videos, frame_from, frame_to, **kwargs) classmethod

Add consecutive frame chunk to label suggestion

Source code in sleap/gui/suggestions.py
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
@classmethod
def frame_chunk(
    cls,
    labels: "Labels",
    videos: List[Video],
    frame_from: int,
    frame_to: int,
    **kwargs,
):
    """Add consecutive frame chunk to label suggestion"""

    proposed_suggestions = []

    # Check the validity of inputs, frame_from <= frame_to
    if frame_from > frame_to:
        return proposed_suggestions

    for video in videos:
        # Make sure when targeting all videos the from and to do not exceed
        # frame number
        if frame_from > len(video):
            continue
        this_video_frame_to = min(frame_to, len(video))
        # Generate list of frame numbers
        idx = list(range(frame_from - 1, this_video_frame_to))
        proposed_suggestions.extend(cls.idx_list_to_frame_list(idx, video))

    suggestions = VideoFrameSuggestions.filter_unique_suggestions(
        labels, videos, proposed_suggestions
    )
    return suggestions

image_feature_based_method(labels, videos, per_video, sample_method, scale, merge_video_features, feature_type, pca_components, n_clusters, per_cluster, **kwargs) classmethod

Method to generate suggestions based on image features.

This is a wrapper for feature_suggestion_pipeline implemented in sleap.info.feature_suggestions.

Source code in sleap/gui/suggestions.py
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
@classmethod
def image_feature_based_method(
    cls,
    labels,
    videos: List[Video],
    per_video,
    sample_method,
    scale,
    merge_video_features,
    feature_type,
    pca_components,
    n_clusters,
    per_cluster,
    **kwargs,
):
    """
    Method to generate suggestions based on image features.

    This is a wrapper for `feature_suggestion_pipeline` implemented in
    `sleap.info.feature_suggestions`.
    """

    brisk_threshold = kwargs.get("brisk_threshold", 80)
    vocab_size = kwargs.get("vocab_size", 20)

    # Propose new suggestions
    pipeline = FeatureSuggestionPipeline(
        per_video=per_video,
        scale=scale,
        sample_method=sample_method,
        feature_type=feature_type,
        brisk_threshold=brisk_threshold,
        vocab_size=vocab_size,
        n_components=pca_components,
        n_clusters=n_clusters,
        per_cluster=per_cluster,
    )

    if merge_video_features == "across all videos":
        # Run single pipeline with all videos
        proposed_suggestions = pipeline.get_suggestion_frames(videos=videos)
    else:
        # Run pipeline separately (in parallel) for each video
        proposed_suggestions = ParallelFeaturePipeline.run(pipeline, videos)

    suggestions = VideoFrameSuggestions.filter_unique_suggestions(
        labels, videos, proposed_suggestions
    )

    return suggestions

max_point_displacement(labels, videos, displacement_threshold, **kwargs) classmethod

Finds frames with maximum point displacement above a threshold.

Source code in sleap/gui/suggestions.py
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
@classmethod
def max_point_displacement(
    cls,
    labels: "Labels",
    videos: List[Video],
    displacement_threshold: float,
    **kwargs,
):
    """Finds frames with maximum point displacement above a threshold."""

    proposed_suggestions = []
    for video in videos:
        proposed_suggestions.extend(
            cls._max_point_displacement_video(video, labels, displacement_threshold)
        )

    suggestions = VideoFrameSuggestions.filter_unique_suggestions(
        labels, videos, proposed_suggestions
    )

    return suggestions

prediction_score(labels, videos, score_limit, instance_limit_upper, instance_limit_lower, **kwargs) classmethod

Method to generate suggestions for proofreading frames with low score.

Source code in sleap/gui/suggestions.py
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
@classmethod
def prediction_score(
    cls,
    labels: "Labels",
    videos: List[Video],
    score_limit,
    instance_limit_upper,
    instance_limit_lower,
    **kwargs,
):
    """Method to generate suggestions for proofreading frames with low score."""
    score_limit = float(score_limit)
    instance_limit_upper = int(instance_limit_upper)
    instance_limit_lower = int(instance_limit_lower)

    proposed_suggestions = []
    for video in videos:
        proposed_suggestions.extend(
            cls._prediction_score_video(
                video,
                labels,
                score_limit,
                instance_limit_upper,
                instance_limit_lower,
            )
        )

    suggestions = VideoFrameSuggestions.filter_unique_suggestions(
        labels, videos, proposed_suggestions
    )

    return suggestions

suggest(params, labels=None) classmethod

This is the main entry point for generating lists of suggested frames.

Parameters:

Name Type Description Default
params dict

A dictionary with all params to control how we generate suggestions, minimally this will have a "method" key with the name of one of the class methods.

required
labels Labels

A Labels object for which we are generating suggestions.

None

Returns:

Type Description
List[SuggestionFrame]

List of SuggestionFrame objects.

Source code in sleap/gui/suggestions.py
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
@classmethod
def suggest(cls, params: dict, labels: "Labels" = None) -> List[SuggestionFrame]:
    """
    This is the main entry point for generating lists of suggested frames.

    Args:
        params: A dictionary with all params to control how we generate
            suggestions, minimally this will have a "method" key with
            the name of one of the class methods.
        labels: A `Labels` object for which we are generating suggestions.

    Returns:
        List of `SuggestionFrame` objects.
    """

    # map from method param value to corresponding class method
    method_functions = dict(
        sample=cls.basic_sample_suggestion_method,
        image_features=cls.image_feature_based_method,
        prediction_score=cls.prediction_score,
        velocity=cls.velocity,
        frame_chunk=cls.frame_chunk,
        max_point_displacement=cls.max_point_displacement,
    )

    method = str.replace(params["method"], " ", "_")
    if method_functions.get(method, None) is not None:
        return method_functions[method](labels=labels, **params)
    else:
        raise ValueError(
            f"No {'' if method == '_' else method + ' '}method found for "
            "generating suggestions."
        )

velocity(labels, videos, node, threshold, **kwargs) classmethod

Finds frames for proofreading with high node velocity.

Source code in sleap/gui/suggestions.py
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
@classmethod
def velocity(
    cls,
    labels: "Labels",
    videos: List[Video],
    node: Union[int, str],
    threshold: float,
    **kwargs,
):
    """Finds frames for proofreading with high node velocity."""

    if isinstance(node, str):
        node_name = node
    else:
        try:
            node_name = labels.skeletons[0].nodes[node]
        except IndexError:
            node_name = ""

    proposed_suggestions = []
    for video in videos:
        proposed_suggestions.extend(
            cls._velocity_video(video, labels, node_name, threshold)
        )

    suggestions = VideoFrameSuggestions.filter_unique_suggestions(
        labels, videos, proposed_suggestions
    )

    return suggestions