Skip to content

receptivefield

sleap.gui.learning.receptivefield

Widget for previewing receptive field on sample image using model hyperparams.

Classes:

Name Description
ReceptiveFieldImageWidget

Widget for showing image with receptive field and optional crop box.

ReceptiveFieldWidget

Widget for previewing receptive field on sample image, with caption.

Functions:

Name Description
compute_anchor_point

Computes the anchor point for an instance.

compute_crop_size_from_cfg

Computes crop size from model configuration.

compute_rf

Computes receptive field for specified model architecture.

find_instance_crop_size

Compute the size of the largest instance bounding box from labels.

find_max_instance_bbox_size

Find the maximum bounding box dimension across all instances in labels.

get_first_labeled_frame_and_instance

Gets the first frame with ground truth labels and the first instance.

receptive_field_info_from_model_cfg

Gets receptive field and architecture information from model configuration.

ReceptiveFieldImageWidget

Bases: GraphicsView

Widget for showing image with receptive field and optional crop box.

Methods:

Name Description
viewportEvent

Re-draw receptive field and crop box when needed.

Source code in sleap/gui/learning/receptivefield.py
class ReceptiveFieldImageWidget(GraphicsView):
    """Widget for showing image with receptive field and optional crop box."""

    def __init__(self, *args, **kwargs):
        self._widget_size = 200
        self._pen_width = 4
        self._crop_pen_width = 2
        self._box_size = None
        self._scale = None
        self._crop_size = None
        self._crop_anchor = None  # (x, y) coordinates of anchor point

        # Receptive field box (blue, solid)
        box_pen = QtGui.QPen(QtGui.QColor("blue"), self._pen_width)
        box_pen.setCosmetic(True)

        self.box = QtWidgets.QGraphicsRectItem()
        self.box.setPen(box_pen)

        # Crop box (red, dotted, thinner line)
        crop_pen = QtGui.QPen(QtGui.QColor("red"), self._crop_pen_width)
        crop_pen.setCosmetic(True)
        crop_pen.setStyle(QtCore.Qt.DotLine)

        self.crop_box = QtWidgets.QGraphicsRectItem()
        self.crop_box.setPen(crop_pen)

        super(ReceptiveFieldImageWidget, self).__init__(*args, **kwargs)

        self.setFixedSize(self._widget_size, self._widget_size)
        self.scene.addItem(self.box)
        self.scene.addItem(self.crop_box)

    def viewportEvent(self, event):
        """Re-draw receptive field and crop box when needed."""
        # Update the position and visible size of field
        if isinstance(event, QtGui.QPaintEvent):
            self._set_field_size()
            self._set_crop_size()

        # Now draw the viewport
        return super(ReceptiveFieldImageWidget, self).viewportEvent(event)

    def _set_field_size(self, size: Optional[int] = None, scale: float = 1.0):
        """Draws receptive field preview rect, updating size if needed."""
        if size is not None:
            self._box_size = size
            self._scale = scale if scale else 1.0

        if not self._box_size or not self._scale:
            self.box.hide()
            return

        self.box.show()

        # Adjust box relative to scaling on image that will happen in training
        scaled_box_size = self._box_size // self._scale

        # Calculate offset so that box stays centered in the view
        vis_box_rect = self.mapFromScene(
            0, 0, scaled_box_size, scaled_box_size
        ).boundingRect()
        offset = self._widget_size / 2
        scene_center = self.mapToScene(
            offset - (vis_box_rect.width() / 2), offset - (vis_box_rect.height() / 2)
        )

        self.box.setRect(
            scene_center.x(), scene_center.y(), scaled_box_size, scaled_box_size
        )

    def _set_crop_size(
        self,
        size: Optional[int] = None,
        scale: float = 1.0,
        anchor: Optional[Tuple[float, float]] = None,
    ):
        """Draws crop size preview rect centered in the view.

        The crop box tracks the view center (like the receptive field box) so both
        overlays move together when the view changes. The anchor parameter is stored
        but not used for positioning since this is a size comparison preview.

        Args:
            size: The crop size in pixels. If None, uses previously set value.
            scale: The scale factor applied to the image during training.
            anchor: The (x, y) coordinates of the anchor point in scene coordinates.
                If None, uses previously set value. Stored for reference but not
                used for positioning.
        """
        if size is not None:
            self._crop_size = size
            self._scale = scale if scale else 1.0
        if anchor is not None:
            self._crop_anchor = anchor

        if not self._crop_size or not self._scale:
            self.crop_box.hide()
            return

        self.crop_box.show()

        # Adjust box relative to scaling on image that will happen in training
        scaled_crop_size = self._crop_size // self._scale

        # Calculate offset so that box stays centered in the view
        # (same logic as _set_field_size for consistency)
        vis_box_rect = self.mapFromScene(
            0, 0, scaled_crop_size, scaled_crop_size
        ).boundingRect()
        offset = self._widget_size / 2
        scene_center = self.mapToScene(
            offset - (vis_box_rect.width() / 2), offset - (vis_box_rect.height() / 2)
        )

        self.crop_box.setRect(
            scene_center.x(),
            scene_center.y(),
            scaled_crop_size,
            scaled_crop_size,
        )

viewportEvent(event)

Re-draw receptive field and crop box when needed.

Source code in sleap/gui/learning/receptivefield.py
def viewportEvent(self, event):
    """Re-draw receptive field and crop box when needed."""
    # Update the position and visible size of field
    if isinstance(event, QtGui.QPaintEvent):
        self._set_field_size()
        self._set_crop_size()

    # Now draw the viewport
    return super(ReceptiveFieldImageWidget, self).viewportEvent(event)

ReceptiveFieldWidget

Bases: QWidget

Widget for previewing receptive field on sample image, with caption.

Parameters:

Name Type Description Default
head_name Text

If given, then used in caption to show which model the preview is for.

''
show_crop_box bool

If True, shows a crop size box centered on anchor point. This is intended for centered_instance and multi_class_topdown heads.

False
Usage

Create, then call setImage and setModelConfig methods. For crop box display, also call setLabels and setCropConfig.

Methods:

Name Description
addButtonWidget

Add a widget (typically a button) between the legend and explanation.

setCropConfig

Sets crop box configuration.

setImage

Sets image on which receptive field box will be drawn.

setLabels

Sets labels and displays the first labeled frame.

setModelConfig

Updates receptive field preview from model config.

Source code in sleap/gui/learning/receptivefield.py
class ReceptiveFieldWidget(QtWidgets.QWidget):
    """
    Widget for previewing receptive field on sample image, with caption.

    Args:
        head_name: If given, then used in caption to show which model the
            preview is for.
        show_crop_box: If True, shows a crop size box centered on anchor point.
            This is intended for centered_instance and multi_class_topdown heads.

    Usage:
        Create, then call `setImage` and `setModelConfig` methods.
        For crop box display, also call `setLabels` and `setCropConfig`.
    """

    def __init__(
        self, head_name: Text = "", show_crop_box: bool = False, *args, **kwargs
    ):
        super(ReceptiveFieldWidget, self).__init__(*args, **kwargs)

        self._show_crop_box = show_crop_box
        self._labels = None
        self._instance = None
        self._anchor_part = None
        self._crop_size = None
        self._rf_size = None  # Track receptive field size for legend
        self._head_name = head_name

        self.layout = QtWidgets.QVBoxLayout()

        self._field_image_widget = ReceptiveFieldImageWidget()

        # Legend (crop size + receptive field)
        self._legend_widget = QtWidgets.QLabel("")

        # Placeholder layout for button insertion (between legend and explanation)
        self._button_layout = QtWidgets.QVBoxLayout()
        self._button_layout.setContentsMargins(0, 4, 0, 4)

        # Explanation text (below legend and optional button)
        self._explanation_widget = QtWidgets.QLabel("")

        # UNet architecture info (params and channels)
        self._arch_info_widget = QtWidgets.QLabel("")

        self.layout.addWidget(self._field_image_widget)
        self.layout.addWidget(self._legend_widget)
        self.layout.addLayout(self._button_layout)
        self.layout.addWidget(self._explanation_widget)
        self.layout.addWidget(self._arch_info_widget)
        self.layout.addStretch()

        self.setLayout(self.layout)

    def _get_legend_text(self) -> Text:
        """Returns the legend text for crop size and receptive field."""
        result = ""

        # Crop size line (if enabled)
        if self._show_crop_box:
            if self._crop_size:
                result += (
                    f'<span style="color: red;">\u25a0</span> '
                    f"<b>Crop Size:</b> {self._crop_size} px<br/>"
                )
            else:
                result += (
                    '<span style="color: red;">\u25a0</span> <b>Crop Size:</b><br/>'
                )

        # Receptive field line
        if self._rf_size:
            result += (
                f'<span style="color: blue;">\u25a0</span> '
                f"<b>Receptive Field:</b> {self._rf_size} px"
            )
        else:
            result += (
                '<span style="color: blue;">\u25a0</span> '
                "<b>Receptive Field:</b> <i>N/A</i>"
            )

        return result

    def _get_explanation_text(
        self, scale, max_stride, down_blocks, convs_per_block, kernel_size
    ) -> Text:
        """Returns explanatory text about receptive field parameters."""
        return f"""<p>Receptive field size is a function<br />
        of the number of down blocks ({down_blocks}), the<br />
        number of convolutions per block ({convs_per_block}),<br />
        and the convolution kernel size ({kernel_size}).</p>

        <p>You can control the number of down<br />
        blocks by setting the <b>Max Stride</b> ({max_stride}).</p>

        <p>You can also control the receptive<br />
        field size relative to the original<br />
        image by adjusting the <b>Input Scaling</b> ({scale}).</p>"""

    def addButtonWidget(self, widget: QtWidgets.QWidget):
        """Add a widget (typically a button) between the legend and explanation.

        Args:
            widget: The widget to add (e.g., QPushButton for "Analyze Sizes...")
        """
        self._button_layout.addWidget(widget)

    def _get_head_output_channels(self, head_name: str) -> Optional[int]:
        """Get the number of output channels required by a head type.

        Args:
            head_name: Name of the sub-head (e.g., "confmaps", "pafs", "class_vectors")

        Returns:
            Number of output channels needed, or None if cannot be determined.
        """
        if self._labels is None:
            return None

        if not self._labels.skeletons:
            return None
        skeleton = self._labels.skeletons[0]

        if head_name == "confmaps":
            # One channel per keypoint
            return len(skeleton.nodes)
        elif head_name == "pafs":
            # Two channels (x, y) per edge
            return len(skeleton.edges) * 2
        elif head_name in ("class_vectors", "class_maps"):
            # Number of unique classes/tracks - skip validation for now
            return None
        else:
            return None

    def _get_arch_info_text(
        self,
        params_formatted: Optional[str],
        head_features: list,
        backbone_type: Optional[str] = "unet",
        model_type: Optional[str] = None,
    ) -> Text:
        """Returns text showing backbone architecture info (params and channels).

        Args:
            params_formatted: Human-readable param count (e.g., "1.30M")
            head_features: List of (head_name, output_stride, channels) tuples
            backbone_type: Type of backbone (e.g., "unet", "convnext", "swint").
            model_type: For convnext/swint, the model variant (e.g., "tiny").
        """
        if backbone_type is None:
            return ""

        if backbone_type == "unet":
            if params_formatted is None:
                return ""

            result = "<p><b>UNet:</b><br/>"
            result += f"<b>Parameters:</b> ~{params_formatted}<br/>"

            # Show features for each head with validation
            for i, (head_name, stride, backbone_channels) in enumerate(head_features):
                head_output = self._get_head_output_channels(head_name)

                if head_output is not None:
                    if backbone_channels >= head_output:
                        # Good: backbone has enough channels
                        result += (
                            f"<b>Features ({head_name} @ stride {stride}):</b> "
                            f'<span style="color: green;">'
                            f"{backbone_channels}\u2192{head_output} \u2713</span>"
                        )
                    else:
                        # Warning: backbone channels less than head output
                        result += (
                            f"<b>Features ({head_name} @ stride {stride}):</b> "
                            f'<span style="color: red;">'
                            f"{backbone_channels}\u2192{head_output} \u26a0</span>"
                        )
                else:
                    # Can't determine head output, just show backbone channels
                    result += (
                        f"<b>Features ({head_name} @ stride {stride}):</b> "
                        f"{backbone_channels} ch"
                    )

                if i < len(head_features) - 1:
                    result += "<br/>"

            result += "</p>"
            return result

        elif backbone_type == "convnext":
            model_display = model_type.capitalize() if model_type else "Tiny"
            result = f"<p><b>ConvNeXt ({model_display}):</b><br/>"
            result += "<b>Max Stride:</b> 32 (fixed)<br/>"
            if params_formatted:
                result += f"<b>Parameters:</b> ~{params_formatted}<br/>"
            result += "<b>Pretrained:</b> ImageNet weights available</p>"
            return result

        elif backbone_type == "swint":
            model_display = model_type.capitalize() if model_type else "Tiny"
            result = f"<p><b>Swin Transformer ({model_display}):</b><br/>"
            result += "<b>Max Stride:</b> 32 (fixed)<br/>"
            if params_formatted:
                result += f"<b>Parameters:</b> ~{params_formatted}<br/>"
            result += "<b>Pretrained:</b> ImageNet weights available</p>"
            return result

        return ""

    def setModelConfig(self, model_cfg: OmegaConf, scale: float):
        """Updates receptive field preview from model config."""
        rf_info = receptive_field_info_from_model_cfg(model_cfg)

        # Store receptive field size for legend
        self._rf_size = rf_info["size"]

        # Update architecture info (params and channels) - only for supported backbones
        self._arch_info_widget.setText(
            self._get_arch_info_text(
                params_formatted=rf_info["params_formatted"],
                head_features=rf_info["head_features"],
                backbone_type=rf_info["backbone_type"],
                model_type=rf_info.get("model_type"),
            )
        )

        # Update legend (crop size + receptive field)
        self._legend_widget.setText(self._get_legend_text())

        # Update explanation text
        self._explanation_widget.setText(
            self._get_explanation_text(
                scale=scale,
                max_stride=rf_info["max_stride"],
                down_blocks=rf_info["down_blocks"],
                convs_per_block=rf_info["convs_per_block"],
                kernel_size=rf_info["kernel_size"],
            )
        )

        self._field_image_widget._set_field_size(rf_info["size"] or 0, scale)

    def setImage(self, *args, **kwargs):
        """Sets image on which receptive field box will be drawn."""
        self._field_image_widget.setImage(*args, **kwargs)

    def setLabels(self, labels: Optional[sio.Labels], fallback_video=None):
        """Sets labels and displays the first labeled frame.

        This finds the first frame with ground truth labels, displays that frame,
        and stores the instance for crop box anchor point calculation (if enabled).

        Args:
            labels: The Labels object containing labeled frames.
            fallback_video: Video to use for getting test frame if labeled frame
                cannot be loaded.
        """
        self._labels = labels
        frame_image, instance = get_first_labeled_frame_and_instance(labels)

        # Store instance for crop box (only used if show_crop_box is True)
        if self._show_crop_box:
            self._instance = instance

        # Set the image - prefer the labeled frame, fall back to video test frame
        if frame_image is not None:
            self._field_image_widget.setImage(frame_image)
        elif fallback_video is not None:
            self._field_image_widget.setImage(fallback_video.backend.read_test_frame())

    def setCropConfig(
        self,
        crop_size: Optional[int],
        scale: float,
        anchor_part: Optional[Text] = None,
    ):
        """Sets crop box configuration.

        Args:
            crop_size: The crop size in pixels.
            scale: The scale factor applied to the image during training.
            anchor_part: The name of the body part to use as anchor.
                If None, the mean of all keypoints is used.
        """
        if not self._show_crop_box:
            return

        self._anchor_part = anchor_part
        self._crop_size = crop_size

        # Compute anchor point from the instance
        anchor = compute_anchor_point(self._instance, anchor_part)

        # Update the legend to include crop size
        self._legend_widget.setText(self._get_legend_text())

        if crop_size and anchor:
            self._field_image_widget._set_crop_size(crop_size, scale, anchor)

addButtonWidget(widget)

Add a widget (typically a button) between the legend and explanation.

Parameters:

Name Type Description Default
widget QWidget

The widget to add (e.g., QPushButton for "Analyze Sizes...")

required
Source code in sleap/gui/learning/receptivefield.py
def addButtonWidget(self, widget: QtWidgets.QWidget):
    """Add a widget (typically a button) between the legend and explanation.

    Args:
        widget: The widget to add (e.g., QPushButton for "Analyze Sizes...")
    """
    self._button_layout.addWidget(widget)

setCropConfig(crop_size, scale, anchor_part=None)

Sets crop box configuration.

Parameters:

Name Type Description Default
crop_size Optional[int]

The crop size in pixels.

required
scale float

The scale factor applied to the image during training.

required
anchor_part Optional[Text]

The name of the body part to use as anchor. If None, the mean of all keypoints is used.

None
Source code in sleap/gui/learning/receptivefield.py
def setCropConfig(
    self,
    crop_size: Optional[int],
    scale: float,
    anchor_part: Optional[Text] = None,
):
    """Sets crop box configuration.

    Args:
        crop_size: The crop size in pixels.
        scale: The scale factor applied to the image during training.
        anchor_part: The name of the body part to use as anchor.
            If None, the mean of all keypoints is used.
    """
    if not self._show_crop_box:
        return

    self._anchor_part = anchor_part
    self._crop_size = crop_size

    # Compute anchor point from the instance
    anchor = compute_anchor_point(self._instance, anchor_part)

    # Update the legend to include crop size
    self._legend_widget.setText(self._get_legend_text())

    if crop_size and anchor:
        self._field_image_widget._set_crop_size(crop_size, scale, anchor)

setImage(*args, **kwargs)

Sets image on which receptive field box will be drawn.

Source code in sleap/gui/learning/receptivefield.py
def setImage(self, *args, **kwargs):
    """Sets image on which receptive field box will be drawn."""
    self._field_image_widget.setImage(*args, **kwargs)

setLabels(labels, fallback_video=None)

Sets labels and displays the first labeled frame.

This finds the first frame with ground truth labels, displays that frame, and stores the instance for crop box anchor point calculation (if enabled).

Parameters:

Name Type Description Default
labels Optional[Labels]

The Labels object containing labeled frames.

required
fallback_video

Video to use for getting test frame if labeled frame cannot be loaded.

None
Source code in sleap/gui/learning/receptivefield.py
def setLabels(self, labels: Optional[sio.Labels], fallback_video=None):
    """Sets labels and displays the first labeled frame.

    This finds the first frame with ground truth labels, displays that frame,
    and stores the instance for crop box anchor point calculation (if enabled).

    Args:
        labels: The Labels object containing labeled frames.
        fallback_video: Video to use for getting test frame if labeled frame
            cannot be loaded.
    """
    self._labels = labels
    frame_image, instance = get_first_labeled_frame_and_instance(labels)

    # Store instance for crop box (only used if show_crop_box is True)
    if self._show_crop_box:
        self._instance = instance

    # Set the image - prefer the labeled frame, fall back to video test frame
    if frame_image is not None:
        self._field_image_widget.setImage(frame_image)
    elif fallback_video is not None:
        self._field_image_widget.setImage(fallback_video.backend.read_test_frame())

setModelConfig(model_cfg, scale)

Updates receptive field preview from model config.

Source code in sleap/gui/learning/receptivefield.py
def setModelConfig(self, model_cfg: OmegaConf, scale: float):
    """Updates receptive field preview from model config."""
    rf_info = receptive_field_info_from_model_cfg(model_cfg)

    # Store receptive field size for legend
    self._rf_size = rf_info["size"]

    # Update architecture info (params and channels) - only for supported backbones
    self._arch_info_widget.setText(
        self._get_arch_info_text(
            params_formatted=rf_info["params_formatted"],
            head_features=rf_info["head_features"],
            backbone_type=rf_info["backbone_type"],
            model_type=rf_info.get("model_type"),
        )
    )

    # Update legend (crop size + receptive field)
    self._legend_widget.setText(self._get_legend_text())

    # Update explanation text
    self._explanation_widget.setText(
        self._get_explanation_text(
            scale=scale,
            max_stride=rf_info["max_stride"],
            down_blocks=rf_info["down_blocks"],
            convs_per_block=rf_info["convs_per_block"],
            kernel_size=rf_info["kernel_size"],
        )
    )

    self._field_image_widget._set_field_size(rf_info["size"] or 0, scale)

compute_anchor_point(instance, anchor_part=None)

Computes the anchor point for an instance.

Parameters:

Name Type Description Default
instance Optional[Instance]

The instance to compute the anchor point for.

required
anchor_part Optional[Text]

The name of the body part to use as anchor. If None, the mean of all visible keypoints is used.

None

Returns:

Type Description
Optional[Tuple[float, float]]

A tuple (x, y) representing the anchor point coordinates, or None if the anchor cannot be computed.

Source code in sleap/gui/learning/receptivefield.py
def compute_anchor_point(
    instance: Optional[sio.Instance], anchor_part: Optional[Text] = None
) -> Optional[Tuple[float, float]]:
    """Computes the anchor point for an instance.

    Args:
        instance: The instance to compute the anchor point for.
        anchor_part: The name of the body part to use as anchor. If None,
            the mean of all visible keypoints is used.

    Returns:
        A tuple (x, y) representing the anchor point coordinates, or None
        if the anchor cannot be computed.
    """
    if instance is None:
        return None

    # If anchor_part is specified, try to use that node
    if anchor_part:
        for node, point in zip(instance.skeleton.nodes, instance.numpy()):
            if node.name == anchor_part and not np.isnan(point).any():
                return (float(point[0]), float(point[1]))

    # Fall back to mean of all visible keypoints
    points = instance.numpy()
    visible_points = points[~np.isnan(points).any(axis=1)]
    if len(visible_points) > 0:
        mean_point = np.mean(visible_points, axis=0)
        return (float(mean_point[0]), float(mean_point[1]))

    return None

compute_crop_size_from_cfg(data_cfg, model_cfg, labels=None, aug_form_data=None)

Computes crop size from model configuration.

When crop_size is not set (None/auto), computes it from the largest user-labeled instance bounding box plus augmentation padding, matching the logic in sleap-nn's training pipeline.

Parameters:

Name Type Description Default
data_cfg OmegaConf

Data configuration OmegaConf from the data form.

required
model_cfg OmegaConf

Model configuration OmegaConf from the model form.

required
labels Optional[Labels]

Labels object for computing instance bounding boxes.

None
aug_form_data Optional[dict]

Raw dict from the augmentation form's get_form_data(), used to compute augmentation padding from virtual fields.

None
Source code in sleap/gui/learning/receptivefield.py
def compute_crop_size_from_cfg(
    data_cfg: OmegaConf,
    model_cfg: OmegaConf,
    labels: Optional[sio.Labels] = None,
    aug_form_data: Optional[dict] = None,
) -> int:
    """Computes crop size from model configuration.

    When crop_size is not set (None/auto), computes it from the largest
    user-labeled instance bounding box plus augmentation padding, matching
    the logic in sleap-nn's training pipeline.

    Args:
        data_cfg: Data configuration OmegaConf from the data form.
        model_cfg: Model configuration OmegaConf from the model form.
        labels: Labels object for computing instance bounding boxes.
        aug_form_data: Raw dict from the augmentation form's get_form_data(),
            used to compute augmentation padding from virtual fields.
    """
    crop_size = data_cfg.data_config.preprocessing.crop_size
    if crop_size is None:
        try:
            backbone = model_cfg["_backbone_name"]
            max_stride = int(
                model_cfg.model_config.backbone_config[backbone].max_stride
            )

            bbox_size = find_max_instance_bbox_size(labels)
            padding = (
                _compute_padding_from_aug_form(aug_form_data, bbox_size)
                if aug_form_data
                else 0
            )
            crop_size = find_instance_crop_size(
                labels, padding=padding, maximum_stride=max_stride
            )
        except Exception:
            crop_size = None
    if crop_size is not None and data_cfg.data_config.preprocessing.scale is not None:
        crop_size = int(crop_size * data_cfg.data_config.preprocessing.scale)
    return crop_size

compute_rf(down_blocks, convs_per_block=2, kernel_size=3)

Computes receptive field for specified model architecture.

Ref: https://distill.pub/2019/computing-receptive-fields/ (Eq. 2)

Source code in sleap/gui/learning/receptivefield.py
def compute_rf(down_blocks: int, convs_per_block: int = 2, kernel_size: int = 3) -> int:
    """
    Computes receptive field for specified model architecture.

    Ref: https://distill.pub/2019/computing-receptive-fields/ (Eq. 2)
    """
    # Define the strides and kernel sizes for a single down block.
    # convs have stride 1, pooling has stride 2:
    block_strides = [1] * convs_per_block + [2]

    # convs have `kernel_size` x `kernel_size` kernels, pooling has 2 x 2 kernels:
    block_kernels = [kernel_size] * convs_per_block + [2]

    # Repeat block parameters by the total number of down blocks.
    strides = np.array(block_strides * down_blocks)
    kernels = np.array(block_kernels * down_blocks)

    # L = Total number of layers
    L = len(strides)

    # Compute the product term of the RF equation.
    rf = 1
    for l in range(L):
        rf += (kernels[l] - 1) * np.prod(strides[:l])

    return int(rf)

find_instance_crop_size(labels, padding=0, maximum_stride=2, min_crop_size=None)

Compute the size of the largest instance bounding box from labels.

This is a local implementation that avoids importing sleap_nn (which would trigger importing torch, adding ~2s to startup time).

Parameters:

Name Type Description Default
labels Labels

A sio.Labels containing user-labeled instances.

required
padding int

Integer number of pixels to add to the bounds as margin padding.

0
maximum_stride int

Ensure that the returned crop size is divisible by this value. Useful for ensuring that the crop size will not be truncated in a given architecture.

2
min_crop_size Optional[int]

The minimum crop size to return. If this value is already divisible by maximum_stride, it is returned directly.

None

Returns:

Type Description
int

An integer crop size denoting the length of the side of the bounding boxes that will contain the instances when cropped. The returned crop size will be larger or equal to the input min_crop_size.

Source code in sleap/gui/learning/receptivefield.py
def find_instance_crop_size(
    labels: sio.Labels,
    padding: int = 0,
    maximum_stride: int = 2,
    min_crop_size: Optional[int] = None,
) -> int:
    """Compute the size of the largest instance bounding box from labels.

    This is a local implementation that avoids importing sleap_nn (which would
    trigger importing torch, adding ~2s to startup time).

    Args:
        labels: A `sio.Labels` containing user-labeled instances.
        padding: Integer number of pixels to add to the bounds as margin padding.
        maximum_stride: Ensure that the returned crop size is divisible by this
            value. Useful for ensuring that the crop size will not be truncated
            in a given architecture.
        min_crop_size: The minimum crop size to return. If this value is already
            divisible by maximum_stride, it is returned directly.

    Returns:
        An integer crop size denoting the length of the side of the bounding
        boxes that will contain the instances when cropped. The returned crop
        size will be larger or equal to the input `min_crop_size`.
    """
    # Check if user-specified crop size is divisible by max stride
    min_crop_size = 0 if min_crop_size is None else min_crop_size
    if (min_crop_size > 0) and (min_crop_size % maximum_stride == 0):
        return min_crop_size

    # Calculate crop size by iterating over user-labeled instances only
    min_crop_size_no_pad = min_crop_size - padding
    max_length = 0.0
    for lf in labels:
        for inst in lf.instances:
            if isinstance(inst, sio.PredictedInstance):
                continue
            if not inst.is_empty:
                pts = inst.numpy()
                diff_x = np.nanmax(pts[:, 0]) - np.nanmin(pts[:, 0])
                diff_x = 0 if np.isnan(diff_x) else diff_x
                max_length = np.maximum(max_length, diff_x)
                diff_y = np.nanmax(pts[:, 1]) - np.nanmin(pts[:, 1])
                diff_y = 0 if np.isnan(diff_y) else diff_y
                max_length = np.maximum(max_length, diff_y)
                max_length = np.maximum(max_length, min_crop_size_no_pad)

    max_length += float(padding)
    crop_size = math.ceil(max_length / float(maximum_stride)) * maximum_stride

    return int(crop_size)

find_max_instance_bbox_size(labels)

Find the maximum bounding box dimension across all instances in labels.

This is a local implementation that avoids importing sleap_nn (which would trigger importing torch, adding ~2s to startup time).

Parameters:

Name Type Description Default
labels Labels

A sio.Labels containing user-labeled instances.

required

Returns:

Type Description
float

The maximum bounding box dimension (max of width or height) across all instances.

Source code in sleap/gui/learning/receptivefield.py
def find_max_instance_bbox_size(labels: sio.Labels) -> float:
    """Find the maximum bounding box dimension across all instances in labels.

    This is a local implementation that avoids importing sleap_nn (which would
    trigger importing torch, adding ~2s to startup time).

    Args:
        labels: A `sio.Labels` containing user-labeled instances.

    Returns:
        The maximum bounding box dimension (max of width or height) across all
        instances.
    """
    max_length = 0.0
    for lf in labels:
        for inst in lf.instances:
            if isinstance(inst, sio.PredictedInstance):
                continue
            if not inst.is_empty:
                pts = inst.numpy()
                diff_x = np.nanmax(pts[:, 0]) - np.nanmin(pts[:, 0])
                diff_x = 0 if np.isnan(diff_x) else diff_x
                max_length = np.maximum(max_length, diff_x)
                diff_y = np.nanmax(pts[:, 1]) - np.nanmin(pts[:, 1])
                diff_y = 0 if np.isnan(diff_y) else diff_y
                max_length = np.maximum(max_length, diff_y)
    return float(max_length)

get_first_labeled_frame_and_instance(labels)

Gets the first frame with ground truth labels and the first instance.

Parameters:

Name Type Description Default
labels Optional[Labels]

The Labels object containing labeled frames.

required

Returns:

Type Description
Tuple[Optional[ndarray], Optional[Instance]]

A tuple of (frame_image, instance) where frame_image is a numpy array and instance is the first user instance. Returns (None, None) if no labeled frames with user instances are found.

Source code in sleap/gui/learning/receptivefield.py
def get_first_labeled_frame_and_instance(
    labels: Optional[sio.Labels],
) -> Tuple[Optional[np.ndarray], Optional[sio.Instance]]:
    """Gets the first frame with ground truth labels and the first instance.

    Args:
        labels: The Labels object containing labeled frames.

    Returns:
        A tuple of (frame_image, instance) where frame_image is a numpy array
        and instance is the first user instance. Returns (None, None) if no
        labeled frames with user instances are found.
    """
    if labels is None:
        return None, None

    for lf in labels:
        if lf.user_instances:
            # Get the first user instance
            instance = lf.user_instances[0]
            # Get the frame image using sleap-io's Video indexing
            try:
                video = lf.video if hasattr(lf, "video") else labels.videos[0]
                # sleap-io Video uses __getitem__ for frame access
                frame_image = video[lf.frame_idx]
                return frame_image, instance
            except Exception:
                # If we can't load the frame, still return the instance
                # The caller will need to handle the None frame_image
                return None, instance

    return None, None

receptive_field_info_from_model_cfg(cfg)

Gets receptive field and architecture information from model configuration.

Returns a dict with
  • size: Receptive field size in pixels
  • max_stride: Maximum stride (bottleneck)
  • down_blocks: Number of encoder downsampling blocks
  • convs_per_block: Convolutions per block (fixed at 2)
  • kernel_size: Convolution kernel size (fixed at 3)
  • output_stride: Minimum head output stride (for RF calculation)
  • params: Total backbone parameter count
  • params_formatted: Human-readable param count (e.g., "1.30M")
  • head_features: List of (head_name, output_stride, channels) for each head
  • backbone_type: Type of backbone (unet, convnext, swint)
  • model_type: For convnext/swint, the model variant (tiny, small, base, large)
Source code in sleap/gui/learning/receptivefield.py
def receptive_field_info_from_model_cfg(cfg: OmegaConf) -> dict:
    """Gets receptive field and architecture information from model configuration.

    Returns a dict with:
        - size: Receptive field size in pixels
        - max_stride: Maximum stride (bottleneck)
        - down_blocks: Number of encoder downsampling blocks
        - convs_per_block: Convolutions per block (fixed at 2)
        - kernel_size: Convolution kernel size (fixed at 3)
        - output_stride: Minimum head output stride (for RF calculation)
        - params: Total backbone parameter count
        - params_formatted: Human-readable param count (e.g., "1.30M")
        - head_features: List of (head_name, output_stride, channels) for each head
        - backbone_type: Type of backbone (unet, convnext, swint)
        - model_type: For convnext/swint, the model variant (tiny, small, base, large)
    """
    model_cfg = cfg.model_config
    backbone_config = model_cfg.backbone_config

    rf_info = dict(
        size=None,
        max_stride=None,
        down_blocks=None,
        convs_per_block=None,
        kernel_size=None,
        output_stride=None,
        params=None,
        params_formatted=None,
        head_features=[],  # List of (head_name, output_stride, channels)
        backbone_type=None,  # e.g., "unet", "convnext", "swint"
        model_type=None,  # For convnext/swint: "tiny", "small", "base", "large"
    )

    # Detect backbone type
    backbone_type = None
    for bt in ["unet", "convnext", "swint"]:
        if hasattr(backbone_config, bt) and getattr(backbone_config, bt) is not None:
            backbone_type = bt
            break

    rf_info["backbone_type"] = backbone_type

    if backbone_type is None:
        return rf_info

    # Get max_stride based on backbone type
    if backbone_type == "unet":
        backbone_config.unet.max_stride = int(backbone_config.unet.max_stride)
        max_stride = backbone_config.unet.max_stride
    else:
        # ConvNeXt and SwinT have fixed max_stride of 32
        max_stride = 32

    rf_info["max_stride"] = max_stride

    head_type = get_head_from_omegaconf(cfg)

    # Collect output strides for each sub-head
    head_output_strides = []  # List of (sub_head_name, output_stride)
    for k, head_cfg in model_cfg.head_configs[head_type].items():
        if k == "class_vectors":
            head_output_strides.append((k, int(max_stride)))
        else:
            head_output_strides.append((k, int(head_cfg.output_stride)))

    output_strides = [s for _, s in head_output_strides]
    output_stride = min(output_strides)
    rf_info["output_stride"] = output_stride

    # Handle backbone-specific RF calculations
    if backbone_type == "unet":
        try:
            _ = np.log2(max_stride / output_stride)
        except ZeroDivisionError:
            return rf_info

        rf_info["convs_per_block"] = 2
        rf_info["kernel_size"] = 3

        stem_stride = None
        stem_blocks = 0
        if hasattr(backbone_config.unet, "stem_stride"):
            cfg_stem_stride = backbone_config.unet.stem_stride
            if cfg_stem_stride is not None:
                stem_stride = int(cfg_stem_stride)
                stem_blocks = np.log2(cfg_stem_stride).astype(int)

        down_blocks = np.log2(max_stride).astype(int) - stem_blocks
        rf_info["down_blocks"] = down_blocks

        has_rf_params = (
            rf_info["down_blocks"]
            and rf_info["convs_per_block"]
            and rf_info["kernel_size"]
        )
        if has_rf_params:
            rf_info["size"] = compute_rf(
                down_blocks=rf_info["down_blocks"],
                convs_per_block=rf_info["convs_per_block"],
                kernel_size=rf_info["kernel_size"],
            )

        # Extract UNet config for architecture calculations
        unet_cfg = backbone_config.unet
        filters = int(getattr(unet_cfg, "filters", 32))
        filters_rate = float(getattr(unet_cfg, "filters_rate", 1.5))
        middle_block = bool(getattr(unet_cfg, "middle_block", True))
        up_interpolate = bool(getattr(unet_cfg, "up_interpolate", False))

        # Compute channel counts at each stride
        try:
            min_output_stride = min(s for _, s in head_output_strides)
            stride_to_channels = unet_utils.compute_unet_channels(
                filters=filters,
                filters_rate=filters_rate,
                max_stride=max_stride,
                output_stride=min_output_stride,
                stem_stride=stem_stride,
            )
            for head_name, head_stride in head_output_strides:
                channels = stride_to_channels.get(head_stride)
                if channels is not None:
                    rf_info["head_features"].append((head_name, head_stride, channels))
        except Exception:
            pass

        # Compute total parameter count
        try:
            params = unet_utils.compute_unet_params(
                filters=filters,
                filters_rate=filters_rate,
                max_stride=max_stride,
                output_stride=output_stride,
                stem_stride=stem_stride,
                middle_block=middle_block,
                up_interpolate=up_interpolate,
            )
            rf_info["params"] = params
            rf_info["params_formatted"] = unet_utils.format_params(params)
        except Exception:
            pass

    elif backbone_type in ("convnext", "swint"):
        # ConvNeXt and SwinT have fixed max_stride=32
        rf_info["down_blocks"] = 5  # log2(32) = 5

        # Get model type (tiny, small, base, large)
        backbone_cfg = getattr(backbone_config, backbone_type)
        model_type = getattr(backbone_cfg, "model_type", "tiny")
        rf_info["model_type"] = model_type

        # Approximate parameter counts for pretrained models
        # These are rough estimates based on torchvision model sizes
        if backbone_type == "convnext":
            param_counts = {
                "tiny": 28_600_000,
                "small": 50_200_000,
                "base": 88_600_000,
                "large": 197_800_000,
            }
        else:  # swint
            param_counts = {
                "tiny": 28_300_000,
                "small": 49_600_000,
                "base": 87_800_000,
            }

        params = param_counts.get(model_type)
        if params:
            rf_info["params"] = params
            rf_info["params_formatted"] = unet_utils.format_params(params)

        # RF size is architecture-dependent and complex for transformer-based models
        # For now, leave as None since exact RF calculation is non-trivial

    return rf_info