Metadata-Version: 2.5
Name: yahoo-open-nsfw-onnx
Version: 1.0.0
Summary: Yahoo's Open-NSFW image classifier as a self-contained ONNX Runtime package
Project-URL: Homepage, https://github.com/Robinbinu/yahoo-open-nsfw-onnx
Project-URL: Issues, https://github.com/Robinbinu/yahoo-open-nsfw-onnx/issues
License: MIT License
        
        Copyright (c) 2026 Robin Binu
        
        Permission is hereby granted, free of charge, to any person obtaining a copy
        of this software and associated documentation files (the "Software"), to deal
        in the Software without restriction, including without limitation the rights
        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
        copies of the Software, and to permit persons to whom the Software is
        furnished to do so, subject to the following conditions:
        
        The above copyright notice and this permission notice shall be included in all
        copies or substantial portions of the Software.
        
        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
        SOFTWARE.
License-File: LICENSE
License-File: NOTICE
Keywords: content-moderation,image-classification,nsfw,onnx,open-nsfw
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
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: Topic :: Scientific/Engineering :: Image Recognition
Classifier: Typing :: Typed
Requires-Python: >=3.9
Requires-Dist: numpy>=1.22
Requires-Dist: onnxruntime>=1.16
Requires-Dist: pillow>=9.0
Provides-Extra: dev
Requires-Dist: pytest>=7.4; extra == 'dev'
Requires-Dist: ruff>=0.6; extra == 'dev'
Provides-Extra: export
Requires-Dist: keras>=3.6; extra == 'export'
Requires-Dist: onnx>=1.15; extra == 'export'
Requires-Dist: opennsfw2>=0.18; extra == 'export'
Requires-Dist: torch>=2.1; extra == 'export'
Description-Content-Type: text/markdown

# yahoo-open-nsfw-onnx

Yahoo's Open-NSFW image classifier, packaged as an ONNX Runtime model with no
TensorFlow, Keras or PyTorch at runtime.

The model scores an image between 0 and 1 for pornographic content. It is small
(5.9M parameters, 24 MB) and fast enough on CPU that a GPU is hard to justify:
7 ms per image on four cores, in a process that holds around 130 MB of memory.

<picture>
  <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/Robinbinu/yahoo-open-nsfw-onnx/main/docs/runtime-comparison-dark.svg">
  <img src="https://raw.githubusercontent.com/Robinbinu/yahoo-open-nsfw-onnx/main/docs/runtime-comparison-light.svg" alt="Latency, memory and cold start compared against running the same model under Keras and TensorFlow" width="720">
</picture>

## Install

```
pip install yahoo-open-nsfw-onnx
```

The wheel bundles the `.onnx` file. There is no download step at first use and
no network access at runtime.

## Use

```python
from PIL import Image
from yahoo_open_nsfw_onnx import Classifier

classifier = Classifier()

score = classifier.score(Image.open("photo.jpg"))
print(score)  # 0.0041

# Batching is roughly three times faster per image than one call each.
scores = classifier.score_paths(["a.jpg", "b.png", "c.webp"])
```

A `Classifier` holds one ONNX Runtime session. Sessions are safe to call from
multiple threads, so build one and share it rather than creating one per
request.

From the shell:

```
yahoo-open-nsfw-onnx photos/ --recursive --format csv > scores.csv
```

```
yahoo-open-nsfw-onnx a.jpg b.jpg
  0.004132  sfw        a.jpg
  0.981204  nsfw       b.jpg
```

## Reading the score

The score is P(NSFW). Yahoo published no fixed cutoff and suggested tuning it
per application. As a starting point, treat 0.2 and 0.8 as the edges of a band
where the model is not committing to an answer:

| score | reading |
| --- | --- |
| below 0.2 | safe |
| 0.2 to 0.8 | undecided, needs a second opinion |
| above 0.8 | pornographic |

Two things the model does not do, both of which catch people out:

**It only detects pornography.** It was trained on pornographic versus
non-pornographic images. Violence, gore, weapons, drugs and hate symbols all
score low. If you need those, you need a different model.

**Large areas of skin-toned texture drive the score up.** Running 3,594 AI
generated illustrations through it, 20 crossed 0.8. Six of those were
Spider-Man and Hulk pictures. A form-fitting suit on a human silhouette hits
the same features as bare skin, and the model has no concept of clothing. Line
art is not a defence either: pencil sketches of nudes scored above 0.99.

Treating this as a prefilter in front of a slower and more capable model works
well. In that run, 3.7% of images landed at 0.2 or above, so the expensive
check ran on a thirtieth of the traffic.

## Performance

Measured on an Apple M4 (4 performance cores, 6 efficiency cores), macOS 26.5,
ONNX Runtime 1.28. Reproduce with `python benchmarks/bench.py`.

| batch size | latency | per image | throughput |
| ---: | ---: | ---: | ---: |
| 1 | 6.7 ms | 6.73 ms | 149 img/s |
| 8 | 67 ms | 8.39 ms | 119 img/s |
| 32 | 277 ms | 8.65 ms | 116 img/s |
| 64 | 595 ms | 9.30 ms | 108 img/s |

Memory, resident set size of the whole process:

| stage | RSS |
| --- | ---: |
| after importing onnxruntime | 47 MB |
| after loading the model | 108 MB |
| serving single images | 134 MB |

Cold start, from importing the package to the first score, is under 0.1 s.

Thread count is worth setting explicitly. On this chip throughput peaks at the
four performance cores and does not improve past them, because every thread
waits at the layer barrier for the slowest one and work scheduled onto an
efficiency core finishes late:

| intra-op threads | throughput at batch 64 |
| ---: | ---: |
| 2 | 74 img/s |
| 4 | 117 img/s |
| 6 | 110 img/s |
| 8 | 87 img/s |
| 10 | 106 img/s |

```python
Classifier(intra_op_threads=4)
```

Numbers above 4 threads vary run to run, which is itself the point: leaving the
count unpinned makes latency unpredictable. Machines with uniform cores do not
behave this way, so measure on your own hardware before tuning.

Decoding and preprocessing costs a further 1.3 ms per image on one core, around
5% of the total, and is not usually worth optimising.

## Preprocessing

The model is sensitive to how images reach it, and getting this wrong shifts
scores by several percent. `yahoo_open_nsfw_onnx.preprocessing` reproduces the
original Caffe pipeline exactly:

1. Resize to 256x256, bilinear.
2. Encode to JPEG and decode again.
3. Centre crop to 224x224.
4. Convert RGB to BGR.
5. Subtract the per-channel training means, `[104, 117, 123]`.

Step 2 looks pointless and is not. The original model was fitted on JPEG data,
and the compression artifacts are part of the distribution it learnt. Removing
the round trip changes the output.

`tests/test_reference_parity.py` asserts that this produces byte identical
arrays to the reference implementation in
[bhky/opennsfw2](https://github.com/bhky/opennsfw2), and that the resulting
scores agree to within 1e-4 with no image changing side of a threshold. It is
skipped unless opennsfw2 is installed, since that pulls in Keras and
TensorFlow. During development the same check was run over 3,594 photographs
and illustrations, where the largest disagreement was 3e-06.

## Reproducing the model file

The bundled `open_nsfw.onnx` is not a binary you have to take on trust:

```
pip install "yahoo-open-nsfw-onnx[export]"
python scripts/export_onnx.py
```

This loads the upstream Keras weights and re-exports them. The export runs
under the Torch backend of Keras, because the TensorFlow backend routes ONNX
export through tf2onnx, which pins `protobuf~=3.20` and will not install
alongside a current TensorFlow. The graph itself is backend independent.

`tests/test_model_parity.py` locks the outputs to known values so an accidental
re-export cannot silently change behaviour.

## Relation to other projects

The weights are Yahoo's, originally released for Caffe at
[yahoo/open_nsfw](https://github.com/yahoo/open_nsfw) and unchanged since 2016.
This project adds no training and claims no accuracy improvement.

[bhky/opennsfw2](https://github.com/bhky/opennsfw2) is the maintained Keras port
and is where the weights here come from. Use it if you want to fine tune, work
with video, or stay inside a Keras pipeline. This package exists for the case
where you only want to run the model and would rather not install a training
framework to do it.

[opennsfw-onnx](https://pypi.org/project/opennsfw-onnx/) covers much the same
ground and is the one to look at first. It was published in July 2026, is
Apache-2.0, and does several things this package does not: it selects CUDA,
TensorRT, DirectML or CoreML execution providers automatically, accepts
file-like objects and arrays as well as paths, and lets you skip the JPEG round
trip for a speed gain. It requires Python 3.11 or newer.

[SectorLabs/opennsfw-standalone](https://pypi.org/project/opennsfw-standalone/)
was the original ONNX packaging, in 2021. Its scores agree with this one to four
decimal places. It has had no release since July 2021, its repository now
returns 404, it pins `Pillow<9.0.0`, and it raises on NumPy 2 because of a
`copy=False` argument whose meaning changed.

What is left that is particular to this package: the bundled model is
re-exported from the current upstream Keras weights by a script in the
repository rather than inherited from the 2021 conversion, the preprocessing is
asserted byte identical to the reference implementation rather than matched
approximately, and it supports Python 3.9. If none of those matter to you,
use `opennsfw-onnx`.

## Licence

The code is MIT. The model weights in `src/yahoo_open_nsfw_onnx/open_nsfw.onnx` are
Yahoo's and carry a BSD 2-Clause licence. Both are reproduced in `NOTICE`.
