• PSA Using Python Pillow to foil camera image PRNU fingerprinting

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

    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: <114d9tb$1fht$1@nnrp.usenet.blueworldhosting.com>

    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 i= rotate i= crop i= resize i= noise i= 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 (i+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.
    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From Lawrence =?iso-8859-13?q?D=FFOliveiro?=@ldo@nz.invalid to rec.photo.digital,comp.lang.python on Thu Jul 30 08:06:00 2026
    From Newsgroup: rec.photo.digital

    On Wed, 29 Jul 2026 22:22:37 -0700, Maria Sophia wrote:

    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.

    This was the first I had heard of such a thing, so I looked it up <https://en.wikipedia.org/wiki/Photo_response_non-uniformity>.

    ThatrCOs just an artifact of the way image sensors are made, so itrCOs not deliberately designed to be a secret identification feature or
    anything; the article even says that it is possible to characterize
    the noise pattern for a given sensor, and subtract it out to produce higher-quality images, e.g. for metrology purposes.
    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From Maria Sophia@mariasophia@comprehension.com to rec.photo.digital,comp.lang.python on Thu Jul 30 07:42:33 2026
    From Newsgroup: rec.photo.digital

    Lawrence D'Oliveiro wrote:
    On Wed, 29 Jul 2026 22:22:37 -0700, Maria Sophia wrote:

    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.

    This was the first I had heard of such a thing, so I looked it up <https://en.wikipedia.org/wiki/Photo_response_non-uniformity>.

    That's just an artifact of the way image sensors are made, so it's not deliberately designed to be a secret identification feature or
    anything; the article even says that it is possible to characterize
    the noise pattern for a given sensor, and subtract it out to produce higher-quality images, e.g. for metrology purposes.

    Hi Lawrence,

    Thank you for digging into why those who care about privacy should know
    about PRNU fingerprinting which can identify the camera that took a photo.

    Therefore, PRNU fingerprinting can be exploited for identification.

    For example, imagine three photos:
    a. Photo A is posted on Facebook
    b. Photo B is posted on LinkedIn
    c. Photo C is posted on Usenet
    Even though the images may appear wholly unrelated, forensic analysts can extract the PRNU pattern (that sensor fingerprint discussed in the
    wikipedia article you referenced) to potentially determine, sans doubt,
    that all three were almost certainly taken with the same physical camera.

    You're absolutely right that PRNU is an unintended manufacturing artifact
    and not a deliberate tracking feature, but it's there, and it can be used
    to correlate unrelated images (and almost certainly is used, en masse).

    I no longer have TS/SCI clearance, but I'm well aware that scraping tools "can" assemble all the images on any set of sources to identify the camera.

    A key rule in cyber security is not only to protect against what you think "they" are doing, but what you know they "can" do, if they want to do it.

    Especially when it appears to be as trivial to foil image fingerprinting as
    it is to foil, oh, say, radio AP, web browser and network fingerprinting.

    With respect to the Pillow imaging tools, the part I'm experimenting with
    is how best to easily break that PRNU fingerprint with image manipulation.

    Right now, the PRNU-scrubbing script pipeline is running two full
    destructive passes, each intended to disrupt PRNU by performing
    a. blur
    b. rotate
    c. crop
    d. resize
    e. inject noise
    But that doubly scrubbed image is a bit to destroyed to be usable.

    To that end, here is a gentler version of the previous script for testing.
    # prnu.py
    # Gentle obfuscation of camera sensor PRNU fingerprints
    # ---------------------------------------------------------------------
    # 1. Place an image called input.jpg in the current directory.
    # 2. Run: python prnu.py
    # 3. The result is a scrubbed.jpg with reduced PRNU correlation.
    # ---------------------------------------------------------------------
    # This script applies a series of lightweight image transformations
    # that aim to disrupt PRNU correlation while preserving visual quality:
    # a. light Gaussian blur (removes high-frequency PRNU components)
    # b. tiny random rotation (breaks pixel-to-sensor alignment)
    # c. minimal center crop (removes interpolation triangles)
    # d. micro-resize (adds slight resampling noise)
    # e. tiny random pixel noise (destroys residual PRNU structure)
    # f. EXIF stripping (removes camera metadata)
    # g. gentle JPEG recompression (further decorrelates noise)
    # ---------------------------------------------------------------------
    # PRNU (Photo Response Non-Uniformity) is a subtle, sensor-specific
    # noise pattern present in every digital photograph. Forensic tools
    # can correlate PRNU across images posted to the Internet to easily
    # correlate whether they were taken by the same physical camera.
    # ---------------------------------------------------------------------
    # v1p7 20260730 Switched to a single gentler set of scrubbing parameters
    # v1p6 20260729 Added EXIF stripping, 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):
    """
    Gentle PRNU scrubbing:
    light blur > tiny rotation > minimal crop > micro-resize > tiny noise
    """
    # Very light blur
    img = img.filter(ImageFilter.GaussianBlur(radius=0.4))

    # Tiny rotation
    angle = random.uniform(-0.6, 0.6)
    rotated = img.rotate(angle, expand=True)

    # Compute maximal inner rectangle
    W, H = img.size
    crop_w, crop_h = maximal_inner_rect(W, H, angle)

    # Very small safety margin
    margin = 1
    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 micro-resize
    scale = random.uniform(0.995, 1.0)
    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)

    # Very tiny noise
    arr = np.array(resized).astype(np.int16)
    noise = np.random.randint(-1, 2, 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)

    # Single gentle scrubbing pass
    img = scrub_once(img)

    # Final JPEG recompression (gentle)
    img.save(OUTPUT_IMAGE, "JPEG", quality=94)

    print("Saved:", OUTPUT_IMAGE)

    # end of prnu.py
    --
    Hygiene keeps the body clean while myriad privacy habits keep the data clean. --- Synchronet 3.22a-Linux NewsLink 1.2
  • From Piergiorgio Sartor@piergiorgio.sartor.this.should.not.be.used@nexgo.REMOVETHIS.de to rec.photo.digital,comp.lang.python on Fri Jul 31 09:35:21 2026
    From Newsgroup: rec.photo.digital

    On 30/07/2026 17.42, Maria Sophia wrote:
    [...]
    Right now, the PRNU-scrubbing script pipeline is running two full
    destructive passes, each intended to disrupt PRNU by performing
    a. blur
    b. rotate
    c. crop
    d. resize
    e. inject noise
    But that doubly scrubbed image is a bit to destroyed to be usable.
    Have you tried to de-noise first, with
    something like BM3D, and then re-noise?

    For both operations likely some noise
    parameter is required.

    I know this is not the same as the PRNU,
    but de-noising will reduce / remove any
    uncorrelated information.

    Re-noise will make it plausible.

    bye,
    --

    piergiorgio
    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From Maria Sophia@mariasophia@comprehension.com to rec.photo.digital,comp.lang.python,alt.comp.os.windows-10 on Fri Jul 31 13:59:58 2026
    From Newsgroup: rec.photo.digital

    Piergiorgio Sartor wrote:
    Have you tried to de-noise first, with
    something like BM3D, and then re-noise?

    For both operations likely some noise
    parameter is required.

    I know this is not the same as the PRNU,
    but de-noising will reduce / remove any
    uncorrelated information.

    Re-noise will make it plausible.

    Thank you for the suggestion of BM3D denoise-renoise sequencing to reduce
    PRNU fingerprinting of images posted to various Internet locations.

    A digital camera sensor has millions of pixels.
    Each pixel is supposed to respond the same way to light, but, in reality, every pixel response is slightly different in a way unique to each sensor.

    I never heard of BM3D denoising/renoising until you mentioned itt, so
    looking it up, BM3D denoising + re-noising is apparently wonderful for
    removing random noise, but PRNU isn't random noise.

    Each pixel multiplies the incoming light by its own personal gain factor.
    So the imperfection is baked into the signal allowing it to partially
    survive much of our attempts at denoising, sharpening and compression

    Apparently, BM3D will reduce some PRNU energy, but it may not reliably
    destroy the correlation because PRNU is tied to pixel-level sensitivity variations, not additive noise. Re-noising afterward doesn't help much, apparently, because it doesn't greatly change the underlying
    sensor-specific gain pattern.

    Unfortunately for us, much of the forensic literature I've seen shows that
    PRNU survives denoising, sharpening, resizing, and even moderate JPEG compression.

    Given that, the main reliable ways to break PRNU correlation seem to be:
    a. geometric misalignment (rotation, crop, resample)
    b. strong blur or downsampling (which damages the image, unfortunately)
    c. pixel-level perturbations (we have to change the gain, per pixel)
    d. multiple rounds of resampling (we have to randomize this per pixel)

    So far, the attached script focuses on slightly decorrelating the pixel
    grid from the sensor grid rather than trying to "wash" the noise.

    But we don't know if it works unless we subject it to a PRNU test.

    If anyone knows of a public PRNU-matching site, I'd love to test it, but as
    far as I know, all the real PRNU matchers (Amped Authenticate,
    Forensically, DHS tools, academic implementations) require uploading a
    camera reference set, not just a single set of two arbitrary images .
    --
    Privacy isn't something we get by accident. It's in everything that we do.
    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From Maria Sophia@mariasophia@comprehension.com to rec.photo.digital,comp.lang.python,alt.comp.os.windows-10 on Fri Jul 31 14:10:09 2026
    From Newsgroup: rec.photo.digital

    Lawrence D'Oliveiro wrote:
    With respect to the Pillow imaging tools, the part I'm experimenting
    with is how best to easily break that PRNU fingerprint with image
    manipulation.

    You mean in the general case, for an image obtained from an unknown
    camera?

    Because if you only want to do it for your own camera(s), you can do
    what the Wikipedia article describes, and use a calibration image to determine the sensor "fingerprint", so you can subtract that from all
    images you intend to publish.

    You bring up a good point, which is we control the camera so we control the image that comes out of that camera and which is posted to the Internet.

    Given PRNU subtraction requires something like 20-50 images from the same camera, we could indeed compute a reference fingerprint amd then subtract
    that fingerprint from future images posted to the Internet.

    Thanks for that idea.
    It changes how the PRNU.py script works, so it's another script algogether.

    The PRNU.py script I'm experimenting with is aimed at the general case,
    where the camera is unknown and we don't have access to a calibration set.

    In that situation, we can't subtract a fingerprint because we don't have
    one. So the first practical option I came up with was to decorrelate the
    sensor pattern through geometric distortion, resampling, blur and tiny perturbations.

    However, your suggestion is actually a better approach overall, since I am trying to help all of us protect our own images out of our own cameras.

    I'll have to do a lot more research to write the program to do that though.

    But how does this sound as a game plan for creating that flow on Windows?
    1. Build a reference PRNU fingerprint from a folder of calibration images
    2. Subtract that fingerprint from a target image we want to publish
    --
    Privacy isn't something we get by accident. It's something we build .
    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From Lawrence =?iso-8859-13?q?D=FFOliveiro?=@ldo@nz.invalid to rec.photo.digital,comp.lang.python on Fri Jul 31 22:49:14 2026
    From Newsgroup: rec.photo.digital

    On Fri, 31 Jul 2026 13:59:58 -0800, Maria Sophia wrote:

    ... but PRNU isn't random noise.

    It is random noise, but only in the spatial domain; it is not
    time-varying.
    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From Maria Sophia@mariasophia@comprehension.com to rec.photo.digital,comp.lang.python,alt.comp.os.windows-10 on Fri Jul 31 14:51:06 2026
    From Newsgroup: rec.photo.digital

    Maria Sophia wrote:
    But how does this sound as a game plan for creating that flow on Windows?
    1. Build a reference PRNU fingerprint from a folder of calibration images
    2. Subtract that fingerprint from a target image we want to publish

    Here's the first generation of an attempt to follow up on Lawrence's kind-hearted helpful astute suggestion to build a calibration database.

    I tested this only on Windows but the process should work on any platform. Here's what I did.
    1. Run adbcopy.bat to copy your images over Wi-Fi to your desktop
    2. Put those copied images from your phone into the calibration folder
    3. Take one of those images to be scrubbed and copy it to input.jpg

    Then run:
    python prnu_wash.py

    Drat. The first pass errored because the images have to be the same size. Bummer. Apparently NumPy cannot average arrays of different shapes.
    So the second pass worked, but I had to throw out images of other sizes.
    So consider this only a test showing whether the wash concept is feasible.

    C:\tmp\synthid\prnu_wash> python prnuwash.py
    input.jpg resolution: (3264, 1468)
    Calibrated: 20260710_014519.jpg
    Skipping (size mismatch): 20260710_083333.jpg ((4000, 1800))
    Calibrated: 20260710_091529.jpg
    Calibrated: 20260710_091533.jpg
    Calibrated: 20260710_091552.jpg
    Skipping (size mismatch): 20260710_102702.jpg ((4000, 1800))
    Skipping (size mismatch): 20260710_102752.jpg ((4000, 1800))
    Skipping (size mismatch): 20260710_102954.jpg ((4000, 1800))
    Calibrated: 20260710_111547.jpg
    Skipping (size mismatch): 20260710_111600.jpg ((4128, 3096))
    Skipping (size mismatch): 20260710_111603.jpg ((4128, 3096))
    Calibrated: 20260710_112210.jpg
    Skipping (size mismatch): 20260710_125740.jpg ((4000, 1800))
    Skipping (size mismatch): 20260710_125741.jpg ((4000, 1800))
    Skipping (size mismatch): 20260710_125743.jpg ((4000, 1800))
    Skipping (size mismatch): 20260710_125744.jpg ((4000, 1800))
    Skipping (size mismatch): 20260710_125745.jpg ((4000, 1800))
    Skipping (size mismatch): 20260710_125746.jpg ((4000, 1800))
    Skipping (size mismatch): 20260710_131330.jpg ((4000, 1800))
    Skipping (size mismatch): 20260710_131332.jpg ((4000, 1800))
    Skipping (size mismatch): 20260710_131334.jpg ((4000, 1800))
    Skipping (size mismatch): 20260710_135144.jpg ((4000, 1800))
    Skipping (size mismatch): 20260710_135147.jpg ((4000, 1800))
    Calibrated: 20260710_135152.jpg
    Calibrated: 20260710_184326.jpg
    Calibrated: 20260710_184331.jpg
    Skipping (size mismatch): 20260710_191700.jpg ((3408, 2556))
    Skipping (size mismatch): 20260710_191702.jpg ((3408, 2556))
    Skipping (size mismatch): 20260725_125158.jpg ((3408, 2556))
    Skipping (size mismatch): 20260725_125318.jpg ((4000, 1800))
    Skipping (size mismatch): 20260725_125329.jpg ((4000, 1800))
    Skipping (size mismatch): 20260725_125332.jpg ((4000, 1800))
    Skipping (size mismatch): 20260725_125338.jpg ((4000, 1800))
    Skipping (size mismatch): 20260725_125341.jpg ((4000, 1800))
    Calibrated: 20260725_141007.jpg
    Calibrated: 20260725_144131.jpg
    Calibrated: 20260725_144140.jpg
    Skipping (size mismatch): input.jpg ((800, 1067))
    Fingerprint built from 12 images.
    Saved: scrubbed.jpg
    C:\tmp\synthid\prnu_wash>

    Here is the hash of the unadulterated original:
    Name: input.jpg
    Size: 1467088 bytes (1432 KiB)
    SHA256: 231CA2774263739CF2351CA2FF9CCDB3309716D64B0BC2B4788326C14A8CF33F
    Which happened to be the same file as one of the calibration images:
    Name: 20260710_091552.jpg
    Size: 1467088 bytes (1432 KiB)
    SHA256: 231CA2774263739CF2351CA2FF9CCDB3309716D64B0BC2B4788326C14A8CF33F
    Where this is the resulting hash of the scrubbed image that resulted.
    Name: scrubbed.jpg
    Size: 1311253 bytes (1280 KiB)
    SHA256: D9B80BCBAE3CB679B858587186F373C5242D121D07B39E78844F843ECD31AD85

    When you test this out, please let us all know what you think of the
    fidelity of the resulting scrubbed image, and whether this is reasonable.

    # prnuwash.py
    # ---------------------------------------------------------------
    # Build a PRNU fingerprint from a set of calibration images
    # and subtract it from new images before posting them online.
    # <https://en.wikipedia.org/wiki/Photo_response_non-uniformity>
    #
    # 1. Place 20-50 calibration images in ./calibration/
    # 2. Place the image you want to scrub as input.jpg
    # 3. Run: python prnuwash.py
    # 4. Output: scrubbed.jpg (PRNU-reduced)
    # ---------------------------------------------------------------
    # PRNU subtraction only works when we control the camera such
    # that we have access to multiple images from the same sensor.
    # For PRNU cleaning of images from unknown cameras, use prnu.py
    # For removal of AI fingerprints, use crush.py instead.
    # So this script...
    # 1. Reads input.jpg to determine the required resolution.
    # 2. Scans all JPEGs in ./calibration/
    # 3. Uses ONLY those images that match input.jpg's resolution.
    # 4. Builds a fingerprint from matching images.
    # 5. Subtracts that fingerprint from input.jpg.
    # This resolution filtering is needed because phone cameras do
    # NOT guarantee identical resolution across across all due to
    # different modes (e.g., HDR, night mode, zoom, wide-angle,
    # telephoto, screenshots, crops, panoramas, etc.).
    # ---------------------------------------------------------------
    # v1p1 20260731 added necessary automatic-resolution filtering
    # v1p0 20260731 simple wavelet noise extraction + averaging
    # ---------------------------------------------------------------
    import os
    import numpy as np
    from PIL import Image, ImageFilter

    CALIB_DIR = "calibration"
    INPUT_IMAGE = "input.jpg"
    OUTPUT_IMAGE = "scrubbed.jpg"

    # Extract high-frequency noise (approx PRNU)
    def noise_residual(img):
    blur = img.filter(ImageFilter.GaussianBlur(radius=1.2))
    arr = np.asarray(img).astype(np.float32)
    blur_arr = np.asarray(blur).astype(np.float32)
    return arr - blur_arr

    # Build fingerprint only from images matching input.jpg resolution
    def build_fingerprint(target_size):
    noise_maps = []

    for fname in sorted(os.listdir(CALIB_DIR)):
    if not fname.lower().endswith((".jpg", ".jpeg", ".png")):
    continue

    path = os.path.join(CALIB_DIR, fname)
    img = Image.open(path).convert("RGB")

    if img.size != target_size:
    print(f"Skipping (size mismatch): {fname} ({img.size})")
    continue

    noise_maps.append(noise_residual(img))
    print("Calibrated:", fname)

    if not noise_maps:
    raise RuntimeError("No calibration images matched input.jpg resolution.")

    fp = np.mean(noise_maps, axis=0)
    print("Fingerprint built from", len(noise_maps), "images.")
    return fp

    # Subtract fingerprint
    def subtract_fp(img, fp):
    arr = np.asarray(img).astype(np.float32)
    fp_norm = fp / (np.std(fp) + 1e-6)
    cleaned = np.clip(arr - fp_norm, 0, 255).astype(np.uint8)
    return Image.fromarray(cleaned)

    # Main
    img = Image.open(INPUT_IMAGE).convert("RGB")
    target_size = img.size
    print("input.jpg resolution:", target_size)

    fp = build_fingerprint(target_size)

    cleaned = subtract_fp(img, fp)
    cleaned.save(OUTPUT_IMAGE, "JPEG", quality=95)

    print("Saved:", OUTPUT_IMAGE)

    # end of prnuwash.py
    --
    We all strive to add privacy that marketing doesn't want us to have.

    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From Maria Sophia@mariasophia@comprehension.com to rec.photo.digital,comp.lang.python,alt.comp.os.windows-10 on Fri Jul 31 15:02:08 2026
    From Newsgroup: rec.photo.digital

    Maria Sophia wrote:
    1. Run adbcopy.bat to copy your images over Wi-Fi to your desktop

    For those who don't have the aforementioned adbcopy.bat, here it is.
    I wrote it for my filespecs, so you need to change the defaults.

    It is designed to copy images to/from desktop/phone using Wi-Fi or USB.
    Mostly Wi-Fi (since with USB you can more easily drag and drop using MTP).

    The result is you can copy any images to and from any Android on your LAN.


    :: adbcopy.bat
    :: Uses desktop adb to copy image files to:from Android:Windows (Wi-Fi:USB)
    :: -------------------------------------------------------------------------
    :: v3p4 20260727 Added checks for typos when inputting Windows file paths
    :: v3p3 20260727 Added Windows browsing capability if full path is not known
    :: v3p2 20260726 Added choice of known Android destinations for convenience
    :: v3p1 20260725 Added Windows push to Android using a known full path
    :: Added broadcast registration so images can quickly be found
    :: v3p0 20260708 added dual-layer deletion (filesystem + mediastore)
    :: which prevents FUSE from repeatedly restoring ghost files
    :: and which handles private-app sandbox directories
    :: v2p9 20260707 fixed quoting and path issues caused by filenames with spaces
    :: v2p8 20260707 added skipping of empty directories before prompting user
    :: v2p7 20260702 changed to delete only files (not folders)
    :: v2p6 20260701 added auto-detect all DCIM subdirectories
    :: v2p5 20260701 expanded image file matching
    :: v2p4 20260701 added internal DCIM/Camera option
    :: v2p3 20260621 open destination folder when done
    :: v2p2 20260617 added screenshot directory detection
    :: v2p2 20260612 fixed quoting bug in DCIM ls command
    :: v2p1 20260607 added Windows-to-Android APK install mode
    :: v2p0 20260603 added Screenshot directory support
    :: v1p9 20260528 trim trailing spaces from destination path
    :: v1p8 20260528 replaced destructive path truncation with CR stripping
    :: v1p7 20260528 escaped redirection character in backtick loop
    :: v1p6 20260528 updated external SD card path handling
    :: v1p5 20260528 fixed null-redirection error and stabilized extraction
    :: v1p4 20260528 fixed "unexpected at this time" crash
    :: v1p3 20260528 fixed crash on empty variable comparison
    :: v1p2 20260528 fixed multi-device detection and nested quoting
    :: v1p1 20260528 added debug output for failure cases
    :: v1p0 20260528 initial version: copy images by date
    :: -------------------------------------------------------------------------

    @echo off
    setlocal enabledelayedexpansion

    cd /d "%~dp0"
    set "ADB_EXE=c:\app\editor\android\scrcpy\adb.exe"

    :: --- DEVICE DETECTION ---
    for /f "skip=1 tokens=1" %%g in ('"%ADB_EXE%" devices') do (
    if not "%%g"=="" (
    if "!ADB_TARGET!"=="" set "ADB_TARGET=%%g"
    )
    )

    if "!ADB_TARGET!"=="" (
    echo [ERROR] No connected ADB devices found.
    pause
    exit /b
    )

    echo Targeting device: %ADB_TARGET%
    echo ----------------

    :: --- MAIN DIRECTION MENU --- default is [1] ---
    echo Select Direction:
    echo [1] Android to Windows (Pull Images/Screenshots) [Default]
    echo [2] Windows to Android (Copy/Install APK)
    echo.
    set "CHOICE=1"
    set /p "CHOICE=Enter choice [1 or 2]: "

    if "%CHOICE%"=="2" goto :windows_to_android
    if "%CHOICE%"=="1" goto :android_to_windows
    goto :android_to_windows

    :windows_to_android
    echo.
    echo --- Windows to Android Push Mode ---

    :: begin file selection
    echo Select file input mode:
    echo [0] Browse for file (no full path needed)
    echo [1] Enter full path manually
    echo.
    set /p "FILE_MODE=Enter choice [0 or 1]: "

    if "%FILE_MODE%"=="0" (
    echo Opening file browser...
    for /f "delims=" %%F in ('powershell -command "Add-Type -AssemblyName System.Windows.Forms; $f=New-Object System.Windows.Forms.OpenFileDialog; $f.Filter='Images|*.jpg;*.jpeg;*.png;*.gif'; if($f.ShowDialog() -eq 'OK'){Write-Output $f.FileName}"') do set "SRC_FILE=%%F"
    ) else (
    :ask_path
    set /p "SRC_FILE=Enter full path of file to push: "
    if "%SRC_FILE%"=="" (
    echo You must enter a full path or choose option 0.
    goto ask_path
    )
    )

    if not exist "%SRC_FILE%" (
    echo [ERROR] File not found: %SRC_FILE%
    pause
    exit /b
    )

    :: end file selection

    :: Extract just the filename (e.g., pizzamyheart.jpg)
    for %%F in ("%SRC_FILE%") do set "SRC_NAME=%%~nxF"

    echo.
    echo Select Android destination:
    echo [1] /storage/emulated/0/DCIM/Camera
    echo [2] /storage/emulated/0/DCIM/Screenshots
    echo [3] /storage/emulated/0/Pictures
    echo [4] /storage/emulated/0/Pictures/Screenshots
    echo [5] /storage/emulated/0/Download
    echo [6] /storage/emulated/0/Snapseed
    echo [7] /storage/emulated/0/Samsung
    echo [8] /storage/emulated/0/Android/media/com.whatsapp/WhatsApp/Media/WhatsApp Images
    echo.
    set /p "DEST_CHOICE=Enter choice [1-8]: "

    if "%DEST_CHOICE%"=="1" set "DEST_DIR=/storage/emulated/0/DCIM/Camera"
    if "%DEST_CHOICE%"=="2" set "DEST_DIR=/storage/emulated/0/DCIM/Screenshots"
    if "%DEST_CHOICE%"=="3" set "DEST_DIR=/storage/emulated/0/Pictures"
    if "%DEST_CHOICE%"=="4" set "DEST_DIR=/storage/emulated/0/Pictures/Screenshots"
    if "%DEST_CHOICE%"=="5" set "DEST_DIR=/storage/emulated/0/Download"
    if "%DEST_CHOICE%"=="6" set "DEST_DIR=/storage/emulated/0/Snapseed"
    if "%DEST_CHOICE%"=="7" set "DEST_DIR=/storage/emulated/0/Samsung"
    if "%DEST_CHOICE%"=="8" set "DEST_DIR=/storage/emulated/0/Android/media/com.whatsapp/WhatsApp/Media/WhatsApp Images"

    echo.
    echo Pushing "%SRC_FILE%" to "%DEST_DIR%"
    "%ADB_EXE%" -s %ADB_TARGET% push "%SRC_FILE%" "%DEST_DIR%"

    echo Forcing media scan...
    "%ADB_EXE%" -s %ADB_TARGET% shell am broadcast -a android.intent.action.MEDIA_SCANNER_SCAN_FILE -d file://%DEST_DIR%/%SRC_NAME%

    echo Done.
    pause
    exit /b

    :: ROUTINE: ANDROID TO WINDOWS
    :android_to_windows
    echo.
    echo --- Running Android to Windows Pull ---

    echo.
    echo What do you want to pull?
    echo [1] Camera photos (SD Card DCIM/Camera)
    echo [2] Screenshots
    echo [3] Camera photos (Internal Storage DCIM/Camera)
    echo [4] Pull directories one-by-one (user selected)
    set "PULL_CHOICE=1"
    set /p "PULL_CHOICE=Enter choice [1, 2, 3 or 4]: "

    :: Only ask for date if pulling dated files
    if "%PULL_CHOICE%"=="4" goto :skip_date

    :: --- DATE DETECTION ---
    for /f "tokens=2 delims==" %%i in ('wmic os get localdatetime /value') do set "dt=%%i"
    set "DETECTED_DATE=%dt:~0,8%"

    echo Detected Date: %DETECTED_DATE%
    set /p "USER_DATE=Press Enter to confirm, or type a different date (YYYYMMDD): "
    if "%USER_DATE%"=="" (
    set "TARGET_DATE=%DETECTED_DATE%"
    ) else (
    set "TARGET_DATE=%USER_DATE%"
    )

    :skip_date

    echo.
    if "%PULL_CHOICE%"=="4" (
    rem Interactive, pulling everything iV no date-based subfolder
    set "DEST_DIR=H:\003_camera\20260701"

    ) else (
    rem Date-based pulls still go into YYYYMMDD subfolder
    set "DEST_DIR=H:\004_upload\home\pool\assay\%TARGET_DATE%"
    )

    set /p "USER_DIR=Enter destination directory [Default: %DEST_DIR%]: "
    if not "%USER_DIR%"=="" set "DEST_DIR=%USER_DIR%"

    :: Remove quotes
    set "DEST_DIR=%DEST_DIR:"=%"

    :: Trim trailing spaces
    :trim
    if "!DEST_DIR:~-1!"==" " (
    set "DEST_DIR=!DEST_DIR:~0,-1!"
    goto trim
    )

    if not exist "%DEST_DIR%" (
    echo Creating directory: %DEST_DIR%
    mkdir "%DEST_DIR%"
    )

    if "%PULL_CHOICE%"=="1" goto :pull_camera
    if "%PULL_CHOICE%"=="2" goto :pull_screenshots
    if "%PULL_CHOICE%"=="3" goto :pull_camera_internal
    if "%PULL_CHOICE%"=="4" goto :pull_interactive_dirs

    goto :pull_camera

    if "%PULL_CHOICE%"=="1" goto :pull_camera
    if "%PULL_CHOICE%"=="2" goto :pull_screenshots
    if "%PULL_CHOICE%"=="3" goto :pull_camera_internal
    if "%PULL_CHOICE%"=="4" goto :pull_interactive_dirs

    goto :pull_camera

    :: PULL CAMERA (DCIM)
    :pull_camera
    echo.
    echo Pulling CAMERA images matching *%TARGET_DATE%*.jpg ...
    echo Sending files to: "%DEST_DIR%"
    echo ----------------

    for /f "usebackq delims=" %%i in (`
    %ADB_EXE% -s !ADB_TARGET! shell ls /storage/0CA4-352D/DCIM/Camera/*%TARGET_DATE%*.jpg
    `) do (
    set "FILE_PATH=%%i"
    for /f "delims=" %%r in ("!FILE_PATH!") do set "FILE_PATH=%%r"
    if not "!FILE_PATH!"=="" (
    echo Pulling: !FILE_PATH!
    "%ADB_EXE%" -s !ADB_TARGET! pull "!FILE_PATH!" "%DEST_DIR%"
    )
    )

    goto :complete

    :: PULL SCREENSHOTS (handle both directories explicitly)
    :pull_screenshots
    echo.
    echo Pulling SCREENSHOTS matching *%TARGET_DATE%*.jpg ...
    echo Sending files to: "%DEST_DIR%"
    echo ----------------

    :: Directory A Android 11 legacy
    for /f "usebackq delims=" %%i in (`
    %ADB_EXE% -s !ADB_TARGET! shell ls /sdcard/DCIM/Screenshots/*%TARGET_DATE%*.jpg
    `) do (
    set "FILE_PATH=%%i"
    if not "!FILE_PATH!"=="" (
    echo Pulling (legacy): !FILE_PATH!
    "%ADB_EXE%" -s !ADB_TARGET! pull "!FILE_PATH!" "%DEST_DIR%"
    )
    )

    :: Directory B Android 13 modern
    for /f "usebackq delims=" %%i in (`
    %ADB_EXE% -s !ADB_TARGET! shell ls /storage/emulated/0/DCIM/Screenshots/*%TARGET_DATE%*.jpg
    `) do (
    set "FILE_PATH=%%i"
    if not "!FILE_PATH!"=="" (
    echo Pulling (modern): !FILE_PATH!
    "%ADB_EXE%" -s !ADB_TARGET! pull "!FILE_PATH!" "%DEST_DIR%"
    )
    )

    goto :complete

    :: PULL CAMERA (INTERNAL STORAGE DCIM)
    :pull_camera_internal
    echo.
    echo Pulling CAMERA images from INTERNAL STORAGE matching *%TARGET_DATE%*.jpg ...
    echo Sending files to: "%DEST_DIR%"
    echo ----------------

    for /f "usebackq delims=" %%i in (`
    %ADB_EXE% -s !ADB_TARGET! shell ls /storage/emulated/0/DCIM/Camera/*%TARGET_DATE%*.jpg
    `) do (
    set "FILE_PATH=%%i"
    for /f "delims=" %%r in ("!FILE_PATH!") do set "FILE_PATH=%%r"
    if not "!FILE_PATH!"=="" (
    echo Pulling: !FILE_PATH!
    "%ADB_EXE%" -s !ADB_TARGET! pull "!FILE_PATH!" "%DEST_DIR%"
    )
    )
    goto :complete

    :: FINALIZE
    :complete
    start "" "%DEST_DIR%"
    echo Process complete
    pause
    exit /b

    :pull_interactive_dirs
    echo.
    echo --- Interactive Directory Pull Mode ---
    echo Destination: "%DEST_DIR%"

    echo.

    :: --- Detect ALL folders under /storage/emulated/0/DCIM ---
    echo Detecting DCIM folders...

    rem Correct quoting: do NOT quote the ls command
    "%ADB_EXE%" -s %ADB_TARGET% shell ls -d /storage/emulated/0/DCIM/*/ > "%TEMP%\dcimdirs.txt" 2>&1

    echo.
    echo --- Interactive Directory Pull Mode (adb uses the FUSE layer) ---
    echo Destination: "%DEST_DIR%"
    echo.

    :: First pull all DCIM subfolders dynamically
    for /f "usebackq delims=" %%D in ("%TEMP%\dcimdirs.txt") do (
    echo Found DCIM folder: %%D
    call :pull_dir "%%D"
    )

    :: Then pull the other known directories
    call :pull_dir "/storage/emulated/0/Pictures"
    call :pull_dir "/storage/emulated/0/Download"
    call :pull_dir "/storage/emulated/0/Android/media/com.whatsapp/WhatsApp/Media/WhatsApp Images"
    call :pull_dir "/storage/emulated/0/Android/media/net.psyberia.offlinemaps/OfflineMaps Exports"
    call :pull_dir "/storage/emulated/0/ReadEra/Covers"
    call :pull_dir "/storage/emulated/0/OziExplorer/System Data"
    call :pull_dir "/storage/0CA4-352D/DCIM/Camera"
    call :pull_dir "/sdcard/DCIM/Screenshots"

    goto :eod

    :pull_dir
    set "DIR=%~1"

    echo.
    echo Directory: %DIR%

    echo Listing files in %DIR% ...
    %ADB_EXE% -s %ADB_TARGET% shell ls "%DIR%" > "%TEMP%\filelist_raw.txt"

    :: Filter out directories so as to keep only real files
    > "%TEMP%\filelist.txt" (
    for /f "usebackq delims=" %%Z in ("%TEMP%\filelist_raw.txt") do (
    %ADB_EXE% -s %ADB_TARGET% shell "[ -f \"%DIR%/%%Z\" ]" && echo %%Z
    )
    )

    :: Check if any real files exist
    set "HASFILES="
    for /f "usebackq delims=" %%Z in ("%TEMP%\filelist.txt") do set "HASFILES=1"

    if not defined HASFILES (
    echo No files found in %DIR%. Skipping.
    goto :eod
    )

    :: Print the actual file list (Option A)
    echo Files found:
    type "%TEMP%\filelist.txt"
    echo.

    :: Ask user only if real files exist
    set /p "ANS=Pull this directory? [y/N]: "
    if /i not "%ANS%"=="y" goto :eod

    echo Checking directory: %DIR%

    :: Count files
    set "COUNT=0"
    for /f "usebackq delims=" %%C in ("%TEMP%\filelist.txt") do set /a COUNT+=1
    echo Found %COUNT% files in %DIR%.

    echo Pulling files...
    for /f "usebackq delims=" %%F in ("%TEMP%\filelist.txt") do (
    echo Pulling: %DIR%/%%F
    %ADB_EXE% -s %ADB_TARGET% pull "%DIR%/%%F" "%DEST_DIR%"
    )

    :: delete the file and the MediaStore entry and handle space differences
    :: Take into account many mediastore naming syntax and access intricacies
    set /p "DEL=Delete original files from phone (and MediaStore)? [y/N]: "
    if /i "%DEL%"=="y" (
    echo Deleting originals and MediaStore entries...

    for /f "usebackq delims=" %%F in ("%TEMP%\filelist.txt") do (

    rem --- Delete file (handles spaces correctly) ---
    echo Deleting file: %DIR%/%%F
    "%ADB_EXE%" -s %ADB_TARGET% shell rm "\"%DIR%/%%F\""

    rem --- Delete MediaStore entry using full filename (with spaces) ---
    echo Deleting MediaStore entry for: %%F
    "%ADB_EXE%" -s %ADB_TARGET% shell content delete --uri content://media/external/images/media --where "\"_data LIKE '%/%%F'\""
    )
    )

    :eod
    exit /b

    REM end of adbcopy.bat
    --
    It takes a different kind of person to automate every action on a device.
    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From Maria Sophia@mariasophia@comprehension.com to rec.photo.digital,alt.comp.os.windows-10,comp.lang.python on Fri Jul 31 15:13:53 2026
    From Newsgroup: rec.photo.digital

    Lawrence D'Oliveiro wrote:
    ... but PRNU isn't random noise.

    It is random noise, but only in the spatial domain; it is not
    time-varying.

    Thank you for that correction as all of this is brand new to me.

    Regarding my mis-statement of:
    "I never heard of BM3D denoising/renoising until you mentioned itt, so
    looking it up, BM3D denoising + re-noising is apparently wonderful for
    removing random noise, but PRNU isn't random noise."

    Taking Lawrence's kind-hearted and correct admonishment into account, what
    I perhaps should have said is that PRNU is indeed random noise in the
    spatial domain, but it's fixed-pattern noise and time-invariant noise.

    Therefore, the same pixel-level gain variations show up in every photo from that sensor in the spatial coordinates, which is how they correlate images.

    That's the distinction I was trying to get at when I implied that my
    research seems to indicate that BM3D is excellent at removing time-varying random noise, but PRNU behaves like a stable multiplicative gain map.

    I think BM3D can reduce some PRNU energy, but it might not reliably break
    the correlation the way geometric misalignment or resampling does.

    Thanks again to Lawrence and to Piergiorgio for offering useful advice.

    Your correction helps me phrase it more accurately as all this is new to
    me, so were learning how to protect our images on the Internet together.
    --
    Privacy is a million technical things, of which most people know about 3.
    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From Piergiorgio Sartor@piergiorgio.sartor.this.should.not.be.used@nexgo.REMOVETHIS.de to rec.photo.digital,alt.comp.os.windows-10,comp.lang.python on Sat Aug 1 11:48:05 2026
    From Newsgroup: rec.photo.digital

    On 01/08/2026 01.13, Maria Sophia wrote:
    [...]

    I'm under the impression there is some
    confusion here.

    De-noising algorithms, like BM3D or any
    de-noising auto-encoder, de-noise just
    one image.
    There is no temporal noise involved, it
    is only spatial, since the algorithms
    see only one, single, image.

    There are temporal de-noising algorithm
    which could be applied to video sequences,
    but this does not seem to be the case here.

    On the other hand, PRNU will have some
    spatial statistical properties, even if
    it is fixed for a given sensor.

    Now, assuming we have some PRNU from some
    unrelated sensor (meaning we can generate
    a spatial noise with same statistical
    properties), it would be possible to
    consider to apply this noise ("apply", not
    "add") to a given image.

    This means the effective PRNU will be the
    combination ("combination", not "sum") of
    two PRNUs, resulting in a new fingerprint,
    different from the original one.

    So, de-noising is not really needed, but it
    might be helpful to reduce / remove / modify
    the original PRNU, so that the applied one
    will be more evident.

    The point here is that the image content
    will not be altered too much, so quality
    will be somehow preserved.
    Different than blurring or resizing.

    If you do not like BM3D, you can look, as
    mentioned above, into de-noising auto-encoders.
    These will be even more aggressive in removing
    the PRNU content (maybe with more damage to
    the overall image).

    Finally, consider the "simple" case of generative
    algorithm (AI stuff, so to speak).
    These could (YMMV) generate image *without* any
    PRNU, similar or identical to the one taken
    with the camera.
    So, the fingerprint will be gone (maybe there will
    be another one from the AI).

    It seems to me there is a lot to explore way
    beyond the simple image modification algorithms.

    bye,
    --

    piergiorgio
    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From Maria Sophia@mariasophia@comprehension.com to rec.photo.digital,alt.comp.os.windows-10,comp.lang.python on Sat Aug 1 09:42:11 2026
    From Newsgroup: rec.photo.digital

    Piergiorgio Sartor wrote:
    I'm under the impression there is some
    confusion here.
    It seems to me there is a lot to explore way
    beyond the simple image modification algorithms.

    Hi Piergiorgio,

    Thank you for your expert input, as you obviously understand the topic
    better than I do, by far, so your advice is welcome and very useful.

    I'm slow...
    Lawrence tried 2 turns before I realized he was suggesting calibration.
    You also tried 2 turns before I began to understand the fake-PRNU concept

    For example, you stress the verb "apply" rather than the verb "add".
    a. You stress my scripts only add noise [img = img + noise]
    b. You stress a real PRNU is different [img = img * (1 + prnu-map)]

    Is that what you're saying?

    If so, you (& Lawrence) are bringing up EXCELLENT points, where, for
    example, Lawrence prodded me into building the calibration script, which, I think, is a vast improvement on the single-image PRNU script prior.

    My goal is to provide myself and others a script to run on our images to
    avoid fingerprinting of images that we ourselves, post to various websites.

    I first must admit I know almost nothing about PRNU fingerprinting (or AI-watermark identification), so any and all help and advice is welcome.

    Second, I must admit I have to look up in depth what you and Lawrence say,
    'cuz I'm a noob in this stuff. I don't know anything about fingerprinting.

    I simply want to find or write a script that I can run on an image from my
    own camera that obfuscates the fingerprint, and I want others to have it.

    Because my goal, here anyway, is to provide ,myself & others with a usable script that they can use themselves to avoid correlation fingerprinting.

    Looking up what you said in the prior post, am I write in summarizing:
    a. I seem to be confusing PRNU with ordinary image noise.
    b. You're saying PRNU is a fixed-pattern noise, not random temporal noise
    c. Adding noise does not mimic PRNU because PRNU is a per-pixel factor
    d. Denoising algorithms operate only on spatial noise in a single image.
    e. BM3D doesn't know anything about temporal noise or sensor noise
    f. If we denoise an image, we remove the original PRNU
    g. If we subsequently realistically re-noise the image
    we can create a new fingerprint (if the noise is realistic spatially)

    A takeaway is that adding or removing noise doesn't help much with PRNU.
    On the other hand, overlaying a completely fake PRNU works better.

    So, I may be confused, which is why I appreciate that you're gently chiding
    me to be more responsive to what you're saying, and not what I think prior.

    Are you saying...
    1. We should first remove the original PRNU
    2. And then we should apply a realistically synthetic PRNU
    3. Which produces a new fingerprint, different from the camera

    If so, that's a great idea as then all images in one batch uploaded to the
    net will have a similar realistic fingerprint, instead of being random.

    Are you also saying...
    A. The initial denoising is actually optional but still useful
    B. By adding a fake PRNU, image quality remains mostly intact
    C. It's the fake PRNU that is most effective because just
    adding noise doesn't remove the underlying fingerprint

    And yes, I get the point that generative AI can produce images with no PRNU but, as noted in the other thread, AI-generated images have watermarks.
    Message-ID: <114d9tb$1fht$1@nnrp.usenet.blueworldhosting.com>

    Also, it took me a long time to figure out your autoencoder suggestion.
    Is this what you're saying about the autoencoders?
    a. An autoencoder reconstructs image content, not noise (incl. PRNU)
    b. They discard sensor-specific noise patterns
    c. So autoencoders may be better than BM3D
    import torch
    from torchvision import transforms
    model = torch.load("denoiser.pth")
    model.eval()
    img_tensor = transforms.ToTensor()(img).unsqueeze(0)
    denoised = model(img_tensor).squeeze().permute(1,2,0).numpy()

    For the benefit of others noobs like I am on image manipulation...
    a. BM3D Block-Matching 3D is a denoising algorithm
    b. Which finds similar patches and stacks them into 3D blocks
    c. And then filters and aggregates the patches back into the image
    pip install bm3d
    from bm3d import bm3d
    denoised = bm3d(image, sigma_psd=10/255)

    Overall, I think what you're saying, in a nutshell, is the most effective approach is for us to come up with a way to add a fake PRNU fingerprint.

    Is that right?
    --
    On Usenet you find people who know more than you, yourself, will ever know.
    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From Chris@ithinkiam@gmail.com to alt.comp.os.windows-10,rec.photo.digital,comp.lang.python on Sun Aug 2 17:48:26 2026
    From Newsgroup: rec.photo.digital

    Piergiorgio Sartor <piergiorgio.sartor.this.should.not.be.used@nexgo.REMOVETHIS.de> wrote:
    On 01/08/2026 01.13, Maria Sophia wrote:
    [...]

    I'm under the impression there is some
    confusion here.

    To be expected considering who the OP is...

    On the other hand, PRNU will have some
    spatial statistical properties, even if
    it is fixed for a given sensor.

    Now, assuming we have some PRNU from some
    unrelated sensor (meaning we can generate
    a spatial noise with same statistical
    properties), it would be possible to
    consider to apply this noise ("apply", not
    "add") to a given image.

    This means the effective PRNU will be the
    combination ("combination", not "sum") of
    two PRNUs, resulting in a new fingerprint,
    different from the original one.

    So, de-noising is not really needed, but it
    might be helpful to reduce / remove / modify
    the original PRNU, so that the applied one
    will be more evident.

    The two main issues regarding PRNU that the OP has missed when jumping to conclusions are: 1) it is probabilitic, 2) it is comparative.

    That means for 1) there's no guarantee that the noise in your camera
    sensor is unique to you. It might be common, or very similar, to all
    cameras of that model and batch. The statistical model is based on small samples of camera sensors so it isn't a globally accurate model.

    For 2) that means it cannot state which device was used to take a photo
    without having the device in question in your possession.

    Just like ballistics. Once you have a suspect gun in your possession you
    can make some assessment of the probability of whether that gun fired a particular round.

    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From Maria Sophia@mariasophia@comprehension.com to alt.comp.os.windows-10,rec.photo.digital,comp.lang.python on Sun Aug 2 11:18:31 2026
    From Newsgroup: rec.photo.digital

    Chris wrote:
    I'm under the impression there is some
    confusion here.

    To be expected considering who the OP is...

    I'm curious, Chris, why you always feel so desperate to try to insult me?
    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From Lawrence =?iso-8859-13?q?D=FFOliveiro?=@ldo@nz.invalid to alt.comp.os.windows-10,rec.photo.digital,comp.lang.python on Sun Aug 2 22:54:27 2026
    From Newsgroup: rec.photo.digital

    On Sun, 2 Aug 2026 11:18:31 -0800, Maria Sophia wrote:

    I'm curious, Chris, why you always feel so desperate to try to
    insult me?

    Just killfile the haters and move on.
    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From Maria Sophia@mariasophia@comprehension.com to alt.comp.os.windows-10,rec.photo.digital,comp.lang.python on Mon Aug 3 09:20:43 2026
    From Newsgroup: rec.photo.digital

    Hi Lawrence,

    Thanks for your advice, where I have been looking up the methods that you & Piergiorgio suggested, both of which are far better than my original idea.

    I liked your idea of building a PRNU fingerprint from a set of same-camera calibration images of matching resolution and subtracting that fingerprint from the target image to produce a PRNU-reduced scrubbed image.

    That's what the prnuwash.py script attempted to accomplish, which was a
    better method than the original method I had tried, since prnu.py used
    blind PRNU estimation techniques to suppress sensor-specific fingerprints.

    I think each of us adds more value to the problem set discussion, where everyone can benefit from our ideas, even those who are only lurking here.

    Below is a a script that reduces the real fingerprint in order to then
    apply a stronger fake fingerprint to implement Piergiorgio's suggestion.

    I had to add cv2 since it produces a more realistic PRNU overall.
    pip3.exe install opencv-python
    But I really need to also add BM3D as Piergiorgio had suggested.

    python fakeprnu.py
    Loaded: input.jpg resolution: (1067, 800, 3)
    Denoised image to weaken original PRNU.
    Generated synthetic PRNU map.
    Applied synthetic PRNU multiplicatively.
    Saved: fakeprnu.jpg

    # --------------------------------------------------------------------
    # fakeprnu.py
    # A cv2-based PRNU scrubber that applies a fake fingerprint to an image.
    # --------------------------------------------------------------------
    # This script is intended to apply a fake fingerprint onto an image file.
    # It's designed to hinder PRNU fingerprinting when posting images online.
    # It does not require calibration images like the previous prnuwash.py
    did.
    #
    # 1. Place the image you want to process as input.jpg
    # 2. Run: python fakeprnu.py
    # 3. Output: scrubbed.jpg
    #
    # The approach:
    # A. Denoise the image to weaken the original PRNU
    # B. Generate a synthetic fixed-pattern PRNU map
    # C. Apply the synthetic PRNU multiplicatively:
    # img_out = img_denoised * (1 + prnu_map)
    #
    # --------------------------------------------------------------------
    # v1p1 20260803 reduced denoising from 10 to 5 due to visible blur effect
    # WIP: BM3D should be added as it reduces noise without edge blur.
    # v1p0 20260803 initial version implementing synthetic PRNU overlay
    # --------------------------------------------------------------------

    import cv2
    import numpy as np

    INPUT_IMAGE = "input.jpg"
    OUTPUT_IMAGE = "fakeprnu.jpg"

    # Step 1: Denoise image to weaken original PRNU
    # Note the option to skip denoicing altogether
    # img_denoised = img
    # 10 was a bit too blurry
    # def denoise_image(img, strength=10):
    def denoise_image(img, strength=5):
    print("Denoised image to weaken original PRNU.")
    # Uses OpenCV fastNlMeansDenoisingColored
    # This is not BM3D, but it is simple and available everywhere.
    return cv2.fastNlMeansDenoisingColored(
    img, None,
    h=strength,
    hColor=strength,
    templateWindowSize=7,
    searchWindowSize=21
    )

    # Step 2: Generate synthetic fixed-pattern PRNU
    def generate_fake_prnu(shape, amplitude=0.02, smooth_kernel=21):
    h, w, c = shape

    # Start with random noise
    noise = np.random.randn(h, w, c).astype(np.float32)

    # Smooth to create spatial correlation
    smooth = cv2.GaussianBlur(noise, (smooth_kernel, smooth_kernel), 0)

    # Normalize to zero mean, unit variance
    mean = np.mean(smooth)
    std = np.std(smooth) + 1e-8
    norm = (smooth - mean) / std

    # Scale to desired amplitude
    prnu_map = amplitude * norm

    return prnu_map

    # Step 3: Apply multiplicative fake PRNU
    def apply_fake_prnu(img, prnu_map):
    img_f = img.astype(np.float32) / 255.0
    out = img_f * (1.0 + prnu_map)

    out = np.clip(out, 0.0, 1.0)
    out = (out * 255.0).astype(np.uint8)
    return out

    # Main
    def main():
    img = cv2.imread(INPUT_IMAGE, cv2.IMREAD_COLOR)
    if img is None:
    raise RuntimeError("Could not load input.jpg")

    print("Loaded:", INPUT_IMAGE, "resolution:", img.shape)

    # Step A: weaken original PRNU
    img_denoised = denoise_image(img)
    print("Denoised image to weaken original PRNU.")

    # Step B: synthetic PRNU
    fake_prnu = generate_fake_prnu(img.shape, amplitude=0.02, smooth_kernel=21)
    print("Generated synthetic PRNU map.")

    # Step C: apply multiplicative PRNU
    img_out = apply_fake_prnu(img_denoised, fake_prnu)
    print("Applied synthetic PRNU multiplicatively.")

    cv2.imwrite(OUTPUT_IMAGE, img_out)
    print("Saved:", OUTPUT_IMAGE)

    if __name__ == "__main__":
    main()

    # end of fakeprnu.py
    --
    On Usenet, we all try to help each other by leveraging knowledge.
    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From Maria Sophia@mariasophia@comprehension.com to rec.photo.digital,alt.comp.os.windows-10,comp.lang.python on Mon Aug 3 09:28:08 2026
    From Newsgroup: rec.photo.digital

    Hi Piergiorgio Sartor,

    I appreciate your expert advice so much that I wrote a script that
    implements some of what you had suggested (BM3D to come later).

    Please see the scripts attached to this thread, where prnuwash.py subtracts
    a real camera fingerprint built from calibration images while fakeprnu.py destroys the real fingerprint and replaces it with a synthetic fingerprint.

    Which do you think is the better approach to improve moving forward?
    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From Chris@ithinkiam@gmail.com to alt.comp.os.windows-10,rec.photo.digital,comp.lang.python on Mon Aug 3 17:50:54 2026
    From Newsgroup: rec.photo.digital

    Maria Sophia <mariasophia@comprehension.com> wrote:
    Chris wrote:
    I'm under the impression there is some
    confusion here.

    To be expected considering who the OP is...

    I'm curious, Chris, why you always feel so desperate to try to insult me?

    As you've argued before that calling someone stupid is not an insult if
    it's true. I'm not insulting you...

    I also gave you a lot of useful information which you've chosen to ignore.

    And then you lie when you say you don't know of another way to share python code. You've been informed many times how to share code usefully.

    Posting code on usenet when it's python is particularly bad as all tabs are lost and therefore the script will not work. Attempts at fixing the
    indentation may make the code run, but probably incorrectly.

    --- Synchronet 3.22a-Linux NewsLink 1.2