The feature ladder — v1.3 → v1.8
My incremental climb from hypothesis baseline to the frozen warp engine, curated from my original research notebooks.
After v1.3 I worked deliberately incrementally: add one feature family at a time, measure the AUC step, and write down why it moved — I was drafting the fundamental analysis for the paper as much as for the model.
The ladder
| version | added | per-tile AUC |
|---|---|---|
| v1.3 | hypothesis baseline only: fragility_rate + cross_residual_corr | 0.627 |
| v1.4 | + local_var (spatial heterogeneity of the residual), + cross_recovery_ratio_mean (symmetry of reconstruction failure across geometric tile pairs) | 0.688 |
| v1.5 | constrain to features with confirmed hypothesis direction | 0.754 |
| v1.6 | high-pass content isolation (data prep, not a feature) | — |
| v1.7 | + 4 seam features (boundary residual + spectral discontinuity) | 0.761 |
| v1.8 | + circ_var_noise, composite_noise → the canonical 10 | 0.775 |
v1.3: two hypothesis-grounded features, AUC 0.627. Weak — but every point of it is physics, and SHAP shows cross_residual_corr carrying the load.
The v1.5 feature space that justified the constraint: low recovery + high fragility → real; high recovery + low fragility → generated. The hypothesis's direction, visible as a linear boundary in two dimensions.
v1.6 — the pivot that made the signal honest
Raw residual is contaminated: high-texture tiles produce large residuals regardless of origin, so my early fragility_rate was partly a texture detector. I fixed this in v1.6 with a single preprocessing change: subtract a low-frequency (blurred) copy of the residual from itself, leaving only the high-frequency component.
High-pass filtering strips the low-frequency content component, leaving the PRNU-dominated residual. Every downstream feature is computed on this noise residual. This mattered most for intricate textures — fabrics and fine patterns where content energy would otherwise drown the sensor-noise signal. It is the cleanest single improvement I made in the whole project.
An implementation footnote from v1.7 that I nearly missed: computing the return warp as an exact matrix inverse rather than an independently-fitted reverse transform cost a sliver of AUC (~0.0015) — the exact inverse round-trip is marginally less discriminative than the independently-fit one. Small asymmetries in the probe are information.
The v1.8 engine
The frozen configuration is a dozen small constants — tile geometry, the differential strength pair, high-pass and seam windows — which I'm withholding here. But the shape of the result matters more than the values: my ablation confirmed they sit on a flat optimum (neighboring tile sizes and high-pass widths differ by < 0.004 AUC; I tested a much smaller tile from my early planning notes and rejected it). Warp strength climbs as a differential pair, and the homographies per strength stay random — consistency across trials is exactly what averaging provides without sacrificing isotropy.
One tile through the probe: original → warped → reconstructed → per-pixel error; below, the trial-accumulated residual, its high-passed version, the structured-noise ratio, and residual edge structure. This is the entire hypothesis in one figure.
The ten features, spatially
| # | feature | reads |
|---|---|---|
| 1 | fragility_rate | ∂R/∂s — PRNU destruction rate |
| 2 | local_var | spatial heterogeneity of reconstruction failure |
| 3 | circ_var_noise | incoherence of the residual gradient field |
| 4 | composite_noise | joint magnitude × coherence |
| 5 | cross_residual_corr | shared physics across diagonally-mirrored tiles |
| 6 | cross_recovery_ratio_mean | symmetry of reconstruction failure |
| 7–8 | seam_h/v_disc | boundary residual discontinuity |
| 9–10 | seam_h/v_spectral | boundary spectral discontinuity |
circ_var_noise earned its place on the hardest cases — iPhone-based pictures, even downsampled (JPEG-compressed remained a known problem). And an XGBoost on these 10 scalars per tile:
Per-tile, the signal is deliberately weak (≈0.60 on the harder scaled corpus; 0.775 on the v1.8 dataset) — a single tile is a whisper. circ_var_noise and local_var lead the SHAP ranking. Aggregation is where the whisper becomes a verdict.
Aggregation: center weight and the voting insight
In v1.8 I added texture-, center-, and confidence-weighting to the image-level aggregation over the tile grid. The center bias encodes something I kept seeing: patches containing subject matter have richer texture → stronger PRNU signal (real) or clearer generation artifacts (generated) → more reliable predictions. Edge and background patches with smooth uniform content are noisy voters that dilute the signal.
On the ~15k-image run:
real : maj = 87.1% wgt = 65.8%
generated : maj = 94.7% wgt = 96.1%
overall : maj = 90.9% wgt = 80.9%I closed the chapter in v1.8.2 with an analytic (model-free) scorer probe, confirming that the warp and the features — not the trees — do the discriminative work: a hand-calibrated scorer on the same 10 features approaches the XGBoost, which is exactly what I wanted to be true of a physics claim.
I froze v1.8 there. Everything after it builds around this engine, not inside it — starting with the fusion that broke the warp ceiling.