Spaces:
Runtime error
Runtime error
File size: 10,224 Bytes
0ba6002 380a8c4 0ba6002 380a8c4 0ba6002 380a8c4 0ba6002 380a8c4 0ba6002 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 | """
Feature-Based Pokemon Card Validator
Validates that extracted features match Pokemon card characteristics.
This acts as a safety net to catch non-Pokemon cards that pass geometric
and back pattern validation.
"""
import numpy as np
from typing import Dict, Tuple, Any, List, Union
import logging
logger = logging.getLogger(__name__)
class FeatureBasedValidator:
"""
Validates Pokemon cards using extracted feature ranges.
Uses knowledge of Pokemon card characteristics to detect non-Pokemon cards:
- Aspect ratio should be close to 0.716 (standard Pokemon card)
- Saturation should be moderate (colorful fronts, blue backs)
- Texture and print quality should match Pokemon cards
"""
# Expected feature ranges for Pokemon cards (based on training data)
# Tightened ranges to better distinguish Pokemon from other trading cards
POKEMON_FEATURE_RANGES = {
# Geometric features - Pokemon cards have very consistent aspect ratio
'aspect_ratio_accuracy': (0.90, 1.00), # Very close to 0.716 target (tightened from 0.85)
# Color features (Pokemon cards have moderate-high saturation and blue backs)
'mean_saturation': (35.0, 160.0), # HSV saturation (tightened lower bound)
'dominant_color_blue': (0.10, 0.65), # Pokemon backs have blue dominance (tightened)
'mean_hue': (100.0, 130.0), # Pokemon backs have blue hue (~120 degrees)
# Texture features (Pokemon cards have fine, complex print quality)
'glcm_contrast': (15.0, 220.0), # GLCM texture contrast (tightened)
'glcm_energy': (0.001, 0.40), # Energy/uniformity (tightened upper bound)
'glcm_homogeneity': (0.30, 0.90), # Texture homogeneity
# Print quality features (Pokemon cards have distinctive print patterns)
'mean_fft_magnitude': (8.0, 55.0), # Frequency domain patterns (tightened)
'blur_score': (100.0, 4000.0), # Laplacian variance (tightened lower bound)
}
def __init__(self, confidence_threshold: float = 0.75, feature_extractor=None):
"""
Initialize the feature-based validator.
Args:
confidence_threshold: Minimum Pokemon likelihood score (0.0-1.0)
feature_extractor: Optional FeatureExtractor instance for converting lists to dicts
"""
self.confidence_threshold = confidence_threshold
self.feature_extractor = feature_extractor
def _features_to_dict(self, features: Union[List[float], np.ndarray], feature_names: List[str]) -> Dict[str, float]:
"""
Convert feature list to dictionary using feature names.
Args:
features: List or array of feature values
feature_names: List of feature names
Returns:
Dictionary mapping feature names to values
"""
if len(features) != len(feature_names):
logger.warning(f"Feature count mismatch: {len(features)} features vs {len(feature_names)} names")
return {name: float(value) for name, value in zip(feature_names, features)}
def validate_features(self, features: Union[Dict[str, Any], List[float], np.ndarray],
feature_names: List[str] = None) -> Tuple[bool, float, str]:
"""
Validate that features match Pokemon card characteristics.
Args:
features: Dictionary of extracted features, or list/array of feature values
feature_names: Optional list of feature names (required if features is a list/array)
Returns:
Tuple of (is_pokemon_like, confidence, reason):
- is_pokemon_like: True if features match Pokemon patterns
- confidence: Pokemon likelihood score (0.0-1.0)
- reason: Human-readable explanation
"""
try:
# Convert list/array to dict if needed
if isinstance(features, (list, np.ndarray)):
if feature_names is None:
if self.feature_extractor is not None:
feature_names = self.feature_extractor.get_feature_names()
else:
# Cannot validate without feature names
return True, 1.0, "Feature validation skipped (no feature names available)"
features = self._features_to_dict(features, feature_names)
# Calculate how many features match Pokemon ranges
matches = []
checks = []
for feature_name, (min_val, max_val) in self.POKEMON_FEATURE_RANGES.items():
if feature_name in features:
value = features[feature_name]
is_match = min_val <= value <= max_val
matches.append(is_match)
checks.append({
'feature': feature_name,
'value': value,
'range': (min_val, max_val),
'match': is_match
})
# Calculate Pokemon likelihood score
if len(matches) > 0:
confidence = sum(matches) / len(matches)
else:
# No features available, pass through
return True, 1.0, "Feature validation skipped (no features extracted)"
# Check threshold
is_pokemon_like = confidence >= self.confidence_threshold
# Build detailed reason
if is_pokemon_like:
reason = (
f"Features match Pokemon card patterns "
f"({sum(matches)}/{len(matches)} checks passed, "
f"score: {confidence:.2%})"
)
else:
# List failing checks
failing_checks = [c for c in checks if not c['match']]
reason = (
f"Features do not match Pokemon card patterns "
f"({sum(matches)}/{len(matches)} checks passed, "
f"score: {confidence:.2%}, threshold: {self.confidence_threshold:.2%}). "
f"Failing checks: " +
", ".join([
f"{c['feature']}={c['value']:.2f} (expected {c['range'][0]:.2f}-{c['range'][1]:.2f})"
for c in failing_checks[:3] # Show first 3 failures
])
)
return is_pokemon_like, confidence, reason
except Exception as e:
logger.error(f"Error during feature-based validation: {e}")
# On error, pass through to avoid blocking legitimate cards
return True, 0.0, f"Feature validation error: {str(e)}"
def get_feature_summary(self, features: Dict[str, Any]) -> Dict[str, Any]:
"""
Get a summary of feature validation results.
Args:
features: Dictionary of extracted features
Returns:
Dictionary with detailed validation results for each feature
"""
summary = {
'total_checks': 0,
'passed_checks': 0,
'failed_checks': 0,
'details': []
}
for feature_name, (min_val, max_val) in self.POKEMON_FEATURE_RANGES.items():
if feature_name in features:
value = features[feature_name]
is_match = min_val <= value <= max_val
summary['total_checks'] += 1
if is_match:
summary['passed_checks'] += 1
else:
summary['failed_checks'] += 1
summary['details'].append({
'feature': feature_name,
'value': float(value),
'expected_range': (float(min_val), float(max_val)),
'passed': bool(is_match)
})
if summary['total_checks'] > 0:
summary['confidence'] = summary['passed_checks'] / summary['total_checks']
else:
summary['confidence'] = 1.0
return summary
def validate_pokemon_back_colors(self, back_image, blue_threshold: float = 0.08) -> Tuple[bool, float, str]:
"""
Validate that back image has Pokemon-specific color characteristics.
Pokemon card backs have distinctive blue color (HSV hue ~180-220 degrees).
This is a quick heuristic check that doesn't require TensorFlow.
Args:
back_image: Back card image (BGR numpy array)
blue_threshold: Minimum fraction of blue pixels required (default 0.08).
Use a higher value (e.g. 0.25) when checking whether a *front* image
is erroneously a back, to avoid false positives on blue-heavy fronts.
Returns:
Tuple of (is_pokemon_back, confidence, reason)
"""
import cv2
import numpy as np
try:
# Convert to HSV
hsv = cv2.cvtColor(back_image, cv2.COLOR_BGR2HSV)
# Calculate blue pixel percentage
# Pokemon backs have hue in range 100-130 (blue)
hue = hsv[:, :, 0]
saturation = hsv[:, :, 1]
# Blue pixels: hue 100-125 (cyan-blue, NOT purple which is 130-160)
# Pokemon card backs are distinctively blue, not purple/violet
blue_mask = ((hue >= 100) & (hue <= 125) & (saturation >= 30))
blue_percentage = np.sum(blue_mask) / blue_mask.size
is_pokemon_back = blue_percentage >= blue_threshold
confidence = float(min(blue_percentage / blue_threshold, 1.0))
if is_pokemon_back:
reason = f"Back image has Pokemon blue color pattern ({blue_percentage:.1%} blue pixels)"
else:
reason = (
f"Back image lacks Pokemon blue color pattern "
f"({blue_percentage:.1%} blue pixels, expected ≥{blue_threshold:.0%})"
)
return is_pokemon_back, confidence, reason
except Exception as e:
logger.error(f"Error during back color validation: {e}")
return True, 0.0, f"Back color validation error: {str(e)}"
|