Skip to content

Depth Annotators

import supervision as sv

image = ...
depth_map = sv.DepthMap(...)

depth_annotator = sv.DepthAnnotator(opacity=0.65)
annotated_image = depth_annotator.annotate(
    scene=image.copy(),
    depth_map=depth_map,
)

Stereo disparity coloured with sv.DepthAnnotator: a matcher's output over the frame, holes unpainted, and the ground truth alone

OpenCV SGBM disparity over the frame (left) and the ground truth (right). Spring dataset by Mehl et al. (doi:10.18419/darus-3376) and Spring open movie by Blender Foundation, CC BY 4.0; downscaled.

supervision.depth.annotators.DepthAnnotator

Colours a :class:~supervision.depth.core.DepthMap over an image, near objects warm and far ones cool.

Pixels without depth are left unpainted, so the scene shows through.

import supervision as sv
from inference import get_model
from PIL import Image

image = Image.open("<SOURCE_IMAGE_PATH>")
model = get_model(model_id="depth-anything-v3/small")
depth_map = sv.DepthMap.from_inference(model.infer(image)[0])

depth_annotator = sv.DepthAnnotator(display_range="auto", opacity=0.6)
annotated_image = depth_annotator.annotate(image.copy(), depth_map)
Source code in src/supervision/depth/annotators.py
class DepthAnnotator:
    """Colours a :class:`~supervision.depth.core.DepthMap` over an image, near objects
    warm and far ones cool.

    Pixels without depth are left unpainted, so the scene shows through.

    === "Image"

        ```python
        import supervision as sv
        from inference import get_model
        from PIL import Image

        image = Image.open("<SOURCE_IMAGE_PATH>")
        model = get_model(model_id="depth-anything-v3/small")
        depth_map = sv.DepthMap.from_inference(model.infer(image)[0])

        depth_annotator = sv.DepthAnnotator(display_range="auto", opacity=0.6)
        annotated_image = depth_annotator.annotate(image.copy(), depth_map)
        ```
    """

    def __init__(
        self,
        colormap: DepthColormap | str = DepthColormap.TURBO,
        display_range: str | tuple[float, float] | DepthClipRange = "auto",
        opacity: float = 1.0,
        scale: DepthScale | str = DepthScale.INVERSE,
    ) -> None:
        """
        Args:
            colormap: Colour table: `"turbo"` (default), `"viridis"`, `"cividis"`,
                `"inferno"`, `"magma"` or `"grayscale"`. The near end is always the
                warm or bright end.
            display_range: The values the colour table spans; values outside clamp
                to its ends.
                `"auto"` (default) uses this map's 2nd to 98th percentile. When
                they coincide, as on a flat map, every pixel takes the far-end
                colour.
                A `(low, high)` tuple fixes the range in the map's own unit,
                whatever the `scale`: metres for `depth_m`, pixels for
                `disparity_px` and the raw values for `relative_inverse`. Whatever
                the unit, the near end of the range takes the warm colour.
                Pixels of a `disparity_px` range belong to the map's resolution:
                disparity scales with width, so the same tuple means another
                depth after :meth:`~supervision.depth.core.DepthMap.resize`.
                A :class:`~supervision.depth.annotators.DepthClipRange` uses one
                range, in the maps' own unit like a tuple, for every frame of a
                clip.
            opacity: Opacity of the colours over the scene, from 0 to 1. Values
                outside are clamped: `<= 0` draws nothing and `>= 1` fully replaces
                the pixels that have depth.
            scale: `"inverse"` (default) colours disparity, or inverse depth for a
                metric map, which spends colour on near detail the way stereo
                measures it. `"metric"` colours metres; it needs a `depth_m` map or a
                disparity map with a camera.

        Raises:
            ValueError: If `colormap`, `scale` or `display_range` is invalid.
        """
        self.colormap = DepthColormap.from_value(colormap)
        self.scale = DepthScale.from_value(scale)
        self.display_range = _check_display_range_option(display_range)
        self.opacity = opacity

    @ensure_cv2_image_for_class_method
    def annotate(self, scene: ImageType, depth_map: DepthMap) -> ImageType:
        """Colour the depth map over the scene.

        A map whose size differs from the scene's is stretched over the whole scene,
        each scene pixel showing the map pixel under its centre, so a smaller map
        with the scene's aspect ratio lines up. A map with another aspect ratio is
        stretched along each axis on its own; if the model saw a letterboxed or
        cropped frame, undo that on the map before annotating.

        Args:
            scene: The image to draw on, a 3-channel `uint8` `numpy.ndarray` (BGR) or
                a `PIL.Image.Image`. It is drawn on in place.
            depth_map: The depth map to colour.

        Returns:
            The annotated image, matching the type of `scene` (`numpy.ndarray` or
                `PIL.Image.Image`).

        Raises:
            TypeError: If `scene` is not a `numpy.ndarray` or `PIL.Image.Image`.
            ValueError: If `scene` is not a 3-channel `uint8` image, if the scale
                is impossible for this map, for example `"metric"` for a relative
                map, or if `display_range` gives no usable colour range for this
                map's kind, such as a `depth_m` range with `low <= 0`.

        Examples:
            ```pycon
            >>> import numpy as np
            >>> import supervision as sv
            >>> disparity = np.array([[0.0, 1.0], [2.0, 3.0]], dtype=np.float32)
            >>> depth_map = sv.DepthMap(disparity, kind="disparity_px")
            >>> annotator = sv.DepthAnnotator(display_range=(1.0, 3.0))
            >>> image = np.zeros((2, 2, 3), dtype=np.uint8)
            >>> annotator.annotate(image, depth_map)[..., ::-1].tolist()
            [[[0, 0, 0], [48, 18, 59]], [[162, 252, 60], [122, 4, 3]]]

            ```
        """
        # The conversion decorator rejects other types and turns a Pillow image into
        # an array, so this always holds; it narrows `ImageType` for the type checker.
        assert isinstance(scene, np.ndarray)
        if scene.ndim != 3 or scene.shape[2] != 3:
            raise ValueError(
                f"DepthAnnotator draws on 3-channel images, got shape {scene.shape}."
            )
        # The colours are uint8; a wider scene would blend differently on OpenCV and
        # on the NumPy fallback, and `opacity >= 1` would write 0-255 into a float one.
        if scene.dtype != np.uint8:
            raise ValueError(
                f"DepthAnnotator draws on uint8 images, got dtype {scene.dtype}."
            )
        conversion = _resolve_conversion(depth_map.kind, depth_map.camera, self.scale)
        # The frame is converted once, for both the mask and the colours.
        converted = conversion.apply(depth_map.values)
        valid = depth_map.valid_mask
        if self.scale is DepthScale.METRIC and depth_map.kind is DepthKind.DISPARITY_PX:
            # A disparity with `disparity + doffs_px <= 0`, or one whose distance
            # overflows, has no distance, so colouring metres keeps only the pixels
            # `to_depth` keeps.
            with np.errstate(invalid="ignore"):
                valid &= _holds_depth(converted, DepthKind.DEPTH_M)
        value_range = self._resolve_range(depth_map, valid, conversion)
        coordinates = _color_coordinates(converted, valid, conversion, value_range)
        colors = _colorize(coordinates, self.colormap)

        scene_height, scene_width = scene.shape[:2]
        map_width, map_height = depth_map.resolution_wh
        if (map_height, map_width) != (scene_height, scene_width):
            rows = _index_map(map_height, scene_height)
            columns = _index_map(map_width, scene_width)
            # One 1-D take per axis is several times faster than a broadcast
            # `[rows[:, None], columns]` fancy index and samples the same pixels.
            colors = colors.take(rows, axis=0).take(columns, axis=1)
            valid = valid.take(rows, axis=0).take(columns, axis=1)

        _blend(scene, valid, colors, self.opacity)
        return scene

    def _resolve_range(
        self,
        depth_map: DepthMap,
        valid: npt.NDArray[np.bool_],
        conversion: _Conversion,
    ) -> tuple[float, float]:
        """Return the colour range in the scale's unit for this map.

        An explicit range or clip range is converted from the map's unit. `"auto"`
        spans the pixels set in `valid`; a map without any depth falls back to
        `(0, 1)`.
        """
        option = self.display_range
        if isinstance(option, DepthClipRange):
            option = option.display_range
        if isinstance(option, tuple):
            return _convert_display_range(option, depth_map.kind, conversion)
        percentile = _percentile_range(depth_map.values[valid], conversion)
        return percentile if percentile is not None else _FALLBACK_RANGE

Methods:

__init__(colormap: DepthColormap | str = DepthColormap.TURBO, display_range: str | tuple[float, float] | DepthClipRange = 'auto', opacity: float = 1.0, scale: DepthScale | str = DepthScale.INVERSE) -> None

Parameters:

Name Type Description Default
colormap
DepthColormap | str

Colour table: "turbo" (default), "viridis", "cividis", "inferno", "magma" or "grayscale". The near end is always the warm or bright end.

TURBO
display_range
str | tuple[float, float] | DepthClipRange

The values the colour table spans; values outside clamp to its ends. "auto" (default) uses this map's 2nd to 98th percentile. When they coincide, as on a flat map, every pixel takes the far-end colour. A (low, high) tuple fixes the range in the map's own unit, whatever the scale: metres for depth_m, pixels for disparity_px and the raw values for relative_inverse. Whatever the unit, the near end of the range takes the warm colour. Pixels of a disparity_px range belong to the map's resolution: disparity scales with width, so the same tuple means another depth after :meth:~supervision.depth.core.DepthMap.resize. A :class:~supervision.depth.annotators.DepthClipRange uses one range, in the maps' own unit like a tuple, for every frame of a clip.

'auto'
opacity
float

Opacity of the colours over the scene, from 0 to 1. Values outside are clamped: <= 0 draws nothing and >= 1 fully replaces the pixels that have depth.

1.0
scale
DepthScale | str

"inverse" (default) colours disparity, or inverse depth for a metric map, which spends colour on near detail the way stereo measures it. "metric" colours metres; it needs a depth_m map or a disparity map with a camera.

INVERSE

Raises:

Type Description
ValueError

If colormap, scale or display_range is invalid.

Source code in src/supervision/depth/annotators.py
def __init__(
    self,
    colormap: DepthColormap | str = DepthColormap.TURBO,
    display_range: str | tuple[float, float] | DepthClipRange = "auto",
    opacity: float = 1.0,
    scale: DepthScale | str = DepthScale.INVERSE,
) -> None:
    """
    Args:
        colormap: Colour table: `"turbo"` (default), `"viridis"`, `"cividis"`,
            `"inferno"`, `"magma"` or `"grayscale"`. The near end is always the
            warm or bright end.
        display_range: The values the colour table spans; values outside clamp
            to its ends.
            `"auto"` (default) uses this map's 2nd to 98th percentile. When
            they coincide, as on a flat map, every pixel takes the far-end
            colour.
            A `(low, high)` tuple fixes the range in the map's own unit,
            whatever the `scale`: metres for `depth_m`, pixels for
            `disparity_px` and the raw values for `relative_inverse`. Whatever
            the unit, the near end of the range takes the warm colour.
            Pixels of a `disparity_px` range belong to the map's resolution:
            disparity scales with width, so the same tuple means another
            depth after :meth:`~supervision.depth.core.DepthMap.resize`.
            A :class:`~supervision.depth.annotators.DepthClipRange` uses one
            range, in the maps' own unit like a tuple, for every frame of a
            clip.
        opacity: Opacity of the colours over the scene, from 0 to 1. Values
            outside are clamped: `<= 0` draws nothing and `>= 1` fully replaces
            the pixels that have depth.
        scale: `"inverse"` (default) colours disparity, or inverse depth for a
            metric map, which spends colour on near detail the way stereo
            measures it. `"metric"` colours metres; it needs a `depth_m` map or a
            disparity map with a camera.

    Raises:
        ValueError: If `colormap`, `scale` or `display_range` is invalid.
    """
    self.colormap = DepthColormap.from_value(colormap)
    self.scale = DepthScale.from_value(scale)
    self.display_range = _check_display_range_option(display_range)
    self.opacity = opacity

annotate(scene: ImageType, depth_map: DepthMap) -> ImageType

Colour the depth map over the scene.

A map whose size differs from the scene's is stretched over the whole scene, each scene pixel showing the map pixel under its centre, so a smaller map with the scene's aspect ratio lines up. A map with another aspect ratio is stretched along each axis on its own; if the model saw a letterboxed or cropped frame, undo that on the map before annotating.

Parameters:

Name Type Description Default
scene
ImageType

The image to draw on, a 3-channel uint8 numpy.ndarray (BGR) or a PIL.Image.Image. It is drawn on in place.

required
depth_map
DepthMap

The depth map to colour.

required

Returns:

Type Description
ImageType

The annotated image, matching the type of scene (numpy.ndarray or PIL.Image.Image).

Raises:

Type Description
TypeError

If scene is not a numpy.ndarray or PIL.Image.Image.

ValueError

If scene is not a 3-channel uint8 image, if the scale is impossible for this map, for example "metric" for a relative map, or if display_range gives no usable colour range for this map's kind, such as a depth_m range with low <= 0.

Examples:

>>> import numpy as np
>>> import supervision as sv
>>> disparity = np.array([[0.0, 1.0], [2.0, 3.0]], dtype=np.float32)
>>> depth_map = sv.DepthMap(disparity, kind="disparity_px")
>>> annotator = sv.DepthAnnotator(display_range=(1.0, 3.0))
>>> image = np.zeros((2, 2, 3), dtype=np.uint8)
>>> annotator.annotate(image, depth_map)[..., ::-1].tolist()
[[[0, 0, 0], [48, 18, 59]], [[162, 252, 60], [122, 4, 3]]]
Source code in src/supervision/depth/annotators.py
@ensure_cv2_image_for_class_method
def annotate(self, scene: ImageType, depth_map: DepthMap) -> ImageType:
    """Colour the depth map over the scene.

    A map whose size differs from the scene's is stretched over the whole scene,
    each scene pixel showing the map pixel under its centre, so a smaller map
    with the scene's aspect ratio lines up. A map with another aspect ratio is
    stretched along each axis on its own; if the model saw a letterboxed or
    cropped frame, undo that on the map before annotating.

    Args:
        scene: The image to draw on, a 3-channel `uint8` `numpy.ndarray` (BGR) or
            a `PIL.Image.Image`. It is drawn on in place.
        depth_map: The depth map to colour.

    Returns:
        The annotated image, matching the type of `scene` (`numpy.ndarray` or
            `PIL.Image.Image`).

    Raises:
        TypeError: If `scene` is not a `numpy.ndarray` or `PIL.Image.Image`.
        ValueError: If `scene` is not a 3-channel `uint8` image, if the scale
            is impossible for this map, for example `"metric"` for a relative
            map, or if `display_range` gives no usable colour range for this
            map's kind, such as a `depth_m` range with `low <= 0`.

    Examples:
        ```pycon
        >>> import numpy as np
        >>> import supervision as sv
        >>> disparity = np.array([[0.0, 1.0], [2.0, 3.0]], dtype=np.float32)
        >>> depth_map = sv.DepthMap(disparity, kind="disparity_px")
        >>> annotator = sv.DepthAnnotator(display_range=(1.0, 3.0))
        >>> image = np.zeros((2, 2, 3), dtype=np.uint8)
        >>> annotator.annotate(image, depth_map)[..., ::-1].tolist()
        [[[0, 0, 0], [48, 18, 59]], [[162, 252, 60], [122, 4, 3]]]

        ```
    """
    # The conversion decorator rejects other types and turns a Pillow image into
    # an array, so this always holds; it narrows `ImageType` for the type checker.
    assert isinstance(scene, np.ndarray)
    if scene.ndim != 3 or scene.shape[2] != 3:
        raise ValueError(
            f"DepthAnnotator draws on 3-channel images, got shape {scene.shape}."
        )
    # The colours are uint8; a wider scene would blend differently on OpenCV and
    # on the NumPy fallback, and `opacity >= 1` would write 0-255 into a float one.
    if scene.dtype != np.uint8:
        raise ValueError(
            f"DepthAnnotator draws on uint8 images, got dtype {scene.dtype}."
        )
    conversion = _resolve_conversion(depth_map.kind, depth_map.camera, self.scale)
    # The frame is converted once, for both the mask and the colours.
    converted = conversion.apply(depth_map.values)
    valid = depth_map.valid_mask
    if self.scale is DepthScale.METRIC and depth_map.kind is DepthKind.DISPARITY_PX:
        # A disparity with `disparity + doffs_px <= 0`, or one whose distance
        # overflows, has no distance, so colouring metres keeps only the pixels
        # `to_depth` keeps.
        with np.errstate(invalid="ignore"):
            valid &= _holds_depth(converted, DepthKind.DEPTH_M)
    value_range = self._resolve_range(depth_map, valid, conversion)
    coordinates = _color_coordinates(converted, valid, conversion, value_range)
    colors = _colorize(coordinates, self.colormap)

    scene_height, scene_width = scene.shape[:2]
    map_width, map_height = depth_map.resolution_wh
    if (map_height, map_width) != (scene_height, scene_width):
        rows = _index_map(map_height, scene_height)
        columns = _index_map(map_width, scene_width)
        # One 1-D take per axis is several times faster than a broadcast
        # `[rows[:, None], columns]` fancy index and samples the same pixels.
        colors = colors.take(rows, axis=0).take(columns, axis=1)
        valid = valid.take(rows, axis=0).take(columns, axis=1)

    _blend(scene, valid, colors, self.opacity)
    return scene

supervision.depth.annotators.DepthClipRange dataclass

One colour range for a whole clip of depth maps.

Colouring each frame with its own range makes a still wall change colour as things enter and leave the frame. A clip range, computed in a first pass over the clip, keeps the colour scale fixed on every frame. Colours then stay put only where the depth values are steady, as in ground truth or calibrated stereo; a model's own frame-to-frame wobble becomes more visible.

A clip range applies to maps of the kind it was computed from; on maps of another kind its values would be read in the wrong unit. A disparity_px clip range also applies to maps of the resolution it was computed at, because disparity scales with the map's width.

Attributes:

Name Type Description
display_range tuple[float, float]

(low, high) colour range in the maps' own unit, read like a (low, high) tuple passed to :class:~supervision.depth.annotators.DepthAnnotator. Equal ends, as from a clip of one flat value, paint every pixel the far-end colour, as "auto" does on a flat map.

Examples:

>>> import numpy as np
>>> import supervision as sv
>>> frames = [
...     sv.DepthMap(np.full((4, 4), d, dtype=np.float32), kind="disparity_px")
...     for d in (10.0, 20.0, 30.0)
... ]
>>> sv.DepthClipRange.from_depth_maps(frames)
DepthClipRange(display_range=(10.0, 30.0))
Source code in src/supervision/depth/annotators.py
@dataclass(frozen=True)
class DepthClipRange:
    """One colour range for a whole clip of depth maps.

    Colouring each frame with its own range makes a still wall change colour as
    things enter and leave the frame. A clip range, computed in a first pass over the
    clip, keeps the colour scale fixed on every frame. Colours then stay put only
    where the depth values are steady, as in ground truth or calibrated stereo; a
    model's own frame-to-frame wobble becomes more visible.

    A clip range applies to maps of the kind it was computed from; on maps of another
    kind its values would be read in the wrong unit. A `disparity_px` clip range also
    applies to maps of the resolution it was computed at, because disparity scales
    with the map's width.

    Attributes:
        display_range: `(low, high)` colour range in the maps' own unit, read like a
            `            (low, high)` tuple passed to
            :class:`~supervision.depth.annotators.DepthAnnotator`. Equal ends, as from a
            clip of one flat value, paint every pixel the far-end colour, as `"auto"`
            does on a flat map.

    Examples:
        ```pycon
        >>> import numpy as np
        >>> import supervision as sv
        >>> frames = [
        ...     sv.DepthMap(np.full((4, 4), d, dtype=np.float32), kind="disparity_px")
        ...     for d in (10.0, 20.0, 30.0)
        ... ]
        >>> sv.DepthClipRange.from_depth_maps(frames)
        DepthClipRange(display_range=(10.0, 30.0))

        ```
    """

    display_range: tuple[float, float]

    def __post_init__(self) -> None:
        """Store plain floats and reject a reversed or unusable range."""
        low, high = _display_range_pair(self.display_range)
        object.__setattr__(self, "display_range", (low, high))
        finite = math.isfinite(low) and math.isfinite(high)
        if not (finite and (low == high or _has_usable_span(low, high))):
            raise ValueError(
                "DepthClipRange display_range must be two finite numbers with "
                "low <= high and a span that fits in float32, got "
                f"{self.display_range}."
            )

    @classmethod
    def from_depth_maps(
        cls,
        depth_maps: Iterable[DepthMap],
        low_percentile: float = _DEFAULT_LOW_PERCENTILE,
        high_percentile: float = _DEFAULT_HIGH_PERCENTILE,
    ) -> DepthClipRange:
        """Compute a clip's percentile range in one pass.

        Each map contributes up to about 65,536 valid values, thinned at random when
        it has more. Frames with at least that many valid values weigh equally, and
        sparser frames weigh by their count of valid values. Whenever the clip's
        samples pass 4,194,304, each one is kept with probability 1/2 and later maps
        are kept at half the previous rate, so memory stays bounded however long the
        clip is and early and late frames are sampled alike. Once thinning starts, the
        percentiles are estimates from the samples, so 0 and 100 only approximate the
        clip's minimum and maximum. The random draws are seeded, so the result is
        reproducible for a given NumPy version. Pass a generator to read the clip once
        without holding it.

        Args:
            depth_maps: The clip's maps, all of one kind.
            low_percentile: Percentile of the clip's valid values at the range's low
                end, from 0 to 100. Defaults to 2, as for `display_range="auto"`.
            high_percentile: Percentile at the range's high end, from 0 to 100.
                Defaults to 98, as for `display_range="auto"`.

        Returns:
            The clip's :class:`~supervision.depth.annotators.DepthClipRange`, in the
            maps' own unit.

        Raises:
            ValueError: If the percentiles are not
                `0 <= low_percentile < high_percentile <= 100`, or the maps mix kinds
                or hold no depth at all.
        """
        _check_percentiles(low_percentile, high_percentile)
        kind: DepthKind | None = None
        samples: list[npt.NDArray[np.float32]] = []
        retained = 0
        keep_share = 1.0
        rng = np.random.default_rng(0)
        for depth_map in depth_maps:
            if kind is None:
                kind = depth_map.kind
            elif depth_map.kind is not kind:
                raise ValueError(
                    f"A clip range needs maps of one kind; got {kind.value} and "
                    f"{depth_map.kind.value}."
                )
            values = depth_map.values[depth_map.valid_mask]
            if values.size == 0:
                continue
            share = keep_share * min(1.0, _CLIP_SAMPLES_PER_FRAME / values.size)
            if share < 1.0:
                # Random rather than every n-th sample: a fixed step aliases with
                # structured frames and skews the percentiles.
                values = _thin_at_random(values, share, rng)
                # Once the share has been halved, a sparse frame may keep nothing.
                if values.size == 0:
                    continue
            samples.append(values)
            retained += values.size
            if retained > _CLIP_SAMPLE_BUDGET:
                # Thinning keeps each sample with probability 1/2, so every sample of
                # the clip is now kept with probability `keep_share / 2`.
                kept = _thin_at_random(np.concatenate(samples), 0.5, rng)
                samples, retained, keep_share = [kept], kept.size, keep_share / 2
            elif len(samples) > _CLIP_SAMPLE_ARRAYS:
                # The budget bounds the values but not the arrays holding them; many
                # sparse frames would each add one. Merging copies every retained
                # value, so it runs only once per this many frames.
                samples = [np.concatenate(samples)]
        if retained == 0:
            raise ValueError("The clip holds no depth to compute a range from.")
        display_range = _values_at_ranks(
            np.concatenate(samples), low_percentile, high_percentile
        )
        return cls(display_range=display_range)

Methods:

__post_init__() -> None

Store plain floats and reject a reversed or unusable range.

Source code in src/supervision/depth/annotators.py
def __post_init__(self) -> None:
    """Store plain floats and reject a reversed or unusable range."""
    low, high = _display_range_pair(self.display_range)
    object.__setattr__(self, "display_range", (low, high))
    finite = math.isfinite(low) and math.isfinite(high)
    if not (finite and (low == high or _has_usable_span(low, high))):
        raise ValueError(
            "DepthClipRange display_range must be two finite numbers with "
            "low <= high and a span that fits in float32, got "
            f"{self.display_range}."
        )

from_depth_maps(depth_maps: Iterable[DepthMap], low_percentile: float = _DEFAULT_LOW_PERCENTILE, high_percentile: float = _DEFAULT_HIGH_PERCENTILE) -> DepthClipRange classmethod

Compute a clip's percentile range in one pass.

Each map contributes up to about 65,536 valid values, thinned at random when it has more. Frames with at least that many valid values weigh equally, and sparser frames weigh by their count of valid values. Whenever the clip's samples pass 4,194,304, each one is kept with probability 1/2 and later maps are kept at half the previous rate, so memory stays bounded however long the clip is and early and late frames are sampled alike. Once thinning starts, the percentiles are estimates from the samples, so 0 and 100 only approximate the clip's minimum and maximum. The random draws are seeded, so the result is reproducible for a given NumPy version. Pass a generator to read the clip once without holding it.

Parameters:

Name Type Description Default
depth_maps
Iterable[DepthMap]

The clip's maps, all of one kind.

required
low_percentile
float

Percentile of the clip's valid values at the range's low end, from 0 to 100. Defaults to 2, as for display_range="auto".

_DEFAULT_LOW_PERCENTILE
high_percentile
float

Percentile at the range's high end, from 0 to 100. Defaults to 98, as for display_range="auto".

_DEFAULT_HIGH_PERCENTILE

Returns:

Type Description
DepthClipRange

The clip's :class:~supervision.depth.annotators.DepthClipRange, in the

DepthClipRange

maps' own unit.

Raises:

Type Description
ValueError

If the percentiles are not 0 <= low_percentile < high_percentile <= 100, or the maps mix kinds or hold no depth at all.

Source code in src/supervision/depth/annotators.py
@classmethod
def from_depth_maps(
    cls,
    depth_maps: Iterable[DepthMap],
    low_percentile: float = _DEFAULT_LOW_PERCENTILE,
    high_percentile: float = _DEFAULT_HIGH_PERCENTILE,
) -> DepthClipRange:
    """Compute a clip's percentile range in one pass.

    Each map contributes up to about 65,536 valid values, thinned at random when
    it has more. Frames with at least that many valid values weigh equally, and
    sparser frames weigh by their count of valid values. Whenever the clip's
    samples pass 4,194,304, each one is kept with probability 1/2 and later maps
    are kept at half the previous rate, so memory stays bounded however long the
    clip is and early and late frames are sampled alike. Once thinning starts, the
    percentiles are estimates from the samples, so 0 and 100 only approximate the
    clip's minimum and maximum. The random draws are seeded, so the result is
    reproducible for a given NumPy version. Pass a generator to read the clip once
    without holding it.

    Args:
        depth_maps: The clip's maps, all of one kind.
        low_percentile: Percentile of the clip's valid values at the range's low
            end, from 0 to 100. Defaults to 2, as for `display_range="auto"`.
        high_percentile: Percentile at the range's high end, from 0 to 100.
            Defaults to 98, as for `display_range="auto"`.

    Returns:
        The clip's :class:`~supervision.depth.annotators.DepthClipRange`, in the
        maps' own unit.

    Raises:
        ValueError: If the percentiles are not
            `0 <= low_percentile < high_percentile <= 100`, or the maps mix kinds
            or hold no depth at all.
    """
    _check_percentiles(low_percentile, high_percentile)
    kind: DepthKind | None = None
    samples: list[npt.NDArray[np.float32]] = []
    retained = 0
    keep_share = 1.0
    rng = np.random.default_rng(0)
    for depth_map in depth_maps:
        if kind is None:
            kind = depth_map.kind
        elif depth_map.kind is not kind:
            raise ValueError(
                f"A clip range needs maps of one kind; got {kind.value} and "
                f"{depth_map.kind.value}."
            )
        values = depth_map.values[depth_map.valid_mask]
        if values.size == 0:
            continue
        share = keep_share * min(1.0, _CLIP_SAMPLES_PER_FRAME / values.size)
        if share < 1.0:
            # Random rather than every n-th sample: a fixed step aliases with
            # structured frames and skews the percentiles.
            values = _thin_at_random(values, share, rng)
            # Once the share has been halved, a sparse frame may keep nothing.
            if values.size == 0:
                continue
        samples.append(values)
        retained += values.size
        if retained > _CLIP_SAMPLE_BUDGET:
            # Thinning keeps each sample with probability 1/2, so every sample of
            # the clip is now kept with probability `keep_share / 2`.
            kept = _thin_at_random(np.concatenate(samples), 0.5, rng)
            samples, retained, keep_share = [kept], kept.size, keep_share / 2
        elif len(samples) > _CLIP_SAMPLE_ARRAYS:
            # The budget bounds the values but not the arrays holding them; many
            # sparse frames would each add one. Merging copies every retained
            # value, so it runs only once per this many frames.
            samples = [np.concatenate(samples)]
    if retained == 0:
        raise ValueError("The clip holds no depth to compute a range from.")
    display_range = _values_at_ranks(
        np.concatenate(samples), low_percentile, high_percentile
    )
    return cls(display_range=display_range)

supervision.depth.colormaps.DepthColormap

Bases: Enum

Colour tables a depth map can be painted with.

The near end of the depth range is always the warm or bright end of the table. Turbo separates the most depth steps and is the default; Viridis and Cividis keep their order in grayscale and for colour-blind viewers; Inferno and Magma start near black, so pixels without depth should stay unpainted with them.

Attributes:

Name Type Description
TURBO

Google's Turbo rainbow, built for depth and disparity.

VIRIDIS

Perceptually uniform, blue to yellow.

CIVIDIS

Perceptually uniform and optimised for colour-vision deficiency.

INFERNO

Perceptually uniform, black to pale yellow.

MAGMA

Perceptually uniform, black to pale pink.

GRAYSCALE

Black far, white near.

Source code in src/supervision/depth/colormaps.py
class DepthColormap(Enum):
    """Colour tables a depth map can be painted with.

    The near end of the depth range is always the warm or bright end of the table.
    Turbo separates the most depth steps and is the default; Viridis and Cividis keep
    their order in grayscale and for colour-blind viewers; Inferno and Magma start
    near black, so pixels without depth should stay unpainted with them.

    Attributes:
        TURBO: Google's Turbo rainbow, built for depth and disparity.
        VIRIDIS: Perceptually uniform, blue to yellow.
        CIVIDIS: Perceptually uniform and optimised for colour-vision deficiency.
        INFERNO: Perceptually uniform, black to pale yellow.
        MAGMA: Perceptually uniform, black to pale pink.
        GRAYSCALE: Black far, white near.
    """

    TURBO = "turbo"
    VIRIDIS = "viridis"
    CIVIDIS = "cividis"
    INFERNO = "inferno"
    MAGMA = "magma"
    GRAYSCALE = "grayscale"

    @classmethod
    def list(cls) -> list[str]:
        """Return the string value of every colormap."""
        return [member.value for member in cls]

    @classmethod
    def from_value(cls, value: DepthColormap | str) -> DepthColormap:
        """Resolve a colormap from an enum member or its case-insensitive name.

        Args:
            value: A :class:`~supervision.depth.colormaps.DepthColormap` member or
                one of its string values.

        Returns:
            The matching :class:`~supervision.depth.colormaps.DepthColormap`.

        Raises:
            ValueError: If `value` names no colormap.

        Examples:
            ```pycon
            >>> import supervision as sv
            >>> sv.DepthColormap.from_value("Viridis")
            <DepthColormap.VIRIDIS: 'viridis'>

            ```
        """
        if isinstance(value, cls):
            return value
        if isinstance(value, str):
            try:
                return cls(value.lower())
            except ValueError:
                pass
        raise ValueError(
            f"Invalid depth colormap: {value!r}. Must be one of {cls.list()}."
        )

Methods:

from_value(value: DepthColormap | str) -> DepthColormap classmethod

Resolve a colormap from an enum member or its case-insensitive name.

Parameters:

Name Type Description Default
value
DepthColormap | str

A :class:~supervision.depth.colormaps.DepthColormap member or one of its string values.

required

Returns:

Type Description
DepthColormap

The matching :class:~supervision.depth.colormaps.DepthColormap.

Raises:

Type Description
ValueError

If value names no colormap.

Examples:

>>> import supervision as sv
>>> sv.DepthColormap.from_value("Viridis")
<DepthColormap.VIRIDIS: 'viridis'>
Source code in src/supervision/depth/colormaps.py
@classmethod
def from_value(cls, value: DepthColormap | str) -> DepthColormap:
    """Resolve a colormap from an enum member or its case-insensitive name.

    Args:
        value: A :class:`~supervision.depth.colormaps.DepthColormap` member or
            one of its string values.

    Returns:
        The matching :class:`~supervision.depth.colormaps.DepthColormap`.

    Raises:
        ValueError: If `value` names no colormap.

    Examples:
        ```pycon
        >>> import supervision as sv
        >>> sv.DepthColormap.from_value("Viridis")
        <DepthColormap.VIRIDIS: 'viridis'>

        ```
    """
    if isinstance(value, cls):
        return value
    if isinstance(value, str):
        try:
            return cls(value.lower())
        except ValueError:
            pass
    raise ValueError(
        f"Invalid depth colormap: {value!r}. Must be one of {cls.list()}."
    )

list() -> list[str] classmethod

Return the string value of every colormap.

Source code in src/supervision/depth/colormaps.py
@classmethod
def list(cls) -> list[str]:
    """Return the string value of every colormap."""
    return [member.value for member in cls]

supervision.depth.core.DepthScale

Bases: Enum

The scale a depth map is coloured on: inverse depth or metric depth.

An explicit display_range stays in the map's own unit whatever the scale.

Attributes:

Name Type Description
INVERSE

Inverse depth, where larger is nearer: a disparity or relative map as it is, and a metric map as 1 / depth (disparity with a camera). It spends colour on near detail, the way stereo measures it.

METRIC

Metric depth in metres, linear in distance. Needs a depth_m map, or a disparity_px map with a camera.

Source code in src/supervision/depth/core.py
class DepthScale(Enum):
    """The scale a depth map is coloured on: inverse depth or metric depth.

    An explicit `display_range` stays in the map's own unit whatever the scale.

    Attributes:
        INVERSE: Inverse depth, where larger is nearer: a disparity or relative map
            as it is, and a metric map as 1 / depth (disparity with a camera). It
            spends colour on near detail, the way stereo measures it.
        METRIC: Metric depth in metres, linear in distance. Needs a `depth_m` map,
            or a `disparity_px` map with a camera.
    """

    INVERSE = "inverse"
    METRIC = "metric"

    @classmethod
    def list(cls) -> list[str]:
        """Return the string value of every scale."""
        return [member.value for member in cls]

    @classmethod
    def from_value(cls, value: DepthScale | str) -> DepthScale:
        """Resolve a scale from an enum member or its case-insensitive value.

        Args:
            value: A :class:`~supervision.depth.core.DepthScale` member or one of
                its string values.

        Returns:
            The matching :class:`~supervision.depth.core.DepthScale`.

        Raises:
            ValueError: If `value` names no scale.
        """
        if isinstance(value, cls):
            return value
        if isinstance(value, str):
            try:
                return cls(value.lower())
            except ValueError:
                pass
        raise ValueError(
            f"Invalid depth scale: {value!r}. Must be one of {cls.list()}."
        )

Methods:

from_value(value: DepthScale | str) -> DepthScale classmethod

Resolve a scale from an enum member or its case-insensitive value.

Parameters:

Name Type Description Default
value
DepthScale | str

A :class:~supervision.depth.core.DepthScale member or one of its string values.

required

Returns:

Type Description
DepthScale

The matching :class:~supervision.depth.core.DepthScale.

Raises:

Type Description
ValueError

If value names no scale.

Source code in src/supervision/depth/core.py
@classmethod
def from_value(cls, value: DepthScale | str) -> DepthScale:
    """Resolve a scale from an enum member or its case-insensitive value.

    Args:
        value: A :class:`~supervision.depth.core.DepthScale` member or one of
            its string values.

    Returns:
        The matching :class:`~supervision.depth.core.DepthScale`.

    Raises:
        ValueError: If `value` names no scale.
    """
    if isinstance(value, cls):
        return value
    if isinstance(value, str):
        try:
            return cls(value.lower())
        except ValueError:
            pass
    raise ValueError(
        f"Invalid depth scale: {value!r}. Must be one of {cls.list()}."
    )

list() -> list[str] classmethod

Return the string value of every scale.

Source code in src/supervision/depth/core.py
@classmethod
def list(cls) -> list[str]:
    """Return the string value of every scale."""
    return [member.value for member in cls]

Comments