Metadata-Version: 2.4
Name: offlinetags
Version: 1.0.2
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/Offline-Tags
Project-URL: Repository, https://github.com/fiker31/Offline-Tags
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
