Merge pull request #13 from hobostay/fix/multiple-bugs

Fix multiple bugs in extraction pipeline
This commit is contained in:
Alosh Denny
2026-04-10 09:32:15 +05:30
committed by GitHub
2 changed files with 55 additions and 45 deletions
+41 -34
View File
@@ -17,11 +17,15 @@ Based on insights from:
""" """
import os import os
import sys
import io import io
import numpy as np import numpy as np
import cv2 import cv2
from scipy.fft import fft2, ifft2, fftshift, ifftshift from scipy.fft import fft2, ifft2, fftshift, ifftshift
from scipy import ndimage from scipy import ndimage
# Ensure same-directory modules are importable regardless of cwd
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from typing import Optional, Dict, List, Tuple from typing import Optional, Dict, List, Tuple
from dataclasses import dataclass from dataclasses import dataclass
from PIL import Image from PIL import Image
@@ -118,11 +122,7 @@ class SynthIDBypass:
) -> np.ndarray: ) -> np.ndarray:
"""Edge-preserving bilateral filter denoising.""" """Edge-preserving bilateral filter denoising."""
img_uint8 = (image * 255).clip(0, 255).astype(np.uint8) img_uint8 = (image * 255).clip(0, 255).astype(np.uint8)
denoised = cv2.bilateralFilter(img_uint8, d, sigma_color, sigma_space)
if len(image.shape) == 3:
denoised = cv2.bilateralFilter(img_uint8, d, sigma_color, sigma_space)
else:
denoised = cv2.bilateralFilter(img_uint8, d, sigma_color, sigma_space)
return denoised.astype(np.float32) / 255.0 return denoised.astype(np.float32) / 255.0
@@ -185,8 +185,10 @@ class SynthIDBypass:
current = image.copy() current = image.copy()
for i in range(passes): for i in range(passes):
# Decrease noise sigma slightly each pass # Decrease noise sigma slightly each pass, clamp to avoid negative
sigma = noise_sigma * (1 - i * 0.2) sigma = noise_sigma * max(0, 1 - i * 0.2)
if sigma <= 0:
break
current = self.noise_replacement_pass(current, noise_sigma=sigma) current = self.noise_replacement_pass(current, noise_sigma=sigma)
return current return current
@@ -1248,35 +1250,40 @@ class SynthIDBypass:
stages_applied = [] stages_applied = []
s = params['base'] s = params['base']
# Single pass through transform categories for iteration in range(iterations):
# Each attacks a different dimension of the watermark embedding # Diminish strength slightly on subsequent iterations
iter_s = s * max(0.5, 1.0 - iteration * 0.15)
iter_jpeg_q = min(95, params['jpeg_q'] + iteration * 5)
# Stage 1: Spatial disruption — only in 'maximum' mode # Pass through transform categories
# (causes significant pixel misalignment affecting SSIM, but # Each attacks a different dimension of the watermark embedding
# targets SynthID's weakest category at 52% TPR worst-case)
if strength == 'maximum':
current = self._spatial_disruption(current, strength=s)
stages_applied.append('spatial')
# Stage 2: Quality degradation (JPEG/WebP/resize cycling) # Stage 1: Spatial disruption — only in 'maximum' mode
current = self._quality_degradation( # (causes significant pixel misalignment affecting SSIM, but
current, jpeg_quality=params['jpeg_q'], strength=s # targets SynthID's weakest category at 52% TPR worst-case)
) if strength == 'maximum':
stages_applied.append('quality') current = self._spatial_disruption(current, strength=iter_s)
stages_applied.append(f'spatial_{iteration}')
# Stage 3: Noise injection + denoising # Stage 2: Quality degradation (JPEG/WebP/resize cycling)
current = self._noise_disruption( current = self._quality_degradation(
current, sigma=params['noise_sigma'], strength=s current, jpeg_quality=iter_jpeg_q, strength=iter_s
) )
stages_applied.append('noise') stages_applied.append(f'quality_{iteration}')
# Stage 4: Color manipulation # Stage 3: Noise injection + denoising
current = self._color_disruption(current, strength=s) current = self._noise_disruption(
stages_applied.append('color') current, sigma=params['noise_sigma'], strength=iter_s
)
stages_applied.append(f'noise_{iteration}')
# Stage 5: Overlay disruption # Stage 4: Color manipulation
current = self._overlay_disruption(current, strength=s) current = self._color_disruption(current, strength=iter_s)
stages_applied.append('overlay') stages_applied.append(f'color_{iteration}')
# Stage 5: Overlay disruption
current = self._overlay_disruption(current, strength=iter_s)
stages_applied.append(f'overlay_{iteration}')
# Clamp output to valid [0,1] range # Clamp output to valid [0,1] range
current = np.clip(current, 0, 1) current = np.clip(current, 0, 1)
@@ -1321,9 +1328,9 @@ class SynthIDBypass:
} }
# Determine success # Determine success
# Note: Internal SSIM computation is bugged on Python 3.14 (returns ~0) # Rely on PSNR > 28 dB as primary quality gate; SSIM is computed
# while external computation is correct. We rely on PSNR > 28 dB which # for reporting but heavy multi-pass transforms can depress it below
# strongly correlates with SSIM > 0.90 for these types of distortions. # the 0.90 threshold even when visual quality is acceptable.
success = psnr > 28 success = psnr > 28
if detection_before and detection_after: if detection_before and detection_after:
conf_drop = detection_before['confidence'] - detection_after['confidence'] conf_drop = detection_before['confidence'] - detection_after['confidence']
+3
View File
@@ -23,6 +23,9 @@ from scipy.ndimage import zoom
from dataclasses import dataclass, field from dataclasses import dataclass, field
from typing import Optional, Dict, Tuple from typing import Optional, Dict, Tuple
# Ensure same-directory modules are importable regardless of cwd
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
@dataclass @dataclass
class RemovalResult: class RemovalResult: