Metadata-Version: 2.4
Name: avmosaic
Version: 0.1.2
Summary: Detect and mosaic explicit regions in images and videos (CLI + Python API).
Author: avmosaic contributors
License-Expression: MIT
Project-URL: Homepage, https://github.com/fattaillabs/avmosaic
Project-URL: Repository, https://github.com/fattaillabs/avmosaic
Project-URL: Issues, https://github.com/fattaillabs/avmosaic/issues
Project-URL: Korean README, https://github.com/fattaillabs/avmosaic/blob/main/README.ko.md
Project-URL: Japanese README, https://github.com/fattaillabs/avmosaic/blob/main/README.ja.md
Keywords: censorship,mosaic,nudity-detection,pixelate,video,onnx
Classifier: Development Status :: 3 - Alpha
Classifier: Environment :: Console
Classifier: Intended Audience :: End Users/Desktop
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Multimedia :: Graphics
Classifier: Topic :: Multimedia :: Video
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
License-File: NOTICE
Requires-Dist: numpy>=1.23
Requires-Dist: opencv-python-headless>=4.8
Requires-Dist: onnxruntime>=1.16
Requires-Dist: nudenet<4,>=3.4
Requires-Dist: imageio-ffmpeg>=0.4.9
Provides-Extra: dev
Requires-Dist: pytest>=7; extra == "dev"
Requires-Dist: ruff>=0.5; extra == "dev"
Dynamic: license-file

# avmosaic

Detect explicit regions in images and videos and cover them with a mosaic — from the
command line or from Python, on Windows, macOS and Linux.

![original · detection boxes (--draw-boxes) · mosaic](https://raw.githubusercontent.com/fattaillabs/avmosaic/main/docs/demo.jpg)

*Demo uses the `faces` preset on a public-domain NASA portrait; the default preset
censors genitalia and the anus.*

**English** · [한국어](https://github.com/fattaillabs/avmosaic/blob/main/README.ko.md) · [日本語](https://github.com/fattaillabs/avmosaic/blob/main/README.ja.md)

> **Automated detection is an aid, not a guarantee.** The detector will miss regions
> (unusual angles, close-ups, occlusion, lighting) and will sometimes censor the wrong
> thing. Review every output before publishing it. You are responsible for complying
> with the laws that apply to your content. Only process material you have the right to
> process.

## Features

- Detection with the open-source [NudeNet](https://github.com/notAI-tech/NudeNet) v3
  ONNX models (`320n` ships with the package; `640m` is more accurate and downloaded on
  demand). Runs on CPU, or with CUDA / DirectML / CoreML through ONNX Runtime.
- Mosaic tiles aligned to a fixed grid so they don't shimmer between frames. The default
  tile size follows a commonly cited guideline: at least 1/100 of the image's long side
  and never under 4 px. Blur and solid fill are available too.
- Video tracking: each region is followed with optical flow, so the mosaic moves with it
  instead of smearing along its path. This covers the frames between detector runs, short
  stretches where the detector loses the region, and a few frames before it is first
  detected. Tracks end at scene cuts, and audio is kept.
- Every key parameter is a flag: classes, threshold, model, padding, tile size, shape,
  hold, codec and quality.
- Batch processing of folders. JSON reports of every detection. A `--draw-boxes` mode
  for tuning.
- Handles Unicode file names (Korean, Japanese, …), alpha channels, odd video sizes, and
  NTSC frame rates.

## Install

```bash
pip install avmosaic
```

To keep the command-line tool in its own environment, use `pipx install avmosaic` instead.

Python 3.10+ is required. ffmpeg comes with `imageio-ffmpeg`, so there is nothing else to
install. On Intel Macs, note that recent ONNX Runtime releases ship no Intel-macOS wheels,
so `pip` can only use an older ONNX Runtime, and only if one exists for your Python
version (Python 3.14 on Intel macOS has none).

GPU (optional): avmosaic installs the CPU build of ONNX Runtime. For a GPU, swap it for
the matching build, then pass `--device cuda` or `--device directml`:

```bash
pip uninstall -y onnxruntime
pip install onnxruntime-gpu          # NVIDIA CUDA (Linux, Windows)
# or: pip install onnxruntime-directml   (Windows, any GPU)
```

Upgrading avmosaic can bring the CPU build back; repeat the swap if `--device cuda`
reports that CUDA is missing. The default `--device auto` picks CUDA, then DirectML, then
CoreML (included in the standard build on Apple Silicon), then CPU.

From source, for development:

```bash
git clone https://github.com/fattaillabs/avmosaic && cd avmosaic
pip install -e ".[dev]"
```

## Quick start

After `pip install avmosaic`:

```bash
avmosaic video.mp4                    # -> video_censored.mp4
avmosaic photo.jpg -o out.jpg
avmosaic ./scenes -o ./censored       # whole folder, structure mirrored
avmosaic video.mp4 --model 640m --detect-every 2 --hold 8
avmosaic photo.jpg --draw-boxes       # see what was detected and how confident it was
avmosaic video.mp4 --add-region 2.3-3.9:190,340,275,430   # also cover a region the detector misses
avmosaic detect video.mp4 -o detections.json   # detect only, no output media
avmosaic classes                      # list classes and presets
avmosaic models download 640m         # fetch the bigger model ahead of time
avmosaic frames sample.mp4 -o ./frames --every 2   # video -> images to label
avmosaic label ./frames                # mark where the mosaic must go, in your browser
avmosaic eval regions ./frames         # is the mosaic where you marked it?
avmosaic eval run ./NPDI -o npdi.jsonl  # benchmark on a labelled dataset (see below)
avmosaic eval summary npdi.jsonl
```

`avmosaic FILE …` is shorthand for `avmosaic censor FILE …`.

## Parameters

| Flag | Default | Meaning |
|---|---|---|
| `--classes` | `genitals` | Presets and/or class names, comma separated. Presets: `genitals` (female/male genitalia, anus), `breasts`, `buttocks`, `nudity`, `faces`, `all`. |
| `--threshold` | `0.25` | Minimum detection score. Lower it to censor more aggressively. |
| `--model` | `320n` | `320n`, `640m`, or a path to an `.onnx` file. |
| `--resolution` | model's own | Model input size (a multiple of 32). |
| `--device` | `auto` | `auto`, `cpu`, `cuda`, `tensorrt`, `directml`, `coreml`, `openvino`. |
| `--channels` | `rgb` | Channel order fed to the model (`bgr` reproduces upstream NudeNet exactly). |
| `--method` | `mosaic` | `mosaic`, `blur` or `fill`. |
| `--block-size` | `auto` | Mosaic tile: `auto`, a pixel count (`24`), or a share of the long side (`2%`). |
| `--shape` | `rect` | `rect`, or `ellipse` (still covers the whole box). |
| `--padding` | `0.15` | Grow each box by this fraction of its size on every side. |
| `--fill-color` | `black` | For `--method fill`: `black`, `white`, `#rrggbb`, `r,g,b`. |
| `--detect-every` | `1` | Video: run the detector on every Nth frame (and on every scene cut). Tracking fills the frames in between. |
| `--tracking` | `on` | Video: follow regions with optical flow. `off` censors the union of boxes found within `--hold` frames instead (simpler, but smears fast motion). |
| `--hold` | `5` | Tracking on: follow a new region back N frames before its first detection. Off: censor boxes found up to N frames before and after. |
| `--max-lost` | `15` | Tracking on: keep following a region N frames after the detector lost it. |
| `--smooth` | `2` | Tracking on: average box positions over ±N frames (the raw box is always covered too). |
| `--codec` / `--crf` / `--preset` | `libx264` / `18` / `medium` | Video encoder settings. `--ffmpeg-args "…"` passes extra encoder flags. |
| `--no-audio` | off | Drop the audio track. Audio is otherwise copied, and converted to AAC when the source codec doesn't belong in MP4 (e.g. PCM). |
| `--report FILE` | — | Write every detection (per frame for video) as JSON. |
| `--draw-boxes` | off | Draw boxes, classes and scores on the output. |
| `--add-region SPEC` | — | Also censor a region the detector misses: `x1,y1,x2,y2` for an image or a whole video, or `START-END:x1,y1,x2,y2` for part of a video (seconds or m:ss). The box is where the region is at START; optical flow follows it until END. Repeatable. |
| `--progress` | `auto` | Progress: a bar in a terminal (`auto`); `always` also logs a line every 10 s when output goes to a file. |
| `-o`, `--suffix`, `-y` | — | Output file or folder, suffix used when `-o` is absent, overwrite. |

### Tuning

1. Run `avmosaic sample.mp4 --draw-boxes --report r.json` and look at the result.
2. Missed regions → lower `--threshold` (e.g. `0.15`), try `--model 640m`, raise `--hold`
   and `--padding`.
3. Too much censored → raise `--threshold`, and check which classes the report says
   were found.

## Recommended settings

These are starting points, not guarantees. Check the result with `--draw-boxes` and adjust.

| Goal | Flags | Notes |
|---|---|---|
| Balanced (default) | none | `320n`, `--threshold 0.25`, tracking on |
| Fewer false alarms | `--threshold 0.32` (or `0.35`) | On 7,783 swimwear photos, false alarms fell from 0.8% at 0.25 to 0.4% at 0.35. On a 12-minute test clip, 0.32 cut the mosaic segments from 59 to 33 and still caught every long segment. |
| Fewer misses (safety first) | `--threshold 0.15 --model 640m --max-lost 30 --hold 8 --padding 0.25` | Lower bar, bigger model, longer tracking, larger boxes. Slower, and censors more. |
| Faster | `--detect-every 2` (30 fps video) or `--detect-every 3` (60 fps) | Tracking fills the frames in between. A 1080p clip went from 22.0 s to 16.1 s with `--detect-every 2`. |
| Coarser mosaic on small videos | `--block-size 2%` | `auto` is max(4 px, 1/100 of the long side), so a 640×360 video gets 7 px tiles. |
| Also cover breasts | `--classes genitals,breasts` | |
| Anonymize faces | `--classes faces` | |
| Something the detector misses | `--add-region 2.3-3.9:190,340,275,430` | Mark it once where it first appears and optical flow follows it. Read the coordinates off a frame, e.g. one saved with `avmosaic frames`. |

## Marking regions the detector misses

Sometimes the detector never finds a region: nudity on a phone screen, monitor or poster,
a heavily blurred shot, an unusual angle. Lowering `--threshold` does not help when the
model gives it almost no score. Mark such a region once and avmosaic follows it.

1. **Find when and where it is.** Watch the output; `--draw-boxes` shows what was found.
   For exact numbers, save frames around that moment and mark the region in the label
   page:

   ```bash
   avmosaic frames video.mp4 -o ./frames --start 2 --end 4 --every 0.1   # video_00002300ms.jpg = the frame at 2.3 s
   avmosaic label ./frames                              # drag a box; it is saved as video_00002300ms.json
   ```

   The JSON holds `[x1, y1, x2, y2]` in video pixels.

2. **Add it.** Give the time range and the box at the start time:

   ```bash
   avmosaic video.mp4 --add-region 2.3-3.95:190,340,275,430
   ```

   Times can be seconds (`12.5`), `m:ss` (`1:02.5`) or `h:mm:ss`. Without a time range
   (`--add-region 190,340,275,430`) the box applies to the whole video, or to an image.
   Repeat the flag to mark several regions.

3. **Check it.** With `--draw-boxes`, marked regions are labelled `MANUAL`. If the box
   slips (fast motion, rotation, the region leaving the frame), split the range where it
   slips and mark it again, e.g.
   `--add-region 2.3-3.4:190,340,275,430 --add-region 3.4-3.95:150,140,270,330`.

A marked region stays until its end time even when nothing is detected, is never merged
with detections, follows the region's motion and scale with optical flow, and holds its
place across a scene cut. With `--tracking off` it stays where you put it.

## How tracking works

For each region the detector finds, avmosaic keeps a track and follows it frame by frame
with median-flow optical flow (Lucas–Kanade on a grid of points, with a forward–backward
check). The tracker is built to cover too much rather than too little:

- **Between detector runs** (`--detect-every N`): when the next detection arrives, the
  accumulated flow drift is spread back over the frames in between. Both the flow path and
  the corrected path are covered.
- **Detector misses**: the track continues on optical flow for up to `--max-lost` frames.
  If flow cannot follow (occlusion, blur, a featureless region), the box covers both "it
  stopped" and "it kept its recent speed". When the detector finds the region again within
  `--max-lost` frames, the frames of the gap are still buffered, so they also get the
  straight path between the two detections. The box also grows 1% per frame without a
  detection, up to +50%.
- **Before the first detection**: a new track is followed backwards over `--hold` frames.
- **Scene cuts**: these end all detector tracks and force a detection. Nothing is carried
  across the cut.
- **Marked regions** (`--add-region`) are tracks of their own. They follow optical flow from
  their start to their end time whatever the detector does, and hold their place across a
  scene cut.

Frames are rendered with a look-ahead of `max(--max-lost, --hold, --smooth,
--detect-every − 1)` frames. At the defaults that is 15 frames, or about 100 MB at 1080p.

## Checking that the mosaic is in the right place

Whether a file is explicit is the wrong question for a censoring tool. What matters is
whether the mosaic lands where it should. A person marks that once, on a few samples, and
then it can be checked automatically every time the code or settings change:

1. `avmosaic frames sample.mp4 -o ./frames --every 2` saves a frame every 2 seconds.
2. `avmosaic label ./frames` opens a local page (served on 127.0.0.1 only). Drag a box
   around every region that must be censored. Press **C** for an image that must stay
   clean. Boxes are saved next to each image as `<name>.json`. The page starts empty;
   `--suggest` pre-fills the detector's boxes, which is faster but biases the labels
   towards what the detector already finds.
3. `avmosaic eval regions ./frames` prints a text report:
   - how much of each marked region the mosaic covers, with the worst cases listed
   - how much of the mosaic falls outside the marked regions
   - whether any clean image was censored

   It exits with status 1 if a region is covered less than 90% (`--min-coverage`) or a
   clean image loses more than 2% (`--max-false-area`). It takes the same detection and
   censoring flags as `censor`, so settings can be compared directly.
   `AVMOSAIC_SAMPLES=./frames pytest tests/test_samples.py` runs the same check as tests.

## Trying it on real footage

avmosaic does not include, host or distribute any explicit material, and its tests use
none. To try it on real footage, you can use a public research dataset such as
[The Image and video dataset for adult content detection in image and video](https://figshare.com/articles/dataset/The_Image_and_video_dataset_for_adult_content_detection_in_image_and_video/14495367?file=27771210)
(figshare).

> **18+, at your own risk.** Such datasets contain explicit adult content. Before
> downloading, read the dataset page, including its license and disclaimer. Use the data
> at your own risk and only where that is lawful. Never commit files from it to this
> repository or attach them to issues.

```bash
avmosaic "some video.mp4" --draw-boxes --report report.json   # then watch the result
avmosaic frames "some video.mp4" -o ./frames --every 2           # label + eval regions
```

More about test data and private samples: [tests/data/README.md](https://github.com/fattaillabs/avmosaic/blob/main/tests/data/README.md).

## Benchmarking on NPDI / Pornography-2k

These research datasets are not public downloads. They contain copyrighted adult material,
so their maintainers share them only after a formal request and a signed responsibility
agreement:

- **NPDI** (800 videos, ~80 h; half clean, including 200 "difficult" clips such as beaches
  and wrestling): Prof. Sandra Avila, UNICAMP.
- **Pornography-2k** (2,000 videos, ~140 h): Prof. Anderson Rocha, UNICAMP.

Both have per-video labels only, without region boxes, so `avmosaic eval` measures what
those labels allow. It also accepts folders of images, where each image counts as one
sample, e.g. explicit images vs. swimwear photos as hard negatives. A file's label comes
from its path below (and including) the folder you pass: anything matching
`no[nt][-_ ]?porn` (`nonporn`, `non_porn`, `not_porn`, …) is clean, then anything matching
`porn` is explicit. Change this with `--positive` and `--negative`.

```bash
avmosaic eval run ./NPDI -o npdi-320n.jsonl --sample-fps 1             # resumable
avmosaic eval run ./NPDI -o npdi-640m.jsonl --model 640m
avmosaic eval summary npdi-320n.jsonl npdi-640m.jsonl --thresholds 0.15,0.25,0.35
```

The summary reports the following, as text only:

- the file-level AUC
- for each threshold, the share of explicit and clean videos that get censored at all,
  and the mean share of sampled frames censored (split into easy and difficult clean
  videos when the path says so)
- the clean files that scored highest, with timestamps, so you can check them for false
  alarms
- the explicit files where nothing was found

## Speed

Measured on an Apple M1 (CoreML) with a 1080p, 30 fps, 20-second clip (600 frames):

| Settings | Time | Throughput |
|---|---|---|
| default (`320n`) | 22.0 s | 27 fps |
| `--detect-every 2` | 16.1 s | 37 fps |
| `--model 640m --detect-every 3` | 75.6 s | 7.9 fps |

Detection alone takes 15.7 ms per 1080p frame for `320n` (33.1 ms on CPU) and 346 ms for
`640m` (469 ms on CPU). Re-encoding the same clip with ffmpeg alone takes 8.3 s.

Tracking added about 10% in a separate run on the same clip (31.8 s → 34.8 s). The clip
contained no regions to follow, so this is the scene-cut check. With tracking,
`--detect-every 3` keeps boxes on target and took 20.0 s.

## Python API

```python
from avmosaic import CensorOptions, VideoOptions, censor_image, censor_video, create_detector

detector = create_detector("320n", classes=["FEMALE_GENITALIA_EXPOSED", "MALE_GENITALIA_EXPOSED"],
                           threshold=0.2)
censor_image("in.jpg", "out.jpg", detector, CensorOptions(block_size="2%"))
censor_video("in.mp4", "out.mp4", detector, CensorOptions(), VideoOptions(hold=8))
```

Any object with a `detect(bgr_image) -> list[Detection]` method can replace the
detector, for example a model you trained yourself.

## Testing

```bash
git clone https://github.com/fattaillabs/avmosaic && cd avmosaic
pip install -e ".[dev]"
pytest                    # everything (about 10 s)
pytest -m "not model"     # skip the tests that run the real model
```

The tests use no explicit material.

- **Pipeline tests** replace the model with a colour detector that treats a synthetic
  red/black checkerboard as the region to censor. They check mosaic geometry, frame count,
  audio and duration, the frame-hold logic (a deliberately missed frame leaks with
  `hold=0` and is covered with `hold=2`), odd sizes, NTSC rates and Unicode paths.
- **Tracking tests** run the planner on in-memory frames and measure exactly how much of
  the true object box is covered in every frame. The scenarios are fast motion, 15-frame
  detector misses, curved paths with `--detect-every 6`, backfill, scene cuts, two objects
  moving in opposite directions, and forced optical-flow failure. In the fast-motion test,
  a 40 px object stays fully covered either way. Tracking censors 1.03× the object's area
  at 8 px/frame and 1.14× at 25 px/frame. `--tracking off` censors 2.70× and 5.48×.
- **Model tests** run the real NudeNet model. They check that its output matches upstream
  `nudenet` exactly in BGR mode, and use the model's *face* class on a public-domain photo
  as an end-to-end check. A 3-second clip made from a CC0 photo of Michelangelo's David
  checks the real model and tracking against a hand-marked region in every frame
  (see [tests/data/README.md](https://github.com/fattaillabs/avmosaic/blob/main/tests/data/README.md)).
- **Your own accuracy check:** put labelled images in a private folder (never commit them)
  and run `AVMOSAIC_SAMPLES=/path/to/samples pytest tests/test_samples.py -v`. See
  [tests/test_samples.py](https://github.com/fattaillabs/avmosaic/blob/main/tests/test_samples.py) for the label format.

CI runs the suite on Linux, macOS and Windows with Python 3.10, 3.12 and 3.14.

## Limitations

- The detector is the main source of error. NudeNet was trained on general nudity
  images. Small regions in large frames, extreme close-ups and partial occlusion are
  hard for it.
- Nudity shown on a phone screen, monitor or poster, or heavily blurred, is often not
  detected at all, and lowering `--threshold` does not help (in one test clip the model
  scored it below 0.05 in every frame). Review the output and mark such regions with
  `--add-region`.
- Variable-frame-rate video is converted to a constant rate (this keeps audio in sync).
- Subtitle and data streams are not copied. Odd video sizes are padded by one pixel for
  H.264.
- Image metadata (EXIF) is not copied to the output. The EXIF orientation is applied
  first.

## License

MIT for this project's code — see [LICENSE](https://github.com/fattaillabs/avmosaic/blob/main/LICENSE). Third-party components, including
an important note about NudeNet's license, are listed in [NOTICE](https://github.com/fattaillabs/avmosaic/blob/main/NOTICE).
