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


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

PSA Using Python Pillow to foil camera image PRNU fingerprinting

Started byMaria Sophia <mariasophia@comprehension.com>
First post2026-07-29 22:22 -0700
Last post2026-08-20 19:30 +0200
Articles 20 on this page of 22 — 6 participants

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


Contents

  PSA Using Python Pillow to foil camera image PRNU fingerprinting Maria Sophia <mariasophia@comprehension.com> - 2026-07-29 22:22 -0700
    Re: PSA Using Python Pillow to foil camera image PRNU fingerprinting Lawrence D’Oliveiro <ldo@nz.invalid> - 2026-07-30 08:06 +0000
      Re: PSA Using Python Pillow to foil camera image PRNU fingerprinting Maria Sophia <mariasophia@comprehension.com> - 2026-07-30 07:42 -0800
        Re: PSA Using Python Pillow to foil camera image PRNU fingerprinting Lawrence D’Oliveiro <ldo@nz.invalid> - 2026-07-30 21:03 +0000
          Re: PSA Using Python Pillow to foil camera image PRNU fingerprinting Maria Sophia <mariasophia@comprehension.com> - 2026-07-31 14:10 -0800
            Re: PSA Using Python Pillow to foil camera image PRNU fingerprinting Maria Sophia <mariasophia@comprehension.com> - 2026-07-31 14:51 -0800
              Re: PSA Using Python Pillow to foil camera image PRNU fingerprinting Maria Sophia <mariasophia@comprehension.com> - 2026-07-31 15:02 -0800
        Re: PSA Using Python Pillow to foil camera image PRNU fingerprinting Piergiorgio Sartor <piergiorgio.sartor.this.should.not.be.used@nexgo.REMOVETHIS.de> - 2026-07-31 09:35 +0200
          Re: PSA Using Python Pillow to foil camera image PRNU fingerprinting Maria Sophia <mariasophia@comprehension.com> - 2026-07-31 13:59 -0800
            Re: PSA Using Python Pillow to foil camera image PRNU fingerprinting Lawrence D’Oliveiro <ldo@nz.invalid> - 2026-07-31 22:49 +0000
              Re: PSA Using Python Pillow to foil camera image PRNU fingerprinting Maria Sophia <mariasophia@comprehension.com> - 2026-07-31 15:13 -0800
                Re: PSA Using Python Pillow to foil camera image PRNU fingerprinting Piergiorgio Sartor <piergiorgio.sartor.this.should.not.be.used@nexgo.REMOVETHIS.de> - 2026-08-01 11:48 +0200
                  Re: PSA Using Python Pillow to foil camera image PRNU fingerprinting Maria Sophia <mariasophia@comprehension.com> - 2026-08-01 09:42 -0800
                    Re: PSA Using Python Pillow to foil camera image PRNU fingerprinting Maria Sophia <mariasophia@comprehension.com> - 2026-08-03 09:28 -0800
                  Re: PSA Using Python Pillow to foil camera image PRNU fingerprinting Chris <ithinkiam@gmail.com> - 2026-08-02 17:48 +0000
                    Re: PSA Using Python Pillow to foil camera image PRNU fingerprinting Maria Sophia <mariasophia@comprehension.com> - 2026-08-02 11:18 -0800
                      Re: PSA Using Python Pillow to foil camera image PRNU fingerprinting Lawrence D’Oliveiro <ldo@nz.invalid> - 2026-08-02 22:54 +0000
                        Re: PSA Using Python Pillow to foil camera image PRNU fingerprinting Maria Sophia <mariasophia@comprehension.com> - 2026-08-03 09:20 -0800
                      Re: PSA Using Python Pillow to foil camera image PRNU fingerprinting Chris <ithinkiam@gmail.com> - 2026-08-03 17:50 +0000
                        Re: PSA Using Python Pillow to foil camera image PRNU fingerprinting Maria Sophia <mariasophia@comprehension.com> - 2026-08-20 09:03 -0800
                          Re: PSA Using Python Pillow to foil camera image PRNU fingerprinting danny burstein <dannyb@panix.com> - 2026-08-20 17:28 +0000
                          How to increase your "Convincingness" [Anthropic AI Text Hacked] (Was: PSA Using Python Pillow to foil camera image PRNU fingerprinting) Mild Shock <janburse@fastmail.fm> - 2026-08-20 19:30 +0200

Page 1 of 2  [1] 2  Next page →


#197835 — PSA Using Python Pillow to foil camera image PRNU fingerprinting

FromMaria Sophia <mariasophia@comprehension.com>
Date2026-07-29 22:22 -0700
SubjectPSA Using Python Pillow to foil camera image PRNU fingerprinting
Message-ID<114en2t$7bu$1@nnrp.usenet.blueworldhosting.com>
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 ¡÷ 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.

[toc] | [next] | [standalone]


#197836

FromLawrence D’Oliveiro <ldo@nz.invalid>
Date2026-07-30 08:06 +0000
Message-ID<114f0l7$1is0l$1@dont-email.me>
In reply to#197835
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.

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


#197838

FromMaria Sophia <mariasophia@comprehension.com>
Date2026-07-30 07:42 -0800
Message-ID<114frd8$2okp$1@nnrp.usenet.blueworldhosting.com>
In reply to#197836
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.

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


#197839

FromLawrence D’Oliveiro <ldo@nz.invalid>
Date2026-07-30 21:03 +0000
Message-ID<114ge7d$24ndu$3@dont-email.me>
In reply to#197838
On Thu, 30 Jul 2026 07:42:33 -0800, Maria Sophia 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.

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


#197843

FromMaria Sophia <mariasophia@comprehension.com>
Date2026-07-31 14:10 -0800
Message-ID<114j6g1$1o5m$1@nnrp.usenet.blueworldhosting.com>
In reply to#197839
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	.

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


#197846

FromMaria Sophia <mariasophia@comprehension.com>
Date2026-07-31 14:51 -0800
Message-ID<114j8sp$20pe$1@nnrp.usenet.blueworldhosting.com>
In reply to#197843
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.
	

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


#197847

FromMaria Sophia <mariasophia@comprehension.com>
Date2026-07-31 15:02 -0800
Message-ID<114j9hf$t5v$1@nnrp.usenet.blueworldhosting.com>
In reply to#197846
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 ¡V 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.

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


#197841

FromPiergiorgio Sartor <piergiorgio.sartor.this.should.not.be.used@nexgo.REMOVETHIS.de>
Date2026-07-31 09:35 +0200
Message-ID<pqvtjm-u55.ln1@lazy.lzy>
In reply to#197838
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

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


#197842

FromMaria Sophia <mariasophia@comprehension.com>
Date2026-07-31 13:59 -0800
Message-ID<114j5su$4kq$1@nnrp.usenet.blueworldhosting.com>
In reply to#197841
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.

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


#197845

FromLawrence D’Oliveiro <ldo@nz.invalid>
Date2026-07-31 22:49 +0000
Message-ID<114j8pa$34k3s$2@dont-email.me>
In reply to#197842
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.

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


#197848

FromMaria Sophia <mariasophia@comprehension.com>
Date2026-07-31 15:13 -0800
Message-ID<114ja7g$2elo$1@nnrp.usenet.blueworldhosting.com>
In reply to#197845
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.

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


#197850

FromPiergiorgio Sartor <piergiorgio.sartor.this.should.not.be.used@nexgo.REMOVETHIS.de>
Date2026-08-01 11:48 +0200
Message-ID<lvr0km-235.ln1@lazy.lzy>
In reply to#197848
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

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


#197853

FromMaria Sophia <mariasophia@comprehension.com>
Date2026-08-01 09:42 -0800
Message-ID<114lb5k$1ek2$1@nnrp.usenet.blueworldhosting.com>
In reply to#197850
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.

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


#197861

FromMaria Sophia <mariasophia@comprehension.com>
Date2026-08-03 09:28 -0800
Message-ID<114qj38$2r28$1@nnrp.usenet.blueworldhosting.com>
In reply to#197853
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?

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


#197857

FromChris <ithinkiam@gmail.com>
Date2026-08-02 17:48 +0000
Message-ID<114nvta$m7jf$1@dont-email.me>
In reply to#197850
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. 

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


#197858

FromMaria Sophia <mariasophia@comprehension.com>
Date2026-08-02 11:18 -0800
Message-ID<114o568$rh8$1@nnrp.usenet.blueworldhosting.com>
In reply to#197857
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?

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


#197859

FromLawrence D’Oliveiro <ldo@nz.invalid>
Date2026-08-02 22:54 +0000
Message-ID<114ohr2$rsgh$4@dont-email.me>
In reply to#197858
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.

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


#197860

FromMaria Sophia <mariasophia@comprehension.com>
Date2026-08-03 09:20 -0800
Message-ID<114qilb$1dva$1@nnrp.usenet.blueworldhosting.com>
In reply to#197859
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.

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


#197862

FromChris <ithinkiam@gmail.com>
Date2026-08-03 17:50 +0000
Message-ID<114qkdu$1hfre$1@dont-email.me>
In reply to#197858
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. 

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


#197886

FromMaria Sophia <mariasophia@comprehension.com>
Date2026-08-20 09:03 -0800
Message-ID<1167c0o$2nrm$1@nnrp.usenet.blueworldhosting.com>
In reply to#197862
Somewhat related to AI watermarking is this recent article in the news.

 *What does it mean to put a watermark on AI text?*
 <https://edition.cnn.com/2026/08/20/us/word-of-the-week-ai-watermark-cec>

Which references: 
 *How Claude's text watermark works*
 <https://www.anthropic.com/news/claude-text-watermark>

 *How AI text watermarking works*
 <https://declaude.org/watermarking/>

Where apparently there is an EU law which requires watermarking...
 <https://edition.cnn.com/2026/08/11/business/video/invisible-watermarks-coming-claudes-ai-written-text-digvid-vrtc>

There's also this referenced, but it's behind a paywall.
 *Technology that can detect text written by AI with 99.9% certainty*
 <https://www.wsj.com/tech/ai/openai-tool-chatgpt-cheating-writing-135b755a>

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


Page 1 of 2  [1] 2  Next page →

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


csiph-web