Spaces:
Running
Running
| # -*- coding: utf-8 -*- | |
| from transformers import AutoImageProcessor, AutoModelForImageClassification | |
| from PIL import Image | |
| import torch | |
| import io | |
| import base64 | |
| class EnsembleImageDetector: | |
| def __init__(self): | |
| """Load multiple models for better accuracy""" | |
| print("Loading ensemble image detectors...") | |
| self.models = [] | |
| model_names = [ | |
| "umm-maybe/AI-image-detector", | |
| "Organika/sdxl-detector" | |
| ] | |
| for model_name in model_names: | |
| try: | |
| print(f" Loading {model_name}...") | |
| processor = AutoImageProcessor.from_pretrained(model_name) | |
| model = AutoModelForImageClassification.from_pretrained(model_name) | |
| model.eval() | |
| self.models.append({ | |
| 'name': model_name, | |
| 'processor': processor, | |
| 'model': model | |
| }) | |
| print(f" ✓ {model_name} loaded") | |
| except Exception as e: | |
| print(f" ✗ Failed to load {model_name}: {e}") | |
| if len(self.models) == 0: | |
| raise Exception("Failed to load any models!") | |
| print(f"Loaded {len(self.models)} models for ensemble\n") | |
| def detect_from_base64(self, base64_string): | |
| """Detect using ensemble voting""" | |
| try: | |
| if ',' in base64_string: | |
| base64_string = base64_string.split(',')[1] | |
| image_data = base64.b64decode(base64_string) | |
| image = Image.open(io.BytesIO(image_data)).convert('RGB') | |
| return self.detect_from_image(image) | |
| except Exception as e: | |
| print(f"Error decoding image: {e}") | |
| raise | |
| def detect_from_image(self, image): | |
| """Ensemble detection with voting and metadata analysis""" | |
| width, height = image.size | |
| total_pixels = width * height | |
| megapixels = total_pixels / 1000000 | |
| print(f"Analyzing: {width}x{height} ({megapixels:.1f}MP)") | |
| # Get predictions from all models | |
| predictions = [] | |
| for model_info in self.models: | |
| try: | |
| inputs = model_info['processor'](images=image, return_tensors="pt") | |
| with torch.no_grad(): | |
| outputs = model_info['model'](**inputs) | |
| probs = torch.nn.functional.softmax(outputs.logits, dim=-1) | |
| if probs.shape[1] == 2: | |
| ai_prob = probs[0][1].item() | |
| else: | |
| ai_prob = probs[0][0].item() | |
| predictions.append(ai_prob) | |
| print(f" Model prediction: {ai_prob*100:.1f}% AI") | |
| except Exception as e: | |
| print(f" Model error: {e}") | |
| if not predictions: | |
| raise Exception("All models failed!") | |
| # Average predictions | |
| avg_ai_prob = sum(predictions) / len(predictions) | |
| # Metadata analysis | |
| has_exif = False | |
| exif_count = 0 | |
| try: | |
| exif = image.getexif() | |
| if exif: | |
| exif_count = len(exif) | |
| has_exif = exif_count > 8 | |
| except: | |
| pass | |
| # Check AI characteristics | |
| aspect_ratio = width / height | |
| is_square = 0.95 < aspect_ratio < 1.05 | |
| common_ai_sizes = [512, 768, 1024, 1536, 2048] | |
| is_ai_size = width in common_ai_sizes and height in common_ai_sizes | |
| # Strong indicators | |
| strong_real = sum([has_exif, megapixels > 8, not is_ai_size]) | |
| strong_ai = sum([exif_count == 0, is_square, is_ai_size]) | |
| # Apply calibration | |
| final_prob = avg_ai_prob | |
| if strong_real >= 2: | |
| final_prob = final_prob * 0.5 | |
| elif has_exif: | |
| final_prob = final_prob * 0.6 | |
| if strong_ai >= 2: | |
| final_prob = final_prob * 1.3 | |
| final_prob = final_prob * 0.9 | |
| final_prob = max(0.05, min(0.95, final_prob)) | |
| print(f"Final: {final_prob*100:.1f}% AI") | |
| # Generate explanations | |
| explanations = self._generate_explanations( | |
| has_exif, is_square, is_ai_size, megapixels, width, height, final_prob | |
| ) | |
| distance = abs(final_prob - 0.5) | |
| confidence = "High" if distance > 0.3 else "Medium" if distance > 0.2 else "Low" | |
| return { | |
| 'prediction': 'AI' if final_prob > 0.5 else 'Real', | |
| 'ai_probability': round(final_prob * 100, 2), | |
| 'real_probability': round((1 - final_prob) * 100, 2), | |
| 'confidence': confidence, | |
| 'explanations': explanations | |
| } | |
| def _generate_explanations(self, has_exif, is_square, is_ai_size, mp, w, h, prob): | |
| """Generate user-friendly explanations""" | |
| explanations = [] | |
| if has_exif: | |
| explanations.append({ | |
| 'indicator': 'Camera Metadata Detected', | |
| 'description': 'Image contains extensive EXIF data with camera settings, strongly suggesting authentic photograph.', | |
| 'type': 'Real' | |
| }) | |
| else: | |
| explanations.append({ | |
| 'indicator': 'No Camera Metadata', | |
| 'description': 'Missing EXIF data normally present in photos from cameras and smartphones.', | |
| 'type': 'AI' | |
| }) | |
| if is_ai_size: | |
| explanations.append({ | |
| 'indicator': 'AI-Standard Dimensions', | |
| 'description': f'Image size ({w}x{h}) matches common AI generation formats.', | |
| 'type': 'AI' | |
| }) | |
| else: | |
| explanations.append({ | |
| 'indicator': 'Unique Dimensions', | |
| 'description': f'Non-standard dimensions ({w}x{h}) typical of real camera sensors.', | |
| 'type': 'Real' | |
| }) | |
| if mp > 8: | |
| explanations.append({ | |
| 'indicator': 'High Camera Resolution', | |
| 'description': f'Very high resolution ({mp:.1f}MP) typical of modern cameras.', | |
| 'type': 'Real' | |
| }) | |
| elif mp < 2: | |
| explanations.append({ | |
| 'indicator': 'Low Resolution', | |
| 'description': f'Low resolution ({mp:.1f}MP) common in AI-generated images.', | |
| 'type': 'AI' | |
| }) | |
| if prob > 0.7: | |
| explanations.append({ | |
| 'indicator': 'Strong AI Patterns', | |
| 'description': 'Multiple models detected characteristic AI generation patterns.', | |
| 'type': 'AI' | |
| }) | |
| elif prob < 0.3: | |
| explanations.append({ | |
| 'indicator': 'Authentic Photography', | |
| 'description': 'Multiple models confirmed natural photographic characteristics.', | |
| 'type': 'Real' | |
| }) | |
| else: | |
| explanations.append({ | |
| 'indicator': 'Uncertain', | |
| 'description': 'Modern AI generation is extremely realistic. Consider other evidence.', | |
| 'type': 'Neutral' | |
| }) | |
| return explanations[:5] | |