Re: PSA Using Python Pillow to foil camera image PRNU fingerprinting
Maria Sophia <[email protected]> Thu, 30 Jul 2026 07:42:33 -0800
| Newsgroups | rec.photo.digital,comp.lang.python |
|---|---|
| Organization | BWH Usenet Archive (https://usenet.blueworldhosting.com) |
| Message-ID | <[email protected]> |
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.