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

    From Maria Sophia@mariasophia@comprehension.com to rec.photo.digital,alt.comp.os.windows-10,comp.lang.python on Wed Jul 29 09:31:42 2026
    From Newsgroup: alt.comp.os.windows-10

    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.

    python --version
    Python 3.14.1

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

    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

    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.
    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From Maria Sophia@mariasophia@comprehension.com to rec.photo.digital,alt.comp.os.windows-10 on Wed Jul 29 22:13:00 2026
    From Newsgroup: alt.comp.os.windows-10

    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.
    # ---------------------------------------------------------
    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.
    --- Synchronet 3.22a-Linux NewsLink 1.2