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.
-- 
https://mail.python.org/mailman3//lists/python-list.python.org

Reply via email to