Skip to main content

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 by ultralytics on 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

  1. Detect — the image is converted to grayscale and passed to the trained YOLOv8 model, which returns the badges it finds.
  2. Act — based on the detected tags:
    • No Photo → the image is flagged discarded.
    • 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.
  3. 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, raw bytes, or a BGR np.ndarray. Use bytes for uploads.
  • The result image is a NumPy BGR array (result.processed); encode it with cv2.imencode(".jpg", result.processed) to return it over HTTP.
  • For many concurrent users, run behind a production server (e.g. gunicorn/uvicorn with 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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

offlinetags-1.0.2.tar.gz (5.7 MB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

offlinetags-1.0.2-py3-none-any.whl (5.7 MB view details)

Uploaded Python 3

File details

Details for the file offlinetags-1.0.2.tar.gz.

File metadata

  • Download URL: offlinetags-1.0.2.tar.gz
  • Upload date:
  • Size: 5.7 MB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.10.0

File hashes

Hashes for offlinetags-1.0.2.tar.gz
Algorithm Hash digest
SHA256 c34c58873a249119fa8d6cb9fbd903d54ee063ac322c9d33fd92673f64473b0a
MD5 bd0aa931e26a5cdde2ca02a90017e4f9
BLAKE2b-256 1384aafb5cdc7abec298ccf06e86f9633ec4be606fd45b041add9143b279650c

See more details on using hashes here.

File details

Details for the file offlinetags-1.0.2-py3-none-any.whl.

File metadata

  • Download URL: offlinetags-1.0.2-py3-none-any.whl
  • Upload date:
  • Size: 5.7 MB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.10.0

File hashes

Hashes for offlinetags-1.0.2-py3-none-any.whl
Algorithm Hash digest
SHA256 e567bf3256220096586a311513bee6e2ca3573d6fed2f66c246f06b433a0cce6
MD5 81bd5df2592cd6e38ab5cf52eb99d3cb
BLAKE2b-256 4cba7e4fcbf2497ee80db32470bce1aed8c4994f7027b02d4643c9ec396db687

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page