Spaces:
Sleeping
Sleeping
| import cv2 | |
| import numpy as np | |
| import os | |
| import uuid | |
| import matplotlib.pyplot as plt | |
| from scipy.signal import convolve2d | |
| from pathlib import Path | |
| def analyze_cfa_artifacts(image_path, save_dir=None, face_results=None, quality_multiplier=1.0): | |
| """ | |
| Detects the presence and consistency of Color Filter Array (CFA) | |
| demosaicing artifacts, which are present in all real digital camera photos | |
| but absent in pure GAN/Diffusion generations. | |
| """ | |
| try: | |
| # Load image | |
| img = cv2.imread(image_path) | |
| if img is None: | |
| return {"cfa_score": 0.5, "error": "Could not read image"} | |
| # Convert to RGB and Gray | |
| img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) | |
| gray = cv2.cvtColor(img_rgb, cv2.COLOR_RGB2GRAY) | |
| # CFA Demosaicing residual filter (captures Bayer interpolation artifacts) | |
| # This filter isolates high-frequency diagonal differences inherent to CFA | |
| cfa_filter = np.array([ | |
| [-0.25, 0.5, -0.25], | |
| [ 0.5, -1.0, 0.5], | |
| [-0.25, 0.5, -0.25] | |
| ]) | |
| # Apply filter to extract the CFA residual pattern | |
| cfa_residual = convolve2d(gray.astype(float), cfa_filter, mode='same', boundary='symm') | |
| # Calculate local variance (block processing) to find CFA strength | |
| block_size = 8 | |
| h, w = cfa_residual.shape | |
| h_blocks = h // block_size | |
| w_blocks = w // block_size | |
| variance_map = np.zeros((h_blocks, w_blocks)) | |
| for i in range(h_blocks): | |
| for j in range(w_blocks): | |
| block = cfa_residual[i*block_size:(i+1)*block_size, j*block_size:(j+1)*block_size] | |
| variance_map[i, j] = np.var(block) | |
| # Normalize variance map for visualization | |
| if np.max(variance_map) > 0: | |
| norm_variance = variance_map / np.max(variance_map) | |
| else: | |
| norm_variance = variance_map | |
| # Analyze Face vs Background if face is detected | |
| face_cfa_variance = 0.0 | |
| bg_cfa_variance = 0.0 | |
| if face_results and face_results.get("face_detected") and "box" in face_results: | |
| x, y, fw, fh = face_results["box"] | |
| # Map box to block coordinates | |
| bx1 = max(0, x // block_size) | |
| by1 = max(0, y // block_size) | |
| bx2 = min(w_blocks, (x + fw) // block_size) | |
| by2 = min(h_blocks, (y + fh) // block_size) | |
| face_region = variance_map[by1:by2, bx1:bx2] | |
| # Background is everything else (we create a mask) | |
| mask = np.ones_like(variance_map, dtype=bool) | |
| mask[by1:by2, bx1:bx2] = False | |
| bg_region = variance_map[mask] | |
| if face_region.size > 0: | |
| face_cfa_variance = np.mean(face_region) | |
| if bg_region.size > 0: | |
| bg_cfa_variance = np.mean(bg_region) | |
| # If the face has significantly less CFA noise, it's likely synthetic. | |
| # If the face has completely different CFA noise than bg, it's likely spliced. | |
| ratio = face_cfa_variance / (bg_cfa_variance + 1e-6) | |
| # Score calculation: | |
| # Normal ratio is around 0.8 - 1.2. | |
| if ratio < 0.5: | |
| cfa_score = 1.0 - (ratio / 0.5) # 0.0 ratio = 1.0 score | |
| elif ratio > 2.0: | |
| cfa_score = min(1.0, (ratio - 2.0) / 2.0) | |
| else: | |
| cfa_score = abs(1.0 - ratio) * 0.5 # Small penalty for normal variance | |
| # NEW: If the ENTIRE image lacks CFA noise, it's heavily compressed or fully AI-generated! | |
| global_variance = np.mean(variance_map) | |
| # If both regions have very low variance, it's heavily compressed video. | |
| # The ratio becomes mathematically unstable and meaningless. | |
| if face_cfa_variance < 15.0 and bg_cfa_variance < 15.0: | |
| # Bypass ratio penalty for compressed videos, just use global smoothness | |
| cfa_score = max(0.0, min(1.0, (5 - global_variance) / 5)) * 0.4 # Cap confidence | |
| else: | |
| if global_variance < 10.0: | |
| # Override the ratio score if the whole image is smooth | |
| cfa_score = max(cfa_score, min(1.0, (15 - global_variance) / 15)) | |
| else: | |
| # No face detected. Measure global CFA strength. | |
| global_variance = np.mean(variance_map) | |
| cfa_score = max(0.0, min(1.0, (5 - global_variance) / 5)) | |
| # Scale score by quality (low quality = lower confidence) | |
| cfa_score = cfa_score * quality_multiplier | |
| cfa_score = float(np.clip(cfa_score, 0.05, 0.95)) | |
| # --- Visualization Generation --- | |
| plt.style.use('dark_background') | |
| fig, ax = plt.subplots(figsize=(8, 6)) | |
| # Display the normalized variance map as a heatmap | |
| # Resize to original dimensions for overlay or display | |
| heatmap_resized = cv2.resize(norm_variance, (w, h), interpolation=cv2.INTER_NEAREST) | |
| # Create a viridis colormap representation | |
| im = ax.imshow(heatmap_resized, cmap='inferno') | |
| # Draw face bounding box if available | |
| if face_results and face_results.get("face_detected") and "box" in face_results: | |
| x, y, fw, fh = face_results["box"] | |
| import matplotlib.patches as patches | |
| rect = patches.Rectangle((x, y), fw, fh, linewidth=2, edgecolor='cyan', facecolor='none', linestyle='dashed') | |
| ax.add_patch(rect) | |
| ax.axis('off') | |
| plt.tight_layout(pad=0) | |
| # Save visualization | |
| filename = f"cfa_{uuid.uuid4().hex[:8]}.png" | |
| if save_dir is None: | |
| save_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), "static", "results") | |
| os.makedirs(save_dir, exist_ok=True) | |
| save_path = os.path.join(save_dir, filename) | |
| plt.savefig(save_path, bbox_inches='tight', pad_inches=0, dpi=100, facecolor='black') | |
| plt.close(fig) | |
| # Calculate web relative path | |
| if "uploads" in str(save_path): | |
| web_path = "uploads/" + Path(save_path).parts[-2] + "/" + filename | |
| else: | |
| web_path = f"static/results/{filename}" | |
| face_var = float(face_cfa_variance) if 'face_cfa_variance' in locals() else 0.0 | |
| bg_var = float(bg_cfa_variance) if 'bg_cfa_variance' in locals() else 0.0 | |
| score = float(np.clip(cfa_score, 0.05, 0.95)) | |
| return { | |
| "cfa_score": score, | |
| "face_variance": face_var, | |
| "bg_variance": bg_var, | |
| "cfa_map_path": web_path, | |
| "explanation": { | |
| "what_happened": "Extracted the microscopic Color Filter Array (Bayer) grid pattern created by physical camera sensors.", | |
| "result": "Grid Disrupted (Deepfake)" if score > 0.5 else "Authentic Sensor Grid", | |
| "why_it_happened": "The face region's microscopic pixel grid was completely destroyed or out-of-sync compared to the background, which happens when AI generates new pixels." if score > 0.5 else "The physical camera pixel grid is perfectly consistent across the entire image.", | |
| "variables": { | |
| "Face Grid Variance": f"{face_var:.4f}", | |
| "Background Grid Variance": f"{bg_var:.4f}", | |
| "Mismatch Ratio": f"{(bg_var / max(0.0001, face_var)):.2f}x" | |
| } | |
| } | |
| } | |
| except Exception as e: | |
| print(f"Error in CFA analysis: {e}") | |
| return {"cfa_score": 0.5, "error": str(e)} | |
| if __name__ == "__main__": | |
| import sys | |
| if len(sys.argv) > 1: | |
| res = analyze_cfa_artifacts(sys.argv[1]) | |
| print(res) | |