# Classifying Images

Source: https://docs.prodia.com/guides/classifying-images/

The image classification endpoint runs a Vision Transformer over an input image and returns a set of labels with confidence scores. The recommended model is [Freepik/nsfw\_image\_detector](https://huggingface.co/Freepik/nsfw_image_detector), an EVA-02 based classifier that produces a four-bucket severity breakdown — far more useful for tunable moderation than a single `normal` / `nsfw` flag.

The model returns probabilities across four labels that sum to `1.0`:

| Label     | Meaning                  |
| --------- | ------------------------ |
| `neutral` | Safe — no NSFW content   |
| `low`     | Mildly suggestive        |
| `medium`  | Suggestive or borderline |
| `high`    | Explicit                 |

Two endpoints are exposed:

- **`inference.vit.img2label.v1`** — returns a single JSON document. Labels are embedded under `config.labels` on the returned job.
- **`inference.vit.img2label.v2`** — returns a multipart response containing `labels.json` *and* the original input image. Useful when chaining classification into a wider workflow that also needs the image bytes downstream.

### Project Setup

```bash
# Create a project directory.
mkdir prodia-classifying-images
cd prodia-classifying-images
```

### prodia-js

Install Node (if not already installed):

### macOS

```bash
brew install node
# Close the current terminal and open a new one so that node is available.
```

### Linux

```bash
apt install node
# Close the current terminal and open a new one so that node is available.
```

### Windows

```bash
winget install -e --id OpenJS.NodeJS.LTS
# Close the current terminal and open a new one so that node is available.
```

Create project skeleton:

```bash
# Requires node --version >= 18
# Initialize the project with npm.
npm init -y

# Install the prodia-js library.
npm install prodia --save
```

### requests

Install Python (if not already installed):

### macOS

```bash
brew install python
# Close the current terminal and open a new one so that python is available.
```

### Linux

```bash
apt install python3 python3-venv python-is-python3
# Close the current terminal and open a new one so that python is available.
```

### Windows

```bash
winget install -e --id Python.Python.3.12
# Close the current terminal and open a new one so that python is available.
```

```bash
# Requires python --version >= 3.12
python -m venv venv
source venv/bin/activate
pip install requests
```

### curl

Install curl (if not already installed):

### macOS

```bash
brew install curl
# Close the current terminal and open a new one so that curl is available.
```

### Linux

```bash
apt install curl
# Close the current terminal and open a new one so that curl is available.
```

### Windows

```bash
# NOTE: Windows 10 and up have curl installed by default and this can be
# skipped.
winget install -e --id cURL.cURL
# Close the current terminal and open a new one so that curl is available.
```

```bash
# Export your token so it can be used by the main code.
export PRODIA_TOKEN=your-token-here
```

> note:
>
> Your token is exported to an environment variable. If you close or switch your
> shell you'll need to run `export PRODIA_TOKEN=your-token-here` again.

Create a main file for your project:

### prodia-js

```js title="main.js"
const { createProdia } = require("prodia/v2");

const prodia = createProdia({
    token: process.env.PRODIA_TOKEN // get it from environment
});
```

### requests

Create the following `main.py`

```python title="main.py"
from requests.adapters import HTTPAdapter, Retry
import os
import requests
import sys


prodia_token = os.getenv('PRODIA_TOKEN')
prodia_url = 'https://inference.prodia.com/v2/job'

session = requests.Session()
retries = Retry(allowed_methods=None, status_forcelist=Retry.RETRY_AFTER_STATUS_CODES)
session.mount('http://', HTTPAdapter(max_retries=retries))
session.mount('https://', HTTPAdapter(max_retries=retries))
session.headers.update({'Authorization': f"Bearer {prodia_token}"})
```

### curl

```bash title="main.sh"
set -euo pipefail
```

You're now ready to make some API calls!

### Classify an image

Pass the input image and request `Freepik/nsfw_image_detector`. The response contains a `labels` object with one score per severity bucket.

### prodia-js

```javascript title="main.js"
const { createProdia } = require("prodia/v2");

const prodia = createProdia({
  token: process.env.PRODIA_TOKEN,
});

(async () => {
  // get input image
  const inputBuffer = await (await fetch("https://docs.prodia.com/sunny-day.jpg")).arrayBuffer();

  const { job } = await prodia.job({
    type: "inference.vit.img2label.v1",
    config: {
      model: "Freepik/nsfw_image_detector",
    },
  }, {
    inputs: [ inputBuffer ],
  });

  console.log(job.config.labels);
  // => { neutral: 0.9995, high: 0.00038, low: 0.000087, medium: 0.000056 }
})();
```

```bash
node main.js
```

### requests

```python title="main.py"
from requests.adapters import HTTPAdapter, Retry
from io import BytesIO
import json
import os
import requests
import sys


prodia_token = os.getenv('PRODIA_TOKEN')
prodia_url = 'https://inference.prodia.com/v2/job'

session = requests.Session()
retries = Retry(allowed_methods=None, status_forcelist=Retry.RETRY_AFTER_STATUS_CODES)
session.mount('http://', HTTPAdapter(max_retries=retries))
session.mount('https://', HTTPAdapter(max_retries=retries))
session.headers.update({'Authorization': f"Bearer {prodia_token}"})

try:
    with open('sunny-day.jpg', 'rb') as f:
        input_image = f.read()
except FileNotFoundError:
    res = requests.get('https://docs.prodia.com/sunny-day.jpg')
    input_image = res.content
    with open('sunny-day.jpg', 'wb') as f:
        f.write(res.content)

headers = {
    'Accept': 'application/json',
}

job = {
    'type': 'inference.vit.img2label.v1',
    'config': {
        'model': 'Freepik/nsfw_image_detector',
    },
}

files = [
    ('job', ('job.json', BytesIO(json.dumps(job).encode('utf-8')), 'application/json')),
    ('input', ('sunny-day.jpg', input_image, 'image/jpeg')),
]

res = session.post(prodia_url, headers=headers, files=files)
print(f"Request ID: {res.headers['x-request-id']}")
print(f"Status: {res.status_code}")

if res.status_code != 200:
    print(res.text)
    sys.exit(1)

labels = res.json()['config']['labels']
print(labels)
# => {'neutral': 0.9995, 'high': 0.00038, 'low': 8.7e-05, 'medium': 5.6e-05}
```

```bash
python main.py
```

### curl

```bash title="main.sh"
set -euo pipefail

cat <<EOF > job.json
{
  "type": "inference.vit.img2label.v1",
  "config": {
    "model": "Freepik/nsfw_image_detector"
  }
}
EOF

if [[ ! -f sunny-day.jpg ]]; then
  curl -Lo sunny-day.jpg 'https://docs.prodia.com/sunny-day.jpg'
fi

curl -sSf --retry 3 \
  -H "Authorization: Bearer $PRODIA_TOKEN" \
  -H 'Accept: application/json' \
  -F "job=@job.json;type=application/json" \
  -F "input=@sunny-day.jpg;type=image/jpeg" \
  https://inference.prodia.com/v2/job
```

```bash
bash main.sh
```

The full response looks like this:

```json
{
  "type": "inference.vit.img2label.v1",
  "id": "65a10fac-b90f-40a8-8f5c-e41dbfbb4991",
  "state": { "current": "completed" },
  "config": {
    "model": "Freepik/nsfw_image_detector",
    "labels": {
      "neutral": 0.9994731545448303,
      "high": 0.0003829085035249591,
      "low": 0.00008746454113861546,
      "medium": 0.00005647135549224913
    }
  },
  "metrics": { "elapsed": 0.16 }
}
```

### Choosing a threshold

The four scores sum to `1.0` and represent the model's belief that the image *primarily belongs to* that severity bucket. To turn them into an allow/block decision, the [Freepik model card](https://huggingface.co/Freepik/nsfw_image_detector) recommends a *cumulative* scheme: pick the lowest severity you want to flag, sum that bucket and all higher ones, then compare against a threshold (typically `0.5`).

| Policy                               | Score formula                  | Use case                                                   |
| ------------------------------------ | ------------------------------ | ---------------------------------------------------------- |
| Strict — block only explicit         | `P(high)`                      | Permissive platforms; reject only the most extreme content |
| Moderate — block suggestive and up   | `P(medium) + P(high)`          | General audiences; common default                          |
| Lenient — block anything non-neutral | `P(low) + P(medium) + P(high)` | Family-safe / under-13 surfaces                            |

The closer the threshold is to `0`, the more aggressively borderline images are flagged (more false positives). The closer to `1`, the more permissive (more false negatives).

A minimal moderate-policy check looks like this:

```javascript
const { neutral, low, medium, high } = job.config.labels;
const flagged = (medium + high) > 0.5;
```

### Keep the image alongside the labels (v2)

If you want both the labels *and* a pass-through copy of the original image in a single response — for example, to pipe straight into a downstream job in a workflow — use the `v2` endpoint. It returns a multipart body containing `labels.json` followed by the original image bytes.

### prodia-js

```javascript title="main.js"
const { createProdia } = require("prodia/v2");
const fs = require("node:fs/promises");

const prodia = createProdia({
  token: process.env.PRODIA_TOKEN,
});

(async () => {
  const inputBuffer = await (await fetch("https://docs.prodia.com/sunny-day.jpg")).arrayBuffer();

  const result = await prodia.job({
    type: "inference.vit.img2label.v2",
    config: { model: "Freepik/nsfw_image_detector" },
  }, {
    accept: "multipart/form-data",
    inputs: [ inputBuffer ],
  });

  const form = await result.formData();
  const outputs = form.getAll("output");
  // outputs[0] is labels.json, outputs[1] is the original image

  const labels = JSON.parse(await outputs[0].text());
  console.log(labels);

  const imageBuffer = await outputs[1].arrayBuffer();
  await fs.writeFile("passthrough.jpg", new Uint8Array(imageBuffer));
})();
```

```bash
node main.js
```

### requests

```bash
pip install requests-toolbelt
```

```python title="main.py"
from requests.adapters import HTTPAdapter, Retry
from requests_toolbelt.multipart import decoder
from io import BytesIO
import json
import os
import requests
import sys


prodia_token = os.getenv('PRODIA_TOKEN')
prodia_url = 'https://inference.prodia.com/v2/job'

session = requests.Session()
retries = Retry(allowed_methods=None, status_forcelist=Retry.RETRY_AFTER_STATUS_CODES)
session.mount('http://', HTTPAdapter(max_retries=retries))
session.mount('https://', HTTPAdapter(max_retries=retries))
session.headers.update({'Authorization': f"Bearer {prodia_token}"})

with open('sunny-day.jpg', 'rb') as f:
    input_image = f.read()

job = {
    'type': 'inference.vit.img2label.v2',
    'config': {'model': 'Freepik/nsfw_image_detector'},
}

files = [
    ('job', ('job.json', BytesIO(json.dumps(job).encode('utf-8')), 'application/json')),
    ('input', ('sunny-day.jpg', input_image, 'image/jpeg')),
]

res = session.post(prodia_url, headers={'Accept': 'multipart/form-data'}, files=files)
if res.status_code != 200:
    print(res.text); sys.exit(1)

multipart = decoder.MultipartDecoder.from_response(res)
for part in multipart.parts:
    disposition = part.headers[b'Content-Disposition'].decode()
    if 'filename="labels.json"' in disposition:
        labels = json.loads(part.content)
        print(labels)
    elif 'filename="sunny-day.jpg"' in disposition:
        with open('passthrough.jpg', 'wb') as f:
            f.write(part.content)
```

```bash
python main.py
```

### curl

```bash title="main.sh"
set -euo pipefail

cat <<EOF > job.json
{
  "type": "inference.vit.img2label.v2",
  "config": {
    "model": "Freepik/nsfw_image_detector"
  }
}
EOF

curl -sSf --retry 3 \
  -H "Authorization: Bearer $PRODIA_TOKEN" \
  -H 'Accept: multipart/form-data' \
  -F "job=@job.json;type=application/json" \
  -F "input=@sunny-day.jpg;type=image/jpeg" \
  --output response.bin \
  https://inference.prodia.com/v2/job

# response.bin is a multipart body containing job.json, labels.json, and the image.
```

```bash
bash main.sh
```

### Input requirements

| Constraint         | Value           |
| ------------------ | --------------- |
| Accepted formats   | PNG, JPEG, WebP |
| Minimum dimensions | 128 x 128       |
| Maximum dimensions | 2048 x 2048     |
| Maximum file size  | 10 MB           |

### Guides

[Generating Images with Moderation](https://docs.prodia.com/workflows/generating-images-with-moderation/) — Chain text-to-image generation with a classification gate.

[Polling Async Jobs](https://docs.prodia.com/guides/polling-async-jobs/) — Use the async API for long-running pipelines.
