Metadata-Version: 2.4
Name: offlinetags
Version: 1.0.1
Summary: Privacy-preference tag detection — detect, blur, annotate, and smart-match tags across photos
Author: fiker
License: MIT
Project-URL: Homepage, https://github.com/fiker31/OfflineTags
Project-URL: Repository, https://github.com/fiker31/OfflineTags
Keywords: computer-vision,privacy,yolo,face-blur,object-detection
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Scientific/Engineering :: Image Recognition
Classifier: Operating System :: OS Independent
Requires-Python: >=3.10
Description-Content-Type: text/markdown
Requires-Dist: opencv-python>=4.5
Requires-Dist: numpy<2.0,>=1.21
Requires-Dist: ultralytics>=8.0
Requires-Dist: facenet-pytorch
Requires-Dist: torch
Requires-Dist: Pillow
Requires-Dist: tqdm>=4.60
Requires-Dist: requests>=2.27
Provides-Extra: smart
Requires-Dist: facenet-pytorch; extra == "smart"
Requires-Dist: torch; extra == "smart"

# 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

```bash
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

```python
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`

```python
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

```python
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

```python
import glob
paths = glob.glob("event/*.jpg")
for r in ot.batch(paths):
    print(r.filename, "->", r.action)
```

### Smart batch (face matching across photos)

```python
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.

```python
# 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
