ring-sizer / src /confidence.py
feng-x's picture
Upload folder using huggingface_hub
347d1a8 verified
"""
Confidence scoring utilities.
This module handles:
- Card detection confidence
- Finger detection confidence
- Measurement stability confidence
- Edge quality confidence (v1)
- Aggregate confidence calculation
All thresholds and weights are imported from confidence_constants.py.
"""
import logging
import numpy as np
from typing import Dict, Any, Optional, Literal
from .confidence_constants import (
# Card confidence constants
CARD_IDEAL_ASPECT_RATIO,
CARD_MAX_ASPECT_DEVIATION,
CARD_WEIGHT_DETECTION,
CARD_WEIGHT_ASPECT,
CARD_WEIGHT_SCALE,
# Finger confidence constants
FINGER_IDEAL_MIN_AREA_FRACTION,
FINGER_IDEAL_MAX_AREA_FRACTION,
FINGER_WEIGHT_HAND_DETECTION,
FINGER_WEIGHT_MASK_VALIDITY,
# Measurement confidence constants
MEASUREMENT_CV_POOR,
MEASUREMENT_CONSISTENCY_THRESHOLD,
MEASUREMENT_OUTLIER_STD_MULTIPLIER,
MEASUREMENT_WIDTH_TYPICAL_MIN,
MEASUREMENT_WIDTH_TYPICAL_MAX,
MEASUREMENT_WIDTH_ABSOLUTE_MIN,
MEASUREMENT_WIDTH_ABSOLUTE_MAX,
MEASUREMENT_WEIGHT_VARIANCE,
MEASUREMENT_WEIGHT_CONSISTENCY,
MEASUREMENT_WEIGHT_OUTLIERS,
MEASUREMENT_WEIGHT_RANGE,
MEASUREMENT_RANGE_SCORE_IDEAL,
MEASUREMENT_RANGE_SCORE_BORDERLINE,
MEASUREMENT_RANGE_SCORE_OUTSIDE,
# Overall confidence constants
V0_WEIGHT_CARD,
V0_WEIGHT_FINGER,
V0_WEIGHT_MEASUREMENT,
V1_WEIGHT_CARD,
V1_WEIGHT_FINGER,
V1_WEIGHT_EDGE_QUALITY,
V1_WEIGHT_MEASUREMENT,
CONFIDENCE_LEVEL_HIGH_THRESHOLD,
CONFIDENCE_LEVEL_MEDIUM_THRESHOLD,
)
logger = logging.getLogger(__name__)
EdgeMethod = Literal["contour", "sobel", "sobel_fallback"]
def compute_card_confidence(
card_result: Dict[str, Any],
scale_confidence: float,
) -> float:
"""
Compute confidence score from card detection.
Uses constants:
- CARD_IDEAL_ASPECT_RATIO: ISO/IEC 7810 ID-1 aspect ratio
- CARD_MAX_ASPECT_DEVIATION: Maximum acceptable deviation (0.15)
- CARD_WEIGHT_*: Component weights (detection: 50%, aspect: 25%, scale: 25%)
Args:
card_result: Output from detect_credit_card()
scale_confidence: Scale calibration confidence
Returns:
Card confidence score [0, 1]
"""
# Base confidence from card detection
detection_conf = card_result.get("confidence", 0.0)
# Aspect ratio deviation penalty
aspect_ratio = card_result.get("aspect_ratio", 0.0)
aspect_deviation = abs(aspect_ratio - CARD_IDEAL_ASPECT_RATIO) / CARD_IDEAL_ASPECT_RATIO
# Penalize deviation beyond threshold
aspect_score = max(0, 1.0 - (aspect_deviation / CARD_MAX_ASPECT_DEVIATION))
# Combine components with weights
card_conf = (
CARD_WEIGHT_DETECTION * detection_conf +
CARD_WEIGHT_ASPECT * aspect_score +
CARD_WEIGHT_SCALE * scale_confidence
)
return float(np.clip(card_conf, 0, 1))
def compute_finger_confidence(
hand_data: Dict[str, Any],
finger_data: Dict[str, Any],
mask_area: int,
image_area: int,
) -> float:
"""
Compute confidence score from finger detection.
Uses constants:
- FINGER_IDEAL_MIN_AREA_FRACTION: Minimum ideal mask area (0.5% of image)
- FINGER_IDEAL_MAX_AREA_FRACTION: Maximum ideal mask area (5% of image)
- FINGER_WEIGHT_*: Component weights (hand: 70%, mask: 30%)
Args:
hand_data: Output from segment_hand()
finger_data: Output from isolate_finger()
mask_area: Area of cleaned finger mask in pixels
image_area: Total image area in pixels
Returns:
Finger confidence score [0, 1]
"""
# Hand landmark detection confidence from MediaPipe
hand_conf = hand_data.get("confidence", 0.0)
# Mask area validity (should be reasonable fraction of image)
mask_fraction = mask_area / image_area
# Ideal range: FINGER_IDEAL_MIN_AREA_FRACTION to FINGER_IDEAL_MAX_AREA_FRACTION
if mask_fraction < FINGER_IDEAL_MIN_AREA_FRACTION:
area_score = mask_fraction / FINGER_IDEAL_MIN_AREA_FRACTION
elif mask_fraction > FINGER_IDEAL_MAX_AREA_FRACTION:
area_score = max(0, 1.0 - (mask_fraction - FINGER_IDEAL_MAX_AREA_FRACTION) / FINGER_IDEAL_MAX_AREA_FRACTION)
else:
area_score = 1.0
# Combine components with weights
finger_conf = FINGER_WEIGHT_HAND_DETECTION * hand_conf + FINGER_WEIGHT_MASK_VALIDITY * area_score
return float(np.clip(finger_conf, 0, 1))
def compute_measurement_confidence(
width_data: Dict[str, Any],
median_width_cm: float,
) -> float:
"""
Compute confidence score from measurement stability.
Uses constants:
- MEASUREMENT_CV_POOR: Coefficient of variation threshold (0.15)
- MEASUREMENT_CONSISTENCY_THRESHOLD: Median-mean difference threshold (0.1)
- MEASUREMENT_OUTLIER_STD_MULTIPLIER: Outlier detection threshold (2.0)
- MEASUREMENT_WIDTH_*: Realistic width ranges (1.0-3.0 cm)
- MEASUREMENT_WEIGHT_*: Component weights (variance: 40%, consistency: 20%, outliers: 20%, range: 20%)
- MEASUREMENT_RANGE_SCORE_*: Range score values
Args:
width_data: Output from compute_cross_section_width()
median_width_cm: Median width in centimeters
Returns:
Measurement confidence score [0, 1]
"""
widths_px = np.array(width_data.get("widths_px", []))
if len(widths_px) == 0:
return 0.0
median_px = width_data.get("median_width_px", 0.0)
mean_px = width_data.get("mean_width_px", 0.0)
std_px = width_data.get("std_width_px", 0.0)
# 1. Variance score (lower variance = higher confidence)
coefficient_of_variation = std_px / (median_px + 1e-8)
# CV < MEASUREMENT_CV_POOR is acceptable
variance_score = max(0, 1.0 - coefficient_of_variation / MEASUREMENT_CV_POOR)
# 2. Median-Mean consistency
median_mean_diff = abs(median_px - mean_px) / (median_px + 1e-8)
consistency_score = max(0, 1.0 - median_mean_diff / MEASUREMENT_CONSISTENCY_THRESHOLD)
# 3. Outlier ratio (measurements far from median)
outlier_threshold = MEASUREMENT_OUTLIER_STD_MULTIPLIER * std_px
outliers = np.sum(np.abs(widths_px - median_px) > outlier_threshold)
outlier_ratio = outliers / len(widths_px)
outlier_score = max(0, 1.0 - outlier_ratio)
# 4. Realistic range check
if MEASUREMENT_WIDTH_TYPICAL_MIN <= median_width_cm <= MEASUREMENT_WIDTH_TYPICAL_MAX:
range_score = MEASUREMENT_RANGE_SCORE_IDEAL
elif MEASUREMENT_WIDTH_ABSOLUTE_MIN <= median_width_cm <= MEASUREMENT_WIDTH_ABSOLUTE_MAX:
# Borderline acceptable
range_score = MEASUREMENT_RANGE_SCORE_BORDERLINE
else:
# Outside realistic range
range_score = MEASUREMENT_RANGE_SCORE_OUTSIDE
# Combine components with weights
measurement_conf = (
MEASUREMENT_WEIGHT_VARIANCE * variance_score +
MEASUREMENT_WEIGHT_CONSISTENCY * consistency_score +
MEASUREMENT_WEIGHT_OUTLIERS * outlier_score +
MEASUREMENT_WEIGHT_RANGE * range_score
)
return float(np.clip(measurement_conf, 0, 1))
def compute_edge_quality_confidence(
edge_quality_data: Optional[Dict[str, Any]] = None
) -> float:
"""
Compute confidence score from edge quality (v1 Sobel method).
Args:
edge_quality_data: Output from compute_edge_quality_score()
None if using contour method (v0)
Returns:
Edge quality confidence score [0, 1]
Returns 1.0 for contour method (not applicable)
"""
if edge_quality_data is None:
# Contour method - edge quality not applicable
return 1.0
# Use overall edge quality score directly
# It's already a weighted combination of 4 metrics
edge_conf = edge_quality_data.get("overall_score", 0.0)
return float(np.clip(edge_conf, 0, 1))
def compute_overall_confidence(
card_confidence: float,
finger_confidence: float,
measurement_confidence: float,
edge_method: EdgeMethod = "contour",
edge_quality_confidence: Optional[float] = None,
) -> Dict[str, Any]:
"""
Compute overall confidence by combining component scores.
Supports both v0 (contour) and v1 (Sobel) confidence calculation:
- v0 (contour): 3 components with V0_WEIGHT_* constants
- v1 (sobel): 4 components with V1_WEIGHT_* constants
Uses constants:
- V0_WEIGHT_*: v0 component weights (card: 30%, finger: 30%, measurement: 40%)
- V1_WEIGHT_*: v1 component weights (card: 25%, finger: 25%, edge: 20%, measurement: 30%)
- CONFIDENCE_LEVEL_*_THRESHOLD: Level thresholds (high: >0.85, medium: >=0.6)
Args:
card_confidence: Card detection confidence
finger_confidence: Finger detection confidence
measurement_confidence: Measurement stability confidence
edge_method: Edge detection method used
edge_quality_confidence: Edge quality confidence (v1 only)
Returns:
Dictionary containing:
- overall: Overall confidence [0, 1]
- card: Card component score
- finger: Finger component score
- measurement: Measurement component score
- edge_quality: Edge quality score (v1 only, None for v0)
- level: "high", "medium", or "low"
- method: Edge method used
"""
result = {
"card": float(card_confidence),
"finger": float(finger_confidence),
"measurement": float(measurement_confidence),
"method": edge_method,
}
# Calculate overall confidence based on method
if edge_method == "sobel" and edge_quality_confidence is not None:
# v1 scoring: 4 components with V1_WEIGHT_* constants
overall = (
V1_WEIGHT_CARD * card_confidence +
V1_WEIGHT_FINGER * finger_confidence +
V1_WEIGHT_EDGE_QUALITY * edge_quality_confidence +
V1_WEIGHT_MEASUREMENT * measurement_confidence
)
result["edge_quality"] = float(edge_quality_confidence)
else:
# v0 scoring: 3 components with V0_WEIGHT_* constants (contour method or sobel fallback)
overall = (
V0_WEIGHT_CARD * card_confidence +
V0_WEIGHT_FINGER * finger_confidence +
V0_WEIGHT_MEASUREMENT * measurement_confidence
)
result["edge_quality"] = None
overall = float(np.clip(overall, 0, 1))
# Classify confidence level using threshold constants
if overall > CONFIDENCE_LEVEL_HIGH_THRESHOLD:
level = "high"
elif overall >= CONFIDENCE_LEVEL_MEDIUM_THRESHOLD:
level = "medium"
else:
level = "low"
result["overall"] = overall
result["level"] = level
return result