Library / SDK
bhky/opennsfw2 avatar
bhky/opennsfw2

OpenNSFW2: the Yahoo NSFW classifier rebuilt on Keras 3

Keras implementation of the Yahoo Open-NSFW model

541 stars73 forksPythonMIT

At a glance

What is it?
OpenNSFW2 is a Keras implementation of the discontinued Yahoo Open-NSFW model, giving you a pretrained pornographic image and video classifier with a small API, TensorFlow or JAX backends, and a Dockerised HTTP API. It suits teams that need the old model working on a modern stack, and it inherits the original model's limits and biases.
Who is it for?
Adopt OpenNSFW2 when you specifically need the Yahoo Open-NSFW model running on Keras 3, on TensorFlow or on JAX, because that is what this project is and does not attempt to be a better classifier. Do not adopt it as a general content moderation stack, since it covers pornographic images and video only, says nothing about the other categories of unsuitable content, and produces a probability rather than a decision.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 41 days ago.
What is it written in?
Mainly Python, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.

Editorial analysis

Why a discontinued Caffe model still needs a Keras port

The Yahoo Open-NSFW model was, as the README puts it, a favourite choice for detecting Not-Suitable-For-Work content in computer vision. It was developed with the Caffe framework, the work is now discontinued, and Caffe is also becoming less popular. A model that was trained in a framework nobody wants to install any more is a model that is effectively unusable, even if the weights are still good.

Open-NSFW 2 is the fix. It provides a Keras implementation of the Yahoo model, with references to its previous third-party TensorFlow 1 implementation. The scope of the problem is stated narrowly: while there are many types of NSFW content, the project focuses on pornographic images and videos.

That narrowness is the main thing to understand before you adopt it. This is not a general content moderation classifier, and it does not attempt to be. It answers one question about images and video frames, and the README points to the Yahoo project page for the context, definitions and model training details, because those are the semantics of the weights and this repository does not redefine them.

Keras 3 with TensorFlow and JAX backends is supported, as is tf-keras. A simple API is provided for making predictions on images and videos. The project is MIT licensed and the topics list gives the technical shape: image classification, image preprocessing, jax, keras, nsfw-classifier and tensorflow2.

Installing with the Keras extra that matches your stack

The package is tested with Python 3.9 to 3.12, and the install command depends on which Keras you want.

For Keras 3, on either the TensorFlow or the JAX backend:

bash
python3 -m pip install "opennsfw2[keras3]"

For tf-keras, the TensorFlow-integrated Keras:

bash
python3 -m pip install "opennsfw2[tf-keras]"

There is also a route from a source checkout of the repository:

bash
git clone [email protected]:bhky/opennsfw2.git
cd opennsfw2
python3 -m pip install ".[keras3]"  # or ".[tf-keras]"

The runtime dependencies in `requirements.txt` are the interesting part for planning an environment. They are `gdown>=4.2.0` for downloading the weights, `keras>=3.0.0`, `matplotlib>=3.0.0`, `numpy>=1.22.0`, `opencv-python>=4.0.0.0` for video decoding, `Pillow>=8.0.0` for image handling, `scikit-image>=0.21.0` and `tqdm>=4.62`. The comment `# Alternative: tf-keras>=2.14.0` sits in that file, so the Keras flavour is the one thing you switch by editing dependencies.

If both Keras versions end up installed, Keras 3 is used by default, and to force tf-keras you set an environment variable before importing:

bash
OPENNSFW2_KERAS=tf-keras python3 your_script.py

The ordering matters because the import happens at module load, so the variable has to be set in the environment of the process rather than inside the script.

The prediction API, and why batching is the faster path

The high level API is small, and the image functions come in single and list forms.

python
import opennsfw2 as n2

# To get the NSFW probability of a single image, provide your image file path,
# or a `PIL.Image.Image` object.
image_handle = "path/to/your/image.jpg"

nsfw_probability = n2.predict_image(image_handle)

The list version takes a list of file paths, or a list of `PIL.Image.Image` objects, and the README says plainly that using this function is better than looping with `predict_image`, because the model will only be instantiated once and batching is done during inference. That is a real consideration for a classifier you intend to run over a large set of images, since constructing the model per image is the expensive part.

Video works through OpenCV, and any format OpenCV supports will do. The function returns two lists, the elapsed time in seconds and the NSFW probability of each frame:

python
import opennsfw2 as n2

# The video can be in any format supported by OpenCV.
video_path = "path/to/your/video.mp4"

# Return two lists giving the elapsed time in seconds and the NSFW probability of each frame.
elapsed_seconds, nsfw_probabilities = n2.predict_video_frames(video_path)

Note that video prediction is per frame, and the return value gives you the timings so you can attribute a probability to a moment in the file. There is no temporal smoothing in this API, so a decision about a video segment is a decision you have to make yourself.

The lower level Keras route exists for when you want to control preprocessing, or fine-tune, and it is where the weights download behaviour becomes visible.

Weights are downloaded on first use, and the tensor path skips the JPEG round trip

The lower level API is where the mechanics are exposed, and there are two details worth understanding before you build a pipeline on it.

The first is the weights. `make_open_nsfw_model()` searches for the pre-trained weights file at `$HOME/.opennsfw2/weights/open_nsfw_weights.h5` and downloads it from the repository if it is not there. The API documentation also names a default of `$HOME/.opennsfw/weights/open_nsfw_weights.h5` for the `weights_path` parameter, and says the environment variable `OPENNSFW2_HOME` can be used to indicate where the weights live. Passing `weights_path=None` means no weights are downloaded or loaded, which is the setting you want for training from scratch.

The second is preprocessing, and it is where the tensor and array paths differ. `preprocess_image` takes a Pillow image and a `Preprocessing` enum, defaulting to `Preprocessing.YAHOO`, and returns a NumPy array of shape (224, 224, 3). `preprocess_image_tensor` is the tensor-based equivalent, suitable for use in a dataset pipeline such as `tf.data.Dataset.map`, and the documentation notes that the JPEG round-trip in the `YAHOO` pipeline is intentionally omitted. That is a meaningful difference: the array path re-encodes the image, and if you are building a training pipeline on tensors you get a slightly different input distribution than the pretrained weights were calibrated on.

The `Preprocessing` enum has two members, `Preprocessing.YAHOO` and `Preprocessing.SIMPLE`, so you can move away from the original pipeline if you need to.

Training and fine-tuning with a tf.data pipeline

Because the model is a Keras model and the weights are provided, fine-tuning is a supported path, and the README gives a complete pipeline.

The shape of it is a tf.data dataset built from image paths and integer labels, where 0 means SFW and 1 means NSFW, mapped through a per-sample load and preprocess function, then batched and prefetched:

python
def load_and_preprocess(image_path, label):
  image = tf.io.read_file(image_path)
  image = tf.io.decode_jpeg(image, channels=3)
  # The preprocessed image is a tensor of shape (224, 224, 3).
  image = n2.preprocess_image_tensor(image, n2.Preprocessing.YAHOO)
  return image, label

dataset = (
  dataset
  .map(load_and_preprocess, num_parallel_calls=tf.data.AUTOTUNE)
  .batch(32)
  .prefetch(tf.data.AUTOTUNE)
)

The model is then compiled with the adam optimizer, sparse categorical crossentropy loss and accuracy as the metric, and fitted for 10 epochs. The comments in the README are precise about one choice: for fine-tuning you load the pre-trained weights, which is the default behaviour, and for training from scratch you pass `weights_path=None`.

One important constraint is stated in a section title rather than in the code, and it is that training and fine-tuning are described with the TensorFlow backend. The same section also notes the deliberate omission of the JPEG round-trip in the tensor preprocessing path, which as noted above means the inputs in this pipeline are not byte-identical to what the YAHOO pipeline produces for the same image.

The repository layout supports this being a first-class use rather than an afterthought. There is an `opennsfw2/` package, a `tests/` directory, an `app/` directory for the HTTP API, a `plot/` directory, and separate `requirements.txt`, `requirements-api.txt` and `requirements-dev.txt` files.

The container image pre-downloads the weights, and the API is a FastAPI service

The repository also ships a Dockerised HTTP API, and the compose file in the tree shows the shape of it.

The service is named `opennsfw2-api`, it builds from the repository context using the `Dockerfile`, and it publishes port 8000 on the host mapped to port 8000 in the container. A named volume `model_weights` is mounted at `/home/appuser/.opennsfw2`, which is where the weights file lives, so the weights survive a container replacement instead of being downloaded on every start. The healthcheck calls a `/health/` endpoint with Python's urllib, on a 30 second interval with a 15 second timeout, three retries and a 120 second start period.

The `Dockerfile` is built on `python:3.12-slim-bookworm` and does three things worth noting. It installs `libgl1` and `libglib2.0-0` to avoid the OpenCV import errors for `libGL.so.1` and `libgthread-2.0.so.0`. It creates a non-root `appuser` and chowns the application directory to it. And it pre-downloads the model weights at build time, running `python -c "import opennsfw2; opennsfw2.make_open_nsfw_model()"`, with the failure tolerated so the image still builds without network access, since `download_weights_to` removes a bad file and the runtime retry can then succeed. The `start_period` in the healthcheck covers a first-run weights download during startup.

The container entry point is `uvicorn` running `app.main:app` on `0.0.0.0:8000`, and the repository also carries a `README-HTTP.md` describing the HTTP interface, plus a `CITATION.cff` file for citation metadata.

Where OpenNSFW2 is the wrong tool

The scope limitation is the first and it is not negotiable. The model detects pornographic images and video frames, and the README is explicit that other kinds of unsuitable content are out of scope. A moderation pipeline that needs violence detection, hate symbols, or text-based abuse needs other models, and assuming this one covers them is a category error.

The second limitation is the PyTorch backend. The README says the model can run on PyTorch, but that inference output differs noticeably from TensorFlow and JAX, and that inference is slower because PyTorch uses `channels_first` while this model uses `channels_last`. The conclusion drawn is blunt: PyTorch is not recommended. So if your organisation has standardised on PyTorch, this project will run but the numbers will not match the reference implementation, which makes threshold calibration awkward.

The third is the model itself. These are the Yahoo weights, retrained by nobody, so whatever the original model got wrong about representation, and it was trained in the last decade on a particular distribution of images, is inherited verbatim. The project is careful to point you at the Yahoo page for definitions and training details rather than claiming an improvement, and that is the right posture, but it means the model is a fixed baseline, not a state of the art classifier.

The fourth is that you get a probability, not a policy. The API returns a number, and turning it into a block, a warning or a review queue is your decision, on your threshold, for your user base. There is no notion of a category beyond the binary output, and no handling of the edge cases a real moderation service must deal with, such as artistic nudity, which is a judgement about context rather than pixels.

The alternative worth naming is a current-purpose image moderation model, and the difference in approach is exactly what the two paragraphs above describe: an actively trained model with its own categories, or an actively maintained one, will have been trained on data that reflects current norms, will have been evaluated for bias, and will come with documentation of its known failure cases. This project offers none of that, and offers instead a portable, dependency-light way to run one specific historical model.

Editorial conclusion

Adopt OpenNSFW2 when you specifically need the Yahoo Open-NSFW model running on Keras 3, on TensorFlow or on JAX, because that is what this project is and does not attempt to be a better classifier. Do not adopt it as a general content moderation stack, since it covers pornographic images and video only, says nothing about the other categories of unsuitable content, and produces a probability rather than a decision. Before you deploy, calibrate the threshold on your own data rather than trusting the default, verify that the weights file in your environment matches the one you tested, and remember that the PyTorch backend is explicitly not recommended because the output differs noticeably and inference is slower.

Frequently asked questions

What is OpenNSFW2 and what model does it implement?

It is a Keras implementation of the Yahoo Open-NSFW model, which was originally developed with the Caffe framework and whose original work is now discontinued. The project focuses on pornographic images and videos, and points to the Yahoo project page for the context, definitions and model training details.

How do I install OpenNSFW2 with Keras 3 or with tf-keras?

Use the extra matching your stack, `python3 -m pip install "opennsfw2[keras3]"` for Keras 3 on the TensorFlow or JAX backend, or `python3 -m pip install "opennsfw2[tf-keras]"` for tf-keras. If both Keras versions are installed, Keras 3 is used by default, and setting `OPENNSFW2_KERAS=tf-keras` before importing forces tf-keras.

How are the pretrained model weights obtained?

The model call searches for the weights file under the user's home directory and downloads it from the repository if it is not there. The environment variable `OPENNSFW2_HOME` can be used to indicate where the weights live, and passing `weights_path=None` means no weights are downloaded or loaded, which is what you want for training from scratch.

Can OpenNSFW2 predict on video?

Yes. The `predict_video_frames` function takes a video path in any format supported by OpenCV and returns two lists, the elapsed time in seconds and the NSFW probability of each frame.

Is the PyTorch backend supported by OpenNSFW2?

The model can run on PyTorch, but the README states that inference output differs noticeably from TensorFlow and JAX, and that inference is slower because PyTorch uses `channels_first` while the model uses `channels_last`. The project does not recommend PyTorch.

Official sources

  1. bhky/opennsfw2 on GitHub
  2. Issues
  3. License: MIT
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/bhky-opennsfw2.svg)](https://hysenlabs.com/projects/bhky-opennsfw2)

Was this useful?