Privacy-preference tag detection — detect, blur, annotate, and smart-match tags across photos
Project description
OfflineTags
Privacy-preference tag detection for photos — detect, blur, annotate, and match across images.
OfflineTags detects small circular privacy badges worn by people in photos and
automatically enforces each person's stated preference: blurring faces, marking consent,
or flagging photos that must not be shared. It is a pure-Python library built on a trained
YOLOv8 model — import it and call its API; there is no UI or server.
The four tags
| Tag | Colour | Symbol | Meaning | Action taken |
|---|---|---|---|---|
| Tag Me | Green | target ◎ | Welcomes being tagged | Draws a green box around the person |
| Upload Me | Yellow | check ✓ | Consents to uploads | Draws a yellow box around the person |
| Blur Me | Blue | minus — | Please blur my face | Pixelates the person's face |
| No Photo | Red | cross ✕ | Do not photograph me | Flags the image as discarded |
Installation
pip install offlinetags
- The tag-detection model is bundled inside the package (~6 MB), so detection works offline with no extra download.
- The person-detection model (
yolov8n.pt) is fetched automatically byultralyticson first use. - Installation pulls PyTorch (large), because the YOLO model requires it.
Requires Python ≥ 3.10. Runs on CPU (a GPU is optional and used automatically if available).
Quickstart
from offlinetags import OfflineTags
ot = OfflineTags()
result = ot.process("photo.jpg")
print(result.action) # "clean" | "annotated" | "blurred" | "discarded"
print([d.tag_type for d in result.detections])
# Save the processed image (only present for "annotated" / "blurred")
if result.processed is not None:
import cv2
cv2.imwrite("photo_processed.jpg", result.processed)
Public API
OfflineTags(config: OfflineTagsConfig | None = None)
The main entry point. Loads the models once and reuses them.
.process(image, filename=None) -> ProcessResult
Detect tags in one image and apply the appropriate action.
image may be a file path (str/Path), raw bytes, or a BGR np.ndarray.
.batch(images, filenames=None) -> list[ProcessResult]
Process a list of images independently (one ProcessResult each).
.smart_batch(images, filenames=None) -> SmartBatchResult
Cross-image face matching: builds a face registry from images where a tag is visible, then infers tags for the same person in images where their badge is hidden.
Return types
ProcessResult
| Field | Type | Description |
|---|---|---|
filename |
str |
Image name |
action |
str |
"clean", "annotated", "blurred", or "discarded" |
detections |
list[Detection] |
All detected (and inferred) tags |
processed |
np.ndarray | None |
BGR result image (only for annotated/blurred) |
.to_dict() |
dict |
JSON-serialisable form (excludes the image array) |
Detection
| Field | Type | Description |
|---|---|---|
tag_type |
str |
"Tag Me", "Upload Me", "Blur Me", "No Photo" |
circle_color |
str |
green / yellow / blue / red |
inner_symbol |
str |
target / check / minus / x |
confidence |
float |
Detection confidence (0–1) |
position |
dict |
{"x", "y", "radius"} in pixels |
inferred |
bool |
True if assigned via smart-batch face matching |
similarity |
float | None |
Face-match similarity (smart batch only) |
SmartBatchResult
| Field | Type | Description |
|---|---|---|
results |
list[ProcessResult] |
One per input image |
edge_cases |
list[dict] |
Conflicts where the same face had different tags |
Configuration — OfflineTagsConfig
from offlinetags import OfflineTags, OfflineTagsConfig
cfg = OfflineTagsConfig(
tag_conf_threshold=0.25, # tag detection confidence
person_conf_threshold=0.30, # person detection confidence
face_padding=0.45, # extra margin around a blurred face
pixel_block=25, # blur mosaic block size (bigger = blockier)
jpeg_quality=92,
smart_match_threshold=0.75, # face-match similarity for smart batch
tag_model_path=None, # override the bundled model (path to a .pt)
person_model_path=None, # override yolov8n.pt
)
ot = OfflineTags(cfg)
You can also point at a custom model via environment variables:
OFFLINETAGS_TAG_MODEL and OFFLINETAGS_PERSON_MODEL.
Examples
Handle each outcome
r = ot.process("photo.jpg")
if r.action == "discarded":
print("Contains a 'No Photo' tag — do not share.")
elif r.action == "clean":
print("No privacy tags found.")
else: # "annotated" or "blurred"
import cv2
cv2.imwrite(f"out_{r.filename}", r.processed)
Standard batch
import glob
paths = glob.glob("event/*.jpg")
for r in ot.batch(paths):
print(r.filename, "->", r.action)
Smart batch (face matching across photos)
import glob, cv2
paths = glob.glob("event/*.jpg")
batch = ot.smart_batch(paths)
for r in batch.results:
print(r.filename, r.action,
[(d.tag_type, "inferred" if d.inferred else "detected") for d in r.detections])
if r.processed is not None:
cv2.imwrite(f"out_{r.filename}", r.processed)
# Photos where the same person showed conflicting tags
for case in batch.edge_cases:
print("conflict:", case)
How it works
- Detect — the image is converted to grayscale and passed to the trained YOLOv8 model, which returns the badges it finds.
- Act — based on the detected tags:
No Photo→ the image is flaggeddiscarded.Blur Me→ the person is located (YOLO) and their face (OpenCV Haar cascade) is pixelated.Tag Me/Upload Me→ a coloured consent box is drawn around the person.
- Smart batch — faces are embedded with FaceNet and matched across images, so a person's preference is applied even in photos where their badge is not visible.
Using OfflineTags in a web backend
OfflineTags is a framework-agnostic library, so you can put your own frontend
(web page, mobile app, anything) in front of it. Your server receives the uploaded image
and calls process() — process() accepts raw bytes, which is exactly what a file
upload provides.
Example: a minimal Flask backend powering an image-upload page.
# pip install offlinetags flask
from flask import Flask, request, send_file, jsonify
from offlinetags import OfflineTags
import cv2, io
app = Flask(__name__)
ot = OfflineTags() # load the model ONCE at startup, then reuse for every request
@app.route("/process", methods=["POST"])
def process():
file = request.files["image"]
result = ot.process(file.read()) # OfflineTags does all the detection/processing
if result.action == "discarded":
return jsonify(action="discarded", reason="No Photo tag found")
if result.processed is None: # "clean" — no tags
return jsonify(action="clean")
# send the blurred / annotated image back to the frontend
ok, buf = cv2.imencode(".jpg", result.processed)
return send_file(io.BytesIO(buf.tobytes()), mimetype="image/jpeg")
The frontend simply POSTs the uploaded file to /process and displays the response.
Integration tips
- Create
OfflineTags()once (at startup), not per request — loading the model is the slow part (~seconds). Reusing the instance makes each request fast (~100 ms/image). process()accepts a file path, rawbytes, or a BGRnp.ndarray. Usebytesfor uploads.- The result image is a NumPy BGR array (
result.processed); encode it withcv2.imencode(".jpg", result.processed)to return it over HTTP. - For many concurrent users, run behind a production server (e.g.
gunicorn/uvicornwith multiple workers). The loaded model is read-only and safe to share. - The same pattern works with FastAPI, Django, or any other framework — only the
request/response glue changes; the
ot.process(...)call stays identical.
Limitations
- Smart-batch face matching works best on clear, front-facing faces; extreme angles, occlusion, or very different lighting can reduce matches.
- Detection is trained on the four specific badge designs above.
- First run downloads
yolov8n.pt(person model) and, for smart batch, the FaceNet weights.
License
MIT
Project details
Release history Release notifications | RSS feed
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