Fix multiple bugs in extraction pipeline

1. bypass_v2() ignores iterations parameter — the function accepted
   `iterations` but ran the transform pipeline only once. Now properly
   loops, with diminishing strength on subsequent iterations.

2. denoise_bilateral() has identical if/else branches — both 2D and 3D
   cases called the same cv2.bilateralFilter(). Removed dead branch.

3. apply_noise_replacement() allows negative sigma — with passes > 5,
   the formula `sigma * (1 - i * 0.2)` produces negative values. Added
   clamping and early break.

4. Broken import paths — synthid_bypass.py and watermark_remover.py
   used bare module imports that fail when scripts are run from outside
   their directory. Added sys.path.insert like benchmark_extraction.py.

5. Misleading "Python 3.14 bug" comment — the SSIM gate was disabled
   with a comment blaming Python 3.14, but the real reason is that
   heavy multi-pass transforms naturally depress SSIM. Updated comment.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Test User
2026-04-10 10:49:22 +08:00
co-authored by Claude Opus 4.6
parent 7d4dc9a11b
commit 49a7cfc9d8
2 changed files with 55 additions and 45 deletions
+52 -45
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,12 +122,8 @@ 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
def denoise_nlm( def denoise_nlm(
@@ -183,12 +183,14 @@ class SynthIDBypass:
Similar to multiple KSampler passes in the diffusion workflow. Similar to multiple KSampler passes in the diffusion workflow.
""" """
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
# ================================================================ # ================================================================
@@ -1247,40 +1249,45 @@ class SynthIDBypass:
current = img_f.copy() current = img_f.copy()
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)
# Stage 1: Spatial disruption — only in 'maximum' mode iter_jpeg_q = min(95, params['jpeg_q'] + iteration * 5)
# (causes significant pixel misalignment affecting SSIM, but
# targets SynthID's weakest category at 52% TPR worst-case) # Pass through transform categories
if strength == 'maximum': # Each attacks a different dimension of the watermark embedding
current = self._spatial_disruption(current, strength=s)
stages_applied.append('spatial') # Stage 1: Spatial disruption — only in 'maximum' mode
# (causes significant pixel misalignment affecting SSIM, but
# Stage 2: Quality degradation (JPEG/WebP/resize cycling) # targets SynthID's weakest category at 52% TPR worst-case)
current = self._quality_degradation( if strength == 'maximum':
current, jpeg_quality=params['jpeg_q'], strength=s current = self._spatial_disruption(current, strength=iter_s)
) stages_applied.append(f'spatial_{iteration}')
stages_applied.append('quality')
# Stage 2: Quality degradation (JPEG/WebP/resize cycling)
# Stage 3: Noise injection + denoising current = self._quality_degradation(
current = self._noise_disruption( current, jpeg_quality=iter_jpeg_q, strength=iter_s
current, sigma=params['noise_sigma'], strength=s )
) stages_applied.append(f'quality_{iteration}')
stages_applied.append('noise')
# Stage 3: Noise injection + denoising
# Stage 4: Color manipulation current = self._noise_disruption(
current = self._color_disruption(current, strength=s) current, sigma=params['noise_sigma'], strength=iter_s
stages_applied.append('color') )
stages_applied.append(f'noise_{iteration}')
# Stage 5: Overlay disruption
current = self._overlay_disruption(current, strength=s) # Stage 4: Color manipulation
stages_applied.append('overlay') current = self._color_disruption(current, strength=iter_s)
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)
# Quantize to uint8 for consistent quality metrics # Quantize to uint8 for consistent quality metrics
cleaned_uint8 = (current * 255).clip(0, 255).astype(np.uint8) cleaned_uint8 = (current * 255).clip(0, 255).astype(np.uint8)
original_uint8 = (img_f * 255).clip(0, 255).astype(np.uint8) original_uint8 = (img_f * 255).clip(0, 255).astype(np.uint8)
@@ -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: