Contrastive Privacy contains image and text anonymization tools plus experiment drivers for measuring how much semantic information remains after obfuscation. The main experiment scripts create obfuscated copies of a dataset, compute an "effective resolution" score across sampled pairs, and then optionally run a similarity analysis between each original item and its obfuscated version.
The current workflow is centered on:
resolution-analysis for image datasets.python -m contrastive_privacy.scripts.text_resolution_analysis for text datasets.similarity-analysis for post-run original-vs-obfuscated similarity histograms.results-webpage for a filterable HTML report plus a cached JSON analysis bundle.experiments.sh as a command catalog for the paper-style image and text experiments.If you use this repository, please cite:
@article{bissias2026contrastive,
title = {Contrastive Privacy: A Semantic Approach to Measuring Privacy of AI-based Sanitization},
author = {Bissias, George and Bagdasarian, Eugene and Levine, Brian Neil},
journal = {arXiv preprint arXiv:2605.02977},
year = {2026}
}
What works locally without external services:
clipseg image runs and small text entity runs.What may require authentication or API keys:
sam3.--segmenter ai-gen, --segmenter vlm-bounding-box, identify-obfuscation-concepts, or text --approach concept.OPENAI_API_KEY, but the main experiment workflow does not depend on it.conda create -n priv python=3.11
conda activate priv
pip install -e .
For this repo, the validated local environment in this workspace has been cp0. If you already have that environment, the equivalent install path is:
conda activate cp0
pip install -e .
pip install -e . uses pyproject.toml and installs the console scripts listed there. Prefer it over requirements.txt, which is older and does not list every current dependency.
If you change [project.scripts] in pyproject.toml, rerun pip install -e . so new console commands are registered in the active environment.
If you use Hugging Face gated models, log in before the first run:
hf auth login
Set your fal.ai key when using cloud image editing, OpenRouter vision, or concept-based text redaction:
export FAL_KEY="your-fal-api-key"
Some legacy single-image OpenAI editing code paths use OPENAI_API_KEY, but the experiment commands in experiments.sh use fal.ai/OpenRouter through FAL_KEY.
Experiment commands expect datasets under data/:
data/
dicaprio/ # image dataset expected by image experiments
mcdonalds_large/ # image dataset expected by image experiments
mcdonalds_small/ # image dataset expected by image experiments
avengers_large/ # text dataset
avengers_small/ # text dataset
Text files can use .txt, .md, .text, .csv, or .json. Image files can use .jpg, .jpeg, .png, .webp, .bmp, .gif, or .tiff.
The image experiment commands require the corresponding image folders under data/, and the text experiment commands use the avengers_* folders.
Generated analysis runs are expected to live outside the tracked source tree, typically under runs/. The repo now ignores that directory so local experiment outputs, HTML reports, cached JSON bundles, and generated media do not show up as unstaged changes.
These do not require FAL_KEY and can be run entirely locally once dependencies are installed:
--segmenter clipseg.--segmenter groundedsam, assuming the local model weights are available.--approach entity.results-webpage and open-results-webpage.--segmenter sam3 is local at inference time, but the facebook/sam3 weights are gated on Hugging Face.FAL_KEY: required for ai-gen, vlm-bounding-box, identify-obfuscation-concepts, and text --approach concept.OPENAI_API_KEY: only needed for the older OpenAI-specific single-image utilities, not for the default local workflow.The smallest no-external-service image path is a clipseg run with explicit objects:
resolution-analysis data/dicaprio \
--mode blackout \
--trials 1 \
--samples 2 \
--segmenter clipseg \
--objects face person \
--output runs/dicaprio_clipseg_local_cp0 \
--seed 1 \
--min-coverage 0.00001 \
--max-coverage 1.0 \
--threshold 0.2 \
--blur 5 \
--dilate 5 \
--embedder-model openai/clip-vit-base-patch32 \
--embed-batch-size 2 \
--device cpu \
--skip-comparisons
After that run completes, open the local HTML report with:
open-results-webpage runs/dicaprio_clipseg_local_cp0 --skip-similarity
The --skip-similarity flag is optional here. It keeps the local review loop fast by rendering from the resolution results only, without recomputing original-vs-obfuscated utility metrics.
Use the privacy-checks command to run privacy detectors over text inputs. Supported tools are OpenAI Privacy Filter, AWS Comprehend, Microsoft Presidio, and Google Cloud Sensitive Data Protection (Cloud DLP).
privacy-checks "Alice Smith emailed alice@example.com"
privacy-checks --file notes.txt --format json
privacy-checks --path data/avengers_small --fail-on-findings
privacy-checks "Alice Smith emailed alice@example.com" \
--sanitization-technique aws-comprehend --aws-region us-east-1
privacy-checks "Alice Smith emailed alice@example.com" \
--sanitization-technique presidio --presidio-language en
privacy-checks "Alice Smith emailed alice@example.com" \
--sanitization-technique google-cloud-dlp --gcp-project my-project
The first real run may download the Privacy Filter checkpoint. Use --device cpu for
CPU-only inference. Use --sanitized-types as a whitelist or --unsanitized-types as a
blacklist; the options are mutually exclusive and type matching is case-insensitive. The legacy
names --include-labels and --exclude-labels remain available as aliases. Use --no-snippets
when reports should avoid copying detected text spans.
For example, these commands respectively sanitize only direct contact/identity fields and sanitize every detected AWS PII type except dates:
privacy-checks "Alice called on July 4 at 555-0100" --tool aws-comprehend \
--sanitized-types NAME EMAIL PHONE ADDRESS
privacy-checks "Alice called on July 4 at 555-0100" --tool aws-comprehend \
--unsanitized-types DATE_TIME
For AWS Comprehend, credentials come from boto3's standard credential chain (for example,
AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY, an AWS profile, or an instance/task role). Keys
are never accepted as command-line options or saved in run parameters. The required IAM
permission is only comprehend:DetectPiiEntities. Use --aws-region, --aws-profile, and
--aws-language-code to select the AWS configuration. Texts above the 100 KB real-time API
limit are split into smaller requests automatically.
Microsoft Presidio runs locally in this integration, so it does not require an Azure subscription, access key, API token, endpoint, or Microsoft Entra application. Install the project and Presidio's default English spaCy model once:
pip install -e .
python -m spacy download en_core_web_lg
Then select --sanitization-technique presidio. --presidio-language defaults to en.
Presidio entity names include PERSON, EMAIL_ADDRESS, PHONE_NUMBER, LOCATION,
CREDIT_CARD, and US_SSN; the available set depends on the language and configured
recognizers. This project uses presidio-analyzer for detection and its existing shared
redact/blackout implementation for replacement, so presidio-anonymizer is not required.
Google Cloud DLP uses the content.inspect API and Application Default Credentials (ADC).
The integration sends text to Google Cloud for inspection, chunks requests below the 0.5 MB
API limit, and uses Unicode code-point offsets when applying local redaction. Credentials are
never accepted as command-line flags or saved in run parameters. Initial setup:
gcloud services enable dlp.googleapis.com --project=PROJECT_ID
gcloud auth login --force
gcloud auth application-default login \
--scopes=https://www.googleapis.com/auth/cloud-platform
gcloud auth application-default set-quota-project PROJECT_ID
gcloud auth application-default print-access-token
When running on a remote machine over SSH, prevent gcloud from opening a browser with a
callback to the remote machine's localhost. Open the printed URLs on a trusted local machine
and paste each authorization code back into the SSH terminal:
gcloud auth login --force --no-launch-browser
gcloud auth application-default login \
--no-launch-browser \
--scopes=https://www.googleapis.com/auth/cloud-platform
gcloud auth application-default set-quota-project PROJECT_ID
gcloud auth application-default print-access-token
If authentication reports that the requested scope changed to only openid and
userinfo.email, first update the Google Cloud CLI and retry:
gcloud version
gcloud components update
To stay below Google's default 600-request-per-minute project quota, Google Cloud DLP requests
are spaced at 540 requests per minute by default (about 111 ms apart). A 429 rate-limit
response is retried up to six times with exponential backoff. These defaults apply automatically;
use --gcp-requests-per-minute and --gcp-max-retries to tune them. Set
--gcp-requests-per-minute 0 to disable local throttling. The quota is shared by all workloads in
the project, so choose a lower rate if another process or service uses the same quota.
Privacy Filter may require a newer Transformers build than the latest stable release because its model type is openai_privacy_filter. If loading fails with an unrecognized architecture error, install Transformers from source inside your active environment:
pip install --upgrade git+https://github.com/huggingface/transformers.git
Use parse-enron-dataset to parse the standard CMU Enron dataset tree, write body-only .txt files such as email_000000001.txt into one flat emails/ folder, and write one metadata JSON file with named entities found in each retained body. Emails with no named-entity matches are omitted. By default, person entities are filtered with spaCy POS tags: GLiNER2 must label the span as person, and spaCy must tag part of the same span as PROPN.
parse-enron-dataset \
--input text/enron \
--output runs/enron_parsed
For a quick smoke test:
parse-enron-dataset \
--input text/enron \
--output runs/enron_sample \
--limit 100 \
--entities person organization location date email url
Here --limit 100 means up to 100 retained body files with at least one named-entity match; the script will keep scanning past dropped emails when possible. Use --max-scanned if you also want to cap the number of raw emails inspected.
Add --randomize to shuffle the source collection before scanning. In flat mode,
this selects matching emails randomly instead of using source-path order. With
--organize-by, eligible entity groups are also chosen in random order before
--limit is applied. Use --seed 1234 to reproduce the same selection. When
--max-scanned is supplied, the scanned files are a random subset of the input.
Duplicate cleaned email bodies are skipped by default, keeping the first retained copy only. Use --no-dedupe to keep duplicates.
The default person filter requires spaCy and an English model:
pip install spacy
python -m spacy download en_core_web_sm
Use --person-filter heuristic for the older capitalization filter, or --person-filter none to keep raw GLiNER2 person output.
To organize retained emails by a metadata entity, use --organize-by. If multiple values of that entity type appear in one email, the body file is copied into each corresponding folder:
parse-enron-dataset \
--input text/enron \
--output runs/enron_by_person \
--limit 100 \
--organize-by person
That creates paths like emails/bob_smith/email_000000001.txt, with all generated paths listed in metadata.json.
To keep only entity folders with enough examples, add --min-group-emails:
parse-enron-dataset \
--input text/enron \
--output runs/enron_by_person \
--limit 100 \
--organize-by person \
--min-group-emails 5
Folders with fewer than 5 retained email records are removed from the output and from metadata.json.
Use resolution-analysis to obfuscate image datasets and evaluate effective resolution.
Local-only example:
resolution-analysis data/dicaprio \
--mode blackout \
--trials 1 \
--samples 2 \
--segmenter clipseg \
--objects face person \
--output runs/dicaprio_clipseg_local_cp0 \
--seed 1 \
--min-coverage 0.00001 \
--max-coverage 1.0 \
--threshold 0.2 \
--blur 5 \
--dilate 5 \
--embedder-model openai/clip-vit-base-patch32 \
--embed-batch-size 2 \
--device cpu \
--skip-comparisons
API-backed example from the experiment catalog:
resolution-analysis data/dicaprio \
--mode blackout \
--trials 29 \
--samples 30 \
--segmenter ai-gen \
--fal-image-model fal-ai/gpt-image-1.5 \
--privacy-concept "the identity of the celebrities" \
--output dicaprio_large/gpt-image-1.5_remove_t3 \
--seed 34518474335 \
--min-coverage 0.000001 \
--max-coverage 1.0 \
--blur 5 \
--dilate 25 \
--skip-empty-labels \
--convex-hull \
--embedder-model BAAI/EVA-CLIP-18B \
--skip-comparisons
Important flags:
image_folder: folder of original images.--mode: blackout, redact, or replace.--trials: number of comparison items sampled per reference item.--samples: number of reference items to sample. Omit it to use all items.--output: output directory. It will contain obfuscated/, results.csv, params.json, report.txt, and usually summary/comparison images.--segmenter: groundedsam, clipseg, sam3, ai-gen, or vlm-bounding-box.--objects: explicit object/concept labels to detect and obfuscate.--privacy-concept: concept-level instruction for --segmenter ai-gen; this can replace --objects.--base-concepts: additional labels always obfuscated with SAM3 after the primary obfuscation.--min-coverage and --max-coverage: discard runs that change too little or too much of the item.--continue: reuse existing output/obfuscated/ files and fill in missing ones.--retry: retry skipped references from a previous report.txt using the saved params.json.--skip-comparisons: skip compound comparison images while still computing metrics. Some commands in experiments.sh use --skip-comparison; argparse accepts that as an abbreviation, but new commands should use the plural spelling.Segmenter notes:
clipseg: easiest local path for a first run.groundedsam: local path with stronger object grounding, but heavier than clipseg.sam3: local inference path, but requires gated Hugging Face access before first download.ai-gen and vlm-bounding-box: external-service paths that require FAL_KEY.Use identify-obfuscation-concepts to ask a fal.ai OpenRouter vision model which visible concepts should be removed to protect a high-level privacy target:
identify-obfuscation-concepts data/dicaprio \
--model gpt-5.4 \
--output-format simple \
"the identity of the celebrities"
The command accepts either one image file or a folder of images. It uploads each image, asks the selected vision model for reusable obfuscation concepts, deduplicates the concepts across the dataset, and prints the result.
Important flags:
path: a single image or folder of images to analyze.target_concept: the privacy target to protect, such as the identity of the celebrities.--model: gpt-5.4, gemini-3.1-pro, opus-4.6, or a raw OpenRouter model ID.--output-format: text, json, or simple. The experiments use simple, which prints a quoted, comma-separated list that can be copied into a later resolution-analysis --objects ... invocation.--output: optional file path for saving the concept list instead of only printing it.This command requires FAL_KEY.
There is no installed console script for text resolution analysis in pyproject.toml, so run it as a module.
Local-only example:
python -m contrastive_privacy.scripts.text_resolution_analysis data/avengers_small \
--mode blackout \
--trials 2 \
--samples 3 \
--approach entity \
--entities movie person organization \
--output runs/avengers_entity_local_cp0 \
--embedder sbert \
--embedder-model sentence-transformers/all-MiniLM-L6-v2 \
--device cpu \
--embed-batch-size 2
API-backed concept example:
python -m contrastive_privacy.scripts.text_resolution_analysis data/avengers_small \
--mode blackout \
--trials 8 \
--samples 9 \
--approach concept \
--concept "anything that can identify the movie discussed in this passage" \
--concept-model openai/gpt-5.4 \
--output avg_small_gpt-5.4_t3 \
--embedder qwen \
--embedder-model Qwen/Qwen3-Embedding-8B \
--embedder-quantization none \
--device cuda \
--embed-batch-size 1 \
--min-coverage 0.00001 \
--max-coverage 1.0 \
--sequential-labels
Privacy Filter example (the first run may download openai/privacy-filter):
python -m contrastive_privacy.scripts.text_resolution_analysis data/avengers_small \
--mode redact \
--trials 2 \
--samples 3 \
--approach privacy-check \
--sanitization-technique privacy-filter \
--privacy-check-model openai/privacy-filter \
--threshold 0.3 \
--output runs/avengers_privacy_filter \
--embedder sbert \
--device cpu
AWS Comprehend example:
python -m contrastive_privacy.scripts.text_resolution_analysis data/avengers_small \
--mode redact \
--trials 2 \
--samples 3 \
--approach privacy-check \
--sanitization-technique aws-comprehend \
--aws-region us-east-1 \
--aws-language-code en \
--unsanitized-types DATE_TIME \
--threshold 0.3 \
--output runs/avengers_aws_comprehend \
--embedder sbert \
--device cpu
Microsoft Presidio example (local; no Azure credentials):
python -m contrastive_privacy.scripts.text_resolution_analysis data/avengers_small \
--mode redact \
--trials 2 \
--samples 3 \
--approach privacy-check \
--sanitization-technique presidio \
--presidio-language en \
--unsanitized-types DATE_TIME \
--threshold 0.3 \
--output runs/avengers_presidio \
--embedder sbert \
--device cpu
Google Cloud Sensitive Data Protection example:
python -m contrastive_privacy.scripts.text_resolution_analysis data/avengers_small \
--mode redact \
--trials 2 \
--samples 3 \
--approach privacy-check \
--sanitization-technique google-cloud-dlp \
--gcp-project PROJECT_ID \
--gcp-location global \
--gcp-requests-per-minute 540 \
--gcp-max-retries 6 \
--sanitized-types PERSON_NAME EMAIL_ADDRESS PHONE_NUMBER STREET_ADDRESS \
--threshold 0.6 \
--output runs/avengers_google_cloud_dlp \
--embedder sbert \
--device cpu
Text analysis supports three obfuscation approaches:
--approach entity: uses GLiNER2 NER with --entities, optionally plus literal --instances.--approach concept: uses fal.ai OpenRouter with --concept and --concept-model; this requires FAL_KEY.--approach privacy-check: delegates detection and sanitization to contrastive_privacy.privacy_checks. Select privacy-filter, aws-comprehend, presidio, or google-cloud-dlp with --sanitization-technique. --mode, --placeholder, and --threshold configure all techniques; use --privacy-check-model for Privacy Filter, the --aws-* options for Comprehend, --presidio-language for Presidio, or the --gcp-* options for Google Cloud DLP. --sanitized-types whitelists the provider PII types to sanitize, while --unsanitized-types blacklists types to preserve; specify at most one policy.Common text flags:
--skip-summary-grids: skips summary_originals.pdf and summary_obfuscated.pdf; use this for large corpora whose combined text grid would exceed Pillow's image-size limit.
--mode blackout: replaces sensitive spans with block characters.
--mode redact: replaces sensitive spans with --placeholder, defaulting to [REDACTED].
--entities: entity types for GLiNER2, such as person, organization, movie, or custom open-vocabulary labels.
--instances: literal substrings to obfuscate without NER.
--threshold: NER detection threshold.
--sequential-labels: process entity labels separately and merge results, so adding labels cannot reduce detections.
--propagate / --no-propagate: control whether repeated occurrences of detected text are anonymized.
--embedder: sbert, clip, or qwen; the experiments use Qwen embeddings.
--embedder-quantization: none, half, 4bit, or 8bit.
After a resolution run, run similarity-analysis on the output folder:
similarity-analysis dicaprio_large/gpt-image-1.5_remove_t3 --model BAAI/EVA-CLIP-18B
For text runs:
similarity-analysis avg_small_gpt-5.4_t3 \
--embedder qwen \
--embedder-model Qwen/Qwen3-Embedding-8B
similarity-analysis auto-detects image vs. text outputs from obfuscated/. For image outputs, --model selects the CLIP image embedder. For text outputs, use --embedder and --embedder-model, or omit them and let the script reuse the settings saved in params.json. Some text follow-up commands in experiments.sh pass --model Qwen/Qwen3-Embedding-8B; that flag is image-only in the current CLI, so the text embedder still comes from params.json unless you pass --embedder-model.
Use --image-folder or --text-folder if the original data has moved since the resolution run.
Image and text resolution runs now auto-generate two extra files in the output folder unless you pass --skip-analysis-artifacts:
analysis_report.html: a static webpage with summary metrics, histograms, a searchable/sortable all-pairs table, and example pairs with explanations.analysis_report.json: a cached structured bundle for notebooks, dashboards, or downstream plotting without recomputing similarity.You can also regenerate or customize the report later with results-webpage:
results-webpage dicaprio_large/gpt-image-1.5_remove_t3
results-webpage avg_small_gpt-5.4_t3 --title "Avengers Text Run"
results-webpage some_output_folder --refresh --threshold 0.02 --top-n 8
results-webpage some_output_folder --low-utility-threshold 0.25 --high-utility-threshold 0.8 --open
open-results-webpage some_output_folder
Useful flags:
--refresh: ignore any cached analysis_report.json and recompute the bundle.--skip-similarity: render the report from resolution results only, without recomputing original-vs-obfuscated similarity.--low-utility-threshold and --high-utility-threshold: control the utility bands shown in the report and the low-utility example filter.--json-output and --output: override the default JSON/HTML output paths.--open: open the generated report immediately in your default browser.--image-folder or --text-folder: resolve original files from a different location if the dataset moved.Recommended local review loop:
open-results-webpage runs/dicaprio_clipseg_local_cp0 --skip-similarity
Use --skip-similarity when you only want to inspect an existing run and do not need to recompute original-vs-obfuscated utility metrics. Omit it when you want the report to include the similarity summary, low-utility examples, and utility bands based on fresh original-vs-obfuscated embeddings.
If you usually want the report opened after generation, open-results-webpage is a thin wrapper around results-webpage with browser opening enabled by default. Pass --no-open when you only want the artifacts refreshed.
For interactive inspection, see the notebook at notebooks/inspect_results.ipynb, which reuses the same JSON/HTML artifact pipeline.
experiments.shexperiments.sh is a flat list of commands, not a parameterized launcher. Running the entire file will attempt every experiment. New users should copy one command pair at a time: first the resolution-analysis or python -m ...text_resolution_analysis command, then the following similarity-analysis command against the output folder that was just created.
Each experiment usually has this shape:
# 1. Create obfuscated outputs and compute effective resolution.
resolution-analysis ... --output some_output_folder ...
# 2. Measure original-vs-obfuscated similarity for that output folder.
similarity-analysis some_output_folder ...
For text:
python -m contrastive_privacy.scripts.text_resolution_analysis ... --output some_output_folder ...
similarity-analysis some_output_folder ...
Some follow-up similarity commands point at suffixed folders such as _t2, _t3, or _t4. Those suffixes reflect iterative runs and retries. If you adapt a command, make sure the similarity-analysis argument exactly matches the --output folder you produced.
The image commands cover three datasets:
data/dicaprio with outputs under dicaprio_large/, usually --trials 29 --samples 30.data/mcdonalds_large with outputs under mcdonalds_large/, usually --trials 48 --samples 49.data/mcdonalds_small with outputs under mcdonalds_small/, usually --trials 8 --samples 9.Within each dataset, experiments.sh compares several image obfuscation backends:
--segmenter ai-gen --fal-image-model fal-ai/gpt-image-1.5--segmenter ai-gen --fal-image-model fal-ai/gpt-image-1-mini--segmenter ai-gen --fal-image-model fal-ai/gemini-3-pro-image-preview--segmenter ai-gen --fal-image-model fal-ai/gemini-3.1-flash-image-preview--segmenter ai-gen --fal-image-model fal-ai/flux-2-pro--segmenter ai-gen --fal-image-model fal-ai/flux-2--segmenter sam3 for several dicaprio manual/object-list runsBefore the resolution runs, each image dataset now has a concept-identification block:
identify-obfuscation-concepts data/dicaprio --model gpt-5.4 --output-format simple "the identity of the celebrities"
identify-obfuscation-concepts data/dicaprio --model gemini-3.1-pro --output-format simple "the identity of the celebrities"
identify-obfuscation-concepts data/dicaprio --model opus-4.6 --output-format simple "the identity of the celebrities"
The same pattern appears for data/mcdonalds_large and data/mcdonalds_small with the target concept the identity of the fast food restaurant. These calls are for generating model-specific candidate object lists. The resulting concepts correspond to the later --objects lists in output folders named with the model family, such as *_remove_gpt-5.4_t3, *_remove_gemini-3.1-pro_t3, and *_remove_opus-4.6_t3.
The ai-gen runs ask a fal.ai image editing model to handle detection and obfuscation together. They either use:
--privacy-concept, for high-level redaction such as "the identity of the celebrities" or "the identity of the fast food restaurant".--objects, for explicit target lists produced manually or by different LLMs.The output folder names encode the backend and target-list source:
*_remove_t3: concept-level removal using --privacy-concept.*_remove_manual_t3: manually chosen target objects.*_remove_gpt-5.4_t3, *_remove_gemini-3.1-pro_t3, *_remove_opus-4.6_t3: target object lists generated by those LLM families.The common image settings in the experiment file are:
--mode blackout: replace selected regions with black pixels.--blur 5 --dilate 25: expand and soften masks before blackout.--convex-hull: hide silhouettes more aggressively by filling concave mask gaps.--skip-empty-labels: ignore ambiguous empty-label detections from GroundingDINO-style paths.--fal-vision-temperature 0.1: low-temperature OpenRouter vision calls where applicable.--embedder-model BAAI/EVA-CLIP-18B: use EVA-CLIP embeddings for resolution scoring.--embed-batch-size 1: used with large embedders to fit GPU memory.The text commands cover two datasets:
data/avengers_small with outputs under avg_small_*, usually --trials 8 --samples 9.data/avengers_large with outputs under avg_large_*, usually --trials 48 --samples 49.Each text block starts with one manual/entity baseline:
python -m contrastive_privacy.scripts.text_resolution_analysis data/avengers_small \
--mode blackout \
--trials 8 \
--samples 9 \
--sequential-labels \
--output avg_small_manual \
--entities comic movie publisher author superhero abbreviation archetype year \
--threshold 0.05 \
--embedder qwen \
--embedder-model Qwen/Qwen3-Embedding-8B \
--embedder-quantization none \
--device cuda \
--embed-batch-size 1 \
--instances remake blockbuster battle first powerful defeated movies die
That baseline combines GLiNER2 entity labels (--entities) with literal substrings (--instances).
The rest of each text block uses concept redaction:
--approach concept \
--concept "anything that can identify the movie discussed in this passage" \
--concept-model <provider/model>
The experiment file compares OpenRouter models from OpenAI, Google, and Anthropic, including openai/gpt-5.4, openai/gpt-5.4-mini, openai/gpt-5.2, openai/gpt-4o, google/gemini-3.1-pro-preview, google/gemini-2.5-flash, anthropic/claude-opus-4.6, and related variants.
All text experiment commands use Qwen embeddings:
--embedder qwen \
--embedder-model Qwen/Qwen3-Embedding-8B \
--embedder-quantization none \
--embed-batch-size 1
If you run out of GPU memory, try --embedder-quantization half, reduce --embed-batch-size, or switch to --device cpu for smaller runs.
After installation, these image utilities are available as console scripts:
flux-anonymize: anonymize one image with local segmentation, fal.ai image editing, or OpenRouter vision.flux-create-mask: create a segmentation mask.flux-inpaint: inpaint an image using a mask.flux-generate and flux-edit: older FLUX generation/editing helpers.compare-images, recognize-objects, concept-resolution, identify-obfuscation-concepts, reanalyze-image-resolution, and reanalyze-text-resolution.Text single-file utilities are run as modules:
python -m contrastive_privacy.scripts.text_anonymize \
--input document.txt \
--output anonymized.txt \
--entities person organization \
--mode blackout
Use each script's --help for the complete CLI:
resolution-analysis --help
python -m contrastive_privacy.scripts.text_resolution_analysis --help
similarity-analysis --help
Resolution runs write reproducible artifacts to the chosen --output directory:
obfuscated/: one obfuscated_<original-name> file per successfully obfuscated item.results.csv: per-comparison resolution values.params.json: the effective parameters used by the run.report.txt: summary statistics, skipped items, and configuration.comparisons/: compound comparison images when comparison generation is enabled for image runs.summary_originals.jpg and summary_obfuscated.jpg: image-grid summaries for image runs.Use --continue after a partial run to reuse already-created obfuscations. Use --retry --output <previous-output> to retry references listed as skipped in a previous report.txt.
command not found: activate the environment and run pip install -e . again.FAL_KEY: required for ai-gen, vlm-bounding-box, and text --approach concept.sam3 access errors: run hf auth login and ensure your account has access to facebook/sam3.experiments.sh expect data/dicaprio, data/mcdonalds_large, and data/mcdonalds_small.--embed-batch-size, use --embedder-quantization half, use smaller samples, or run on CPU for smaller text experiments.hf auth login and accept any required model licenses on Hugging Face.nano-banana-2.data/dogs.txt, were generated using Google's gemini-3.1-pro.This project is licensed under the MIT License. See LICENSE for details.
29 commits
Python
81.7%
Shell
15.6%
HTML
2.0%
Contrastive Privacy contains image and text anonymization tools plus experiment drivers for measuring how much semantic information remains after obfuscation. The main experiment scripts create obfuscated copies of a dataset, compute an "effective resolution" score across sampled pairs, and then optionally run a similarity analysis between each original item and its obfuscated version.
The current workflow is centered on:
resolution-analysis for image datasets.python -m contrastive_privacy.scripts.text_resolution_analysis for text datasets.similarity-analysis for post-run original-vs-obfuscated similarity histograms.results-webpage for a filterable HTML report plus a cached JSON analysis bundle.experiments.sh as a command catalog for the paper-style image and text experiments.If you use this repository, please cite:
@article{bissias2026contrastive,
title = {Contrastive Privacy: A Semantic Approach to Measuring Privacy of AI-based Sanitization},
author = {Bissias, George and Bagdasarian, Eugene and Levine, Brian Neil},
journal = {arXiv preprint arXiv:2605.02977},
year = {2026}
}
What works locally without external services:
clipseg image runs and small text entity runs.What may require authentication or API keys:
sam3.--segmenter ai-gen, --segmenter vlm-bounding-box, identify-obfuscation-concepts, or text --approach concept.OPENAI_API_KEY, but the main experiment workflow does not depend on it.conda create -n priv python=3.11
conda activate priv
pip install -e .
For this repo, the validated local environment in this workspace has been cp0. If you already have that environment, the equivalent install path is:
conda activate cp0
pip install -e .
pip install -e . uses pyproject.toml and installs the console scripts listed there. Prefer it over requirements.txt, which is older and does not list every current dependency.
If you change [project.scripts] in pyproject.toml, rerun pip install -e . so new console commands are registered in the active environment.
If you use Hugging Face gated models, log in before the first run:
hf auth login
Set your fal.ai key when using cloud image editing, OpenRouter vision, or concept-based text redaction:
export FAL_KEY="your-fal-api-key"
Some legacy single-image OpenAI editing code paths use OPENAI_API_KEY, but the experiment commands in experiments.sh use fal.ai/OpenRouter through FAL_KEY.
Experiment commands expect datasets under data/:
data/
dicaprio/ # image dataset expected by image experiments
mcdonalds_large/ # image dataset expected by image experiments
mcdonalds_small/ # image dataset expected by image experiments
avengers_large/ # text dataset
avengers_small/ # text dataset
Text files can use .txt, .md, .text, .csv, or .json. Image files can use .jpg, .jpeg, .png, .webp, .bmp, .gif, or .tiff.
The image experiment commands require the corresponding image folders under data/, and the text experiment commands use the avengers_* folders.
Generated analysis runs are expected to live outside the tracked source tree, typically under runs/. The repo now ignores that directory so local experiment outputs, HTML reports, cached JSON bundles, and generated media do not show up as unstaged changes.
These do not require FAL_KEY and can be run entirely locally once dependencies are installed:
--segmenter clipseg.--segmenter groundedsam, assuming the local model weights are available.--approach entity.results-webpage and open-results-webpage.--segmenter sam3 is local at inference time, but the facebook/sam3 weights are gated on Hugging Face.FAL_KEY: required for ai-gen, vlm-bounding-box, identify-obfuscation-concepts, and text --approach concept.OPENAI_API_KEY: only needed for the older OpenAI-specific single-image utilities, not for the default local workflow.The smallest no-external-service image path is a clipseg run with explicit objects:
resolution-analysis data/dicaprio \
--mode blackout \
--trials 1 \
--samples 2 \
--segmenter clipseg \
--objects face person \
--output runs/dicaprio_clipseg_local_cp0 \
--seed 1 \
--min-coverage 0.00001 \
--max-coverage 1.0 \
--threshold 0.2 \
--blur 5 \
--dilate 5 \
--embedder-model openai/clip-vit-base-patch32 \
--embed-batch-size 2 \
--device cpu \
--skip-comparisons
After that run completes, open the local HTML report with:
open-results-webpage runs/dicaprio_clipseg_local_cp0 --skip-similarity
The --skip-similarity flag is optional here. It keeps the local review loop fast by rendering from the resolution results only, without recomputing original-vs-obfuscated utility metrics.
Use the privacy-checks command to run privacy detectors over text inputs. Supported tools are OpenAI Privacy Filter, AWS Comprehend, Microsoft Presidio, and Google Cloud Sensitive Data Protection (Cloud DLP).
privacy-checks "Alice Smith emailed alice@example.com"
privacy-checks --file notes.txt --format json
privacy-checks --path data/avengers_small --fail-on-findings
privacy-checks "Alice Smith emailed alice@example.com" \
--sanitization-technique aws-comprehend --aws-region us-east-1
privacy-checks "Alice Smith emailed alice@example.com" \
--sanitization-technique presidio --presidio-language en
privacy-checks "Alice Smith emailed alice@example.com" \
--sanitization-technique google-cloud-dlp --gcp-project my-project
The first real run may download the Privacy Filter checkpoint. Use --device cpu for
CPU-only inference. Use --sanitized-types as a whitelist or --unsanitized-types as a
blacklist; the options are mutually exclusive and type matching is case-insensitive. The legacy
names --include-labels and --exclude-labels remain available as aliases. Use --no-snippets
when reports should avoid copying detected text spans.
For example, these commands respectively sanitize only direct contact/identity fields and sanitize every detected AWS PII type except dates:
privacy-checks "Alice called on July 4 at 555-0100" --tool aws-comprehend \
--sanitized-types NAME EMAIL PHONE ADDRESS
privacy-checks "Alice called on July 4 at 555-0100" --tool aws-comprehend \
--unsanitized-types DATE_TIME
For AWS Comprehend, credentials come from boto3's standard credential chain (for example,
AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY, an AWS profile, or an instance/task role). Keys
are never accepted as command-line options or saved in run parameters. The required IAM
permission is only comprehend:DetectPiiEntities. Use --aws-region, --aws-profile, and
--aws-language-code to select the AWS configuration. Texts above the 100 KB real-time API
limit are split into smaller requests automatically.
Microsoft Presidio runs locally in this integration, so it does not require an Azure subscription, access key, API token, endpoint, or Microsoft Entra application. Install the project and Presidio's default English spaCy model once:
pip install -e .
python -m spacy download en_core_web_lg
Then select --sanitization-technique presidio. --presidio-language defaults to en.
Presidio entity names include PERSON, EMAIL_ADDRESS, PHONE_NUMBER, LOCATION,
CREDIT_CARD, and US_SSN; the available set depends on the language and configured
recognizers. This project uses presidio-analyzer for detection and its existing shared
redact/blackout implementation for replacement, so presidio-anonymizer is not required.
Google Cloud DLP uses the content.inspect API and Application Default Credentials (ADC).
The integration sends text to Google Cloud for inspection, chunks requests below the 0.5 MB
API limit, and uses Unicode code-point offsets when applying local redaction. Credentials are
never accepted as command-line flags or saved in run parameters. Initial setup:
gcloud services enable dlp.googleapis.com --project=PROJECT_ID
gcloud auth login --force
gcloud auth application-default login \
--scopes=https://www.googleapis.com/auth/cloud-platform
gcloud auth application-default set-quota-project PROJECT_ID
gcloud auth application-default print-access-token
When running on a remote machine over SSH, prevent gcloud from opening a browser with a
callback to the remote machine's localhost. Open the printed URLs on a trusted local machine
and paste each authorization code back into the SSH terminal:
gcloud auth login --force --no-launch-browser
gcloud auth application-default login \
--no-launch-browser \
--scopes=https://www.googleapis.com/auth/cloud-platform
gcloud auth application-default set-quota-project PROJECT_ID
gcloud auth application-default print-access-token
If authentication reports that the requested scope changed to only openid and
userinfo.email, first update the Google Cloud CLI and retry:
gcloud version
gcloud components update
To stay below Google's default 600-request-per-minute project quota, Google Cloud DLP requests
are spaced at 540 requests per minute by default (about 111 ms apart). A 429 rate-limit
response is retried up to six times with exponential backoff. These defaults apply automatically;
use --gcp-requests-per-minute and --gcp-max-retries to tune them. Set
--gcp-requests-per-minute 0 to disable local throttling. The quota is shared by all workloads in
the project, so choose a lower rate if another process or service uses the same quota.
Privacy Filter may require a newer Transformers build than the latest stable release because its model type is openai_privacy_filter. If loading fails with an unrecognized architecture error, install Transformers from source inside your active environment:
pip install --upgrade git+https://github.com/huggingface/transformers.git
Use parse-enron-dataset to parse the standard CMU Enron dataset tree, write body-only .txt files such as email_000000001.txt into one flat emails/ folder, and write one metadata JSON file with named entities found in each retained body. Emails with no named-entity matches are omitted. By default, person entities are filtered with spaCy POS tags: GLiNER2 must label the span as person, and spaCy must tag part of the same span as PROPN.
parse-enron-dataset \
--input text/enron \
--output runs/enron_parsed
For a quick smoke test:
parse-enron-dataset \
--input text/enron \
--output runs/enron_sample \
--limit 100 \
--entities person organization location date email url
Here --limit 100 means up to 100 retained body files with at least one named-entity match; the script will keep scanning past dropped emails when possible. Use --max-scanned if you also want to cap the number of raw emails inspected.
Add --randomize to shuffle the source collection before scanning. In flat mode,
this selects matching emails randomly instead of using source-path order. With
--organize-by, eligible entity groups are also chosen in random order before
--limit is applied. Use --seed 1234 to reproduce the same selection. When
--max-scanned is supplied, the scanned files are a random subset of the input.
Duplicate cleaned email bodies are skipped by default, keeping the first retained copy only. Use --no-dedupe to keep duplicates.
The default person filter requires spaCy and an English model:
pip install spacy
python -m spacy download en_core_web_sm
Use --person-filter heuristic for the older capitalization filter, or --person-filter none to keep raw GLiNER2 person output.
To organize retained emails by a metadata entity, use --organize-by. If multiple values of that entity type appear in one email, the body file is copied into each corresponding folder:
parse-enron-dataset \
--input text/enron \
--output runs/enron_by_person \
--limit 100 \
--organize-by person
That creates paths like emails/bob_smith/email_000000001.txt, with all generated paths listed in metadata.json.
To keep only entity folders with enough examples, add --min-group-emails:
parse-enron-dataset \
--input text/enron \
--output runs/enron_by_person \
--limit 100 \
--organize-by person \
--min-group-emails 5
Folders with fewer than 5 retained email records are removed from the output and from metadata.json.
Use resolution-analysis to obfuscate image datasets and evaluate effective resolution.
Local-only example:
resolution-analysis data/dicaprio \
--mode blackout \
--trials 1 \
--samples 2 \
--segmenter clipseg \
--objects face person \
--output runs/dicaprio_clipseg_local_cp0 \
--seed 1 \
--min-coverage 0.00001 \
--max-coverage 1.0 \
--threshold 0.2 \
--blur 5 \
--dilate 5 \
--embedder-model openai/clip-vit-base-patch32 \
--embed-batch-size 2 \
--device cpu \
--skip-comparisons
API-backed example from the experiment catalog:
resolution-analysis data/dicaprio \
--mode blackout \
--trials 29 \
--samples 30 \
--segmenter ai-gen \
--fal-image-model fal-ai/gpt-image-1.5 \
--privacy-concept "the identity of the celebrities" \
--output dicaprio_large/gpt-image-1.5_remove_t3 \
--seed 34518474335 \
--min-coverage 0.000001 \
--max-coverage 1.0 \
--blur 5 \
--dilate 25 \
--skip-empty-labels \
--convex-hull \
--embedder-model BAAI/EVA-CLIP-18B \
--skip-comparisons
Important flags:
image_folder: folder of original images.--mode: blackout, redact, or replace.--trials: number of comparison items sampled per reference item.--samples: number of reference items to sample. Omit it to use all items.--output: output directory. It will contain obfuscated/, results.csv, params.json, report.txt, and usually summary/comparison images.--segmenter: groundedsam, clipseg, sam3, ai-gen, or vlm-bounding-box.--objects: explicit object/concept labels to detect and obfuscate.--privacy-concept: concept-level instruction for --segmenter ai-gen; this can replace --objects.--base-concepts: additional labels always obfuscated with SAM3 after the primary obfuscation.--min-coverage and --max-coverage: discard runs that change too little or too much of the item.--continue: reuse existing output/obfuscated/ files and fill in missing ones.--retry: retry skipped references from a previous report.txt using the saved params.json.--skip-comparisons: skip compound comparison images while still computing metrics. Some commands in experiments.sh use --skip-comparison; argparse accepts that as an abbreviation, but new commands should use the plural spelling.Segmenter notes:
clipseg: easiest local path for a first run.groundedsam: local path with stronger object grounding, but heavier than clipseg.sam3: local inference path, but requires gated Hugging Face access before first download.ai-gen and vlm-bounding-box: external-service paths that require FAL_KEY.Use identify-obfuscation-concepts to ask a fal.ai OpenRouter vision model which visible concepts should be removed to protect a high-level privacy target:
identify-obfuscation-concepts data/dicaprio \
--model gpt-5.4 \
--output-format simple \
"the identity of the celebrities"
The command accepts either one image file or a folder of images. It uploads each image, asks the selected vision model for reusable obfuscation concepts, deduplicates the concepts across the dataset, and prints the result.
Important flags:
path: a single image or folder of images to analyze.target_concept: the privacy target to protect, such as the identity of the celebrities.--model: gpt-5.4, gemini-3.1-pro, opus-4.6, or a raw OpenRouter model ID.--output-format: text, json, or simple. The experiments use simple, which prints a quoted, comma-separated list that can be copied into a later resolution-analysis --objects ... invocation.--output: optional file path for saving the concept list instead of only printing it.This command requires FAL_KEY.
There is no installed console script for text resolution analysis in pyproject.toml, so run it as a module.
Local-only example:
python -m contrastive_privacy.scripts.text_resolution_analysis data/avengers_small \
--mode blackout \
--trials 2 \
--samples 3 \
--approach entity \
--entities movie person organization \
--output runs/avengers_entity_local_cp0 \
--embedder sbert \
--embedder-model sentence-transformers/all-MiniLM-L6-v2 \
--device cpu \
--embed-batch-size 2
API-backed concept example:
python -m contrastive_privacy.scripts.text_resolution_analysis data/avengers_small \
--mode blackout \
--trials 8 \
--samples 9 \
--approach concept \
--concept "anything that can identify the movie discussed in this passage" \
--concept-model openai/gpt-5.4 \
--output avg_small_gpt-5.4_t3 \
--embedder qwen \
--embedder-model Qwen/Qwen3-Embedding-8B \
--embedder-quantization none \
--device cuda \
--embed-batch-size 1 \
--min-coverage 0.00001 \
--max-coverage 1.0 \
--sequential-labels
Privacy Filter example (the first run may download openai/privacy-filter):
python -m contrastive_privacy.scripts.text_resolution_analysis data/avengers_small \
--mode redact \
--trials 2 \
--samples 3 \
--approach privacy-check \
--sanitization-technique privacy-filter \
--privacy-check-model openai/privacy-filter \
--threshold 0.3 \
--output runs/avengers_privacy_filter \
--embedder sbert \
--device cpu
AWS Comprehend example:
python -m contrastive_privacy.scripts.text_resolution_analysis data/avengers_small \
--mode redact \
--trials 2 \
--samples 3 \
--approach privacy-check \
--sanitization-technique aws-comprehend \
--aws-region us-east-1 \
--aws-language-code en \
--unsanitized-types DATE_TIME \
--threshold 0.3 \
--output runs/avengers_aws_comprehend \
--embedder sbert \
--device cpu
Microsoft Presidio example (local; no Azure credentials):
python -m contrastive_privacy.scripts.text_resolution_analysis data/avengers_small \
--mode redact \
--trials 2 \
--samples 3 \
--approach privacy-check \
--sanitization-technique presidio \
--presidio-language en \
--unsanitized-types DATE_TIME \
--threshold 0.3 \
--output runs/avengers_presidio \
--embedder sbert \
--device cpu
Google Cloud Sensitive Data Protection example:
python -m contrastive_privacy.scripts.text_resolution_analysis data/avengers_small \
--mode redact \
--trials 2 \
--samples 3 \
--approach privacy-check \
--sanitization-technique google-cloud-dlp \
--gcp-project PROJECT_ID \
--gcp-location global \
--gcp-requests-per-minute 540 \
--gcp-max-retries 6 \
--sanitized-types PERSON_NAME EMAIL_ADDRESS PHONE_NUMBER STREET_ADDRESS \
--threshold 0.6 \
--output runs/avengers_google_cloud_dlp \
--embedder sbert \
--device cpu
Text analysis supports three obfuscation approaches:
--approach entity: uses GLiNER2 NER with --entities, optionally plus literal --instances.--approach concept: uses fal.ai OpenRouter with --concept and --concept-model; this requires FAL_KEY.--approach privacy-check: delegates detection and sanitization to contrastive_privacy.privacy_checks. Select privacy-filter, aws-comprehend, presidio, or google-cloud-dlp with --sanitization-technique. --mode, --placeholder, and --threshold configure all techniques; use --privacy-check-model for Privacy Filter, the --aws-* options for Comprehend, --presidio-language for Presidio, or the --gcp-* options for Google Cloud DLP. --sanitized-types whitelists the provider PII types to sanitize, while --unsanitized-types blacklists types to preserve; specify at most one policy.Common text flags:
--skip-summary-grids: skips summary_originals.pdf and summary_obfuscated.pdf; use this for large corpora whose combined text grid would exceed Pillow's image-size limit.
--mode blackout: replaces sensitive spans with block characters.
--mode redact: replaces sensitive spans with --placeholder, defaulting to [REDACTED].
--entities: entity types for GLiNER2, such as person, organization, movie, or custom open-vocabulary labels.
--instances: literal substrings to obfuscate without NER.
--threshold: NER detection threshold.
--sequential-labels: process entity labels separately and merge results, so adding labels cannot reduce detections.
--propagate / --no-propagate: control whether repeated occurrences of detected text are anonymized.
--embedder: sbert, clip, or qwen; the experiments use Qwen embeddings.
--embedder-quantization: none, half, 4bit, or 8bit.
After a resolution run, run similarity-analysis on the output folder:
similarity-analysis dicaprio_large/gpt-image-1.5_remove_t3 --model BAAI/EVA-CLIP-18B
For text runs:
similarity-analysis avg_small_gpt-5.4_t3 \
--embedder qwen \
--embedder-model Qwen/Qwen3-Embedding-8B
similarity-analysis auto-detects image vs. text outputs from obfuscated/. For image outputs, --model selects the CLIP image embedder. For text outputs, use --embedder and --embedder-model, or omit them and let the script reuse the settings saved in params.json. Some text follow-up commands in experiments.sh pass --model Qwen/Qwen3-Embedding-8B; that flag is image-only in the current CLI, so the text embedder still comes from params.json unless you pass --embedder-model.
Use --image-folder or --text-folder if the original data has moved since the resolution run.
Image and text resolution runs now auto-generate two extra files in the output folder unless you pass --skip-analysis-artifacts:
analysis_report.html: a static webpage with summary metrics, histograms, a searchable/sortable all-pairs table, and example pairs with explanations.analysis_report.json: a cached structured bundle for notebooks, dashboards, or downstream plotting without recomputing similarity.You can also regenerate or customize the report later with results-webpage:
results-webpage dicaprio_large/gpt-image-1.5_remove_t3
results-webpage avg_small_gpt-5.4_t3 --title "Avengers Text Run"
results-webpage some_output_folder --refresh --threshold 0.02 --top-n 8
results-webpage some_output_folder --low-utility-threshold 0.25 --high-utility-threshold 0.8 --open
open-results-webpage some_output_folder
Useful flags:
--refresh: ignore any cached analysis_report.json and recompute the bundle.--skip-similarity: render the report from resolution results only, without recomputing original-vs-obfuscated similarity.--low-utility-threshold and --high-utility-threshold: control the utility bands shown in the report and the low-utility example filter.--json-output and --output: override the default JSON/HTML output paths.--open: open the generated report immediately in your default browser.--image-folder or --text-folder: resolve original files from a different location if the dataset moved.Recommended local review loop:
open-results-webpage runs/dicaprio_clipseg_local_cp0 --skip-similarity
Use --skip-similarity when you only want to inspect an existing run and do not need to recompute original-vs-obfuscated utility metrics. Omit it when you want the report to include the similarity summary, low-utility examples, and utility bands based on fresh original-vs-obfuscated embeddings.
If you usually want the report opened after generation, open-results-webpage is a thin wrapper around results-webpage with browser opening enabled by default. Pass --no-open when you only want the artifacts refreshed.
For interactive inspection, see the notebook at notebooks/inspect_results.ipynb, which reuses the same JSON/HTML artifact pipeline.
experiments.shexperiments.sh is a flat list of commands, not a parameterized launcher. Running the entire file will attempt every experiment. New users should copy one command pair at a time: first the resolution-analysis or python -m ...text_resolution_analysis command, then the following similarity-analysis command against the output folder that was just created.
Each experiment usually has this shape:
# 1. Create obfuscated outputs and compute effective resolution.
resolution-analysis ... --output some_output_folder ...
# 2. Measure original-vs-obfuscated similarity for that output folder.
similarity-analysis some_output_folder ...
For text:
python -m contrastive_privacy.scripts.text_resolution_analysis ... --output some_output_folder ...
similarity-analysis some_output_folder ...
Some follow-up similarity commands point at suffixed folders such as _t2, _t3, or _t4. Those suffixes reflect iterative runs and retries. If you adapt a command, make sure the similarity-analysis argument exactly matches the --output folder you produced.
The image commands cover three datasets:
data/dicaprio with outputs under dicaprio_large/, usually --trials 29 --samples 30.data/mcdonalds_large with outputs under mcdonalds_large/, usually --trials 48 --samples 49.data/mcdonalds_small with outputs under mcdonalds_small/, usually --trials 8 --samples 9.Within each dataset, experiments.sh compares several image obfuscation backends:
--segmenter ai-gen --fal-image-model fal-ai/gpt-image-1.5--segmenter ai-gen --fal-image-model fal-ai/gpt-image-1-mini--segmenter ai-gen --fal-image-model fal-ai/gemini-3-pro-image-preview--segmenter ai-gen --fal-image-model fal-ai/gemini-3.1-flash-image-preview--segmenter ai-gen --fal-image-model fal-ai/flux-2-pro--segmenter ai-gen --fal-image-model fal-ai/flux-2--segmenter sam3 for several dicaprio manual/object-list runsBefore the resolution runs, each image dataset now has a concept-identification block:
identify-obfuscation-concepts data/dicaprio --model gpt-5.4 --output-format simple "the identity of the celebrities"
identify-obfuscation-concepts data/dicaprio --model gemini-3.1-pro --output-format simple "the identity of the celebrities"
identify-obfuscation-concepts data/dicaprio --model opus-4.6 --output-format simple "the identity of the celebrities"
The same pattern appears for data/mcdonalds_large and data/mcdonalds_small with the target concept the identity of the fast food restaurant. These calls are for generating model-specific candidate object lists. The resulting concepts correspond to the later --objects lists in output folders named with the model family, such as *_remove_gpt-5.4_t3, *_remove_gemini-3.1-pro_t3, and *_remove_opus-4.6_t3.
The ai-gen runs ask a fal.ai image editing model to handle detection and obfuscation together. They either use:
--privacy-concept, for high-level redaction such as "the identity of the celebrities" or "the identity of the fast food restaurant".--objects, for explicit target lists produced manually or by different LLMs.The output folder names encode the backend and target-list source:
*_remove_t3: concept-level removal using --privacy-concept.*_remove_manual_t3: manually chosen target objects.*_remove_gpt-5.4_t3, *_remove_gemini-3.1-pro_t3, *_remove_opus-4.6_t3: target object lists generated by those LLM families.The common image settings in the experiment file are:
--mode blackout: replace selected regions with black pixels.--blur 5 --dilate 25: expand and soften masks before blackout.--convex-hull: hide silhouettes more aggressively by filling concave mask gaps.--skip-empty-labels: ignore ambiguous empty-label detections from GroundingDINO-style paths.--fal-vision-temperature 0.1: low-temperature OpenRouter vision calls where applicable.--embedder-model BAAI/EVA-CLIP-18B: use EVA-CLIP embeddings for resolution scoring.--embed-batch-size 1: used with large embedders to fit GPU memory.The text commands cover two datasets:
data/avengers_small with outputs under avg_small_*, usually --trials 8 --samples 9.data/avengers_large with outputs under avg_large_*, usually --trials 48 --samples 49.Each text block starts with one manual/entity baseline:
python -m contrastive_privacy.scripts.text_resolution_analysis data/avengers_small \
--mode blackout \
--trials 8 \
--samples 9 \
--sequential-labels \
--output avg_small_manual \
--entities comic movie publisher author superhero abbreviation archetype year \
--threshold 0.05 \
--embedder qwen \
--embedder-model Qwen/Qwen3-Embedding-8B \
--embedder-quantization none \
--device cuda \
--embed-batch-size 1 \
--instances remake blockbuster battle first powerful defeated movies die
That baseline combines GLiNER2 entity labels (--entities) with literal substrings (--instances).
The rest of each text block uses concept redaction:
--approach concept \
--concept "anything that can identify the movie discussed in this passage" \
--concept-model <provider/model>
The experiment file compares OpenRouter models from OpenAI, Google, and Anthropic, including openai/gpt-5.4, openai/gpt-5.4-mini, openai/gpt-5.2, openai/gpt-4o, google/gemini-3.1-pro-preview, google/gemini-2.5-flash, anthropic/claude-opus-4.6, and related variants.
All text experiment commands use Qwen embeddings:
--embedder qwen \
--embedder-model Qwen/Qwen3-Embedding-8B \
--embedder-quantization none \
--embed-batch-size 1
If you run out of GPU memory, try --embedder-quantization half, reduce --embed-batch-size, or switch to --device cpu for smaller runs.
After installation, these image utilities are available as console scripts:
flux-anonymize: anonymize one image with local segmentation, fal.ai image editing, or OpenRouter vision.flux-create-mask: create a segmentation mask.flux-inpaint: inpaint an image using a mask.flux-generate and flux-edit: older FLUX generation/editing helpers.compare-images, recognize-objects, concept-resolution, identify-obfuscation-concepts, reanalyze-image-resolution, and reanalyze-text-resolution.Text single-file utilities are run as modules:
python -m contrastive_privacy.scripts.text_anonymize \
--input document.txt \
--output anonymized.txt \
--entities person organization \
--mode blackout
Use each script's --help for the complete CLI:
resolution-analysis --help
python -m contrastive_privacy.scripts.text_resolution_analysis --help
similarity-analysis --help
Resolution runs write reproducible artifacts to the chosen --output directory:
obfuscated/: one obfuscated_<original-name> file per successfully obfuscated item.results.csv: per-comparison resolution values.params.json: the effective parameters used by the run.report.txt: summary statistics, skipped items, and configuration.comparisons/: compound comparison images when comparison generation is enabled for image runs.summary_originals.jpg and summary_obfuscated.jpg: image-grid summaries for image runs.Use --continue after a partial run to reuse already-created obfuscations. Use --retry --output <previous-output> to retry references listed as skipped in a previous report.txt.
command not found: activate the environment and run pip install -e . again.FAL_KEY: required for ai-gen, vlm-bounding-box, and text --approach concept.sam3 access errors: run hf auth login and ensure your account has access to facebook/sam3.experiments.sh expect data/dicaprio, data/mcdonalds_large, and data/mcdonalds_small.--embed-batch-size, use --embedder-quantization half, use smaller samples, or run on CPU for smaller text experiments.hf auth login and accept any required model licenses on Hugging Face.nano-banana-2.data/dogs.txt, were generated using Google's gemini-3.1-pro.This project is licensed under the MIT License. See LICENSE for details.
29 commits
Python
81.7%
Shell
15.6%
HTML
2.0%