Source code for pyvisim.features._sift

from typing import Any

import numpy as np
from PIL import Image

from ..base import FeatureExtractorBase
from ..typing import Float32NumpyArray, MatLike, UInt8NumpyArray
from ._utils import _check_output_shape, _to_single_image
from ._vendored.sift.sift import SIFT as _SIFT

__all__ = ["SIFT"]


[docs] class SIFT(FeatureExtractorBase, _SIFT): """ Scale-Invariant Feature Transform (SIFT) feature extractor. References: =========== [1] Lowe, D. G. (2004). Distinctive image features from scale-invariant keypoints. """ def __init__( self, upsampling: int = 2, n_octaves: int = 8, n_scales: int = 3, sigma_min: float = 1.6, sigma_in: float = 0.5, c_dog: float = 0.04 / 3, c_edge: float = 10, n_bins: int = 36, lambda_ori: float = 1.5, c_max: float = 0.8, lambda_descr: float = 6, n_hist: int = 4, n_ori: int = 8, ) -> None: params: dict[str, Any] = { "upsampling": upsampling, "n_octaves": n_octaves, "n_scales": n_scales, "sigma_min": sigma_min, "sigma_in": sigma_in, "c_dog": c_dog, "c_edge": c_edge, "n_bins": n_bins, "lambda_ori": lambda_ori, "c_max": c_max, "lambda_descr": lambda_descr, "n_hist": n_hist, "n_ori": n_ori, } _SIFT.__init__(self, **params) # type: ignore[no-untyped-call] self._params = params self._output_dim = n_hist**2 * n_ori @property def output_dim(self) -> int: return self._output_dim def _state(self) -> dict[str, Any]: return {"config": dict(self._params)} @staticmethod def _to_grayscale(image: UInt8NumpyArray) -> UInt8NumpyArray: """ Collapse a canonical ``uint8`` image to the 2-D grayscale layout. :param image: A ``uint8`` array of shape ``(H, W)`` or ``(H, W, C)``. :return: A ``uint8`` array of shape ``(H, W)``. """ if image.ndim == 2: return image return np.asarray(Image.fromarray(image).convert("L"))
[docs] @_check_output_shape def __call__( self, image: MatLike, /, *, dims: str = "HWC", value_range: tuple[float, float] = (0.0, 255.0), ) -> Float32NumpyArray: canonical = _to_single_image(image, dims=dims, value_range=value_range) grayscale = self._to_grayscale(canonical) try: self.detect_and_extract(grayscale) # type: ignore[no-untyped-call] except RuntimeError: # The vendored detector raises when an image yields no keypoints; # the extractor contract is an empty descriptor matrix instead. return np.zeros((0, self.output_dim), dtype=np.float32) return np.asarray(self.descriptors, dtype=np.float32)
def __repr__(self) -> str: return f"SIFT(output_dim={self.output_dim})"