Spaces:
Sleeping
Sleeping
File size: 15,160 Bytes
07fe054 |
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 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 |
"""
Additional Forensic Feature Extractors
Implements CFA (Color Filter Array) pattern consistency and defocus map analysis
for deepfake detection.
References:
- Kirchner & Gloe "Efficient Estimation of CFA Pattern Configuration" (SPIE 2010)
- "Unlocking Defocus Maps for Deepfake Detection" (arXiv:2509.23289)
"""
import numpy as np
import cv2
from scipy import ndimage
from scipy.stats import entropy
from typing import Dict, Tuple, Optional
class CFAExtractor:
"""
Extracts CFA (Color Filter Array) pattern consistency features.
Real camera images have consistent CFA demosaicing patterns (Bayer, X-Trans, etc.),
while synthetic images may lack these patterns or show inconsistencies.
"""
def __init__(self):
# Common Bayer patterns: RGGB, GRBG, GBRG, BGGR
# We'll check for RGGB pattern (most common)
self.bayer_patterns = {
'RGGB': ((0, 0), (0, 1), (1, 0), (1, 1)), # Red, Green, Green, Blue
'GRBG': ((0, 1), (0, 0), (1, 1), (1, 0)), # Green, Red, Blue, Green
'GBRG': ((1, 0), (1, 1), (0, 0), (0, 1)), # Green, Blue, Red, Green
'BGGR': ((1, 1), (1, 0), (0, 1), (0, 0)), # Blue, Green, Green, Red
}
def _estimate_bayer_pattern(self, image: np.ndarray) -> Tuple[str, float]:
"""
Estimate the most likely Bayer pattern by analyzing channel correlations.
Args:
image: RGB image array (H, W, 3)
Returns:
Tuple of (pattern_name, confidence_score)
"""
h, w = image.shape[:2]
r, g, b = image[:, :, 0], image[:, :, 1], image[:, :, 2]
# Compute correlations for different pattern offsets
best_pattern = 'RGGB'
best_score = 0.0
for pattern_name, offsets in self.bayer_patterns.items():
# Extract pixels at pattern positions
scores = []
# Check consistency at 2x2 block level
for i in range(0, h - 1, 2):
for j in range(0, w - 1, 2):
block_r = r[i:i+2, j:j+2]
block_g = g[i:i+2, j:j+2]
block_b = b[i:i+2, j:j+2]
# Compute variance within each channel at pattern positions
# Real CFA patterns show structured variance
r_var = np.var(block_r)
g_var = np.var(block_g)
b_var = np.var(block_b)
# Pattern consistency: channels should have different variance patterns
# This is a simplified heuristic
score = 1.0 / (1.0 + abs(r_var - g_var) + abs(g_var - b_var))
scores.append(score)
avg_score = np.mean(scores) if scores else 0.0
if avg_score > best_score:
best_score = avg_score
best_pattern = pattern_name
return best_pattern, float(best_score)
def _compute_demosaicing_consistency(self, image: np.ndarray) -> float:
"""
Compute spatial consistency of demosaicing patterns.
Real images have consistent demosaicing artifacts, while synthetic
images may lack these or show inconsistencies.
Args:
image: RGB image array (H, W, 3)
Returns:
Consistency score (higher = more consistent)
"""
h, w = image.shape[:2]
gray = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY).astype(np.float32)
# Compute gradients in both directions
grad_x = cv2.Sobel(gray, cv2.CV_32F, 1, 0, ksize=3)
grad_y = cv2.Sobel(gray, cv2.CV_32F, 0, 1, ksize=3)
# Check for periodic patterns (CFA creates periodic artifacts)
# Analyze gradient patterns at 2x2 block level
block_size = 2
consistency_scores = []
for i in range(0, h - block_size, block_size):
for j in range(0, w - block_size, block_size):
block_gx = grad_x[i:i+block_size, j:j+block_size]
block_gy = grad_y[i:i+block_size, j:j+block_size]
# Compute variance within block
# Consistent CFA patterns show structured variance
var_gx = np.var(block_gx)
var_gy = np.var(block_gy)
# Consistency: variance should be similar across similar blocks
consistency_scores.append(var_gx + var_gy)
if not consistency_scores:
return 0.0
# Compute coefficient of variation (lower = more consistent)
scores_array = np.array(consistency_scores)
mean_score = np.mean(scores_array)
std_score = np.std(scores_array)
if mean_score < 1e-6:
return 0.0
cv_score = std_score / (mean_score + 1e-6)
# Invert: lower CV = higher consistency
consistency = 1.0 / (1.0 + cv_score)
return float(consistency)
def extract_features(self, image: np.ndarray) -> Dict[str, float]:
"""
Extract CFA pattern consistency features.
Args:
image: RGB image array (H, W, 3) or PIL Image
Returns:
Dictionary of CFA features
"""
if not isinstance(image, np.ndarray):
from PIL import Image
image = np.array(image)
if len(image.shape) != 3 or image.shape[2] != 3:
# Convert to RGB if needed
if len(image.shape) == 2:
image = cv2.cvtColor(image, cv2.COLOR_GRAY2RGB)
else:
image = image[:, :, :3]
# Estimate Bayer pattern
pattern, pattern_confidence = self._estimate_bayer_pattern(image)
# Compute demosaicing consistency
consistency_score = self._compute_demosaicing_consistency(image)
# Detect anomalies: low consistency suggests synthetic
anomalies_detected = consistency_score < 0.3
features = {
'cfa_pattern_confidence': pattern_confidence,
'cfa_consistency_score': consistency_score,
'cfa_anomalies_detected': float(anomalies_detected),
'cfa_pattern': hash(pattern) % 1000 # Encode pattern as numeric feature
}
return features
class DefocusExtractor:
"""
Extracts defocus map features for depth-of-field consistency analysis.
Real images have consistent defocus patterns based on depth, while
synthetic images may show inconsistent or unnatural defocus.
Reference: "Unlocking Defocus Maps for Deepfake Detection" (arXiv:2509.23289)
"""
def __init__(self):
pass
def _estimate_defocus_map(self, image: np.ndarray) -> np.ndarray:
"""
Estimate defocus map from image using edge-based method.
Defocus blurs edges, so we can estimate defocus by analyzing
edge sharpness across the image.
Args:
image: Grayscale image array (H, W)
Returns:
Defocus map (H, W) where higher values indicate more defocus
"""
# Convert to grayscale if needed
if len(image.shape) == 3:
gray = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY).astype(np.float32)
else:
gray = image.astype(np.float32)
# Compute edge strength using Laplacian
laplacian = cv2.Laplacian(gray, cv2.CV_32F, ksize=3)
edge_strength = np.abs(laplacian)
# Defocus reduces edge strength, so invert
# Higher values in defocus map = more defocus
defocus_map = 255.0 - np.clip(edge_strength * 10, 0, 255)
# Smooth to reduce noise
defocus_map = cv2.GaussianBlur(defocus_map, (5, 5), 1.0)
return defocus_map
def _compute_consistency_score(self, defocus_map: np.ndarray) -> float:
"""
Compute spatial consistency of defocus map.
Real images have smooth, consistent defocus transitions,
while synthetic images may show abrupt or inconsistent changes.
Args:
defocus_map: Defocus map array (H, W)
Returns:
Consistency score (higher = more consistent)
"""
# Compute gradient of defocus map
grad_x = cv2.Sobel(defocus_map, cv2.CV_32F, 1, 0, ksize=3)
grad_y = cv2.Sobel(defocus_map, cv2.CV_32F, 0, 1, ksize=3)
gradient_magnitude = np.sqrt(grad_x**2 + grad_y**2)
# Real images have smooth defocus transitions (low gradient)
# Synthetic images may have abrupt changes (high gradient)
mean_gradient = np.mean(gradient_magnitude)
std_gradient = np.std(gradient_magnitude)
# Consistency: lower mean gradient and lower variance = more consistent
if mean_gradient < 1e-6:
return 1.0
# Normalize and invert: lower gradient = higher consistency
consistency = 1.0 / (1.0 + mean_gradient / 255.0 + std_gradient / 255.0)
return float(consistency)
def _detect_anomalies(self, defocus_map: np.ndarray,
consistency_score: float) -> Tuple[list, float]:
"""
Detect anomalous regions in defocus map.
Args:
defocus_map: Defocus map array (H, W)
consistency_score: Overall consistency score
Returns:
Tuple of (anomaly_regions as list of bboxes, anomaly_score)
"""
# Compute local variance: high variance indicates inconsistent defocus
kernel_size = 15
local_mean = cv2.blur(defocus_map, (kernel_size, kernel_size))
local_var = cv2.blur((defocus_map - local_mean)**2, (kernel_size, kernel_size))
# Threshold for anomalies: regions with high local variance
threshold = np.percentile(local_var, 95)
anomaly_mask = local_var > threshold
# Find connected components (anomalous regions)
num_labels, labels, stats, centroids = cv2.connectedComponentsWithStats(
anomaly_mask.astype(np.uint8), connectivity=8
)
# Extract bounding boxes for significant anomalies
anomaly_regions = []
min_area = defocus_map.size * 0.01 # At least 1% of image
for i in range(1, num_labels): # Skip background (label 0)
area = stats[i, cv2.CC_STAT_AREA]
if area > min_area:
x = int(stats[i, cv2.CC_STAT_LEFT])
y = int(stats[i, cv2.CC_STAT_TOP])
w = int(stats[i, cv2.CC_STAT_WIDTH])
h = int(stats[i, cv2.CC_STAT_HEIGHT])
anomaly_regions.append([x, y, w, h])
# Overall anomaly score: fraction of image that is anomalous
anomaly_fraction = np.sum(anomaly_mask) / defocus_map.size
anomaly_score = float(anomaly_fraction)
return anomaly_regions, anomaly_score
def extract_features(self, image: np.ndarray) -> Dict[str, float]:
"""
Extract defocus map features.
Args:
image: RGB image array (H, W, 3) or PIL Image
Returns:
Dictionary of defocus features
"""
if not isinstance(image, np.ndarray):
from PIL import Image
image = np.array(image)
# Convert to grayscale for defocus estimation
if len(image.shape) == 3:
gray = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY)
else:
gray = image
# Estimate defocus map
defocus_map = self._estimate_defocus_map(gray)
# Compute consistency score
consistency_score = self._compute_consistency_score(defocus_map)
# Detect anomalies
anomaly_regions, anomaly_score = self._detect_anomalies(
defocus_map, consistency_score
)
# Compute statistics on defocus map
defocus_mean = float(np.mean(defocus_map))
defocus_std = float(np.std(defocus_map))
defocus_entropy = float(entropy(defocus_map.flatten() + 1e-10))
features = {
'defocus_consistency_score': consistency_score,
'defocus_anomaly_score': anomaly_score,
'defocus_mean': defocus_mean,
'defocus_std': defocus_std,
'defocus_entropy': defocus_entropy,
'defocus_n_anomalies': float(len(anomaly_regions)),
'defocus_anomalies_detected': float(len(anomaly_regions) > 0)
}
return features
def extract_additional_features(image_path: str,
feature_types: list = None) -> Dict:
"""
Extract additional forensic features (CFA, defocus, etc.).
Args:
image_path: Path to image file
feature_types: List of feature types to extract (e.g., ['cfa', 'defocus'])
If None, extracts all available features
Returns:
Dictionary of extracted features
"""
from PIL import Image
# Load image
try:
image = Image.open(image_path).convert('RGB')
image_np = np.array(image)
except Exception as e:
return {
'status': 'error',
'error': f'Failed to load image: {str(e)}'
}
if feature_types is None:
feature_types = ['cfa', 'defocus']
results = {
'status': 'completed',
'image_path': image_path,
'features': {}
}
# Extract CFA features
if 'cfa' in feature_types:
try:
cfa_extractor = CFAExtractor()
cfa_features = cfa_extractor.extract_features(image_np)
results['features']['cfa'] = cfa_features
except Exception as e:
results['features']['cfa'] = {
'status': 'error',
'error': str(e)
}
# Extract defocus features
if 'defocus' in feature_types:
try:
defocus_extractor = DefocusExtractor()
defocus_features = defocus_extractor.extract_features(image_np)
results['features']['defocus'] = defocus_features
except Exception as e:
results['features']['defocus'] = {
'status': 'error',
'error': str(e)
}
return results
|