Today, this article showed up in my technical-news feed about using Python 
Pillow to discern the reliability of watermarking in AI-generated images.
https://arstechnica.com/ai/2026/07/tested-google-synthid-works-great-but-labeling-ai-content-may-be-a-losing-game/

As is my wont, I hacked out a Python script on Windows to test their 
methods and I posted that script so that others can try it out too.
 Newsgroups: rec.photo.digital,alt.comp.os.windows-10,comp.lang.python
 Subject: PSA: Python Pillow was used for image crushing to foil Gemini AI 
image identification
 Date: Wed, 29 Jul 2026 09:31:42 -0700
 Message-ID: <[email protected]>

Having never used Python Pillow, and while I was already writing code, 
I decided to try to use it to foil camera sensor PRNU fingerprinting.

To that end, here's a script that others can test out, but I don't
know of a place to upload the original & the scrubbed image to test.

So it's just a guess if this script really scrubs PRNU fingerprints.
How would I know if it worked?

Do you know of a web site that compares two images to tell you if 
both came from the same camera based on the unique PRNU fingerprint?

  # prnu.py
  # Obfuscate camera sensor PRNU fingerprints
  # Place an image called input.jpg in the current directory.
  # Run: python prnu.py 
  # ----------------------------------------------------------------------
  # v1p6 20260729 Added EXIF stripping, tiny noise injection, dual-pass 
scrubbing
  # v1p5 20260729 Brought the margin in a few pixels to handle interpolation
  # v1p4 20260729 Changed to trigonometry to figure out the crop angles
  # v1p3 20260729 Switched to making the rotation triangles transparent
  # v1p2 20260729 Further refined as a mask is needed to remove triangles
  # v1p1 20260729 Refined crop to remove the white rotation edge triangles
  # v1p0 20260729 Original version 
  #      blur, rotate, crop, recompress, resize
  # ----------------------------------------------------------------------
  import math
  import random
  import numpy as np
  from PIL import Image, ImageFilter
  
  INPUT_IMAGE = "input.jpg"
  OUTPUT_IMAGE = "scrubbed.jpg"
  
  def maximal_inner_rect(w, h, angle):
      """
      Compute the largest axis-aligned rectangle inside a rotated rectangle.
      """
      theta = abs(angle)
      if theta == 0:
          return w, h
  
      t = math.radians(theta)
      W = w
      H = h
  
      W_prime = W * math.cos(t) - H * math.sin(t)
      H_prime = H * math.cos(t) - W * math.sin(t)
  
      return int(W_prime), int(H_prime)
  
  def scrub_once(img):
      """
      One full PRNU scrubbing pass:
      blur ¡÷ rotate ¡÷ crop ¡÷ resize ¡÷ noise ¡÷ JPEG recompress
      """
      # Blur to kill PRNU high-frequency noise
      img = img.filter(ImageFilter.GaussianBlur(radius=1.2))
  
      # Random slight rotation
      angle = random.uniform(-2.0, 2.0)
      rotated = img.rotate(angle, expand=True)
  
      # Compute maximal inner rectangle
      W, H = img.size
      crop_w, crop_h = maximal_inner_rect(W, H, angle)
  
      # Safety margin
      margin = 3
      crop_w = max(1, crop_w - 2 * margin)
      crop_h = max(1, crop_h - 2 * margin)
  
      # Center crop
      cx, cy = rotated.size
      left = (cx - crop_w) // 2
      top = (cy - crop_h) // 2
      right = left + crop_w
      bottom = top + crop_h
  
      cropped = rotated.crop((left, top, right, bottom))
  
      # Optional slight resize
      scale = random.uniform(0.97, 1.00)
      new_w = max(1, int(cropped.width * scale))
      new_h = max(1, int(cropped.height * scale))
      resized = cropped.resize((new_w, new_h), Image.LANCZOS)
  
      # Add tiny random noise (¡Ó3)
      arr = np.array(resized).astype(np.int16)
      noise = np.random.randint(-3, 4, arr.shape, dtype=np.int16)
      arr = np.clip(arr + noise, 0, 255).astype(np.uint8)
      resized = Image.fromarray(arr)
  
      return resized
  
  # Load image
  img = Image.open(INPUT_IMAGE).convert("RGB")
  
  # Strip EXIF metadata
  img.info.pop("exif", None)
  
  # First scrubbing pass
  img = scrub_once(img)
  
  # Second scrubbing pass (different random parameters)
  img = scrub_once(img)
  
  # Final JPEG recompression
  quality = random.randint(70, 90)
  img.save(OUTPUT_IMAGE, "JPEG", quality=quality)
  
  print("Saved:", OUTPUT_IMAGE)
  
  # end of prnu.py
  
-- 
Posted out of the goodness of my heart to help others & to learn from them.
-- 
https://mail.python.org/mailman3//lists/python-list.python.org

Reply via email to