CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/pypi-imutils

A series of convenience functions to make basic image processing functions such as translation, rotation, resizing, skeletonization, displaying Matplotlib images, sorting contours, detecting edges, and much more easier with OpenCV and both Python 2.7 and Python 3.

91

1.33x
Overview
Eval results
Files

feature-detection.mddocs/

Feature Detection

OpenCV-compatible feature detectors and descriptors with cross-version compatibility. These utilities provide unified interfaces for creating feature detectors, extractors, and matchers that work across different OpenCV versions.

Capabilities

Factory Functions

Factory functions that provide unified interfaces for creating feature detectors, extractors, and matchers with cross-version compatibility.

def FeatureDetector_create(detector, *args, **kwargs):
    """
    Create feature detector with cross-version compatibility.
    
    Args:
        detector (str): Type of detector to create
        *args: Positional arguments for detector
        **kwargs: Keyword arguments for detector
    
    Returns:
        Detector object with detect() method
        
    Supported detectors:
        BRISK, DENSE, FAST, GFTT, HARRIS, MSER, ORB, SIFT, SURF, STAR
    """

def DescriptorExtractor_create(extractor, *args, **kwargs):
    """
    Create descriptor extractor with cross-version compatibility.
    
    Args:
        extractor (str): Type of extractor to create
        *args: Positional arguments for extractor
        **kwargs: Keyword arguments for extractor
    
    Returns:
        Extractor object with compute() method
        
    Supported extractors:
        SIFT, ROOTSIFT, SURF, BRIEF, ORB, BRISK, FREAK
    """

def DescriptorMatcher_create(matcher):
    """
    Create descriptor matcher with cross-version compatibility.
    
    Args:
        matcher (str): Type of matcher to create
    
    Returns:
        Matcher object
        
    Supported matchers:
        BruteForce, BruteForce-SL2, BruteForce-L1, BruteForce-Hamming, FlannBased
    """

Usage Example:

import cv2
from imutils.feature import FeatureDetector_create, DescriptorExtractor_create, DescriptorMatcher_create

# Load images
image1 = cv2.imread("image1.jpg", cv2.IMREAD_GRAYSCALE)
image2 = cv2.imread("image2.jpg", cv2.IMREAD_GRAYSCALE)

# Create detector and extractor
detector = FeatureDetector_create("SIFT")
extractor = DescriptorExtractor_create("SIFT")

# Detect keypoints
kp1 = detector.detect(image1)
kp2 = detector.detect(image2)

# Compute descriptors
kp1, desc1 = extractor.compute(image1, kp1)
kp2, desc2 = extractor.compute(image2, kp2)

# Create matcher and find matches
matcher = DescriptorMatcher_create("BruteForce")
matches = matcher.match(desc1, desc2)

# Draw matches
output = cv2.drawMatches(image1, kp1, image2, kp2, matches, None)
cv2.imshow("Matches", output)
cv2.waitKey(0)
cv2.destroyAllWindows()

Custom Feature Detectors

Custom implementations of feature detectors that maintain consistent APIs across OpenCV versions.

Dense Feature Detector

class DENSE:
    def __init__(self, step=6, radius=0.5):
        """
        Dense keypoint detector that samples keypoints on a grid.
        
        Args:
            step (int): Step size for grid sampling (default: 6)
            radius (float): Keypoint radius (default: 0.5)
        """
    
    def detect(self, img):
        """
        Detect dense keypoints on a grid.
        
        Args:
            img (np.ndarray): Input image
        
        Returns:
            list: List of cv2.KeyPoint objects
        """
    
    def setInt(self, var, val):
        """
        Set integer parameters.
        
        Args:
            var (str): Parameter name ("initXyStep")
            val (int): Parameter value
        """

Good Features To Track Detector

class GFTT:
    def __init__(self, maxCorners=0, qualityLevel=0.01, minDistance=1, mask=None, 
                 blockSize=3, useHarrisDetector=False, k=0.04):
        """
        Good Features To Track detector.
        
        Args:
            maxCorners (int): Maximum number of corners (0 = no limit) (default: 0)
            qualityLevel (float): Quality level parameter (default: 0.01)
            minDistance (int): Minimum distance between corners (default: 1)
            mask (np.ndarray, optional): Mask image
            blockSize (int): Size of averaging block (default: 3)
            useHarrisDetector (bool): Use Harris detector (default: False)
            k (float): Harris detector free parameter (default: 0.04)
        """
    
    def detect(self, img):
        """
        Detect good features to track.
        
        Args:
            img (np.ndarray): Input image
        
        Returns:
            list: List of cv2.KeyPoint objects
        """

Harris Corner Detector

class HARRIS:
    def __init__(self, blockSize=2, apertureSize=3, k=0.1, T=0.02):
        """
        Harris corner detector.
        
        Args:
            blockSize (int): Size of neighborhood (default: 2)
            apertureSize (int): Aperture parameter for Sobel operator (default: 3)
            k (float): Harris detector free parameter (default: 0.1)
            T (float): Threshold for corner detection (default: 0.02)
        """
    
    def detect(self, img):
        """
        Detect Harris corners.
        
        Args:
            img (np.ndarray): Input image
        
        Returns:
            list: List of cv2.KeyPoint objects with 3-pixel radius
        """

Usage Example:

import cv2
from imutils.feature import DENSE, GFTT, HARRIS

image = cv2.imread("example.jpg", cv2.IMREAD_GRAYSCALE)

# Dense detector
dense = DENSE(step=10, radius=3)
dense_kp = dense.detect(image)

# GFTT detector
gftt = GFTT(maxCorners=100, qualityLevel=0.01, minDistance=10)
gftt_kp = gftt.detect(image)

# Harris detector
harris = HARRIS(blockSize=2, k=0.04, T=0.01)
harris_kp = harris.detect(image)

# Visualize keypoints
dense_img = cv2.drawKeypoints(image, dense_kp, None, color=(0, 255, 0))
gftt_img = cv2.drawKeypoints(image, gftt_kp, None, color=(255, 0, 0))
harris_img = cv2.drawKeypoints(image, harris_kp, None, color=(0, 0, 255))

cv2.imshow("Dense", dense_img)
cv2.imshow("GFTT", gftt_img)
cv2.imshow("Harris", harris_img)
cv2.waitKey(0)
cv2.destroyAllWindows()

Enhanced Descriptors

RootSIFT Descriptor

Enhanced SIFT descriptor with L1 normalization and square root operation for improved matching performance.

class RootSIFT:
    def __init__(self):
        """
        RootSIFT descriptor extractor.
        
        Automatically initializes SIFT extractor based on OpenCV version.
        Applies L1 normalization followed by square root operation.
        """
    
    def compute(self, image, kps, eps=1e-7):
        """
        Compute RootSIFT descriptors.
        
        Args:
            image (np.ndarray): Input image
            kps (list): List of keypoints
            eps (float): Small epsilon value for numerical stability (default: 1e-7)
        
        Returns:
            tuple: (keypoints, descriptors) where descriptors are RootSIFT-normalized
            
        Note:
            RootSIFT normalization: L1 normalize -> square root -> L2 normalize
            This provides better matching performance than standard SIFT.
        """

Usage Example:

import cv2
from imutils.feature import RootSIFT, FeatureDetector_create

# Load images
image1 = cv2.imread("image1.jpg", cv2.IMREAD_GRAYSCALE)
image2 = cv2.imread("image2.jpg", cv2.IMREAD_GRAYSCALE)

# Create detector and RootSIFT extractor
detector = FeatureDetector_create("SIFT")
rootsift = RootSIFT()

# Detect keypoints
kp1 = detector.detect(image1)
kp2 = detector.detect(image2)

# Compute RootSIFT descriptors
kp1, desc1 = rootsift.compute(image1, kp1)
kp2, desc2 = rootsift.compute(image2, kp2)

# Match descriptors
bf = cv2.BFMatcher()
matches = bf.knnMatch(desc1, desc2, k=2)

# Apply ratio test
good_matches = []
for m, n in matches:
    if m.distance < 0.75 * n.distance:
        good_matches.append([m])

# Draw matches
output = cv2.drawMatchesKnn(image1, kp1, image2, kp2, good_matches, None, flags=2)
cv2.imshow("RootSIFT Matches", output)
cv2.waitKey(0)
cv2.destroyAllWindows()

Helper Utilities

def corners_to_keypoints(corners):
    """
    Convert corners from cv2.goodFeaturesToTrack to cv2.KeyPoint objects.
    
    Args:
        corners (np.ndarray): Corners array from cv2.goodFeaturesToTrack
    
    Returns:
        list: List of cv2.KeyPoint objects
    """

Usage Example:

import cv2
from imutils.feature import corners_to_keypoints

image = cv2.imread("example.jpg", cv2.IMREAD_GRAYSCALE)

# Use OpenCV's goodFeaturesToTrack
corners = cv2.goodFeaturesToTrack(image, maxCorners=100, qualityLevel=0.01, minDistance=10)

# Convert to KeyPoint objects
keypoints = corners_to_keypoints(corners)

# Draw keypoints
output = cv2.drawKeypoints(image, keypoints, None, color=(0, 255, 0))
cv2.imshow("Keypoints from Corners", output)
cv2.waitKey(0)
cv2.destroyAllWindows()

Complete Feature Matching Pipeline

Here's a comprehensive example demonstrating feature detection, description, and matching:

import cv2
import numpy as np
from imutils.feature import (FeatureDetector_create, DescriptorExtractor_create,
                            DescriptorMatcher_create, RootSIFT)

def match_features(image1_path, image2_path, detector_type="SIFT", extractor_type="ROOTSIFT"):
    # Load images
    img1 = cv2.imread(image1_path, cv2.IMREAD_GRAYSCALE)
    img2 = cv2.imread(image2_path, cv2.IMREAD_GRAYSCALE)
    
    # Create detector
    detector = FeatureDetector_create(detector_type)
    
    # Create extractor
    if extractor_type == "ROOTSIFT":
        extractor = RootSIFT()
    else:
        extractor = DescriptorExtractor_create(extractor_type)
    
    # Detect keypoints
    kp1 = detector.detect(img1)
    kp2 = detector.detect(img2)
    
    print(f"Image 1: {len(kp1)} keypoints")
    print(f"Image 2: {len(kp2)} keypoints")
    
    # Compute descriptors
    if extractor_type == "ROOTSIFT":
        kp1, desc1 = extractor.compute(img1, kp1)
        kp2, desc2 = extractor.compute(img2, kp2)
    else:
        desc1 = extractor.compute(img1, kp1)[1]
        desc2 = extractor.compute(img2, kp2)[1]
    
    # Skip if no descriptors found
    if desc1 is None or desc2 is None:
        print("No descriptors found")
        return
    
    # Create matcher
    if extractor_type in ["ORB", "BRISK"]:
        matcher = DescriptorMatcher_create("BruteForce-Hamming")
    else:
        matcher = DescriptorMatcher_create("BruteForce")
    
    # Match descriptors
    matches = matcher.knnMatch(desc1, desc2, k=2)
    
    # Apply ratio test (Lowe's ratio test)
    good_matches = []
    for match_pair in matches:
        if len(match_pair) == 2:
            m, n = match_pair
            if m.distance < 0.75 * n.distance:
                good_matches.append(m)
    
    print(f"Good matches: {len(good_matches)}")
    
    # Draw matches
    output = cv2.drawMatches(img1, kp1, img2, kp2, good_matches, None,
                            flags=cv2.DrawMatchesFlags_NOT_DRAW_SINGLE_POINTS)
    
    # Resize for display if too large
    if output.shape[1] > 1200:
        scale = 1200 / output.shape[1]
        new_width = int(output.shape[1] * scale)
        new_height = int(output.shape[0] * scale)
        output = cv2.resize(output, (new_width, new_height))
    
    cv2.imshow(f"{detector_type}-{extractor_type} Matches", output)
    cv2.waitKey(0)
    cv2.destroyAllWindows()
    
    return good_matches

def compare_detectors(image1_path, image2_path):
    """Compare different detector/extractor combinations."""
    combinations = [
        ("SIFT", "SIFT"),
        ("SIFT", "ROOTSIFT"),
        ("ORB", "ORB"),
        ("GFTT", "SIFT"),
        ("HARRIS", "SIFT")
    ]
    
    results = {}
    
    for detector, extractor in combinations:
        try:
            print(f"\nTesting {detector}-{extractor}...")
            matches = match_features(image1_path, image2_path, detector, extractor)
            results[f"{detector}-{extractor}"] = len(matches) if matches else 0
        except Exception as e:
            print(f"Error with {detector}-{extractor}: {e}")
            results[f"{detector}-{extractor}"] = 0
    
    # Print comparison results
    print("\nComparison Results:")
    print("-" * 30)
    for combo, count in sorted(results.items(), key=lambda x: x[1], reverse=True):
        print(f"{combo:15}: {count:3} matches")
    
    return results

# Usage
if __name__ == "__main__":
    # Match features between two images
    matches = match_features("image1.jpg", "image2.jpg", "SIFT", "ROOTSIFT")
    
    # Compare different detector/extractor combinations
    comparison_results = compare_detectors("image1.jpg", "image2.jpg")

Advanced Feature Matching with Homography

Example of robust feature matching with RANSAC homography estimation:

import cv2
import numpy as np
from imutils.feature import RootSIFT, FeatureDetector_create

def robust_feature_matching(image1_path, image2_path, min_matches=10):
    # Load images
    img1 = cv2.imread(image1_path, cv2.IMREAD_GRAYSCALE)
    img2 = cv2.imread(image2_path, cv2.IMREAD_GRAYSCALE)
    
    # Initialize detector and RootSIFT
    detector = FeatureDetector_create("SIFT")
    rootsift = RootSIFT()
    
    # Detect and compute
    kp1 = detector.detect(img1)
    kp2 = detector.detect(img2)
    kp1, desc1 = rootsift.compute(img1, kp1)
    kp2, desc2 = rootsift.compute(img2, kp2)
    
    # Match descriptors
    bf = cv2.BFMatcher()
    matches = bf.knnMatch(desc1, desc2, k=2)
    
    # Apply ratio test
    good_matches = []
    for m, n in matches:
        if m.distance < 0.75 * n.distance:
            good_matches.append(m)
    
    if len(good_matches) < min_matches:
        print(f"Not enough matches found: {len(good_matches)}/{min_matches}")
        return None
    
    # Extract matched points
    src_pts = np.float32([kp1[m.queryIdx].pt for m in good_matches]).reshape(-1, 1, 2)
    dst_pts = np.float32([kp2[m.trainIdx].pt for m in good_matches]).reshape(-1, 1, 2)
    
    # Find homography with RANSAC
    homography, mask = cv2.findHomography(src_pts, dst_pts, 
                                        cv2.RANSAC, 5.0)
    
    # Count inliers
    inliers = mask.ravel().tolist()
    inlier_matches = [good_matches[i] for i in range(len(good_matches)) if inliers[i]]
    
    print(f"Total matches: {len(good_matches)}")
    print(f"Inlier matches: {len(inlier_matches)}")
    
    # Draw matches
    draw_params = dict(matchColor=(0, 255, 0),
                      singlePointColor=None,
                      matchesMask=inliers,
                      flags=2)
    
    output = cv2.drawMatches(img1, kp1, img2, kp2, good_matches, None, **draw_params)
    
    # Draw bounding box of detected object in second image
    if homography is not None:
        h, w = img1.shape
        corners = np.float32([[0, 0], [w, 0], [w, h], [0, h]]).reshape(-1, 1, 2)
        transformed_corners = cv2.perspectiveTransform(corners, homography)
        
        # Draw the bounding box
        img2_color = cv2.cvtColor(img2, cv2.COLOR_GRAY2BGR)
        cv2.polylines(img2_color, [np.int32(transformed_corners)], True, (0, 255, 0), 3)
        
        cv2.imshow("Detected Object", img2_color)
    
    cv2.imshow("Robust Feature Matching", output)
    cv2.waitKey(0)
    cv2.destroyAllWindows()
    
    return homography

# Usage
homography = robust_feature_matching("template.jpg", "scene.jpg", min_matches=15)

Install with Tessl CLI

npx tessl i tessl/pypi-imutils

docs

core-processing.md

face-analysis.md

feature-detection.md

index.md

utilities.md

video-processing.md

tile.json