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.
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
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 |
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:
- Resize to 256x256, bilinear.
- Encode to JPEG and decode again.
- Centre crop to 224x224.
- Convert RGB to BGR.
- 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, 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 and unchanged since 2016. This project adds no training and claims no accuracy improvement.
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 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
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.
Metadata
Release files for yahoo-open-nsfw-onnx 1.0.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| yahoo_open_nsfw_onnx-1.0.0.tar.gz | 22.1 MB | With provenance | Details for yahoo_open_nsfw_onnx-1.0.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| yahoo_open_nsfw_onnx-1.0.0-py3-none-any.whl | Python 3 | none | any | Details for yahoo_open_nsfw_onnx-1.0.0-py3-none-any.whl |
Total release size: 44.2 MB
Release files / yahoo_open_nsfw_onnx-1.0.0.tar.gz
| Download URL | yahoo_open_nsfw_onnx-1.0.0.tar.gz |
|---|---|
| Size | 22.1 MB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
5f2c3001f32afbb73469773bfcf14fdef6110de6123e80d473ee374dec89891f
|
|
BLAKE2b-256 checksum How to use checksums |
660c2cb62f108410c55a93ff590f861a7f4c7dc9fe57904b05094c0906887077
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 19, 2026.
Transparency logRelease files / yahoo_open_nsfw_onnx-1.0.0-py3-none-any.whl
| Download URL | yahoo_open_nsfw_onnx-1.0.0-py3-none-any.whl |
|---|---|
| Size | 22.1 MB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
581cea9f295ef059861f4917e5403d436ded8b038ec17287fa6d68fa2e632081
|
|
BLAKE2b-256 checksum How to use checksums |
f39ce58d8f39db530e4f08170678dc482bbfcf99434fba2755aba9edf1127636
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 19, 2026.
Transparency logSecurity for yahoo-open-nsfw-onnx 1.0.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for yahoo_open_nsfw_onnx-1.0.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 19, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.