caly-camera-engine / utils /math_utils.py
Mohamed
initial deploy
1928c42
Raw
History Blame Contribute Delete
3.64 kB
"""Mathematical helpers for masks, calibration, and volume estimation."""
from __future__ import annotations
import math
from typing import Sequence
import numpy as np
def mask_iou(a: np.ndarray, b: np.ndarray) -> float:
"""Return intersection-over-union for two binary masks."""
a_bool = np.asarray(a).astype(bool)
b_bool = np.asarray(b).astype(bool)
intersection = np.logical_and(a_bool, b_bool).sum()
union = np.logical_or(a_bool, b_bool).sum()
return float(intersection) / float(union) if union else 0.0
def bbox_iou(a: Sequence[float], b: Sequence[float]) -> float:
"""Return intersection-over-union for two ``[x1, y1, x2, y2]`` boxes."""
ax1, ay1, ax2, ay2 = map(float, a)
bx1, by1, bx2, by2 = map(float, b)
ix1, iy1 = max(ax1, bx1), max(ay1, by1)
ix2, iy2 = min(ax2, bx2), min(ay2, by2)
iw, ih = max(0.0, ix2 - ix1), max(0.0, iy2 - iy1)
inter = iw * ih
area_a = max(0.0, ax2 - ax1) * max(0.0, ay2 - ay1)
area_b = max(0.0, bx2 - bx1) * max(0.0, by2 - by1)
union = area_a + area_b - inter
return inter / union if union else 0.0
def polygon_area(points: np.ndarray) -> float:
"""Return polygon area in pixel units using the shoelace formula."""
if points is None or len(points) < 3:
return 0.0
pts = np.asarray(points, dtype=np.float64)
x = pts[:, 0]
y = pts[:, 1]
return float(abs(np.dot(x, np.roll(y, -1)) - np.dot(y, np.roll(x, -1))) / 2.0)
def cm_per_pixel_from_real_width(real_width_cm: float, width_pixels: float) -> float:
"""Convert a known real-world width and pixel width into cm-per-pixel."""
if width_pixels <= 0:
raise ValueError("width_pixels must be positive")
return float(real_width_cm) / float(width_pixels)
def area_cm2_per_pixel(cm_per_pixel: float) -> float:
"""Return square centimeters represented by one image pixel."""
if cm_per_pixel <= 0:
raise ValueError("cm_per_pixel must be positive")
return float(cm_per_pixel) ** 2
def cylinder_volume_cm3(radius_cm: float, height_cm: float) -> float:
"""Return cylinder volume in cubic centimeters."""
return math.pi * max(radius_cm, 0.0) ** 2 * max(height_cm, 0.0)
def solid_volume_cm3(mask_area_cm2: float, height_cm: float) -> float:
"""Return prism-like solid volume in cubic centimeters."""
return max(mask_area_cm2, 0.0) * max(height_cm, 0.0)
def soft_clamp(value: float, minimum: float, maximum: float) -> float:
"""Clamp low values hard, reduce high outliers smoothly.
Examples:
soft_clamp(100, 50, 200) -> 100 (within range, unchanged)
soft_clamp(10, 50, 200) -> 50 (below min, hard floor)
soft_clamp(500, 50, 200) -> ~280 (above max, smoothly reduced)
soft_clamp(900, 50, 200) -> ~320 (far above max, asymptotes to 320)
"""
value = float(value)
minimum = float(minimum)
maximum = float(maximum)
if maximum < minimum:
minimum, maximum = maximum, minimum
if value < minimum:
return minimum
if value <= maximum:
return value
soft_top = maximum * 1.6
excess = value - maximum
squashed = maximum + (soft_top - maximum) * (1.0 - math.exp(-excess / max(maximum, 1.0)))
return float(min(squashed, soft_top))
def robust_percentile(values: np.ndarray, percentile: float, default: float = 0.0) -> float:
"""Return a percentile while tolerating empty and non-finite arrays."""
arr = np.asarray(values, dtype=np.float32)
arr = arr[np.isfinite(arr)]
if arr.size == 0:
return float(default)
return float(np.percentile(arr, percentile))