Pseudopros
Pseudo (false) + Prosopon (face/mask)
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣠⣾⡆⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣠⣶⣿⣿⣿⠃⣠⠀⠀⠀
⠀⠀⠀⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣀⣤⣶⣿⣿⣿⣿⣿⠇⢰⣿⣇⠀⠀
⠀⠀⣼⣿⣿⣶⣤⣀⡀⠀⠀⠀⠀⠻⠿⠟⠛⣿⣿⣿⣿⣿⣿⡏⠀⠛⣿⣿⡄⠀
⠀⢠⣿⣿⣿⣿⣿⣿⣿⣿⣿⣶⣶⣶⣶⡆⢀⡛⠛⠛⠛⢿⣿⣆⣀⣠⣿⣿⡧⠀
⠀⢸⣉⠁⠀⢠⣿⣿⠁⣈⡙⠛⠻⠿⠿⠃⠸⠿⠇⠀⠀⢈⡁⢸⣿⣿⡿⠟⣡⠀
⠀⣿⣿⣆⣀⣸⣿⡿⠀⣿⣿⣿⣷⣶⣶⣶⣶⣶⣶⣾⣿⣿⣇⠈⠟⢉⣤⣾⡇⠀
⠀⢸⣿⣿⣿⣿⣿⡇⠀⠿⠿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⠿⠀⣶⣿⣿⠟⠀⠀
⠀⠈⣉⣉⣉⣉⡉⠓⠀⣶⣦⣤⠀⠉⠙⣿⣿⡏⠉⠀⣤⣤⡖⠀⠉⠉⠁⠀⠀⠀
⠀⠀⠘⢿⣿⣿⣿⣿⡀⢿⣿⣿⣶⣤⠀⣿⣿⠁⢠⣶⣿⣿⠇⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠈⠛⠻⠿⠟⠃⠘⣿⣿⣿⣿⣶⣿⣿⣶⣿⣿⣿⡿⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠹⣿⣯⣉⠉⠉⠉⢉⣩⣿⡿⠁⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠘⢿⣿⣿⣶⣿⣿⣿⠟⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠙⠛⠛⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
A Python library for cryptographically secure facial anonymization in computer vision pipelines.
Pseudopros replaces real faces in video streams with deterministic synthetic faces that:
- Cannot be re-identified - output faces are cryptographically unlinked from the original (validated against ArcFace ReID)
- Are repeatable - the same input face always produces the same synthetic output for a given key and salt
- Are cross-source - the same key and salt produce consistent identities across independently captured video of the same subject
- Preserve motion - expression, pose, and movement data are retained for downstream tracking and aggregate analysis
Intended for use within streaming pipelines (e.g. GStreamer). Keys and salts are passed as parameters and should be stored remotely, never embedded with the video.
Security properties
| Property | Mechanism |
|---|---|
| Re-identification resistance | LDM-generated face is orthogonal to original in ArcFace space (validated: similarity ≈ -0.03 vs threshold 0.28) |
| Repeatability | HKDF is deterministic; same inputs always produce the same seed and therefore the same synthetic face |
| Cross-source consistency | Key derivation depends only on the identity embedding, not the video source |
| Key isolation | Rotating the salt changes all synthetic identities without changing the secret |
Cryptography
pseudopros depends on the PyCA cryptography library for one purpose: HKDF-SHA256 key derivation in keying.py.
HKDF (HMAC-based Key Derivation Function, RFC 5869) turns a secret key and a subject embedding into a fixed-length seed:
| Input | Value |
|---|---|
| IKM (input key material) | Your secret bytes (secret) |
| Salt | Per-deployment salt (salt) |
| Info | b"pseudopros-v1:" + subject_embedding_bytes |
| Output length | 32 bytes |
The info field provides domain separation: it binds the derived seed to this library version and to the specific subject identity. Changing the subject embedding, the salt, or the secret each produces an independent seed.
No encryption, signing, MAC verification, or other crypto primitives are used anywhere else in the codebase.
How it works
- Enrollment - ArcFace embeds the subject's face into a 512-dimensional identity vector
- Key derivation - HKDF-SHA256 over
(secret, salt, identity_vector)produces a 32-byte seed - Synthetic face generation - the seed drives a Latent Diffusion Model to produce a deterministic synthetic face unrelated to the original
- Animation - First Order Motion Model transfers the subject's motion onto the synthetic face frame by frame
- Paste-back - the animated region is blended back into the original frame with a soft oval mask
The synthetic face is the cryptographic output: it is a pure function of (secret, salt, subject) with no recoverable path to the original identity.
Installation
pip install pseudopros
Requires Python 3.12. PyTorch is installed from the PyTorch CPU index on Intel Mac:
pip install pseudopros --extra-index-url https://download.pytorch.org/whl/cpu
On first use, pseudopros downloads three models:
- InsightFace buffalo_l (~280 MB, cached in
~/.insightface/) - ArcFace face detection and embedding - dactylroot/pseudopros-fomm (~695 MB, cached in
~/.cache/huggingface/) - First Order Motion Model checkpoint - CompVis/ldm-celebahq-256 (~1.3 GB, cached in
~/.cache/huggingface/) - Latent Diffusion Model for synthetic face generation (only when usingRosterSynthesizer)
Usage
pseudopros has two deployment modes, reflected directly in the package
structure - see experiments/CONCEPT.MD for the full picture:
- Primary (
pseudopros.Anonymizer, below) - the full pipeline, including its own face detection. A transparent, drop-in replacement for a raw video source. - Secondary (
pseudopros.Renderer, needspip install pseudopros[secondary]) - rendering only, no detection, for use after an existing CV pipeline's own detection/localization has already happened. See "Secondary mode" below.
Common usage (Primary mode)
import pseudopros
# Build the pipeline (FOMM weights auto-download ~695 MB on first call)
anon = pseudopros.Anonymizer.build(
secret=b"your-secret-key-min-16-bytes",
salt=b"your-deployment-salt",
)
# Open-world by default: every face process() sees is anonymized, including
# ones never explicitly enrolled - auto-enrolled on first sight so the same
# real person gets a stable synthetic identity across frames.
for frame in video_frames:
output = anon.process(frame) # HWC uint8 RGB, same shape as input
Pre-enrolling still works, and is how you find out who pseudopros is currently tracking (auto-enrolled subjects show up here too):
anon.enroll(frame_of_alice, "alice")
print(anon.enrolled_ids) # ["alice", "auto:...", ...]
anon.unenroll("alice")
For a bounded set of known subjects instead - anonymize only pre-enrolled
people, leave everyone else untouched, and skip the per-face work of
deriving a style/motion state for strangers - pass closed_roster=True:
anon = pseudopros.Anonymizer.build(
secret=b"your-secret-key-min-16-bytes",
salt=b"your-deployment-salt",
closed_roster=True,
)
anon.enroll(frame_of_alice, "alice")
anon.enroll(frame_of_bob, "bob")
for frame in video_frames:
output = anon.process(frame) # only alice/bob get anonymized
Each subject keeps their own motion baseline across frames. Caution for
open-world, long-running deployments: auto-enrolled subjects accumulate in
memory for the process's lifetime (no eviction policy yet) - see
Anonymizer's docstring.
Secondary mode
For a pipeline where face detection/localization already happens upstream
(your own detector, not pseudopros's) - Renderer never calls a face
detector per frame; ArcFace is used once, at enrollment, for the style code:
# pip install pseudopros[secondary]
from pseudopros import Renderer
renderer = Renderer.build(
checkpoint_path="path/to/renderer_final.pt",
secret=b"your-secret-key-min-16-bytes",
salt=b"your-deployment-salt",
)
renderer.enroll(frame_of_alice)
# Already have a face crop from your own detector (the common case):
anonymized_crop = renderer.process(face_crop)
# Or have a full frame + bounding box instead:
anonymized_frame = renderer.process(frame, bbox=[x1, y1, x2, y2])
To use a local FOMM checkpoint instead of downloading:
anon = pseudopros.Anonymizer.build(
secret=b"your-secret-key-min-16-bytes",
salt=b"your-deployment-salt",
checkpoint_path="path/to/fomm.pth.tar",
config_path="path/to/fomm-config.yml",
)
Security validation
import pseudopros
audit = pseudopros.SecurityAudit(embedder)
result = audit.audit_frame(original_frame, anonymized_frame)
print(result.is_secure) # True if similarity < threshold
print(result.similarity) # ArcFace cosine similarity (lower is more secure)
# Audit a sequence
summary = audit.audit_sequence(original_frames, anonymized_frames)
print(summary)
# SecurityAudit - 30 frame(s)
# All secure: True
# Threshold: 0.280
# Max similarity: 0.0031
# Mean similarity: -0.0018
# Face detection: 30/30 anonymized frames
# Beyond leakage: is each subject's synthetic identity stable across frames,
# and are different subjects' synthetic identities distinguishable from
# each other? Same ArcFace-cosine-similarity methodology.
consistency = audit.audit_consistency(alice_anonymized_frames)
print(consistency.mean_similarity) # higher = more stable pseudonym
distinctiveness = audit.audit_distinctiveness([
alice_anonymized_frames, bob_anonymized_frames,
])
print(distinctiveness.all_distinct) # True if every pair stayed below threshold
GStreamer integration (Primary mode)
pseudopros.gst registers a pseudopros in-place video filter element -
the Primary-mode convenience wrapper, appropriate for a general-purpose
filter since it does its own detection. Requires GStreamer's Python
bindings (python3-gi), installed separately from pseudopros itself:
import base64
import pseudopros.gst as ppgst
ppgst.register()
secret = base64.b64encode(b"your-secret-key-min-16-bytes").decode()
salt = base64.b64encode(b"your-deployment-salt").decode()
pipeline = Gst.parse_launch(
f"v4l2src ! videoconvert ! video/x-raw,format=RGB"
f" ! pseudopros secret={secret} salt={salt}"
f" ! videoconvert ! autovideosink"
)
Add closed-roster=true enrollment-image=/path/to/face.jpg to the element
properties for the closed-roster/single-known-subject variant instead of
the open-world default. See pseudopros/gst.py's module docstring for the
full property list.
Configuring the synthesizer
The default synthesizer is NoiseSynthesizer - a fast placeholder that produces non-face-like output. Use it during development to skip the LDM model download.
For production anonymization, use RosterSynthesizer (downloads ~1.3 GB on first call):
import pseudopros
anon = pseudopros.Anonymizer.build(
secret=b"your-secret-key",
salt=b"your-salt",
synthesizer=pseudopros.RosterSynthesizer(num_steps=50),
)
For large known subject populations, RapidSynthesizer uses a pre-generated face bank for O(1) enrollment with no per-subject model inference:
anon = pseudopros.Anonymizer.build(
secret=b"your-secret-key",
salt=b"your-salt",
synthesizer=pseudopros.RapidSynthesizer("path/to/face_bank.npy"),
)
# Build a face bank offline (one-time, ~20s per face on CPU)
pseudopros.build_face_bank(n=10_000, output_path="face_bank.npy")
Advanced / custom components
Lower-level components are importable from their submodules when you need to customize the pipeline:
# Custom embedder (e.g. swap InsightFace for another ArcFace backend)
from pseudopros.embedding import InsightFaceEmbedder, FaceEmbedder, DetectedFace
embedder = InsightFaceEmbedder(model_pack="buffalo_sc") # lighter model
anon = pseudopros.Anonymizer(model, key, embedder)
# Custom synthesizer (implement SynthesizerBackend protocol)
from pseudopros.synthesizer import SynthesizerBackend
class MyGANSynthesizer:
def synthesize(self, seed: bytes) -> np.ndarray:
... # return (256, 256, 3) float32 [0, 1]
# Access FOMM directly
from pseudopros.fomm import FommModel, FommSubjectState
model = FommModel.from_pretrained(device="cpu") # auto-download
model = FommModel.from_checkpoint("model.pth.tar", "config.yml", device="cpu") # local
# Per-subject stateless API — safe to interleave subjects on a shared model
state_alice = model.prepare_subject(reference_image_alice)
state_bob = model.prepare_subject(reference_image_bob)
output_alice, state_alice = model.animate_subject(driving_frame_alice, state_alice)
output_bob, state_bob = model.animate_subject(driving_frame_bob, state_bob)
# Audit result types
from pseudopros.security import AuditSummary, FrameAuditResult
Command-line interface
Anonymize a video file directly from the terminal:
pseudopros anonymize input.mp4 output.mp4 \
--enrollment face.jpg \
--secret-file /path/to/secret.bin \
--synthesizer roster
Secret options (pick one):
| Flag | Description |
|---|---|
--secret-file PATH |
Read raw secret bytes from a file. Recommended for production. |
--secret-hex HEX |
Secret as a hex string. |
PSEUDOPROS_SECRET |
Env var (hex). Used when no flag is provided. |
Additional options: --salt-hex HEX, --device cpu\|cuda\|mps, --checkpoint PATH --config PATH (to use a local FOMM checkpoint instead of downloading).
GStreamer integration
pseudopros.gst provides a pseudopros GStreamer element that applies face anonymization as an in-place video filter. It is a separate submodule so that the rest of the library works without GStreamer installed.
Requirements
# Debian / Ubuntu
sudo apt-get install python3-gi gstreamer1.0-python3-plugin-scanner \
gstreamer1.0-plugins-base gstreamer1.0-plugins-good
# macOS (Homebrew)
brew install pygobject3 gstreamer
Usage
Register the element once after Gst.init(), then use it in any pipeline string:
import gi
gi.require_version("Gst", "1.0")
from gi.repository import Gst
import pseudopros.gst as ppgst
Gst.init(None)
ppgst.register()
pipeline = Gst.parse_launch(
"v4l2src"
" ! videoconvert ! video/x-raw,format=RGB"
" ! pseudopros"
" secret=BASE64_SECRET"
" salt=BASE64_SALT"
" enrollment-image=/path/to/face.jpg"
" synthesizer=noise"
" ! videoconvert ! autovideosink"
)
pipeline.set_state(Gst.State.PLAYING)
Element properties
| Property | Type | Description |
|---|---|---|
secret |
string | Base64-encoded secret key (min 16 bytes decoded) |
salt |
string | Base64-encoded deployment salt (default: pseudopros) |
enrollment-image |
string | Path to an image file for subject enrollment |
synthesizer |
string | noise (fast placeholder) or roster (full LDM, ~1.3 GB download) |
Secrets are base64-encoded for safe embedding in pipeline strings:
import base64
secret_b64 = base64.b64encode(b"your-secret-key-min-16-bytes").decode()
salt_b64 = base64.b64encode(b"your-deployment-salt").decode()
The element enrolls on the first frame after enrollment-image is set. Changing secret or salt at runtime clears the cached anonymizer and re-enrolls on the next frame.
Running tests
# Fast tests only (no model downloads)
pytest -m "not slow"
# Full suite including security validation (~5 min)
pytest
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file pseudopros-0.1.1.tar.gz.
File metadata
- Download URL: pseudopros-0.1.1.tar.gz
- Upload date:
- Size: 148.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
cb133f327cc4320688e1fb3dc84cc3f66cd6e0a167193ce31714679fc73c8871
|
|
| MD5 |
cd6c5d5478018c6f3c4a494584820bb9
|
|
| BLAKE2b-256 |
aa553bf3a59304127486469abf82b3af9bf6d6fd949eb392fcd78ebe431833ee
|
Provenance
The following attestation bundles were made for pseudopros-0.1.1.tar.gz:
Publisher:
publish.yml on dactylroot/pseudopros
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pseudopros-0.1.1.tar.gz -
Subject digest:
cb133f327cc4320688e1fb3dc84cc3f66cd6e0a167193ce31714679fc73c8871 - Sigstore transparency entry: 2231531613
- Sigstore integration time:
-
Permalink:
dactylroot/pseudopros@1728adffa555bc0ff347b5171921de37c59253cc -
Branch / Tag:
refs/tags/0.1.1 - Owner: https://github.com/dactylroot
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@1728adffa555bc0ff347b5171921de37c59253cc -
Trigger Event:
release
-
Statement type:
File details
Details for the file pseudopros-0.1.1-py3-none-any.whl.
File metadata
- Download URL: pseudopros-0.1.1-py3-none-any.whl
- Upload date:
- Size: 58.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
8662cf66082b109b1bab6efa78aad36294423f576ab5370dc07382811094873e
|
|
| MD5 |
8926e883c578f5207c6dc56830b1d860
|
|
| BLAKE2b-256 |
e28241663da0744322152812d0641f22cb9a4e117824887badded9e3fb23ad6c
|
Provenance
The following attestation bundles were made for pseudopros-0.1.1-py3-none-any.whl:
Publisher:
publish.yml on dactylroot/pseudopros
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pseudopros-0.1.1-py3-none-any.whl -
Subject digest:
8662cf66082b109b1bab6efa78aad36294423f576ab5370dc07382811094873e - Sigstore transparency entry: 2231532112
- Sigstore integration time:
-
Permalink:
dactylroot/pseudopros@1728adffa555bc0ff347b5171921de37c59253cc -
Branch / Tag:
refs/tags/0.1.1 - Owner: https://github.com/dactylroot
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@1728adffa555bc0ff347b5171921de37c59253cc -
Trigger Event:
release
-
Statement type: