Skip to content

Vision — Algorithms

Image-processing algorithms that run on frames from any camera: ArUco markers, color filtering, line detection, distance regression, and MediaPipe hand/face tracking. Each is a small class you construct once and call per frame.

See also: Cameras · ROS 2 nodes · Vision overview.

Class hierarchy

classDiagram
    class Aruco {
        -camera_matrix ndarray
        -camera_distortion ndarray
        -aruco_detect Dictionary
        -aruco_param DetectorParameters
        +marker_dict int
        +tag_size float
        +detect(img, draw) tuple
        +pose_estimate(img, draw) tuple
        +calculateYawFromCorners(bbox) float
        +aruco_config(marker_dict, tag_size)
    }

    class ColorDetector {
        -mode str
        -color_space ColorSpace
        -color_values list
        -file_path str
        +mask ndarray
        +result ndarray
        +initTrackbars()
        +getTrackValues() list
        +filterColor(img)
        +get_color_values(color_name) list
        +saveColorValues()
    }

    class ColorSpace {
        <<enumeration>>
        HSV
        LAB
    }

    class ILineEstimationMethod {
        <<abstract>>
        +estimate(img_detect, img_out, offset, draw, draw_color)* tuple
    }

    class HoughLinesP {
        +estimate(img_detect, img_out, offset, draw, draw_color) tuple
    }

    class RotatedRect {
        +estimate(img_detect, img_out, offset, draw, draw_color) tuple
    }

    class FitEllipse {
        +estimate(img_detect, img_out, offset, draw, draw_color) tuple
    }

    class RansacLine {
        +estimate(img_detect, img_out, offset, draw, draw_color) tuple
    }

    class AdaptiveHoughLinesP {
        +estimate(img_detect, img_out, offset, draw, draw_color) tuple
    }

    class LineDetector {
        -color_detector ColorDetector
        -estimation_method ILineEstimationMethod
        -prev_angle float
        +detect_line(img, region, draw, draw_color) tuple
        +set_text_positions(positions_dict)
    }

    ColorDetector --> ColorSpace
    ILineEstimationMethod <|.. HoughLinesP
    ILineEstimationMethod <|.. RotatedRect
    ILineEstimationMethod <|.. FitEllipse
    ILineEstimationMethod <|.. RansacLine
    ILineEstimationMethod <|.. AdaptiveHoughLinesP
    LineDetector o-- ColorDetector
    LineDetector o-- ILineEstimationMethod

ArUco markers

Detects ArUco markers using cv2.aruco.ArucoDetector and estimates marker position plus yaw via cv2.aruco.estimatePoseSingleMarkers (pose_estimate returns translation and a corner-derived yaw; the rotation vector is not exposed).

Note: pose estimation requires the camera intrinsic matrix from camera calibration.

Aruco(marker_dict: int, tag_size: float)

bbox, marker_id = aruco.detect(img, draw=True)                  # detection only
marker_id, translation, yaw = aruco.pose_estimate(img, draw=True)  # requires calibration
  • marker_dict: dictionary size (4, 5, 6, 7 for 4x4, 5x5, ...)
  • tag_size: physical marker size in meters
  • pose_estimate returns the marker id (or None), translation [x, y, z] in meters (camera frame), and yaw in degrees (0-360)
from nectar.vision import Aruco

aruco = Aruco(marker_dict=5, tag_size=0.05)  # 5x5, 5 cm
marker_id, translation, yaw = aruco.pose_estimate(frame, draw=True)
if marker_id is not None:
    x, y, z = translation
    print(f"Marker {marker_id}: x={x:.2f} y={y:.2f} z={z:.2f} yaw={yaw:.1f}")

Color detection

Color-space filtering using cv2.inRange() with morphological operations. HSV (hue 0-179, saturation/value 0-255) and LAB (L 0-255, a/b 0-255) are supported. Two modes: track (interactive trackbar calibration) and preset (load pre-calibrated values from JSON).

ColorDetector(mode: str = "track", color: str = None, color_space: ColorSpace = ColorSpace.HSV, file_path: str = None)

detector.filterColor(img)  # updates detector.mask and detector.result
detector.initTrackbars()   # calibration window (track mode)
detector.saveColorValues() # save calibration to JSON
from nectar.vision import ColorDetector, ColorSpace

# track: tune thresholds live, then saveColorValues("red")
detector = ColorDetector(mode="track", color_space=ColorSpace.HSV)
detector.initTrackbars()

# preset: load saved values from JSON
detector = ColorDetector(mode="preset", color="red", color_space=ColorSpace.HSV)
detector.filterColor(frame)
mask = detector.mask

Calibration JSON (default ~/.config/nectar/color_calibration.json; pass file_path= for a mission file). Calibrate once with the color calibration node or saveColorValues(), then load by name. Copy that file to the same path on the vehicle, or pass -p calibration_file:=/path/to/colors.json.

{
    "red": {
        "HSV": [[0, 100, 100], [10, 255, 255]],
        "LAB": [[20, 150, 128], [255, 200, 200]]
    },
    "blue": {
        "HSV": [[100, 100, 100], [130, 255, 255]]
    }
}

Line detection

Color-based line detection: a binary mask from ColorDetector plus a geometric estimation method.

Method Algorithm When to use
HoughLinesP cv2.HoughLinesP → cv2.fitLine on endpoints Thin lines, multiple segments to merge
RotatedRect cv2.minAreaRect on largest contour Thick continuous lines, single blob
FitEllipse cv2.fitEllipse on contour Curved/arc-shaped lines
RansacLine cv2.fitLine with DIST_L2 on contour points Noisy masks with outlier pixels
AdaptiveHoughLinesP HoughLinesP with threshold from mean + std of mask Varying illumination
LineDetector(color: str, estimation_method: ILineEstimationMethod, color_space: ColorSpace = None, file_path: str = None)

img, mask, cx, cy, angle, width, height = detector.detect_line(
    img, region=(400, 300), draw=True, draw_color=(0, 255, 0)
)

detect_line returns the annotated image, the binary region mask, the line center cx, cy in pixels, the angle in degrees (-90 to 90), and the average line width, height in pixels.

from nectar.vision import LineDetector, HoughLinesP, ColorSpace

detector = LineDetector(
    color="blue", estimation_method=HoughLinesP, color_space=ColorSpace.HSV
)
result, mask, cx, cy, angle, w, h = detector.detect_line(frame, draw=True)
if not math.isnan(cx):
    print(f"Line at ({cx:.0f}, {cy:.0f}), angle {angle:.1f}")

Distance estimation

Converts a pixel measurement to a real-world distance using regression models.

Note: requires calibration data — (distance_cm, pixel_value) pairs collected at known distances.

classDiagram
    class DistanceEstimator {
        -_params Dict
        -_models Dict~ModelType,EstimationModel~
        -_model_type ModelType
        +model EstimationModel
        +model_type ModelType
        +estimate(value, model_type) float
        +estimate_batch(values, model_type) ndarray
        +compare_methods(value) Dict~str,float~
        +get_model(model_type) EstimationModel
        +set_model_params(model_type, params)
        +available_methods()$ list
    }

    class ModelCalibrator {
        -pixels ndarray
        -distances ndarray
        -results Dict~ModelType,CalibrationResult~
        +fit_model(model_type) CalibrationResult
        +fit_all() Dict
        +best_model(criterion) CalibrationResult
        +save_params(path, default_method)
        +print_results()
        +plot(save_path)
    }

    class CalibrationResult {
        <<dataclass>>
        +model_type ModelType
        +params Dict
        +rmse float
        +r2 float
        +mae float
        +aic float
        +predictions ndarray
        +name str
    }

    class ModelType {
        <<enumeration>>
        LINEAR
        POLYNOMIAL
        EXPONENTIAL
        LOGARITHMIC
        INVERSE_POWER
        ROBUST_POLY2
    }

    class EstimationModel {
        <<abstract>>
        +estimate(value)* float
        +fit(x, y)* Dict
    }

    class LinearModel {
        +k float
        +estimate(value) float
        +fit(x, y) Dict
    }

    class PolynomialModel {
        +coeffs list
        +degree int
        +estimate(value) float
        +fit(x, y) Dict
    }

    class ExponentialModel {
        +a float
        +b float
        +c float
        +estimate(value) float
        +fit(x, y) Dict
    }

    class LogarithmicModel {
        +a float
        +b float
        +estimate(value) float
        +fit(x, y) Dict
    }

    class InversePowerModel {
        +k float
        +p float
        +estimate(value) float
        +fit(x, y) Dict
    }

    class RobustPoly2Model {
        +a float
        +b float
        +c float
        +estimate(value) float
        +fit(x, y) Dict
    }

    DistanceEstimator o-- EstimationModel
    ModelCalibrator --> CalibrationResult
    EstimationModel <|.. LinearModel
    EstimationModel <|.. PolynomialModel
    EstimationModel <|.. ExponentialModel
    EstimationModel <|.. LogarithmicModel
    EstimationModel <|.. InversePowerModel
    EstimationModel <|.. RobustPoly2Model
Model Formula Fitting method
LINEAR d = k / pixels Mean of distance × pixels
POLYNOMIAL d = Σ(aᵢ × pixelsⁱ) np.polyfit (least squares)
EXPONENTIAL d = a × e^(-b×pixels) + c scipy.optimize.curve_fit
LOGARITHMIC d = a × ln(pixels) + b scipy.optimize.curve_fit
INVERSE_POWER d = k / pixels^p scipy.optimize.curve_fit with bounds
ROBUST_POLY2 d = a×pixels² + b×pixels + c sklearn.linear_model.HuberRegressor (outlier-resistant)

Calibrate once, then estimate:

from nectar.vision.algorithms.distance import ModelCalibrator
from nectar.vision import DistanceEstimator, ModelType
from pathlib import Path

data = [(50, 32.2), (60, 28.5), (70, 24.2), (100, 21.6), (150, 16.8)]  # (distance_cm, pixels)
calibrator = ModelCalibrator(data)
calibrator.fit_all()
calibrator.print_results()
calibrator.save_params(Path("parameters.yaml"))
calibrator.plot(Path("model_comparison.png"))

estimator = DistanceEstimator(model_type=ModelType.POLYNOMIAL)
distance_cm = estimator.estimate(21.6)                 # single
distances = estimator.estimate_batch([15, 20, 25, 30]) # batch
results = estimator.compare_methods(21.6)              # {model: distance}

MediaPipe tracking

Real-time hand and face landmark detection using MediaPipe's models.

classDiagram
    class HandTrackerConfig {
        <<dataclass>>
        +model_path Optional~str~
        +num_hands int
        +min_detection_confidence float
        +min_presence_confidence float
        +min_tracking_confidence float
        +running_mode str
    }

    class HandResult {
        <<dataclass>>
        +landmarks list
        +world_landmarks list
        +handedness str
        +confidence float
    }

    class HandTracker {
        -_config HandTrackerConfig
        -_detector HandLandmarker
        +is_running bool
        +start()
        +close()
        +detect(frame, draw) ndarray
        +get_hands() List~HandResult~
        +raised_fingers(hand_idx) List~int~
        +get_landmarks(hand_idx, landmark_ids) list
        +find_distance(l1, l2, image, hand_idx, draw) tuple
        +estimate_depth(hand_idx, width, height) float
    }

    class FaceMeshTrackerConfig {
        <<dataclass>>
        +model_path Optional~str~
        +num_faces int
        +min_detection_confidence float
        +min_presence_confidence float
        +min_tracking_confidence float
        +output_blendshapes bool
        +running_mode str
    }

    class FaceResult {
        <<dataclass>>
        +landmarks list
        +blendshapes Optional~list~
    }

    class FaceMeshTracker {
        -_config FaceMeshTrackerConfig
        -_detector FaceLandmarker
        +is_running bool
        +start()
        +close()
        +detect(frame, draw) ndarray
        +draw_region(image, landmark_indices, color, radius) ndarray
        +get_faces() List~FaceResult~
        +get_landmarks(face_idx, landmark_ids) list
        +get_eye_aspect_ratio(eye, face_idx) float
        +get_mouth_aspect_ratio(face_idx) float
    }

    HandTracker o-- HandTrackerConfig
    HandTracker --> HandResult
    FaceMeshTracker o-- FaceMeshTrackerConfig
    FaceMeshTracker --> FaceResult

Both trackers share the same shape: construct with a config, use as a context manager, call detect(frame, draw=True) per frame, then read landmarks. model_path=None auto-downloads the model; running_mode is "IMAGE" (sync — results available immediately after detect()) or "LIVE_STREAM" (async — results arrive via callback). Use "IMAGE" in simple scripts unless you need a live camera callback pipeline.

Hand tracking — 21 landmarks per hand, with finger-state gesture recognition:

from nectar.vision import HandTracker, HandTrackerConfig

with HandTracker(HandTrackerConfig(num_hands=2, running_mode="IMAGE")) as tracker:
    tracker.detect(frame, draw=True)
    fingers = tracker.raised_fingers()          # [thumb, index, middle, ring, pinky]
    gestures = {
        (0, 0, 0, 0, 0): "fist",
        (1, 1, 1, 1, 1): "open_palm",
        (0, 1, 0, 0, 0): "pointing",
        (0, 1, 1, 0, 0): "peace",
    }
    gesture = gestures.get(tuple(fingers), "unknown")

Face mesh tracking — 478 landmarks per face, with eye/mouth aspect ratios and region extraction:

from nectar.vision import FaceMeshTracker, FaceMeshTrackerConfig, FaceLandmarkRegion

with FaceMeshTracker(FaceMeshTrackerConfig(num_faces=1, running_mode="IMAGE")) as tracker:
    tracker.detect(frame, draw=True)
    if tracker.get_eye_aspect_ratio("left") < 0.15:
        print("Blink detected")
    left_eye = tracker.get_landmarks(landmark_ids=FaceLandmarkRegion.LEFT_EYE)
    tracker.draw_region(frame, FaceLandmarkRegion.LEFT_EYE, color=(0, 255, 0))

Optical flow

OpticalFlowEstimator provides sparse and dense frame-to-frame motion estimation. See examples/vision/optical_flow_example.py for a runnable visualization.

References

Function / library Documentation Used in
cv2.aruco.ArucoDetector ArUco Detection Aruco.detect()
cv2.aruco.estimatePoseSingleMarkers ArUco Pose Aruco.pose_estimate()
cv2.inRange, cv2.morphologyEx Thresholding ColorDetector.filterColor()
cv2.HoughLinesP, cv2.fitLine Hough Line Transform HoughLinesP, RansacLine
cv2.minAreaRect, cv2.fitEllipse Contour Features RotatedRect, FitEllipse
MediaPipe Hand Landmarker docs HandTracker
MediaPipe Face Landmarker docs FaceMeshTracker
numpy.polyfit / scipy.optimize.curve_fit / sklearn.HuberRegressor numpy · scipy · sklearn distance models