Groups | Search | Server Info | Keyboard shortcuts | Login | Register [http] [https] [nntp] [nntps]


Groups > comp.lang.python > #197830 > unrolled thread

PSA: Python Pillow was used for image crushing to foil Gemini AI image identification

Started byMaria Sophia <mariasophia@comprehension.com>
First post2026-07-29 09:31 -0700
Last post2026-07-29 22:09 -0700
Articles 3 — 2 participants

Back to article view | Back to comp.lang.python


Contents

  PSA: Python Pillow was used for image crushing to foil Gemini AI image identification Maria Sophia <mariasophia@comprehension.com> - 2026-07-29 09:31 -0700
    Re: PSA: Python Pillow was used for image crushing to foil Gemini AI image identification Lawrence D’Oliveiro <ldo@nz.invalid> - 2026-07-29 23:13 +0000
      Re: PSA: Python Pillow was used for image crushing to foil Gemini AI image identification Maria Sophia <mariasophia@comprehension.com> - 2026-07-29 22:09 -0700

#197830 — PSA: Python Pillow was used for image crushing to foil Gemini AI image identification

FromMaria Sophia <mariasophia@comprehension.com>
Date2026-07-29 09:31 -0700
SubjectPSA: Python Pillow was used for image crushing to foil Gemini AI image identification
Message-ID<114d9tb$1fht$1@nnrp.usenet.blueworldhosting.com>
Python Pillow was used to try to foil AI image detection
https://arstechnica.com/ai/2026/07/tested-google-synthid-works-great-but-labeling-ai-content-may-be-a-losing-game/

I'm surprised it's so hard, but where it matters is for years, 
we've been "destroying" images so that they can't be correlated
by their sensor defects, but if it's this difficult to destroy
the cryptography Google AI adds, that means it's not easy.

C:\> python --version
     Python 3.14.1

C:\> python -m pip --version
     pip 25.3 from C:\app\os\python\Lib\site-packages\pip (python 3.14)

C:\> python -m pip install --upgrade Pillow
     Collecting Pillow
     Downloading pillow-12.3.0-cp314-cp314-win_amd64.whl.metadata (9.3 kB)
     Downloading pillow-12.3.0-cp314-cp314-win_amd64.whl (7.2 MB)
     ---------------------------------------- 7.2/7.2 MB 4.4 MB/s  0:00:01
     Installing collected packages: Pillow
     Successfully installed Pillow-12.3.0

     [notice] A new release of pip is available: 25.3 -> 26.1.2
     [notice] To update, run: python.exe -m pip install --upgrade pip

C:\> python crush.py
     output/
       gen_1.jpg
       gen_2.jpg
       ...
       gen_300.jpg

Where each file is one generation of degradation..

In the article, they repeatedly simulated an image being shared
over the Internet (compression, resizing, cropping, etc.).

And then they tested if Google Gemini SynthID could still identify it:
 https://gemini.google.com (Ask: Check SynthID watermark)

Gemini will respond with one of:
 a. "SynthID watermark detected"
 b. "No SynthID watermark detected"
 c. "This image may have been cropped too much to detect SynthID"
 d. Or it may rate-limit you (the article mentions ~10 checks/day)	.

Here is a working Python script that does the same thing.
Note this script is too aggressive, but that's easy to change.

  # crush.py
  # Use model: python crush.py
  # Simulates image being shared on the net to see if the 
  # AI watermarks can still be found by Gemini testing.
  # ---------------------------------------------------------
  # v1p2 20260729
  #      Changed quality from (10, 40) to (60, 85)
  #      Changed scale from (0.80, 1.00) to (0.95, 1.00)
  #      Changed crop from every 50 generations to every 100
  #      Changed crop amount from 20% to 5%
  #      This makes it to about gen20 before it's unusable.
  # v1p1 20260729
  #      It worked but it barfed when the image got too small
  #      Added a clamp so width/height never drop below 1px
  #      JPEG quality 10-40 is extremely destructive
  #      Resize 80-100% shrinks the image every single generation
  #      Crop 20% every 50 gens removes huge chunks
  #      This makes it to about gen12 before it's unusable.
  # v1p0 20260729
  #      Simulate an image being shared over the Internet
  #      for the purpose of testing if it can be crushed
  #      enough such that it can no longer be identified.
  # ---------------------------------------------------------
  # crush.py
  import random
  import os
  from PIL import Image
  
  INPUT_IMAGE = "input.jpg"
  GENERATIONS = 300
  OUTPUT_DIR = "output"
  
  os.makedirs(OUTPUT_DIR, exist_ok=True)
  
  img = Image.open(INPUT_IMAGE)
  
  for i in range(1, GENERATIONS + 1):
  
      # Random JPEG compression quality between 10 and 40 was too much
      # quality = random.randint(10, 40)
      quality = random.randint(60, 85)
  
      # Random resize factor between 80% and 100% was too much
      # scale = random.uniform(0.80, 1.00)
      scale = random.uniform(0.95, 1.00)
      new_w = max(1, int(img.width * scale))
      new_h = max(1, int(img.height * scale))
      img = img.resize((new_w, new_h), Image.LANCZOS)
  
      # Save this generation
      out_path = f"{OUTPUT_DIR}/gen_{i}.jpg"
      img.save(out_path, "JPEG", quality=quality)
  
      # Reload it to simulate "download -> reupload"
      img = Image.open(out_path)
  
      # Crop every 50 generations was too much
      # if i % 50 == 0:
      if i % 100 == 0:
          w, h = img.size
          if w > 10 and h > 10:
              # Crop amount of 20% was too much
              # crop_amount = int(w * 0.20)
              crop_amount = int(w * 0.05) 
              img = img.crop((crop_amount, crop_amount, w - crop_amount, h - crop_amount))
              img.save(out_path)
  
  print("Done! Check the output/ folder.")
  
  # end of crush.py
--
Privacy is a million technical things, of which most people know about 3.

[toc] | [next] | [standalone]


#197831

FromLawrence D’Oliveiro <ldo@nz.invalid>
Date2026-07-29 23:13 +0000
Message-ID<114e1e9$18m2n$3@dont-email.me>
In reply to#197830
On Wed, 29 Jul 2026 09:31:42 -0700, Maria Sophia wrote:

> Python Pillow was used to try to foil AI image detection
> https://arstechnica.com/ai/2026/07/tested-google-synthid-works-great-but-labeling-ai-content-may-be-a-losing-game/
>
> I'm surprised it's so hard, but where it matters is for years, we've
> been "destroying" images so that they can't be correlated by their
> sensor defects, but if it's this difficult to destroy the
> cryptography Google AI adds, that means it's not easy.

One thing the article didn’t mention, is trying to mix multiple
AI-generated images in various ways, to see what that does to their
respective SynthID markers. And applying various bandwidth-filtering
techniques (because the SynthID marker apparently takes the form of an
“audio waveform”).

[toc] | [prev] | [next] | [standalone]


#197834

FromMaria Sophia <mariasophia@comprehension.com>
Date2026-07-29 22:09 -0700
Message-ID<114emam$1f1r$1@nnrp.usenet.blueworldhosting.com>
In reply to#197831
Lawrence D'Oliveiro wrote:
> On Wed, 29 Jul 2026 09:31:42 -0700, Maria Sophia wrote:
> 
>> Python Pillow was used to try to foil AI image detection
>> https://arstechnica.com/ai/2026/07/tested-google-synthid-works-great-but-labeling-ai-content-may-be-a-losing-game/
>>
>> I'm surprised it's so hard, but where it matters is for years, we've
>> been "destroying" images so that they can't be correlated by their
>> sensor defects, but if it's this difficult to destroy the
>> cryptography Google AI adds, that means it's not easy.
> 
> One thing the article didn't mention, is trying to mix multiple
> AI-generated images in various ways, to see what that does to their
> respective SynthID markers. And applying various bandwidth-filtering
> techniques (because the SynthID marker apparently takes the form of an
> "audio waveform").

Hi Lawrence,

I had never heard of SynthID before this article popped up in my feed
today, so anything I say can be wrong (and likely is wrong), but it seems
to me that your instinct is spot on that if SynthID really behaves like a
kind of embedded "audio-like waveform" distributed across pixel space, then
mixing multiple AI-generated images or applying frequency-domain filtering
is perhaps just the kind of stress test that could reveal how fragile (or
how robust) the watermark actually is. 

The Ars Technica piece we're reading confirms the robustness under
compression, resizing, and moderate cropping, but, as you noted, it doesn't
explore multi-image blending or frequency-domain attacks at all.

If it's genuinely difficult to destroy SynthID without destroying the
image, then watermarking is much more robust than traditional sensor-noise
fingerprinting. 

To their credit, the article makes clear that SynthID is not designed to
resist adversarial attacks and will eventually be bypassed by determined
attackers, of which I am not one, but I did update the script today.

  # crush.py
  # Use model: python crush.py
  # Simulates image being shared on the net to see if the 
  # AI watermarks can still be found by Gemini testing.
  # ---------------------------------------------------------
  # v1p3 20260729
  #      Further slowed degradation to about gen_100 or so
  # v1p2 20260729
  #      It worked but by gen_12 the image was unusable
  #      JPEG quality 10-40 is extremely destructive
  #      Resize 80-100% shrinks the image every single generation
  #      Crop 20% every 50 gens removes huge chunks
  #      Changed quality from (10, 40) to (60, 85)
  #      Changed scale from (0.80, 1.00) to (0.95, 1.00)
  #      Changed crop from every 50 generations to every 100
  #      Changed crop amount from 20% to 5%
  # v1p1 20260729
  #      It worked but it barfed when the image got too small
  #      Added a clamp so width/height never drop below 1px
  # v1p0 20260729
  #      Simulate an image being shared over the Internet
  #      for the purpose of testing if it can be crushed
  #      enough such that it can no longer be identified.
  # ---------------------------------------------------------
  # crush_slow.py
  import random
  import os
  from PIL import Image
  
  INPUT_IMAGE = "input.jpg"
  GENERATIONS = 300
  OUTPUT_DIR = "output"
  
  os.makedirs(OUTPUT_DIR, exist_ok=True)
  
  img = Image.open(INPUT_IMAGE)
  
  for i in range(1, GENERATIONS + 1):
  
      # Very gentle JPEG compression
      quality = random.randint(75, 95)
  
      # Only resize occasionally (every 10 generations)
      if i % 10 == 0:
          scale = random.uniform(0.97, 1.00)
          new_w = max(1, int(img.width * scale))
          new_h = max(1, int(img.height * scale))
          img = img.resize((new_w, new_h), Image.LANCZOS)
  
      # Save this generation
      out_path = f"{OUTPUT_DIR}/gen_{i}.jpg"
      img.save(out_path, "JPEG", quality=quality)
  
      # Reload it
      img = Image.open(out_path)
  
      # Crop very rarely and very lightly
      if i % 100 == 0:
          w, h = img.size
          if w > 40 and h > 40:
              crop_amount = int(w * 0.03)  # 3% crop
              img = img.crop((crop_amount, crop_amount,
                              w - crop_amount, h - crop_amount))
              img.save(out_path)
  
  print("Done! Check the output/ folder.")
  
  # end of crush.py
  # Craiyon, Nano Banana 2 in Gemini, ChatGPG (3 per day)
-- 
Usenet allows people with shared interests to discuss topics of import.

[toc] | [prev] | [standalone]


Back to top | Article view | comp.lang.python


csiph-web