# Photo pixel classification `classify` gives a pixel opinion on a **photograph**: camera-like, generated, or unknown, and optionally which renderer it resembles. It is a separate command from `identify`. Metadata inspection never starts it. This page is the library guide. The Hugging Face model card is the same freeze written for Hub publication: [photo-classify-hf/README.md](photo-classify-hf/README.md). Campaign notes and rejected variants live in [AI-generated image classifiers](ai-generated-image-classifiers.md). ## When to use it Use `identify` for C2PA, IPTC, visible marks, and other provenance. Use `classify` when those signals are gone and the file is still a photograph. Do not use `classify` to prove a file is clean. Do not use it on UI or digital art. Receipt photographs are detected by the receipt-document gate and abstain to `unknown` instead of a false `ai`; they are still not a supported verdict. Do not treat `provider=openai` as a decoded SynthID payload. ## Install and run ```bash uv tool install --force "remove-ai-watermarks[classify]" remove-ai-watermarks classify image.png remove-ai-watermarks classify image.png --json ``` ```python from pathlib import Path from remove_ai_watermarks.classify import classify_pixels result = classify_pixels(Path("image.png")) print(result.label, result.detector, result.provider) ``` Missing extra raises `RuntimeError` with `pip install 'remove-ai-watermarks[classify]'`. `device` is a library parameter (`None` / `"auto"` / `"cpu"` / `"cuda"`), not a CLI option. Weights are not in git. First call downloads [`wiltodelta/raiw-photo-classify`](https://huggingface.co/wiltodelta/raiw-photo-classify) at the freeze revision pinned in `classify.py`, or reads `RAIW_CLASSIFY_WEIGHTS` if that directory already has `clip-l-ft.pt`, `probe-weights-clip-l-ft.npz`, `detector.pt`, and `provider.pt`. The training catalog is not published with the package. ## What one call returns One request runs both heads. The receipt gate and Model 2 run only after Model 1 is DEFINITELY. ```mermaid flowchart TD image[Photograph] --> m1[Model 1 CLIP-L-ft ridge AND MLP] m1 -->|likely_human| human["label human, provider none"] m1 -->|possibly| unk["label unknown, provider none"] m1 -->|definitely| rg[Receipt gate on the same CLIP vector] rg -->|receipt document| rgu["label unknown, detector definitely, provider none"] rg -->|not a receipt| m2[Model 2 124-d focal heads] m2 -->|openai google muse-image tc260| named["label ai plus provider"] m2 -->|no_ai or extract fail| aiOnly["label ai, provider none"] ``` | Field | Values | Meaning | | --- | --- | --- | | `label` | `ai`, `human`, `unknown` | Public verdict. `ai` only on DEFINITELY | | `domain` | `photo` | This freeze is photographic only | | `detector` | `definitely`, `possibly`, `likely_human` | Raw Model 1 gate | | `provider` | `openai`, `google`, `muse-image`, `tc260`, or `None` | Model 2, only if `label` is `ai` | `unknown` with `detector=definitely` is the receipt-document gate abstaining: the file looked AI-generated to Model 1 and like a receipt photograph to the gate, so no verdict is published and no provider is read. `human` is camera-like under this contract, not a proof of authorship. `unknown` is POSSIBLY, or a domain this freeze does not support. `provider=None` on an `ai` file is often FLUX or another generator that is not one of the four heads, or a 124-d extract refusal. ## Approach CLIP-L content embeddings separate generated photographs from camera photographs. They do not separate generated photographs from receipts or UI. A ridge on fine-tuned CLIP-L (last two vision blocks, 224 letterbox) is Model 1. A small MLP on the same vectors, ANDed with the ridge, is the freeze DEFINITELY gate. Provider attribution uses a different feature: 124-d residual ratios on 256 px patches. OpenAI versus Gemini is a strong split there. "AI or not" is the wrong job on that bank, which is why Model 2 is gated. Provider names the renderer, not the product UI. Bing Image Creator signed Microsoft, OpenAI scores `openai`. Designer signed Microsoft, Google LLC scores `google`. There is no Microsoft pixel class. `openai` and `google` are provider classes. `muse-image` is Muse Image output, not a general Meta class. `tc260` is the China AIGC label standard, not one producer: Doubao, Jimeng, Qwen, Kling, and others share that residual class. ## Evaluation Two CPU retrains were byte-identical. DEFINITELY is the shipped cut. | Head | Cell | Result | | --- | --- | ---: | | Detector DEFINITELY | AI-test | 92.4% (n=1,847) | | Detector DEFINITELY | Open Images fresh | 1.3% (n=3,000) | | Detector DEFINITELY | Kodak | 0/24 | | Detector DEFINITELY | FLUX hold | 83.0% (n=300) | | Class | OpenAI | 90.8% (345/380 of 381) | | Class | Google | 90.9% (339/373 of 377) | | Class | TC260 | 78.6% (298/379 of 384) | | Class | Muse Image hold-out v3 | 89.4% (177/198) | | Class | Muse Image hold-out pooled | 88.4% (243/275 listed 277) | | Class | meme templates, ungated | 29.1% leak | The ungated meme row is why Model 2 never runs on `human` or `unknown`. ## Receipt-document gate Added 2026-09-02. A linear head on the CLIP-L-ft vector Model 1 already computes, trained on CORD-v2 train (800, CC BY 4.0) plus 200 synthetic capture-style receipts against 2,400 ai_train negatives. The threshold is certified on CORD validation plus a synthetic holdout; no field-receipt pixels were used for training or the threshold. On DEFINITELY only: a hit publishes `unknown` and skips the 124-d provider pass. | Cell | Result | | --- | ---: | | Field phone receipts, false `ai` before | 19/59 -> **2/59** | | Field phone receipts, gated to `unknown` | 57/59 | | CORD photographs (99) | **0/99** false `ai` | | ai_test DEFINITELY cost | 7/1,847 (0.38%) | Artifacts: `receipt-gate-shipped-2026-09-02/report.json` (private research tree). The head ships with the model (`receipt-gate-2026-09-02.npz` in the Hub snapshot, package asset as the fallback for pre-gate freezes); the operating point records it in [photo-classify-hf/operating-point.json](photo-classify-hf/operating-point.json). ## Limits - Receipts abstain through the 2026-09-02 gate (below); a photorealistic AI receipt can still score as `human`. UI, scans, maps, and community art remain out of contract. - Not SynthID, not C2PA, not `is_ai_generated`. - Images under 256 px cannot yield 124-d features, so provider abstains. - FLUX is a hold-out at 83% DEF, not a named provider class. - `tc260` is mixed producers under one label standard. A later retrain should split it by manufacturer. `identify`, `has_invisible_target`, `all`, and `invisible` do not import this module. A no-signal provenance result stays unknown until you call `classify` yourself. Training data, the retrain pack (caches only, no images), and `scripts/retrain_photo_classify.py` are in [photo-classify-training.md](photo-classify-training.md).