""" 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