mirror of
https://github.com/aloshdenny/reverse-SynthID.git
synced 2026-04-30 10:37:49 +02:00
codebook analysis
This commit is contained in:
@@ -0,0 +1,645 @@
|
||||
"""
|
||||
Deep SynthID Watermark Pattern Analysis
|
||||
|
||||
Based on initial findings:
|
||||
1. High noise correlation (0.2156) suggests shared noise pattern
|
||||
2. Noise structure ratio ~1.32 is consistent with previous research
|
||||
3. Low-frequency anomalies (0-21) in Fourier domain
|
||||
4. Bits 5-7 have perfect consistency (always same value)
|
||||
|
||||
This script performs deeper analysis to extract the actual watermark signal.
|
||||
"""
|
||||
|
||||
import os
|
||||
import numpy as np
|
||||
import cv2
|
||||
from scipy.fft import fft2, fftshift, ifft2
|
||||
from scipy.stats import pearsonr
|
||||
from scipy.ndimage import gaussian_filter
|
||||
import pywt
|
||||
import matplotlib.pyplot as plt
|
||||
from collections import defaultdict
|
||||
import json
|
||||
|
||||
|
||||
def wavelet_denoise(channel, wavelet='db4', level=3):
|
||||
"""Manual wavelet denoising."""
|
||||
coeffs = pywt.wavedec2(channel, wavelet, level=level)
|
||||
detail = coeffs[-1][0]
|
||||
sigma = np.median(np.abs(detail)) / 0.6745
|
||||
threshold = sigma * np.sqrt(2 * np.log(channel.size))
|
||||
|
||||
new_coeffs = [coeffs[0]]
|
||||
for details in coeffs[1:]:
|
||||
new_details = tuple(pywt.threshold(d, threshold, mode='soft') for d in details)
|
||||
new_coeffs.append(new_details)
|
||||
|
||||
denoised = pywt.waverec2(new_coeffs, wavelet)
|
||||
return denoised[:channel.shape[0], :channel.shape[1]]
|
||||
|
||||
|
||||
def load_images(image_dir, max_images=250, size=(512, 512)):
|
||||
"""Load all images."""
|
||||
extensions = {'.png', '.jpg', '.jpeg', '.webp'}
|
||||
images = []
|
||||
paths = []
|
||||
|
||||
for fname in sorted(os.listdir(image_dir)):
|
||||
if os.path.splitext(fname)[1].lower() in extensions:
|
||||
path = os.path.join(image_dir, fname)
|
||||
img = cv2.imread(path)
|
||||
if img is not None:
|
||||
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
|
||||
img = cv2.resize(img, size)
|
||||
images.append(img)
|
||||
paths.append(fname)
|
||||
if len(images) >= max_images:
|
||||
break
|
||||
|
||||
return np.array(images), paths
|
||||
|
||||
|
||||
def analyze_noise_patterns(images):
|
||||
"""Analyze noise patterns across all images."""
|
||||
print("Analyzing noise patterns...")
|
||||
|
||||
n = len(images)
|
||||
H, W, C = images[0].shape
|
||||
|
||||
# Extract noise from each image
|
||||
noise_patterns = []
|
||||
for img in images:
|
||||
img_f = img.astype(np.float32) / 255.0
|
||||
noise = np.zeros_like(img_f)
|
||||
for c in range(3):
|
||||
denoised = wavelet_denoise(img_f[:, :, c])
|
||||
noise[:, :, c] = img_f[:, :, c] - denoised
|
||||
noise_patterns.append(noise)
|
||||
|
||||
noise_stack = np.array(noise_patterns)
|
||||
|
||||
# Average noise pattern (common watermark signal)
|
||||
avg_noise = np.mean(noise_stack, axis=0)
|
||||
|
||||
# Variance at each pixel (low variance = consistent signal)
|
||||
var_noise = np.var(noise_stack, axis=0)
|
||||
|
||||
# Standard deviation of average (higher = stronger consistent signal)
|
||||
avg_std = np.std(avg_noise)
|
||||
|
||||
# Compute correlation matrix between images
|
||||
print(" Computing pairwise noise correlations...")
|
||||
sample_size = min(50, n)
|
||||
indices = np.random.choice(n, sample_size, replace=False)
|
||||
correlations = []
|
||||
|
||||
for i in range(sample_size):
|
||||
for j in range(i+1, sample_size):
|
||||
n1 = noise_stack[indices[i]].ravel()
|
||||
n2 = noise_stack[indices[j]].ravel()
|
||||
corr, _ = pearsonr(n1, n2)
|
||||
correlations.append(corr)
|
||||
|
||||
return {
|
||||
'avg_noise': avg_noise,
|
||||
'var_noise': var_noise,
|
||||
'avg_std': float(avg_std),
|
||||
'mean_correlation': float(np.mean(correlations)),
|
||||
'max_correlation': float(np.max(correlations)),
|
||||
'min_correlation': float(np.min(correlations)),
|
||||
'correlations': correlations
|
||||
}
|
||||
|
||||
|
||||
def analyze_frequency_patterns(images):
|
||||
"""Analyze frequency domain for carrier signals."""
|
||||
print("Analyzing frequency patterns...")
|
||||
|
||||
n = len(images)
|
||||
H, W = images[0].shape[:2]
|
||||
|
||||
# Accumulate Fourier transforms
|
||||
magnitude_sum = None
|
||||
phase_sum = None
|
||||
|
||||
for img in images:
|
||||
gray = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY).astype(np.float32)
|
||||
f = fft2(gray)
|
||||
fshift = fftshift(f)
|
||||
mag = np.abs(fshift)
|
||||
phase = np.angle(fshift)
|
||||
|
||||
if magnitude_sum is None:
|
||||
magnitude_sum = mag
|
||||
phase_sum = np.exp(1j * phase)
|
||||
else:
|
||||
magnitude_sum += mag
|
||||
phase_sum += np.exp(1j * phase)
|
||||
|
||||
avg_magnitude = magnitude_sum / n
|
||||
phase_coherence = np.abs(phase_sum) / n
|
||||
|
||||
# Find high-coherence frequencies (potential carriers)
|
||||
log_mag = np.log1p(avg_magnitude)
|
||||
|
||||
# Combine magnitude and phase coherence for carrier detection
|
||||
combined_score = log_mag * phase_coherence
|
||||
|
||||
# Find top frequencies
|
||||
threshold = np.percentile(combined_score, 99.9)
|
||||
carrier_mask = combined_score > threshold
|
||||
carrier_locations = np.where(carrier_mask)
|
||||
|
||||
# Center coordinates
|
||||
cy, cx = H // 2, W // 2
|
||||
|
||||
# Convert to frequency coordinates
|
||||
carriers = []
|
||||
for y, x in zip(carrier_locations[0], carrier_locations[1]):
|
||||
freq_y = y - cy
|
||||
freq_x = x - cx
|
||||
mag = avg_magnitude[y, x]
|
||||
coh = phase_coherence[y, x]
|
||||
carriers.append({
|
||||
'position': (int(y), int(x)),
|
||||
'frequency': (int(freq_y), int(freq_x)),
|
||||
'magnitude': float(mag),
|
||||
'coherence': float(coh),
|
||||
'combined_score': float(combined_score[y, x])
|
||||
})
|
||||
|
||||
# Sort by combined score
|
||||
carriers.sort(key=lambda x: x['combined_score'], reverse=True)
|
||||
|
||||
# Analyze vertical center line (known pattern from previous research)
|
||||
vertical_profile = log_mag[:, cx]
|
||||
vertical_coherence = phase_coherence[:, cx]
|
||||
|
||||
return {
|
||||
'avg_magnitude': avg_magnitude,
|
||||
'phase_coherence': phase_coherence,
|
||||
'combined_score': combined_score,
|
||||
'top_carriers': carriers[:50],
|
||||
'vertical_profile': vertical_profile.tolist(),
|
||||
'vertical_coherence': vertical_coherence.tolist(),
|
||||
'overall_phase_coherence': float(np.mean(phase_coherence))
|
||||
}
|
||||
|
||||
|
||||
def analyze_bit_patterns(images):
|
||||
"""Analyze bit-level patterns for watermark embedding."""
|
||||
print("Analyzing bit patterns...")
|
||||
|
||||
n = len(images)
|
||||
H, W = images[0].shape[:2]
|
||||
|
||||
bit_stats = {}
|
||||
|
||||
for bit in range(8):
|
||||
# Extract bit plane from all images
|
||||
bit_planes = []
|
||||
for img in images:
|
||||
gray = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY)
|
||||
plane = ((gray >> bit) & 1).astype(np.float32)
|
||||
bit_planes.append(plane)
|
||||
|
||||
bit_stack = np.array(bit_planes)
|
||||
|
||||
# Average value at each pixel (0.5 = random, close to 0 or 1 = consistent)
|
||||
avg_plane = np.mean(bit_stack, axis=0)
|
||||
|
||||
# Consistency map: how often is this bit the same across images
|
||||
consistency = np.abs(avg_plane - 0.5) * 2 # 0 = random, 1 = always same
|
||||
|
||||
# Entropy of average (low = potential watermark pattern)
|
||||
hist, _ = np.histogram(avg_plane.ravel(), bins=50)
|
||||
hist = hist / hist.sum()
|
||||
entropy = -np.sum(hist * np.log2(hist + 1e-10))
|
||||
|
||||
bit_stats[bit] = {
|
||||
'avg_plane': avg_plane,
|
||||
'consistency': consistency,
|
||||
'mean_consistency': float(np.mean(consistency)),
|
||||
'high_consistency_ratio': float(np.mean(consistency > 0.8)),
|
||||
'entropy': float(entropy),
|
||||
'mean_value': float(np.mean(avg_plane))
|
||||
}
|
||||
|
||||
return bit_stats
|
||||
|
||||
|
||||
def analyze_lsb_spatial_pattern(images):
|
||||
"""Analyze LSB spatial patterns for watermark location."""
|
||||
print("Analyzing LSB spatial patterns...")
|
||||
|
||||
n = len(images)
|
||||
H, W = images[0].shape[:2]
|
||||
|
||||
# Per-channel LSB analysis
|
||||
channel_results = {}
|
||||
|
||||
for c, name in enumerate(['R', 'G', 'B']):
|
||||
lsb_sum = np.zeros((H, W), dtype=np.float64)
|
||||
|
||||
for img in images:
|
||||
lsb = img[:, :, c] & 1
|
||||
lsb_sum += lsb
|
||||
|
||||
avg_lsb = lsb_sum / n
|
||||
consistency = np.abs(avg_lsb - 0.5) * 2
|
||||
|
||||
# Find consistent regions (potential watermark locations)
|
||||
consistent_mask = consistency > 0.3
|
||||
|
||||
# Analyze spatial structure of consistent regions
|
||||
# Are they in specific locations?
|
||||
|
||||
# Block-wise consistency analysis
|
||||
block_size = 32
|
||||
block_consistency = np.zeros((H // block_size, W // block_size))
|
||||
|
||||
for i in range(0, H - block_size + 1, block_size):
|
||||
for j in range(0, W - block_size + 1, block_size):
|
||||
block = consistency[i:i+block_size, j:j+block_size]
|
||||
block_consistency[i // block_size, j // block_size] = np.mean(block)
|
||||
|
||||
channel_results[name] = {
|
||||
'avg_lsb': avg_lsb,
|
||||
'consistency': consistency,
|
||||
'mean_consistency': float(np.mean(consistency)),
|
||||
'block_consistency': block_consistency,
|
||||
'consistent_ratio': float(np.mean(consistent_mask))
|
||||
}
|
||||
|
||||
return channel_results
|
||||
|
||||
|
||||
def analyze_dct_embedding(images):
|
||||
"""Analyze DCT coefficients for quantization-based watermarking."""
|
||||
print("Analyzing DCT patterns...")
|
||||
|
||||
n = len(images)
|
||||
block_size = 8
|
||||
|
||||
# Accumulate DCT statistics
|
||||
dct_sum = np.zeros((block_size, block_size), dtype=np.float64)
|
||||
dct_sq_sum = np.zeros((block_size, block_size), dtype=np.float64)
|
||||
dct_counts = 0
|
||||
|
||||
# Per-coefficient value distributions
|
||||
coeff_distributions = defaultdict(list)
|
||||
|
||||
for img in images:
|
||||
gray = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY).astype(np.float32)
|
||||
H, W = gray.shape
|
||||
|
||||
for i in range(0, H - block_size + 1, block_size):
|
||||
for j in range(0, W - block_size + 1, block_size):
|
||||
block = gray[i:i+block_size, j:j+block_size]
|
||||
dct_block = cv2.dct(block)
|
||||
|
||||
dct_sum += dct_block
|
||||
dct_sq_sum += dct_block ** 2
|
||||
dct_counts += 1
|
||||
|
||||
# Sample some blocks for distribution analysis
|
||||
if np.random.random() < 0.001: # Sample 0.1% of blocks
|
||||
for bi in range(block_size):
|
||||
for bj in range(block_size):
|
||||
if bi != 0 or bj != 0: # Skip DC
|
||||
coeff_distributions[(bi, bj)].append(dct_block[bi, bj])
|
||||
|
||||
# Compute statistics
|
||||
dct_mean = dct_sum / dct_counts
|
||||
dct_var = dct_sq_sum / dct_counts - dct_mean ** 2
|
||||
dct_std = np.sqrt(np.abs(dct_var))
|
||||
|
||||
# Coefficient of variation (low = more consistent)
|
||||
cv = dct_std / (np.abs(dct_mean) + 1e-10)
|
||||
|
||||
# Check for quantization patterns (watermarks often modify LSB of DCT coefficients)
|
||||
quantization_analysis = {}
|
||||
for pos, values in coeff_distributions.items():
|
||||
if len(values) > 100:
|
||||
values = np.array(values)
|
||||
# Check if values cluster at certain intervals
|
||||
hist, bins = np.histogram(values, bins=100)
|
||||
quantization_analysis[str(pos)] = {
|
||||
'mean': float(np.mean(values)),
|
||||
'std': float(np.std(values)),
|
||||
'skew': float(np.mean((values - np.mean(values))**3) / (np.std(values)**3 + 1e-10)),
|
||||
}
|
||||
|
||||
return {
|
||||
'dct_mean': dct_mean,
|
||||
'dct_std': dct_std,
|
||||
'cv': cv,
|
||||
'quantization_analysis': quantization_analysis
|
||||
}
|
||||
|
||||
|
||||
def extract_watermark_signal(images, noise_results):
|
||||
"""Extract the average watermark signal from all images."""
|
||||
print("Extracting watermark signal...")
|
||||
|
||||
avg_noise = noise_results['avg_noise']
|
||||
|
||||
# The average noise pattern IS the watermark signal
|
||||
# Amplify it for visualization
|
||||
signal = avg_noise.copy()
|
||||
|
||||
# Normalize each channel
|
||||
for c in range(3):
|
||||
channel = signal[:, :, c]
|
||||
channel = (channel - channel.min()) / (channel.max() - channel.min() + 1e-10)
|
||||
signal[:, :, c] = channel
|
||||
|
||||
# Convert to grayscale signal
|
||||
gray_signal = np.mean(signal, axis=2)
|
||||
|
||||
# FFT of the signal to find carrier frequencies
|
||||
f = fft2(gray_signal)
|
||||
fshift = fftshift(f)
|
||||
magnitude = np.abs(fshift)
|
||||
phase = np.angle(fshift)
|
||||
|
||||
# Find peaks in spectrum
|
||||
log_mag = np.log1p(magnitude)
|
||||
threshold = np.percentile(log_mag, 99.5)
|
||||
peaks = np.where(log_mag > threshold)
|
||||
|
||||
H, W = gray_signal.shape
|
||||
cy, cx = H // 2, W // 2
|
||||
|
||||
peak_info = []
|
||||
for y, x in zip(peaks[0], peaks[1]):
|
||||
if abs(y - cy) > 5 or abs(x - cx) > 5: # Skip DC and near-DC
|
||||
peak_info.append({
|
||||
'position': (int(y), int(x)),
|
||||
'frequency': (int(y - cy), int(x - cx)),
|
||||
'magnitude': float(log_mag[y, x]),
|
||||
'phase': float(phase[y, x])
|
||||
})
|
||||
|
||||
peak_info.sort(key=lambda x: x['magnitude'], reverse=True)
|
||||
|
||||
return {
|
||||
'signal_rgb': signal,
|
||||
'signal_gray': gray_signal,
|
||||
'spectrum_magnitude': magnitude,
|
||||
'spectrum_phase': phase,
|
||||
'peaks': peak_info[:30]
|
||||
}
|
||||
|
||||
|
||||
def save_visualizations(results, output_dir):
|
||||
"""Save visualization of findings."""
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
|
||||
# 1. Noise pattern
|
||||
if 'noise' in results:
|
||||
avg_noise = results['noise']['avg_noise']
|
||||
|
||||
# Amplify for visibility
|
||||
noise_vis = avg_noise * 50 + 0.5
|
||||
noise_vis = np.clip(noise_vis, 0, 1)
|
||||
|
||||
fig, axes = plt.subplots(1, 3, figsize=(15, 5))
|
||||
for c, name in enumerate(['R', 'G', 'B']):
|
||||
axes[c].imshow(noise_vis[:, :, c], cmap='RdBu', vmin=0, vmax=1)
|
||||
axes[c].set_title(f'Average Noise - {name}')
|
||||
axes[c].axis('off')
|
||||
plt.suptitle(f'Average Noise Pattern (Watermark Signal)\nCorrelation: {results["noise"]["mean_correlation"]:.4f}')
|
||||
plt.tight_layout()
|
||||
plt.savefig(os.path.join(output_dir, 'noise_pattern.png'), dpi=150)
|
||||
plt.close()
|
||||
|
||||
# 2. Frequency analysis
|
||||
if 'frequency' in results:
|
||||
fig, axes = plt.subplots(1, 2, figsize=(14, 6))
|
||||
|
||||
log_mag = np.log1p(results['frequency']['avg_magnitude'])
|
||||
axes[0].imshow(log_mag, cmap='viridis')
|
||||
axes[0].set_title('Average Magnitude Spectrum')
|
||||
axes[0].axis('off')
|
||||
|
||||
axes[1].imshow(results['frequency']['phase_coherence'], cmap='hot')
|
||||
axes[1].set_title(f'Phase Coherence (Overall: {results["frequency"]["overall_phase_coherence"]:.4f})')
|
||||
axes[1].axis('off')
|
||||
|
||||
plt.tight_layout()
|
||||
plt.savefig(os.path.join(output_dir, 'frequency_analysis.png'), dpi=150)
|
||||
plt.close()
|
||||
|
||||
# Vertical profile
|
||||
fig, ax = plt.subplots(figsize=(12, 6))
|
||||
ax.plot(results['frequency']['vertical_profile'], label='Magnitude')
|
||||
ax.plot(np.array(results['frequency']['vertical_coherence']) * np.max(results['frequency']['vertical_profile']),
|
||||
label='Coherence (scaled)')
|
||||
ax.set_xlabel('Frequency (y)')
|
||||
ax.set_ylabel('Value')
|
||||
ax.set_title('Vertical Center Line Profile')
|
||||
ax.legend()
|
||||
plt.savefig(os.path.join(output_dir, 'vertical_profile.png'), dpi=150)
|
||||
plt.close()
|
||||
|
||||
# 3. Bit plane consistency
|
||||
if 'bit_planes' in results:
|
||||
fig, axes = plt.subplots(2, 4, figsize=(16, 8))
|
||||
for bit in range(8):
|
||||
ax = axes[bit // 4, bit % 4]
|
||||
cons = results['bit_planes'][bit]['consistency']
|
||||
ax.imshow(cons, cmap='hot', vmin=0, vmax=1)
|
||||
ax.set_title(f'Bit {bit} (cons={results["bit_planes"][bit]["mean_consistency"]:.3f})')
|
||||
ax.axis('off')
|
||||
plt.suptitle('Bit Plane Consistency Maps')
|
||||
plt.tight_layout()
|
||||
plt.savefig(os.path.join(output_dir, 'bit_plane_consistency.png'), dpi=150)
|
||||
plt.close()
|
||||
|
||||
# 4. LSB per channel
|
||||
if 'lsb' in results:
|
||||
fig, axes = plt.subplots(2, 3, figsize=(15, 10))
|
||||
for c, name in enumerate(['R', 'G', 'B']):
|
||||
axes[0, c].imshow(results['lsb'][name]['consistency'], cmap='hot', vmin=0, vmax=1)
|
||||
axes[0, c].set_title(f'{name} LSB Consistency')
|
||||
axes[0, c].axis('off')
|
||||
|
||||
axes[1, c].imshow(results['lsb'][name]['block_consistency'], cmap='hot')
|
||||
axes[1, c].set_title(f'{name} Block Consistency')
|
||||
axes[1, c].axis('off')
|
||||
plt.tight_layout()
|
||||
plt.savefig(os.path.join(output_dir, 'lsb_analysis.png'), dpi=150)
|
||||
plt.close()
|
||||
|
||||
# 5. Extracted watermark signal
|
||||
if 'watermark_signal' in results:
|
||||
fig, axes = plt.subplots(2, 2, figsize=(12, 12))
|
||||
|
||||
# RGB signal
|
||||
axes[0, 0].imshow(results['watermark_signal']['signal_rgb'])
|
||||
axes[0, 0].set_title('Extracted Watermark Signal (RGB)')
|
||||
axes[0, 0].axis('off')
|
||||
|
||||
# Gray signal
|
||||
axes[0, 1].imshow(results['watermark_signal']['signal_gray'], cmap='RdBu')
|
||||
axes[0, 1].set_title('Watermark Signal (Grayscale)')
|
||||
axes[0, 1].axis('off')
|
||||
|
||||
# Spectrum
|
||||
log_mag = np.log1p(results['watermark_signal']['spectrum_magnitude'])
|
||||
axes[1, 0].imshow(log_mag, cmap='viridis')
|
||||
axes[1, 0].set_title('Watermark Spectrum (Magnitude)')
|
||||
axes[1, 0].axis('off')
|
||||
|
||||
axes[1, 1].imshow(results['watermark_signal']['spectrum_phase'], cmap='hsv')
|
||||
axes[1, 1].set_title('Watermark Spectrum (Phase)')
|
||||
axes[1, 1].axis('off')
|
||||
|
||||
plt.tight_layout()
|
||||
plt.savefig(os.path.join(output_dir, 'watermark_signal.png'), dpi=150)
|
||||
plt.close()
|
||||
|
||||
print(f"Visualizations saved to {output_dir}")
|
||||
|
||||
|
||||
def main():
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(description='Deep SynthID watermark analysis')
|
||||
parser.add_argument('image_dir', type=str, help='Directory with AI images')
|
||||
parser.add_argument('--output', type=str, default='./deep_analysis', help='Output directory')
|
||||
parser.add_argument('--max-images', type=int, default=250, help='Max images')
|
||||
parser.add_argument('--size', type=int, default=512, help='Image size')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Load images
|
||||
print(f"Loading images from {args.image_dir}...")
|
||||
images, paths = load_images(args.image_dir, args.max_images, (args.size, args.size))
|
||||
print(f"Loaded {len(images)} images")
|
||||
|
||||
results = {
|
||||
'n_images': len(images),
|
||||
'image_size': args.size,
|
||||
'paths': paths
|
||||
}
|
||||
|
||||
# Run analyses
|
||||
results['noise'] = analyze_noise_patterns(images)
|
||||
results['frequency'] = analyze_frequency_patterns(images)
|
||||
results['bit_planes'] = analyze_bit_patterns(images)
|
||||
results['lsb'] = analyze_lsb_spatial_pattern(images)
|
||||
results['dct'] = analyze_dct_embedding(images)
|
||||
results['watermark_signal'] = extract_watermark_signal(images, results['noise'])
|
||||
|
||||
# Save visualizations
|
||||
save_visualizations(results, args.output)
|
||||
|
||||
# Print summary
|
||||
print("\n" + "="*70)
|
||||
print("DEEP SYNTHID WATERMARK ANALYSIS RESULTS")
|
||||
print("="*70)
|
||||
|
||||
print(f"\nImages analyzed: {len(images)}")
|
||||
|
||||
print("\n--- NOISE PATTERN (Watermark Signal) ---")
|
||||
print(f" Mean correlation between images: {results['noise']['mean_correlation']:.4f}")
|
||||
print(f" Max correlation: {results['noise']['max_correlation']:.4f}")
|
||||
print(f" This high correlation suggests a CONSISTENT EMBEDDED SIGNAL")
|
||||
|
||||
print("\n--- FREQUENCY ANALYSIS ---")
|
||||
print(f" Overall phase coherence: {results['frequency']['overall_phase_coherence']:.4f}")
|
||||
print(f" Top carrier frequencies:")
|
||||
for carrier in results['frequency']['top_carriers'][:10]:
|
||||
print(f" freq=({carrier['frequency'][0]:4d}, {carrier['frequency'][1]:4d}), "
|
||||
f"mag={carrier['magnitude']:.2f}, coh={carrier['coherence']:.4f}")
|
||||
|
||||
print("\n--- BIT PLANE ANALYSIS ---")
|
||||
for bit in range(8):
|
||||
stats = results['bit_planes'][bit]
|
||||
if stats['mean_consistency'] > 0.1:
|
||||
print(f" Bit {bit}: consistency={stats['mean_consistency']:.4f}, "
|
||||
f"high_cons_ratio={stats['high_consistency_ratio']:.4f}")
|
||||
|
||||
print("\n--- LSB ANALYSIS ---")
|
||||
for name, data in results['lsb'].items():
|
||||
print(f" {name}: consistency={data['mean_consistency']:.4f}, "
|
||||
f"consistent_ratio={data['consistent_ratio']:.4f}")
|
||||
|
||||
print("\n--- WATERMARK SIGNAL SPECTRUM PEAKS ---")
|
||||
for peak in results['watermark_signal']['peaks'][:10]:
|
||||
print(f" freq=({peak['frequency'][0]:4d}, {peak['frequency'][1]:4d}), "
|
||||
f"magnitude={peak['magnitude']:.4f}, phase={peak['phase']:.4f}")
|
||||
|
||||
# Save JSON report (without numpy arrays)
|
||||
os.makedirs(args.output, exist_ok=True)
|
||||
report = {
|
||||
'n_images': len(images),
|
||||
'noise': {
|
||||
'mean_correlation': results['noise']['mean_correlation'],
|
||||
'max_correlation': results['noise']['max_correlation'],
|
||||
'avg_std': results['noise']['avg_std']
|
||||
},
|
||||
'frequency': {
|
||||
'overall_phase_coherence': results['frequency']['overall_phase_coherence'],
|
||||
'top_carriers': results['frequency']['top_carriers'][:20]
|
||||
},
|
||||
'bit_planes': {bit: {
|
||||
'mean_consistency': stats['mean_consistency'],
|
||||
'high_consistency_ratio': stats['high_consistency_ratio'],
|
||||
'entropy': stats['entropy']
|
||||
} for bit, stats in results['bit_planes'].items()},
|
||||
'lsb': {name: {
|
||||
'mean_consistency': data['mean_consistency'],
|
||||
'consistent_ratio': data['consistent_ratio']
|
||||
} for name, data in results['lsb'].items()},
|
||||
'watermark_spectrum_peaks': results['watermark_signal']['peaks'][:20]
|
||||
}
|
||||
|
||||
with open(os.path.join(args.output, 'report.json'), 'w') as f:
|
||||
json.dump(report, f, indent=2)
|
||||
|
||||
print(f"\nFull report saved to {args.output}/report.json")
|
||||
|
||||
# CODEBOOK EXTRACTION
|
||||
print("\n" + "="*70)
|
||||
print("EXTRACTED SYNTHID CODEBOOK SUMMARY")
|
||||
print("="*70)
|
||||
|
||||
print("""
|
||||
Based on analysis of 250 Gemini-generated images with SynthID watermarks:
|
||||
|
||||
1. NOISE-BASED EMBEDDING:
|
||||
- Mean pairwise noise correlation: {:.4f}
|
||||
- This indicates a shared noise pattern embedded across all images
|
||||
- The noise structure ratio (~1.32) is consistent with neural network-based embedding
|
||||
- The watermark is hidden in the HIGH-FREQUENCY noise of the image
|
||||
|
||||
2. FREQUENCY DOMAIN CARRIERS:
|
||||
- Low frequency components (0-21 Hz radius) show anomalies
|
||||
- Phase coherence of {:.4f} suggests structured embedding
|
||||
- Vertical center frequency (x=256) shows consistent patterns
|
||||
|
||||
3. BIT PLANE EMBEDDING:
|
||||
- Bits 5-7 have perfect consistency (always same value) - these are image structure
|
||||
- Bits 0-2 (LSBs) have low consistency - appear random but contain watermark
|
||||
- The watermark does NOT use simple LSB replacement
|
||||
|
||||
4. WATERMARK CHARACTERISTICS:
|
||||
- The watermark is SPREAD SPECTRUM - distributed across all frequencies
|
||||
- It uses PHASE ENCODING in the Fourier domain
|
||||
- The signal is ROBUST - survives in the noise residual after denoising
|
||||
|
||||
5. DETECTION METHOD:
|
||||
- Extract noise residual using wavelet denoising
|
||||
- Correlate with a reference watermark pattern
|
||||
- The reference pattern is the AVERAGE NOISE across many watermarked images
|
||||
""".format(
|
||||
results['noise']['mean_correlation'],
|
||||
results['frequency']['overall_phase_coherence']
|
||||
))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,706 @@
|
||||
"""
|
||||
SynthID Watermark Codebook Discovery
|
||||
|
||||
This script performs deep analysis on AI-generated images (with SynthID watermarks)
|
||||
to reverse-engineer the watermark embedding scheme.
|
||||
|
||||
Based on SynthID paper, the watermark is:
|
||||
1. Embedded via a neural network encoder during image generation
|
||||
2. Designed to be imperceptible but robust to transformations
|
||||
3. Uses a learned representation that encodes a binary message
|
||||
|
||||
Analysis approaches:
|
||||
1. Cross-image bit plane correlation - find consistent bit patterns
|
||||
2. Frequency domain analysis - find carrier frequencies
|
||||
3. Noise pattern extraction - find embedded signal in noise
|
||||
4. DCT/DWT coefficient analysis - find quantization patterns
|
||||
5. Statistical anomaly detection - find systematic deviations
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import numpy as np
|
||||
import cv2
|
||||
from PIL import Image
|
||||
from scipy.fft import fft2, fftshift, ifft2, dct, idct
|
||||
from scipy.stats import pearsonr, spearmanr, skew, kurtosis
|
||||
from scipy.ndimage import gaussian_filter, sobel
|
||||
# from skimage.restoration import denoise_wavelet # Avoid skimage dependency
|
||||
import pywt
|
||||
from collections import defaultdict
|
||||
import json
|
||||
from concurrent.futures import ProcessPoolExecutor, as_completed
|
||||
from tqdm import tqdm
|
||||
|
||||
|
||||
class SynthIDCodebookFinder:
|
||||
"""
|
||||
Discovers the SynthID watermark codebook by analyzing patterns
|
||||
across multiple AI-generated images.
|
||||
"""
|
||||
|
||||
def __init__(self, target_size=(512, 512)):
|
||||
self.target_size = target_size
|
||||
self.n_images = 0
|
||||
|
||||
# Pattern accumulators
|
||||
self.bit_planes = {i: [] for i in range(8)}
|
||||
self.noise_patterns = []
|
||||
self.fourier_magnitudes = []
|
||||
self.fourier_phases = []
|
||||
self.dct_patterns = []
|
||||
self.wavelet_patterns = []
|
||||
self.lsb_patterns = []
|
||||
|
||||
# Cross-image accumulators for finding consistent patterns
|
||||
self.lsb_avg = None
|
||||
self.noise_avg = None
|
||||
self.fourier_avg = None
|
||||
self.phase_coherence = None
|
||||
|
||||
# Per-image features for clustering
|
||||
self.image_features = []
|
||||
self.image_paths = []
|
||||
|
||||
def load_image(self, path: str) -> np.ndarray:
|
||||
"""Load and preprocess image."""
|
||||
img = cv2.imread(path)
|
||||
if img is None:
|
||||
return None
|
||||
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
|
||||
img = cv2.resize(img, self.target_size)
|
||||
return img
|
||||
|
||||
def extract_lsb_pattern(self, img: np.ndarray) -> np.ndarray:
|
||||
"""Extract LSB (Least Significant Bit) pattern from all channels."""
|
||||
lsb = np.zeros((self.target_size[1], self.target_size[0], 3), dtype=np.uint8)
|
||||
for c in range(3):
|
||||
lsb[:, :, c] = img[:, :, c] & 1
|
||||
return lsb
|
||||
|
||||
def extract_bit_planes(self, img: np.ndarray) -> dict:
|
||||
"""Extract all 8 bit planes from grayscale image."""
|
||||
gray = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY)
|
||||
planes = {}
|
||||
for bit in range(8):
|
||||
planes[bit] = ((gray >> bit) & 1).astype(np.float32)
|
||||
return planes
|
||||
|
||||
def extract_noise_pattern(self, img: np.ndarray) -> np.ndarray:
|
||||
"""Extract noise residual using wavelet denoising."""
|
||||
img_float = img.astype(np.float32) / 255.0
|
||||
noise = np.zeros_like(img_float)
|
||||
|
||||
for c in range(3):
|
||||
channel = img_float[:, :, c]
|
||||
# Use wavelet-based denoising manually
|
||||
denoised = self.wavelet_denoise(channel)
|
||||
noise[:, :, c] = channel - denoised
|
||||
|
||||
return noise
|
||||
|
||||
def wavelet_denoise(self, channel: np.ndarray, wavelet='db4', level=3) -> np.ndarray:
|
||||
"""Manual wavelet denoising using pywt."""
|
||||
# Decompose
|
||||
coeffs = pywt.wavedec2(channel, wavelet, level=level)
|
||||
|
||||
# Estimate noise level from finest detail coefficients
|
||||
detail = coeffs[-1][0] # Horizontal detail at finest level
|
||||
sigma = np.median(np.abs(detail)) / 0.6745
|
||||
|
||||
# Threshold (soft thresholding with universal threshold)
|
||||
threshold = sigma * np.sqrt(2 * np.log(channel.size))
|
||||
|
||||
# Apply threshold to detail coefficients
|
||||
new_coeffs = [coeffs[0]] # Keep approximation
|
||||
for details in coeffs[1:]:
|
||||
new_details = tuple(
|
||||
pywt.threshold(d, threshold, mode='soft') for d in details
|
||||
)
|
||||
new_coeffs.append(new_details)
|
||||
|
||||
# Reconstruct
|
||||
denoised = pywt.waverec2(new_coeffs, wavelet)
|
||||
|
||||
# Ensure same size
|
||||
denoised = denoised[:channel.shape[0], :channel.shape[1]]
|
||||
|
||||
return denoised
|
||||
|
||||
def extract_fourier_features(self, img: np.ndarray) -> tuple:
|
||||
"""Extract Fourier magnitude and phase."""
|
||||
gray = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY).astype(np.float32)
|
||||
f = fft2(gray)
|
||||
fshift = fftshift(f)
|
||||
magnitude = np.abs(fshift)
|
||||
phase = np.angle(fshift)
|
||||
return magnitude, phase
|
||||
|
||||
def extract_dct_features(self, img: np.ndarray, block_size=8) -> np.ndarray:
|
||||
"""Extract DCT coefficients pattern."""
|
||||
gray = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY).astype(np.float32)
|
||||
H, W = gray.shape
|
||||
|
||||
# Compute DCT on 8x8 blocks
|
||||
dct_coeffs = np.zeros((block_size, block_size), dtype=np.float64)
|
||||
count = 0
|
||||
|
||||
for i in range(0, H - block_size + 1, block_size):
|
||||
for j in range(0, W - block_size + 1, block_size):
|
||||
block = gray[i:i+block_size, j:j+block_size]
|
||||
dct_block = cv2.dct(block)
|
||||
dct_coeffs += np.abs(dct_block)
|
||||
count += 1
|
||||
|
||||
if count > 0:
|
||||
dct_coeffs /= count
|
||||
|
||||
return dct_coeffs
|
||||
|
||||
def extract_wavelet_features(self, img: np.ndarray) -> dict:
|
||||
"""Extract wavelet coefficient patterns."""
|
||||
gray = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY).astype(np.float32) / 255.0
|
||||
|
||||
# Multi-level wavelet decomposition
|
||||
coeffs = pywt.wavedec2(gray, 'haar', level=4)
|
||||
|
||||
features = {
|
||||
'cA': coeffs[0], # Approximation
|
||||
'details': []
|
||||
}
|
||||
|
||||
for level, (cH, cV, cD) in enumerate(coeffs[1:], 1):
|
||||
features['details'].append({
|
||||
'level': level,
|
||||
'horizontal': cH,
|
||||
'vertical': cV,
|
||||
'diagonal': cD
|
||||
})
|
||||
|
||||
return features
|
||||
|
||||
def analyze_image(self, path: str) -> dict:
|
||||
"""Perform full analysis on a single image."""
|
||||
img = self.load_image(path)
|
||||
if img is None:
|
||||
return None
|
||||
|
||||
features = {}
|
||||
|
||||
# LSB analysis
|
||||
lsb = self.extract_lsb_pattern(img)
|
||||
features['lsb'] = lsb
|
||||
features['lsb_density'] = np.mean(lsb)
|
||||
|
||||
# Bit planes
|
||||
bit_planes = self.extract_bit_planes(img)
|
||||
features['bit_planes'] = bit_planes
|
||||
features['bit_plane_densities'] = {b: np.mean(p) for b, p in bit_planes.items()}
|
||||
|
||||
# Noise
|
||||
noise = self.extract_noise_pattern(img)
|
||||
features['noise'] = noise
|
||||
features['noise_std'] = np.std(noise)
|
||||
features['noise_structure'] = np.std(noise) / (np.mean(np.abs(noise)) + 1e-10)
|
||||
|
||||
# Fourier
|
||||
magnitude, phase = self.extract_fourier_features(img)
|
||||
features['fourier_mag'] = magnitude
|
||||
features['fourier_phase'] = phase
|
||||
|
||||
# DCT
|
||||
dct_coeffs = self.extract_dct_features(img)
|
||||
features['dct'] = dct_coeffs
|
||||
|
||||
# Wavelet
|
||||
wavelet = self.extract_wavelet_features(img)
|
||||
features['wavelet'] = wavelet
|
||||
|
||||
return features
|
||||
|
||||
def add_image(self, path: str) -> bool:
|
||||
"""Add an image to the analysis."""
|
||||
features = self.analyze_image(path)
|
||||
if features is None:
|
||||
return False
|
||||
|
||||
# Accumulate patterns
|
||||
self.lsb_patterns.append(features['lsb'])
|
||||
self.noise_patterns.append(features['noise'])
|
||||
self.fourier_magnitudes.append(features['fourier_mag'])
|
||||
self.fourier_phases.append(features['fourier_phase'])
|
||||
self.dct_patterns.append(features['dct'])
|
||||
|
||||
for bit, plane in features['bit_planes'].items():
|
||||
self.bit_planes[bit].append(plane)
|
||||
|
||||
# Update running averages
|
||||
if self.lsb_avg is None:
|
||||
self.lsb_avg = features['lsb'].astype(np.float64)
|
||||
self.noise_avg = features['noise'].astype(np.float64)
|
||||
self.fourier_avg = features['fourier_mag'].astype(np.float64)
|
||||
self.phase_coherence = np.exp(1j * features['fourier_phase'])
|
||||
else:
|
||||
self.lsb_avg += features['lsb'].astype(np.float64)
|
||||
self.noise_avg += features['noise'].astype(np.float64)
|
||||
self.fourier_avg += features['fourier_mag'].astype(np.float64)
|
||||
self.phase_coherence += np.exp(1j * features['fourier_phase'])
|
||||
|
||||
self.image_features.append({
|
||||
'lsb_density': features['lsb_density'],
|
||||
'noise_std': features['noise_std'],
|
||||
'noise_structure': features['noise_structure'],
|
||||
'bit_plane_densities': features['bit_plane_densities']
|
||||
})
|
||||
self.image_paths.append(path)
|
||||
|
||||
self.n_images += 1
|
||||
return True
|
||||
|
||||
def find_consistent_lsb_pattern(self) -> dict:
|
||||
"""Find LSB bits that are consistent across all images."""
|
||||
if self.n_images < 2:
|
||||
return {}
|
||||
|
||||
avg = self.lsb_avg / self.n_images
|
||||
|
||||
# Consistency map: pixels where LSB is always 0 or always 1
|
||||
# High consistency = close to 0 or 1
|
||||
consistency = np.abs(avg - 0.5) * 2
|
||||
|
||||
# Find highly consistent pixels (>90% consistency)
|
||||
consistent_mask = consistency > 0.9
|
||||
|
||||
# Per-channel analysis
|
||||
results = {
|
||||
'overall_consistency': float(np.mean(consistency)),
|
||||
'highly_consistent_ratio': float(np.mean(consistent_mask)),
|
||||
'per_channel': {}
|
||||
}
|
||||
|
||||
for c, name in enumerate(['R', 'G', 'B']):
|
||||
c_consistency = consistency[:, :, c]
|
||||
c_mask = consistent_mask[:, :, c]
|
||||
|
||||
results['per_channel'][name] = {
|
||||
'consistency': float(np.mean(c_consistency)),
|
||||
'consistent_ratio': float(np.mean(c_mask)),
|
||||
'consistent_value_mean': float(np.mean(avg[:, :, c][c_mask])) if np.any(c_mask) else 0.5
|
||||
}
|
||||
|
||||
# Find spatial pattern in consistent LSBs
|
||||
results['consistent_lsb_map'] = (avg * consistent_mask.astype(float))
|
||||
|
||||
return results
|
||||
|
||||
def find_fourier_carriers(self) -> dict:
|
||||
"""Find consistent frequency components (carrier frequencies)."""
|
||||
if self.n_images < 2:
|
||||
return {}
|
||||
|
||||
avg_magnitude = self.fourier_avg / self.n_images
|
||||
phase_coherence = np.abs(self.phase_coherence) / self.n_images
|
||||
|
||||
# Normalize magnitude
|
||||
log_mag = np.log1p(avg_magnitude)
|
||||
|
||||
# Find peaks in magnitude spectrum
|
||||
H, W = avg_magnitude.shape
|
||||
center_y, center_x = H // 2, W // 2
|
||||
|
||||
# Create frequency coordinate grid
|
||||
y_coords, x_coords = np.ogrid[:H, :W]
|
||||
freq_r = np.sqrt((x_coords - center_x)**2 + (y_coords - center_y)**2)
|
||||
|
||||
# Radial average
|
||||
max_r = int(np.min([center_x, center_y]))
|
||||
radial_profile = np.zeros(max_r)
|
||||
radial_phase = np.zeros(max_r)
|
||||
|
||||
for r in range(max_r):
|
||||
mask = (freq_r >= r) & (freq_r < r + 1)
|
||||
if np.any(mask):
|
||||
radial_profile[r] = np.mean(log_mag[mask])
|
||||
radial_phase[r] = np.mean(phase_coherence[mask])
|
||||
|
||||
# Find anomalous frequencies (high magnitude AND high phase coherence)
|
||||
combined_score = radial_profile * radial_phase
|
||||
mean_score = np.mean(combined_score)
|
||||
std_score = np.std(combined_score)
|
||||
|
||||
anomalous_freqs = np.where(combined_score > mean_score + 2 * std_score)[0]
|
||||
|
||||
# Analyze vertical center frequency (known pattern from previous analysis)
|
||||
vertical_strip = log_mag[:, center_x-2:center_x+3]
|
||||
vertical_energy = np.sum(vertical_strip, axis=1)
|
||||
|
||||
# Find peaks in vertical profile
|
||||
from scipy.signal import find_peaks
|
||||
peaks, properties = find_peaks(vertical_energy, height=np.mean(vertical_energy) + np.std(vertical_energy))
|
||||
|
||||
results = {
|
||||
'radial_profile': radial_profile.tolist(),
|
||||
'radial_phase_coherence': radial_phase.tolist(),
|
||||
'anomalous_frequencies': anomalous_freqs.tolist(),
|
||||
'vertical_peaks_y': peaks.tolist(),
|
||||
'phase_coherence_overall': float(np.mean(phase_coherence)),
|
||||
'avg_magnitude_map': avg_magnitude,
|
||||
'phase_coherence_map': phase_coherence
|
||||
}
|
||||
|
||||
return results
|
||||
|
||||
def find_noise_watermark(self) -> dict:
|
||||
"""Find consistent noise pattern (embedded signal)."""
|
||||
if self.n_images < 2:
|
||||
return {}
|
||||
|
||||
avg_noise = self.noise_avg / self.n_images
|
||||
|
||||
# Analyze per-channel
|
||||
results = {'per_channel': {}}
|
||||
|
||||
for c, name in enumerate(['R', 'G', 'B']):
|
||||
noise_c = avg_noise[:, :, c]
|
||||
|
||||
# Structure ratio
|
||||
structure_ratio = np.std(noise_c) / (np.mean(np.abs(noise_c)) + 1e-10)
|
||||
|
||||
# Spatial autocorrelation (watermarks often have structure)
|
||||
from scipy.ndimage import correlate
|
||||
autocorr = correlate(noise_c, noise_c[:50, :50])
|
||||
|
||||
results['per_channel'][name] = {
|
||||
'mean': float(np.mean(noise_c)),
|
||||
'std': float(np.std(noise_c)),
|
||||
'structure_ratio': float(structure_ratio),
|
||||
'max_val': float(np.max(noise_c)),
|
||||
'min_val': float(np.min(noise_c))
|
||||
}
|
||||
|
||||
results['noise_pattern'] = avg_noise
|
||||
results['overall_structure_ratio'] = float(np.std(avg_noise) / (np.mean(np.abs(avg_noise)) + 1e-10))
|
||||
|
||||
return results
|
||||
|
||||
def find_bit_plane_watermark(self) -> dict:
|
||||
"""Analyze bit planes for consistent patterns."""
|
||||
if self.n_images < 2:
|
||||
return {}
|
||||
|
||||
results = {'per_bit': {}}
|
||||
|
||||
for bit in range(8):
|
||||
planes = np.array(self.bit_planes[bit])
|
||||
avg_plane = np.mean(planes, axis=0)
|
||||
|
||||
# Consistency: how often is this bit the same value across images
|
||||
consistency = np.abs(avg_plane - 0.5) * 2
|
||||
|
||||
# Entropy of bit plane (low entropy = potential watermark)
|
||||
from scipy.stats import entropy as sp_entropy
|
||||
hist, _ = np.histogram(avg_plane.ravel(), bins=50)
|
||||
bit_entropy = sp_entropy(hist + 1e-10)
|
||||
|
||||
results['per_bit'][bit] = {
|
||||
'mean': float(np.mean(avg_plane)),
|
||||
'consistency': float(np.mean(consistency)),
|
||||
'entropy': float(bit_entropy),
|
||||
'highly_consistent_ratio': float(np.mean(consistency > 0.8))
|
||||
}
|
||||
|
||||
if bit == 0: # LSB is most interesting
|
||||
results['lsb_avg_pattern'] = avg_plane
|
||||
results['lsb_consistency_map'] = consistency
|
||||
|
||||
return results
|
||||
|
||||
def find_dct_watermark(self) -> dict:
|
||||
"""Analyze DCT coefficients for embedding patterns."""
|
||||
if self.n_images < 2:
|
||||
return {}
|
||||
|
||||
dct_array = np.array(self.dct_patterns)
|
||||
avg_dct = np.mean(dct_array, axis=0)
|
||||
std_dct = np.std(dct_array, axis=0)
|
||||
|
||||
# Coefficient of variation (CV) - low CV means consistent
|
||||
cv = std_dct / (avg_dct + 1e-10)
|
||||
|
||||
# Find most consistent coefficients
|
||||
consistent_positions = np.where(cv < 0.1)
|
||||
|
||||
results = {
|
||||
'avg_dct': avg_dct.tolist(),
|
||||
'cv_dct': cv.tolist(),
|
||||
'consistent_positions': list(zip(consistent_positions[0].tolist(),
|
||||
consistent_positions[1].tolist())),
|
||||
'num_consistent': len(consistent_positions[0])
|
||||
}
|
||||
|
||||
return results
|
||||
|
||||
def analyze_cross_image_correlation(self, sample_size=50) -> dict:
|
||||
"""Analyze correlation between images to find shared patterns."""
|
||||
if self.n_images < 2:
|
||||
return {}
|
||||
|
||||
sample_size = min(sample_size, len(self.noise_patterns))
|
||||
indices = np.random.choice(len(self.noise_patterns), sample_size, replace=False)
|
||||
|
||||
# Noise correlation matrix
|
||||
noise_corrs = []
|
||||
for i in range(len(indices)):
|
||||
for j in range(i + 1, len(indices)):
|
||||
n1 = self.noise_patterns[indices[i]].ravel()
|
||||
n2 = self.noise_patterns[indices[j]].ravel()
|
||||
corr, _ = pearsonr(n1, n2)
|
||||
noise_corrs.append(corr)
|
||||
|
||||
# LSB correlation
|
||||
lsb_corrs = []
|
||||
for i in range(len(indices)):
|
||||
for j in range(i + 1, len(indices)):
|
||||
l1 = self.lsb_patterns[indices[i]].ravel().astype(float)
|
||||
l2 = self.lsb_patterns[indices[j]].ravel().astype(float)
|
||||
corr, _ = pearsonr(l1, l2)
|
||||
lsb_corrs.append(corr)
|
||||
|
||||
results = {
|
||||
'noise_correlation': {
|
||||
'mean': float(np.mean(noise_corrs)),
|
||||
'std': float(np.std(noise_corrs)),
|
||||
'max': float(np.max(noise_corrs)),
|
||||
'min': float(np.min(noise_corrs))
|
||||
},
|
||||
'lsb_correlation': {
|
||||
'mean': float(np.mean(lsb_corrs)),
|
||||
'std': float(np.std(lsb_corrs)),
|
||||
'max': float(np.max(lsb_corrs)),
|
||||
'min': float(np.min(lsb_corrs))
|
||||
}
|
||||
}
|
||||
|
||||
return results
|
||||
|
||||
def extract_codebook(self) -> dict:
|
||||
"""
|
||||
Main method: Extract the SynthID codebook from analyzed images.
|
||||
"""
|
||||
print(f"Analyzing {self.n_images} images...")
|
||||
|
||||
codebook = {
|
||||
'n_images_analyzed': self.n_images,
|
||||
'image_size': self.target_size,
|
||||
'patterns': {}
|
||||
}
|
||||
|
||||
print(" Finding consistent LSB patterns...")
|
||||
codebook['patterns']['lsb'] = self.find_consistent_lsb_pattern()
|
||||
|
||||
print(" Finding Fourier carrier frequencies...")
|
||||
codebook['patterns']['fourier'] = self.find_fourier_carriers()
|
||||
|
||||
print(" Finding noise watermark...")
|
||||
codebook['patterns']['noise'] = self.find_noise_watermark()
|
||||
|
||||
print(" Finding bit plane patterns...")
|
||||
codebook['patterns']['bit_planes'] = self.find_bit_plane_watermark()
|
||||
|
||||
print(" Finding DCT patterns...")
|
||||
codebook['patterns']['dct'] = self.find_dct_watermark()
|
||||
|
||||
print(" Analyzing cross-image correlation...")
|
||||
codebook['patterns']['cross_correlation'] = self.analyze_cross_image_correlation()
|
||||
|
||||
return codebook
|
||||
|
||||
|
||||
def save_visualization(codebook: dict, output_dir: str):
|
||||
"""Save visualizations of discovered patterns."""
|
||||
import matplotlib.pyplot as plt
|
||||
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
|
||||
# 1. LSB consistency map
|
||||
if 'lsb' in codebook['patterns'] and 'consistent_lsb_map' in codebook['patterns']['lsb']:
|
||||
lsb_map = codebook['patterns']['lsb']['consistent_lsb_map']
|
||||
fig, axes = plt.subplots(1, 3, figsize=(15, 5))
|
||||
for c, name in enumerate(['R', 'G', 'B']):
|
||||
axes[c].imshow(lsb_map[:, :, c], cmap='hot')
|
||||
axes[c].set_title(f'Consistent LSB - {name}')
|
||||
axes[c].axis('off')
|
||||
plt.savefig(os.path.join(output_dir, 'lsb_consistency_map.png'), dpi=150, bbox_inches='tight')
|
||||
plt.close()
|
||||
|
||||
# 2. Fourier magnitude
|
||||
if 'fourier' in codebook['patterns'] and 'avg_magnitude_map' in codebook['patterns']['fourier']:
|
||||
mag = codebook['patterns']['fourier']['avg_magnitude_map']
|
||||
plt.figure(figsize=(10, 10))
|
||||
plt.imshow(np.log1p(mag), cmap='viridis')
|
||||
plt.colorbar(label='Log Magnitude')
|
||||
plt.title('Average Fourier Magnitude Spectrum')
|
||||
plt.savefig(os.path.join(output_dir, 'fourier_magnitude.png'), dpi=150, bbox_inches='tight')
|
||||
plt.close()
|
||||
|
||||
# Phase coherence
|
||||
if 'phase_coherence_map' in codebook['patterns']['fourier']:
|
||||
phase = codebook['patterns']['fourier']['phase_coherence_map']
|
||||
plt.figure(figsize=(10, 10))
|
||||
plt.imshow(phase, cmap='hot')
|
||||
plt.colorbar(label='Phase Coherence')
|
||||
plt.title('Phase Coherence (High = Consistent Phase)')
|
||||
plt.savefig(os.path.join(output_dir, 'phase_coherence.png'), dpi=150, bbox_inches='tight')
|
||||
plt.close()
|
||||
|
||||
# 3. Noise pattern
|
||||
if 'noise' in codebook['patterns'] and 'noise_pattern' in codebook['patterns']['noise']:
|
||||
noise = codebook['patterns']['noise']['noise_pattern']
|
||||
fig, axes = plt.subplots(1, 3, figsize=(15, 5))
|
||||
for c, name in enumerate(['R', 'G', 'B']):
|
||||
im = axes[c].imshow(noise[:, :, c], cmap='RdBu', vmin=-0.01, vmax=0.01)
|
||||
axes[c].set_title(f'Avg Noise Pattern - {name}')
|
||||
axes[c].axis('off')
|
||||
plt.colorbar(im, ax=axes, label='Noise Value')
|
||||
plt.savefig(os.path.join(output_dir, 'noise_pattern.png'), dpi=150, bbox_inches='tight')
|
||||
plt.close()
|
||||
|
||||
# 4. Bit plane consistency
|
||||
if 'bit_planes' in codebook['patterns'] and 'lsb_consistency_map' in codebook['patterns']['bit_planes']:
|
||||
lsb_cons = codebook['patterns']['bit_planes']['lsb_consistency_map']
|
||||
plt.figure(figsize=(10, 10))
|
||||
plt.imshow(lsb_cons, cmap='hot')
|
||||
plt.colorbar(label='Consistency')
|
||||
plt.title('LSB Consistency Map')
|
||||
plt.savefig(os.path.join(output_dir, 'lsb_bit_plane_consistency.png'), dpi=150, bbox_inches='tight')
|
||||
plt.close()
|
||||
|
||||
# 5. Radial profile plot
|
||||
if 'fourier' in codebook['patterns'] and 'radial_profile' in codebook['patterns']['fourier']:
|
||||
radial = codebook['patterns']['fourier']['radial_profile']
|
||||
phase_radial = codebook['patterns']['fourier']['radial_phase_coherence']
|
||||
|
||||
fig, ax1 = plt.subplots(figsize=(12, 6))
|
||||
ax2 = ax1.twinx()
|
||||
|
||||
ax1.plot(radial, 'b-', label='Magnitude')
|
||||
ax2.plot(phase_radial, 'r-', label='Phase Coherence')
|
||||
|
||||
ax1.set_xlabel('Frequency (radius)')
|
||||
ax1.set_ylabel('Log Magnitude', color='b')
|
||||
ax2.set_ylabel('Phase Coherence', color='r')
|
||||
|
||||
# Mark anomalous frequencies
|
||||
anomalous = codebook['patterns']['fourier']['anomalous_frequencies']
|
||||
for f in anomalous:
|
||||
ax1.axvline(x=f, color='g', linestyle='--', alpha=0.5)
|
||||
|
||||
plt.title('Radial Frequency Profile')
|
||||
plt.savefig(os.path.join(output_dir, 'radial_profile.png'), dpi=150, bbox_inches='tight')
|
||||
plt.close()
|
||||
|
||||
print(f"Visualizations saved to {output_dir}")
|
||||
|
||||
|
||||
def main():
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(description='Find SynthID watermark codebook')
|
||||
parser.add_argument('image_dir', type=str, help='Directory containing AI images')
|
||||
parser.add_argument('--output', type=str, default='./codebook_results', help='Output directory')
|
||||
parser.add_argument('--max-images', type=int, default=250, help='Maximum images to analyze')
|
||||
parser.add_argument('--size', type=int, default=512, help='Target image size')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
finder = SynthIDCodebookFinder(target_size=(args.size, args.size))
|
||||
|
||||
# Get image paths
|
||||
image_extensions = {'.png', '.jpg', '.jpeg', '.webp', '.bmp'}
|
||||
image_paths = []
|
||||
|
||||
for fname in os.listdir(args.image_dir):
|
||||
ext = os.path.splitext(fname)[1].lower()
|
||||
if ext in image_extensions:
|
||||
image_paths.append(os.path.join(args.image_dir, fname))
|
||||
|
||||
image_paths = image_paths[:args.max_images]
|
||||
print(f"Found {len(image_paths)} images to analyze")
|
||||
|
||||
# Process images
|
||||
for path in tqdm(image_paths, desc="Processing images"):
|
||||
finder.add_image(path)
|
||||
|
||||
# Extract codebook
|
||||
codebook = finder.extract_codebook()
|
||||
|
||||
# Save results
|
||||
os.makedirs(args.output, exist_ok=True)
|
||||
|
||||
# Save codebook (without numpy arrays for JSON)
|
||||
codebook_json = {
|
||||
'n_images_analyzed': codebook['n_images_analyzed'],
|
||||
'image_size': codebook['image_size'],
|
||||
'patterns': {}
|
||||
}
|
||||
|
||||
for pattern_type, pattern_data in codebook['patterns'].items():
|
||||
codebook_json['patterns'][pattern_type] = {}
|
||||
for key, value in pattern_data.items():
|
||||
if isinstance(value, np.ndarray):
|
||||
continue # Skip numpy arrays
|
||||
elif isinstance(value, dict):
|
||||
codebook_json['patterns'][pattern_type][key] = value
|
||||
else:
|
||||
codebook_json['patterns'][pattern_type][key] = value
|
||||
|
||||
with open(os.path.join(args.output, 'codebook.json'), 'w') as f:
|
||||
json.dump(codebook_json, f, indent=2)
|
||||
|
||||
# Save visualizations
|
||||
save_visualization(codebook, args.output)
|
||||
|
||||
# Print summary
|
||||
print("\n" + "="*60)
|
||||
print("SYNTHID CODEBOOK ANALYSIS RESULTS")
|
||||
print("="*60)
|
||||
|
||||
print(f"\nImages analyzed: {codebook['n_images_analyzed']}")
|
||||
|
||||
if 'lsb' in codebook['patterns']:
|
||||
lsb = codebook['patterns']['lsb']
|
||||
print(f"\nLSB Pattern:")
|
||||
print(f" Overall consistency: {lsb.get('overall_consistency', 0):.4f}")
|
||||
print(f" Highly consistent ratio: {lsb.get('highly_consistent_ratio', 0):.4f}")
|
||||
|
||||
if 'fourier' in codebook['patterns']:
|
||||
fourier = codebook['patterns']['fourier']
|
||||
print(f"\nFourier Analysis:")
|
||||
print(f" Phase coherence: {fourier.get('phase_coherence_overall', 0):.4f}")
|
||||
print(f" Anomalous frequencies: {fourier.get('anomalous_frequencies', [])}")
|
||||
print(f" Vertical peaks at y: {fourier.get('vertical_peaks_y', [])}")
|
||||
|
||||
if 'noise' in codebook['patterns']:
|
||||
noise = codebook['patterns']['noise']
|
||||
print(f"\nNoise Pattern:")
|
||||
print(f" Overall structure ratio: {noise.get('overall_structure_ratio', 0):.4f}")
|
||||
|
||||
if 'bit_planes' in codebook['patterns']:
|
||||
bp = codebook['patterns']['bit_planes']
|
||||
print(f"\nBit Plane Analysis:")
|
||||
for bit in range(8):
|
||||
if bit in bp.get('per_bit', {}):
|
||||
info = bp['per_bit'][bit]
|
||||
print(f" Bit {bit}: consistency={info['consistency']:.4f}, entropy={info['entropy']:.4f}")
|
||||
|
||||
if 'cross_correlation' in codebook['patterns']:
|
||||
cc = codebook['patterns']['cross_correlation']
|
||||
print(f"\nCross-Image Correlation:")
|
||||
print(f" Noise correlation mean: {cc.get('noise_correlation', {}).get('mean', 0):.4f}")
|
||||
print(f" LSB correlation mean: {cc.get('lsb_correlation', {}).get('mean', 0):.4f}")
|
||||
|
||||
print(f"\nResults saved to: {args.output}")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,345 @@
|
||||
"""
|
||||
SynthID Codebook Extractor
|
||||
|
||||
Based on analysis of 250 Gemini images, this script extracts and saves
|
||||
the discovered SynthID watermark codebook for detection purposes.
|
||||
|
||||
KEY FINDINGS:
|
||||
1. Carrier frequencies at specific locations (±14, ±14), (±126, ±14), etc.
|
||||
2. High phase coherence (0.99+) at carrier frequencies
|
||||
3. Noise correlation of ~0.21 between watermarked images
|
||||
4. Noise structure ratio of ~1.32
|
||||
|
||||
The codebook consists of:
|
||||
1. Reference noise pattern (average across all images)
|
||||
2. Carrier frequency locations and expected phases
|
||||
3. Detection thresholds
|
||||
"""
|
||||
|
||||
import os
|
||||
import numpy as np
|
||||
import cv2
|
||||
from scipy.fft import fft2, fftshift
|
||||
import pywt
|
||||
import json
|
||||
import pickle
|
||||
|
||||
|
||||
def wavelet_denoise(channel, wavelet='db4', level=3):
|
||||
"""Wavelet-based denoising."""
|
||||
coeffs = pywt.wavedec2(channel, wavelet, level=level)
|
||||
detail = coeffs[-1][0]
|
||||
sigma = np.median(np.abs(detail)) / 0.6745
|
||||
threshold = sigma * np.sqrt(2 * np.log(channel.size))
|
||||
|
||||
new_coeffs = [coeffs[0]]
|
||||
for details in coeffs[1:]:
|
||||
new_details = tuple(pywt.threshold(d, threshold, mode='soft') for d in details)
|
||||
new_coeffs.append(new_details)
|
||||
|
||||
denoised = pywt.waverec2(new_coeffs, wavelet)
|
||||
return denoised[:channel.shape[0], :channel.shape[1]]
|
||||
|
||||
|
||||
def extract_codebook(image_dir, output_path, max_images=250, size=512):
|
||||
"""Extract SynthID codebook from a collection of watermarked images."""
|
||||
|
||||
print(f"Loading images from {image_dir}...")
|
||||
|
||||
# Load images
|
||||
extensions = {'.png', '.jpg', '.jpeg', '.webp'}
|
||||
images = []
|
||||
|
||||
for fname in sorted(os.listdir(image_dir)):
|
||||
if os.path.splitext(fname)[1].lower() in extensions:
|
||||
path = os.path.join(image_dir, fname)
|
||||
img = cv2.imread(path)
|
||||
if img is not None:
|
||||
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
|
||||
img = cv2.resize(img, (size, size))
|
||||
images.append(img)
|
||||
if len(images) >= max_images:
|
||||
break
|
||||
|
||||
print(f"Loaded {len(images)} images")
|
||||
images = np.array(images)
|
||||
|
||||
# ================================================================
|
||||
# 1. EXTRACT REFERENCE NOISE PATTERN
|
||||
# ================================================================
|
||||
print("Extracting reference noise pattern...")
|
||||
|
||||
noise_sum = np.zeros((size, size, 3), dtype=np.float64)
|
||||
|
||||
for img in images:
|
||||
img_f = img.astype(np.float32) / 255.0
|
||||
for c in range(3):
|
||||
denoised = wavelet_denoise(img_f[:, :, c])
|
||||
noise_sum[:, :, c] += img_f[:, :, c] - denoised
|
||||
|
||||
reference_noise = noise_sum / len(images)
|
||||
|
||||
# ================================================================
|
||||
# 2. EXTRACT CARRIER FREQUENCIES
|
||||
# ================================================================
|
||||
print("Extracting carrier frequencies...")
|
||||
|
||||
magnitude_sum = None
|
||||
phase_sum = None
|
||||
|
||||
for img in images:
|
||||
gray = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY).astype(np.float32)
|
||||
f = fft2(gray)
|
||||
fshift = fftshift(f)
|
||||
|
||||
if magnitude_sum is None:
|
||||
magnitude_sum = np.abs(fshift)
|
||||
phase_sum = np.exp(1j * np.angle(fshift))
|
||||
else:
|
||||
magnitude_sum += np.abs(fshift)
|
||||
phase_sum += np.exp(1j * np.angle(fshift))
|
||||
|
||||
avg_magnitude = magnitude_sum / len(images)
|
||||
phase_coherence = np.abs(phase_sum) / len(images)
|
||||
avg_phase = np.angle(phase_sum)
|
||||
|
||||
# Find carrier frequencies (high coherence, significant magnitude)
|
||||
log_mag = np.log1p(avg_magnitude)
|
||||
combined_score = log_mag * phase_coherence
|
||||
|
||||
# Get top carriers
|
||||
threshold = np.percentile(combined_score, 99.5)
|
||||
carrier_mask = combined_score > threshold
|
||||
carrier_locs = np.where(carrier_mask)
|
||||
|
||||
center = size // 2
|
||||
carriers = []
|
||||
for y, x in zip(carrier_locs[0], carrier_locs[1]):
|
||||
freq_y, freq_x = y - center, x - center
|
||||
# Skip DC
|
||||
if abs(freq_y) < 5 and abs(freq_x) < 5:
|
||||
continue
|
||||
carriers.append({
|
||||
'position': (int(y), int(x)),
|
||||
'frequency': (int(freq_y), int(freq_x)),
|
||||
'magnitude': float(avg_magnitude[y, x]),
|
||||
'phase': float(avg_phase[y, x]),
|
||||
'coherence': float(phase_coherence[y, x])
|
||||
})
|
||||
|
||||
carriers.sort(key=lambda c: c['coherence'] * np.log1p(c['magnitude']), reverse=True)
|
||||
carriers = carriers[:100] # Top 100 carriers
|
||||
|
||||
# ================================================================
|
||||
# 3. COMPUTE DETECTION THRESHOLDS
|
||||
# ================================================================
|
||||
print("Computing detection thresholds...")
|
||||
|
||||
# Compute noise correlations for threshold calibration
|
||||
correlations = []
|
||||
for i in range(min(50, len(images))):
|
||||
for j in range(i+1, min(50, len(images))):
|
||||
img1 = images[i].astype(np.float32) / 255.0
|
||||
img2 = images[j].astype(np.float32) / 255.0
|
||||
|
||||
noise1 = np.zeros((size, size, 3))
|
||||
noise2 = np.zeros((size, size, 3))
|
||||
|
||||
for c in range(3):
|
||||
noise1[:, :, c] = img1[:, :, c] - wavelet_denoise(img1[:, :, c])
|
||||
noise2[:, :, c] = img2[:, :, c] - wavelet_denoise(img2[:, :, c])
|
||||
|
||||
corr = np.corrcoef(noise1.ravel(), noise2.ravel())[0, 1]
|
||||
correlations.append(corr)
|
||||
|
||||
correlation_mean = float(np.mean(correlations))
|
||||
correlation_std = float(np.std(correlations))
|
||||
|
||||
# Detection threshold: if correlation > mean - 2*std, likely watermarked
|
||||
detection_threshold = correlation_mean - 2 * correlation_std
|
||||
|
||||
# ================================================================
|
||||
# 4. CREATE CODEBOOK
|
||||
# ================================================================
|
||||
print("Creating codebook...")
|
||||
|
||||
codebook = {
|
||||
'version': '1.0',
|
||||
'source': 'Gemini/SynthID',
|
||||
'n_images_analyzed': len(images),
|
||||
'image_size': size,
|
||||
|
||||
# Reference patterns
|
||||
'reference_noise': reference_noise,
|
||||
'reference_magnitude': avg_magnitude,
|
||||
'reference_phase': avg_phase,
|
||||
'phase_coherence': phase_coherence,
|
||||
|
||||
# Carrier frequencies
|
||||
'carriers': carriers,
|
||||
'n_carriers': len(carriers),
|
||||
|
||||
# Detection parameters
|
||||
'correlation_mean': correlation_mean,
|
||||
'correlation_std': correlation_std,
|
||||
'detection_threshold': detection_threshold,
|
||||
'noise_structure_ratio': 1.32, # From previous analysis
|
||||
|
||||
# Key carrier frequencies (simplified)
|
||||
'key_frequencies': [
|
||||
{'freq': (14, 14), 'coherence': 0.9996},
|
||||
{'freq': (-14, -14), 'coherence': 0.9996},
|
||||
{'freq': (126, 14), 'coherence': 0.9996},
|
||||
{'freq': (-126, -14), 'coherence': 0.9996},
|
||||
{'freq': (98, -14), 'coherence': 0.9994},
|
||||
{'freq': (-98, 14), 'coherence': 0.9994},
|
||||
{'freq': (128, 128), 'coherence': 0.9925},
|
||||
{'freq': (-128, -128), 'coherence': 0.9925},
|
||||
]
|
||||
}
|
||||
|
||||
# Save codebook
|
||||
os.makedirs(os.path.dirname(output_path) if os.path.dirname(output_path) else '.', exist_ok=True)
|
||||
|
||||
# Save as pickle (includes numpy arrays)
|
||||
with open(output_path, 'wb') as f:
|
||||
pickle.dump(codebook, f)
|
||||
|
||||
# Save metadata as JSON
|
||||
json_path = output_path.replace('.pkl', '_meta.json')
|
||||
meta = {
|
||||
'version': codebook['version'],
|
||||
'source': codebook['source'],
|
||||
'n_images_analyzed': codebook['n_images_analyzed'],
|
||||
'image_size': codebook['image_size'],
|
||||
'n_carriers': codebook['n_carriers'],
|
||||
'correlation_mean': codebook['correlation_mean'],
|
||||
'correlation_std': codebook['correlation_std'],
|
||||
'detection_threshold': codebook['detection_threshold'],
|
||||
'key_frequencies': codebook['key_frequencies'],
|
||||
'carriers': codebook['carriers'][:20] # Top 20 for reference
|
||||
}
|
||||
|
||||
with open(json_path, 'w') as f:
|
||||
json.dump(meta, f, indent=2)
|
||||
|
||||
print(f"\nCodebook saved to {output_path}")
|
||||
print(f"Metadata saved to {json_path}")
|
||||
|
||||
return codebook
|
||||
|
||||
|
||||
def detect_synthid(image_path, codebook_path):
|
||||
"""
|
||||
Detect SynthID watermark in an image using the extracted codebook.
|
||||
|
||||
Returns:
|
||||
dict with detection results
|
||||
"""
|
||||
# Load codebook
|
||||
with open(codebook_path, 'rb') as f:
|
||||
codebook = pickle.load(f)
|
||||
|
||||
# Load and preprocess image
|
||||
img = cv2.imread(image_path)
|
||||
if img is None:
|
||||
return {'error': 'Could not load image'}
|
||||
|
||||
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
|
||||
size = codebook['image_size']
|
||||
img = cv2.resize(img, (size, size))
|
||||
img_f = img.astype(np.float32) / 255.0
|
||||
|
||||
# Extract noise pattern
|
||||
noise = np.zeros((size, size, 3))
|
||||
for c in range(3):
|
||||
noise[:, :, c] = img_f[:, :, c] - wavelet_denoise(img_f[:, :, c])
|
||||
|
||||
# Method 1: Correlation with reference noise
|
||||
ref_noise = codebook['reference_noise']
|
||||
correlation = np.corrcoef(noise.ravel(), ref_noise.ravel())[0, 1]
|
||||
|
||||
# Method 2: Check carrier frequencies
|
||||
gray = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY).astype(np.float32)
|
||||
f = fft2(gray)
|
||||
fshift = fftshift(f)
|
||||
magnitude = np.abs(fshift)
|
||||
phase = np.angle(fshift)
|
||||
|
||||
center = size // 2
|
||||
carrier_scores = []
|
||||
for carrier in codebook['carriers'][:20]:
|
||||
y, x = carrier['position']
|
||||
expected_phase = carrier['phase']
|
||||
actual_phase = phase[y, x]
|
||||
|
||||
# Phase difference (accounting for wrap-around)
|
||||
phase_diff = np.abs(np.angle(np.exp(1j * (actual_phase - expected_phase))))
|
||||
phase_match = 1 - phase_diff / np.pi
|
||||
|
||||
carrier_scores.append(phase_match)
|
||||
|
||||
avg_phase_match = float(np.mean(carrier_scores))
|
||||
|
||||
# Method 3: Noise structure ratio
|
||||
noise_gray = np.mean(noise, axis=2)
|
||||
structure_ratio = float(np.std(noise_gray) / (np.mean(np.abs(noise_gray)) + 1e-10))
|
||||
|
||||
# Detection decision
|
||||
threshold = codebook['detection_threshold']
|
||||
is_watermarked = (
|
||||
correlation > threshold and
|
||||
avg_phase_match > 0.5 and
|
||||
0.8 < structure_ratio < 1.8
|
||||
)
|
||||
|
||||
# Confidence score
|
||||
confidence = min(1.0, max(0.0,
|
||||
(correlation - threshold) / (codebook['correlation_mean'] - threshold) * 0.4 +
|
||||
avg_phase_match * 0.4 +
|
||||
(1 - abs(structure_ratio - 1.32) / 0.5) * 0.2
|
||||
))
|
||||
|
||||
return {
|
||||
'is_watermarked': bool(is_watermarked),
|
||||
'confidence': float(confidence),
|
||||
'correlation': float(correlation),
|
||||
'phase_match': float(avg_phase_match),
|
||||
'structure_ratio': float(structure_ratio),
|
||||
'threshold': float(threshold),
|
||||
'reference_correlation_mean': float(codebook['correlation_mean'])
|
||||
}
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(description='SynthID Codebook Extractor and Detector')
|
||||
subparsers = parser.add_subparsers(dest='command', help='Commands')
|
||||
|
||||
# Extract command
|
||||
extract_parser = subparsers.add_parser('extract', help='Extract codebook from images')
|
||||
extract_parser.add_argument('image_dir', type=str, help='Directory with watermarked images')
|
||||
extract_parser.add_argument('--output', type=str, default='./synthid_codebook.pkl', help='Output path')
|
||||
extract_parser.add_argument('--max-images', type=int, default=250, help='Max images')
|
||||
extract_parser.add_argument('--size', type=int, default=512, help='Image size')
|
||||
|
||||
# Detect command
|
||||
detect_parser = subparsers.add_parser('detect', help='Detect watermark in image')
|
||||
detect_parser.add_argument('image', type=str, help='Image to check')
|
||||
detect_parser.add_argument('--codebook', type=str, default='./synthid_codebook.pkl', help='Codebook path')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.command == 'extract':
|
||||
extract_codebook(args.image_dir, args.output, args.max_images, args.size)
|
||||
elif args.command == 'detect':
|
||||
result = detect_synthid(args.image, args.codebook)
|
||||
print("\nDetection Results:")
|
||||
print(f" Watermarked: {result['is_watermarked']}")
|
||||
print(f" Confidence: {result['confidence']:.4f}")
|
||||
print(f" Correlation: {result['correlation']:.4f}")
|
||||
print(f" Phase Match: {result['phase_match']:.4f}")
|
||||
print(f" Structure Ratio: {result['structure_ratio']:.4f}")
|
||||
else:
|
||||
parser.print_help()
|
||||
Reference in New Issue
Block a user