pii-image-redactor / README.md
louis030195's picture
docs: model card with bench numbers + privacy contract
f1943f2 verified
---
license: cc-by-nc-4.0
language:
- en
library_name: onnxruntime
pipeline_tag: object-detection
tags:
- pii
- privacy
- redaction
- object-detection
- rf-detr
- screen-capture
- accessibility
- computer-use
- agentic
- screenpipe
metrics:
- zero-leak
- oversmash
- precision
- recall
extra_gated_prompt: >-
This model is licensed CC BY-NC 4.0 (non-commercial). For commercial
use — production deployment, SaaS / API embedding, agent privacy
middleware, custom fine-tunes — contact louis@screenpi.pe.
---
# screenpipe-pii-image-redactor
> A [screenpipe](https://screenpi.pe) project. The image-modality
> companion to [`screenpipe/pii-redactor`](https://huggingface.co/screenpipe/pii-redactor).
A fine-tuned **image PII detector** for the same three surfaces an AI
agent sees a user's machine through:
1. **Screen captures** — JPGs / PNGs of the user's screen, rendered
text and structured chrome (Slack, Outlook, Cursor, Terminal,
Confluence, GitHub, 1Password, calendars, browsers).
2. **Computer-use traces** — the visual frames an agentic model
(Claude Computer Use, GPT operator, etc.) reads when it controls a
desktop.
3. **Accessibility-tree visualizations** — when an agent screenshots
what it inferred from the AX tree to debug a tool call.
These surfaces are **dense, multi-PII, semi-structured** in ways no
prose-trained PII detector handles well. Returns pixel-space bounding
boxes for 12 canonical PII categories.
ONNX, ~108 MB. Same `.onnx` ships across macOS / Windows / Linux —
the user's ONNX Runtime selects the Execution Provider at load time
(CoreML, DirectML, CUDA, or CPU baseline).
> **License: CC BY-NC 4.0** (non-commercial). For commercial use —
> production redaction, SaaS / API embedding, AI-agent privacy
> middleware, custom fine-tunes — contact **louis@screenpi.pe**. See
> [`LICENSE`](LICENSE).
## Headline numbers
`rfdetr_v8` on a held-out 221-image validation split (190 PII-bearing,
31 hard negatives) of the [screenpipe-pii-bench-image](https://github.com/screenpipe/screenpipe-pii-bench-image)
corpus, IoU ≥ 0.30:
| metric | this model | regex+OCR floor | Microsoft Presidio (published OSS) |
|---|---:|---:|---:|
| **zero-leak** (every gold span caught) | **95.3%** | 2.6% | 0.5% |
| **oversmash** (false-fire on negatives) | **0.0%** | 3.2% | 48.4% |
| micro-precision | 99% | 87% | 47% |
| micro-recall | 97% | 26% | 42% |
| macro-F1 | 0.871 | 0.318 | 0.190 |
Per-label recall (a few highlights): `private_person` 0.99 ·
`private_company` 1.00 · `private_repo` 1.00 · `private_url` 1.00 ·
`secret` 0.99 · `private_email` 0.98 · `private_phone` 0.92 ·
`private_address` 0.92.
### Latency (rfdetr_v8, 320×320 input, FP32)
| platform | EP | p50 |
|-------------------------------|-----------|----------:|
| macOS Apple Silicon (M-series) | CoreML | **66 ms** ([real-screen sample](https://github.com/screenpipe/screenpipe-pii-bench-image)) |
| macOS Apple Silicon (M-series) | CPU | 163 ms |
| Windows + DX12 GPU | DirectML | ~30-60 ms (estimated) |
| Linux + NVIDIA | CUDA | ~10-20 ms (estimated) |
| Linux/Windows CPU-only | CPU | ~140 ms |
Same `.onnx` everywhere — Execution Provider is selected at load time
by the user's ONNX Runtime build. **No CUDA / Vulkan / GPU vendor SDKs
required at the consumer.**
## Why this exists (vs Presidio Image Redactor and friends)
The published baselines are trained on prose / generic-document
imagery. A typical screenpipe frame looks nothing like that:
- A Slack channel sidebar with 8 names, 12 channel mentions, 3 emails,
and 1 pasted AWS key — all in 1440×900 px at 14 px font.
- A 1Password vault entry with structured `[Username | Password |
Server | One-time password]` rows, half of which are masked dots.
- A Cursor workspace open on `.env.production` with five secret-shaped
values stacked top-to-bottom.
These images are **dense** (10-20 PII spans per frame), **structured**
(rows / columns / aligned chrome), and **layout-cued** (a thing in the
"Username" cell is a username regardless of its surface text). A
generic NER-on-OCR pipeline misfires by over-redacting UI chrome
(48% false-fire on negatives in our bench, vs. 0% for this model).
If you're building an **agentic system that reads screen state** — a
desktop-control agent, a memory layer for browsing, anything that
streams screen captures into an LLM — this is the redactor designed
for that pipe.
## What it does
Per-image **object detection**. Given a JPG or PNG, returns
`[(bbox, label, score)]` where each detection is a region the model
thinks is PII, classified into one of the 12 canonical categories
shared with [`screenpipe/pii-redactor`](https://huggingface.co/screenpipe/pii-redactor):
```
private_person, private_email, private_phone, private_address,
private_url, private_company, private_repo, private_handle,
private_channel, private_id, private_date, secret
```
`secret` covers passwords, API keys, JWTs, DB connection strings,
PRIVATE-KEY block markers, etc. — same coverage as the text model.
## Inference
```python
# pip install onnxruntime pillow numpy
import numpy as np
import onnxruntime as ort
from PIL import Image
CLASSES = [
"private_person", "private_email", "private_phone",
"private_address", "private_url", "private_company",
"private_repo", "private_handle", "private_channel",
"private_id", "private_date", "secret",
]
INPUT_SIZE = 320 # rfdetr_v8 was exported at 320x320
THRESHOLD = 0.30
sess = ort.InferenceSession(
"rfdetr_v8.onnx",
providers=["CoreMLExecutionProvider", "CPUExecutionProvider"],
)
img = Image.open("screenshot.png").convert("RGB")
W, H = img.size
resized = img.resize((INPUT_SIZE, INPUT_SIZE), Image.BILINEAR)
arr = np.asarray(resized, dtype=np.float32) / 255.0
arr = (arr - [0.485, 0.456, 0.406]) / [0.229, 0.224, 0.225]
arr = arr.transpose(2, 0, 1)[None].astype(np.float32) # NCHW
boxes, logits = sess.run(None, {sess.get_inputs()[0].name: arr})
boxes = boxes[0] # (300, 4) cx, cy, w, h normalized
logits = logits[0] # (300, 13) — last channel is "no-object"
probs = 1.0 / (1.0 + np.exp(-logits[:, :12])) # per-class sigmoid
best_class = probs.argmax(axis=1)
best_score = probs[np.arange(300), best_class]
keep = best_score >= THRESHOLD
for q in np.where(keep)[0]:
cx, cy, bw, bh = boxes[q]
x1 = (cx - bw / 2) * W
y1 = (cy - bh / 2) * H
print(f" {CLASSES[best_class[q]]:18} score={best_score[q]:.2f} "
f"bbox=[{int(x1)}, {int(y1)}, {int(bw*W)}, {int(bh*H)}]")
```
Full example with image overlay → `examples/inference.py`.
For Rust integration via the `ort` crate, see the
[`rust_smoke/`](https://github.com/screenpipe/screenpipe-pii-bench-image/tree/main/rust_smoke)
prototype and the production wiring in PR
[`screenpipe/screenpipe#3188`](https://github.com/screenpipe/screenpipe/pull/3188).
## Redacting the image (vs. just detecting)
This model **detects**. To actually remove the PII, draw a solid
rectangle over each detected bbox. Solid black, **not blur** — blur
is reversible by super-resolution attacks; opaque rectangles aren't.
```python
from PIL import ImageDraw
draw = ImageDraw.Draw(img)
for det in detections: # from the snippet above
x, y, w, h = det.bbox
draw.rectangle([x, y, x + w, y + h], fill=(0, 0, 0))
img.save("screenshot_redacted.png")
```
That's the entire redactor wrapper. ~5 lines.
## Architecture
- Base: [RF-DETR-Nano](https://github.com/roboflow/rf-detr) (Roboflow,
ICLR 2026) — DINOv2-backbone real-time detection transformer, ~25 M
params, claims first real-time model to break 60 mAP on COCO.
- Fine-tuned at 320×320 input on a 2,833-image synthetic + WebPII
union (synthetic via DOM-truth bbox extraction; WebPII via the
[arxiv 2603.17357 release](https://arxiv.org/abs/2603.17357)).
- Output head: 300 detection queries × 13 channels (12 PII classes +
no-object). Per-class sigmoid (NOT softmax — RF-DETR uses
independent classification per query).
- Trained on a single A100 80 GB; ~100 minutes wall-clock for the
best-EMA epoch.
## What was the training data
| source | size | labels | notes |
|---|---:|---|---|
| **synthetic bench** | 2,206 imgs | DOM-truth bboxes (pixel-perfect) | 9 templates rendered via headless Chromium with `data-span` attributes — labels come from the same DOM tree the browser laid out. |
| **WebPII** | 500 imgs (balanced sample) | bbox-labeled by the original authors | March 2026 release, e-commerce screenshots. Class-imbalance capped at 2× our synthetic frequency. |
| **cascade auto-labels** | 100 imgs | OCR + text-PII model alignment | Old screenshots from this project's own bench, weakly labeled. |
**No real user data was used during fine-tuning.** Membership
inference attacks recover no real-user content because no real-user
content was in the training set. If you discover a failure mode on
your real screens, the project's recipe is to add a new SYNTHETIC
template that reproduces it — the screenshot becomes a bug report,
never a training row.
## Limitations
1. **Hand-curated gold set is small** — bench `data/` has 5
manually-built cases. Larger-scale held-out evaluation depends on
the synthetic corpus, which is in-distribution by construction.
2. **`private_handle` and `private_id` recall are 0%** in the
reference numbers because the val split has only 2 and 1 examples
respectively. Don't deploy without a domain-specific eval pass.
3. **Synthetic-template ceiling.** 95.3% zero-leak is the bench's
stable ceiling at this corpus size. Gains beyond come from training
on more real-screen failure modes (tracked in the bench's backlog).
4. **WebPII is e-commerce-heavy.** Adding the full WebPII split
actually *hurt* dev-app accuracy in our experiments (rfdetr_v4 at
90.5% zero-leak vs. v8's 95.3%). The 500-image balanced sample is
our best-of-both compromise.
5. **CPU-only floors at ~140 ms p50.** INT8 quantization (planned)
gets that under 100 ms, but the FP32 release is what's on this
page today.
6. **English-only.** Synthetic templates render Latin-script text;
the WebPII supplement is English. CJK / Arabic / Cyrillic not
evaluated — don't deploy without a locale-specific eval.
7. **Adversarial robustness not tested.** A user who knows the
detector exists could craft layouts that confuse it (handwritten
PII, embedded-image PII, partial occlusion). Use this for
honest-user privacy, not as a security boundary.
## Files
```
rfdetr_v8.onnx 108 MB · the model · sha256 below
README.md this file
LICENSE CC BY-NC 4.0
NOTICE attribution to base model + datasets
examples/
inference.py the snippet above, runnable
```
SHA-256 of `rfdetr_v8.onnx`:
`431acc0f0beb22a39572b7a50af4fc446e799840fb71320dc124fbd79a121eb3`
## Reproducing inference
```bash
git clone https://huggingface.co/screenpipe/pii-image-redactor
cd pii-image-redactor
git lfs pull
pip install onnxruntime pillow numpy
python examples/inference.py path/to/your_screenshot.png
```
Reproducing the eval scores requires the screenpipe-pii-bench-image
benchmark, which is not redistributed (it's the training corpus).
Contact **louis@screenpi.pe** for benchmark access or commercial
licensing.
## License
[CC BY-NC 4.0](LICENSE) — non-commercial use only. The base model
(RF-DETR) is Apache-2.0; obligations are preserved (see
[`NOTICE`](NOTICE)).
For commercial licensing (production deployment, redistribution
rights, SaaS / API embedding, custom fine-tunes for your domain):
**louis@screenpi.pe**.
## Citation
```bibtex
@misc{screenpipe-pii-image-redactor-2026,
title = {screenpipe-pii-image-redactor: a screen-PII detector for
accessibility-aware agents},
author = {{screenpipe}},
year = {2026},
url = {https://huggingface.co/screenpipe/pii-image-redactor}
}
```