Mustafa Akcanca commited on
Commit
8582b96
·
1 Parent(s): 8251321

Migrate app

Browse files
Files changed (1) hide show
  1. src/tools/forensic/frequency_tools.py +114 -12
src/tools/forensic/frequency_tools.py CHANGED
@@ -8,32 +8,134 @@ import json
8
  def analyze_frequency_domain(input_str: str) -> str:
9
  """
10
  Analyze DCT/FFT frequency domain features.
 
 
 
 
 
 
11
  """
12
  image_path = input_str.strip()
13
  try:
14
  import numpy as np
 
15
  from PIL import Image
16
- from scipy import fft
 
17
 
18
  img = Image.open(image_path).convert("RGB")
19
- img_array = np.array(img)
20
 
21
- gray = np.mean(img_array, axis=2).astype(np.float32)
 
 
 
 
22
 
23
- dct_result = fft.dctn(gray, norm="ortho")
24
- dct_energy = np.abs(dct_result)
25
 
26
- fft_result = np.fft.fft2(gray)
27
- fft_magnitude = np.abs(fft_result)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28
 
29
  result = {
30
  "tool": "analyze_frequency_domain",
31
  "status": "completed",
32
- "dct_mean": float(np.mean(dct_energy)),
33
- "dct_std": float(np.std(dct_energy)),
34
- "fft_mean": float(np.mean(fft_magnitude)),
35
- "fft_std": float(np.std(fft_magnitude)),
36
- "note": "Basic frequency domain statistics extracted",
37
  }
38
 
39
  return json.dumps(result)
 
8
  def analyze_frequency_domain(input_str: str) -> str:
9
  """
10
  Analyze DCT/FFT frequency domain features.
11
+
12
+ Extracts comprehensive frequency domain features including:
13
+ - DCT coefficient statistics (mean, std)
14
+ - FFT radial profile statistics (mean, std, decay rate)
15
+ - Frequency band energies (low, mid, high)
16
+ - Peakiness metric for detecting upsampling artifacts
17
  """
18
  image_path = input_str.strip()
19
  try:
20
  import numpy as np
21
+ import cv2
22
  from PIL import Image
23
+ from scipy.fftpack import dct, fft2, fftshift
24
+ from scipy import stats
25
 
26
  img = Image.open(image_path).convert("RGB")
27
+ image = np.array(img)
28
 
29
+ # Convert to grayscale using cv2 (matches frequency.py)
30
+ if len(image.shape) == 3:
31
+ gray = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY)
32
+ else:
33
+ gray = image
34
 
35
+ features = {}
 
36
 
37
+ # --- DCT Features ---
38
+ # Compute 2D DCT (orthonormal normalization)
39
+ dct_map = dct(dct(gray.T, norm='ortho').T, norm='ortho')
40
+
41
+ # Histogram of DCT coefficients (excluding DC component)
42
+ dct_coeffs = dct_map.flatten()
43
+ dct_coeffs = dct_coeffs[1:] # Remove DC component
44
+
45
+ # Statistics on DCT coefficients (using absolute values)
46
+ dct_abs = np.abs(dct_coeffs)
47
+ features['dct_mean'] = float(np.mean(dct_abs))
48
+ features['dct_std'] = float(np.std(dct_abs))
49
+
50
+ # --- FFT Features ---
51
+ # Compute 2D FFT and shift to center DC component
52
+ f = fft2(gray.astype(np.float64)) # Use float64 for better precision
53
+ fshift = fftshift(f)
54
+
55
+ # Use log-magnitude spectrum (20*log10) for consistent normalization
56
+ # This matches the noiseprint extractor and provides better dynamic range
57
+ magnitude_spectrum = 20 * np.log10(np.abs(fshift) + 1e-10)
58
+
59
+ # Azimuthal average (Radial Profile)
60
+ # This computes the average magnitude at each radial distance from center
61
+ h, w = magnitude_spectrum.shape
62
+ cy, cx = h // 2, w // 2
63
+ y, x = np.ogrid[-cy:h-cy, -cx:w-cx]
64
+ r = np.sqrt(x**2 + y**2)
65
+ r = r.astype(int)
66
+
67
+ # Compute radial profile: average magnitude at each radius
68
+ tbin = np.bincount(r.ravel(), magnitude_spectrum.ravel())
69
+ nr = np.bincount(r.ravel())
70
+ radial_profile = tbin / np.maximum(nr, 1)
71
+
72
+ # Remove zero-radius (DC component) for better statistics
73
+ if len(radial_profile) > 1:
74
+ radial_profile_nonzero = radial_profile[1:]
75
+ else:
76
+ radial_profile_nonzero = radial_profile
77
+
78
+ # Summary stats of radial profile
79
+ features['fft_radial_mean'] = float(np.mean(radial_profile_nonzero))
80
+ features['fft_radial_std'] = float(np.std(radial_profile_nonzero))
81
+
82
+ # Improved radial decay metric: fit linear slope instead of assuming monotonic decay
83
+ # This is more robust to non-monotonic profiles (e.g., peaks at intermediate frequencies)
84
+ n = len(radial_profile_nonzero)
85
+ if n >= 3:
86
+ # Fit linear regression to log(radius) vs magnitude to estimate decay rate
87
+ # This captures the overall trend without assuming monotonicity
88
+ radii = np.arange(1, n + 1, dtype=np.float64)
89
+ # Use log(radius) to better capture power-law decay
90
+ log_radii = np.log(radii + 1e-10)
91
+
92
+ # Fit linear model: magnitude = a * log(radius) + b
93
+ # Negative slope indicates decay (typical for natural images)
94
+ # Positive slope indicates high-frequency emphasis (typical for upsampled images)
95
+ try:
96
+ slope, intercept, r_value, p_value, std_err = stats.linregress(
97
+ log_radii, radial_profile_nonzero
98
+ )
99
+ features['fft_radial_decay'] = float(slope) # Decay rate (negative = decay)
100
+ features['fft_radial_decay_r2'] = float(r_value**2) # Goodness of fit
101
+ except:
102
+ # Fallback: simple difference if regression fails
103
+ features['fft_radial_decay'] = float(
104
+ radial_profile_nonzero[0] - radial_profile_nonzero[-1]
105
+ )
106
+ features['fft_radial_decay_r2'] = 0.0
107
+ else:
108
+ features['fft_radial_decay'] = 0.0
109
+ features['fft_radial_decay_r2'] = 0.0
110
+
111
+ # Frequency band energies (low, mid, high)
112
+ if n >= 9: # Ensure enough samples for band division
113
+ # Divide radial profile into 3 bands: low, mid, high frequency
114
+ edges = np.linspace(0, n, 4, dtype=int) # 3 bands
115
+ low_band = radial_profile_nonzero[edges[0]:edges[1]]
116
+ mid_band = radial_profile_nonzero[edges[1]:edges[2]]
117
+ high_band = radial_profile_nonzero[edges[2]:edges[3]]
118
+
119
+ features['fft_low_energy'] = float(np.mean(low_band))
120
+ features['fft_mid_energy'] = float(np.mean(mid_band))
121
+ features['fft_high_energy'] = float(np.mean(high_band))
122
+
123
+ # Peakiness: ratio of max to mean (detects sharp peaks from upsampling)
124
+ # High peakiness indicates periodic patterns (upsampling artifacts)
125
+ profile_mean = np.mean(radial_profile_nonzero)
126
+ profile_max = np.max(radial_profile_nonzero)
127
+ features['fft_peakiness'] = float(profile_max / (profile_mean + 1e-10))
128
+ else:
129
+ # Not enough samples for band analysis
130
+ features['fft_low_energy'] = 0.0
131
+ features['fft_mid_energy'] = 0.0
132
+ features['fft_high_energy'] = 0.0
133
+ features['fft_peakiness'] = 0.0
134
 
135
  result = {
136
  "tool": "analyze_frequency_domain",
137
  "status": "completed",
138
+ **features,
 
 
 
 
139
  }
140
 
141
  return json.dumps(result)