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