|
""" |
|
Enhanced AI Image Detector - Hugging Face Version |
|
This model detects whether an image is real or AI-generated using a trained PyTorch model. |
|
""" |
|
|
|
import os |
|
import cv2 |
|
import numpy as np |
|
from PIL import Image |
|
import json |
|
from pytorch_model import PyTorchAIDetector |
|
|
|
class EnhancedAIDetector: |
|
""" |
|
Enhanced detector for AI-generated images using a trained PyTorch model. |
|
""" |
|
|
|
def __init__(self, model_path='best_model_improved.pth'): |
|
""" |
|
Initialize the enhanced detector with a trained PyTorch model. |
|
|
|
Args: |
|
model_path: Path to the trained PyTorch model |
|
""" |
|
|
|
self.model = PyTorchAIDetector(model_path) |
|
|
|
def analyze_image(self, image_path): |
|
""" |
|
Analyze an image to detect if it's AI-generated using the PyTorch model. |
|
|
|
Args: |
|
image_path: Path to the image. |
|
|
|
Returns: |
|
Dictionary with analysis results. |
|
""" |
|
|
|
try: |
|
|
|
result = self.model.analyze_image(image_path) |
|
|
|
|
|
result.update({ |
|
"model_name": "Enhanced AI Image Detector", |
|
"model_version": "1.0.0", |
|
"confidence": float(result["overall_score"] if result["is_ai_generated"] else 1.0 - result["overall_score"]) |
|
}) |
|
|
|
return result |
|
|
|
except Exception as e: |
|
raise ValueError(f"Failed to analyze image: {str(e)}") |
|
|
|
def _analyze_noise_patterns(self, cv_img): |
|
""" |
|
Analyze noise patterns in the image. |
|
""" |
|
|
|
gray = cv2.cvtColor(cv_img, cv2.COLOR_BGR2GRAY) |
|
|
|
|
|
|
|
temp_path = "temp_ela.jpg" |
|
cv2.imwrite(temp_path, cv_img, [cv2.IMWRITE_JPEG_QUALITY, 90]) |
|
|
|
|
|
compressed_img = cv2.imread(temp_path) |
|
os.remove(temp_path) |
|
|
|
|
|
if compressed_img is not None: |
|
diff = cv2.absdiff(cv_img, compressed_img) |
|
diff_gray = cv2.cvtColor(diff, cv2.COLOR_BGR2GRAY) |
|
|
|
|
|
mean_diff = np.mean(diff_gray) |
|
std_diff = np.std(diff_gray) |
|
|
|
|
|
|
|
ela_score = min(mean_diff / 8.0, 1.0) |
|
else: |
|
ela_score = 0.5 |
|
|
|
|
|
laplacian = cv2.Laplacian(gray, cv2.CV_64F) |
|
laplacian_std = np.std(laplacian) |
|
|
|
|
|
|
|
|
|
laplacian_score = 1.0 - min(laplacian_std / 15.0, 1.0) |
|
|
|
|
|
|
|
f_transform = np.fft.fft2(gray) |
|
f_shift = np.fft.fftshift(f_transform) |
|
magnitude_spectrum = 20 * np.log(np.abs(f_shift) + 1) |
|
|
|
|
|
h, w = gray.shape |
|
center_y, center_x = h // 2, w // 2 |
|
radius = min(center_y, center_x) // 4 |
|
|
|
|
|
y, x = np.ogrid[:h, :w] |
|
low_freq_mask = ((y - center_y) ** 2 + (x - center_x) ** 2) <= radius ** 2 |
|
|
|
|
|
low_freq_mean = np.mean(magnitude_spectrum[low_freq_mask]) |
|
high_freq_mean = np.mean(magnitude_spectrum[~low_freq_mask]) |
|
|
|
|
|
freq_ratio = high_freq_mean / low_freq_mean if low_freq_mean > 0 else 0 |
|
|
|
|
|
|
|
freq_score = 1.0 - min(freq_ratio / 0.4, 1.0) |
|
|
|
|
|
noise_score = 0.4 * ela_score + 0.35 * laplacian_score + 0.25 * freq_score |
|
|
|
return noise_score |
|
|
|
def _analyze_texture(self, cv_img): |
|
""" |
|
Analyze image texture. |
|
""" |
|
|
|
gray = cv2.cvtColor(cv_img, cv2.COLOR_BGR2GRAY) |
|
|
|
|
|
distances = [1] |
|
angles = [0, np.pi/4, np.pi/2, 3*np.pi/4] |
|
|
|
|
|
try: |
|
glcm = feature.graycomatrix(gray, distances, angles, 256, symmetric=True, normed=True) |
|
|
|
|
|
contrast = feature.graycoprops(glcm, 'contrast')[0, 0] |
|
dissimilarity = feature.graycoprops(glcm, 'dissimilarity')[0, 0] |
|
homogeneity = feature.graycoprops(glcm, 'homogeneity')[0, 0] |
|
energy = feature.graycoprops(glcm, 'energy')[0, 0] |
|
correlation = feature.graycoprops(glcm, 'correlation')[0, 0] |
|
|
|
|
|
contrast_norm = min(contrast / 80.0, 1.0) |
|
dissimilarity_norm = min(dissimilarity / 8.0, 1.0) |
|
homogeneity_norm = homogeneity |
|
energy_norm = energy |
|
correlation_norm = correlation |
|
|
|
|
|
texture_score = ( |
|
0.15 * (1.0 - contrast_norm) + |
|
0.15 * (1.0 - dissimilarity_norm) + |
|
0.25 * homogeneity_norm + |
|
0.25 * energy_norm + |
|
0.20 * correlation_norm |
|
) |
|
except Exception: |
|
|
|
|
|
variance = np.var(gray) |
|
variance_norm = min(variance / 800.0, 1.0) |
|
|
|
|
|
hist = cv2.calcHist([gray], [0], None, [256], [0, 256]) |
|
hist = hist / hist.sum() |
|
entropy = -np.sum(hist * np.log2(hist + 1e-10)) |
|
entropy_norm = min(entropy / 7.5, 1.0) |
|
|
|
|
|
texture_score = 0.6 * (1.0 - variance_norm) + 0.4 * (1.0 - entropy_norm) |
|
|
|
return texture_score |
|
|
|
def _analyze_color_coherence(self, cv_img): |
|
""" |
|
Analyze color coherence in the image. |
|
""" |
|
|
|
b, g, r = cv2.split(cv_img) |
|
|
|
|
|
std_b = np.std(b) |
|
std_g = np.std(g) |
|
std_r = np.std(r) |
|
|
|
|
|
avg_std = (std_b + std_g + std_r) / 3.0 |
|
|
|
|
|
|
|
std_score = 1.0 - min(avg_std / 45.0, 1.0) |
|
|
|
|
|
hist_b = cv2.calcHist([b], [0], None, [64], [0, 256]) |
|
hist_g = cv2.calcHist([g], [0], None, [64], [0, 256]) |
|
hist_r = cv2.calcHist([r], [0], None, [64], [0, 256]) |
|
|
|
|
|
hist_b = hist_b / hist_b.sum() |
|
hist_g = hist_g / hist_g.sum() |
|
hist_r = hist_r / hist_r.sum() |
|
|
|
|
|
entropy_b = -np.sum(hist_b * np.log2(hist_b + 1e-10)) |
|
entropy_g = -np.sum(hist_g * np.log2(hist_g + 1e-10)) |
|
entropy_r = -np.sum(hist_r * np.log2(hist_r + 1e-10)) |
|
|
|
|
|
avg_entropy = (entropy_b + entropy_g + entropy_r) / 3.0 |
|
|
|
|
|
|
|
entropy_score = 1.0 - min(avg_entropy / 5.8, 1.0) |
|
|
|
|
|
correlation_bg = np.corrcoef(b.flatten(), g.flatten())[0, 1] |
|
correlation_br = np.corrcoef(b.flatten(), r.flatten())[0, 1] |
|
correlation_gr = np.corrcoef(g.flatten(), r.flatten())[0, 1] |
|
|
|
|
|
avg_correlation = (abs(correlation_bg) + abs(correlation_br) + abs(correlation_gr)) / 3.0 |
|
|
|
|
|
|
|
correlation_score = min(avg_correlation, 1.0) |
|
|
|
|
|
color_score = 0.25 * std_score + 0.25 * entropy_score + 0.5 * correlation_score |
|
|
|
return color_score |
|
|
|
def _analyze_edges(self, cv_img): |
|
""" |
|
Analyze edges in the image. |
|
""" |
|
|
|
gray = cv2.cvtColor(cv_img, cv2.COLOR_BGR2GRAY) |
|
|
|
|
|
edges = cv2.Canny(gray, 100, 200) |
|
|
|
|
|
edge_ratio = np.count_nonzero(edges) / edges.size |
|
|
|
|
|
|
|
edge_ratio_score = 1.0 - min(edge_ratio / 0.08, 1.0) |
|
|
|
|
|
|
|
sobelx = cv2.Sobel(gray, cv2.CV_64F, 1, 0, ksize=3) |
|
sobely = cv2.Sobel(gray, cv2.CV_64F, 0, 1, ksize=3) |
|
|
|
|
|
gradient_direction = np.arctan2(sobely, sobelx) |
|
|
|
|
|
hist, _ = np.histogram(gradient_direction, bins=36, range=(-np.pi, np.pi)) |
|
hist = hist / hist.sum() |
|
|
|
|
|
entropy = -np.sum(hist * np.log2(hist + 1e-10)) |
|
|
|
|
|
|
|
entropy_score = 1.0 - min(entropy / 5.0, 1.0) |
|
|
|
|
|
|
|
gradient_magnitude = np.sqrt(sobelx**2 + sobely**2) |
|
|
|
|
|
gradient_std = np.std(gradient_magnitude) |
|
|
|
|
|
|
|
gradient_score = 1.0 - min(gradient_std / 30.0, 1.0) |
|
|
|
|
|
edge_score = 0.4 * edge_ratio_score + 0.3 * entropy_score + 0.3 * gradient_score |
|
|
|
return edge_score |
|
|
|
def _analyze_faces(self, cv_img): |
|
""" |
|
Analyze faces in the image. |
|
""" |
|
|
|
gray = cv2.cvtColor(cv_img, cv2.COLOR_BGR2GRAY) |
|
|
|
|
|
faces = self.face_cascade.detectMultiScale(gray, 1.3, 5) |
|
|
|
if len(faces) == 0: |
|
return None |
|
|
|
face_scores = [] |
|
|
|
for (x, y, w, h) in faces: |
|
|
|
face_roi = gray[y:y+h, x:x+w] |
|
|
|
|
|
eyes = self.eye_cascade.detectMultiScale(face_roi) |
|
|
|
|
|
symmetry_score = self._analyze_face_symmetry(face_roi) |
|
|
|
|
|
texture_score = self._analyze_face_texture(face_roi) |
|
|
|
|
|
proportion_score = self._analyze_face_proportions(face_roi, eyes) |
|
|
|
|
|
detail_score = self._analyze_face_details(face_roi) |
|
|
|
|
|
face_score = 0.3 * symmetry_score + 0.25 * texture_score + 0.25 * proportion_score + 0.2 * detail_score |
|
face_scores.append(face_score) |
|
|
|
|
|
avg_face_score = np.mean(face_scores) if face_scores else 0.5 |
|
|
|
return avg_face_score |
|
|
|
def _analyze_face_symmetry(self, face_roi): |
|
""" |
|
Analyze face symmetry. |
|
""" |
|
|
|
h, w = face_roi.shape |
|
midpoint = w // 2 |
|
|
|
left_half = face_roi[:, :midpoint] |
|
right_half = face_roi[:, midpoint:] |
|
|
|
|
|
right_half_flipped = cv2.flip(right_half, 1) |
|
|
|
|
|
if left_half.shape[1] != right_half_flipped.shape[1]: |
|
right_half_flipped = cv2.resize(right_half_flipped, (left_half.shape[1], left_half.shape[0])) |
|
|
|
|
|
diff = cv2.absdiff(left_half, right_half_flipped) |
|
|
|
|
|
mean_diff = np.mean(diff) |
|
|
|
|
|
|
|
|
|
symmetry_score = 1.0 - min(mean_diff / 25.0, 1.0) |
|
|
|
return symmetry_score |
|
|
|
def _analyze_face_texture(self, face_roi): |
|
""" |
|
Analyze face texture. |
|
""" |
|
|
|
laplacian = cv2.Laplacian(face_roi, cv2.CV_64F) |
|
laplacian_std = np.std(laplacian) |
|
|
|
|
|
|
|
|
|
texture_score = 1.0 - min(laplacian_std / 15.0, 1.0) |
|
|
|
return texture_score |
|
|
|
def _analyze_face_proportions(self, face_roi, eyes): |
|
""" |
|
Analyze face proportions. |
|
""" |
|
h, w = face_roi.shape |
|
|
|
if len(eyes) >= 2: |
|
|
|
eyes = sorted(eyes, key=lambda e: e[0]) |
|
|
|
|
|
eye1_center = (eyes[0][0] + eyes[0][2] // 2, eyes[0][1] + eyes[0][3] // 2) |
|
eye2_center = (eyes[1][0] + eyes[1][2] // 2, eyes[1][1] + eyes[1][3] // 2) |
|
|
|
eye_distance = np.sqrt((eye2_center[0] - eye1_center[0]) ** 2 + (eye2_center[1] - eye1_center[1]) ** 2) |
|
|
|
|
|
eye_ratio = eye_distance / w |
|
|
|
|
|
|
|
proportion_score = 1.0 - min(abs(eye_ratio - 0.3) / 0.2, 1.0) |
|
else: |
|
|
|
proportion_score = 0.5 |
|
|
|
return proportion_score |
|
|
|
def _analyze_face_details(self, face_roi): |
|
""" |
|
Analyze face details. |
|
""" |
|
|
|
|
|
ksize = 31 |
|
sigma = 4.0 |
|
theta = 0 |
|
lambd = 10.0 |
|
gamma = 0.5 |
|
|
|
|
|
angles = [0, np.pi/4, np.pi/2, 3*np.pi/4] |
|
responses = [] |
|
|
|
for theta in angles: |
|
kernel = cv2.getGaborKernel((ksize, ksize), sigma, theta, lambd, gamma, 0, ktype=cv2.CV_32F) |
|
filtered = cv2.filter2D(face_roi, cv2.CV_8UC3, kernel) |
|
responses.append(filtered) |
|
|
|
|
|
combined_response = np.zeros_like(face_roi) |
|
for response in responses: |
|
combined_response = cv2.add(combined_response, response) |
|
|
|
|
|
response_std = np.std(combined_response) |
|
|
|
|
|
|
|
|
|
detail_score = 1.0 - min(response_std / 25.0, 1.0) |
|
|
|
return detail_score |
|
|
|
def _analyze_additional_features(self, cv_img): |
|
""" |
|
Analyze additional features for borderline images. |
|
""" |
|
|
|
gray = cv2.cvtColor(cv_img, cv2.COLOR_BGR2GRAY) |
|
|
|
|
|
|
|
f_transform = np.fft.fft2(gray) |
|
f_shift = np.fft.fftshift(f_transform) |
|
magnitude_spectrum = np.log(np.abs(f_shift) + 1) |
|
|
|
|
|
spectrum_std = np.std(magnitude_spectrum) |
|
|
|
|
|
|
|
spectrum_score = 1.0 - min(spectrum_std / 2.0, 1.0) |
|
|
|
|
|
|
|
hsv = cv2.cvtColor(cv_img, cv2.COLOR_BGR2HSV) |
|
h, s, v = cv2.split(hsv) |
|
|
|
|
|
hist_s = cv2.calcHist([s], [0], None, [64], [0, 256]) |
|
hist_s = hist_s / hist_s.sum() |
|
|
|
|
|
entropy_s = -np.sum(hist_s * np.log2(hist_s + 1e-10)) |
|
|
|
|
|
|
|
entropy_score = 1.0 - min(entropy_s / 5.0, 1.0) |
|
|
|
|
|
additional_score = 0.6 * spectrum_score + 0.4 * entropy_score |
|
|
|
return additional_score |
|
|