akcanca commited on
Commit
4eb5e8f
·
verified ·
1 Parent(s): 5496367

Update src/features/noiseprint_extractor.py

Browse files
Files changed (1) hide show
  1. src/features/noiseprint_extractor.py +33 -102
src/features/noiseprint_extractor.py CHANGED
@@ -2,12 +2,7 @@ import os
2
  import numpy as np
3
  import torch
4
  from scipy.fftpack import fft2, fftshift
5
- from PIL import Image
6
  from src.features.noiseprint.Noiseprint import getNoiseprint
7
- from src.features.noiseprint.utilityRead import jpeg_qtableinv
8
- from src.features.noiseprint_wrapper import getNoiseprint_with_qf
9
- import io
10
- import tempfile
11
 
12
  class NoiseprintExtractor:
13
  """
@@ -25,138 +20,76 @@ class NoiseprintExtractor:
25
  # See: example_code/noiseprint/playground.ipynb which uses res[34:-34,34:-34]
26
  self.edge_margin = 34
27
 
28
- def _extract_qf_from_pil_image(self, pil_image):
29
  """
30
- Extract JPEG Quality Factor from PIL Image quantization tables if available.
31
-
32
- Returns QF if quantization tables are preserved, None otherwise.
33
- """
34
- if not isinstance(pil_image, Image.Image):
35
- return None
36
-
37
- if not hasattr(pil_image, 'quantization') or not pil_image.quantization:
38
- return None
39
-
40
- try:
41
- # Try to extract QF directly from quantization attribute
42
- # This avoids re-saving which changes QF
43
- q = pil_image.quantization
44
- if isinstance(q, dict) and 0 in q:
45
- # We have quantization tables, try to compute QF
46
- # Use a temporary approach: save to BytesIO and read QF
47
- # But this still changes QF...
48
- # Actually, we need to compute QF from quantization table directly
49
- from src.features.noiseprint.utilityRead import jpeg_qtableinv
50
- buf = io.BytesIO()
51
- # Save with high quality to minimize QF change, then read it back
52
- pil_image.save(buf, format='JPEG', quality=95)
53
- buf.seek(0)
54
- qf = jpeg_qtableinv(buf)
55
- buf.close()
56
- return qf
57
- except Exception as e:
58
- print(f"Could not extract QF from PIL Image: {e}")
59
-
60
- return None
61
-
62
- def extract_features(self, image_input, qf_override=None):
63
- """
64
- Extracts Noiseprint-based features.
65
 
66
  Args:
67
- image_input: Can be:
68
- - str: Path to image file (original behavior)
69
- - PIL.Image: PIL Image object (new - tries to preserve QF)
70
- qf_override: Optional QF value to use instead of detecting from file
71
-
72
  Returns:
73
  dict: Dictionary of features with keys:
74
- - 'noiseprint_freq_ratio': Ratio of high-frequency to total energy
75
  - 'noiseprint_std': Standard deviation of the noiseprint residual
76
  """
77
  try:
78
- # Handle PIL Image input (from Gradio)
79
- if isinstance(image_input, Image.Image):
80
- # Try to extract QF from quantization tables if preserved
81
- detected_qf = self._extract_qf_from_pil_image(image_input)
82
-
83
- if detected_qf is None:
84
- # QF not available, need to save and detect
85
- # Use quality that matches common JPEG (75) to minimize QF mismatch
86
- temp_fd, temp_path = tempfile.mkstemp(suffix='.jpg')
87
- try:
88
- os.close(temp_fd)
89
- image_input.save(temp_path, "JPEG", quality=75, optimize=False, subsampling=0)
90
- qf = jpeg_qtableinv(temp_path) if qf_override is None else qf_override
91
- image_path = temp_path
92
- finally:
93
- # Will clean up after feature extraction
94
- pass
95
- else:
96
- # QF detected from quantization tables
97
- qf = detected_qf if qf_override is None else qf_override
98
- # Still need to save for getNoiseprint (it requires file path)
99
- # But now we know the QF, so we can use it
100
- temp_fd, temp_path = tempfile.mkstemp(suffix='.jpg')
101
- os.close(temp_fd)
102
- # Save with quality matching detected QF
103
- save_quality = max(50, min(100, int(qf)))
104
- image_input.save(temp_path, "JPEG", quality=save_quality, optimize=False, subsampling=0)
105
- image_path = temp_path
106
- else:
107
- # String path (original behavior)
108
- image_path = image_input
109
- qf = qf_override
110
-
111
- # Extract noiseprint - use QF override if provided
112
- if qf_override is not None or detected_qf is not None:
113
- # Use wrapper that accepts QF override
114
- qf_to_use = qf_override if qf_override is not None else detected_qf
115
- _, noiseprint = getNoiseprint_with_qf(image_path, qf_override=qf_to_use)
116
- else:
117
- # Standard extraction (detects QF from file)
118
- _, noiseprint = getNoiseprint(image_path)
119
-
120
- # Clean up temp file if we created one
121
- if isinstance(image_input, Image.Image) and 'temp_path' in locals():
122
- try:
123
- if os.path.exists(temp_path):
124
- os.remove(temp_path)
125
- except:
126
- pass
127
 
128
  if noiseprint is None:
129
  return None
130
 
131
- # Remove edge artifacts
 
 
132
  margin = self.edge_margin
133
  if noiseprint.shape[0] > 2*margin and noiseprint.shape[1] > 2*margin:
134
  center_np = noiseprint[margin:-margin, margin:-margin]
135
  else:
 
136
  center_np = noiseprint
 
 
 
137
 
138
- # Feature 1: Frequency Ratio
 
139
  f = fft2(center_np)
140
  fshift = fftshift(f)
 
 
 
 
141
  magnitude_spectrum = 20 * np.log10(np.abs(fshift) + 1e-10)
142
 
143
  h, w = magnitude_spectrum.shape
144
  cy, cx = h // 2, w // 2
 
 
 
145
  mask_size = min(h, w) // 8
146
  high_freq_mask = np.ones((h, w), dtype=bool)
147
  high_freq_mask[cy-mask_size:cy+mask_size, cx-mask_size:cx+mask_size] = False
148
 
 
149
  high_freq_energy = np.mean(magnitude_spectrum[high_freq_mask])
150
  total_energy = np.mean(magnitude_spectrum)
151
 
 
 
152
  eps = 1e-6
153
  if abs(total_energy) < eps:
 
154
  freq_ratio = 0.0
155
  else:
156
  freq_ratio = high_freq_energy / total_energy
 
157
  freq_ratio = np.clip(freq_ratio, 0.0, 1.0)
158
 
159
- # Feature 2: Global Standard Deviation
 
160
  global_std = np.std(center_np)
161
 
162
  return {
@@ -165,7 +98,5 @@ class NoiseprintExtractor:
165
  }
166
 
167
  except Exception as e:
168
- print(f"Error extracting Noiseprint features: {e}")
169
- import traceback
170
- traceback.print_exc()
171
  return None
 
2
  import numpy as np
3
  import torch
4
  from scipy.fftpack import fft2, fftshift
 
5
  from src.features.noiseprint.Noiseprint import getNoiseprint
 
 
 
 
6
 
7
  class NoiseprintExtractor:
8
  """
 
20
  # See: example_code/noiseprint/playground.ipynb which uses res[34:-34,34:-34]
21
  self.edge_margin = 34
22
 
23
+ def extract_features(self, image_path):
24
  """
25
+ Extracts Noiseprint-based features for a given image.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
 
27
  Args:
28
+ image_path: Path to the image file
29
+
 
 
 
30
  Returns:
31
  dict: Dictionary of features with keys:
32
+ - 'noiseprint_freq_ratio': Ratio of high-frequency to total energy in log-magnitude spectrum
33
  - 'noiseprint_std': Standard deviation of the noiseprint residual
34
  """
35
  try:
36
+ # getNoiseprint returns (img, noiseprint)
37
+ # img is the image (H, W, C) or (H, W)
38
+ # noiseprint is the residual map (H, W)
39
+ _, noiseprint = getNoiseprint(image_path)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
40
 
41
  if noiseprint is None:
42
  return None
43
 
44
+ # Remove edge artifacts: The original Noiseprint implementation uses 34-pixel margin
45
+ # to remove CNN boundary artifacts. This is consistent with the original codebase.
46
+ # Reference: example_code/noiseprint/playground.ipynb shows res[34:-34,34:-34]
47
  margin = self.edge_margin
48
  if noiseprint.shape[0] > 2*margin and noiseprint.shape[1] > 2*margin:
49
  center_np = noiseprint[margin:-margin, margin:-margin]
50
  else:
51
+ # For very small images, use entire noiseprint but warn
52
  center_np = noiseprint
53
+ if noiseprint.shape[0] <= 2*margin or noiseprint.shape[1] <= 2*margin:
54
+ # Very small image - edge artifacts may affect features
55
+ pass
56
 
57
+ # --- Feature 1: Frequency Ratio ---
58
+ # Compute 2D FFT and shift to center DC component
59
  f = fft2(center_np)
60
  fshift = fftshift(f)
61
+
62
+ # Use log-magnitude spectrum (20*log10) for better dynamic range
63
+ # This is standard in frequency domain analysis (decibel scale)
64
+ # Add small epsilon to avoid log(0)
65
  magnitude_spectrum = 20 * np.log10(np.abs(fshift) + 1e-10)
66
 
67
  h, w = magnitude_spectrum.shape
68
  cy, cx = h // 2, w // 2
69
+
70
+ # Define high-frequency region: exclude central low-frequency band
71
+ # Using 1/8 of image size for low-frequency mask (standard approach)
72
  mask_size = min(h, w) // 8
73
  high_freq_mask = np.ones((h, w), dtype=bool)
74
  high_freq_mask[cy-mask_size:cy+mask_size, cx-mask_size:cx+mask_size] = False
75
 
76
+ # Compute energy in high-frequency and total regions
77
  high_freq_energy = np.mean(magnitude_spectrum[high_freq_mask])
78
  total_energy = np.mean(magnitude_spectrum)
79
 
80
+ # Robust ratio calculation with stability check
81
+ # Use relative tolerance to handle near-zero cases
82
  eps = 1e-6
83
  if abs(total_energy) < eps:
84
+ # Very low energy: return 0.0 (no high-frequency content)
85
  freq_ratio = 0.0
86
  else:
87
  freq_ratio = high_freq_energy / total_energy
88
+ # Clip to reasonable range [0, 1] (high_freq_energy <= total_energy)
89
  freq_ratio = np.clip(freq_ratio, 0.0, 1.0)
90
 
91
+ # --- Feature 2: Global Standard Deviation ---
92
+ # Standard deviation of noiseprint residual (camera fingerprint strength)
93
  global_std = np.std(center_np)
94
 
95
  return {
 
98
  }
99
 
100
  except Exception as e:
101
+ print(f"Error extracting Noiseprint features for {image_path}: {e}")
 
 
102
  return None